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 generate metadata on the fly. Override this if you ship a static $metadata document.

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