Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Chore #3

Merged
merged 6 commits into from
Dec 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Aura.PHP application
### Features

* ID & password login
* Cloudflare turnstile check
* Flash message

### Architecture
Expand All @@ -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/)
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

declare(strict_types=1);

namespace AppCore\Application\Shared;

interface DbConnectionInterface
{
public function begin(): void;

public function commit(): void;

public function rollback(): void;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

declare(strict_types=1);

namespace AppCore\Infrastructure\Persistence;

use AppCore\Application\Shared\DbConnectionInterface;
use Aura\Sql\ExtendedPdoInterface;

final class DbConnection implements DbConnectionInterface
{
public function __construct(
private ExtendedPdoInterface $pdo,
) {
}

public function begin(): void
{
if ($this->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();
}
}
4 changes: 2 additions & 2 deletions source/app/env.dist.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
1 change: 1 addition & 0 deletions source/app/phpcs.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
<file>tests</file>
<exclude-pattern>*/tmp/*</exclude-pattern>
<exclude-pattern>*/Fake/*</exclude-pattern>
<file>ddd/core/src</file>

<!-- PSR12 Coding Standard -->
<rule ref="PSR12"/>
Expand Down
1 change: 1 addition & 0 deletions source/app/phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ parameters:
paths:
- src
- tests
- ddd/core/src
1 change: 1 addition & 0 deletions source/app/psalm.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@
<ignoreFiles>
<directory name="vendor" />
</ignoreFiles>
<directory name="ddd/core/src" />
</projectFiles>
</psalm>
7 changes: 5 additions & 2 deletions source/app/src/Auth/AdminAuthenticationHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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;
}
}
48 changes: 37 additions & 11 deletions source/app/src/DiBinder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -16,8 +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 Laminas\Diactoros\ServerRequestFactory;
use Koriym\QueryLocator\QueryLocatorInterface;
use MyVendor\MyPackage\Auth\AdminAuthenticationHandler;
use MyVendor\MyPackage\Auth\AdminAuthenticator;
use MyVendor\MyPackage\Auth\AdminAuthenticatorInterface;
Expand All @@ -34,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;
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -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) {
Expand All @@ -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));
Expand Down
36 changes: 36 additions & 0 deletions source/app/src/Handler/Download.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

namespace MyVendor\MyPackage\Handler;

use AppCore\Exception\RuntimeException;
use MyVendor\MyPackage\RequestHandler;

use function fopen;
use function fputcsv;

final class Download extends RequestHandler
{
public function onGet(): self
{
$stream = fopen('php://temp', 'wb');
if ($stream === false) {
throw new RuntimeException();
}

$this->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;
}
}
49 changes: 44 additions & 5 deletions source/app/src/RequestDispatcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@
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;
use Laminas\Diactoros\Response\TextResponse;
use Laminas\Diactoros\Stream;
use MyVendor\MyPackage\Auth\AdminAuthenticationHandler;
use MyVendor\MyPackage\Auth\AdminAuthenticationRequestHandlerInterface;
use MyVendor\MyPackage\Auth\AuthenticationException;
Expand All @@ -28,14 +30,18 @@
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;

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_resource;
use function is_string;
use function method_exists;
use function sprintf;
Expand All @@ -61,7 +67,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,
Expand Down Expand Up @@ -136,7 +142,16 @@ 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))));
$parsedBody = $serverRequest->getParsedBody();
if (
is_array($parsedBody) &&
isset($parsedBody['_method']) &&
$serverRequest->getMethod() === Method::POST
) {
$action = sprintf('on%s', ucfirst(strtolower($parsedBody['_method'])));
}

if (! method_exists($object, $action)) {
throw new RouteHandlerMethodNotAllowedException('Method not allowed.');
}
Expand All @@ -145,16 +160,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,
);
Expand All @@ -178,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);
Expand All @@ -186,6 +211,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([
Expand Down
Loading
Loading