Architecture
The library is organized into five layers with strict dependency rules.
Layer diagram
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:
| Layer | Must not import | Why |
|---|---|---|
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\Http | resolvers 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.
Request flow
$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
| Tier | Scope | Laravel container? |
|---|---|---|
| 1 — Edm | Interfaces, implementations, vocabularies | No |
| 2 — Plan | QueryPlanner, FilterParser | No |
| 3 — Resolver | EloquentEntitySetResolver, SqlEntitySetResolver (SQLite) | Yes (Orchestra) |
| 4 — Protocol | Full HTTP round-trips | Yes (Orchestra) |