Skip to content

Architecture ​

The library is organized into five layers with strict dependency rules.

Layer diagram ​

The five layers of laravelui5/odataFive stacked layers. Http, Protocol, Service and Driver all depend inward on Edm, the pure metamodel, which depends on nothing.Http/Controller · ODataRequest · ODataResponse · ReadGateProtocol/Parser · Planning (QueryPlanner, QueryPlan) · Execution (Engine, Handlers)Service/Builder · Discovery · Cache · SerializationDriver/Sql/EloquentEntitySetResolver · Sql/SqlEntitySetResolver · Expression/Edm/Contracts · Type · Property · Container · Annotation · Vocabulariesdepends on nothingdepends on
Dependencies point one way only — inward, toward a metamodel that knows nothing about Laravel, HTTP, or SQL.

Dependency rules ​

Four rules hold, and they are enforced by the test suite rather than by convention — they live as architecture tests (tests/ArchTest.php) and run on every build:

LayerMust not importWhy
Edm/Illuminate, Service/, Protocol/, Driver/the metamodel is portable and testable without Laravel
Protocol/Service/Discovery/, Driver/the protocol layer is testable without a database
Driver/Illuminate\Httpresolvers are decoupled from HTTP concerns
Protocol/Planning/— (must be readonly throughout, enum and visitor types aside)a query plan is a value, not a mutable buffer

Everything not named here is permitted: Protocol/ may reach for the HTTP response type it returns, and Service/ — the assembly layer — wires Driver/, Protocol/ and Http/ together, which is what an assembly layer is for. The rules above are deliberately the narrow set that can be checked; a rule no test protects is an intention, not a rule.

Reading the rules in the source

tests/ArchTest.php is short and is the authoritative statement. If a rule here and a rule there ever disagree, the test wins.

Two-stage schema building ​

The schema is built in two stages:

Stage 1 — EdmBuilder (structure)

EdmBuilder accumulates entity types, properties, entity sets, functions, and annotations. It produces a frozen, immutable EdmxInterface — the pure data model with no runtime behavior.

Stage 2 — RuntimeSchemaBuilder (resolution)

RuntimeSchemaBuilder takes the frozen EdmxInterface and binds resolvers to each entity set, function import, and singleton. It produces a RuntimeSchemaInterface that the engine uses to execute queries.

How a service becomes a runtime schemaTwo independent stages run in parallel: configure() feeds EdmBuilder which freezes an EdmxInterface, and registerBindings() feeds ResolverMapBuilder which freezes a ResolverMap. Both, plus bindFunctions(), are consumed by RuntimeSchemaBuilder to produce the RuntimeSchemaInterface the engine executes against.STAGE 1 — STRUCTURESTAGE 2 — BINDINGSconfigure()EdmBuilderEdmxInterfacefrozen structureregisterBindings()ResolverMapBuilderResolverMapfrozen bindingsRuntimeSchemaBuilderResolverMap.applyTo()bindFunctions()RuntimeSchemaInterfacestructure + resolvers, ready to executeThis is what odata:cache freezes to PHP — and what the engine loads on a warm boot.
Structure is built and frozen before a single resolver is bound. The two stages never reach into each other.

Request flow ​

The path of one OData requestAn HTTP request passes through the controller, becomes an ODataRequest, is planned into a QueryPlan, gated by the read authorizer, executed by a handler, resolved to rows by a resolver, and streamed back as JSON.HTTP requestGET /odata/Products?$filter=…OData controllerthe registry resolves the serviceODataRequestpath + system query optionsQueryPlanner.plan()produces a typed QueryPlanReadGate.authorize()hard denial → 403 · gated $expand prunedEngine.execute(plan)picks the handler for the plan typeResolver.resolve(plan)yields rows — Generator<array>ODataResponsestreamed JSON, row by rowONE PASS · NOTHING BUFFERED
Every $batch inner request runs this same path, gate included.

Key design decisions ​

Immutable Edm model: Once built, the EdmxInterface is frozen. This makes it safe to cache, share across requests, and reason about.

Generator-based streaming: Resolvers yield rows one at a time via PHP generators. The handler writes each row to the output stream immediately, keeping memory usage constant regardless of result set size.

Object identity for resolver binding: RuntimeSchemaBuilder maps resolvers by spl_object_id() of the entity set instance. This avoids string-based lookups and ensures type safety.

Visitor pattern for filters: The FilterExpression AST uses the visitor pattern. FilterToEloquent and FilterToQuery are visitors that translate the AST to SQL WHERE clauses.

Contract-driven: All public APIs are defined as interfaces in Edm/Contracts/ and Service/Contracts/. Implementations are final readonly classes.

Source layout ​

src/
├── Console/            Artisan commands (odata:cache, odata:clear)
├── Driver/             Database resolvers (Eloquent, SQL)
│   └── Sql/            SQL-specific implementations
│       └── Expression/ Filter visitors (FilterToEloquent, FilterToQuery)
├── Edm/                Pure metamodel (zero dependencies)
│   ├── Contracts/      Complete interface layer
│   ├── Annotation/     Annotation value types
│   ├── Container/      EntityContainer, EntitySet, Singleton, FunctionImport, EnumType
│   ├── Property/       Property, NavigationProperty
│   ├── Type/           EntityType, ComplexType, PrimitiveType
│   └── Vocabularies/   VocabularyRegistry, VocabularyCatalog
├── Exception/          Protocol exceptions (400, 403, 404, 500, 501)
├── Http/               HTTP layer (Controller, Request, Response)
├── Protocol/           OData wire protocol
│   ├── Parser/         Expression lexer, filter parser, property resolver
│   ├── Planning/       QueryPlanner, query plan hierarchy, filter AST
│   └── Execution/      Engine, typed handlers
├── Service/            Lifecycle layer
│   ├── Contracts/      Service interfaces, resolver interfaces
│   ├── Builder/        EdmBuilder, RuntimeSchemaBuilder
│   ├── Cache/          EdmxWriter, EdmxLoader
│   ├── Discovery/      ModelDiscovery, AttributeReader, override attributes
│   └── Serialization/  CsdlSerializer (XML output)
└── Vocabularies/       Generated vocabulary classes (Core, UI, Common, ...)

Testing tiers ​

TierScopeLaravel container?
1 — EdmInterfaces, implementations, vocabulariesNo
2 — PlanQueryPlanner, FilterParserNo
3 — ResolverEloquentEntitySetResolver, SqlEntitySetResolver (SQLite)Yes (Orchestra)
4 — ProtocolFull HTTP round-tripsYes (Orchestra)