refactor: move Api and Mcp frontages to their packages
Move Core\Front\Api\* to php-api and Core\Front\Mcp\* to php-mcp. Inline the api/mcp middleware group definitions in Front\Boot since middleware() runs before package providers load. Simplify McpToolsRegistering to just collect class names without validating the McpToolHandler interface (avoids framework→package dependency). Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
7c20a5f905
commit
2c598f022d
14 changed files with 23 additions and 1654 deletions
|
|
@ -11,8 +11,6 @@ declare(strict_types=1);
|
|||
|
||||
namespace Core\Events;
|
||||
|
||||
use Core\Front\Mcp\Contracts\McpToolHandler;
|
||||
|
||||
/**
|
||||
* Fired when MCP (Model Context Protocol) tools are being registered.
|
||||
*
|
||||
|
|
@ -27,8 +25,9 @@ use Core\Front\Mcp\Contracts\McpToolHandler;
|
|||
*
|
||||
* ## Handler Requirements
|
||||
*
|
||||
* Each handler class must implement `McpToolHandler` interface. Handlers
|
||||
* define the tools, their input schemas, and execution logic.
|
||||
* Each handler class must implement `McpToolHandler` interface (from php-mcp).
|
||||
* Handlers define the tools, their input schemas, and execution logic.
|
||||
* Validation is performed by the MCP package at runtime.
|
||||
*
|
||||
* ## Usage Example
|
||||
*
|
||||
|
|
@ -43,9 +42,6 @@ use Core\Front\Mcp\Contracts\McpToolHandler;
|
|||
* $event->handler(InventoryQueryHandler::class);
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
*
|
||||
* @see \Core\Front\Mcp\Contracts\McpToolHandler
|
||||
*/
|
||||
class McpToolsRegistering extends LifecycleEvent
|
||||
{
|
||||
|
|
@ -56,14 +52,9 @@ class McpToolsRegistering extends LifecycleEvent
|
|||
* Register an MCP tool handler class.
|
||||
*
|
||||
* @param string $handlerClass Fully qualified class name implementing McpToolHandler
|
||||
*
|
||||
* @throws \InvalidArgumentException If class doesn't implement McpToolHandler
|
||||
*/
|
||||
public function handler(string $handlerClass): void
|
||||
{
|
||||
if (! is_a($handlerClass, McpToolHandler::class, true)) {
|
||||
throw new \InvalidArgumentException("{$handlerClass} must implement McpToolHandler");
|
||||
}
|
||||
$this->handlers[] = $handlerClass;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,253 +0,0 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* Core PHP Framework
|
||||
*
|
||||
* Licensed under the European Union Public Licence (EUPL) v1.2.
|
||||
* See LICENSE file for details.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Front\Api;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
/**
|
||||
* API Version Service.
|
||||
*
|
||||
* Provides helper methods for working with API versions in controllers
|
||||
* and other application code.
|
||||
*
|
||||
* ## Usage in Controllers
|
||||
*
|
||||
* ```php
|
||||
* use Core\Front\Api\ApiVersionService;
|
||||
*
|
||||
* class UserController
|
||||
* {
|
||||
* public function __construct(
|
||||
* protected ApiVersionService $versions
|
||||
* ) {}
|
||||
*
|
||||
* public function index(Request $request)
|
||||
* {
|
||||
* if ($this->versions->isV2($request)) {
|
||||
* return $this->indexV2($request);
|
||||
* }
|
||||
* return $this->indexV1($request);
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* ## Version Negotiation
|
||||
*
|
||||
* The service supports version negotiation where controllers can provide
|
||||
* different responses based on the requested version:
|
||||
*
|
||||
* ```php
|
||||
* return $this->versions->negotiate($request, [
|
||||
* 1 => fn() => $this->responseV1(),
|
||||
* 2 => fn() => $this->responseV2(),
|
||||
* ]);
|
||||
* ```
|
||||
*/
|
||||
class ApiVersionService
|
||||
{
|
||||
/**
|
||||
* Get the current API version from the request.
|
||||
*
|
||||
* Returns null if no version middleware has processed the request.
|
||||
*/
|
||||
public function current(?Request $request = null): ?int
|
||||
{
|
||||
$request ??= request();
|
||||
|
||||
return $request->attributes->get('api_version');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current API version as a string (e.g., 'v1').
|
||||
*/
|
||||
public function currentString(?Request $request = null): ?string
|
||||
{
|
||||
$request ??= request();
|
||||
|
||||
return $request->attributes->get('api_version_string');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the request is for a specific version.
|
||||
*/
|
||||
public function is(int $version, ?Request $request = null): bool
|
||||
{
|
||||
return $this->current($request) === $version;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the request is for version 1.
|
||||
*/
|
||||
public function isV1(?Request $request = null): bool
|
||||
{
|
||||
return $this->is(1, $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the request is for version 2.
|
||||
*/
|
||||
public function isV2(?Request $request = null): bool
|
||||
{
|
||||
return $this->is(2, $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the request version is at least the given version.
|
||||
*/
|
||||
public function isAtLeast(int $version, ?Request $request = null): bool
|
||||
{
|
||||
$current = $this->current($request);
|
||||
|
||||
return $current !== null && $current >= $version;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current version is deprecated.
|
||||
*/
|
||||
public function isDeprecated(?Request $request = null): bool
|
||||
{
|
||||
$current = $this->current($request);
|
||||
$deprecated = config('api.versioning.deprecated', []);
|
||||
|
||||
return $current !== null && in_array($current, $deprecated, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the configured default version.
|
||||
*/
|
||||
public function defaultVersion(): int
|
||||
{
|
||||
return (int) config('api.versioning.default', 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current/latest version.
|
||||
*/
|
||||
public function latestVersion(): int
|
||||
{
|
||||
return (int) config('api.versioning.current', 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all supported versions.
|
||||
*
|
||||
* @return array<int>
|
||||
*/
|
||||
public function supportedVersions(): array
|
||||
{
|
||||
return config('api.versioning.supported', [1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all deprecated versions.
|
||||
*
|
||||
* @return array<int>
|
||||
*/
|
||||
public function deprecatedVersions(): array
|
||||
{
|
||||
return config('api.versioning.deprecated', []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get sunset dates for versions.
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public function sunsetDates(): array
|
||||
{
|
||||
return config('api.versioning.sunset', []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a version is supported.
|
||||
*/
|
||||
public function isSupported(int $version): bool
|
||||
{
|
||||
return in_array($version, $this->supportedVersions(), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Negotiate response based on API version.
|
||||
*
|
||||
* Calls the appropriate handler based on the request's API version.
|
||||
* Falls back to lower version handlers if exact match not found.
|
||||
*
|
||||
* ```php
|
||||
* return $versions->negotiate($request, [
|
||||
* 1 => fn() => ['format' => 'v1'],
|
||||
* 2 => fn() => ['format' => 'v2', 'extra' => 'field'],
|
||||
* ]);
|
||||
* ```
|
||||
*
|
||||
* @param array<int, callable> $handlers Version handlers keyed by version number
|
||||
* @return mixed Result from the appropriate handler
|
||||
*
|
||||
* @throws \InvalidArgumentException If no suitable handler found
|
||||
*/
|
||||
public function negotiate(Request $request, array $handlers): mixed
|
||||
{
|
||||
$version = $this->current($request) ?? $this->defaultVersion();
|
||||
|
||||
// Try exact match first
|
||||
if (isset($handlers[$version])) {
|
||||
return $handlers[$version]();
|
||||
}
|
||||
|
||||
// Fall back to highest version that's <= requested version
|
||||
krsort($handlers);
|
||||
foreach ($handlers as $handlerVersion => $handler) {
|
||||
if ($handlerVersion <= $version) {
|
||||
return $handler();
|
||||
}
|
||||
}
|
||||
|
||||
// No suitable handler found
|
||||
throw new \InvalidArgumentException(
|
||||
"No handler found for API version {$version}. Available versions: ".implode(', ', array_keys($handlers))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform response data based on API version.
|
||||
*
|
||||
* Useful for removing or adding fields based on version.
|
||||
*
|
||||
* ```php
|
||||
* return $versions->transform($request, $data, [
|
||||
* 1 => fn($data) => Arr::except($data, ['new_field']),
|
||||
* 2 => fn($data) => $data,
|
||||
* ]);
|
||||
* ```
|
||||
*
|
||||
* @param array<int, callable> $transformers Version transformers
|
||||
*/
|
||||
public function transform(Request $request, mixed $data, array $transformers): mixed
|
||||
{
|
||||
$version = $this->current($request) ?? $this->defaultVersion();
|
||||
|
||||
// Try exact match first
|
||||
if (isset($transformers[$version])) {
|
||||
return $transformers[$version]($data);
|
||||
}
|
||||
|
||||
// Fall back to highest version that's <= requested version
|
||||
krsort($transformers);
|
||||
foreach ($transformers as $transformerVersion => $transformer) {
|
||||
if ($transformerVersion <= $version) {
|
||||
return $transformer($data);
|
||||
}
|
||||
}
|
||||
|
||||
// No transformer, return data unchanged
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,111 +0,0 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* Core PHP Framework
|
||||
*
|
||||
* Licensed under the European Union Public Licence (EUPL) v1.2.
|
||||
* See LICENSE file for details.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Front\Api;
|
||||
|
||||
use Core\Front\Api\Middleware\ApiSunset;
|
||||
use Core\Front\Api\Middleware\ApiVersion;
|
||||
use Core\LifecycleEventProvider;
|
||||
use Illuminate\Cache\RateLimiting\Limit;
|
||||
use Illuminate\Foundation\Configuration\Middleware;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Router;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
/**
|
||||
* API frontage - API stage.
|
||||
*
|
||||
* Provides api middleware group for API routes and API versioning support.
|
||||
*
|
||||
* ## API Versioning
|
||||
*
|
||||
* This provider registers middleware for API versioning:
|
||||
* - `api.version` - Parses and validates API version from URL or headers
|
||||
* - `api.sunset` - Adds deprecation/sunset headers to endpoints
|
||||
*
|
||||
* Configure versioning in config/api.php:
|
||||
* ```php
|
||||
* 'versioning' => [
|
||||
* 'default' => 1, // Default version when none specified
|
||||
* 'current' => 1, // Current/latest version
|
||||
* 'supported' => [1], // List of supported versions
|
||||
* 'deprecated' => [], // Deprecated but still supported versions
|
||||
* 'sunset' => [], // Sunset dates: [1 => '2025-06-01']
|
||||
* ],
|
||||
* ```
|
||||
*
|
||||
* @see ApiVersion Middleware for version parsing
|
||||
* @see ApiVersionService Service for programmatic version checks
|
||||
* @see VersionedRoutes Helper for version-based route registration
|
||||
*/
|
||||
class Boot extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Configure api middleware group.
|
||||
*/
|
||||
public static function middleware(Middleware $middleware): void
|
||||
{
|
||||
$middleware->group('api', [
|
||||
\Illuminate\Routing\Middleware\ThrottleRequests::class.':api',
|
||||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||
]);
|
||||
|
||||
// Register versioning middleware aliases
|
||||
$middleware->alias([
|
||||
'api.version' => ApiVersion::class,
|
||||
'api.sunset' => ApiSunset::class,
|
||||
]);
|
||||
}
|
||||
|
||||
public function register(): void
|
||||
{
|
||||
// Merge API configuration
|
||||
$this->mergeConfigFrom(__DIR__.'/config.php', 'api');
|
||||
|
||||
// Register API version service as singleton
|
||||
$this->app->singleton(ApiVersionService::class);
|
||||
}
|
||||
|
||||
public function boot(): void
|
||||
{
|
||||
$this->configureRateLimiting();
|
||||
$this->registerMiddlewareAliases();
|
||||
|
||||
// Fire ApiRoutesRegistering event for lazy-loaded modules
|
||||
LifecycleEventProvider::fireApiRoutes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register middleware aliases via router.
|
||||
*
|
||||
* This ensures aliases are available even if the static middleware()
|
||||
* method isn't called (e.g., in testing or custom bootstrap).
|
||||
*/
|
||||
protected function registerMiddlewareAliases(): void
|
||||
{
|
||||
/** @var Router $router */
|
||||
$router = $this->app->make(Router::class);
|
||||
|
||||
$router->aliasMiddleware('api.version', ApiVersion::class);
|
||||
$router->aliasMiddleware('api.sunset', ApiSunset::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure API rate limiting.
|
||||
*/
|
||||
protected function configureRateLimiting(): void
|
||||
{
|
||||
RateLimiter::for('api', function (Request $request) {
|
||||
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,112 +0,0 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* Core PHP Framework
|
||||
*
|
||||
* Licensed under the European Union Public Licence (EUPL) v1.2.
|
||||
* See LICENSE file for details.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Front\Api\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* API Sunset Middleware.
|
||||
*
|
||||
* Adds the HTTP Sunset header to responses to indicate when an endpoint
|
||||
* will be deprecated or removed.
|
||||
*
|
||||
* The Sunset header is defined in RFC 8594 and indicates that a resource
|
||||
* will become unresponsive at the specified date.
|
||||
*
|
||||
* ## Usage
|
||||
*
|
||||
* Apply to routes that will be sunset:
|
||||
*
|
||||
* ```php
|
||||
* Route::middleware('api.sunset:2025-06-01')->group(function () {
|
||||
* Route::get('/legacy-endpoint', LegacyController::class);
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* Or with a replacement link:
|
||||
*
|
||||
* ```php
|
||||
* Route::middleware('api.sunset:2025-06-01,/api/v2/new-endpoint')->group(function () {
|
||||
* Route::get('/old-endpoint', OldController::class);
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* ## Response Headers
|
||||
*
|
||||
* The middleware adds these headers:
|
||||
* - Sunset: <date in RFC7231 format>
|
||||
* - Deprecation: true
|
||||
* - Link: <replacement-url>; rel="successor-version" (if replacement provided)
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc8594 RFC 8594: The "Sunset" HTTP Header Field
|
||||
*/
|
||||
class ApiSunset
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param string $sunsetDate The sunset date (YYYY-MM-DD or RFC7231 format)
|
||||
* @param string|null $replacement Optional replacement endpoint URL
|
||||
*/
|
||||
public function handle(Request $request, Closure $next, string $sunsetDate, ?string $replacement = null): Response
|
||||
{
|
||||
/** @var Response $response */
|
||||
$response = $next($request);
|
||||
|
||||
// Convert date to RFC7231 format if needed
|
||||
$formattedDate = $this->formatSunsetDate($sunsetDate);
|
||||
|
||||
// Add Sunset header
|
||||
$response->headers->set('Sunset', $formattedDate);
|
||||
|
||||
// Add Deprecation header
|
||||
$response->headers->set('Deprecation', 'true');
|
||||
|
||||
// Add warning header
|
||||
$version = $request->attributes->get('api_version', 'unknown');
|
||||
$response->headers->set(
|
||||
'X-API-Warn',
|
||||
"This endpoint is deprecated and will be removed on {$sunsetDate}."
|
||||
);
|
||||
|
||||
// Add Link header for replacement if provided
|
||||
if ($replacement !== null) {
|
||||
$response->headers->set('Link', "<{$replacement}>; rel=\"successor-version\"");
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the sunset date to RFC7231 format.
|
||||
*
|
||||
* Accepts dates in YYYY-MM-DD format or already-formatted RFC7231 dates.
|
||||
*/
|
||||
protected function formatSunsetDate(string $date): string
|
||||
{
|
||||
// Check if already in RFC7231 format (contains comma, day name)
|
||||
if (str_contains($date, ',')) {
|
||||
return $date;
|
||||
}
|
||||
|
||||
try {
|
||||
return (new \DateTimeImmutable($date))
|
||||
->setTimezone(new \DateTimeZone('GMT'))
|
||||
->format(\DateTimeInterface::RFC7231);
|
||||
} catch (\Exception) {
|
||||
// If parsing fails, return as-is
|
||||
return $date;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,246 +0,0 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* Core PHP Framework
|
||||
*
|
||||
* Licensed under the European Union Public Licence (EUPL) v1.2.
|
||||
* See LICENSE file for details.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Front\Api\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* API Version Middleware.
|
||||
*
|
||||
* Parses the API version from the request and sets it on the request attributes.
|
||||
* Supports version extraction from:
|
||||
*
|
||||
* 1. URL path prefix: /api/v1/users, /api/v2/users
|
||||
* 2. Accept-Version header: Accept-Version: v1, Accept-Version: 2
|
||||
* 3. Accept header with vendor type: Accept: application/vnd.hosthub.v1+json
|
||||
*
|
||||
* The resolved version is stored in request attributes and can be accessed via:
|
||||
* - $request->attributes->get('api_version') - returns integer (e.g., 1, 2)
|
||||
* - $request->attributes->get('api_version_string') - returns string (e.g., 'v1', 'v2')
|
||||
*
|
||||
* ## Configuration
|
||||
*
|
||||
* Configure in config/api.php:
|
||||
* ```php
|
||||
* 'versioning' => [
|
||||
* 'default' => 1, // Default version when none specified
|
||||
* 'current' => 1, // Current/latest version
|
||||
* 'supported' => [1], // List of supported versions
|
||||
* 'deprecated' => [], // List of deprecated (but still supported) versions
|
||||
* 'sunset' => [], // Versions with sunset dates: [1 => '2025-06-01']
|
||||
* ],
|
||||
* ```
|
||||
*
|
||||
* ## Usage in Routes
|
||||
*
|
||||
* ```php
|
||||
* // Apply to specific routes
|
||||
* Route::middleware('api.version')->group(function () {
|
||||
* Route::get('/users', [UserController::class, 'index']);
|
||||
* });
|
||||
*
|
||||
* // Or with version constraint
|
||||
* Route::middleware('api.version:2')->group(function () {
|
||||
* // Only accepts v2 requests
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* ## Deprecation Headers
|
||||
*
|
||||
* When a request uses a deprecated API version, the response includes:
|
||||
* - Deprecation: true
|
||||
* - Sunset: <date> (if configured)
|
||||
* - X-API-Warn: "API version X is deprecated..."
|
||||
*
|
||||
* @see ApiVersionService For programmatic version checks
|
||||
*/
|
||||
class ApiVersion
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param int|null $requiredVersion Minimum version required (optional)
|
||||
*/
|
||||
public function handle(Request $request, Closure $next, ?int $requiredVersion = null): Response
|
||||
{
|
||||
$version = $this->resolveVersion($request);
|
||||
$versionConfig = config('api.versioning', []);
|
||||
|
||||
$default = $versionConfig['default'] ?? 1;
|
||||
$current = $versionConfig['current'] ?? 1;
|
||||
$supported = $versionConfig['supported'] ?? [1];
|
||||
$deprecated = $versionConfig['deprecated'] ?? [];
|
||||
$sunset = $versionConfig['sunset'] ?? [];
|
||||
|
||||
// Use default if no version specified
|
||||
if ($version === null) {
|
||||
$version = $default;
|
||||
}
|
||||
|
||||
// Validate version is supported
|
||||
if (! in_array($version, $supported, true)) {
|
||||
return $this->unsupportedVersion($version, $supported, $current);
|
||||
}
|
||||
|
||||
// Check minimum version requirement
|
||||
if ($requiredVersion !== null && $version < $requiredVersion) {
|
||||
return $this->versionTooLow($version, $requiredVersion);
|
||||
}
|
||||
|
||||
// Store version in request
|
||||
$request->attributes->set('api_version', $version);
|
||||
$request->attributes->set('api_version_string', "v{$version}");
|
||||
|
||||
/** @var Response $response */
|
||||
$response = $next($request);
|
||||
|
||||
// Add version header to response
|
||||
$response->headers->set('X-API-Version', (string) $version);
|
||||
|
||||
// Add deprecation headers if applicable
|
||||
if (in_array($version, $deprecated, true)) {
|
||||
$response->headers->set('Deprecation', 'true');
|
||||
$response->headers->set('X-API-Warn', "API version {$version} is deprecated. Please upgrade to v{$current}.");
|
||||
|
||||
// Add Sunset header if configured
|
||||
if (isset($sunset[$version])) {
|
||||
$sunsetDate = $sunset[$version];
|
||||
// Convert to HTTP date format if not already
|
||||
if (! str_contains($sunsetDate, ',')) {
|
||||
$sunsetDate = (new \DateTimeImmutable($sunsetDate))->format(\DateTimeInterface::RFC7231);
|
||||
}
|
||||
$response->headers->set('Sunset', $sunsetDate);
|
||||
}
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the API version from the request.
|
||||
*
|
||||
* Priority order:
|
||||
* 1. URL path (/api/v1/...)
|
||||
* 2. Accept-Version header
|
||||
* 3. Accept header vendor type
|
||||
*/
|
||||
protected function resolveVersion(Request $request): ?int
|
||||
{
|
||||
// 1. Check URL path for version prefix
|
||||
$version = $this->versionFromPath($request);
|
||||
if ($version !== null) {
|
||||
return $version;
|
||||
}
|
||||
|
||||
// 2. Check Accept-Version header
|
||||
$version = $this->versionFromHeader($request);
|
||||
if ($version !== null) {
|
||||
return $version;
|
||||
}
|
||||
|
||||
// 3. Check Accept header for vendor type
|
||||
return $this->versionFromAcceptHeader($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract version from URL path.
|
||||
*
|
||||
* Matches: /api/v1/..., /api/v2/...
|
||||
*/
|
||||
protected function versionFromPath(Request $request): ?int
|
||||
{
|
||||
$path = $request->path();
|
||||
|
||||
// Match /api/v{n}/ or /v{n}/ at the start
|
||||
if (preg_match('#^(?:api/)?v(\d+)(?:/|$)#', $path, $matches)) {
|
||||
return (int) $matches[1];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract version from Accept-Version header.
|
||||
*
|
||||
* Accepts: v1, v2, 1, 2
|
||||
*/
|
||||
protected function versionFromHeader(Request $request): ?int
|
||||
{
|
||||
$header = $request->header('Accept-Version');
|
||||
|
||||
if ($header === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Strip 'v' prefix if present
|
||||
$version = ltrim($header, 'vV');
|
||||
|
||||
if (is_numeric($version)) {
|
||||
return (int) $version;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract version from Accept header vendor type.
|
||||
*
|
||||
* Matches: application/vnd.hosthub.v1+json
|
||||
*/
|
||||
protected function versionFromAcceptHeader(Request $request): ?int
|
||||
{
|
||||
$accept = $request->header('Accept', '');
|
||||
|
||||
// Match vendor media type: application/vnd.{name}.v{n}+json
|
||||
if (preg_match('#application/vnd\.[^.]+\.v(\d+)\+#', $accept, $matches)) {
|
||||
return (int) $matches[1];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return 400 response for unsupported version.
|
||||
*
|
||||
* @param array<int> $supported
|
||||
*/
|
||||
protected function unsupportedVersion(int $requested, array $supported, int $current): Response
|
||||
{
|
||||
return response()->json([
|
||||
'error' => 'unsupported_api_version',
|
||||
'message' => "API version {$requested} is not supported.",
|
||||
'requested_version' => $requested,
|
||||
'supported_versions' => $supported,
|
||||
'current_version' => $current,
|
||||
'hint' => 'Use Accept-Version header or URL prefix (e.g., /api/v1/) to specify version.',
|
||||
], 400, [
|
||||
'X-API-Version' => (string) $current,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return 400 response when version is too low.
|
||||
*/
|
||||
protected function versionTooLow(int $requested, int $required): Response
|
||||
{
|
||||
return response()->json([
|
||||
'error' => 'api_version_too_low',
|
||||
'message' => "This endpoint requires API version {$required} or higher.",
|
||||
'requested_version' => $requested,
|
||||
'minimum_version' => $required,
|
||||
], 400, [
|
||||
'X-API-Version' => (string) $requested,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,266 +0,0 @@
|
|||
# API Versioning
|
||||
|
||||
Core PHP Framework provides built-in API versioning support with deprecation handling and sunset headers.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Configure Versions
|
||||
|
||||
Add to your `config/api.php`:
|
||||
|
||||
```php
|
||||
'versioning' => [
|
||||
'default' => 1, // Version when none specified
|
||||
'current' => 2, // Latest/current version
|
||||
'supported' => [1, 2], // All supported versions
|
||||
'deprecated' => [1], // Deprecated but still working
|
||||
'sunset' => [ // Removal dates
|
||||
1 => '2025-12-31',
|
||||
],
|
||||
],
|
||||
```
|
||||
|
||||
### 2. Apply Middleware
|
||||
|
||||
The `api.version` middleware is automatically available. Apply it to routes:
|
||||
|
||||
```php
|
||||
// Version-agnostic routes (uses default version)
|
||||
Route::middleware('api.version')->group(function () {
|
||||
Route::get('/status', StatusController::class);
|
||||
});
|
||||
|
||||
// Version-specific routes with URL prefix
|
||||
use Core\Front\Api\VersionedRoutes;
|
||||
|
||||
VersionedRoutes::v1(function () {
|
||||
Route::get('/users', [UserController::class, 'indexV1']);
|
||||
});
|
||||
|
||||
VersionedRoutes::v2(function () {
|
||||
Route::get('/users', [UserController::class, 'indexV2']);
|
||||
});
|
||||
```
|
||||
|
||||
### 3. Version Negotiation in Controllers
|
||||
|
||||
```php
|
||||
use Core\Front\Api\ApiVersionService;
|
||||
|
||||
class UserController
|
||||
{
|
||||
public function __construct(
|
||||
protected ApiVersionService $versions
|
||||
) {}
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
return $this->versions->negotiate($request, [
|
||||
1 => fn() => $this->indexV1(),
|
||||
2 => fn() => $this->indexV2(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Version Resolution
|
||||
|
||||
The middleware resolves the API version from (in priority order):
|
||||
|
||||
1. **URL Path**: `/api/v1/users` or `/v2/users`
|
||||
2. **Accept-Version Header**: `Accept-Version: v1` or `Accept-Version: 2`
|
||||
3. **Accept Header**: `Accept: application/vnd.hosthub.v1+json`
|
||||
4. **Default**: Falls back to configured default version
|
||||
|
||||
## Response Headers
|
||||
|
||||
Successful responses include:
|
||||
|
||||
```
|
||||
X-API-Version: 2
|
||||
```
|
||||
|
||||
Deprecated versions also include:
|
||||
|
||||
```
|
||||
Deprecation: true
|
||||
X-API-Warn: API version 1 is deprecated. Please upgrade to v2.
|
||||
Sunset: Wed, 31 Dec 2025 00:00:00 GMT
|
||||
```
|
||||
|
||||
## Error Responses
|
||||
|
||||
### Unsupported Version (400)
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "unsupported_api_version",
|
||||
"message": "API version 99 is not supported.",
|
||||
"requested_version": 99,
|
||||
"supported_versions": [1, 2],
|
||||
"current_version": 2,
|
||||
"hint": "Use Accept-Version header or URL prefix (e.g., /api/v1/) to specify version."
|
||||
}
|
||||
```
|
||||
|
||||
### Version Too Low (400)
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "api_version_too_low",
|
||||
"message": "This endpoint requires API version 2 or higher.",
|
||||
"requested_version": 1,
|
||||
"minimum_version": 2
|
||||
}
|
||||
```
|
||||
|
||||
## Versioned Routes Helper
|
||||
|
||||
The `VersionedRoutes` class provides a fluent API for registering version-specific routes:
|
||||
|
||||
```php
|
||||
use Core\Front\Api\VersionedRoutes;
|
||||
|
||||
// Simple version registration
|
||||
VersionedRoutes::v1(function () {
|
||||
Route::get('/users', UserController::class);
|
||||
});
|
||||
|
||||
// With URL prefix (default)
|
||||
VersionedRoutes::v2(function () {
|
||||
Route::get('/users', UserControllerV2::class);
|
||||
}); // Accessible at /api/v2/users
|
||||
|
||||
// Header-only versioning (no URL prefix)
|
||||
VersionedRoutes::version(2)
|
||||
->withoutPrefix()
|
||||
->routes(function () {
|
||||
Route::get('/users', UserControllerV2::class);
|
||||
}); // Accessible at /api/users with Accept-Version: 2
|
||||
|
||||
// Multiple versions for the same routes
|
||||
VersionedRoutes::versions([1, 2], function () {
|
||||
Route::get('/health', HealthController::class);
|
||||
});
|
||||
|
||||
// Deprecated version with sunset
|
||||
VersionedRoutes::v1()
|
||||
->deprecated('2025-06-01')
|
||||
->routes(function () {
|
||||
Route::get('/legacy', LegacyController::class);
|
||||
});
|
||||
```
|
||||
|
||||
## ApiVersionService
|
||||
|
||||
Inject `ApiVersionService` for programmatic version checks:
|
||||
|
||||
```php
|
||||
use Core\Front\Api\ApiVersionService;
|
||||
|
||||
class UserController
|
||||
{
|
||||
public function __construct(
|
||||
protected ApiVersionService $versions
|
||||
) {}
|
||||
|
||||
public function show(Request $request, User $user)
|
||||
{
|
||||
$data = $user->toArray();
|
||||
|
||||
// Version-specific transformations
|
||||
return $this->versions->transform($request, $data, [
|
||||
1 => fn($d) => Arr::except($d, ['created_at', 'metadata']),
|
||||
2 => fn($d) => $d,
|
||||
]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Available Methods
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `current($request)` | Get version number (e.g., 1, 2) |
|
||||
| `currentString($request)` | Get version string (e.g., 'v1') |
|
||||
| `is($version, $request)` | Check exact version |
|
||||
| `isV1($request)` | Check if version 1 |
|
||||
| `isV2($request)` | Check if version 2 |
|
||||
| `isAtLeast($version, $request)` | Check minimum version |
|
||||
| `isDeprecated($request)` | Check if version is deprecated |
|
||||
| `defaultVersion()` | Get configured default |
|
||||
| `latestVersion()` | Get current/latest version |
|
||||
| `supportedVersions()` | Get all supported versions |
|
||||
| `deprecatedVersions()` | Get deprecated versions |
|
||||
| `sunsetDates()` | Get sunset dates map |
|
||||
| `isSupported($version)` | Check if version is supported |
|
||||
| `negotiate($request, $handlers)` | Call version-specific handler |
|
||||
| `transform($request, $data, $transformers)` | Transform data per version |
|
||||
|
||||
## Sunset Middleware
|
||||
|
||||
For endpoint-specific deprecation, use the `api.sunset` middleware:
|
||||
|
||||
```php
|
||||
Route::middleware('api.sunset:2025-06-01')->group(function () {
|
||||
Route::get('/legacy-endpoint', LegacyController::class);
|
||||
});
|
||||
|
||||
// With replacement hint
|
||||
Route::middleware('api.sunset:2025-06-01,/api/v2/new-endpoint')->group(function () {
|
||||
Route::get('/old-endpoint', OldController::class);
|
||||
});
|
||||
```
|
||||
|
||||
Adds headers:
|
||||
|
||||
```
|
||||
Sunset: Sun, 01 Jun 2025 00:00:00 GMT
|
||||
Deprecation: true
|
||||
X-API-Warn: This endpoint is deprecated and will be removed on 2025-06-01.
|
||||
Link: </api/v2/new-endpoint>; rel="successor-version"
|
||||
```
|
||||
|
||||
## Versioning Strategy
|
||||
|
||||
### Guidelines
|
||||
|
||||
1. **Add, don't remove**: New fields can be added to any version
|
||||
2. **New version for breaking changes**: Removing/renaming fields requires new version
|
||||
3. **Deprecate before removal**: Give clients time to migrate
|
||||
4. **Document changes**: Maintain changelog per version
|
||||
|
||||
### Version Lifecycle
|
||||
|
||||
```
|
||||
v1: Active -> Deprecated (with sunset) -> Removed from supported
|
||||
v2: Active (current)
|
||||
v3: Future
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
```env
|
||||
API_VERSION_DEFAULT=1
|
||||
API_VERSION_CURRENT=2
|
||||
API_VERSIONS_SUPPORTED=1,2
|
||||
API_VERSIONS_DEPRECATED=1
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
Test versioned endpoints by setting the Accept-Version header:
|
||||
|
||||
```php
|
||||
$response = $this->withHeaders([
|
||||
'Accept-Version' => 'v2',
|
||||
])->getJson('/api/users');
|
||||
|
||||
$response->assertHeader('X-API-Version', '2');
|
||||
```
|
||||
|
||||
Or use URL prefix:
|
||||
|
||||
```php
|
||||
$response = $this->getJson('/api/v2/users');
|
||||
```
|
||||
|
|
@ -1,248 +0,0 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* Core PHP Framework
|
||||
*
|
||||
* Licensed under the European Union Public Licence (EUPL) v1.2.
|
||||
* See LICENSE file for details.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Front\Api;
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
/**
|
||||
* Versioned Routes Helper.
|
||||
*
|
||||
* Provides fluent helpers for registering version-based API routes.
|
||||
*
|
||||
* ## Basic Usage
|
||||
*
|
||||
* Register routes for a specific version:
|
||||
*
|
||||
* ```php
|
||||
* use Core\Front\Api\VersionedRoutes;
|
||||
*
|
||||
* VersionedRoutes::v1(function () {
|
||||
* Route::get('/users', [UserController::class, 'index']);
|
||||
* });
|
||||
*
|
||||
* VersionedRoutes::v2(function () {
|
||||
* Route::get('/users', [UserControllerV2::class, 'index']);
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* ## URL Prefixed Versions
|
||||
*
|
||||
* By default, routes are prefixed with the version (e.g., /api/v1/users):
|
||||
*
|
||||
* ```php
|
||||
* VersionedRoutes::v1(function () {
|
||||
* Route::get('/users', ...); // Accessible at /api/v1/users
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* ## Header-Only Versions
|
||||
*
|
||||
* For routes that use header-based versioning only:
|
||||
*
|
||||
* ```php
|
||||
* VersionedRoutes::version(1)
|
||||
* ->withoutPrefix()
|
||||
* ->routes(function () {
|
||||
* Route::get('/users', ...); // Accessible at /api/users with Accept-Version: 1
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* ## Multiple Versions
|
||||
*
|
||||
* Register the same routes for multiple versions:
|
||||
*
|
||||
* ```php
|
||||
* VersionedRoutes::versions([1, 2], function () {
|
||||
* Route::get('/status', [StatusController::class, 'index']);
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* ## Deprecation
|
||||
*
|
||||
* Mark a version as deprecated with custom sunset date:
|
||||
*
|
||||
* ```php
|
||||
* VersionedRoutes::v1()
|
||||
* ->deprecated('2025-06-01')
|
||||
* ->routes(function () {
|
||||
* Route::get('/legacy', ...);
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
class VersionedRoutes
|
||||
{
|
||||
protected int $version;
|
||||
|
||||
protected bool $usePrefix = true;
|
||||
|
||||
protected ?string $sunsetDate = null;
|
||||
|
||||
protected bool $isDeprecated = false;
|
||||
|
||||
/**
|
||||
* @var array<string>
|
||||
*/
|
||||
protected array $middleware = [];
|
||||
|
||||
/**
|
||||
* Create a new versioned routes instance.
|
||||
*/
|
||||
public function __construct(int $version)
|
||||
{
|
||||
$this->version = $version;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create routes for version 1.
|
||||
*/
|
||||
public static function v1(?callable $routes = null): static
|
||||
{
|
||||
$instance = new static(1);
|
||||
|
||||
if ($routes !== null) {
|
||||
$instance->routes($routes);
|
||||
}
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create routes for version 2.
|
||||
*/
|
||||
public static function v2(?callable $routes = null): static
|
||||
{
|
||||
$instance = new static(2);
|
||||
|
||||
if ($routes !== null) {
|
||||
$instance->routes($routes);
|
||||
}
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create routes for a specific version.
|
||||
*/
|
||||
public static function version(int $version): static
|
||||
{
|
||||
return new static($version);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register routes for multiple versions.
|
||||
*
|
||||
* @param array<int> $versions
|
||||
*/
|
||||
public static function versions(array $versions, callable $routes): void
|
||||
{
|
||||
foreach ($versions as $version) {
|
||||
(new static($version))->routes($routes);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Don't use URL prefix for this version.
|
||||
*
|
||||
* Routes will be accessible without /v{n} prefix but will
|
||||
* still require version header for version-specific behaviour.
|
||||
*/
|
||||
public function withoutPrefix(): static
|
||||
{
|
||||
$this->usePrefix = false;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use URL prefix for this version.
|
||||
*
|
||||
* This is the default behaviour.
|
||||
*/
|
||||
public function withPrefix(): static
|
||||
{
|
||||
$this->usePrefix = true;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark this version as deprecated.
|
||||
*
|
||||
* @param string|null $sunsetDate Optional sunset date (YYYY-MM-DD or RFC7231 format)
|
||||
*/
|
||||
public function deprecated(?string $sunsetDate = null): static
|
||||
{
|
||||
$this->isDeprecated = true;
|
||||
$this->sunsetDate = $sunsetDate;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add additional middleware to the version routes.
|
||||
*
|
||||
* @param array<string>|string $middleware
|
||||
*/
|
||||
public function middleware(array|string $middleware): static
|
||||
{
|
||||
$this->middleware = array_merge(
|
||||
$this->middleware,
|
||||
is_array($middleware) ? $middleware : [$middleware]
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the routes for this version.
|
||||
*/
|
||||
public function routes(callable $routes): void
|
||||
{
|
||||
$attributes = $this->buildRouteAttributes();
|
||||
|
||||
Route::group($attributes, $routes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the route group attributes.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected function buildRouteAttributes(): array
|
||||
{
|
||||
$attributes = [
|
||||
'middleware' => $this->buildMiddleware(),
|
||||
];
|
||||
|
||||
if ($this->usePrefix) {
|
||||
$attributes['prefix'] = "v{$this->version}";
|
||||
}
|
||||
|
||||
return $attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the middleware stack for this version.
|
||||
*
|
||||
* @return array<string>
|
||||
*/
|
||||
protected function buildMiddleware(): array
|
||||
{
|
||||
$middleware = ["api.version:{$this->version}"];
|
||||
|
||||
if ($this->isDeprecated && $this->sunsetDate) {
|
||||
$middleware[] = "api.sunset:{$this->sunsetDate}";
|
||||
}
|
||||
|
||||
return array_merge($middleware, $this->middleware);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,78 +0,0 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* Core PHP Framework
|
||||
*
|
||||
* Licensed under the European Union Public Licence (EUPL) v1.2.
|
||||
* See LICENSE file for details.
|
||||
*/
|
||||
|
||||
/**
|
||||
* API Configuration.
|
||||
*
|
||||
* Settings for the REST API infrastructure including versioning,
|
||||
* rate limiting, and deprecation handling.
|
||||
*/
|
||||
|
||||
return [
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| API Versioning
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Configure how API versions are handled. The middleware supports:
|
||||
| - URL path versioning: /api/v1/users
|
||||
| - Header versioning: Accept-Version: v1
|
||||
| - Accept header: application/vnd.hosthub.v1+json
|
||||
|
|
||||
| Version Strategy:
|
||||
| - Add new fields to existing versions (backwards compatible)
|
||||
| - Use new version for breaking changes (removing/renaming fields)
|
||||
| - Deprecate old versions with sunset dates before removal
|
||||
|
|
||||
*/
|
||||
'versioning' => [
|
||||
// Default version when no version specified in request
|
||||
// Clients should always specify version explicitly
|
||||
'default' => (int) env('API_VERSION_DEFAULT', 1),
|
||||
|
||||
// Current/latest API version
|
||||
// Used in deprecation warnings to suggest upgrade path
|
||||
'current' => (int) env('API_VERSION_CURRENT', 1),
|
||||
|
||||
// Supported API versions (all still functional)
|
||||
// Remove versions from this list to disable them entirely
|
||||
'supported' => array_map('intval', array_filter(
|
||||
explode(',', env('API_VERSIONS_SUPPORTED', '1'))
|
||||
)),
|
||||
|
||||
// Deprecated versions (still work but warn clients)
|
||||
// Responses include Deprecation: true header
|
||||
'deprecated' => array_map('intval', array_filter(
|
||||
explode(',', env('API_VERSIONS_DEPRECATED', ''))
|
||||
)),
|
||||
|
||||
// Sunset dates for deprecated versions
|
||||
// Format: [version => 'YYYY-MM-DD']
|
||||
// After this date, version should be removed from 'supported'
|
||||
'sunset' => [
|
||||
// Example: 1 => '2025-12-31',
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Response Headers
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Standard headers added to API responses.
|
||||
|
|
||||
*/
|
||||
'headers' => [
|
||||
// Add X-API-Version header to all responses
|
||||
'include_version' => true,
|
||||
|
||||
// Add deprecation warnings for old versions
|
||||
'include_deprecation' => true,
|
||||
],
|
||||
];
|
||||
|
|
@ -17,15 +17,17 @@ use Illuminate\Support\AggregateServiceProvider;
|
|||
/**
|
||||
* Core front-end module - I/O translation layer.
|
||||
*
|
||||
* Seven frontages, each translating a transport protocol:
|
||||
* Six frontages bundled in the framework, each translating a transport protocol:
|
||||
* Web - HTTP → HTML (public marketing)
|
||||
* Client - HTTP → HTML (namespace owner dashboard)
|
||||
* Admin - HTTP → HTML (backend admin dashboard)
|
||||
* Api - HTTP → JSON (REST API)
|
||||
* Mcp - HTTP → JSON-RPC (MCP protocol)
|
||||
* Cli - Artisan commands (console context)
|
||||
* Stdio - stdin/stdout (CLI pipes, MCP stdio)
|
||||
* Components - View namespaces (shared across HTTP frontages)
|
||||
*
|
||||
* Additional frontages provided by their packages (auto-discovered):
|
||||
* Api - HTTP → JSON (REST API) — php-api
|
||||
* Mcp - HTTP → JSON-RPC (MCP protocol) — php-mcp
|
||||
*/
|
||||
class Boot extends AggregateServiceProvider
|
||||
{
|
||||
|
|
@ -33,8 +35,6 @@ class Boot extends AggregateServiceProvider
|
|||
Web\Boot::class,
|
||||
Client\Boot::class,
|
||||
Admin\Boot::class,
|
||||
Api\Boot::class,
|
||||
Mcp\Boot::class,
|
||||
Cli\Boot::class,
|
||||
Stdio\Boot::class,
|
||||
Components\Boot::class,
|
||||
|
|
@ -49,7 +49,17 @@ class Boot extends AggregateServiceProvider
|
|||
Web\Boot::middleware($middleware);
|
||||
Client\Boot::middleware($middleware);
|
||||
Admin\Boot::middleware($middleware);
|
||||
Api\Boot::middleware($middleware);
|
||||
Mcp\Boot::middleware($middleware);
|
||||
|
||||
// API and MCP groups — inlined because middleware() runs during
|
||||
// Application::configure(), before package providers load.
|
||||
// Packages add their own aliases during boot via lifecycle events.
|
||||
$middleware->group('api', [
|
||||
\Illuminate\Routing\Middleware\ThrottleRequests::class.':api',
|
||||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||
]);
|
||||
$middleware->group('mcp', [
|
||||
\Illuminate\Routing\Middleware\ThrottleRequests::class.':api',
|
||||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,87 +0,0 @@
|
|||
@php
|
||||
$appName = config('core.app.name', 'Core PHP');
|
||||
$appUrl = config('app.url', 'https://core.test');
|
||||
$privacyUrl = config('core.urls.privacy', '/privacy');
|
||||
$termsUrl = config('core.urls.terms', '/terms');
|
||||
@endphp
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
|
||||
<title>{{ $title ?? 'MCP Portal' }} - {{ $appName }}</title>
|
||||
<meta name="description" content="{{ $description ?? 'Connect AI agents via Model Context Protocol' }}">
|
||||
|
||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||
@fluxAppearance
|
||||
</head>
|
||||
<body class="min-h-screen bg-white dark:bg-zinc-900">
|
||||
<!-- Header -->
|
||||
<header class="border-b border-zinc-200 dark:border-zinc-800">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex items-center justify-between h-16">
|
||||
<div class="flex items-center space-x-8">
|
||||
<a href="{{ route('mcp.landing') }}" class="flex items-center space-x-2">
|
||||
<span class="text-xl font-bold text-zinc-900 dark:text-white">MCP Portal</span>
|
||||
</a>
|
||||
<nav class="hidden md:flex items-center space-x-6 text-sm">
|
||||
<a href="{{ route('mcp.servers.index') }}" class="text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-white">
|
||||
Servers
|
||||
</a>
|
||||
<a href="{{ route('mcp.connect') }}" class="text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-white">
|
||||
Setup Guide
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
@php
|
||||
$workspace = request()->attributes->get('mcp_workspace');
|
||||
@endphp
|
||||
@if($workspace)
|
||||
<a href="{{ route('mcp.dashboard') }}" class="text-sm text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-white">
|
||||
Dashboard
|
||||
</a>
|
||||
<a href="{{ route('mcp.keys') }}" class="text-sm text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-white">
|
||||
API Keys
|
||||
</a>
|
||||
<span class="text-sm text-zinc-500 dark:text-zinc-400">
|
||||
{{ $workspace->name }}
|
||||
</span>
|
||||
@else
|
||||
<a href="{{ route('login') }}" class="text-sm text-cyan-600 hover:text-cyan-700 dark:text-cyan-400 dark:hover:text-cyan-300">
|
||||
Sign in
|
||||
</a>
|
||||
@endif
|
||||
<a href="{{ $appUrl }}" class="text-sm text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-white">
|
||||
← {{ $appName }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
{{ $slot }}
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="border-t border-zinc-200 dark:border-zinc-800 mt-auto">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-sm text-zinc-500 dark:text-zinc-400">
|
||||
© {{ date('Y') }} {{ $appName }}. All rights reserved.
|
||||
</p>
|
||||
<div class="flex items-center space-x-6 text-sm text-zinc-500 dark:text-zinc-400">
|
||||
<a href="{{ $privacyUrl }}" class="hover:text-zinc-900 dark:hover:text-white">Privacy</a>
|
||||
<a href="{{ $termsUrl }}" class="hover:text-zinc-900 dark:hover:text-white">Terms</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@fluxScripts
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* Core PHP Framework
|
||||
*
|
||||
* Licensed under the European Union Public Licence (EUPL) v1.2.
|
||||
* See LICENSE file for details.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Front\Mcp;
|
||||
|
||||
use Core\LifecycleEventProvider;
|
||||
use Illuminate\Foundation\Configuration\Middleware;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
/**
|
||||
* MCP frontage - MCP API stage.
|
||||
*
|
||||
* Provides mcp middleware group for MCP protocol routes.
|
||||
* Authentication middleware should be added by the core-mcp package.
|
||||
*/
|
||||
class Boot extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Configure mcp middleware group.
|
||||
*/
|
||||
public static function middleware(Middleware $middleware): void
|
||||
{
|
||||
$middleware->group('mcp', [
|
||||
\Illuminate\Routing\Middleware\ThrottleRequests::class.':api',
|
||||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||
]);
|
||||
}
|
||||
|
||||
public function register(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
public function boot(): void
|
||||
{
|
||||
// Fire McpRoutesRegistering event for lazy-loaded modules
|
||||
LifecycleEventProvider::fireMcpRoutes();
|
||||
|
||||
// Fire McpToolsRegistering so modules can register tool handlers
|
||||
LifecycleEventProvider::fireMcpTools();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* Core PHP Framework
|
||||
*
|
||||
* Licensed under the European Union Public Licence (EUPL) v1.2.
|
||||
* See LICENSE file for details.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Front\Mcp\Contracts;
|
||||
|
||||
use Core\Front\Mcp\McpContext;
|
||||
|
||||
/**
|
||||
* Interface for MCP tool handlers.
|
||||
*
|
||||
* Each MCP tool is implemented as a handler class that provides:
|
||||
* - A JSON schema describing the tool for Claude
|
||||
* - A handle method that processes tool invocations
|
||||
*
|
||||
* Tool handlers are registered via the McpToolsRegistering event
|
||||
* and can be used by both stdio and HTTP MCP transports.
|
||||
*/
|
||||
interface McpToolHandler
|
||||
{
|
||||
/**
|
||||
* Get the JSON schema describing this tool.
|
||||
*
|
||||
* The schema follows the MCP tool specification:
|
||||
* - name: Tool identifier (snake_case)
|
||||
* - description: What the tool does (for Claude)
|
||||
* - inputSchema: JSON Schema for parameters
|
||||
*
|
||||
* @return array{name: string, description: string, inputSchema: array}
|
||||
*/
|
||||
public static function schema(): array;
|
||||
|
||||
/**
|
||||
* Handle a tool invocation.
|
||||
*
|
||||
* @param array $args Arguments from the tool call
|
||||
* @param McpContext $context Server context (session, notifications, etc.)
|
||||
* @return array Result to return to Claude
|
||||
*/
|
||||
public function handle(array $args, McpContext $context): array;
|
||||
}
|
||||
|
|
@ -1,133 +0,0 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* Core PHP Framework
|
||||
*
|
||||
* Licensed under the European Union Public Licence (EUPL) v1.2.
|
||||
* See LICENSE file for details.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Front\Mcp;
|
||||
|
||||
use Closure;
|
||||
|
||||
/**
|
||||
* Context object passed to MCP tool handlers.
|
||||
*
|
||||
* Abstracts the transport layer (stdio vs HTTP) so tool handlers
|
||||
* can work with either transport without modification.
|
||||
*
|
||||
* Provides access to:
|
||||
* - Current session tracking
|
||||
* - Current plan context
|
||||
* - Notification sending
|
||||
* - Session logging
|
||||
*/
|
||||
class McpContext
|
||||
{
|
||||
/**
|
||||
* @param object|null $currentPlan AgentPlan model instance when Agentic module installed
|
||||
*/
|
||||
public function __construct(
|
||||
private ?string $sessionId = null,
|
||||
private ?object $currentPlan = null,
|
||||
private ?Closure $notificationCallback = null,
|
||||
private ?Closure $logCallback = null,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get the current session ID if one is active.
|
||||
*/
|
||||
public function getSessionId(): ?string
|
||||
{
|
||||
return $this->sessionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current session ID.
|
||||
*/
|
||||
public function setSessionId(?string $sessionId): void
|
||||
{
|
||||
$this->sessionId = $sessionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current plan if one is active.
|
||||
*
|
||||
* @return object|null AgentPlan model instance when Agentic module installed
|
||||
*/
|
||||
public function getCurrentPlan(): ?object
|
||||
{
|
||||
return $this->currentPlan;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current plan.
|
||||
*
|
||||
* @param object|null $plan AgentPlan model instance
|
||||
*/
|
||||
public function setCurrentPlan(?object $plan): void
|
||||
{
|
||||
$this->currentPlan = $plan;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an MCP notification to the client.
|
||||
*
|
||||
* Notifications are one-way messages that don't expect a response.
|
||||
* Common notifications include progress updates, log messages, etc.
|
||||
*/
|
||||
public function sendNotification(string $method, array $params = []): void
|
||||
{
|
||||
if ($this->notificationCallback) {
|
||||
($this->notificationCallback)($method, $params);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a message to the current session.
|
||||
*
|
||||
* Messages are recorded in the session log for handoff context
|
||||
* and audit trail purposes.
|
||||
*/
|
||||
public function logToSession(string $message, string $type = 'info', array $data = []): void
|
||||
{
|
||||
if ($this->logCallback) {
|
||||
($this->logCallback)($message, $type, $data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the notification callback.
|
||||
*/
|
||||
public function setNotificationCallback(?Closure $callback): void
|
||||
{
|
||||
$this->notificationCallback = $callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the log callback.
|
||||
*/
|
||||
public function setLogCallback(?Closure $callback): void
|
||||
{
|
||||
$this->logCallback = $callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a session is currently active.
|
||||
*/
|
||||
public function hasSession(): bool
|
||||
{
|
||||
return $this->sessionId !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a plan is currently active.
|
||||
*/
|
||||
public function hasPlan(): bool
|
||||
{
|
||||
return $this->currentPlan !== null;
|
||||
}
|
||||
}
|
||||
|
|
@ -73,7 +73,7 @@ use Livewire\Livewire;
|
|||
* │ Processes: views, translations, livewire, routes │
|
||||
* │ ('admin' middleware) │
|
||||
* │ │
|
||||
* ├─── Front/Api/Boot ────────────────────────────────────────────────── │
|
||||
* ├─── Front/Api/Boot (php-api package) ─────────────────────────────── │
|
||||
* │ └── LifecycleEventProvider::fireApiRoutes() │
|
||||
* │ Fires: ApiRoutesRegistering │
|
||||
* │ Processes: routes ('api' middleware) │
|
||||
|
|
@ -88,7 +88,7 @@ use Livewire\Livewire;
|
|||
* │ Fires: ConsoleBooting │
|
||||
* │ Processes: command classes │
|
||||
* │ │
|
||||
* └─── Front/Mcp/Boot ────────────────────────────────────────────────── │
|
||||
* └─── Front/Mcp/Boot (php-mcp package) ─────────────────────────────── │
|
||||
* ├── LifecycleEventProvider::fireMcpRoutes() │
|
||||
* │ Fires: McpRoutesRegistering │
|
||||
* │ Processes: routes ('mcp' middleware) │
|
||||
|
|
@ -456,7 +456,7 @@ class LifecycleEventProvider extends ServiceProvider
|
|||
*
|
||||
* @return array<string> Fully qualified class names of McpToolHandler implementations
|
||||
*
|
||||
* @see \Core\Front\Mcp\Contracts\McpToolHandler
|
||||
* @see \Core\Front\Mcp\Contracts\McpToolHandler (in php-mcp package)
|
||||
*/
|
||||
public static function fireMcpTools(): array
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue