This commit is contained in:
30
src/Core/Container.php
Normal file
30
src/Core/Container.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Core;
|
||||
|
||||
use DI\Container as DIContainer;
|
||||
use DI\ContainerBuilder;
|
||||
|
||||
/** @codeCoverageIgnore */
|
||||
final class Container
|
||||
{
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public static ?DIContainer $container = null;
|
||||
|
||||
public static function getInstance(): DIContainer
|
||||
{
|
||||
if (null === self::$container) {
|
||||
$container = (new ContainerBuilder())
|
||||
->addDefinitions(__DIR__.'/../../config/di.php');
|
||||
|
||||
self::$container = $container->build();
|
||||
}
|
||||
|
||||
return self::$container;
|
||||
}
|
||||
}
|
||||
41
src/Core/EnvironmentLoader.php
Normal file
41
src/Core/EnvironmentLoader.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Core;
|
||||
|
||||
use App\Enums\Environment;
|
||||
use Dotenv\Dotenv;
|
||||
|
||||
/** @codeCoverageIgnore */
|
||||
final class EnvironmentLoader
|
||||
{
|
||||
public function __construct(private readonly string $rootDir)
|
||||
{
|
||||
}
|
||||
|
||||
public function load(?Environment $environment = null): void
|
||||
{
|
||||
$filename = $this->determineFileName($environment);
|
||||
|
||||
if (!file_exists($this->rootDir.'/'.$filename)) {
|
||||
throw new \RuntimeException(\sprintf('A %s file is necessary to run this application', $filename));
|
||||
}
|
||||
|
||||
$dotenv = Dotenv::createImmutable($this->rootDir, $filename);
|
||||
|
||||
$dotenv->load();
|
||||
|
||||
$dotenv
|
||||
->required('ENVIRONMENT')
|
||||
->allowedValues(array_map(static fn (Environment $environment): string => $environment->value, Environment::cases()));
|
||||
}
|
||||
|
||||
private function determineFileName(?Environment $environment = null): string
|
||||
{
|
||||
return match ($environment) {
|
||||
Environment::Testing => '.env.testing',
|
||||
default => '.env',
|
||||
};
|
||||
}
|
||||
}
|
||||
23
src/Enums/Environment.php
Normal file
23
src/Enums/Environment.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
/** @codeCoverageIgnore */
|
||||
enum Environment: string
|
||||
{
|
||||
case Development = 'development';
|
||||
case Production = 'production';
|
||||
case Testing = 'testing';
|
||||
|
||||
public function isCurrent(): bool
|
||||
{
|
||||
return $this->value === $_ENV['ENVIRONMENT'];
|
||||
}
|
||||
|
||||
public static function current(): self
|
||||
{
|
||||
return self::from($_ENV['ENVIRONMENT']);
|
||||
}
|
||||
}
|
||||
17
src/Http/Controllers/HomeController.php
Normal file
17
src/Http/Controllers/HomeController.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Response\View;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
final class HomeController
|
||||
{
|
||||
/** @param array<string, mixed> $args */
|
||||
public function __invoke(ServerRequestInterface $request, array $args = []): View
|
||||
{
|
||||
return view('home');
|
||||
}
|
||||
}
|
||||
33
src/Http/ExceptionHandler.php
Normal file
33
src/Http/ExceptionHandler.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http;
|
||||
|
||||
use Laminas\HttpHandlerRunner\Emitter\SapiEmitter;
|
||||
use League\Route\Http\Exception;
|
||||
|
||||
/** @codeCoverageIgnore */
|
||||
final class ExceptionHandler
|
||||
{
|
||||
public function __construct(
|
||||
private readonly SapiEmitter $emitter
|
||||
) {
|
||||
}
|
||||
|
||||
public function handle(\Throwable $error): void
|
||||
{
|
||||
$this
|
||||
->emitter
|
||||
->emit(
|
||||
view(
|
||||
'error',
|
||||
[
|
||||
'status' => $statusCode = $error instanceof Exception ? $error->getStatusCode() : 500,
|
||||
'message' => $error instanceof Exception ? $error->getMessage() : 'Internal Server Error',
|
||||
],
|
||||
)
|
||||
->withStatus($statusCode),
|
||||
);
|
||||
}
|
||||
}
|
||||
48
src/Http/Kernel.php
Normal file
48
src/Http/Kernel.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http;
|
||||
|
||||
use App\Core\EnvironmentLoader;
|
||||
use App\Enums\Environment;
|
||||
use Laminas\Diactoros\ServerRequest;
|
||||
use Laminas\HttpHandlerRunner\Emitter\SapiEmitter;
|
||||
use League\Route\Router;
|
||||
use Whoops\Handler\PrettyPageHandler;
|
||||
use Whoops\Run;
|
||||
|
||||
/** @codeCoverageIgnore */
|
||||
final readonly class Kernel
|
||||
{
|
||||
public function __construct(
|
||||
private EnvironmentLoader $environmentLoader,
|
||||
private ExceptionHandler $exceptionHandler,
|
||||
private ServerRequest $serverRequest,
|
||||
private Router $router,
|
||||
private SapiEmitter $sapiEmitter,
|
||||
) {
|
||||
}
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
$this->environmentLoader->load();
|
||||
$this->setExceptionHandler();
|
||||
|
||||
$response = $this->router->dispatch($this->serverRequest);
|
||||
$this->sapiEmitter->emit($response);
|
||||
}
|
||||
|
||||
private function setExceptionHandler(): void
|
||||
{
|
||||
if (Environment::Production->isCurrent()) {
|
||||
set_exception_handler($this->exceptionHandler->handle(...));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$whoops = new Run();
|
||||
$whoops->pushHandler(new PrettyPageHandler());
|
||||
$whoops->register();
|
||||
}
|
||||
}
|
||||
26
src/Http/Middleware/CspMiddleware.php
Normal file
26
src/Http/Middleware/CspMiddleware.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Security\Csp\CspPolicy;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\MiddlewareInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
|
||||
final readonly class CspMiddleware implements MiddlewareInterface
|
||||
{
|
||||
public function __construct(
|
||||
private CspPolicy $cspPolicy
|
||||
) {
|
||||
}
|
||||
|
||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||
{
|
||||
return $handler
|
||||
->handle($request)
|
||||
->withHeader('Content-Security-Policy', (string) $this->cspPolicy);
|
||||
}
|
||||
}
|
||||
26
src/Http/Middleware/HstsMiddleware.php
Normal file
26
src/Http/Middleware/HstsMiddleware.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Security\Hst\Config;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\MiddlewareInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
|
||||
final class HstsMiddleware implements MiddlewareInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Config $hstsConfig
|
||||
) {
|
||||
}
|
||||
|
||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||
{
|
||||
return $handler
|
||||
->handle($request)
|
||||
->withHeader('Strict-Transport-Security', (string) $this->hstsConfig);
|
||||
}
|
||||
}
|
||||
22
src/Http/Middleware/StartSessionMiddleware.php
Normal file
22
src/Http/Middleware/StartSessionMiddleware.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\MiddlewareInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
|
||||
final class StartSessionMiddleware implements MiddlewareInterface
|
||||
{
|
||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||
{
|
||||
if (\PHP_SESSION_NONE === session_status()) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
return $handler->handle($request);
|
||||
}
|
||||
}
|
||||
11
src/Http/Response/View.php
Normal file
11
src/Http/Response/View.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Response;
|
||||
|
||||
use Laminas\Diactoros\Response;
|
||||
|
||||
final class View extends Response
|
||||
{
|
||||
}
|
||||
16
src/Http/Routes.php
Normal file
16
src/Http/Routes.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http;
|
||||
|
||||
use App\Http\Controllers\HomeController;
|
||||
use League\Route\Router;
|
||||
|
||||
final class Routes
|
||||
{
|
||||
public static function routes(Router $route): void
|
||||
{
|
||||
$route->get('/', HomeController::class);
|
||||
}
|
||||
}
|
||||
23
src/Security/Csp/CspPolicy.php
Normal file
23
src/Security/Csp/CspPolicy.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Security\Csp;
|
||||
|
||||
final class CspPolicy
|
||||
{
|
||||
/** @var array<string, Directive> */
|
||||
private array $directives = [];
|
||||
|
||||
public function addDirective(Directive $directive): self
|
||||
{
|
||||
$this->directives[$directive->getName()] = $directive;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return implode('; ', $this->directives).';';
|
||||
}
|
||||
}
|
||||
25
src/Security/Csp/Directive.php
Normal file
25
src/Security/Csp/Directive.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Security\Csp;
|
||||
|
||||
abstract class Directive
|
||||
{
|
||||
/** @var string[] */
|
||||
private array $sources = [];
|
||||
|
||||
abstract public function getName(): string;
|
||||
|
||||
public function addSource(string $source): self
|
||||
{
|
||||
$this->sources[] = $source;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->getName().' '.implode(' ', $this->sources);
|
||||
}
|
||||
}
|
||||
15
src/Security/Csp/Directives/DefaultSrc.php
Normal file
15
src/Security/Csp/Directives/DefaultSrc.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Security\Csp\Directives;
|
||||
|
||||
use App\Security\Csp\Directive;
|
||||
|
||||
final class DefaultSrc extends Directive
|
||||
{
|
||||
public function getName(): string
|
||||
{
|
||||
return 'default-src';
|
||||
}
|
||||
}
|
||||
15
src/Security/Csp/Directives/ImgSrc.php
Normal file
15
src/Security/Csp/Directives/ImgSrc.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Security\Csp\Directives;
|
||||
|
||||
use App\Security\Csp\Directive;
|
||||
|
||||
final class ImgSrc extends Directive
|
||||
{
|
||||
public function getName(): string
|
||||
{
|
||||
return 'img-src';
|
||||
}
|
||||
}
|
||||
15
src/Security/Csp/Directives/ScriptSrc.php
Normal file
15
src/Security/Csp/Directives/ScriptSrc.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Security\Csp\Directives;
|
||||
|
||||
use App\Security\Csp\Directive;
|
||||
|
||||
final class ScriptSrc extends Directive
|
||||
{
|
||||
public function getName(): string
|
||||
{
|
||||
return 'script-src';
|
||||
}
|
||||
}
|
||||
15
src/Security/Csp/Directives/StyleSrc.php
Normal file
15
src/Security/Csp/Directives/StyleSrc.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Security\Csp\Directives;
|
||||
|
||||
use App\Security\Csp\Directive;
|
||||
|
||||
final class StyleSrc extends Directive
|
||||
{
|
||||
public function getName(): string
|
||||
{
|
||||
return 'style-src';
|
||||
}
|
||||
}
|
||||
25
src/Security/Hst/Config.php
Normal file
25
src/Security/Hst/Config.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Security\Hst;
|
||||
|
||||
final class Config
|
||||
{
|
||||
public function __construct(
|
||||
public readonly int $maxAge,
|
||||
public readonly bool $includeSubdomains,
|
||||
public readonly bool $preload,
|
||||
) {
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return \sprintf(
|
||||
'max-age=%s%s%s',
|
||||
$this->maxAge,
|
||||
$this->includeSubdomains ? '; includeSubdomains' : '',
|
||||
$this->preload ? '; preload' : '',
|
||||
);
|
||||
}
|
||||
}
|
||||
40
src/Utility/Helpers.php
Normal file
40
src/Utility/Helpers.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Core\Container;
|
||||
use App\Http\Response\View;
|
||||
use League\Plates\Engine;
|
||||
|
||||
if (!function_exists('app')) { // @codeCoverageIgnore
|
||||
function app(?string $className = null): mixed
|
||||
{
|
||||
return null === $className ? Container::getInstance() : Container::getInstance()->get($className);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('view')) { // @codeCoverageIgnore
|
||||
/**
|
||||
* @param mixed[] $args
|
||||
*/
|
||||
function view(string $template = '', array $args = []): View
|
||||
{
|
||||
/** @var View $response */
|
||||
$response = app(View::class);
|
||||
|
||||
if ('' === $template) {
|
||||
return $response; // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
/** @var Engine $engine */
|
||||
$engine = app(Engine::class);
|
||||
|
||||
$response
|
||||
->getBody()
|
||||
->write(
|
||||
$engine->render($template, $args),
|
||||
);
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user