diff --git a/composer.json b/composer.json index 6e639d9..289b5b1 100644 --- a/composer.json +++ b/composer.json @@ -13,6 +13,8 @@ "minimum-stability": "stable", "require": { "php": "^7.4", + "ext-json": "*", + "k9u/request-mapper": "^0.11.0", "laminas/laminas-diactoros": "^2.3", "laminas/laminas-httphandlerrunner": "^1.2", "psr/http-factory": "^1.0", diff --git a/demo/src/AppModule.php b/demo/src/AppModule.php index bc3e54a..a7fa24b 100644 --- a/demo/src/AppModule.php +++ b/demo/src/AppModule.php @@ -5,15 +5,21 @@ namespace K9u\Framework\Demo; use K9u\Framework\FrameworkModule; +use K9u\RequestMapper\Annotation\AbstractMapping; use Ray\Di\AbstractModule; class AppModule extends AbstractModule { protected function configure() { + $this->bindInterceptor( + $this->matcher->any(), + $this->matcher->annotatedWith(AbstractMapping::class), + [FakeInterceptor::class] + ); + $middlewares = [ - FakeMiddleware::class, - FakeRequestHandler::class + FakeMiddleware::class ]; $this->install(new FrameworkModule($middlewares)); diff --git a/demo/src/FakeController.php b/demo/src/FakeController.php new file mode 100644 index 0000000..e4d01e0 --- /dev/null +++ b/demo/src/FakeController.php @@ -0,0 +1,47 @@ + + */ + public function index(): array + { + return ['path' => '/']; + } + + /** + * @GetMapping("/blogs") + * + * @return array + */ + public function getBlogs(): array + { + return ['path' => '/blogs']; + } + + /** + * @GetMapping("/blogs/{id}") + * + * @param ServerRequestInterface $request + * + * @return array + */ + public function getBlogById(ServerRequestInterface $request): array + { + $pathParams = $request->getAttribute(PathParams::class); + assert($pathParams instanceof PathParams); + + return ['path' => "/blogs/{$pathParams['id']}"]; + } +} diff --git a/demo/src/FakeInterceptor.php b/demo/src/FakeInterceptor.php new file mode 100644 index 0000000..6c6f714 --- /dev/null +++ b/demo/src/FakeInterceptor.php @@ -0,0 +1,22 @@ +proceed(); + assert(is_array($result)); + + $method = $invocation->getMethod(); + $class = $method->getDeclaringClass(); + + return $result + ['handler' => sprintf('%s::%s', $class->getName(), $method->getName())]; + } +} diff --git a/demo/src/FakeMiddleware.php b/demo/src/FakeMiddleware.php index 9ed85f7..38e5c9d 100644 --- a/demo/src/FakeMiddleware.php +++ b/demo/src/FakeMiddleware.php @@ -13,6 +13,12 @@ class FakeMiddleware implements MiddlewareInterface { public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { - return $handler->handle($request); + $response = $handler->handle($request); + + foreach ($request->getHeaders() as $name => $values) { + $response = $response->withHeader("X-Request-{$name}", $values); + } + + return $response; } } diff --git a/demo/src/FakeRequestHandler.php b/demo/src/FakeRequestHandler.php deleted file mode 100644 index 1945601..0000000 --- a/demo/src/FakeRequestHandler.php +++ /dev/null @@ -1,33 +0,0 @@ -responseFactory = $responseFactory; - $this->streamFactory = $streamFactory; - } - - public function handle(ServerRequestInterface $request): ResponseInterface - { - unset($request); // unused - - return $this->responseFactory->createResponse(200) - ->withHeader('Content-Type', 'application/json; charset=utf-8') - ->withBody($this->streamFactory->createStream('{"greeting": "Hello world !"}')); - } -} diff --git a/src/ExceptionHandler.php b/src/ExceptionHandler.php index 083f60f..9ac1d2a 100644 --- a/src/ExceptionHandler.php +++ b/src/ExceptionHandler.php @@ -4,6 +4,8 @@ namespace K9u\Framework; +use K9u\RequestMapper\Exception\HandlerNotFoundException; +use K9u\RequestMapper\Exception\MethodNotAllowedException; use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; @@ -24,6 +26,17 @@ public function __invoke(Throwable $throwable, ServerRequestInterface $request): error_log((string) $throwable); - return $this->responseFactory->createResponse(505); + return $this->responseFactory->createResponse(self::getStatusCode($throwable)); + } + + private static function getStatusCode(Throwable $throwable): int + { + if ($throwable instanceof HandlerNotFoundException) { + return 404; + } elseif ($throwable instanceof MethodNotAllowedException) { + return 405; + } + + return 500; } } diff --git a/src/FallbackHandler.php b/src/FallbackHandler.php deleted file mode 100644 index 9769361..0000000 --- a/src/FallbackHandler.php +++ /dev/null @@ -1,27 +0,0 @@ -responseFactory = $responseFactory; - } - - public function handle(ServerRequestInterface $request): ResponseInterface - { - unset($request); // unused - - return $this->responseFactory->createResponse(503); - } -} diff --git a/src/FrameworkModule.php b/src/FrameworkModule.php index 200eb24..4d755ec 100644 --- a/src/FrameworkModule.php +++ b/src/FrameworkModule.php @@ -4,6 +4,14 @@ namespace K9u\Framework; +use K9u\RequestMapper\HandlerClassFactoryInterface; +use K9u\RequestMapper\HandlerCollectorInterface; +use K9u\RequestMapper\HandlerInvoker; +use K9u\RequestMapper\HandlerInvokerInterface; +use K9u\RequestMapper\HandlerMethodArgumentsResolverInterface; +use K9u\RequestMapper\HandlerResolver; +use K9u\RequestMapper\HandlerResolverInterface; +use K9u\RequestMapper\OnDemandHandlerCollector; use Laminas\Diactoros\ResponseFactory; use Laminas\Diactoros\StreamFactory; use Psr\Http\Message\ResponseFactoryInterface; @@ -22,27 +30,30 @@ class FrameworkModule extends AbstractModule */ private array $middlewares; + private string $handlerDir; + /** * @param array $middlewares + * @param string|null $handlerDir * @param AbstractModule|null $module */ - public function __construct(array $middlewares = [], AbstractModule $module = null) + public function __construct(array $middlewares = [], string $handlerDir = null, AbstractModule $module = null) { - $this->middlewares = array_merge($middlewares, [FallbackHandler::class]); + $this->middlewares = array_merge($middlewares, [RequestMappingHandler::class]); + + $this->handlerDir = $handlerDir ?? dirname(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 0)[0]['file']); + assert(is_dir($this->handlerDir)); + parent::__construct($module); } protected function configure(): void { - foreach ($this->middlewares as $middleware) { - $this->bind($middleware)->in(Scope::SINGLETON); - } + self::bindHttpFactory($this); - $this->bind()->annotatedWith(MiddlewareCollection::class) - ->toInstance($this->middlewares); + self::bindRequestMapper($this, $this->handlerDir); - $this->bind(RequestHandlerInterface::class) - ->toProvider(RequestHandlerProvider::class)->in(Scope::SINGLETON); + self::bindRequestHandler($this, $this->middlewares); $this->bind(ExceptionHandlerInterface::class) ->to(ExceptionHandler::class)->in(Scope::SINGLETON); @@ -50,13 +61,63 @@ protected function configure(): void $this->bind(ResponseEmitterInterface::class) ->to(ResponseEmitter::class)->in(Scope::SINGLETON); - $this->bind(ResponseFactoryInterface::class) + $this->bind(ApplicationInterface::class) + ->to(Application::class)->in(Scope::SINGLETON); + } + + /** + * @param AbstractModule $module + */ + private static function bindHttpFactory(AbstractModule $module): void + { + $module->bind(ResponseFactoryInterface::class) ->to(ResponseFactory::class)->in(Scope::SINGLETON); - $this->bind(StreamFactoryInterface::class) + $module->bind(StreamFactoryInterface::class) ->to(StreamFactory::class)->in(Scope::SINGLETON); + } - $this->bind(ApplicationInterface::class) - ->to(Application::class)->in(Scope::SINGLETON); + /** + * @param AbstractModule $module + * @param string $handlerDir + */ + private static function bindRequestMapper(AbstractModule $module, string $handlerDir): void + { + $module->bind()->annotatedWith('handler_dir') + ->toInstance($handlerDir); + + // TODO: bind `CachedHandlerCollector` + $module->bind(HandlerCollectorInterface::class) + ->toConstructor(OnDemandHandlerCollector::class, ['baseDir' => 'handler_dir']) + ->in(Scope::SINGLETON); + + $module->bind(HandlerResolverInterface::class) + ->to(HandlerResolver::class)->in(Scope::SINGLETON); + + $module->bind(HandlerClassFactoryInterface::class) + ->to(HandlerClassFactory::class)->in(Scope::SINGLETON); + + $module->bind(HandlerMethodArgumentsResolverInterface::class) + ->to(HandlerMethodArgumentsResolver::class)->in(Scope::SINGLETON); + + $module->bind(HandlerInvokerInterface::class) + ->to(HandlerInvoker::class)->in(Scope::SINGLETON); + } + + /** + * @param AbstractModule $module + * @param array $middlewares + */ + private static function bindRequestHandler(AbstractModule $module, array $middlewares): void + { + foreach ($middlewares as $middleware) { + $module->bind($middleware)->in(Scope::SINGLETON); + } + + $module->bind()->annotatedWith('middleware_collection') + ->toInstance($middlewares); + + $module->bind(RequestHandlerInterface::class) + ->toProvider(RequestHandlerProvider::class)->in(Scope::SINGLETON); } } diff --git a/src/HandlerClassFactory.php b/src/HandlerClassFactory.php new file mode 100644 index 0000000..e2eeca5 --- /dev/null +++ b/src/HandlerClassFactory.php @@ -0,0 +1,23 @@ +injector = $injector; + } + + public function __invoke(string $class): object + { + return $this->injector->getInstance($class); + } +} diff --git a/src/HandlerMethodArgumentsResolver.php b/src/HandlerMethodArgumentsResolver.php new file mode 100644 index 0000000..8b35d24 --- /dev/null +++ b/src/HandlerMethodArgumentsResolver.php @@ -0,0 +1,42 @@ +withAttribute(PathParams::class, $pathParams); + + $args = []; + foreach ($method->getParameters() as $param) { + if ($param->hasType()) { + $type = $param->getType(); + assert($type instanceof ReflectionNamedType); + + if (is_a($type->getName(), ServerRequestInterface::class, true)) { + $args[$param->getName()] = $request; + continue; + } + } + + // TODO: assign Path params, Query params, Parsed body, ... + $args[$param->getName()] = $param->isDefaultValueAvailable() ? $param->getDefaultValue() : null; + } + + return new NamedArguments($args); + } +} diff --git a/src/MiddlewareCollection.php b/src/MiddlewareCollection.php deleted file mode 100644 index 76a9ed6..0000000 --- a/src/MiddlewareCollection.php +++ /dev/null @@ -1,17 +0,0 @@ - $middlewares * @param InjectorInterface $injector diff --git a/src/RequestMappingHandler.php b/src/RequestMappingHandler.php new file mode 100644 index 0000000..249da13 --- /dev/null +++ b/src/RequestMappingHandler.php @@ -0,0 +1,71 @@ +responseFactory = $responseFactory; + $this->streamFactory = $streamFactory; + $this->handlerResolver = $handlerResolver; + $this->handlerInvoker = $handlerInvoker; + } + + /** + * @param ServerRequestInterface $request + * + * @return ResponseInterface + * @throws HandlerNotFoundException + * @throws MethodNotAllowedException + */ + public function handle(ServerRequestInterface $request): ResponseInterface + { + $handler = ($this->handlerResolver)($request); + $invoked = ($this->handlerInvoker)($handler, $request); + + if ($invoked instanceof ResponseInterface) { + return $invoked; + } + + // TODO: detect content-type + try { + $json = json_encode($invoked, JSON_THROW_ON_ERROR); + } catch (JsonException $e) { + throw new LogicException('can not encode to JSON', 0, $e); + } + + $contentType = 'application/json; charset=utf-8'; + $body = $this->streamFactory->createStream($json); + + return $this->responseFactory->createResponse(200) + ->withHeader('Content-Type', $contentType) + ->withBody($body); + } +}