From a55d1c3c2e414b5b7015382035e85281d5b3a758 Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 7 Aug 2023 17:50:34 +0200 Subject: [PATCH] Properly implement the router with config and interfaces Expand the container with aliases and argument autowiring --- app/Controller/BaseController.php | 2 +- composer.json | 5 +- composer.lock | 239 +++++++++++++++++++++- public/index.php | 6 + src/Collector/ClassAttributeCollector.php | 18 +- src/Collector/RouteCollector.php | 36 ++++ src/Container/ContainerInterface.php | 22 ++ src/Container/GenericContainer.php | 49 ++++- src/Http/GenericRouter.php | 26 +-- src/Http/RouterConfig.php | 18 ++ src/Kernel/BaseKernel.php | 33 ++- 11 files changed, 406 insertions(+), 48 deletions(-) create mode 100644 src/Collector/RouteCollector.php create mode 100644 src/Container/ContainerInterface.php create mode 100644 src/Http/RouterConfig.php diff --git a/app/Controller/BaseController.php b/app/Controller/BaseController.php index 7df9387..8e24148 100644 --- a/app/Controller/BaseController.php +++ b/app/Controller/BaseController.php @@ -9,7 +9,7 @@ use Ardent\Undercurrent\Http\ResponseInterface; class BaseController { #[Route('/hello')] - public function HelloWorld(): ResponseInterface + public function helloWorld(): ResponseInterface { return new GenericResponse('Hello World!'); } diff --git a/composer.json b/composer.json index 28e752d..3f0d399 100644 --- a/composer.json +++ b/composer.json @@ -11,5 +11,8 @@ "App\\": "app/" } }, - "license": "GPL-3.0-only" + "license": "GPL-3.0-only", + "require-dev": { + "symfony/var-dumper": "^6.3" + } } diff --git a/composer.lock b/composer.lock index 82e2d25..a5d0861 100644 --- a/composer.lock +++ b/composer.lock @@ -4,9 +4,244 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "04c6989f1bd672a6accd35142e2adac1", + "content-hash": "63924b42ed7cdd90434b1da20abb9381", "packages": [], - "packages-dev": [], + "packages-dev": [ + { + "name": "symfony/deprecation-contracts", + "version": "v3.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/7c3aff79d10325257a001fcf92d991f24fc967cf", + "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.4-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.3.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-05-23T14:45:45+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.27.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/8ad114f6b39e2c98a8b0e3bd907732c207c2b534", + "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.27-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.27.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-11-03T14:55:06+00:00" + }, + { + "name": "symfony/var-dumper", + "version": "v6.3.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-dumper.git", + "reference": "77fb4f2927f6991a9843633925d111147449ee7a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/77fb4f2927f6991a9843633925d111147449ee7a", + "reference": "77fb4f2927f6991a9843633925d111147449ee7a", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/console": "<5.4" + }, + "require-dev": { + "ext-iconv": "*", + "symfony/console": "^5.4|^6.0", + "symfony/http-kernel": "^5.4|^6.0", + "symfony/process": "^5.4|^6.0", + "symfony/uid": "^5.4|^6.0", + "twig/twig": "^2.13|^3.0.4" + }, + "bin": [ + "Resources/bin/var-dump-server" + ], + "type": "library", + "autoload": { + "files": [ + "Resources/functions/dump.php" + ], + "psr-4": { + "Symfony\\Component\\VarDumper\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides mechanisms for walking through any arbitrary PHP variable", + "homepage": "https://symfony.com", + "keywords": [ + "debug", + "dump" + ], + "support": { + "source": "https://github.com/symfony/var-dumper/tree/v6.3.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-07-31T07:08:24+00:00" + } + ], "aliases": [], "minimum-stability": "stable", "stability-flags": [], diff --git a/public/index.php b/public/index.php index f5d063e..78fef51 100644 --- a/public/index.php +++ b/public/index.php @@ -1,5 +1,11 @@ '); +ini_set('error_append_string', ''); +ini_set('display_errors', 1); +ini_set('display_startup_errors', 1); +error_reporting(E_ALL); + use App\Kernel; require_once dirname(__DIR__).'/vendor/autoload.php'; diff --git a/src/Collector/ClassAttributeCollector.php b/src/Collector/ClassAttributeCollector.php index c53aedd..d333a36 100644 --- a/src/Collector/ClassAttributeCollector.php +++ b/src/Collector/ClassAttributeCollector.php @@ -9,8 +9,8 @@ use ReflectionClass; class ClassAttributeCollector { public function __construct( - private readonly string $path, - private readonly string $attribute, + private readonly string $path, + private readonly ?string $attribute = null, ) { } @@ -32,7 +32,7 @@ class ClassAttributeCollector return $classes; } - private function getClassFromFile(mixed $file) + private function getClassFromFile(\SplFileInfo $file): ?string { $contents = file_get_contents($file->getPathname()); $tokens = token_get_all($contents); @@ -50,12 +50,13 @@ class ClassAttributeCollector $classFound = true; continue; } - if ($namespaceFound && $token[0] === T_STRING) { - $namespace .= $token[1] . '\\'; + if ($namespaceFound && $token[0] === T_NAME_QUALIFIED) { + $namespace = $token[1]; + $namespaceFound = false; continue; } if ($classFound && $token[0] === T_STRING) { - $class = $token[1]; + $class .= $token[1]; break; } } @@ -63,9 +64,8 @@ class ClassAttributeCollector if (!$class) { return null; } - $class = $namespace . $class; - $attributes = $this->getClassAttributes($class); - if (!in_array($this->attribute, $attributes)) { + $class = $namespace . "\\" . $class; + if ($this->attribute && !in_array($this->attribute, $this->getClassAttributes($class))) { return null; } return $class; diff --git a/src/Collector/RouteCollector.php b/src/Collector/RouteCollector.php new file mode 100644 index 0000000..252bc65 --- /dev/null +++ b/src/Collector/RouteCollector.php @@ -0,0 +1,36 @@ +path); + $classes = $classCollector->collect(); + + foreach ($classes as $class) { + $reflection = new \ReflectionClass($class); + $reflectionMethods = $reflection->getMethods(); + + foreach ($reflectionMethods as $reflectionMethod) { + $attributes = $reflectionMethod->getAttributes(Route::class, \ReflectionAttribute::IS_INSTANCEOF); + if (count($attributes) === 0) { + continue; + } + // todo: get the attribute, classname and route + } + } + + return $routes; + } +} \ No newline at end of file diff --git a/src/Container/ContainerInterface.php b/src/Container/ContainerInterface.php new file mode 100644 index 0000000..bd50325 --- /dev/null +++ b/src/Container/ContainerInterface.php @@ -0,0 +1,22 @@ + $className + * @return TClassName + * @throws Exception + */ + public function get(string $className): object; +} \ No newline at end of file diff --git a/src/Container/GenericContainer.php b/src/Container/GenericContainer.php index d45c840..4ae6405 100644 --- a/src/Container/GenericContainer.php +++ b/src/Container/GenericContainer.php @@ -3,19 +3,20 @@ namespace Ardent\Undercurrent\Container; use Exception; +use ReflectionClass; -class GenericContainer +class GenericContainer implements ContainerInterface { private array $definitions = []; private array $instances = []; + private array $aliases = []; + public function add(string $className, ?callable $definition = null, bool $singleton = true): self { if (!$definition) { - $definition = function () use ($className) { - return new $className(); - }; + $definition = fn() => $this->autowire($className); } $this->definitions[$className] = [ @@ -26,29 +27,57 @@ class GenericContainer return $this; } + public function alias(string $alias, string $className): self + { + $this->aliases[$alias] = $className; + + return $this; + } + /** - * @template TClassName - * @param class-string $className - * @return TClassName - * @throws Exception + * @inheritDoc */ public function get(string $className): object { + if (isset($this->aliases[$className])) { + $className = $this->aliases[$className]; + } if (!isset($this->definitions[$className])) { throw new Exception("Class $className not found in container"); } $definition = $this->definitions[$className]['definition']; + if ($this->definitions[$className]['singleton']) { if (isset($this->instances[$className])) { return $this->instances[$className]; } - $instance = $definition(); + $instance = $definition($this); $this->instances[$className] = $instance; } else { - $instance = $definition(); + $instance = $definition($this); } return $instance; } + + private function autowire(string $className): ?object + { + $reflection = new ReflectionClass($className); + $constructor = $reflection->getConstructor(); + if (!$constructor) { + return new $className(); + } + + $params = []; + foreach ($constructor->getParameters() as $parameter) { + $type = $parameter->getType(); + if (!$type) { + throw new Exception("Parameter {$parameter->getName()} in $className has no type"); + } + $params[] = $this->get($type->getName()); + } + + return new $className(...$params); + } } \ No newline at end of file diff --git a/src/Http/GenericRouter.php b/src/Http/GenericRouter.php index 0332ce5..fa1bc6a 100644 --- a/src/Http/GenericRouter.php +++ b/src/Http/GenericRouter.php @@ -5,27 +5,21 @@ namespace Ardent\Undercurrent\Http; use App\Controller\BaseController; use Ardent\Undercurrent\Attribute\Route; use Ardent\Undercurrent\Collector\ClassAttributeCollector; +use Ardent\Undercurrent\Container\ContainerInterface; -class GenericRouter +class GenericRouter implements RouterInterface { - private array $routes = []; - - public function __construct() + public function __construct( + private readonly ContainerInterface $container, + private readonly RouterConfig $config, + ) { - $collector = new ClassAttributeCollector( - __DIR__ . '/../../app/Controller', - Route::class, - ); - - $this->routes = $collector->collect(); } - public function getRoute(string $route): array + public function dispatch(RequestInterface $request): ResponseInterface { -// return $this->routes[0]; - return [ - 'controller' => BaseController::class, - 'method' => 'HelloWorld', - ]; + $controller = $this->container->get(BaseController::class); + $method = 'helloWorld'; + return $controller->$method(); } } \ No newline at end of file diff --git a/src/Http/RouterConfig.php b/src/Http/RouterConfig.php new file mode 100644 index 0000000..ad856d6 --- /dev/null +++ b/src/Http/RouterConfig.php @@ -0,0 +1,18 @@ +controllers; + } +} \ No newline at end of file diff --git a/src/Kernel/BaseKernel.php b/src/Kernel/BaseKernel.php index 05ecf06..a6732f4 100644 --- a/src/Kernel/BaseKernel.php +++ b/src/Kernel/BaseKernel.php @@ -3,27 +3,42 @@ namespace Ardent\Undercurrent\Kernel; use App\Controller\BaseController; +use Ardent\Undercurrent\Container\ContainerInterface; use Ardent\Undercurrent\Container\GenericContainer; +use Ardent\Undercurrent\Http\GenericRequest; use Ardent\Undercurrent\Http\GenericRouter; -use Ardent\Undercurrent\Http\ResponseInterface; +use Ardent\Undercurrent\Http\MethodEnum; +use Ardent\Undercurrent\Http\RouterConfig; +use Ardent\Undercurrent\Http\RouterInterface; class BaseKernel { public function __invoke(): void { - $container = new GenericContainer(); - $container->add(GenericRouter::class); - $container->add(BaseController::class); + $container = (new GenericContainer()); + $container + ->alias(RouterInterface::class, GenericRouter::class) + ->alias(ContainerInterface::class, GenericContainer::class) + ->add(GenericContainer::class, fn($container) => $container) + ->add(GenericRouter::class) + ->add(BaseController::class); + + $container->add(RouterConfig::class, fn() => new RouterConfig([ + BaseController::class, + ])); $this->render($container); } private function render(GenericContainer $container): void { - $router = $container->get(GenericRouter::class); - $route = $router->getRoute($_SERVER['REQUEST_URI']); - $controller = $container->get($route['controller']); - $method = $route['method']; - echo $controller->$method()->getBody(); + $request = new GenericRequest( + MethodEnum::from($_SERVER['REQUEST_METHOD']), + $_SERVER['REQUEST_URI'], + $_REQUEST, + ); + + $router = $container->get(RouterInterface::class); + echo $router->dispatch($request)->getBody(); } } \ No newline at end of file