Skip to content

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

LayerMay 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 queries

Request 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

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

OData: MIT | Core: BSL 1.1 | SDK: Commercial License