Properly implement the router with config and interfaces

Expand the container with aliases and argument autowiring
This commit is contained in:
Tim 2023-08-07 17:50:34 +02:00
parent e9a636554f
commit a55d1c3c2e
11 changed files with 406 additions and 48 deletions

View File

@ -9,7 +9,7 @@ use Ardent\Undercurrent\Http\ResponseInterface;
class BaseController class BaseController
{ {
#[Route('/hello')] #[Route('/hello')]
public function HelloWorld(): ResponseInterface public function helloWorld(): ResponseInterface
{ {
return new GenericResponse('Hello World!'); return new GenericResponse('Hello World!');
} }

View File

@ -11,5 +11,8 @@
"App\\": "app/" "App\\": "app/"
} }
}, },
"license": "GPL-3.0-only" "license": "GPL-3.0-only",
"require-dev": {
"symfony/var-dumper": "^6.3"
}
} }

239
composer.lock generated
View File

@ -4,9 +4,244 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "04c6989f1bd672a6accd35142e2adac1", "content-hash": "63924b42ed7cdd90434b1da20abb9381",
"packages": [], "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": [], "aliases": [],
"minimum-stability": "stable", "minimum-stability": "stable",
"stability-flags": [], "stability-flags": [],

View File

@ -1,5 +1,11 @@
<?php <?php
ini_set('error_prepend_string', '<pre style="white-space: pre-wrap;">');
ini_set('error_append_string', '</pre>');
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
use App\Kernel; use App\Kernel;
require_once dirname(__DIR__).'/vendor/autoload.php'; require_once dirname(__DIR__).'/vendor/autoload.php';

View File

@ -9,8 +9,8 @@ use ReflectionClass;
class ClassAttributeCollector class ClassAttributeCollector
{ {
public function __construct( public function __construct(
private readonly string $path, private readonly string $path,
private readonly string $attribute, private readonly ?string $attribute = null,
) )
{ {
} }
@ -32,7 +32,7 @@ class ClassAttributeCollector
return $classes; return $classes;
} }
private function getClassFromFile(mixed $file) private function getClassFromFile(\SplFileInfo $file): ?string
{ {
$contents = file_get_contents($file->getPathname()); $contents = file_get_contents($file->getPathname());
$tokens = token_get_all($contents); $tokens = token_get_all($contents);
@ -50,12 +50,13 @@ class ClassAttributeCollector
$classFound = true; $classFound = true;
continue; continue;
} }
if ($namespaceFound && $token[0] === T_STRING) { if ($namespaceFound && $token[0] === T_NAME_QUALIFIED) {
$namespace .= $token[1] . '\\'; $namespace = $token[1];
$namespaceFound = false;
continue; continue;
} }
if ($classFound && $token[0] === T_STRING) { if ($classFound && $token[0] === T_STRING) {
$class = $token[1]; $class .= $token[1];
break; break;
} }
} }
@ -63,9 +64,8 @@ class ClassAttributeCollector
if (!$class) { if (!$class) {
return null; return null;
} }
$class = $namespace . $class; $class = $namespace . "\\" . $class;
$attributes = $this->getClassAttributes($class); if ($this->attribute && !in_array($this->attribute, $this->getClassAttributes($class))) {
if (!in_array($this->attribute, $attributes)) {
return null; return null;
} }
return $class; return $class;

View File

@ -0,0 +1,36 @@
<?php
namespace Ardent\Undercurrent\Collector;
use Ardent\Undercurrent\Attribute\Route;
class RouteCollector
{
public function __construct(
private readonly string $path,
)
{
}
public function collect(): array
{
$routes = [];
$classCollector = new ClassAttributeCollector($this->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;
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace Ardent\Undercurrent\Container;
use Exception;
interface ContainerInterface
{
public function add(
string $className,
?callable $definition = null,
bool $singleton = true
): self;
/**
* @template TClassName
* @param class-string<TClassName> $className
* @return TClassName
* @throws Exception
*/
public function get(string $className): object;
}

View File

@ -3,19 +3,20 @@
namespace Ardent\Undercurrent\Container; namespace Ardent\Undercurrent\Container;
use Exception; use Exception;
use ReflectionClass;
class GenericContainer class GenericContainer implements ContainerInterface
{ {
private array $definitions = []; private array $definitions = [];
private array $instances = []; private array $instances = [];
private array $aliases = [];
public function add(string $className, ?callable $definition = null, bool $singleton = true): self public function add(string $className, ?callable $definition = null, bool $singleton = true): self
{ {
if (!$definition) { if (!$definition) {
$definition = function () use ($className) { $definition = fn() => $this->autowire($className);
return new $className();
};
} }
$this->definitions[$className] = [ $this->definitions[$className] = [
@ -26,29 +27,57 @@ class GenericContainer
return $this; return $this;
} }
public function alias(string $alias, string $className): self
{
$this->aliases[$alias] = $className;
return $this;
}
/** /**
* @template TClassName * @inheritDoc
* @param class-string<TClassName> $className
* @return TClassName
* @throws Exception
*/ */
public function get(string $className): object public function get(string $className): object
{ {
if (isset($this->aliases[$className])) {
$className = $this->aliases[$className];
}
if (!isset($this->definitions[$className])) { if (!isset($this->definitions[$className])) {
throw new Exception("Class $className not found in container"); throw new Exception("Class $className not found in container");
} }
$definition = $this->definitions[$className]['definition']; $definition = $this->definitions[$className]['definition'];
if ($this->definitions[$className]['singleton']) { if ($this->definitions[$className]['singleton']) {
if (isset($this->instances[$className])) { if (isset($this->instances[$className])) {
return $this->instances[$className]; return $this->instances[$className];
} }
$instance = $definition(); $instance = $definition($this);
$this->instances[$className] = $instance; $this->instances[$className] = $instance;
} else { } else {
$instance = $definition(); $instance = $definition($this);
} }
return $instance; 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);
}
} }

View File

@ -5,27 +5,21 @@ namespace Ardent\Undercurrent\Http;
use App\Controller\BaseController; use App\Controller\BaseController;
use Ardent\Undercurrent\Attribute\Route; use Ardent\Undercurrent\Attribute\Route;
use Ardent\Undercurrent\Collector\ClassAttributeCollector; use Ardent\Undercurrent\Collector\ClassAttributeCollector;
use Ardent\Undercurrent\Container\ContainerInterface;
class GenericRouter class GenericRouter implements RouterInterface
{ {
private array $routes = []; public function __construct(
private readonly ContainerInterface $container,
public function __construct() 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]; $controller = $this->container->get(BaseController::class);
return [ $method = 'helloWorld';
'controller' => BaseController::class, return $controller->$method();
'method' => 'HelloWorld',
];
} }
} }

18
src/Http/RouterConfig.php Normal file
View File

@ -0,0 +1,18 @@
<?php
namespace Ardent\Undercurrent\Http;
class RouterConfig
{
public function __construct(
private readonly array $controllers = [],
)
{
}
public function getControllers(): array
{
return $this->controllers;
}
}

View File

@ -3,27 +3,42 @@
namespace Ardent\Undercurrent\Kernel; namespace Ardent\Undercurrent\Kernel;
use App\Controller\BaseController; use App\Controller\BaseController;
use Ardent\Undercurrent\Container\ContainerInterface;
use Ardent\Undercurrent\Container\GenericContainer; use Ardent\Undercurrent\Container\GenericContainer;
use Ardent\Undercurrent\Http\GenericRequest;
use Ardent\Undercurrent\Http\GenericRouter; 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 class BaseKernel
{ {
public function __invoke(): void public function __invoke(): void
{ {
$container = new GenericContainer(); $container = (new GenericContainer());
$container->add(GenericRouter::class); $container
$container->add(BaseController::class); ->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); $this->render($container);
} }
private function render(GenericContainer $container): void private function render(GenericContainer $container): void
{ {
$router = $container->get(GenericRouter::class); $request = new GenericRequest(
$route = $router->getRoute($_SERVER['REQUEST_URI']); MethodEnum::from($_SERVER['REQUEST_METHOD']),
$controller = $container->get($route['controller']); $_SERVER['REQUEST_URI'],
$method = $route['method']; $_REQUEST,
echo $controller->$method()->getBody(); );
$router = $container->get(RouterInterface::class);
echo $router->dispatch($request)->getBody();
} }
} }