Skip to content

Model Discovery

Model discovery automatically maps Eloquent models to OData entity types. Call $this->discoverModel() in your service's configure() method, and the library inspects the model's database table, column types, casts, and relationships.

Basic usage

php
protected function configure(EdmBuilderInterface $builder): EdmBuilderInterface
{
    $this->discoverModel(Flight::class);
    $this->discoverModel(Passenger::class);

    return $builder->namespace($this->namespace());
}
// No registerBindings() needed -- discovered models are auto-bound.

Each discoverModel() call:

  1. Inspects the model's table via Schema::getColumns()
  2. Maps each column to an OData primitive type
  3. Uses $model->getKeyName() as the entity type key
  4. Discovers Eloquent relationships and maps them to navigation properties
  5. Creates an entity set with a pluralized name
  6. Auto-registers an EloquentBinding in the ResolverMap

Naming conventions

ElementConventionExample
Entity type nameModel short class nameFlight
Entity set namePluralized type nameFlights
Property nameColumn name (snake_case)flight_id

Column type mapping

The library maps database column types to OData primitive types. When a model declares a cast for a column, the cast type takes precedence over the database type.

Database type mapping

DB TypeOData Type
string, varchar, char, textEdm.String
integer, int, tinyint, smallint, mediumintEdm.Int32
bigintEdm.Int64
float, double, realEdm.Double
decimal, numericEdm.Decimal
boolean, boolEdm.Boolean
dateEdm.Date
datetime, timestampEdm.DateTimeOffset
timeEdm.TimeOfDay
blob, binaryEdm.Binary
json, jsonbEdm.String
uuid, guidEdm.Guid
Unknown typesEdm.String (fallback)

Cast type overrides

Eloquent CastOData Type
'integer', 'int'Edm.Int32
'float', 'double'Edm.Double
'decimal'Edm.Decimal
'boolean', 'bool'Edm.Boolean
'date'Edm.Date
'datetime', 'timestamp'Edm.DateTimeOffset
'string'Edm.String
'array', 'json', 'collection', 'object'Edm.String

Cast format suffixes (e.g., 'datetime:Y-m-d H:i:s') are stripped before mapping.

Relationship discovery

The library discovers relationships by instantiating the model and calling its public methods. Methods that return an Eloquent Relation are mapped to navigation properties.

Eloquent RelationOData NavigationisCollection
HasManyCollectionyes
BelongsToSingle-valuedno
HasOneSingle-valuedno
BelongsToManyCollectionyes

Skipped relationships (not mapped):

  • MorphMany, MorphOne, MorphTo, MorphToMany (polymorphic)
  • HasManyThrough, HasOneThrough (through-relationships)

Navigation properties are only wired when both sides of the relationship are discovered. If you discover Flight but not Passenger, the passengers navigation property is not created.

Navigation property bindings on entity sets are automatically created for each discovered navigation property.

Attribute overrides

Four PHP attributes let you customize the auto-discovery behavior. These are not OData vocabulary annotations -- they are configuration attributes consumed only by the discovery engine.

#[ODataEntity] -- class level

Override the entity type name or entity set name:

php
use LaravelUi5\OData\Service\Discovery\Attributes\ODataEntity;

#[ODataEntity(name: 'Airplane', entitySet: 'Airplanes')]
class Flight extends Model
{
    // Entity type will be "Airplane" instead of "Flight"
    // Entity set will be "Airplanes" instead of "Flights"
}

#[ODataProperty] -- property level

Override a property's OData name, type, or nullability:

php
use LaravelUi5\OData\Service\Discovery\Attributes\ODataProperty;

class Flight extends Model
{
    #[ODataProperty(name: 'FlightCode', type: 'Edm.String')]
    public $flight_number;
}

#[ODataIgnore] -- property or method level

Exclude a column or relationship from discovery:

php
use LaravelUi5\OData\Service\Discovery\Attributes\ODataIgnore;

class Flight extends Model
{
    #[ODataIgnore]
    public $internal_notes;  // Not exposed as OData property

    #[ODataIgnore]
    public function auditLog()  // Not exposed as navigation property
    {
        return $this->hasMany(AuditLog::class);
    }
}

#[ODataNavigation] -- method level

Override a navigation property's OData name:

php
use LaravelUi5\OData\Service\Discovery\Attributes\ODataNavigation;

class Flight extends Model
{
    #[ODataNavigation(name: 'Travelers')]
    public function passengers()
    {
        return $this->hasMany(Passenger::class);
    }
}

What is NOT discovered

  • Eloquent accessors (computed attributes) -- not backed by DB columns
  • Hidden attributes ($hidden) -- discovery does not respect $hidden; use #[ODataIgnore] instead
  • Guarded/fillable -- irrelevant for a read-only service

Coexistence with manual schema

You can mix discovered models with manually declared types in the same service:

php
protected function configure(EdmBuilderInterface $builder): EdmBuilderInterface
{
    // Auto-discover these models
    $this->discoverModel(Flight::class);
    $this->discoverModel(Passenger::class);

    // Manually declare a function
    $countFunc = new EdmFunction(name: 'GetFlightCount', returnType: $int32);
    $countImport = new FunctionImport('GetFlightCount', $countFunc);

    return $builder
        ->namespace($this->namespace())
        ->addFunction($countFunc)
        ->addFunctionImport($countImport);
}

protected function bindFunctions(RuntimeSchemaBuilderInterface $builder): void
{
    // Discovered models are auto-bound -- only bind the function:
    $container = $builder->getEdmx()->getEntityContainer();
    $builder->bindFunctionImport(
        $container->getFunctionImport('GetFlightCount'),
        new class implements FunctionResolverInterface {
            public function resolve(QueryPlanInterface $plan): mixed
            {
                return Flight::count();
            }
        },
    );
}

If you register a binding for a discovered model in registerBindings(), the manual binding overrides the auto-registered one.

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