Defining a Service
Every OData endpoint is backed by a service class that extends LaravelUi5\OData\ODataService. The service class declares what data is exposed and how it is resolved.
Service class structure
<?php
namespace App\OData;
use LaravelUi5\OData\ODataService;
use LaravelUi5\OData\Service\Builder\ResolverMapBuilder;
use LaravelUi5\OData\Service\Contracts\EdmBuilderInterface;
use LaravelUi5\OData\Service\Contracts\RuntimeSchemaBuilderInterface;
class PartnerService extends ODataService
{
public function serviceUri(): string
{
return 'partners';
}
public function namespace(): string
{
return 'Partners.Data';
}
protected function configure(EdmBuilderInterface $builder): EdmBuilderInterface
{
// Declare entity types, entity sets, functions, etc.
return $builder->namespace($this->namespace());
}
protected function registerBindings(ResolverMapBuilder $map): void
{
// Bind entity sets to data sources (Eloquent models, SQL views, etc.)
}
protected function bindFunctions(RuntimeSchemaBuilderInterface $builder): void
{
// Bind function imports and singletons
}
}Required overrides
serviceUri(): string
Returns the URL path segment for this service. For a single-service setup, return an empty string ''. For multi-service setups, return a unique segment like 'partners' or 'reporting'.
namespace(): string
Returns the XML namespace used in the $metadata document. Convention: Company.Domain (e.g., 'Partners.Data').
Extension hooks
configure(EdmBuilderInterface $builder): EdmBuilderInterface
This is where you define the schema -- entity types, entity sets, functions, singletons. You have two approaches:
Auto-discovery (recommended for Eloquent models):
protected function configure(EdmBuilderInterface $builder): EdmBuilderInterface
{
$this->discoverModel(Partner::class);
$this->discoverModel(Address::class);
return $builder->namespace($this->namespace());
}Custom entity sets (self-describing, one class per entity set):
protected function configure(EdmBuilderInterface $builder): EdmBuilderInterface
{
$this->discoverModel(User::class);
$this->discoverModel(Project::class);
$this->discoverCustomEntitySet(SearchItems::class);
$this->discoverCustomEntitySet(Kpis::class);
return $builder->namespace($this->namespace());
}discoverCustomEntitySet() handles entity type registration, entity set creation, resolver binding, and (for VirtualExpandResolverInterface implementations) navigation property wiring -- all from the single class.
See Custom Resolvers for the full API.
Manual declaration (for full control):
protected function configure(EdmBuilderInterface $builder): EdmBuilderInterface
{
$partnerType = new EntityType(
namespace: $this->namespace(),
name: 'Partner',
key: [$idProp],
declaredProperties: [$idProp, $nameProp],
);
$partnerSet = new EntitySet('Partners', $partnerType);
return $builder
->namespace($this->namespace())
->addEntityType($partnerType)
->addEntitySet($partnerSet);
}All three approaches can be mixed in the same service.
See Model Discovery and Manual Schema for details.
registerBindings(ResolverMapBuilder $map): void
This is where you connect entity sets to their data sources. Auto-discovered models are bound automatically as EloquentBinding entries -- you only need to manually register sets that were declared by hand.
Bindings are serializable and cached by odata:cache for the warm boot path.
protected function registerBindings(ResolverMapBuilder $map): void
{
$container = $map->getEdmx()->getEntityContainer();
// Eloquent model binding (for manually declared entity sets):
$map->eloquent($container->getEntitySet('Partners'), Partner::class);
// SQL view binding:
$map->sql($container->getEntitySet('ValueHelp'), 'partner_value_help_view');
// SQL source with implicit filters (tenant scoping, etc.):
$map->sqlSource($container->getEntitySet('Report'), ReportSource::class);
}bindFunctions(RuntimeSchemaBuilderInterface $builder): void
This is where you bind function import and singleton resolvers. These are not cached -- they run on every boot (both cold and warm paths).
protected function bindFunctions(RuntimeSchemaBuilderInterface $builder): void
{
$container = $builder->getEdmx()->getEntityContainer();
$builder->bindFunctionImport(
$container->getFunctionImport('GetPartnerCount'),
new class implements FunctionResolverInterface {
public function resolve(QueryPlanInterface $plan): mixed
{
return Partner::count();
}
},
);
$builder->bindSingleton(
$container->getSingleton('DefaultPartner'),
new class implements SingletonResolverInterface {
public function resolve(): array
{
return ['id' => 0, 'name' => 'Default'];
}
},
);
}Schema lifecycle
The schema() method on ODataService orchestrates the build with two paths:
Warm path (when odata:cache has been run):
- Load cached Edmx from generated PHP files
- Load cached ResolverMap from generated PHP files
- Apply resolver bindings from the map
- Call
bindFunctions()for function/singleton resolvers - Freeze the RuntimeSchema
Cold path (no cache):
- Call
configure()to build the Edm structure (discovery + custom entity sets) - Apply custom entity types from
discoverCustomEntitySet()calls - Wire virtual expand navigation properties onto parent entity types
- Apply auto-discovered models to the builder (including virtual nav props)
- Freeze the Edmx via
$builder->build() - Auto-register EloquentBindings for discovered models
- Auto-register CustomBindings for custom entity sets
- Call
registerBindings()for manual bindings - Apply resolver bindings from the map
- Call
bindFunctions()for function/singleton resolvers - Wire schema reference into Eloquent resolvers (for virtual expand delegation)
- Freeze the RuntimeSchema
The schema is built lazily on the first call to schema() and cached in memory for subsequent requests within the same process. The warm path requires no configure(), no model discovery, and no database access.
Registering a service
Services are resolved by the service registry. Update config/odata.php:
'service_registry' => App\OData\ServiceRegistry::class,See Multi-Service Routing for how to host multiple services.
Other methods
endpoint(): string
Returns the full URL to the service root (e.g., http://localhost/odata/partners/). Computed from the config prefix and serviceUri().
route(): string
Returns the relative Laravel route path (e.g., odata/partners).
cachedMetadataXMLPath(): ?string
Returns the path to a pre-built CSDL XML file, or null to generate metadata on the fly. Override this if you ship a static $metadata document.