From 9977acfaa276dd5b32a17b10e68191981c4b6583 Mon Sep 17 00:00:00 2001 From: "Kouhei.Sano" Date: Sat, 3 Aug 2024 18:11:44 +0900 Subject: [PATCH 1/6] refactor: --- README.md | 5 ++ .../Shared/DbConnectionInterface.php | 14 ++++++ .../Persistence/DbConnection.php | 43 +++++++++++++++++ source/app/env.dist.json | 4 +- source/app/phpcs.xml | 1 + source/app/phpstan.neon | 1 + source/app/psalm.xml | 1 + .../src/Auth/AdminAuthenticationHandler.php | 7 ++- source/app/src/DiBinder.php | 46 +++++++++++++++---- source/app/src/RequestDispatcher.php | 10 ++-- source/app/var/conf/aura.route.web.php | 7 ++- 11 files changed, 117 insertions(+), 22 deletions(-) create mode 100644 source/app/ddd/core/src/Application/Shared/DbConnectionInterface.php create mode 100644 source/app/ddd/core/src/Infrastructure/Persistence/DbConnection.php diff --git a/README.md b/README.md index c24e250..62b2f03 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ Aura.PHP application ### Features * ID & password login +* Cloudflare turnstile check * Flash message ### Architecture @@ -49,3 +50,7 @@ sequenceDiagram ```bash composer run-script cli get /hello ``` + +## Cloudflare Turnstile + +* [Dummy sitekeys and secret keys](https://developers.cloudflare.com/turnstile/troubleshooting/testing/) diff --git a/source/app/ddd/core/src/Application/Shared/DbConnectionInterface.php b/source/app/ddd/core/src/Application/Shared/DbConnectionInterface.php new file mode 100644 index 0000000..3233d6f --- /dev/null +++ b/source/app/ddd/core/src/Application/Shared/DbConnectionInterface.php @@ -0,0 +1,14 @@ +pdo->inTransaction()) { + return; + } + + $this->pdo->beginTransaction(); + } + + public function commit(): void + { + if (! $this->pdo->inTransaction()) { + return; + } + + $this->pdo->commit(); + } + + public function rollback(): void + { + if (! $this->pdo->inTransaction()) { + return; + } + + $this->pdo->rollBack(); + } +} diff --git a/source/app/env.dist.json b/source/app/env.dist.json index c3629d7..5a36f0a 100644 --- a/source/app/env.dist.json +++ b/source/app/env.dist.json @@ -1,8 +1,8 @@ { "$schema": "./env.schema.json", "ADMIN_PREFIX": "upvQzoCTaaYrTDP7", - "CLOUDFLARE_TURNSTILE_SECRET_KEY": "", - "CLOUDFLARE_TURNSTILE_SITE_KEY": "", + "CLOUDFLARE_TURNSTILE_SECRET_KEY": "1x0000000000000000000000000000000AA", + "CLOUDFLARE_TURNSTILE_SITE_KEY": "1x00000000000000000000AA", "DB_DSN": "mysql:host=aura-mysql:3306;dbname=aura_db", "DB_PASS": "passw0rd", "DB_USER": "aura", diff --git a/source/app/phpcs.xml b/source/app/phpcs.xml index fcee186..f359471 100644 --- a/source/app/phpcs.xml +++ b/source/app/phpcs.xml @@ -19,6 +19,7 @@ tests */tmp/* */Fake/* + ddd/core/src diff --git a/source/app/phpstan.neon b/source/app/phpstan.neon index 13ec8a3..52dd4c7 100644 --- a/source/app/phpstan.neon +++ b/source/app/phpstan.neon @@ -3,3 +3,4 @@ parameters: paths: - src - tests + - ddd/core/src diff --git a/source/app/psalm.xml b/source/app/psalm.xml index 45a660d..d6fc530 100644 --- a/source/app/psalm.xml +++ b/source/app/psalm.xml @@ -13,5 +13,6 @@ + diff --git a/source/app/src/Auth/AdminAuthenticationHandler.php b/source/app/src/Auth/AdminAuthenticationHandler.php index a600a6b..7341c87 100644 --- a/source/app/src/Auth/AdminAuthenticationHandler.php +++ b/source/app/src/Auth/AdminAuthenticationHandler.php @@ -9,6 +9,7 @@ use Aura\Auth\Exception\PasswordMissing as AuraPasswordMissing; use Aura\Auth\Exception\UsernameMissing as AuraUsernameMissing; use Aura\Auth\Exception\UsernameNotFound as AuraUsernameNotFound; +use Koriym\HttpConstants\Method; use Laminas\Diactoros\Response\RedirectResponse; use MyVendor\MyPackage\Router\RouterMatch; use Psr\Http\Message\ResponseInterface; @@ -95,7 +96,8 @@ private function isLogin(RouterMatch $routerMatch): bool return is_array($auth) && isset($auth['login']) && is_bool($auth['login']) && - $auth['login']; + $auth['login'] && + $routerMatch->method === Method::POST; } private function isLogout(RouterMatch $routerMatch): bool @@ -109,6 +111,7 @@ private function isLogout(RouterMatch $routerMatch): bool return is_array($auth) && isset($auth['logout']) && is_bool($auth['logout']) && - $auth['logout']; + $auth['logout'] && + $routerMatch->method === Method::POST; } } diff --git a/source/app/src/DiBinder.php b/source/app/src/DiBinder.php index 54abf72..bcac0f1 100644 --- a/source/app/src/DiBinder.php +++ b/source/app/src/DiBinder.php @@ -4,7 +4,9 @@ namespace MyVendor\MyPackage; +use AppCore\Application\Shared\DbConnectionInterface; use AppCore\Domain\Hasher\PasswordHasher; +use AppCore\Infrastructure\Persistence\DbConnection; use Aura\Accept\Accept; use Aura\Accept\AcceptFactory; use Aura\Di\Container; @@ -16,7 +18,15 @@ use Aura\Router\RouterContainer; use Aura\Session\Session; use Aura\Session\SessionFactory; +use Aura\Sql\ExtendedPdo; +use Aura\Sql\ExtendedPdoInterface; +use Aura\SqlQuery\Common\DeleteInterface; +use Aura\SqlQuery\Common\InsertInterface; +use Aura\SqlQuery\Common\SelectInterface; +use Aura\SqlQuery\Common\UpdateInterface; +use Aura\SqlQuery\QueryFactory; use Koriym\QueryLocator\QueryLocator; +use Koriym\QueryLocator\QueryLocatorInterface; use Laminas\Diactoros\ServerRequestFactory; use MyVendor\MyPackage\Auth\AdminAuthenticationHandler; use MyVendor\MyPackage\Auth\AdminAuthenticator; @@ -57,15 +67,15 @@ public function __invoke(string $appDir, string $tmpDir): Container $di = $builder->newInstance(true); // NOTE: "$di->types['xxx']" を使うために有効化 $di->values['timestamp'] = time(); - $di->values['baseUrl'] = getenv('BASE_URL'); + $di->values['siteUrl'] = getenv('SITE_URL'); $di->values['pdoDsn'] = getenv('DB_DSN'); $di->values['pdoUsername'] = getenv('DB_USER'); $di->values['pdoPassword'] = getenv('DB_PASS'); $this->appMeta($di, $appDir, $tmpDir); $this->authentication($di); + $this->db($di, $appDir); $this->form($di); - $this->queryLocator($di, $appDir); $this->renderer($di, $appDir); $this->request($di); $this->requestDispatcher($di); @@ -101,6 +111,29 @@ private function authentication(Container $di): void $di->types[AdminAuthenticatorInterface::class] = $di->lazyGet(AdminAuthenticator::class); } + private function db(Container $di, string $appDir): void + { + $di->params[ExtendedPdo::class]['dsn'] = $di->lazyValue('pdoDsn'); + $di->params[ExtendedPdo::class]['username'] = $di->lazyValue('pdoUsername'); + $di->params[ExtendedPdo::class]['password'] = $di->lazyValue('pdoPassword'); + $di->set(ExtendedPdoInterface::class, $di->lazyNew(ExtendedPdo::class)); + $di->types[ExtendedPdoInterface::class] = $di->lazyGet(ExtendedPdoInterface::class); + + $di->types[SelectInterface::class] = $di->lazy(static fn () => (new QueryFactory('mysql'))->newSelect()); + $di->types[InsertInterface::class] = $di->lazy(static fn () => (new QueryFactory('mysql'))->newInsert()); + $di->types[UpdateInterface::class] = $di->lazy(static fn () => (new QueryFactory('mysql'))->newUpdate()); + $di->types[DeleteInterface::class] = $di->lazy(static fn () => (new QueryFactory('mysql'))->newDelete()); + + $di->types[DbConnectionInterface::class] = $di->lazyNew(DbConnection::class); + + $di->values['sqlDir'] = $appDir . '/var/sql'; + $di->params[QueryLocator::class]['sqlDir'] = $di->lazyValue('sqlDir'); + + $di->set(QueryLocatorInterface::class, $di->lazyNew(QueryLocator::class)); + + $di->types[QueryLocatorInterface::class] = $di->lazyGet(QueryLocatorInterface::class); + } + private function form(Container $di): void { $di->params[ExtendedForm::class]['builder'] = $di->lazyNew(Builder::class); @@ -114,13 +147,6 @@ private function form(Container $di): void $di->types[LoginForm::class] = $di->lazyNew(LoginForm::class); } - private function queryLocator(Container $di, string $appDir): void - { - $di->values['sqlDir'] = $appDir . '/var/sql'; - - $di->params[QueryLocator::class]['sqlDir'] = $di->lazyValue('sqlDir'); - } - private function renderer(Container $di, string $appDir): void { $di->params[QiqRenderer::class]['template'] = $di->lazy(static function () use ($appDir, $di) { @@ -137,7 +163,7 @@ private function renderer(Container $di, string $appDir): void ); }); $di->params[QiqRenderer::class]['data'] = $di->lazyArray([ - 'baseUrl' => $di->lazyValue('baseUrl'), + 'siteUrl' => $di->lazyValue('siteUrl'), 'timestamp' => $di->lazyValue('timestamp'), ]); $di->set(QiqRenderer::class, $di->lazyNew(QiqRenderer::class)); diff --git a/source/app/src/RequestDispatcher.php b/source/app/src/RequestDispatcher.php index 82d72b6..83f34d6 100644 --- a/source/app/src/RequestDispatcher.php +++ b/source/app/src/RequestDispatcher.php @@ -9,6 +9,7 @@ use Aura\Di\Container; use Koriym\HttpConstants\MediaType; use Koriym\HttpConstants\Method; +use Koriym\HttpConstants\ResponseHeader; use Koriym\HttpConstants\StatusCode; use Laminas\Diactoros\Response; use Laminas\Diactoros\Response\RedirectResponse; @@ -35,6 +36,7 @@ use function assert; use function call_user_func_array; use function class_exists; +use function is_bool; use function is_callable; use function is_string; use function method_exists; @@ -61,7 +63,7 @@ public function __invoke(): ResponseInterface|null { $routerMatch = $this->router->match($this->serverRequest); $route = $routerMatch->route; - if ($route === false) { + if (is_bool($route)) { return new TextResponse( 'Route not found :(', StatusCode::NOT_FOUND, @@ -145,16 +147,16 @@ public function __invoke(): ResponseInterface|null // NOTE: RequestHandler で ServerRequest や Route の取得をしたい場合は "Typehinted constructor" を使う $callable = [$object, $action]; if (is_callable($callable)) { - call_user_func_array($callable, $route->attributes); + $object = call_user_func_array($callable, $route->attributes); } if (! $object instanceof RequestHandler) { throw new InvalidResponseException('Invalid response type.'); } - if (isset($object->headers['location'])) { + if (isset($object->headers[ResponseHeader::LOCATION])) { return new RedirectResponse( - $object->headers['location'], + $object->headers[ResponseHeader::LOCATION], $object->code, $object->headers, ); diff --git a/source/app/var/conf/aura.route.web.php b/source/app/var/conf/aura.route.web.php index 0474775..21ea70d 100644 --- a/source/app/var/conf/aura.route.web.php +++ b/source/app/var/conf/aura.route.web.php @@ -3,6 +3,7 @@ declare(strict_types=1); use Aura\Router\Map; +use Koriym\HttpConstants\Method; use MyVendor\MyPackage\Handler; use MyVendor\MyPackage\Handler\Admin as AdminHandler; @@ -24,10 +25,8 @@ $map->auth($auth); $map->get('/login', '/login', AdminHandler\Login::class) - ->auth([]); - - $map->post('/_login', '/login', AdminHandler\Login::class) - ->auth(['login' => true]); + ->auth(['login' => true]) + ->allows([Method::POST]); $map->post('/logout', '/logout', AdminHandler\Logout::class) ->auth(array_merge($auth, ['logout' => true])); From 70fa2e454f7bc974440aa3ae7096419ea11d75e5 Mon Sep 17 00:00:00 2001 From: "Kouhei.Sano" Date: Thu, 8 Aug 2024 15:06:09 +0900 Subject: [PATCH 2/6] refactor: --- source/app/src/RequestDispatcher.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/source/app/src/RequestDispatcher.php b/source/app/src/RequestDispatcher.php index 83f34d6..d3fc205 100644 --- a/source/app/src/RequestDispatcher.php +++ b/source/app/src/RequestDispatcher.php @@ -36,6 +36,7 @@ use function assert; use function call_user_func_array; use function class_exists; +use function is_array; use function is_bool; use function is_callable; use function is_string; @@ -139,6 +140,14 @@ public function __invoke(): ResponseInterface|null // NOTE: Request handling $action = sprintf('on%s', ucfirst(strtolower($routerMatch->method))); + if ( + $serverRequest->getMethod() === Method::POST && + is_array($serverRequest->getParsedBody()) && + isset($serverRequest->getParsedBody()['_method']) + ) { + $action = sprintf('on%s', ucfirst(strtolower($serverRequest->getParsedBody()['_method'])));; + } + if (! method_exists($object, $action)) { throw new RouteHandlerMethodNotAllowedException('Method not allowed.'); } From 72a9b5983e974b775a0d4691ea26cacd95f0db0c Mon Sep 17 00:00:00 2001 From: "Kouhei.Sano" Date: Thu, 8 Aug 2024 15:11:18 +0900 Subject: [PATCH 3/6] refactor: --- source/app/src/RequestDispatcher.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/app/src/RequestDispatcher.php b/source/app/src/RequestDispatcher.php index d3fc205..ac2a751 100644 --- a/source/app/src/RequestDispatcher.php +++ b/source/app/src/RequestDispatcher.php @@ -145,7 +145,7 @@ public function __invoke(): ResponseInterface|null is_array($serverRequest->getParsedBody()) && isset($serverRequest->getParsedBody()['_method']) ) { - $action = sprintf('on%s', ucfirst(strtolower($serverRequest->getParsedBody()['_method'])));; + $action = sprintf('on%s', ucfirst(strtolower($serverRequest->getParsedBody()['_method']))); } if (! method_exists($object, $action)) { From 6306c64dc05914d8fd3a7cccb0860dfca0405f42 Mon Sep 17 00:00:00 2001 From: "Kouhei.Sano" Date: Thu, 8 Aug 2024 15:19:45 +0900 Subject: [PATCH 4/6] refactor: --- source/app/src/RequestDispatcher.php | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/source/app/src/RequestDispatcher.php b/source/app/src/RequestDispatcher.php index ac2a751..573490f 100644 --- a/source/app/src/RequestDispatcher.php +++ b/source/app/src/RequestDispatcher.php @@ -29,6 +29,7 @@ use MyVendor\MyPackage\Router\RouteHandlerMethodNotAllowedException; use MyVendor\MyPackage\Router\RouteHandlerNotFoundException; use MyVendor\MyPackage\Router\RouterInterface; +use MyVendor\MyPackage\Router\RouterMatch; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Throwable; @@ -139,7 +140,7 @@ public function __invoke(): ResponseInterface|null } // NOTE: Request handling - $action = sprintf('on%s', ucfirst(strtolower($routerMatch->method))); + $action = sprintf('on%s', ucfirst(strtolower($this->getMethod($routerMatch)))); if ( $serverRequest->getMethod() === Method::POST && is_array($serverRequest->getParsedBody()) && @@ -197,6 +198,20 @@ private function getResponse(RequestHandler $object): ResponseInterface return $response->withStatus($object->code); } + private function getMethod(RouterMatch $routerMatch): string + { + if ($this->serverRequest->getMethod() !== Method::POST) { + return $routerMatch->method; + } + + $body = $this->serverRequest->getParsedBody(); + if (! is_array($body) || ! isset($body['_method'])) { + return $routerMatch->method; + } + + return $body['_method']; + } + private function getRenderer(): RendererInterface { $media = $this->accept->negotiateMedia([ From e38877f3ef8bb7e9754d5634d3b465f96196ed56 Mon Sep 17 00:00:00 2001 From: "Kouhei.Sano" Date: Fri, 16 Aug 2024 18:14:31 +0900 Subject: [PATCH 5/6] refactor: --- source/app/src/DiBinder.php | 2 +- .../app/src/Router/ServerRequestFactory.php | 73 +++++++++++++++++++ source/app/src/Router/WebRouter.php | 67 +---------------- 3 files changed, 75 insertions(+), 67 deletions(-) create mode 100644 source/app/src/Router/ServerRequestFactory.php diff --git a/source/app/src/DiBinder.php b/source/app/src/DiBinder.php index bcac0f1..cafbf70 100644 --- a/source/app/src/DiBinder.php +++ b/source/app/src/DiBinder.php @@ -27,7 +27,6 @@ use Aura\SqlQuery\QueryFactory; use Koriym\QueryLocator\QueryLocator; use Koriym\QueryLocator\QueryLocatorInterface; -use Laminas\Diactoros\ServerRequestFactory; use MyVendor\MyPackage\Auth\AdminAuthenticationHandler; use MyVendor\MyPackage\Auth\AdminAuthenticator; use MyVendor\MyPackage\Auth\AdminAuthenticatorInterface; @@ -44,6 +43,7 @@ use MyVendor\MyPackage\Responder\WebResponder; use MyVendor\MyPackage\Router\CliRouter; use MyVendor\MyPackage\Router\RouterInterface; +use MyVendor\MyPackage\Router\ServerRequestFactory; use MyVendor\MyPackage\Router\WebRouter; use MyVendor\MyPackage\TemplateEngine\QiqCustomHelper; use MyVendor\MyPackage\TemplateEngine\QiqRenderer; diff --git a/source/app/src/Router/ServerRequestFactory.php b/source/app/src/Router/ServerRequestFactory.php new file mode 100644 index 0000000..4aec3bb --- /dev/null +++ b/source/app/src/Router/ServerRequestFactory.php @@ -0,0 +1,73 @@ +getHeader('content-type'), + true, + ); + if ($isFormUrlEncoded) { + return self::parseFormUrlEncoded($serverRequest); + } + + $isJson = in_array( + 'application/json', + $serverRequest->getHeader('content-type'), + true, + ); + if (! $isJson) { + return $serverRequest; + } + + return self::parseJson($serverRequest); + } + + private static function parseFormUrlEncoded(ServerRequestInterface $serverRequest): ServerRequestInterface + { + parse_str((string) $serverRequest->getBody(), $parsedBody); + + return $serverRequest->withParsedBody($parsedBody); + } + + private static function parseJson(ServerRequestInterface $serverRequest): ServerRequestInterface + { + $parsedBody = json_decode( + (string) $serverRequest->getBody(), + true, + 512, + JSON_THROW_ON_ERROR, + ); + + $error = json_last_error(); + if ($error !== JSON_ERROR_NONE) { + throw new InvalidRequestException(json_last_error_msg()); + } + + assert(is_array($parsedBody)); + + return $serverRequest->withParsedBody($parsedBody); + } +} diff --git a/source/app/src/Router/WebRouter.php b/source/app/src/Router/WebRouter.php index ca56b94..85bc719 100644 --- a/source/app/src/Router/WebRouter.php +++ b/source/app/src/Router/WebRouter.php @@ -8,17 +8,6 @@ use Aura\Router\RouterContainer; use Psr\Http\Message\ServerRequestInterface; -use function assert; -use function in_array; -use function is_array; -use function json_decode; -use function json_last_error; -use function json_last_error_msg; -use function parse_str; - -use const JSON_ERROR_NONE; -use const JSON_THROW_ON_ERROR; - final class WebRouter implements RouterInterface { public function __construct( @@ -30,66 +19,12 @@ public function match(ServerRequestInterface $serverRequest): RouterMatch { $matcher = $this->routerContainer->getMatcher(); - $isFormUrlEncoded = in_array( - 'application/x-www-form-urlencoded', - $serverRequest->getHeader('content-type'), - true, - ); - if ($isFormUrlEncoded) { - return new RouterMatch( - $serverRequest->getMethod(), - $serverRequest->getUri()->getPath(), - $matcher->match($serverRequest), - $this->parseFormUrlEncoded($serverRequest), - ); - } - - $isJson = in_array( - 'application/json', - $serverRequest->getHeader('content-type'), - true, - ); - if (! $isJson) { - return new RouterMatch( - $serverRequest->getMethod(), - $serverRequest->getUri()->getPath(), - $matcher->match($serverRequest), - $serverRequest, - ); - } - return new RouterMatch( $serverRequest->getMethod(), $serverRequest->getUri()->getPath(), $matcher->match($serverRequest), - $this->parseJson($serverRequest), - ); - } - - private function parseFormUrlEncoded(ServerRequestInterface $serverRequest): ServerRequestInterface - { - parse_str((string) $serverRequest->getBody(), $parsedBody); - - return $serverRequest->withParsedBody($parsedBody); - } - - private function parseJson(ServerRequestInterface $serverRequest): ServerRequestInterface - { - $parsedBody = json_decode( - (string) $serverRequest->getBody(), - true, - 512, - JSON_THROW_ON_ERROR, + $serverRequest, ); - - $error = json_last_error(); - if ($error !== JSON_ERROR_NONE) { - throw new InvalidRequestException(json_last_error_msg()); - } - - assert(is_array($parsedBody)); - - return $serverRequest->withParsedBody($parsedBody); } /** @param array $data */ From c31fc14804abeba13acc90e9e622dd2ccb436d9c Mon Sep 17 00:00:00 2001 From: "Kouhei.Sano" Date: Fri, 6 Dec 2024 14:52:31 +0900 Subject: [PATCH 6/6] feat: Add stream support --- source/app/src/Handler/Download.php | 36 +++++++++++++++++++++++ source/app/src/RequestDispatcher.php | 21 ++++++++++--- source/app/src/RequestHandler.php | 3 ++ source/app/src/Responder/WebResponder.php | 4 +-- source/app/var/conf/aura.route.web.php | 2 ++ 5 files changed, 59 insertions(+), 7 deletions(-) create mode 100644 source/app/src/Handler/Download.php diff --git a/source/app/src/Handler/Download.php b/source/app/src/Handler/Download.php new file mode 100644 index 0000000..e9a85f5 --- /dev/null +++ b/source/app/src/Handler/Download.php @@ -0,0 +1,36 @@ +headers = [ + 'Content-Type' => 'application/octet-stream', + 'Content-Disposition' => 'attachment; filename=dummy.csv', + 'Content-Transfer-Encoding' => 'binary', + ]; + $this->stream = $stream; + + fputcsv($stream, [ + 'ID', + 'NAME', + ]); + + return $this; + } +} diff --git a/source/app/src/RequestDispatcher.php b/source/app/src/RequestDispatcher.php index 573490f..3981f57 100644 --- a/source/app/src/RequestDispatcher.php +++ b/source/app/src/RequestDispatcher.php @@ -14,6 +14,7 @@ use Laminas\Diactoros\Response; use Laminas\Diactoros\Response\RedirectResponse; use Laminas\Diactoros\Response\TextResponse; +use Laminas\Diactoros\Stream; use MyVendor\MyPackage\Auth\AdminAuthenticationHandler; use MyVendor\MyPackage\Auth\AdminAuthenticationRequestHandlerInterface; use MyVendor\MyPackage\Auth\AuthenticationException; @@ -40,6 +41,7 @@ use function is_array; use function is_bool; use function is_callable; +use function is_resource; use function is_string; use function method_exists; use function sprintf; @@ -141,12 +143,13 @@ public function __invoke(): ResponseInterface|null // NOTE: Request handling $action = sprintf('on%s', ucfirst(strtolower($this->getMethod($routerMatch)))); + $parsedBody = $serverRequest->getParsedBody(); if ( - $serverRequest->getMethod() === Method::POST && - is_array($serverRequest->getParsedBody()) && - isset($serverRequest->getParsedBody()['_method']) + is_array($parsedBody) && + isset($parsedBody['_method']) && + $serverRequest->getMethod() === Method::POST ) { - $action = sprintf('on%s', ucfirst(strtolower($serverRequest->getParsedBody()['_method']))); + $action = sprintf('on%s', ucfirst(strtolower($parsedBody['_method']))); } if (! method_exists($object, $action)) { @@ -190,6 +193,16 @@ private function getResponse(RequestHandler $object): ResponseInterface assert($renderer instanceof RendererInterface); $response = new Response(); + + if (is_resource($object->stream)) { + $response = $response->withBody(new Stream($object->stream)); + foreach ($object->headers as $name => $value) { + $response = $response->withHeader($name, $value); + } + + return $response->withStatus($object->code); + } + $response->getBody()->write($renderer->render($object)); foreach ($object->headers as $name => $value) { $response = $response->withHeader($name, $value); diff --git a/source/app/src/RequestHandler.php b/source/app/src/RequestHandler.php index 48533ee..341a258 100644 --- a/source/app/src/RequestHandler.php +++ b/source/app/src/RequestHandler.php @@ -18,6 +18,9 @@ class RequestHandler implements Stringable /** @var array|null */ public array|null $body = null; + + /** @var resource|null */ + public $stream; public RendererInterface|null $renderer = null; public string|null $string = null; diff --git a/source/app/src/Responder/WebResponder.php b/source/app/src/Responder/WebResponder.php index 9a9646a..e7b7904 100644 --- a/source/app/src/Responder/WebResponder.php +++ b/source/app/src/Responder/WebResponder.php @@ -10,8 +10,6 @@ use function http_response_code; use function sprintf; -use const PHP_EOL; - final class WebResponder implements ResponderInterface { public function handle(ResponseInterface $response): void @@ -24,6 +22,6 @@ public function handle(ResponseInterface $response): void } } - echo $response->getBody() . PHP_EOL; + echo $response->getBody(); } } diff --git a/source/app/var/conf/aura.route.web.php b/source/app/var/conf/aura.route.web.php index 21ea70d..77b6f70 100644 --- a/source/app/var/conf/aura.route.web.php +++ b/source/app/var/conf/aura.route.web.php @@ -18,6 +18,8 @@ $map->get('hello', '/hello', Handler\Hello::class) ->extras(['a' => 'b']); + + $map->get('download', '/download', Handler\Download::class); }); $map->attach('/admin', '/' . $adminPrefix, function (Map $map) {