2025-12-14 23:10:01 +00:00
|
|
|
<?php
|
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
|
|
|
|
namespace Phred\Http;
|
|
|
|
|
|
|
|
|
|
use DI\Container;
|
|
|
|
|
use DI\ContainerBuilder;
|
|
|
|
|
use FastRoute\Dispatcher;
|
|
|
|
|
use FastRoute\RouteCollector;
|
|
|
|
|
use Nyholm\Psr7\Factory\Psr17Factory;
|
|
|
|
|
use Psr\Http\Message\ResponseInterface;
|
|
|
|
|
use Psr\Http\Message\ServerRequestInterface as ServerRequest;
|
|
|
|
|
use Relay\Relay;
|
|
|
|
|
|
|
|
|
|
use function FastRoute\simpleDispatcher;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Core HTTP Kernel builds container, routes, and PSR-15 pipeline and processes requests.
|
|
|
|
|
*/
|
|
|
|
|
final class Kernel
|
|
|
|
|
{
|
|
|
|
|
private Container $container;
|
|
|
|
|
private Dispatcher $dispatcher;
|
|
|
|
|
|
|
|
|
|
public function __construct(?Container $container = null, ?Dispatcher $dispatcher = null)
|
|
|
|
|
{
|
|
|
|
|
$this->container = $container ?? $this->buildContainer();
|
Implement M5 service providers, M6 MVC bases, and URL extension negotiation; update docs and tests
• M5: Add ServiceProviderInterface and ProviderRepository; integrate providers into Kernel (register before container build, boot after); add RouteRegistry with clear(); add default core providers (Routing, Template, ORM, Flags, Testing) and AppServiceProvider; add contracts and default drivers (Template/Eyrie, Orm/Pairity, Flags/Flagpole, Testing/Codeception)
• Routing: allow providers to contribute routes; add ProviderRouteTest
• Config: add config/providers.php; extend config/app.php with driver keys; document env keys
• M6: Introduce MVC bases: Controller, APIController (JSON helpers), ViewController (html + renderView helpers), View (transformData + renderer); add ViewWithDefaultTemplate and default-template flow; adjust method signatures to data-first and delegate template override to View
• HTTP: Add UrlExtensionNegotiationMiddleware (opt-in via URL_EXTENSION_NEGOTIATION, whitelist via URL_EXTENSION_WHITELIST with default json|php|none); wire before ContentNegotiationMiddleware
• Tests: add UrlExtensionNegotiationTest and MvcViewTest; ensure RouteRegistry::clear prevents duplicate routes in tests
• Docs: Update README with M5 provider usage, M6 MVC examples and template selection conventions, and URL extension negotiation; mark M5 complete in MILESTONES; add M12 task to provide XML support and enable xml in whitelist by default
2025-12-15 22:08:57 +00:00
|
|
|
// Providers may contribute routes during boot; ensure dispatcher is built after container init
|
2025-12-14 23:10:01 +00:00
|
|
|
$this->dispatcher = $dispatcher ?? $this->buildDispatcher();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function container(): Container
|
|
|
|
|
{
|
|
|
|
|
return $this->container;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function dispatcher(): Dispatcher
|
|
|
|
|
{
|
|
|
|
|
return $this->dispatcher;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function handle(ServerRequest $request): ResponseInterface
|
|
|
|
|
{
|
|
|
|
|
$psr17 = new Psr17Factory();
|
2025-12-22 21:52:41 +00:00
|
|
|
$config = $this->container->get(\Phred\Support\Contracts\ConfigInterface::class);
|
|
|
|
|
|
|
|
|
|
// CORS
|
|
|
|
|
$corsSettings = new \Neomerx\Cors\Strategies\Settings();
|
|
|
|
|
$corsSettings->init(
|
|
|
|
|
parse_url((string)getenv('APP_URL'), PHP_URL_SCHEME) ?: 'http',
|
|
|
|
|
parse_url((string)getenv('APP_URL'), PHP_URL_HOST) ?: 'localhost',
|
|
|
|
|
(int)parse_url((string)getenv('APP_URL'), PHP_URL_PORT) ?: 80
|
|
|
|
|
);
|
|
|
|
|
$corsSettings->setAllowedOrigins($config->get('cors.origin', ['*']));
|
|
|
|
|
$corsSettings->setAllowedMethods($config->get('cors.methods', ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']));
|
|
|
|
|
$corsSettings->setAllowedHeaders($config->get('cors.headers.allow', ['Content-Type', 'Accept', 'Authorization', 'X-Requested-With']));
|
|
|
|
|
$corsSettings->enableAllOriginsAllowed();
|
|
|
|
|
$corsSettings->enableAllMethodsAllowed();
|
|
|
|
|
$corsSettings->enableAllHeadersAllowed();
|
|
|
|
|
|
2025-12-22 22:04:15 +00:00
|
|
|
$middleware = [];
|
|
|
|
|
if (filter_var($config->get('APP_DEBUG', false), FILTER_VALIDATE_BOOLEAN)) {
|
|
|
|
|
$middleware[] = new class extends Middleware\Middleware {
|
|
|
|
|
public function process(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Server\RequestHandlerInterface $handler): \Psr\Http\Message\ResponseInterface
|
|
|
|
|
{
|
|
|
|
|
self::$timings = []; // Reset timings for each request in debug mode
|
|
|
|
|
$response = $handler->handle($request);
|
|
|
|
|
$timings = self::getTimings();
|
|
|
|
|
if (!empty($timings)) {
|
|
|
|
|
$encoded = json_encode($timings, JSON_UNESCAPED_SLASHES);
|
|
|
|
|
if ($encoded) {
|
|
|
|
|
$response = $response->withHeader('X-Phred-Timings', $encoded);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return $response;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$middleware = array_merge($middleware, [
|
2025-12-22 21:52:41 +00:00
|
|
|
// Security headers
|
|
|
|
|
new Middleware\Security\SecureHeadersMiddleware($config),
|
|
|
|
|
// CORS
|
|
|
|
|
new \Middlewares\Cors(\Neomerx\Cors\Analyzer::instance($corsSettings)),
|
2025-12-15 15:15:49 +00:00
|
|
|
new Middleware\ProblemDetailsMiddleware(
|
2025-12-22 22:04:15 +00:00
|
|
|
filter_var($config->get('APP_DEBUG', 'false'), FILTER_VALIDATE_BOOLEAN),
|
2025-12-15 15:15:49 +00:00
|
|
|
null,
|
|
|
|
|
null,
|
2025-12-22 21:52:41 +00:00
|
|
|
filter_var($config->get('API_PROBLEM_DETAILS', 'true'), FILTER_VALIDATE_BOOLEAN)
|
2025-12-15 15:15:49 +00:00
|
|
|
),
|
Implement M5 service providers, M6 MVC bases, and URL extension negotiation; update docs and tests
• M5: Add ServiceProviderInterface and ProviderRepository; integrate providers into Kernel (register before container build, boot after); add RouteRegistry with clear(); add default core providers (Routing, Template, ORM, Flags, Testing) and AppServiceProvider; add contracts and default drivers (Template/Eyrie, Orm/Pairity, Flags/Flagpole, Testing/Codeception)
• Routing: allow providers to contribute routes; add ProviderRouteTest
• Config: add config/providers.php; extend config/app.php with driver keys; document env keys
• M6: Introduce MVC bases: Controller, APIController (JSON helpers), ViewController (html + renderView helpers), View (transformData + renderer); add ViewWithDefaultTemplate and default-template flow; adjust method signatures to data-first and delegate template override to View
• HTTP: Add UrlExtensionNegotiationMiddleware (opt-in via URL_EXTENSION_NEGOTIATION, whitelist via URL_EXTENSION_WHITELIST with default json|php|none); wire before ContentNegotiationMiddleware
• Tests: add UrlExtensionNegotiationTest and MvcViewTest; ensure RouteRegistry::clear prevents duplicate routes in tests
• Docs: Update README with M5 provider usage, M6 MVC examples and template selection conventions, and URL extension negotiation; mark M5 complete in MILESTONES; add M12 task to provide XML support and enable xml in whitelist by default
2025-12-15 22:08:57 +00:00
|
|
|
// Perform extension-based content negotiation hinting before standard negotiation
|
|
|
|
|
new Middleware\UrlExtensionNegotiationMiddleware(),
|
2025-12-15 15:15:49 +00:00
|
|
|
new Middleware\ContentNegotiationMiddleware(),
|
2025-12-14 23:10:01 +00:00
|
|
|
new Middleware\RoutingMiddleware($this->dispatcher, $psr17),
|
2025-12-15 15:15:49 +00:00
|
|
|
new Middleware\DispatchMiddleware($psr17),
|
2025-12-22 22:04:15 +00:00
|
|
|
]);
|
|
|
|
|
|
2025-12-14 23:10:01 +00:00
|
|
|
$relay = new Relay($middleware);
|
|
|
|
|
return $relay->handle($request);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function buildContainer(): Container
|
|
|
|
|
{
|
|
|
|
|
$builder = new ContainerBuilder();
|
Implement M5 service providers, M6 MVC bases, and URL extension negotiation; update docs and tests
• M5: Add ServiceProviderInterface and ProviderRepository; integrate providers into Kernel (register before container build, boot after); add RouteRegistry with clear(); add default core providers (Routing, Template, ORM, Flags, Testing) and AppServiceProvider; add contracts and default drivers (Template/Eyrie, Orm/Pairity, Flags/Flagpole, Testing/Codeception)
• Routing: allow providers to contribute routes; add ProviderRouteTest
• Config: add config/providers.php; extend config/app.php with driver keys; document env keys
• M6: Introduce MVC bases: Controller, APIController (JSON helpers), ViewController (html + renderView helpers), View (transformData + renderer); add ViewWithDefaultTemplate and default-template flow; adjust method signatures to data-first and delegate template override to View
• HTTP: Add UrlExtensionNegotiationMiddleware (opt-in via URL_EXTENSION_NEGOTIATION, whitelist via URL_EXTENSION_WHITELIST with default json|php|none); wire before ContentNegotiationMiddleware
• Tests: add UrlExtensionNegotiationTest and MvcViewTest; ensure RouteRegistry::clear prevents duplicate routes in tests
• Docs: Update README with M5 provider usage, M6 MVC examples and template selection conventions, and URL extension negotiation; mark M5 complete in MILESTONES; add M12 task to provide XML support and enable xml in whitelist by default
2025-12-15 22:08:57 +00:00
|
|
|
|
|
|
|
|
// Allow service providers to register definitions before defaults
|
|
|
|
|
$configAdapter = new \Phred\Support\DefaultConfig();
|
|
|
|
|
$providers = new \Phred\Support\ProviderRepository($configAdapter);
|
|
|
|
|
$providers->load();
|
|
|
|
|
$providers->registerAll($builder);
|
|
|
|
|
|
|
|
|
|
// Add core definitions/bindings
|
2025-12-15 15:15:49 +00:00
|
|
|
$builder->addDefinitions([
|
|
|
|
|
\Phred\Support\Contracts\ConfigInterface::class => \DI\autowire(\Phred\Support\DefaultConfig::class),
|
|
|
|
|
\Phred\Http\Contracts\ErrorFormatNegotiatorInterface::class => \DI\autowire(\Phred\Http\Support\DefaultErrorFormatNegotiator::class),
|
|
|
|
|
\Phred\Http\Contracts\RequestIdProviderInterface::class => \DI\autowire(\Phred\Http\Support\DefaultRequestIdProvider::class),
|
|
|
|
|
\Phred\Http\Contracts\ExceptionToStatusMapperInterface::class => \DI\autowire(\Phred\Http\Support\DefaultExceptionToStatusMapper::class),
|
|
|
|
|
\Phred\Http\Contracts\ApiResponseFactoryInterface::class => \DI\autowire(\Phred\Http\Responses\DelegatingApiResponseFactory::class),
|
|
|
|
|
\Phred\Http\Responses\RestResponseFactory::class => \DI\autowire(\Phred\Http\Responses\RestResponseFactory::class),
|
|
|
|
|
\Phred\Http\Responses\JsonApiResponseFactory::class => \DI\autowire(\Phred\Http\Responses\JsonApiResponseFactory::class),
|
2025-12-23 23:40:02 +00:00
|
|
|
\Phred\Http\Responses\XmlResponseFactory::class => \DI\autowire(\Phred\Http\Responses\XmlResponseFactory::class),
|
2025-12-15 15:15:49 +00:00
|
|
|
]);
|
Implement M5 service providers, M6 MVC bases, and URL extension negotiation; update docs and tests
• M5: Add ServiceProviderInterface and ProviderRepository; integrate providers into Kernel (register before container build, boot after); add RouteRegistry with clear(); add default core providers (Routing, Template, ORM, Flags, Testing) and AppServiceProvider; add contracts and default drivers (Template/Eyrie, Orm/Pairity, Flags/Flagpole, Testing/Codeception)
• Routing: allow providers to contribute routes; add ProviderRouteTest
• Config: add config/providers.php; extend config/app.php with driver keys; document env keys
• M6: Introduce MVC bases: Controller, APIController (JSON helpers), ViewController (html + renderView helpers), View (transformData + renderer); add ViewWithDefaultTemplate and default-template flow; adjust method signatures to data-first and delegate template override to View
• HTTP: Add UrlExtensionNegotiationMiddleware (opt-in via URL_EXTENSION_NEGOTIATION, whitelist via URL_EXTENSION_WHITELIST with default json|php|none); wire before ContentNegotiationMiddleware
• Tests: add UrlExtensionNegotiationTest and MvcViewTest; ensure RouteRegistry::clear prevents duplicate routes in tests
• Docs: Update README with M5 provider usage, M6 MVC examples and template selection conventions, and URL extension negotiation; mark M5 complete in MILESTONES; add M12 task to provide XML support and enable xml in whitelist by default
2025-12-15 22:08:57 +00:00
|
|
|
$container = $builder->build();
|
|
|
|
|
|
|
|
|
|
// Reset provider-registered routes to avoid duplicates across multiple kernel instantiations (e.g., tests)
|
|
|
|
|
\Phred\Http\Routing\RouteRegistry::clear();
|
|
|
|
|
// Boot providers after container is available
|
|
|
|
|
$providers->bootAll($container);
|
|
|
|
|
|
|
|
|
|
return $container;
|
2025-12-14 23:10:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function buildDispatcher(): Dispatcher
|
|
|
|
|
{
|
|
|
|
|
$routesPath = dirname(__DIR__, 2) . '/routes';
|
|
|
|
|
$collector = static function (RouteCollector $r) use ($routesPath): void {
|
|
|
|
|
// Load user-defined routes if present
|
|
|
|
|
$router = new Router($r);
|
|
|
|
|
foreach (['web.php', 'api.php'] as $file) {
|
|
|
|
|
$path = $routesPath . '/' . $file;
|
|
|
|
|
if (is_file($path)) {
|
|
|
|
|
/** @noinspection PhpIncludeInspection */
|
|
|
|
|
(static function ($router) use ($path) { require $path; })($router);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
Refactor M7 module scaffolding, route inclusion, and tests; implement providers discovery; fix URL extension negotiation; clean docs
• Add Service Providers loading from config/providers.php and merge with runtime config; ensure AppServiceProvider boots and contributes routes
• Create RouteGroups and guard module route includes in routes/web.php; update Kernel to auto-mount module routes and apply provider routes
• Implement create:module as a console Command (extends Phred\Console\Command):
◦ Args: name, prefix; Flags: --update-composer, --no-dump
◦ Stable root resolution (dirname(DIR, 2)); robust args/flags handling under ArrayInput
◦ Scaffolds module dirs (Controllers, Views, Templates, Routes, Providers, etc.), ensures Controllers exists, adds .gitkeep
◦ Writes Provider, View, Controller, Template stubs (fix variable interpolation via placeholders)
◦ Appends guarded include snippet to routes/web.php
◦ Optional composer PSR-4 mapping update (+ backup) and optional autoload dump
◦ Prevents providers.php corruption via name validation and existence checks
• Add URL extension negotiation middleware tweaks:
◦ Only set Accept for .json (and future .xml), never for none/php
◦ Never override existing Accept header
• Add MVC base classes (Controller, APIController, ViewController, View, ViewWithDefaultTemplate); update ViewController signature and View render contract
• Add tests:
◦ CreateModuleCommandTest with setup/teardown to snapshot/restore routes/web.php and composer.json; asserts scaffold and PSR-4 mapping
◦ ProviderRouteTest for provider-contributed route
◦ UrlExtensionNegotiationTest sets API_FORMAT=rest and asserts content-type behavior
◦ MvcViewTest validates transformData+render
• Fix config/providers.php syntax and add comment placeholder for modules
• Update README: M5/M6/M7 docs, MVC examples, template selection conventions, modules section, URL extension negotiation, and module creation workflow
• Update MILESTONES.md: mark M6/M7 complete; add M8 task for register:orm; note M12 XML extension support
2025-12-16 22:14:22 +00:00
|
|
|
// Load module route files under prefixes defined in routes/web.php via RouteGroups includes.
|
|
|
|
|
// Additionally, as a convenience, auto-mount modules without explicit includes using folder name as prefix.
|
|
|
|
|
$modulesDir = dirname(__DIR__, 2) . '/modules';
|
|
|
|
|
if (is_dir($modulesDir)) {
|
|
|
|
|
$entries = array_values(array_filter(scandir($modulesDir) ?: [], static fn($e) => $e !== '.' && $e !== '..'));
|
|
|
|
|
sort($entries, SORT_STRING);
|
|
|
|
|
foreach ($entries as $mod) {
|
|
|
|
|
$modRoutes = $modulesDir . '/' . $mod . '/Routes';
|
|
|
|
|
if (!is_dir($modRoutes)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
// Auto-mount only if the module's web.php wasn't already included via RouteGroups in root file.
|
|
|
|
|
$autoInclude = function (string $relative, string $prefix) use ($modRoutes, $router): void {
|
|
|
|
|
$file = $modRoutes . '/' . $relative;
|
|
|
|
|
if (is_file($file)) {
|
|
|
|
|
$router->group('/' . strtolower($prefix), static function (Router $r) use ($file): void {
|
|
|
|
|
/** @noinspection PhpIncludeInspection */
|
|
|
|
|
(static function ($router) use ($file) { require $file; })($r);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
$autoInclude('web.php', $mod);
|
|
|
|
|
// api.php can be auto-mounted under /api/<module>
|
|
|
|
|
$apiFile = $modRoutes . '/api.php';
|
|
|
|
|
if (is_file($apiFile)) {
|
|
|
|
|
$router->group('/api/' . strtolower($mod), static function (Router $r) use ($apiFile): void {
|
|
|
|
|
/** @noinspection PhpIncludeInspection */
|
|
|
|
|
(static function ($router) use ($apiFile) { require $apiFile; })($r);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
Implement M5 service providers, M6 MVC bases, and URL extension negotiation; update docs and tests
• M5: Add ServiceProviderInterface and ProviderRepository; integrate providers into Kernel (register before container build, boot after); add RouteRegistry with clear(); add default core providers (Routing, Template, ORM, Flags, Testing) and AppServiceProvider; add contracts and default drivers (Template/Eyrie, Orm/Pairity, Flags/Flagpole, Testing/Codeception)
• Routing: allow providers to contribute routes; add ProviderRouteTest
• Config: add config/providers.php; extend config/app.php with driver keys; document env keys
• M6: Introduce MVC bases: Controller, APIController (JSON helpers), ViewController (html + renderView helpers), View (transformData + renderer); add ViewWithDefaultTemplate and default-template flow; adjust method signatures to data-first and delegate template override to View
• HTTP: Add UrlExtensionNegotiationMiddleware (opt-in via URL_EXTENSION_NEGOTIATION, whitelist via URL_EXTENSION_WHITELIST with default json|php|none); wire before ContentNegotiationMiddleware
• Tests: add UrlExtensionNegotiationTest and MvcViewTest; ensure RouteRegistry::clear prevents duplicate routes in tests
• Docs: Update README with M5 provider usage, M6 MVC examples and template selection conventions, and URL extension negotiation; mark M5 complete in MILESTONES; add M12 task to provide XML support and enable xml in whitelist by default
2025-12-15 22:08:57 +00:00
|
|
|
// Allow providers to contribute routes
|
|
|
|
|
\Phred\Http\Routing\RouteRegistry::apply($r, $router);
|
|
|
|
|
|
2025-12-15 15:15:49 +00:00
|
|
|
// Ensure default demo routes exist for acceptance/demo
|
2025-12-14 23:10:01 +00:00
|
|
|
$r->addRoute('GET', '/_phred/health', [Controllers\HealthController::class, '__invoke']);
|
2025-12-15 15:15:49 +00:00
|
|
|
$r->addRoute('GET', '/_phred/format', [Controllers\FormatController::class, '__invoke']);
|
2025-12-14 23:10:01 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return simpleDispatcher($collector);
|
|
|
|
|
}
|
|
|
|
|
}
|