add code
This commit is contained in:
parent
7dd81f65d5
commit
dff0cc92e0
36 changed files with 2020 additions and 0 deletions
19
app/src/Infrastructure/Event/AsListener.php
Normal file
19
app/src/Infrastructure/Event/AsListener.php
Normal file
|
@ -0,0 +1,19 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
namespace Lubian\AttributeMagic\Infrastructure\Event;
|
||||
|
||||
use Attribute;
|
||||
|
||||
#[Attribute(Attribute::TARGET_METHOD)]
|
||||
final class AsListener
|
||||
{
|
||||
/**
|
||||
* @param class-string $eventClass
|
||||
* @param int<-100, 100> $priority
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly string $eventClass,
|
||||
public readonly int $priority = 0,
|
||||
) {
|
||||
}
|
||||
}
|
60
app/src/Infrastructure/Event/Dispatcher.php
Normal file
60
app/src/Infrastructure/Event/Dispatcher.php
Normal file
|
@ -0,0 +1,60 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
namespace Lubian\AttributeMagic\Infrastructure\Event;
|
||||
|
||||
use Invoker\InvokerInterface;
|
||||
|
||||
use function Lubian\AttributeMagic\Infrastructure\arrayFilter;
|
||||
use function usort;
|
||||
|
||||
final class Dispatcher
|
||||
{
|
||||
/**
|
||||
* @param Listener[] $listeners
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly InvokerInterface $invoker,
|
||||
private array $listeners = [],
|
||||
) {
|
||||
}
|
||||
|
||||
public function addListener(Listener $listener): void
|
||||
{
|
||||
$this->listeners[] = $listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Listener[]
|
||||
*/
|
||||
public function getListenerForEvent(GenericEvent $event): array
|
||||
{
|
||||
return arrayFilter(
|
||||
$this->listeners,
|
||||
static fn (Listener $l): bool => $l->eventClass === $event::class,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Listener[]
|
||||
*/
|
||||
public function getSortedListenerForEvent(GenericEvent $event): array
|
||||
{
|
||||
$filtered = $this->getListenerForEvent($event);
|
||||
usort($filtered, static fn (Listener $a, Listener $b): int => $a->priority <=> $b->priority);
|
||||
return $filtered;
|
||||
}
|
||||
|
||||
public function dispatch(GenericEvent $event): void
|
||||
{
|
||||
foreach ($this->getSortedListenerForEvent($event) as $listener) {
|
||||
if ($event->stopped === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->invoker->call(
|
||||
[$listener->listenerClass, $listener->listenerMethod],
|
||||
[$event],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
49
app/src/Infrastructure/Event/DispatcherFactory.php
Normal file
49
app/src/Infrastructure/Event/DispatcherFactory.php
Normal file
|
@ -0,0 +1,49 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
namespace Lubian\AttributeMagic\Infrastructure\Event;
|
||||
|
||||
use Invoker\InvokerInterface;
|
||||
use Lubian\AttributeMagic\Infrastructure\Finder;
|
||||
|
||||
use function file_exists;
|
||||
use function file_get_contents;
|
||||
use function file_put_contents;
|
||||
use function serialize;
|
||||
use function unserialize;
|
||||
|
||||
final class DispatcherFactory
|
||||
{
|
||||
public const CACHE_FILE = __DIR__ . '/../../../var/listenerCache';
|
||||
|
||||
public function __construct(
|
||||
private readonly Finder $finder,
|
||||
private readonly InvokerInterface $invoker
|
||||
) {
|
||||
}
|
||||
|
||||
public function getDispatcher(bool $cached = false): Dispatcher
|
||||
{
|
||||
return new Dispatcher($this->invoker, $this->getListeners($cached));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Listener[]
|
||||
*/
|
||||
private function getListeners(bool $cached = false): array
|
||||
{
|
||||
if ($cached === true && file_exists(self::CACHE_FILE)) {
|
||||
/** @var Listener[] $listeners */
|
||||
$listeners = unserialize(
|
||||
file_get_contents(self::CACHE_FILE)
|
||||
); //@phpstan-ignore-line
|
||||
return $listeners;
|
||||
}
|
||||
|
||||
$listeners = $this->finder->getListeners();
|
||||
if ($cached === true) {
|
||||
file_put_contents(self::CACHE_FILE, serialize($listeners));
|
||||
}
|
||||
|
||||
return $listeners;
|
||||
}
|
||||
}
|
8
app/src/Infrastructure/Event/GenericEvent.php
Normal file
8
app/src/Infrastructure/Event/GenericEvent.php
Normal file
|
@ -0,0 +1,8 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
namespace Lubian\AttributeMagic\Infrastructure\Event;
|
||||
|
||||
abstract class GenericEvent
|
||||
{
|
||||
public bool $stopped = false;
|
||||
}
|
20
app/src/Infrastructure/Event/Listener.php
Normal file
20
app/src/Infrastructure/Event/Listener.php
Normal file
|
@ -0,0 +1,20 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
namespace Lubian\AttributeMagic\Infrastructure\Event;
|
||||
|
||||
final class Listener
|
||||
{
|
||||
/**
|
||||
* @param class-string $eventClass
|
||||
* @param int<-100, 100> $priority
|
||||
* @param class-string $listenerClass
|
||||
* @param non-empty-string $listenerMethod
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly string $eventClass,
|
||||
public readonly int $priority,
|
||||
public readonly string $listenerClass,
|
||||
public readonly string $listenerMethod,
|
||||
) {
|
||||
}
|
||||
}
|
114
app/src/Infrastructure/Finder.php
Normal file
114
app/src/Infrastructure/Finder.php
Normal file
|
@ -0,0 +1,114 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
namespace Lubian\AttributeMagic\Infrastructure;
|
||||
|
||||
use Iterator;
|
||||
use Lubian\AttributeMagic\Infrastructure\Event\AsListener;
|
||||
use Lubian\AttributeMagic\Infrastructure\Event\Listener;
|
||||
use Lubian\AttributeMagic\Infrastructure\Route\AsHandler;
|
||||
use Lubian\AttributeMagic\Infrastructure\Route\Handler;
|
||||
use RecursiveDirectoryIterator;
|
||||
use RecursiveIteratorIterator;
|
||||
use ReflectionClass;
|
||||
use SplFileInfo;
|
||||
|
||||
use function array_diff;
|
||||
use function array_map;
|
||||
use function file_exists;
|
||||
use function file_get_contents;
|
||||
use function file_put_contents;
|
||||
use function get_declared_classes;
|
||||
use function iterator_to_array;
|
||||
use function serialize;
|
||||
use function str_ends_with;
|
||||
use function unserialize;
|
||||
|
||||
final class Finder
|
||||
{
|
||||
public const CACHE_FILE = __DIR__ . '/../../var/classesCache';
|
||||
|
||||
/**
|
||||
* @param class-string[] $classNames
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly string $path,
|
||||
private array $classNames = [],
|
||||
private readonly bool $cached = false,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Listener[]
|
||||
*/
|
||||
public function getListeners(): array
|
||||
{
|
||||
$this->populateClassnames();
|
||||
return array_map(
|
||||
static fn (array $h): Listener => new Listener($h[0]->eventClass, $h[0]->priority, $h[1], $h[2]),
|
||||
iterator_to_array($this->getAttributes(AsListener::class)),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Handler[]
|
||||
*/
|
||||
public function getHandlers(): array
|
||||
{
|
||||
$this->populateClassnames();
|
||||
return array_map(
|
||||
static fn (array $h): Handler => new Handler($h[0]->method, $h[0]->path, $h[1], $h[2]),
|
||||
iterator_to_array($this->getAttributes(AsHandler::class)),
|
||||
);
|
||||
}
|
||||
|
||||
private function populateClassnames(): void
|
||||
{
|
||||
if ($this->classNames !== []) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->cached === true && file_exists(self::CACHE_FILE)) {
|
||||
$this->classNames = unserialize(file_get_contents(self::CACHE_FILE));
|
||||
return;
|
||||
}
|
||||
|
||||
$it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($this->path));
|
||||
/** @var SplFileInfo $file */
|
||||
foreach ($it as $file) {
|
||||
if (! str_ends_with((string) $file, '.php')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$classesBeforeLoad = get_declared_classes();
|
||||
require_once (string) $file;
|
||||
$classesAfterLoad = get_declared_classes();
|
||||
$this->classNames = [...$this->classNames, ...array_diff($classesAfterLoad, $classesBeforeLoad)];
|
||||
}
|
||||
|
||||
if ($this->cached === true) {
|
||||
file_put_contents(self::CACHE_FILE, serialize($this->classNames));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param class-string<T> $attributeClass
|
||||
* @return Iterator<array{T, class-string, non-empty-string}>
|
||||
*/
|
||||
private function getAttributes(
|
||||
string $attributeClass
|
||||
): Iterator {
|
||||
foreach ($this->classNames as $class) {
|
||||
$reflectionClass = new ReflectionClass($class);
|
||||
foreach ($reflectionClass->getMethods() as $method) {
|
||||
foreach ($method->getAttributes($attributeClass) as $attribute) {
|
||||
yield [
|
||||
$attribute->newInstance(),
|
||||
$class,
|
||||
$method->getName(),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
18
app/src/Infrastructure/Route/AsHandler.php
Normal file
18
app/src/Infrastructure/Route/AsHandler.php
Normal file
|
@ -0,0 +1,18 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
namespace Lubian\AttributeMagic\Infrastructure\Route;
|
||||
|
||||
use Attribute;
|
||||
|
||||
#[Attribute(Attribute::TARGET_METHOD)]
|
||||
final class AsHandler
|
||||
{
|
||||
/**
|
||||
* @param non-empty-string $path
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly HttpMethod $method,
|
||||
public readonly string $path,
|
||||
) {
|
||||
}
|
||||
}
|
19
app/src/Infrastructure/Route/Handler.php
Normal file
19
app/src/Infrastructure/Route/Handler.php
Normal file
|
@ -0,0 +1,19 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
namespace Lubian\AttributeMagic\Infrastructure\Route;
|
||||
|
||||
final class Handler
|
||||
{
|
||||
/**
|
||||
* @param non-empty-string $path
|
||||
* @param non-empty-string $handlerClass
|
||||
* @param non-empty-string $handlerMethod
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly HttpMethod $method,
|
||||
public readonly string $path,
|
||||
public readonly string $handlerClass,
|
||||
public readonly string $handlerMethod,
|
||||
) {
|
||||
}
|
||||
}
|
9
app/src/Infrastructure/Route/HttpMethod.php
Normal file
9
app/src/Infrastructure/Route/HttpMethod.php
Normal file
|
@ -0,0 +1,9 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
namespace Lubian\AttributeMagic\Infrastructure\Route;
|
||||
|
||||
enum HttpMethod: string
|
||||
{
|
||||
case GET = 'GET';
|
||||
case POST = 'POST';
|
||||
}
|
18
app/src/Infrastructure/WebApp/Request/RequestEvent.php
Normal file
18
app/src/Infrastructure/WebApp/Request/RequestEvent.php
Normal file
|
@ -0,0 +1,18 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
namespace Lubian\AttributeMagic\Infrastructure\WebApp\Request;
|
||||
|
||||
use Lubian\AttributeMagic\Infrastructure\Event\GenericEvent;
|
||||
use Lubian\AttributeMagic\Infrastructure\Route\Handler;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
final class RequestEvent extends GenericEvent
|
||||
{
|
||||
public function __construct(
|
||||
public Request $request,
|
||||
public Response|null $response = null,
|
||||
public Handler|null $handler = null
|
||||
) {
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
namespace Lubian\AttributeMagic\Infrastructure\WebApp\Route;
|
||||
|
||||
use Lubian\AttributeMagic\Infrastructure\Event\AsListener;
|
||||
use Lubian\AttributeMagic\Infrastructure\Finder;
|
||||
|
||||
final class AttributeRouteCollector
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Finder $finder
|
||||
) {
|
||||
}
|
||||
|
||||
#[AsListener(CollectRoutes::class)]
|
||||
public function collect(CollectRoutes $event): void
|
||||
{
|
||||
$event->routes = $this->finder->getHandlers();
|
||||
}
|
||||
}
|
36
app/src/Infrastructure/WebApp/Route/CachedResponse.php
Normal file
36
app/src/Infrastructure/WebApp/Route/CachedResponse.php
Normal file
|
@ -0,0 +1,36 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
namespace Lubian\AttributeMagic\Infrastructure\WebApp\Route;
|
||||
|
||||
use Lubian\AttributeMagic\Infrastructure\Event\AsListener;
|
||||
use Lubian\AttributeMagic\Infrastructure\WebApp\Request\RequestEvent;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
use function apcu_add;
|
||||
use function md5;
|
||||
use function serialize;
|
||||
use function unserialize;
|
||||
|
||||
final class CachedResponse
|
||||
{
|
||||
#[AsListener(RequestEvent::class, -99)]
|
||||
public function cachedResponse(RequestEvent $event): void
|
||||
{
|
||||
$response = new Response('lol');
|
||||
$serialized = serialize($response);
|
||||
$response = unserialize($serialized);
|
||||
$event->response = $response;
|
||||
// $event->stopped = true;
|
||||
}
|
||||
|
||||
#[AsListener(RequestEvent::class, 99)]
|
||||
public function doCache(RequestEvent $event): void
|
||||
{
|
||||
if ($event->response === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$path = md5($event->request->getPathInfo());
|
||||
apcu_add($path, serialize($event->response));
|
||||
}
|
||||
}
|
44
app/src/Infrastructure/WebApp/Route/CachedRouteCollector.php
Normal file
44
app/src/Infrastructure/WebApp/Route/CachedRouteCollector.php
Normal file
|
@ -0,0 +1,44 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
namespace Lubian\AttributeMagic\Infrastructure\WebApp\Route;
|
||||
|
||||
use Lubian\AttributeMagic\Infrastructure\Event\AsListener;
|
||||
|
||||
use function file_exists;
|
||||
use function file_get_contents;
|
||||
use function file_put_contents;
|
||||
use function serialize;
|
||||
use function unserialize;
|
||||
|
||||
final class CachedRouteCollector
|
||||
{
|
||||
public const ROUTES_DIR = __DIR__ . '/../../../../var/routesCache';
|
||||
|
||||
#[AsListener(CollectRoutes::class, -10)]
|
||||
public function getCached(CollectRoutes $event): void
|
||||
{
|
||||
if ($event->cached === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! file_exists(self::ROUTES_DIR)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$event->routes = unserialize(file_get_contents(self::ROUTES_DIR)); //@phpstan-ignore-line
|
||||
$event->stopped = true;
|
||||
}
|
||||
|
||||
#[AsListener(CollectRoutes::class, 10)]
|
||||
public function setCached(CollectRoutes $event): void
|
||||
{
|
||||
if ($event->cached === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
file_put_contents(
|
||||
self::ROUTES_DIR,
|
||||
serialize($event->routes),
|
||||
);
|
||||
}
|
||||
}
|
19
app/src/Infrastructure/WebApp/Route/CollectRoutes.php
Normal file
19
app/src/Infrastructure/WebApp/Route/CollectRoutes.php
Normal file
|
@ -0,0 +1,19 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
namespace Lubian\AttributeMagic\Infrastructure\WebApp\Route;
|
||||
|
||||
use Lubian\AttributeMagic\Infrastructure\Event\GenericEvent;
|
||||
use Lubian\AttributeMagic\Infrastructure\Route\Handler;
|
||||
|
||||
final class CollectRoutes extends GenericEvent
|
||||
{
|
||||
/**
|
||||
* @var Handler[]
|
||||
*/
|
||||
public array $routes = [];
|
||||
|
||||
public function __construct(
|
||||
public readonly bool $cached = false,
|
||||
) {
|
||||
}
|
||||
}
|
43
app/src/Infrastructure/WebApp/Route/HandlerCaller.php
Normal file
43
app/src/Infrastructure/WebApp/Route/HandlerCaller.php
Normal file
|
@ -0,0 +1,43 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
namespace Lubian\AttributeMagic\Infrastructure\WebApp\Route;
|
||||
|
||||
use Invoker\InvokerInterface;
|
||||
use Lubian\AttributeMagic\Infrastructure\Event\AsListener;
|
||||
use Lubian\AttributeMagic\Infrastructure\WebApp\Request\RequestEvent;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
use function ob_get_clean;
|
||||
use function ob_start;
|
||||
|
||||
final class HandlerCaller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly InvokerInterface $invoker
|
||||
) {
|
||||
}
|
||||
|
||||
#[AsListener(RequestEvent::class, -50)]
|
||||
public function callHandler(RequestEvent $event): void
|
||||
{
|
||||
if ($event->handler === null) {
|
||||
$event->stopped = true;
|
||||
$event->response = new Response('not resolved', 500);
|
||||
return;
|
||||
}
|
||||
|
||||
ob_start();
|
||||
$response = $this->invoker->call(
|
||||
[$event->handler->handlerClass, $event->handler->handlerMethod],
|
||||
$event->request->attributes->all()
|
||||
);
|
||||
$output = (string) ob_get_clean();
|
||||
|
||||
if ($response instanceof Response) {
|
||||
$event->response = $response;
|
||||
return;
|
||||
}
|
||||
|
||||
$event->response = new Response($output, 200);
|
||||
}
|
||||
}
|
68
app/src/Infrastructure/WebApp/Route/HandlerResolver.php
Normal file
68
app/src/Infrastructure/WebApp/Route/HandlerResolver.php
Normal file
|
@ -0,0 +1,68 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
namespace Lubian\AttributeMagic\Infrastructure\WebApp\Route;
|
||||
|
||||
use FastRoute\RouteCollector;
|
||||
use Lubian\AttributeMagic\Infrastructure\Event\AsListener;
|
||||
use Lubian\AttributeMagic\Infrastructure\Event\Dispatcher;
|
||||
use Lubian\AttributeMagic\Infrastructure\Route\Handler;
|
||||
use Lubian\AttributeMagic\Infrastructure\Route\HttpMethod;
|
||||
use Lubian\AttributeMagic\Infrastructure\WebApp\Request\RequestEvent;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
use function FastRoute\cachedDispatcher;
|
||||
|
||||
final class HandlerResolver
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Dispatcher $dispatcher,
|
||||
private readonly bool $cached = false,
|
||||
) {
|
||||
}
|
||||
|
||||
#[AsListener(RequestEvent::class, -90)]
|
||||
public function resolveHandler(RequestEvent $event): void
|
||||
{
|
||||
$routesEvent = new CollectRoutes($this->cached);
|
||||
$this->dispatcher->dispatch($routesEvent);
|
||||
$dispatcher = $this->dispatcher;
|
||||
|
||||
$routeDispatcher = cachedDispatcher(static function (RouteCollector $r) use (
|
||||
$dispatcher,
|
||||
$routesEvent
|
||||
): void {
|
||||
$dispatcher->dispatch($routesEvent);
|
||||
foreach ($routesEvent->routes as $h) {
|
||||
$r->addRoute($h->method->value, $h->path, [$h->handlerClass, $h->handlerMethod]);
|
||||
}
|
||||
}, [
|
||||
'cacheFile' => __DIR__ . '/../../../../var/route.cache',
|
||||
'cacheDisabled' => ! $this->cached,
|
||||
]);
|
||||
|
||||
$routeInfo = $routeDispatcher->dispatch(
|
||||
$event->request->getMethod(),
|
||||
$event->request->getPathInfo(),
|
||||
);
|
||||
|
||||
if ($routeInfo[0] === \FastRoute\Dispatcher::NOT_FOUND) {
|
||||
$event->response = new Response('Not Found', 404);
|
||||
$event->stopped = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if ($routeInfo[0] === \FastRoute\Dispatcher::METHOD_NOT_ALLOWED) {
|
||||
$event->response = new Response('Not Allowed', 403);
|
||||
$event->stopped = true;
|
||||
return;
|
||||
}
|
||||
|
||||
$event->handler = new Handler(
|
||||
HttpMethod::from($event->request->getMethod()),
|
||||
$event->request->getPathInfo(),
|
||||
...$routeInfo[1],
|
||||
);
|
||||
|
||||
$event->request->attributes->add($routeInfo[2]);
|
||||
}
|
||||
}
|
23
app/src/Infrastructure/functions.php
Normal file
23
app/src/Infrastructure/functions.php
Normal file
|
@ -0,0 +1,23 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
namespace Lubian\AttributeMagic\Infrastructure;
|
||||
|
||||
use function array_filter;
|
||||
use function array_values;
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param array<T> $input
|
||||
* @return array<int, T>
|
||||
*/
|
||||
function arrayFilter(
|
||||
array $input,
|
||||
callable $callable
|
||||
): array {
|
||||
return array_values(
|
||||
array_filter(
|
||||
$input,
|
||||
$callable,
|
||||
)
|
||||
);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue