laravelui5/odata
Laravel UI5 OData adds OData service support to Laravel, letting you expose Eloquent models with query options such as $filter, $select, $expand, $orderby, and $count. It integrates with Laravel routing and is designed for SAPUI5/OpenUI5 frontends.
Architecture fit. This package sits directly on top of Laravel's Eloquent layer and derives its OData $metadata from Schema::getColumns(), so it fits any Laravel application whose domain is already modelled as Eloquent relations. It is not a generic OData framework bolted onto PHP — it is a discovery-and-translation layer whose quality is bounded by how faithfully Eloquent models describe the schema. That is a good fit for teams already standardised on Laravel, and a poor fit where the data model lives outside Eloquent or where the schema is intentionally hand-authored.
What v3.1.0 changes in kind, not just degree. Three of the changes are correctness fixes that alter observable behaviour:
Nullable, Precision/Scale and MaxLength are emitted from the column schema, and #[ODataProperty(nullable:)] finally takes effect. Clients (notably UI5 V4 types) treat these as enforced constraints, so the contract between server and UI tightens.MorphTo, MorphToMany and MorphOneOrMany are excluded outright, on the grounds that no ReferentialConstraint can express a type-column join. This removes previously advertised navigation properties.501 unsupported_filter, malformed comparisons 400 invalid_filter, and bad key literals 400 invalid_key. Previously these returned 200 with silently wrong result sets.Integration feasibility. High for greenfield or well-modelled Laravel services. The new ColumnFacetResolverInterface seam (bound with bindIf, one binding, default ColumnFacetsAsDeclared) is a clean extension point for installation-wide facet policy — e.g. announcing an installation-wide price scale over a decimal(19,6) column. TypeFacets gaining withNullable()/withMaxLength()/withPrecision()/withScale()/isDefault() makes resolvers cheap to write without copying facets by hand. Validation is fail-fast: a LogicException at schema build time when facets don't fit the Edm type, so misconfiguration surfaces in CI, not in production traffic.
Technical risk.
$filter=false returning everything, on unsupported functions returning everything or nothing, or on Products(abc) coercing to Products(0), will now see 400/501. This is the correct direction, but it is a consumer-visible contract change shipping under a 3.1.0 label.odata:cache previously dropped facets, collection flags and default values, so a warm service announced a different schema than a cold one. That is fixed, but the corollary is that every existing cache is stale relative to v3.1.0 and must be rebuilt — and after rebuild, the new facets will appear to clients.EntitySetHandler now runs the query to the first row before committing the response, so refusals and SQL errors carry their real status. Rows still stream. This is a genuine improvement, but it shifts the first-byte latency and changes how client-side error handling must be written.Key questions for engineering. Do we have consumers of polymorphic navigation properties, and what are the explicit alternatives from services/model-discovery? What is the package's stated BC policy for these refusal semantics, and do we pin to 3.1.x? Which facet overrides are installation facts versus model facts, and who owns the resolver binding? Does any client rely on the old @odata.nextLink shape (<Set>?$skip=n)?
Stack fit. Laravel + Eloquent + this package + a UI5 V4 (or Excel/Power BI) consumer is the intended shape. The release notes call out UI5 explicitly — sap.ui.model.odata.type.Decimal now formats with the right scale, and V4 types read Nullable and MaxLength as two-way binding constraints. Teams on UI5 get the most value; teams consuming via generated OData clients get schema fidelity but must regenerate proxies.
Migration path.
3.1.0 in a branch, run the suite, and diff cold $metadata against the currently served document. Treat the diff as the migration spec.odata:cache artefact and confirm warm output is byte-identical to cold — the package ships a test asserting this, but our own resolvers need the same guarantee.ColumnFacetResolverInterface before the first production cache build. Because it is a single bindIf binding, provider order does not matter, but ownership does: agree one owner or the binding silently loses.@odata.nextLink.Compatibility. $expand=lines($count=true) is additive and safe — one withCount sub-select per counted navigation, computed under the expand's $filter and not limited by its $top/$skip, and it survives $select. $batch parts correctly use their own URL for nextLink. The parser now accepts in and spaced lambdas (id in (1,3), nav/any(p: p/name eq 'x'), ( id eq 1 ), contains( name , 'x' )), which widens the accepted grammar without breaking valid prior queries. tolower/toupper translating to LOWER()/UPPER() fixes UI5's case-insensitive filters. None of these require client changes; the refusals do.
Sequencing. Metadata rebuild and cache rebuild are one atomic step and must precede any client release. Facet overrides must be settled before the cache is built, because the resolver runs at build time and a cached service keeps the value returned then.
Maintenance. The consolidation of both translators under AbstractFilterTranslator is the single most valuable operational change here: filter and search semantics can no longer drift apart, and the refusal taxonomy (501 unsupported_filter, 400 invalid_filter, 400 invalid_expand, 501 unsupported_expand, 400 invalid_key, 400 parse_error) gives on-call a precise vocabulary. Expect a modest maintenance load for the resolver seam — it is a new extension point with one binding and no chain, so policy composition must be done inside a resolver, not across several.
Support. Two new ticket classes will appear immediately after rollout: (a) clients receiving 400/501 where they previously received 200 with wrong data — these are wins, but they will be reported as regressions; (b) clients missing navigation properties for polymorphic relations. Pre-empt both with a changelog entry and a client-facing note before deploy.
Scaling. $expand with $count adds one withCount sub-select per counted navigation at any nesting depth — depth-multiplied cost is possible and should be bounded by policy. Facet resolution runs at schema-build time only, so it is a cold-start cost, not a per-request cost, provided the cache is warm. Streaming is preserved, which keeps memory flat for large sets.
Failure modes.
200 body and ignored the status will misbehave.Ramp-up. Engineers need working knowledge of Eloquent schema introspection, the OData ABNF subset actually parsed (now including in and spaced lambdas, whitespace around parens/commas, and mirrored comparisons like 3 lt id), and the facet rules per Edm type — Scale and Precision only on Edm.Decimal, Precision additionally on temporal types, MaxLength only on Edm.String/Edm.Binary, and never a Scale above its Precision. Front-end engineers need the UI5 V4 type story, because Nullable="false" and MaxLength are now enforced in two-way bindings. Budget roughly a day for a backend engineer and half a day for a UI5 engineer, plus a joint session on the metadata diff.
How can I help you explore Laravel packages today?