laravelui5 / odata
Multi-OData endpoint support for Laravel
Requires
- php: ^8.4
- ext-dom: *
- ext-json: *
- ext-libxml: *
- ext-simplexml: *
- illuminate/database: ^11.0 || ^12.0
- illuminate/http: ^11.0 || ^12.0
- illuminate/routing: ^11.0 || ^12.0
Requires (Dev)
- ext-pdo_mysql: *
- ext-pdo_pgsql: *
- ext-pdo_sqlite: *
- brianium/paratest: ^7.0
- eclipxe/xmlschemavalidator: ^3.0
- nunomaduro/collision: ^8.0
- orchestra/testbench: ^10.0
- pestphp/pest: ^3.0
- pestphp/pest-plugin-arch: ^3.0
- pestphp/pest-plugin-laravel: ^3.0
- phpstan/phpstan: ^1.0.0
- phpunit/phpunit: ^11.0
- sebastian/diff: ^6.0
- staudenmeir/eloquent-json-relations: ^1.0
This package is not auto-updated.
Last update: 2026-04-06 15:38:56 UTC
README
A read-only OData v4 engine for Laravel 11+, built for OpenUI5 frontends.
This package is a clean-room rewrite of flat3/lodata. Its protocol test suite served as the pivot: ~400 HTTP tests define the OData wire contract the new implementation must honor. No original implementation code was preserved; only relevant, refactored tests remain, forming the permanent regression suite.
What it does
- read-only OData v4 engine
- Supports multiple service endpoints
- Supports schema caching (no discovery at request time;
php artisan odata:cachepre-compiles the EDM to PHP classes) - Serves
$metadata, service documents, entity collections, single entities, navigation, functions, singletons - Full query support:
$filter,$select,$orderby,$expand(nested),$top,$skip,$count,$search,$compute - Supports
$batchwith partial failure - Supports server-driven paging via
Prefer: odata.maxpagesize - Serves streamed responses (large result sets never buffer in memory)
Requirements
- PHP 8.4+
- Laravel 11+
Installation
composer require laravelui5/odata
The service provider registers automatically. Publish the config:
php artisan vendor:publish --tag=config --provider="LaravelUi5\OData\ODataServiceProvider"
Quick start
1. Define a service
namespace App\OData; use LaravelUi5\OData\ODataService; use LaravelUi5\OData\Driver\Sql\EloquentEntitySetResolver; use LaravelUi5\OData\Edm\Container\EntitySet; use LaravelUi5\OData\Edm\Contracts\Container\PrimitiveTypeEnum; use LaravelUi5\OData\Edm\Property\Property; use LaravelUi5\OData\Edm\Type\EntityType; use LaravelUi5\OData\Edm\Type\PrimitiveType; use LaravelUi5\OData\Service\Contracts\EdmBuilderInterface; use LaravelUi5\OData\Service\Contracts\RuntimeSchemaBuilderInterface; class PartnerService extends ODataService { public function serviceUri(): string { return 'partners'; } public function namespace(): string { return 'io.pragmatiqu.partners'; } protected function configure(EdmBuilderInterface $builder): EdmBuilderInterface { $this->discoverModel(Partner::class); return $builder->namespace($this->namespace()); } }
2. Register the service
Create a registry that returns your service:
namespace App\OData; use LaravelUi5\OData\Service\Contracts\ODataServiceInterface; use LaravelUi5\OData\Service\Contracts\ODataServiceRegistryInterface; class AppServiceRegistry implements ODataServiceRegistryInterface { public function services(): array { return [new PartnerService()]; } public function resolve(string $fullPath): ODataServiceInterface { return new PartnerService(); } }
Point the config at it in config/odata.php:
'service_registry' => App\OData\AppServiceRegistry::class,
3. Use it
GET /odata/partners/Partners → entity collection
GET /odata/partners/Partners(1) → single entity
GET /odata/partners/Partners?$filter=name eq 'Acme'
GET /odata/partners/Partners?$select=id,name&$top=10&$orderby=name
GET /odata/partners/$metadata → CSDL XML
Configuration
Published to config/odata.php:
| Key | Default | Description |
|---|---|---|
prefix |
odata |
URL route prefix |
middleware |
[] |
Middleware for OData routes |
streaming |
true |
Stream JSON responses |
namespace |
com.example.odata |
Default XML namespace |
version |
4.0 |
OData protocol version |
service_registry |
ODataServiceRegistry::class |
Registry implementation |
pagination.max |
null |
Server max page size cap |
pagination.default |
200 |
Default page size when client sends no preference |
Schema caching
For production, pre-compile the EDM object graph to PHP classes:
php artisan odata:cache # generates Edm/ directory next to each service class php artisan odata:clear # removes generated Edm/ directories
Cached classes are plain readonly PHP implementing Edm\Contracts\ interfaces.
On warm boot, ODataService::schema() loads them directly — skipping the builder entirely.
Architecture
Three concentric layers. Each layer only depends on the layers inside it.
Http\ → routes requests to the engine
Protocol\ → parses OData URLs, plans queries, executes via handlers
Service\ → contracts, builders, caching, serialization
Edm\ → pure read-only metamodel (zero framework dependencies)
Driver\ → implements resolver contracts (Eloquent/SQL)
Services declare their schema in configure() (what the service looks like) and
bind resolvers in bindResolvers() (how to fetch the data). The engine never
touches the schema after planning — it works entirely from the resolved query plan.
Roadmap
Post-GA improvements for the extensibility layer:
- Serialize EDXML on
odata:cacheand serve XML directly - In-memory filter/sort/paginate helpers for custom resolvers. Tier 3 (fully custom) resolvers must interpret the filter AST, ordering, and pagination themselves. A small utility (e.g.
InMemoryFilter::apply($rows, $plan)) would reduce boilerplate for resolvers backed by REST APIs, PHP computation, or other non-SQL sources. - Nullable column declaration in AbstractEntitySet.
columns()maps names toPrimitiveTypeEnumbut cannot express nullability. Marking a column nullable currently requires overridingentityType(). A declarative option (e.g. nullable enum wrapper or separatenullable()method) would close this gap. - Composite key order validation.
entityType()resolves key properties in column-declaration order, not in the order returned bykey(). A validation warning during schema build would catch accidental reordering. - Discovery logging for skipped relations. Polymorphic and through-relations on Eloquent models are silently ignored during discovery. A
logger->debug()message would help developers understand why a navigation property is absent from$metadata.
Not in scope (by design)
- Write operations (POST/PUT/PATCH/DELETE)
- ETags / conditional requests
$apply(aggregation)- Actions
License
MIT