Ui5Resource
Introduction
A Ui5Resource is the simplest routable artifact in Core: a GET endpoint that runs one provider and returns whatever array it produced, as JSON.
GET /ui5/resource/{namespace}@{version} → { …any JSON… }No schema, no metadata, no query grammar. That freedom is the whole feature — and it is also the reason you should almost always reach for something else.
Read this first
Core has four read mechanisms, and Resource is the last resort. Work down the list; stop at the first match.
| What you need | Use | Why |
|---|---|---|
| A collection of like-typed rows — a list, a table, a master view | an OData entity set | bindList, $filter, $orderby, paging, $batch — all free |
| One object with related data — a detail view, an object-page header | bindElement + $expand | one round trip, cached by the V4 model |
| A computed scalar or bespoke complex value — a total, a projection, a read-out | an OData function (bindFunctions() on your app) | typed, batchable, discoverable in $metadata |
| Static, user-invariant client facts — route handles, capability flags | an infrastructure contribution | injected into every manifest, cacheable |
| None of the above | Resource |
The fourth column is what you give up by choosing Resource. A Resource returns a raw array, so batching, caching, relative bindings, filtering, paging and — on an SDK host — the #[Read] authorization layer are all things you then rebuild by hand, per resource.
The honest state of this artifact
Ui5Resource ships in 1.0 but is not frozen. It is the one Core surface that never found a second consumer: every getResources() in our own applications returns [], and the only implementations are test fixtures.
That is not a defect notice — the contract works and is covered. It is a statement about fit: each time we reached for it, the OData path turned out to be the better answer. We keep it because the two cases below are real, and we document it plainly rather than advertise it.
Why the object-page header is no longer a reason
Historically this artifact existed for one job: assembling a complex page header — a denormalized object with computed parts — because expressing that through OData v2 was painful.
Under OData v4 it is not. A header is bindElement with $expand; a computed read-out is a navigation projection or a function. Our own applications build both that way and never register a Resource. If you arrived here looking for the header recipe, it lives in the OData integration page.
When Resource is still the right answer
Two cases survive:
- Foreign payloads. You are proxying a third-party system whose schema you do not own and do not want to model. An entity type would be a fiction.
- A one-off blob for exactly one view. The shape is genuinely ad hoc, no second consumer will ever see it, and schema discipline would be pure overhead.
If you are outside those two, the table above has your answer.
Conceptual Overview
Two seats, two jobs
- The resource class (
Ui5ResourceInterface) is pure metadata: identity, and the class-string of its provider. It has no behaviour. - The provider does the work. It carries
DataProviderInterfaceand aprovide(): arraymethod.
The provider contract is a convention, not a signature
DataProviderInterface is an empty marker. provide() is not declared on it, because its parameters are resolved by the container at invoke time — which a PHP interface cannot express. (Laravel's own ShouldQueue is a marker for the same reason and never types handle().)
So the contract is documented and runtime-enforced:
provide(): array— JSON-serializable, side-effect free, normalized data (scalars and nested arrays, not raw models).ExecutableInvokercalls it and throwsMissingExecutableMethodExceptionif it is absent.
The same marker backs Cards and Reports, so the convention is worth knowing once.
Injection recipe
- Services, repositories, gateways → the constructor. The framework resolves the declared class-string through the container, so they are autowired.
- Per-request inputs →
provide()'s parameters. Route-resolved models,#[Parameter]values, theUi5ContextInterface. They exist only at invocation.
How to Generate
php artisan ui5:resource Offers/Header| Option | Default | Description |
|---|---|---|
name (argument) | (required) | Format: {AppName}/{ResourceName} |
Namespaces derive from the target module.
Output
ui5/
└── Offers/
└── src/
└── Resources/
├── HeaderResource.php
└── Provider/
└── HeaderProvider.phpArtifact Roles
HeaderResource.php
Identity as class constants; the only behaviour is the handle to the provider:
class HeaderResource extends AbstractUi5Resource
{
public const string NAMESPACE = 'com.acme.offers.resources.header';
public const string VERSION = '1.0.0';
public const string TITLE = 'Offer Header';
public const string DESCRIPTION = 'Aggregated header payload for the offer page';
public function getProvider(): string
{
return HeaderProvider::class;
}
}getProvider() declares the provider by class-string; the framework resolves it. Never write return app(...) or return new ... here — a declaration method declares, it does not build.
HeaderProvider.php
class HeaderProvider implements DataProviderInterface
{
public function __construct(private readonly OfferRepository $offers) {}
public function provide(Ui5ContextInterface $context): array
{
return [
'status' => 'open',
'total' => $this->offers->openTotal(),
'updated' => now()->toIso8601String(),
];
}
}Return normalized data. An Eloquent model serializes, but it leaks column names, casts and appended attributes into your wire contract — and a Resource has no schema to absorb that change later.
Registration
class OffersModule extends AbstractUi5Module
{
public function getResources(): array
{
return [new Resources\HeaderResource($this)];
}
}The endpoint is then:
GET /ui5/resource/com/acme/offers/resources/[email protected]Dots in the namespace become slashes in the URL — that is the convention every artifact URL follows, and the form Ui5Registry::resolve() emits.
A Resource is accessible — on an SDK host it carries #[Access] like any other addressable artifact. It is read-only by contract: GET only, and a provider that mutates state is a bug, not a feature.
Consuming it in UI5
There is no binding. You fetch and place the result in a JSONModel:
const model = new JSONModel();
model.loadData("/ui5/resource/com/acme/offers/resources/[email protected]");
this.getView().setModel(model, "header");Compare that with the OData path, where the same data arrives through bindElement and the model handles caching, batching and refresh for you.
Note what the JSONModel is doing here, though: it is holding view state. That is a perfectly good pattern — and it composes with OData just as well. Our own applications load OData results into a JSONModel when they want client-side filtering. A JSONModel is not a reason to choose a Resource.
Best Practices
- Try the table at the top first, every time. This artifact has no schema, so every shortcut you take here becomes a contract you maintain by hand.
- Normalize the payload. Arrays of scalars, not models.
- Keep it side-effect free.
GETmeansGET. - Version the namespace, not the shape. Because there is no
$metadata, a consumer cannot discover that your payload changed — so a breaking shape change needs a new version, or a very short list of known consumers. - Never take identity from a query parameter. Core carries no actor; on an SDK host the actor comes from the SDK context.
Related
- OData integration — the mechanism that covers most of what this page is about
- Ui5Card, Ui5Report — the other users of
DataProviderInterface - Infrastructure contributions — for static, user-invariant client facts
- Ui5Module — where artifacts are registered