Skip to content

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
<?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):

php
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):

php
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):

php
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.

php
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).

php
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):

  1. Load cached Edmx from generated PHP files
  2. Load cached ResolverMap from generated PHP files
  3. Apply resolver bindings from the map
  4. Call bindFunctions() for function/singleton resolvers
  5. Freeze the RuntimeSchema

Cold path (no cache):

  1. Call configure() to build the Edm structure (discovery + custom entity sets)
  2. Apply custom entity types from discoverCustomEntitySet() calls
  3. Wire virtual expand navigation properties onto parent entity types
  4. Apply auto-discovered models to the builder (including virtual nav props)
  5. Freeze the Edmx via $builder->build()
  6. Auto-register EloquentBindings for discovered models
  7. Auto-register CustomBindings for custom entity sets
  8. Call registerBindings() for manual bindings
  9. Apply resolver bindings from the map
  10. Call bindFunctions() for function/singleton resolvers
  11. Wire schema reference into Eloquent resolvers (for virtual expand delegation)
  12. 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:

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 serialize metadata per request. odata:cache writes such a file, and $metadata is streamed from it when one is present; override the method if you ship a hand-written document instead.

plannedThe writing half is queued — today nothing reads this method.