This commit is contained in:
lubiana 2022-11-05 11:55:58 +01:00
parent 7dd81f65d5
commit dff0cc92e0
No known key found for this signature in database
36 changed files with 2020 additions and 0 deletions

6
.gitignore vendored Normal file
View file

@ -0,0 +1,6 @@
/app/vendor/
/.vagrant/
/.idea/
*.log
/app/var/routesCache
/app/var/listenerCache

51
app/composer.json Normal file
View file

@ -0,0 +1,51 @@
{
"name": "lubian/attribute-magic",
"autoload": {
"psr-4": {
"Lubian\\AttributeMagic\\": "src/"
},
"files": [
"src/Infrastructure/functions.php"
]
},
"authors": [
{
"name": "lubiana",
"email": "lubiana123@gmail.com"
}
],
"require": {
"php": ">=8.1",
"php-di/php-di": "^6.4",
"nikic/fast-route": "^1.3",
"symfony/http-foundation": "^6.1.7"
},
"require-dev": {
"phpstan/phpstan": "^1.9.1",
"lubiana/code-quality": "^1.1",
"phpstan/extension-installer": "^1.2",
"phpstan/phpstan-strict-rules": "^1.4",
"thecodingmachine/phpstan-strict-rules": "^1.0"
},
"config": {
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true,
"phpstan/extension-installer": true
}
},
"scripts": {
"serve": [
"Composer\\Config::disableProcessTimeout",
"php -S 0.0.0.0:1235 -t public"
],
"phpstan": "./vendor/bin/phpstan analyze",
"baseline": "./vendor/bin/phpstan analyze --generate-baseline",
"style": "./vendor/bin/ecs",
"fix": [
"rector process" ,"ecs --fix", "ecs --fix"
],
"rector": "./vendor/bin/rector process"
},
"minimum-stability": "dev",
"prefer-stable": true
}

1162
app/composer.lock generated Normal file

File diff suppressed because it is too large Load diff

0
app/config/.gitkeep Normal file
View file

18
app/ecs.php Normal file
View file

@ -0,0 +1,18 @@
<?php declare(strict_types=1);
use Lubiana\CodeQuality\LubiSetList;
use Symplify\EasyCodingStandard\Config\ECSConfig;
return static function (ECSConfig $c): void {
$c->parallel();
$c->paths([
__DIR__ . '/src',
__DIR__ . '/config',
__DIR__ . '/ecs.php',
__DIR__ . '/rector.php',
]);
$c->sets([
LubiSetList::ECS,
]);
};

View file

8
app/phpstan.neon Normal file
View file

@ -0,0 +1,8 @@
includes:
- phpstan-baseline.neon
parameters:
level: max
paths:
- src
- config

BIN
app/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

3
app/public/index.php Normal file
View file

@ -0,0 +1,3 @@
<?php declare(strict_types=1);
require __DIR__ . '/../src/Bootstrap.php';

14
app/rector.php Normal file
View file

@ -0,0 +1,14 @@
<?php declare(strict_types=1);
use Lubiana\CodeQuality\LubiSetList;
use Rector\Config\RectorConfig;
return static function (RectorConfig $c): void {
$c->paths([__DIR__ . '/src', __DIR__ . '/config', __DIR__ . '/ecs.php', __DIR__ . '/rector.php']);
$c->importNames();
$c->sets([
LubiSetList::RECTOR,
]);
};

30
app/src/Bootstrap.php Normal file
View file

@ -0,0 +1,30 @@
<?php declare(strict_types=1);
namespace Lubian\AttributeMagic;
use DI\ContainerBuilder;
use Lubian\AttributeMagic\Infrastructure\Event\Dispatcher;
use Lubian\AttributeMagic\Infrastructure\Event\DispatcherFactory;
use Lubian\AttributeMagic\Infrastructure\Finder;
use Lubian\AttributeMagic\Infrastructure\WebApp\Request\RequestEvent;
use Lubian\AttributeMagic\Infrastructure\WebApp\Route\HandlerResolver;
use Symfony\Component\HttpFoundation\Request;
use function assert;
require_once __DIR__ . '/../vendor/autoload.php';
$cached = false;
$container = (new ContainerBuilder)->addDefinitions([
Finder::class => static fn (): Finder => new Finder(__DIR__, [], $cached),
Dispatcher::class => static fn (DispatcherFactory $f): Dispatcher => $f->getDispatcher($cached),
HandlerResolver::class => static fn (Dispatcher $d): HandlerResolver => new HandlerResolver($d, $cached),
])->build();
$dispatcher = $container->get(Dispatcher::class);
assert($dispatcher instanceof Dispatcher);
$request = new RequestEvent(Request::createFromGlobals());
$dispatcher->dispatch($request);
$request->response?->send();
exit;

View file

@ -0,0 +1,15 @@
<?php declare(strict_types=1);
namespace Lubian\AttributeMagic\Handler;
use Lubian\AttributeMagic\Infrastructure\Route\AsHandler;
use Lubian\AttributeMagic\Infrastructure\Route\HttpMethod;
final class Auchnoch
{
#[AsHandler(HttpMethod::GET, '/auchnoch')]
public function undso(): void
{
echo 'AUCHNOCH';
}
}

View file

@ -0,0 +1,17 @@
<?php declare(strict_types=1);
namespace Lubian\AttributeMagic\Handler;
use Lubian\AttributeMagic\Infrastructure\Route\AsHandler;
use Lubian\AttributeMagic\Infrastructure\Route\HttpMethod;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
final class HalloDimi
{
#[AsHandler(HttpMethod::GET, '/dimi')]
public function hallo(): Response
{
return new JsonResponse(['Hallo Dimi']);
}
}

24
app/src/Handler/Lol.php Normal file
View file

@ -0,0 +1,24 @@
<?php declare(strict_types=1);
namespace Lubian\AttributeMagic\Handler;
use Lubian\AttributeMagic\Infrastructure\Event\Dispatcher;
use Lubian\AttributeMagic\Infrastructure\Route\AsHandler;
use Lubian\AttributeMagic\Infrastructure\Route\HttpMethod;
use Lubian\AttributeMagic\Listener\LauschEvent;
final class Lol
{
public function __construct(
private readonly Dispatcher $dispatcher,
) {
}
#[AsHandler(HttpMethod::GET, '/')]
public function lol(): void
{
$event = new LauschEvent('aaaaaaaalso');
$this->dispatcher->dispatch($event);
echo $event->message;
}
}

View file

View 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,
) {
}
}

View 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],
);
}
}
}

View 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;
}
}

View file

@ -0,0 +1,8 @@
<?php declare(strict_types=1);
namespace Lubian\AttributeMagic\Infrastructure\Event;
abstract class GenericEvent
{
public bool $stopped = false;
}

View 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,
) {
}
}

View 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(),
];
}
}
}
}
}

View 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,
) {
}
}

View 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,
) {
}
}

View file

@ -0,0 +1,9 @@
<?php declare(strict_types=1);
namespace Lubian\AttributeMagic\Infrastructure\Route;
enum HttpMethod: string
{
case GET = 'GET';
case POST = 'POST';
}

View 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
) {
}
}

View file

@ -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();
}
}

View 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));
}
}

View 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),
);
}
}

View 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,
) {
}
}

View 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);
}
}

View 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]);
}
}

View 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,
)
);
}

View file

@ -0,0 +1,13 @@
<?php declare(strict_types=1);
namespace Lubian\AttributeMagic\Listener;
use Lubian\AttributeMagic\Infrastructure\Event\GenericEvent;
final class LauschEvent extends GenericEvent
{
public function __construct(
public string $message
) {
}
}

View file

@ -0,0 +1,15 @@
<?php declare(strict_types=1);
namespace Lubian\AttributeMagic\Listener;
use Lubian\AttributeMagic\Infrastructure\Event\AsListener;
final class Lauschdoch
{
#[AsListener(LauschEvent::class, 0)]
public function ichmachschon(LauschEvent $event): LauschEvent
{
$event->message = $event->message . ' lolduuuuu';
return $event;
}
}

57
app/test.php Normal file
View file

@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
$template = <<<'EOT'
<?php
declare(strict_types=1);
namespace Lubian\AttributeMagic\Handler\Viel;
use Lubian\AttributeMagic\Infrastructure\Route\AsHandler;
use Lubian\AttributeMagic\Infrastructure\Route\HttpMethod;
final class Template
{
#[AsHandler(HttpMethod::GET, '/Viel/template0')]
public function template0(): void {}
#[AsHandler(HttpMethod::GET, '/Viel/template1')]
public function template2(): void {}
#[AsHandler(HttpMethod::GET, '/Viel/template2')]
public function template3(): void {}
#[AsHandler(HttpMethod::GET, '/Viel/template3')]
public function template4(): void {}
#[AsHandler(HttpMethod::GET, '/Viel/template4')]
public function template5(): void {}
#[AsHandler(HttpMethod::GET, '/Viel/template5')]
public function template6(): void {}
#[AsHandler(HttpMethod::GET, '/Viel/template7')]
public function template7(): void {}
#[AsHandler(HttpMethod::GET, '/Viel/template8')]
public function template8(): void {}
#[AsHandler(HttpMethod::GET, '/Viel/template9')]
public function template9(): void {}
}
EOT;
function randomString(int $length = 20) {
$str = '';
$chars = 'abzdefghijklmnopqrstuvwxyz';
$chars = $chars . strtoupper($chars);
$chars = str_split($chars, 1);
$upper = count($chars) -1;
for ($i = 0; $i < $length; $i++) {
$int = random_int(0, $upper);
$str = $str . $chars[$int];
}
return $str;
}
for($c = 0; $c < 1000; $c++) {
$name = randomString();
$content = str_replace(['Template', 'template'], [$name, $name], $template);
file_put_contents(__DIR__ . '/src/Handler/Viel/' . $name . '.php', $content);
}

0
app/var/.gitkeep Normal file
View file