Skip to content

Multi-Service Routing

By default, a single OData service handles all requests under the configured prefix. To host multiple independent services — each with its own schema, entity types, and resolvers — implement a custom service registry.

The service registry interface

php
interface ODataServiceRegistryInterface
{
    /**
     * Return all registered services (used by odata:cache and odata:clear).
     */
    public function services(): array;

    /**
     * Route a request path to the appropriate service.
     */
    public function resolve(string $fullPath): ODataServiceInterface;
}

Example: path-based routing

php
<?php

namespace App\OData;

use LaravelUi5\OData\Service\Contracts\ODataServiceInterface;
use LaravelUi5\OData\Service\Contracts\ODataServiceRegistryInterface;

class ServiceRegistry implements ODataServiceRegistryInterface
{
    public function resolve(string $fullPath): ODataServiceInterface
    {
        return match (true) {
            str_starts_with($fullPath, 'partners') => new PartnerService(),
            str_starts_with($fullPath, 'reporting') => new ReportingService(),
            default => new PartnerService(), // fallback
        };
    }

    public function services(): array
    {
        return [
            new PartnerService(),
            new ReportingService(),
        ];
    }
}

With this registry, the URLs resolve as:

GET /odata/partners/Partners        → PartnerService
GET /odata/partners/$metadata       → PartnerService metadata
GET /odata/reporting/SalesReport    → ReportingService
GET /odata/reporting/$metadata      → ReportingService metadata

Registration

Update config/odata.php:

php
'service_registry' => App\OData\ServiceRegistry::class,

The service provider binds the registry as a singleton via the container.

Host application integration

When a host application (e.g. LaravelUi5/Core) provides its own routing, set register_routes to false and configure a custom service_registry:

php
// config/odata.php
'register_routes'  => false,
'service_registry' => \App\OData\MyServiceRegistry::class,

The host's service provider re-uses the package's route file, wrapping it with its own prefix and middleware. This keeps a single middleware config:

php
// In the host's ServiceProvider::boot()
Route::prefix('odata')
    ->middleware(config('my-app.odata_middleware'))
    ->group(base_path('vendor/laravelui5/odata/routes/odata.php'));

The route file defines a catch-all {path?} that dispatches to the OData controller. The custom ODataServiceRegistryInterface implementation resolves the correct ODataService from the request path (e.g. extracting {namespace}@{version}). The controller then strips the service's route() prefix to obtain the clean OData resource path (/, $metadata, Flights, etc.).

Route-composed services

A service does not have to live in the registry at all. OData::forService() dispatches a request to an already-resolved service instance, so a service can be mounted on its own route:

php
// In a ServiceProvider::boot() — a service on its own path, outside the registry
Route::any('excel/{path?}', fn (Request $request, ?string $path = null) =>
    app(\LaravelUi5\OData\Http\Controller\OData::class)
        ->forService($request, new \App\Excel\ExcelService()),
)->where('path', '.*');

Use it when a second service needs its own prefix, middleware stack, or auth boundary rather than being selected by a registry that inspects the path. The full engine runs — query options, $batch, $metadata, and read authorization all behave exactly as on a registry-resolved service.

Two consequences worth knowing:

  • Give it its own namespace. The EDM cache is keyed by the service class's directory and namespace, so two services sharing a namespace collide.
  • Cache it explicitly. odata:cache walks the registry, which this service is not in — name it with --class, see Caching.

Service isolation

Each service is fully self-contained:

  • Its own namespace() for $metadata
  • Its own configure() with its own entity types and sets
  • Its own registerBindings() and bindFunctions() with its own resolvers
  • Its own generated Edm/ cache directory (when using odata:cache)

Services do not share types, entity sets, or resolvers.

Caching

The odata:cache and odata:clear commands iterate services() to cache or clear every registered service. Make sure services() returns all services you want to be cacheable — and pass any route-composed service to --class, since it is by definition not in the registry.