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
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:
- Inspects the model's table via
Schema::getColumns() - Maps each column to an OData primitive type
- Uses
$model->getKeyName()as the entity type key - Discovers Eloquent relationships and maps them to navigation properties
- Creates an entity set with a pluralized name
- Auto-registers an
EloquentBindingin the ResolverMap
Naming conventions
| Element | Convention | Example |
|---|---|---|
| Entity type name | Model short class name | Flight |
| Entity set name | Pluralized type name | Flights |
| Property name | Column 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 Type | OData Type |
|---|---|
string, varchar, char, text | Edm.String |
integer, int, tinyint, smallint, mediumint | Edm.Int32 |
bigint | Edm.Int64 |
float, double, real | Edm.Double |
decimal, numeric | Edm.Decimal |
boolean, bool | Edm.Boolean |
date | Edm.Date |
datetime, timestamp | Edm.DateTimeOffset |
time | Edm.TimeOfDay |
blob, binary | Edm.Binary |
json, jsonb | Edm.String |
uuid, guid | Edm.Guid |
| Unknown types | Edm.String (fallback) |
Cast type overrides
| Eloquent Cast | OData 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 Relation | OData Navigation | isCollection |
|---|---|---|
HasMany | Collection | yes |
BelongsTo | Single-valued | no |
HasOne | Single-valued | no |
BelongsToMany | Collection | yes |
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:
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:
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:
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:
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:
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.