Files
slim-blog/src/Shared/Bootstrap.php
2026-03-16 12:01:10 +01:00

234 lines
7.1 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Shared;
use App\Post\PostExtension;
use App\Shared\Database\DatabaseNotProvisionedException;
use App\Shared\Database\DatabaseReadiness;
use App\Shared\Extension\AppExtension;
use App\Shared\Extension\CsrfExtension;
use App\Shared\Extension\SessionExtension;
use DI\ContainerBuilder;
use Dotenv\Dotenv;
use Dotenv\Exception\InvalidPathException;
use PDO;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Log\LoggerInterface;
use Slim\App;
use Slim\Csrf\Guard;
use Slim\Exception\HttpException;
use Slim\Factory\AppFactory;
use Slim\Views\Twig;
use Slim\Views\TwigMiddleware;
use Throwable;
final class Bootstrap
{
private ?ContainerInterface $container = null;
private ?App $app = null;
public static function create(): self
{
return new self();
}
private function __construct()
{
}
public function initialize(): App
{
$this->initializeInfrastructure();
return $this->createHttpApp();
}
public function initializeInfrastructure(): ContainerInterface
{
if ($this->container !== null) {
return $this->container;
}
$this->checkDirectories();
$this->checkExtensions();
$this->loadEnvironment();
$this->buildContainer();
return $this->container;
}
public function createHttpApp(): App
{
if ($this->app !== null) {
return $this->app;
}
$container = $this->initializeInfrastructure();
$this->app = AppFactory::createFromContainer($container);
$this->registerMiddlewares();
$this->registerRoutes();
$this->configureErrorHandling();
return $this->app;
}
public function getContainer(): ContainerInterface
{
return $this->initializeInfrastructure();
}
private function buildContainer(): void
{
$isDev = strtolower($_ENV['APP_ENV'] ?? 'production') === 'development';
$builder = new ContainerBuilder();
$builder->addDefinitions(__DIR__ . '/../../config/container.php');
if (!$isDev) {
$builder->enableCompilation(__DIR__ . '/../../var/cache/di');
}
$this->container = $builder->build();
}
private function checkDirectories(): void
{
$dirs = [
__DIR__.'/../../var/cache/twig',
__DIR__.'/../../var/cache/htmlpurifier',
__DIR__.'/../../var/cache/di',
__DIR__.'/../../var/logs',
__DIR__.'/../../database',
__DIR__.'/../../public/media',
];
foreach ($dirs as $dir) {
if (!is_dir($dir) && !@mkdir($dir, 0755, true)) {
throw new \RuntimeException("Impossible de créer le répertoire : {$dir}");
}
}
}
private function checkExtensions(): void
{
if (!function_exists('imagewebp')) {
throw new \RuntimeException(
'L\'extension PHP GD avec le support WebP est requise. ' .
'Installez le paquet php-gd (ex: apt install php-gd) puis redémarrez PHP.'
);
}
}
private function loadEnvironment(): void
{
$rootDir = dirname(__DIR__, 2);
$envPath = $rootDir . '/.env';
try {
$dotenv = Dotenv::createImmutable($rootDir);
$dotenv->load();
} catch (InvalidPathException $exception) {
throw new \RuntimeException(
sprintf(
"Fichier .env introuvable a la racine du projet (%s). Copiez .env.example vers .env avant de demarrer l'application.",
$envPath
),
previous: $exception,
);
}
$dotenv->required(['APP_URL', 'ADMIN_USERNAME', 'ADMIN_EMAIL', 'ADMIN_PASSWORD']);
date_default_timezone_set($_ENV['TIMEZONE'] ?? 'UTC');
$isDev = strtolower($_ENV['APP_ENV'] ?? 'production') === 'development';
if (!$isDev && ($_ENV['ADMIN_PASSWORD'] ?? '') === 'changeme123') {
throw new \RuntimeException(
'ADMIN_PASSWORD doit être changé avant de démarrer en production.'
);
}
}
private function registerMiddlewares(): void
{
$this->app->addBodyParsingMiddleware();
$twig = $this->container->get(Twig::class);
$twig->addExtension($this->container->get(AppExtension::class));
$twig->addExtension($this->container->get(SessionExtension::class));
$twig->addExtension($this->container->get(PostExtension::class));
$this->app->add(TwigMiddleware::create($this->app, $twig));
$guard = new Guard($this->app->getResponseFactory());
$guard->setPersistentTokenMode(true);
$twig->addExtension(new CsrfExtension($guard));
$this->app->add($guard);
$container = $this->container;
$this->app->add(function (
ServerRequestInterface $request,
RequestHandlerInterface $handler,
) use ($container): ResponseInterface {
DatabaseReadiness::assertProvisioned($container->get(PDO::class));
return $handler->handle($request);
});
}
private function registerRoutes(): void
{
Routes::register($this->app);
}
private function configureErrorHandling(): void
{
$isDev = strtolower($_ENV['APP_ENV'] ?? 'production') === 'development';
$logger = $this->container->get(LoggerInterface::class);
$errorHandler = $this->app->addErrorMiddleware($isDev, true, true, $logger);
$app = $this->app;
$container = $this->container;
$errorHandler->setDefaultErrorHandler(
function (
ServerRequestInterface $request,
Throwable $exception,
bool $displayErrorDetails,
bool $logErrors,
bool $logErrorDetails,
) use ($isDev, $app, $container): ResponseInterface {
if ($isDev && !$exception instanceof DatabaseNotProvisionedException) {
throw $exception;
}
$statusCode = 500;
$message = 'Une erreur inattendue s\'est produite.';
if ($exception instanceof DatabaseNotProvisionedException) {
$statusCode = 503;
$message = $exception->getMessage();
} elseif ($exception instanceof HttpException) {
$statusCode = $exception->getCode() ?: 500;
$message = $statusCode === 404
? 'La page demandée est introuvable.'
: $message;
}
$response = $app->getResponseFactory()->createResponse($statusCode);
$twig = $container->get(Twig::class);
return $twig->render($response, 'pages/error.twig', [
'status' => $statusCode,
'message' => $message,
]);
}
);
}
}