Skip to content

Advanced Usage

Jean-Marc Strauven edited this page Dec 23, 2025 · 1 revision

Advanced Usage

Advanced patterns and customizations for Laravel ApiRoute.


Custom Version Resolver

Create a custom resolver for complex version detection logic.

Implementation

<?php

namespace App\Services;

use Grazulex\ApiRoute\Contracts\VersionResolverInterface;
use Grazulex\ApiRoute\VersionDefinition;
use Grazulex\ApiRoute\ApiRouteManager;
use Illuminate\Http\Request;

class CustomVersionResolver implements VersionResolverInterface
{
    public function __construct(
        private ApiRouteManager $manager
    ) {}

    public function resolve(Request $request): ?VersionDefinition
    {
        $version = $this->getRequestedVersion($request);

        if ($version === null) {
            return $this->getDefaultVersion();
        }

        return $this->manager->getVersion($version);
    }

    public function getRequestedVersion(Request $request): ?string
    {
        // Priority: Header > Query > URI > User preference
        return $request->header('X-API-Version')
            ?? $request->query('api_version')
            ?? $this->extractFromUri($request)
            ?? $this->getUserPreferredVersion($request);
    }

    private function extractFromUri(Request $request): ?string
    {
        if (preg_match('/\/api\/(v\d+)\//', $request->path(), $matches)) {
            return $matches[1];
        }
        return null;
    }

    private function getUserPreferredVersion(Request $request): ?string
    {
        // Get user's preferred version from database
        $user = $request->user();
        return $user?->preferred_api_version;
    }

    private function getDefaultVersion(): ?VersionDefinition
    {
        return $this->manager->currentVersion();
    }
}

Registration

// In AppServiceProvider
use App\Services\CustomVersionResolver;
use Grazulex\ApiRoute\Contracts\VersionResolverInterface;

public function register(): void
{
    $this->app->bind(VersionResolverInterface::class, CustomVersionResolver::class);
}

Custom Usage Tracker

Integrate with external analytics services.

Implementation

<?php

namespace App\Tracking;

use Grazulex\ApiRoute\Contracts\VersionTrackerInterface;
use App\Services\AnalyticsService;

class AnalyticsTracker implements VersionTrackerInterface
{
    public function __construct(
        private AnalyticsService $analytics
    ) {}

    public function track(
        string $version,
        string $endpoint,
        string $method,
        int $status
    ): void {
        $this->analytics->track('api_request', [
            'version' => $version,
            'endpoint' => $endpoint,
            'method' => $method,
            'status' => $status,
            'success' => $status >= 200 && $status < 400,
            'timestamp' => now()->toIso8601String(),
        ]);
    }

    public function getStats(string $version, int $days): array
    {
        return $this->analytics->query('api_request', [
            'version' => $version,
            'from' => now()->subDays($days),
            'to' => now(),
        ]);
    }

    public function getAllStats(int $days): array
    {
        return $this->analytics->query('api_request', [
            'from' => now()->subDays($days),
            'to' => now(),
            'groupBy' => 'version',
        ]);
    }
}

Registration

use App\Tracking\AnalyticsTracker;
use Grazulex\ApiRoute\Contracts\VersionTrackerInterface;

public function register(): void
{
    $this->app->bind(VersionTrackerInterface::class, AnalyticsTracker::class);
}

Dynamic Version Configuration

Load version configuration from database or external source.

Version Configuration Model

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class ApiVersionConfig extends Model
{
    protected $casts = [
        'deprecated_at' => 'datetime',
        'sunset_at' => 'datetime',
        'is_beta' => 'boolean',
        'rate_limit' => 'integer',
    ];
}

Dynamic Version Loader

<?php

namespace App\Services;

use App\Models\ApiVersionConfig;
use Grazulex\ApiRoute\Facades\ApiRoute;
use Illuminate\Support\Facades\Route;

class DynamicVersionLoader
{
    public function load(): void
    {
        $configs = ApiVersionConfig::where('active', true)->get();

        foreach ($configs as $config) {
            $this->registerVersion($config);
        }
    }

    private function registerVersion(ApiVersionConfig $config): void
    {
        $version = ApiRoute::version($config->name, function () use ($config) {
            // Load routes for this version
            $routesFile = base_path("routes/api/{$config->name}.php");
            if (file_exists($routesFile)) {
                require $routesFile;
            }
        });

        if ($config->is_beta) {
            $version->beta();
        } elseif ($config->deprecated_at) {
            $version->deprecated($config->deprecated_at);
        } else {
            $version->current();
        }

        if ($config->sunset_at) {
            $version->sunset($config->sunset_at);
        }

        if ($config->successor) {
            $version->setSuccessor($config->successor);
        }

        if ($config->rate_limit) {
            $version->rateLimit($config->rate_limit);
        }
    }
}

Usage in Service Provider

// In RouteServiceProvider or AppServiceProvider
public function boot(): void
{
    app(DynamicVersionLoader::class)->load();
}

Multi-Tenant API Versioning

Different versions per tenant.

Implementation

<?php

namespace App\Services;

use Grazulex\ApiRoute\Contracts\VersionResolverInterface;
use Grazulex\ApiRoute\VersionDefinition;
use App\Models\Tenant;

class TenantAwareVersionResolver implements VersionResolverInterface
{
    public function __construct(
        private ApiRouteManager $manager
    ) {}

    public function resolve(Request $request): ?VersionDefinition
    {
        $tenant = $this->resolveTenant($request);
        $version = $this->getRequestedVersion($request);

        // Check tenant's allowed versions
        if ($tenant && !$this->isTenantAllowed($tenant, $version)) {
            return $this->manager->getVersion($tenant->api_version);
        }

        return $this->manager->getVersion($version);
    }

    private function resolveTenant(Request $request): ?Tenant
    {
        $tenantId = $request->header('X-Tenant-Id');
        return $tenantId ? Tenant::find($tenantId) : null;
    }

    private function isTenantAllowed(Tenant $tenant, string $version): bool
    {
        $allowedVersions = $tenant->allowed_api_versions ?? [];
        return empty($allowedVersions) || in_array($version, $allowedVersions);
    }
}

Version Transformers

Transform responses based on version.

Transformer Interface

<?php

namespace App\Transformers;

interface VersionTransformer
{
    public function transform(array $data, string $version): array;
}

User Transformer

<?php

namespace App\Transformers;

class UserTransformer implements VersionTransformer
{
    public function transform(array $data, string $version): array
    {
        return match ($version) {
            'v1' => $this->v1($data),
            'v2' => $this->v2($data),
            'v3' => $this->v3($data),
            default => $data,
        };
    }

    private function v1(array $data): array
    {
        // Legacy format
        return [
            'id' => $data['id'],
            'name' => $data['first_name'] . ' ' . $data['last_name'],
            'email' => $data['email'],
        ];
    }

    private function v2(array $data): array
    {
        // Current format
        return [
            'id' => $data['id'],
            'firstName' => $data['first_name'],
            'lastName' => $data['last_name'],
            'email' => $data['email'],
            'createdAt' => $data['created_at'],
        ];
    }

    private function v3(array $data): array
    {
        // New format with nested objects
        return [
            'id' => $data['id'],
            'profile' => [
                'firstName' => $data['first_name'],
                'lastName' => $data['last_name'],
            ],
            'contact' => [
                'email' => $data['email'],
            ],
            'metadata' => [
                'createdAt' => $data['created_at'],
                'updatedAt' => $data['updated_at'],
            ],
        ];
    }
}

Using in Controller

<?php

namespace App\Http\Controllers\Api;

use App\Transformers\UserTransformer;

class UserController extends Controller
{
    public function __construct(
        private UserTransformer $transformer
    ) {}

    public function show(Request $request, User $user)
    {
        $data = $user->toArray();
        $version = $request->apiVersion();

        return response()->json([
            'data' => $this->transformer->transform($data, $version),
        ]);
    }
}

API Gateway Pattern

Central gateway that routes to different services.

Gateway Controller

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;

class ApiGatewayController extends Controller
{
    private array $services = [
        'users' => 'http://users-service:8080',
        'products' => 'http://products-service:8080',
        'orders' => 'http://orders-service:8080',
    ];

    public function handle(Request $request, string $service, string $path)
    {
        $version = $request->apiVersion();
        $baseUrl = $this->services[$service] ?? null;

        if (!$baseUrl) {
            abort(404, "Service not found: {$service}");
        }

        $response = Http::withHeaders([
            'X-API-Version' => $version,
            'X-Request-ID' => $request->header('X-Request-ID'),
        ])->send(
            $request->method(),
            "{$baseUrl}/api/{$version}/{$path}",
            ['json' => $request->all()]
        );

        return response($response->body(), $response->status())
            ->withHeaders($response->headers());
    }
}

Version Negotiation

Negotiate version based on client capabilities.

<?php

namespace App\Services;

use Grazulex\ApiRoute\ApiRouteManager;
use Illuminate\Http\Request;

class VersionNegotiator
{
    public function __construct(
        private ApiRouteManager $manager
    ) {}

    public function negotiate(Request $request): string
    {
        $requested = $request->header('X-API-Version');
        $supported = $this->parseAcceptVersions($request);
        $available = $this->manager->versions()->pluck('name')->toArray();

        // Exact match
        if ($requested && in_array($requested, $available)) {
            return $requested;
        }

        // Find best match from Accept-Version header
        foreach ($supported as $version) {
            if (in_array($version, $available)) {
                return $version;
            }
        }

        // Default to current
        return $this->manager->currentVersion()?->name() ?? 'v1';
    }

    private function parseAcceptVersions(Request $request): array
    {
        $header = $request->header('Accept-Version', '');
        return array_filter(array_map('trim', explode(',', $header)));
    }
}

Caching Strategies

Version-aware caching.

Cache Key Generation

<?php

namespace App\Services;

class VersionAwareCache
{
    public function key(Request $request, string $suffix = ''): string
    {
        $version = $request->apiVersion();
        $path = $request->path();
        $query = md5(serialize($request->query()));

        return "api:{$version}:{$path}:{$query}:{$suffix}";
    }

    public function remember(Request $request, int $ttl, Closure $callback)
    {
        $key = $this->key($request);

        return Cache::remember($key, $ttl, $callback);
    }

    public function invalidateVersion(string $version): void
    {
        Cache::tags(["api:{$version}"])->flush();
    }
}

Usage

class ProductController extends Controller
{
    public function __construct(
        private VersionAwareCache $cache
    ) {}

    public function index(Request $request)
    {
        return $this->cache->remember($request, 3600, function () {
            return Product::all();
        });
    }
}

Testing Utilities

Test Helpers

<?php

namespace Tests\Helpers;

use Grazulex\ApiRoute\Facades\ApiRoute;
use Illuminate\Support\Facades\Route;

trait ApiVersionTestHelpers
{
    protected function setupVersions(array $versions): void
    {
        foreach ($versions as $name => $config) {
            $definition = ApiRoute::version($name, $config['routes']);

            if ($config['deprecated'] ?? false) {
                $definition->deprecated($config['deprecated']);
            }

            if ($config['sunset'] ?? false) {
                $definition->sunset($config['sunset']);
            }

            if ($config['current'] ?? false) {
                $definition->current();
            }

            if ($config['beta'] ?? false) {
                $definition->beta();
            }
        }
    }

    protected function apiGet(string $version, string $endpoint): TestResponse
    {
        return $this->get("/api/{$version}/{$endpoint}");
    }

    protected function apiPost(string $version, string $endpoint, array $data = []): TestResponse
    {
        return $this->postJson("/api/{$version}/{$endpoint}", $data);
    }

    protected function assertDeprecationHeaders(TestResponse $response): void
    {
        $response->assertHeader('X-API-Version-Status', 'deprecated');
        $response->assertHeader('Deprecation');
    }
}

Usage in Tests

use Tests\Helpers\ApiVersionTestHelpers;

class ApiVersionTest extends TestCase
{
    use ApiVersionTestHelpers;

    protected function setUp(): void
    {
        parent::setUp();

        $this->setupVersions([
            'v1' => [
                'routes' => fn() => Route::get('test', fn() => 'v1'),
                'deprecated' => '2025-06-01',
                'sunset' => '2025-12-01',
            ],
            'v2' => [
                'routes' => fn() => Route::get('test', fn() => 'v2'),
                'current' => true,
            ],
        ]);
    }

    public function test_v1_has_deprecation_headers(): void
    {
        $response = $this->apiGet('v1', 'test');

        $response->assertOk();
        $this->assertDeprecationHeaders($response);
    }
}

Performance Optimization

Lazy Version Loading

<?php

namespace App\Providers;

use Grazulex\ApiRoute\Facades\ApiRoute;

class ApiVersionProvider extends ServiceProvider
{
    public function boot(): void
    {
        // Only load version routes when API is accessed
        if ($this->isApiRequest()) {
            $this->loadVersions();
        }
    }

    private function isApiRequest(): bool
    {
        return str_starts_with(request()->path(), 'api/');
    }

    private function loadVersions(): void
    {
        // Load only the requested version's routes
        $version = $this->detectVersion();

        ApiRoute::version($version, function () use ($version) {
            require base_path("routes/api/{$version}.php");
        });
    }
}

Next Steps

Clone this wiki locally