Architecture
The library is organized into five layers with strict dependency rules.
Layer diagram
┌──────────────────────────────────────────────────────┐
│ Http/ │
│ Controller, ODataRequest, ODataResponse │
│ Depends on: Protocol, Service, Edm │
├──────────────────────────────────────────────────────┤
│ Protocol/ │
│ Parser, Planning (QueryPlanner, QueryPlan), │
│ Execution (Engine, Handlers) │
│ Depends on: Service/Contracts, Edm/Contracts │
├──────────────────────────────────────────────────────┤
│ Service/ │
│ Builder (EdmBuilder, RuntimeSchemaBuilder), │
│ Discovery (ModelDiscovery, AttributeReader), │
│ Cache (EdmxWriter, EdmxLoader) │
│ Depends on: Edm, Laravel │
├──────────────────────────────────────────────────────┤
│ Driver/ │
│ Sql/EloquentEntitySetResolver, │
│ Sql/SqlEntitySetResolver, │
│ Sql/Expression/FilterToEloquent, FilterToQuery │
│ Depends on: Edm, Service/Contracts, Protocol/Plan │
├──────────────────────────────────────────────────────┤
│ Edm/ │
│ Contracts (interfaces), Types, Properties, │
│ Container, Annotations, Vocabularies │
│ Depends on: NOTHING (pure PHP, no Laravel) │
└──────────────────────────────────────────────────────┘Dependency rules
| Layer | May import from |
|---|---|
Edm/ | Nothing (zero external dependencies) |
Driver/ | Edm/, Service/Contracts/, Protocol/Planning/ |
Protocol/ | Service/Contracts/, Edm/Contracts/ |
Service/ | Edm/, Laravel |
Http/ | Everything |
These rules ensure that:
- The Edm metamodel is portable and testable without Laravel
- Resolvers are decoupled from HTTP concerns
- The protocol layer is testable without a database
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.
configure() → EdmBuilder → EdmxInterface (frozen structure)
│
registerBindings() → ResolverMapBuilder → ResolverMap (frozen bindings)
│
ResolverMap.applyTo() → RuntimeSchemaBuilder → RuntimeSchemaInterface
bindFunctions() (functions/singletons) │
Engine uses this to resolve queriesRequest flow
HTTP Request
→ OData Controller
→ ODataServiceRegistry.resolve(path) → ODataService
→ ODataRequest (value object)
→ QueryPlanner.plan(request, schema) → QueryPlan
→ Engine.execute(plan)
→ Handler (EntitySet, Entity, Metadata, ...)
→ Resolver.resolve(plan) → Generator<array>
→ ODataResponse (streamed JSON)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
│ ├── Property/ Property, NavigationProperty
│ ├── Type/ EntityType, ComplexType, PrimitiveType, EnumType
│ └── Vocabularies/ VocabularyRegistry, VocabularyCatalog
├── Exception/ Protocol exceptions (400, 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) |