-
-
Notifications
You must be signed in to change notification settings - Fork 4
Advanced Usage
Jean-Marc Strauven edited this page Dec 23, 2025
·
1 revision
Advanced patterns and customizations for Laravel ApiRoute.
Create a custom resolver for complex version detection logic.
<?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();
}
}// In AppServiceProvider
use App\Services\CustomVersionResolver;
use Grazulex\ApiRoute\Contracts\VersionResolverInterface;
public function register(): void
{
$this->app->bind(VersionResolverInterface::class, CustomVersionResolver::class);
}Integrate with external analytics services.
<?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',
]);
}
}use App\Tracking\AnalyticsTracker;
use Grazulex\ApiRoute\Contracts\VersionTrackerInterface;
public function register(): void
{
$this->app->bind(VersionTrackerInterface::class, AnalyticsTracker::class);
}Load version configuration from database or external source.
<?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',
];
}<?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);
}
}
}// In RouteServiceProvider or AppServiceProvider
public function boot(): void
{
app(DynamicVersionLoader::class)->load();
}Different versions per tenant.
<?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);
}
}Transform responses based on version.
<?php
namespace App\Transformers;
interface VersionTransformer
{
public function transform(array $data, string $version): array;
}<?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'],
],
];
}
}<?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),
]);
}
}Central gateway that routes to different services.
<?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());
}
}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)));
}
}Version-aware caching.
<?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();
}
}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();
});
}
}<?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');
}
}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);
}
}<?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");
});
}
}- Examples - Practical use cases
- Configuration - Full configuration reference
- Events - Hook into version events
Laravel ApiRoute - Complete API versioning lifecycle management for Laravel
Home | Getting Started | Examples | Configuration
Made with ❤️ for the Laravel community