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.
Feature and roadmap decisions
Nullable, Precision/Scale and MaxLength from the actual column schema, so Fiori/UI5 controls (notably sap.ui.model.odata.type.Decimal) format and validate against real constraints instead of a bare Edm.Decimal. This is the difference between a demo and a production screen for any customer binding currency, quantity or free-text fields two-way.ColumnFacetResolverInterface is a supported extension seam: one binding, applied per column, composed with #[ODataProperty(precision:, scale:, nullable:)] on individual models. Price decimals that differ per installation become configuration, not a code branch. This unlocks multi-tenant/regulatory variants that were previously "no-bid".@odata.nextLink now preserves $filter, $orderby, $select, custom options and $count, so Excel "Load more", Power BI and UI5 growing lists return the second page of the same query rather than an unfiltered slice of the whole set. Previously documented workarounds/warnings can be removed from onboarding docs.$expand=lines($count=true) now emits [email protected] from a single withCount sub-select at any nesting depth, surviving $select. This removes a strong argument for custom aggregation endpoints.$filter constructs answer 501 unsupported_filter with the construct named; malformed ones answer 400 invalid_filter; errors before the first row keep their status code instead of arriving after a 200. That is a supportability win: the ticket says what failed, not "the grid is empty".Decisions this release forces you to make
odata:cache) now serves byte-identical $metadata warm and cold — but it bakes in the resolver's answer at build time. When an installation's facet facts change, the cache must be rebuilt. Decide who owns that trigger (deploy hook vs. admin action) and document it before customers hit a stale-schema support call.?$skip=n nextLink or on polymorphic navigations appearing in metadata will see behavior change. Plan a release note, a client-compat check and a metadata diff between environments.Adopt when most of these hold
$metadata, $filter, $expand, $select, $orderby, $count and @odata.nextLink to behave per spec.morphTo, morphToMany, morphOneOrMany, even with fixed targets) are omitted from discovery by design, and key literals are strictly validated per type, answering 400 invalid_key on a mismatch.Look elsewhere, or plan extra work, when
$batch change sets as the primary use case; read-path maturity is not evidence of write-path maturity.$count — they return 501 unsupported_expand by design, because the resolver decides the row set.odata:cache; the resolver runs at schema build time, so stale caches are a real operational hazard.For executives (one paragraph). This release turns the OData layer from "works for the demo" into "survives the customer's spreadsheet." Screens and reports now receive the real shape of the data — nullability, decimal precision and scale, string length — so UI5 controls format currency correctly and enforce the same rules the database does. Excel and Power BI "load more" now fetch the next page of the query the user built, not a fresh unfiltered slice, which removes a class of "the totals are wrong" incidents. Requests the layer cannot honour now fail loudly with a specific status and reason instead of quietly returning all rows or no rows. And a supported extension point lets a single installation's pricing and precision conventions be configured rather than coded. Net effect: fewer correctness defects in reporting and forms, less bespoke integration code, and faster sign-off on Fiori/Power BI rollouts — with two things we must manage: a metadata cache rebuild when those configured facts change, and a client-compatibility pass at upgrade.
For engineering leads (bullets).
discoverModel() reads Nullable, Precision/Scale, MaxLength from Schema::getColumns(); facets are only applied where the final Edm type can carry them, so a #[ODataProperty(type:)] override never inherits the source column's length or scale. Key properties are forced non-nullable; SQLite's nullable-integer-PK report is ignored. Facet-free columns serialize exactly as before.EdmxWriter previously rebuilt properties as new Property(name, type), dropping facets, the collection flag and defaults — warm and cold $metadata diverged. Fixed, with a test asserting warm/cold identity. Note the known prior instance of this bug class (enum types, 3.0.3).ColumnFacetResolverInterface → attribute; key stays non-nullable. One binding via bindIf, so an app-level resolver wins regardless of provider order — and there is exactly one resolver, not a chain. TypeFacets gains withNullable()/withMaxLength()/withPrecision()/withScale()/isDefault() for surgical overrides.LogicException at schema build time when facets don't fit the type (scale on non-decimal, precision outside decimal/temporal, maxLength outside string/binary, scale > precision) — this is a startup failure, so it surfaces in CI rather than in a customer's browser.$count=true inside $expand emits <nav>@odata.count computed by one withCount sub-select per counted navigation at any depth, respects the expand's $filter but not its $top/$skip, and survives $select. Single-valued → 400 invalid_expand; virtual → 501 unsupported_expand.AbstractFilterTranslator so they cannot drift. Unsupported functions/operators/lambdas → 501 unsupported_filter naming the construct; property-less comparisons, null with anything but eq/ne, or a non-Boolean standalone → 400 invalid_filter. tolower/toupper map to LOWER()/UPPER() (UI5 case-insensitive filters work), contains/startswith/endswith escape wildcards as $search does, 3 lt id is mirrored, bare Boolean properties filter, $filter=false returns nothing, and in no longer collapses to [].in handled per ABNF (RWS "in" RWS), plus optional whitespace after (, before ), around ,, and after a lambda's :, so id in (1,3), nav/any(p: p/name eq 'x'), ( id eq 1 ) and contains( name , 'x' ) all parse.EntitySetHandler ran the query lazily inside the stream callback, so rejected filters and SQL errors landed after 200. It now executes up to the first row before committing the response; rows still stream.$skip replaced), preserving custom options and $count; $batch parts use their own URL. Do not round-trip through Symfony's normalised query string.MorphTo extends BelongsTo and MorphToMany extends BelongsToMany meant they were silently wired as ordinary navigations with an arbitrary empty-model target, and a type-column join cannot be expressed as a ReferentialConstraint.Products(abc) → Products(0)). Integer ranges, decimals, doubles, true/false, Guid, Date, DateTimeOffset, TimeOfDay and Duration are validated per family; misfits answer 400 invalid_key. Decimal, temporal and Guid keys remain strings to stay exact.$metadata pre/post upgrade; identify clients that depended on the old all-or-nothing filter behaviour or bare nextLinks; confirm no client consumed polymorphic navigations; rebuild odata:cache after upgrading and whenever a bound resolver's inputs change.Risks to track (PM-owned). Cache staleness after facet configuration changes (owner and trigger undefined today); behavioral tightening in $filter status codes that will surface as new client errors; removal of polymorphic navigations from metadata being a visible schema break for existing consumers; and the recurring pattern of EdmxWriter dropping metadata (now fixed twice — worth a standing warm-vs-cold parity test across every future metadata feature).
How can I help you explore Laravel packages today?