From 4ab2dbbb35a6ffc95c8b0ee5583328946603f25e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Debussch=C3=A8re?= Date: Wed, 30 Jul 2025 16:23:32 +0200 Subject: [PATCH 01/20] Tests for now --- config/container.php | 61 ++++++++++++++++++++++++++++++++++++++++++++ public/index.php | 16 +++++++----- 2 files changed, 71 insertions(+), 6 deletions(-) diff --git a/config/container.php b/config/container.php index 72d5d84..aaa27eb 100644 --- a/config/container.php +++ b/config/container.php @@ -1,12 +1,73 @@ defaultToShared(); $container->delegate(new ReflectionContainer(true)); +$container->add(RequestHandlerInterface::class, function (ContainerInterface $container) { + $handler_factory = new HandlerFactory($container); + + $handler = new RequestHandler(); + $handler->middleware($container->get(ErrorHandlerMiddleware::class)); + $handler->middleware($container->get(ProblemDetailsMiddleware::class)); + $handler->middleware($container->get(TrailingSlashMiddleware::class)); + $handler->middleware($container->get(ContentLengthMiddleware::class)); + $handler->middleware($container->get(RouteMiddleware::class)); + $handler->middleware($container->get(ImplicitHeadMiddleware::class)); + $handler->middleware($container->get(ImplicitOptionsMiddleware::class)); + $handler->middleware($container->get(MethodNotAllowedMiddleware::class)); + $handler->middleware(new PipeMiddleware('/api', BodyParserMiddleware::class, $handler_factory)); + $handler->middleware(new PipeMiddleware('/api', UploadedFilesParserMiddleware::class, $handler_factory)); + $handler->middleware($container->get(DispatchMiddleware::class)); + $handler->middleware($container->get(NotFoundHandlerMiddleware::class)); + + return $handler; +})->addArgument($container); + +$container->add(EmitterInterface::class, Emitter::class); + +$container->add(RequestHandlerRunnerInterface::class, function (ContainerInterface $container) { + return new RequestHandlerRunner( + $container->get(RequestHandlerInterface::class), + $container->get(EmitterInterface::class), + static fn() => $container->get(ServerRequestInterface::class), + static function(Throwable $e) use ($container) { + $response = ($container->get(ResponseFactoryInterface::class))->createResponse(500); + $response->getBody()->write(sprintf( + 'An error occurred: %s', + $e->getMessage() + )); + return $response; + } + ); +})->addArgument($container); + (require_once __DIR__.'/containers/app.container.php')($container); (require_once __DIR__.'/containers/logs.container.php')($container); (require_once __DIR__.'/containers/pipeline.container.php')($container); diff --git a/public/index.php b/public/index.php index 5a386eb..951fb2a 100644 --- a/public/index.php +++ b/public/index.php @@ -3,6 +3,7 @@ require_once __DIR__.'/../vendor/autoload.php'; use Borsch\Application\ApplicationInterface; +use Borsch\RequestHandler\RequestHandlerRunnerInterface; use Psr\Container\ContainerInterface; use Psr\Http\Message\ServerRequestInterface; @@ -14,13 +15,16 @@ /** @var ContainerInterface $container */ $container = (require_once __DIR__.'/../config/container.php'); - $app = $container->get(ApplicationInterface::class); + $runner = $container->get(RequestHandlerRunnerInterface::class); + $runner->run(); - (require_once __DIR__.'/../config/pipeline.php')($app); - (require_once __DIR__.'/../config/routes.php')($app); - (require_once __DIR__.'/../config/api.php')($app); +// $app = $container->get(ApplicationInterface::class); - $request = $container->get(ServerRequestInterface::class); +// (require_once __DIR__.'/../config/pipeline.php')($app); +// (require_once __DIR__.'/../config/routes.php')($app); +// (require_once __DIR__.'/../config/api.php')($app); - $app->run($request); +// $request = $container->get(ServerRequestInterface::class); +// +// $app->run($request); })(); From b7fc371e28057826435467bdea46c07cd019e56e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Debussch=C3=A8re?= Date: Fri, 1 Aug 2025 18:30:32 +0200 Subject: [PATCH 02/20] upd: moved to DDD architecture + attribute routing --- composer.json | 15 +- config/api.php | 24 --- config/container.php | 196 ++++++++++++------ config/containers/app.container.php | 56 ----- config/containers/database.container.php | 13 -- config/containers/logs.container.php | 22 -- config/containers/pipeline.container.php | 74 ------- config/containers/template.container.php | 17 -- config/pipeline.php | 60 ------ config/routes.php | 12 -- public/index.php | 12 -- public/worker.php | 15 +- .../Handler/AlbumHandler.php | 15 +- .../Handler/ArtistHandler.php | 15 +- src/{ => Application}/Handler/HomeHandler.php | 26 +-- .../Handler/OpenApiHandler.php | 10 +- .../Handler/RedocHandler.php | 8 +- .../Listener/MonologListener.php | 2 +- src/{Service => Domain}/AlbumService.php | 17 +- src/{Service => Domain}/ArtistService.php | 19 +- src/{ => Domain}/Model/Album.php | 2 +- src/{ => Domain}/Model/Artist.php | 4 +- src/{ => Domain}/Model/Model.php | 2 +- .../RepositoryInterface.php | 8 +- src/Handler/HealthCheckHandler.php | 24 --- .../AbstractRepository.php | 23 +- src/Infrastructure/AlbumRepository.php | 23 ++ src/Infrastructure/ArtistRepository.php | 23 ++ .../Mapper/AlbumMapper.php | 10 +- .../Mapper/ArtistMapper.php | 10 +- src/Infrastructure/Mapper/MapperInterface.php | 11 + src/Repository/AlbumRepository.php | 14 -- src/Repository/ArtistRepository.php | 14 -- storage/database.sqlite | Bin 884736 -> 884736 bytes storage/views/404.tpl | 26 +-- storage/views/500.tpl | 28 +-- storage/views/home.tpl | 139 ++++++------- storage/views/layout.tpl | 16 ++ 38 files changed, 391 insertions(+), 614 deletions(-) delete mode 100644 config/api.php delete mode 100644 config/containers/app.container.php delete mode 100644 config/containers/database.container.php delete mode 100644 config/containers/logs.container.php delete mode 100644 config/containers/pipeline.container.php delete mode 100644 config/containers/template.container.php delete mode 100644 config/pipeline.php delete mode 100644 config/routes.php rename src/{ => Application}/Handler/AlbumHandler.php (95%) rename src/{ => Application}/Handler/ArtistHandler.php (95%) rename src/{ => Application}/Handler/HomeHandler.php (53%) rename src/{ => Application}/Handler/OpenApiHandler.php (81%) rename src/{ => Application}/Handler/RedocHandler.php (82%) rename src/{ => Application}/Listener/MonologListener.php (99%) rename src/{Service => Domain}/AlbumService.php (90%) rename src/{Service => Domain}/ArtistService.php (88%) rename src/{ => Domain}/Model/Album.php (91%) rename src/{ => Domain}/Model/Artist.php (86%) rename src/{ => Domain}/Model/Model.php (85%) rename src/{Repository => Domain}/RepositoryInterface.php (75%) delete mode 100644 src/Handler/HealthCheckHandler.php rename src/{Repository => Infrastructure}/AbstractRepository.php (64%) create mode 100644 src/Infrastructure/AlbumRepository.php create mode 100644 src/Infrastructure/ArtistRepository.php rename src/{Repository => Infrastructure}/Mapper/AlbumMapper.php (61%) rename src/{Repository => Infrastructure}/Mapper/ArtistMapper.php (56%) create mode 100644 src/Infrastructure/Mapper/MapperInterface.php delete mode 100644 src/Repository/AlbumRepository.php delete mode 100644 src/Repository/ArtistRepository.php create mode 100644 storage/views/layout.tpl diff --git a/composer.json b/composer.json index 97a9773..d8955d3 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,6 @@ { "name": "borschphp/borsch-skeleton", + "version": "3.0.0", "description": "A Borsch Framework skeleton application to kick start development.", "homepage": "https://github.com/borschphp/borsck-skeleton", "license": "MIT", @@ -24,16 +25,16 @@ "ext-libxml": "*", "ext-pdo": "*", "ext-simplexml": "*", - "borschphp/application": "^2", - "borschphp/middlewares": "^1", + "borschphp/middlewares": "^2", "borschphp/latte": "^1", - "borschphp/chef": "^1", + "borschphp/requesthandler": "^2", + "borschphp/container": "^2", + "borschphp/router": "^3.1", "laminas/laminas-diactoros": "^3", + "laminas/laminas-db": "^2.20", "monolog/monolog": "^2 || ^3", "vlucas/phpdotenv": "^v5.1", - "league/container": "^4", "zircote/swagger-php": "^5.0", - "laminas/laminas-db": "^2.20", "debuss-a/problem-details": "^1.0" }, "require-dev": { @@ -43,7 +44,9 @@ }, "autoload": { "psr-4": { - "App\\": "src/" + "Application\\": "src/Application/", + "Domain\\": "src/Domain/", + "Infrastructure\\": "src/Infrastructure/" }, "files": [ "bootstrap/defines.inc.php", diff --git a/config/api.php b/config/api.php deleted file mode 100644 index aee602e..0000000 --- a/config/api.php +++ /dev/null @@ -1,24 +0,0 @@ -group('/api', function (Application $app) { - if (!isProduction()) { - // Redoc (/swagger also available) - $app->get('/openapi[.{format:json|yml|yaml}]', OpenApiHandler::class, 'openapi'); - $app->get('/{redoc:redoc|swagger}', RedocHandler::class); - } - - $app->any('/albums[/{id:\d+}]', AlbumHandler::class, 'albums'); - $app->any('/artists[/{id:\d+}]', ArtistHandler::class, 'artists'); - - // Health checks - $app->get('/healthcheck', HealthCheckHandler::class, 'healthcheck'); - }); -}; diff --git a/config/container.php b/config/container.php index aaa27eb..acd1ebb 100644 --- a/config/container.php +++ b/config/container.php @@ -1,77 +1,151 @@ setCacheByDefault(true); -$container->defaultToShared(); -$container->delegate(new ReflectionContainer(true)); - -$container->add(RequestHandlerInterface::class, function (ContainerInterface $container) { - $handler_factory = new HandlerFactory($container); - - $handler = new RequestHandler(); - $handler->middleware($container->get(ErrorHandlerMiddleware::class)); - $handler->middleware($container->get(ProblemDetailsMiddleware::class)); - $handler->middleware($container->get(TrailingSlashMiddleware::class)); - $handler->middleware($container->get(ContentLengthMiddleware::class)); - $handler->middleware($container->get(RouteMiddleware::class)); - $handler->middleware($container->get(ImplicitHeadMiddleware::class)); - $handler->middleware($container->get(ImplicitOptionsMiddleware::class)); - $handler->middleware($container->get(MethodNotAllowedMiddleware::class)); - $handler->middleware(new PipeMiddleware('/api', BodyParserMiddleware::class, $handler_factory)); - $handler->middleware(new PipeMiddleware('/api', UploadedFilesParserMiddleware::class, $handler_factory)); - $handler->middleware($container->get(DispatchMiddleware::class)); - $handler->middleware($container->get(NotFoundHandlerMiddleware::class)); - - return $handler; -})->addArgument($container); - -$container->add(EmitterInterface::class, Emitter::class); - -$container->add(RequestHandlerRunnerInterface::class, function (ContainerInterface $container) { +$container->set(RequestHandlerRunnerInterface::class, static function (ContainerInterface $container) { return new RequestHandlerRunner( $container->get(RequestHandlerInterface::class), - $container->get(EmitterInterface::class), + new Emitter(), static fn() => $container->get(ServerRequestInterface::class), - static function(Throwable $e) use ($container) { + static function() use ($container) { + $engine = $container->get(TemplateRendererInterface::class); $response = ($container->get(ResponseFactoryInterface::class))->createResponse(500); - $response->getBody()->write(sprintf( - 'An error occurred: %s', - $e->getMessage() - )); + + $response->getBody()->write($engine->render('500.tpl')); + return $response; } ); -})->addArgument($container); +}); + +$container->set(RequestHandlerInterface::class, static function (ContainerInterface $container) { + return (new RequestHandler()) + ->middleware($container->get(ErrorHandlerMiddleware::class)) + ->middleware($container->get(ProblemDetailsMiddleware::class)) + ->middleware($container->get(TrailingSlashMiddleware::class)) + ->middleware($container->get(ContentLengthMiddleware::class)) + ->middleware($container->get(RouteMiddleware::class)) + ->middleware($container->get(ImplicitHeadMiddleware::class)) + ->middleware($container->get(ImplicitOptionsMiddleware::class)) + ->middleware($container->get(MethodNotAllowedMiddleware::class)) + ->middleware($container->get(BodyParserMiddleware::class)) + ->middleware($container->get(UploadedFilesParserMiddleware::class)) + ->middleware($container->get(DispatchMiddleware::class)) + ->middleware($container->get(NotFoundHandlerMiddleware::class)); +}); + +$container->set(ServerRequestInterface::class, static fn() => ServerRequestFactory::fromGlobals())->cache(false); + +$container->set(ResponseFactoryInterface::class, ResponseFactory::class); + +$container->set( + AttributeRouteLoader::class, + static fn(ContainerInterface $container) => (new AttributeRouteLoader([__ROOT_DIR__.'/src/Application'], $container))->load() +); + +$container->set(RouterInterface::class, static function (AttributeRouteLoader $loader) { + $router = new FastRouteRouter(); + if (isProduction()) { + $router->setCacheFile(cache_path('routes.cache.php')); + } + + foreach ($loader->getRoutes() as $route) { + $router->addRoute($route); + } + + return $router; +}); + +$container->set(NotFoundHandlerMiddleware::class, static function (TemplateRendererInterface $renderer) { + return new NotFoundHandlerMiddleware(static function (ServerRequestInterface $request) use ($renderer): ResponseInterface { + if (str_starts_with($request->getUri()->getPath(), '/api')) { + throw new ProblemDetailsException(new ProblemDetails( + type: '://problem/not-found', + title: 'Not found.', + status: 404, + detail: "The requested uri ({$request->getUri()->getPath()}) could not be found." + )); + } + + return new HtmlResponse( + $renderer->render('404.tpl'), + 404 + ); + }); +}); + +$container->set(FormatterInterface::class, function () { + return new class implements FormatterInterface { + + public function format(ResponseInterface $response, Throwable $throwable, RequestInterface $request): ResponseInterface + { + $formatter = str_starts_with($request->getUri()->getPath(), '/api') ? + new JsonFormatter() : + new HtmlFormatter(isProduction()); + + return $formatter->format($response, $throwable, $request); + } + }; +}); + +$container->set(TemplateRendererInterface::class, fn() => new LatteRenderer(storage_path('views'), cache_path('views'), !isProduction())); + +$container->set(Logger::class, function (): Logger { + $name = env('APP_NAME', 'App'); + + $handlers = [ + new StreamHandler( + logs_path(env('LOG_CHANNEL', 'app').'.log'), + Level::fromName(env('LOG_LEVEL', 'Debug')) + ) + ]; + + $processors = [new PsrLogMessageProcessor(removeUsedContextFields: true)]; + $datetime_zone = new DateTimeZone(env('TIMEZONE', 'UTC')); + + return new Logger($name, $handlers, $processors, $datetime_zone); +}); -(require_once __DIR__.'/containers/app.container.php')($container); -(require_once __DIR__.'/containers/logs.container.php')($container); -(require_once __DIR__.'/containers/pipeline.container.php')($container); -(require_once __DIR__.'/containers/template.container.php')($container); -(require_once __DIR__.'/containers/database.container.php')($container); +$container + ->set(AdapterInterface::class, Adapter::class) + ->addParameter([ + 'driver' => 'Pdo_Sqlite', + 'dsn' => 'sqlite:'.storage_path('database.sqlite') + ]); return $container; diff --git a/config/containers/app.container.php b/config/containers/app.container.php deleted file mode 100644 index 2d315f4..0000000 --- a/config/containers/app.container.php +++ /dev/null @@ -1,56 +0,0 @@ -addServiceProvider(new class extends AbstractServiceProvider { - - public function provides(string $id): bool - { - return in_array($id, [ - ApplicationInterface::class, - RouterInterface::class, - RequestHandlerInterface::class, - ServerRequestInterface::class - ]); - } - - public function register(): void - { - $this - ->getContainer() - ->add(ApplicationInterface::class, Application::class) - ->addArgument(RequestHandlerInterface::class) - ->addArgument(RouterInterface::class) - ->addArgument($this->getContainer()); - - $this - ->getContainer() - ->add(RouterInterface::class, function () { - $router = new FastRouteRouter(); - if (isProduction()) { - $router->setCacheFile(cache_path('routes.cache.php')); - } - - return $router; - }); - - $this - ->getContainer() - ->add(RequestHandlerInterface::class, RequestHandler::class); - - $this - ->getContainer() - ->add(ServerRequestInterface::class, fn() => ServerRequestFactory::fromGlobals()) - ->setShared(false); - } - }); -}; diff --git a/config/containers/database.container.php b/config/containers/database.container.php deleted file mode 100644 index 0d47b31..0000000 --- a/config/containers/database.container.php +++ /dev/null @@ -1,13 +0,0 @@ -add(AdapterInterface::class, Adapter::class) - ->addArgument([ - 'driver' => 'Pdo_Sqlite', - 'dsn' => 'sqlite:'.storage_path('database.sqlite') - ]); -}; diff --git a/config/containers/logs.container.php b/config/containers/logs.container.php deleted file mode 100644 index ef85ec8..0000000 --- a/config/containers/logs.container.php +++ /dev/null @@ -1,22 +0,0 @@ -add(Logger::class, function (): Logger { - $name = env('APP_NAME', 'App'); - - $handlers = [ - new StreamHandler( - logs_path(env('LOG_CHANNEL', 'app').'.log'), - Level::fromName(env('LOG_LEVEL', 'Debug')) - ) - ]; - - $processors = [new PsrLogMessageProcessor(removeUsedContextFields: true)]; - $datetime_zone = new DateTimeZone(env('TIMEZONE', 'UTC')); - - return new Logger($name, $handlers, $processors, $datetime_zone); - }); -}; diff --git a/config/containers/pipeline.container.php b/config/containers/pipeline.container.php deleted file mode 100644 index e63886e..0000000 --- a/config/containers/pipeline.container.php +++ /dev/null @@ -1,74 +0,0 @@ -addServiceProvider(new class extends AbstractServiceProvider { - - public function provides(string $id): bool - { - return in_array($id, [ - FormatterInterface::class, - ErrorHandlerMiddleware::class, - NotFoundHandlerMiddleware::class, - ProblemDetailsMiddleware::class, - ]); - } - - public function register(): void - { - $this - ->getContainer() - ->add(FormatterInterface::class, fn(): FormatterInterface => new class implements FormatterInterface { - - public function format(ResponseInterface $response, Throwable $throwable, RequestInterface $request): ResponseInterface - { - $formatter = str_starts_with($request->getUri()->getPath(), '/api') ? - new JsonFormatter() : - new HtmlFormatter(isProduction()); - - return $formatter->format($response, $throwable, $request); - } - }); - - $this - ->getContainer() - ->add(ErrorHandlerMiddleware::class) - ->addArgument($this->getContainer()->get(FormatterInterface::class)) - ->addArgument([$this->getContainer()->get(MonologListener::class)]); - - $this - ->getContainer() - ->add(NotFoundHandlerMiddleware::class) - ->addArgument(static function (ServerRequestInterface $request): ResponseInterface { - if (str_starts_with($request->getUri()->getPath(), '/api')) { - throw new ProblemDetailsException(new ProblemDetails( - type: '://problem/not-found', - title: 'Not found.', - status: 404, - detail: "The requested uri ({$request->getUri()->getPath()}) could not be find." - )); - } - - return new HtmlResponse( - '

404 Not Found

', - 404 - ); - }); - - $this - ->getContainer() - ->add(ProblemDetailsMiddleware::class) - ->addArgument(new ResponseFactory()); - } - }); -}; diff --git a/config/containers/template.container.php b/config/containers/template.container.php deleted file mode 100644 index 725c196..0000000 --- a/config/containers/template.container.php +++ /dev/null @@ -1,17 +0,0 @@ -add( - TemplateRendererInterface::class, - fn() => new LatteRenderer( - storage_path('views'), - cache_path('views'), - !isProduction() - ) - ); -}; diff --git a/config/pipeline.php b/config/pipeline.php deleted file mode 100644 index 9dcf45d..0000000 --- a/config/pipeline.php +++ /dev/null @@ -1,60 +0,0 @@ -pipe(ErrorHandlerMiddleware::class); - - // This middleware will handle exceptions coming from API calls throwing a ProblemDetailsException. - $app->pipe(ProblemDetailsMiddleware::class); - - // Pipe more middleware here that you want to execute on every request. - $app->pipe(TrailingSlashMiddleware::class); - $app->pipe(ContentLengthMiddleware::class); - - // Register the routing middleware in the pipeline. - // It will add the Borsch\Router\RouteResult request attribute. - $app->pipe(RouteMiddleware::class); - - // The following handle routing failures for common conditions: - // - HEAD request but no routes answer that method - // - OPTIONS request but no routes answer that method - // - method not allowed - // Order here matters, the MethodNotAllowedMiddleware should be placed after the Implicit*Middleware. - $app->pipe(ImplicitHeadMiddleware::class); - $app->pipe(ImplicitOptionsMiddleware::class); - $app->pipe(MethodNotAllowedMiddleware::class); - - // Middleware can be attached to specific paths, allowing you to mix and match - // applications under a common domain. - $app->pipe('/api', [ - BodyParserMiddleware::class, - UploadedFilesParserMiddleware::class - ]); - - // This will take care of generating the response of your matched route. - $app->pipe(DispatchMiddleware::class); - - // If no Response is returned by any middleware, then send a 404 Not Found response. - // You can provide other fallback middleware to execute. - $app->pipe(NotFoundHandlerMiddleware::class); -}; diff --git a/config/routes.php b/config/routes.php deleted file mode 100644 index 5e1f33c..0000000 --- a/config/routes.php +++ /dev/null @@ -1,12 +0,0 @@ -get('/', HomeHandler::class, 'home'); -}; diff --git a/public/index.php b/public/index.php index 951fb2a..8f4cab1 100644 --- a/public/index.php +++ b/public/index.php @@ -2,10 +2,8 @@ require_once __DIR__.'/../vendor/autoload.php'; -use Borsch\Application\ApplicationInterface; use Borsch\RequestHandler\RequestHandlerRunnerInterface; use Psr\Container\ContainerInterface; -use Psr\Http\Message\ServerRequestInterface; (static function () { // Warning, see: https://www.php.net/manual/en/timezones.others.php @@ -17,14 +15,4 @@ $runner = $container->get(RequestHandlerRunnerInterface::class); $runner->run(); - -// $app = $container->get(ApplicationInterface::class); - -// (require_once __DIR__.'/../config/pipeline.php')($app); -// (require_once __DIR__.'/../config/routes.php')($app); -// (require_once __DIR__.'/../config/api.php')($app); - -// $request = $container->get(ServerRequestInterface::class); -// -// $app->run($request); })(); diff --git a/public/worker.php b/public/worker.php index 67f88d8..2abd606 100644 --- a/public/worker.php +++ b/public/worker.php @@ -11,9 +11,8 @@ require_once __DIR__ . '/../vendor/autoload.php'; -use Borsch\Application\ApplicationInterface; +use Borsch\RequestHandler\RequestHandlerRunnerInterface; use Psr\Container\ContainerInterface; -use Psr\Http\Message\ServerRequestInterface; // Warning, see: https://www.php.net/manual/en/timezones.others.php // do not use any of the timezones listed here (besides UTC) @@ -22,15 +21,9 @@ /** @var ContainerInterface $container */ $container = (require_once __DIR__ . '/../config/container.php'); -$app = $container->get(ApplicationInterface::class); - -(require_once __DIR__.'/../config/pipeline.php')($app); -(require_once __DIR__.'/../config/routes.php')($app); -(require_once __DIR__.'/../config/api.php')($app); - -$handler = static function () use ($app, $container) { - $request = $container->get(ServerRequestInterface::class); - $app->run($request); +$handler = static function () use ($container) { + $runner = $container->get(RequestHandlerRunnerInterface::class); + $runner->run(); }; $max_requests_number = filter_input(INPUT_SERVER, 'MAX_REQUESTS', FILTER_SANITIZE_NUMBER_INT) ?: 25; diff --git a/src/Handler/AlbumHandler.php b/src/Application/Handler/AlbumHandler.php similarity index 95% rename from src/Handler/AlbumHandler.php rename to src/Application/Handler/AlbumHandler.php index 0269ff3..3ad6af6 100644 --- a/src/Handler/AlbumHandler.php +++ b/src/Application/Handler/AlbumHandler.php @@ -1,17 +1,15 @@ getAttribute('id'); diff --git a/src/Handler/ArtistHandler.php b/src/Application/Handler/ArtistHandler.php similarity index 95% rename from src/Handler/ArtistHandler.php rename to src/Application/Handler/ArtistHandler.php index 66b9039..5be75d9 100644 --- a/src/Handler/ArtistHandler.php +++ b/src/Application/Handler/ArtistHandler.php @@ -1,17 +1,15 @@ getAttribute('id'); diff --git a/src/Handler/HomeHandler.php b/src/Application/Handler/HomeHandler.php similarity index 53% rename from src/Handler/HomeHandler.php rename to src/Application/Handler/HomeHandler.php index 8a0d98d..b83abd6 100644 --- a/src/Handler/HomeHandler.php +++ b/src/Application/Handler/HomeHandler.php @@ -1,32 +1,24 @@ engine->assign([ @@ -35,4 +27,4 @@ public function handle(ServerRequestInterface $request): ResponseInterface return new HtmlResponse($this->engine->render('home.tpl')); } -} +} \ No newline at end of file diff --git a/src/Handler/OpenApiHandler.php b/src/Application/Handler/OpenApiHandler.php similarity index 81% rename from src/Handler/OpenApiHandler.php rename to src/Application/Handler/OpenApiHandler.php index 32df909..909590b 100644 --- a/src/Handler/OpenApiHandler.php +++ b/src/Application/Handler/OpenApiHandler.php @@ -1,9 +1,11 @@ getAttribute('format', 'yaml'); diff --git a/src/Handler/RedocHandler.php b/src/Application/Handler/RedocHandler.php similarity index 82% rename from src/Handler/RedocHandler.php rename to src/Application/Handler/RedocHandler.php index be3ac67..8bda4e8 100644 --- a/src/Handler/RedocHandler.php +++ b/src/Application/Handler/RedocHandler.php @@ -1,12 +1,15 @@ router->generateUri('openapi', ['format' => 'json']); diff --git a/src/Listener/MonologListener.php b/src/Application/Listener/MonologListener.php similarity index 99% rename from src/Listener/MonologListener.php rename to src/Application/Listener/MonologListener.php index 470d021..5bc3c38 100644 --- a/src/Listener/MonologListener.php +++ b/src/Application/Listener/MonologListener.php @@ -1,6 +1,6 @@ AlbumMapper::toAlbum($album), - $this->repository->all() - ); + return $this->repository->all(); } public function find(int $id): ?Album @@ -44,7 +39,7 @@ public function find(int $id): ?Album )); } - return AlbumMapper::toAlbum($album); + return $album; } /** @param array{title: string, artist_id: int} $data */ diff --git a/src/Service/ArtistService.php b/src/Domain/ArtistService.php similarity index 88% rename from src/Service/ArtistService.php rename to src/Domain/ArtistService.php index c6871ae..fb7d08f 100644 --- a/src/Service/ArtistService.php +++ b/src/Domain/ArtistService.php @@ -1,15 +1,11 @@ ArtistMapper::toArtist($artist), - $this->repository->all() - ); + return $this->repository->all(); } public function find(int $id): ?Artist @@ -46,7 +39,7 @@ public function find(int $id): ?Artist )); } - return ArtistMapper::toArtist($artist); + return $artist; } /** @param array{name: string} $data */ diff --git a/src/Model/Album.php b/src/Domain/Model/Album.php similarity index 91% rename from src/Model/Album.php rename to src/Domain/Model/Album.php index daa5fe8..2c77860 100644 --- a/src/Model/Album.php +++ b/src/Domain/Model/Album.php @@ -1,6 +1,6 @@ > */ + /** @return array */ public function all(): array; /** @return array|null */ - public function find(int $id): ?array; + public function find(int $id): ?Model; /** @param array{Title?: string, ArtistId?: int, Name?: string} $data */ public function create(array $data): int; diff --git a/src/Handler/HealthCheckHandler.php b/src/Handler/HealthCheckHandler.php deleted file mode 100644 index 6ba3e04..0000000 --- a/src/Handler/HealthCheckHandler.php +++ /dev/null @@ -1,24 +0,0 @@ -table_gateway = new TableGateway($this->getTable(), $adapter); + $this->mapper = $mapper; $this->logger = $logger->withName(__CLASS__); } @@ -26,15 +32,20 @@ abstract protected function getTable(): string; public function all(): array { - return iterator_to_array($this->table_gateway->select()); + return array_map( + fn (iterable $row): Model => $this->mapper->map($row), + iterator_to_array($this->table_gateway->select()) + ); } - public function find(int $id): ?array + public function find(int $id): ?Model { /** @var ResultSet $results */ - $results = $this->table_gateway->select([static::ROW_IDENTIFIER => $id]); + $results = $this->table_gateway->select([static::ROW_IDENTIFIER => $id])->current(); - return (array)$results->current(); + return $results === null + ? null + : $this->mapper->map((array)$results); } public function create(array $data): int diff --git a/src/Infrastructure/AlbumRepository.php b/src/Infrastructure/AlbumRepository.php new file mode 100644 index 0000000..57e154b --- /dev/null +++ b/src/Infrastructure/AlbumRepository.php @@ -0,0 +1,23 @@ + $object */ - public static function toAlbum(iterable $object): Album + public function map(iterable $object): Album { $album = new Album(); $album->id = $object[AlbumRepository::ROW_IDENTIFIER] ?? null; diff --git a/src/Repository/Mapper/ArtistMapper.php b/src/Infrastructure/Mapper/ArtistMapper.php similarity index 56% rename from src/Repository/Mapper/ArtistMapper.php rename to src/Infrastructure/Mapper/ArtistMapper.php index 13627f8..ff83b97 100644 --- a/src/Repository/Mapper/ArtistMapper.php +++ b/src/Infrastructure/Mapper/ArtistMapper.php @@ -1,15 +1,15 @@ $object */ - public static function toArtist(iterable $object): Artist + public function map(iterable $object): Artist { $artist = new Artist(); $artist->id = $object[ArtistRepository::ROW_IDENTIFIER] ?? null; diff --git a/src/Infrastructure/Mapper/MapperInterface.php b/src/Infrastructure/Mapper/MapperInterface.php new file mode 100644 index 0000000..90beddb --- /dev/null +++ b/src/Infrastructure/Mapper/MapperInterface.php @@ -0,0 +1,11 @@ +8ftUk`If0l9h(T(3wm;+Ml_~)M3@9E! delta 88 zcmZo@Fl%Tqn;^}oIZ?)$Rg*!ltaD?E5I1A&-c-0jb}8G)Dyh?#+y1&CRJ bm<@>8ftUk`If0l9h(T(3wm;+Ml_~)M61pB* diff --git a/storage/views/404.tpl b/storage/views/404.tpl index 8b8ab52..4f508d3 100644 --- a/storage/views/404.tpl +++ b/storage/views/404.tpl @@ -1,25 +1,15 @@ - - - - - - - Borsch-Skeleton - Not Found - - - - -
+{layout 'layout.tpl'} + +{block title}Not found{/block} + +{block content}
-

Nothing here...

+

Nothing here...

Go home
-
- - \ No newline at end of file +{/block} diff --git a/storage/views/500.tpl b/storage/views/500.tpl index 7319366..88172ce 100644 --- a/storage/views/500.tpl +++ b/storage/views/500.tpl @@ -1,26 +1,16 @@ -Not Found - - - - - - Borsch-Skeleton - Error - - - - -
+{layout 'layout.tpl'} + +{block title}Whoops !{/block} + +{block content}
-

Whoops !

-

An unexpected error occured, be assured we've been informed and working hard to fix it.

+

Whoops !

+

An unexpected error occurred, be assured we've been informed and working hard to fix it.

Go home
-
- - \ No newline at end of file +{/block} diff --git a/storage/views/home.tpl b/storage/views/home.tpl index 97937b3..b3e2ef7 100644 --- a/storage/views/home.tpl +++ b/storage/views/home.tpl @@ -1,97 +1,88 @@ - - - - - - - Borsch-Skeleton - Home - - - - -
-
- Logo -

Fuel Your Code with Flavor

-
- Github -
+{layout 'layout.tpl'} + +{block title}Home{/block} -
-
+{block content} +
+ Logo +

Fuel Your Code with Flavor

+
+ Github +
+ +
+
-
-

Easy-to-Follow Documentation

-

Get started quickly with clear, well-structured documentation covering everything you need to build and scale.

-
- -
-
+
+

Easy-to-Follow Documentation

+

Get started quickly with clear, well-structured documentation covering everything you need to build and scale.

+
+ +
+
-
-

API-First with OpenAPI & Redoc

-

- Effortlessly document and explore your APIs with built-in OpenAPI support and a sleek ReDoc interface. -

-
- -
-
+
+

API-First with OpenAPI & Redoc

+

+ Effortlessly document and explore your APIs with built-in OpenAPI support and a sleek ReDoc interface. +

+
+ +
+
-
-

Lightweight & Fast

-

Designed for high performance with minimal overhead, ensuring ultra-fast response times.

-
- -
+
+

Lightweight & Fast

+

Designed for high performance with minimal overhead, ensuring ultra-fast response times.

+
+ +
-
+
-
-

Dependency Injection

-

Built-in DI container for managing dependencies efficiently.

-
-
-
+
+

Dependency Injection

+

Built-in DI container for managing dependencies efficiently.

+
+
+
-
-

Middleware Support

-

Customize request handling with PSR-15 middlewares.

-
-
-
+
+

Middleware Support

+

Customize request handling with PSR-15 middlewares.

+
+
+
-
-

Error Handling & Debugging

-

Clear error messages for a smooth development experience.

-
-
+
+

Error Handling & Debugging

+

Clear error messages for a smooth development experience.

- - \ No newline at end of file +
+{/block} diff --git a/storage/views/layout.tpl b/storage/views/layout.tpl new file mode 100644 index 0000000..492ba83 --- /dev/null +++ b/storage/views/layout.tpl @@ -0,0 +1,16 @@ + + + + + + + Borsch-Skeleton - {block title}{/block} + + + + +
+ {block content}{/block} +
+ + From b5275ac3983e034a04859187f1f964bdec450d91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Debussch=C3=A8re?= Date: Fri, 1 Aug 2025 18:32:35 +0200 Subject: [PATCH 03/20] del: removed version from composer.json --- composer.json | 1 - 1 file changed, 1 deletion(-) diff --git a/composer.json b/composer.json index d8955d3..f466b78 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,5 @@ { "name": "borschphp/borsch-skeleton", - "version": "3.0.0", "description": "A Borsch Framework skeleton application to kick start development.", "homepage": "https://github.com/borschphp/borsck-skeleton", "license": "MIT", From 9e0f62ac098c52fd8a55e6eea22bc0aded4e0671 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Debussch=C3=A8re?= Date: Mon, 4 Aug 2025 15:41:16 +0200 Subject: [PATCH 04/20] feat(router): loader cache --- config/container.php | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/config/container.php b/config/container.php index acd1ebb..691dd52 100644 --- a/config/container.php +++ b/config/container.php @@ -2,6 +2,7 @@ use Borsch\Container\Container; use Borsch\Latte\LatteRenderer; +use Borsch\Router\Contract\RouteInterface; use Borsch\Template\TemplateRendererInterface; use Borsch\Formatter\{FormatterInterface, HtmlFormatter, JsonFormatter}; use Borsch\Middleware\{BodyParserMiddleware, @@ -75,16 +76,30 @@ static function() use ($container) { $container->set( AttributeRouteLoader::class, - static fn(ContainerInterface $container) => (new AttributeRouteLoader([__ROOT_DIR__.'/src/Application'], $container))->load() + static fn(ContainerInterface $container) => ( + new AttributeRouteLoader( + [__ROOT_DIR__ . '/src/Application'], + $container, + cache_path('loader.routes.cache.php'), + !isProduction() + ))->load() ); $container->set(RouterInterface::class, static function (AttributeRouteLoader $loader) { - $router = new FastRouteRouter(); + $routes = $loader->getRoutes(); + if (isProduction()) { - $router->setCacheFile(cache_path('routes.cache.php')); + return new FastRouteRouter( + array_combine( + array_map(fn(RouteInterface $route) => $route->getName(), $routes), + $routes + ), + cache_path('router.routes.cache.php') + ); } - foreach ($loader->getRoutes() as $route) { + $router = new FastRouteRouter(); + foreach ($routes as $route) { $router->addRoute($route); } From 4cb54285e059c5047f574a31a01de46b07331480 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Debussch=C3=A8re?= Date: Mon, 4 Aug 2025 16:40:53 +0200 Subject: [PATCH 05/20] tests(pest): reset tests --- .gitignore | 1 + composer.json | 2 +- phpstan.neon | 2 +- phpunit.xml | 1 - src/Application/Handler/HomeHandler.php | 4 +- src/Application/Handler/OpenApiHandler.php | 2 +- src/Application/Listener/MonologListener.php | 147 ------------------- tests/Mock/TestHandler.php | 35 ----- tests/Pest.php | 135 ----------------- tests/TestCase.php | 10 -- tests/Unit/Bootstrap/HelpersTest.php | 23 --- tests/Unit/Listener/MonologListenerTest.php | 139 ------------------ 12 files changed, 5 insertions(+), 496 deletions(-) delete mode 100644 src/Application/Listener/MonologListener.php delete mode 100644 tests/Mock/TestHandler.php delete mode 100644 tests/Pest.php delete mode 100644 tests/TestCase.php delete mode 100644 tests/Unit/Bootstrap/HelpersTest.php delete mode 100644 tests/Unit/Listener/MonologListenerTest.php diff --git a/.gitignore b/.gitignore index 731f1ce..e2445b8 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ composer.phar ### Cache ### /storage/cache/views/*.php /storage/cache/logs/*.log +/storage/database.sqlite # Commit your application's lock file https://getcomposer.org/doc/01-basic-usage.md#commit-your-composer-lock-file-to-version-control # You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file diff --git a/composer.json b/composer.json index f466b78..182e8c3 100644 --- a/composer.json +++ b/composer.json @@ -55,7 +55,7 @@ }, "autoload-dev": { "psr-4": { - "AppTest\\": "tests/" + "Tests\\": "tests/" } }, "config": { diff --git a/phpstan.neon b/phpstan.neon index 35fe330..7243a57 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,3 +1,3 @@ parameters: - level: 6 + level: 0 treatPhpDocTypesAsCertain: false \ No newline at end of file diff --git a/phpunit.xml b/phpunit.xml index 7d0904f..0c12bb9 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -11,7 +11,6 @@ - ./app ./src diff --git a/src/Application/Handler/HomeHandler.php b/src/Application/Handler/HomeHandler.php index b83abd6..8e400dd 100644 --- a/src/Application/Handler/HomeHandler.php +++ b/src/Application/Handler/HomeHandler.php @@ -2,7 +2,6 @@ namespace Application\Handler; -use Borsch\Router\Contract\RouterInterface; use Borsch\Template\TemplateRendererInterface; use Borsch\Router\Attribute\{Controller, Get}; use Laminas\Diactoros\Response\HtmlResponse; @@ -10,11 +9,10 @@ use Psr\Http\Server\RequestHandlerInterface; #[Controller] -class HomeHandler implements RequestHandlerInterface +readonly class HomeHandler implements RequestHandlerInterface { public function __construct( - protected RouterInterface $router, protected TemplateRendererInterface $engine ) {} diff --git a/src/Application/Handler/OpenApiHandler.php b/src/Application/Handler/OpenApiHandler.php index 909590b..890050a 100644 --- a/src/Application/Handler/OpenApiHandler.php +++ b/src/Application/Handler/OpenApiHandler.php @@ -18,7 +18,7 @@ )] #[OA\Server(url: 'http://localhost:8080/api')] #[Controller('/api')] -class OpenApiHandler implements RequestHandlerInterface +readonly class OpenApiHandler implements RequestHandlerInterface { #[Get(path: '/openapi', name: 'openapi')] diff --git a/src/Application/Listener/MonologListener.php b/src/Application/Listener/MonologListener.php deleted file mode 100644 index 5bc3c38..0000000 --- a/src/Application/Listener/MonologListener.php +++ /dev/null @@ -1,147 +0,0 @@ -handleErrorException($throwable, $request); - return; - } - - $this->logger->critical($this->formatLog($throwable, $request)); - } - - /** - * @param ErrorException $exception - * @param ServerRequestInterface $request - */ - protected function handleErrorException(ErrorException $exception, ServerRequestInterface $request): void - { - $log = $this->formatLog($exception, $request); - $severity = $exception->getSeverity(); - - if ($this->isError($severity)) { - $this->logger->error($log); - } elseif ($this->isWarning($severity)) { - $this->logger->warning($log); - } elseif ($this->isNotice($severity)) { - $this->logger->notice($log); - } elseif ($this->isInformation($severity)) { - $this->logger->info($log); - } else { - $this->logger->debug($log); - } - } - - /** - * @param Throwable $throwable - * @param ServerRequestInterface $request - * @return string - */ - protected function formatLog(Throwable $throwable, ServerRequestInterface $request): string - { - return sprintf( - '%s %s => %s Stacktrace: %s', - $request->getMethod(), - (string)$request->getUri(), - $throwable->getMessage(), - $throwable->getTraceAsString() - ); - } - - /** - * @param int $code - * @return bool - */ - protected function isError(int $code): bool - { - return in_array($code, [ - E_ERROR, - E_RECOVERABLE_ERROR, - E_CORE_ERROR, - E_COMPILE_ERROR, - E_USER_ERROR, - E_PARSE - ]); - } - - /** - * @param int $code - * @return bool - */ - protected function isWarning(int $code): bool - { - return in_array($code, [ - E_WARNING, - E_USER_WARNING, - E_CORE_WARNING, - E_COMPILE_WARNING - ]); - } - - /** - * @param int $code - * @return bool - */ - protected function isNotice(int $code): bool - { - return in_array($code, [ - E_NOTICE, - E_USER_NOTICE - ]); - } - - /** - * @param int $code - * @return bool - */ - protected function isInformation(int $code): bool - { - // If PHP version is 8.4 then do not include E_STRICT because it is deprecated (throws exception) - if (version_compare(PHP_VERSION, '8.4', '>=')) { - return in_array($code, [ - E_DEPRECATED, - E_USER_DEPRECATED - ]); - } - - return in_array($code, [ - E_STRICT, - E_DEPRECATED, - E_USER_DEPRECATED - ]); - } -} diff --git a/tests/Mock/TestHandler.php b/tests/Mock/TestHandler.php deleted file mode 100644 index d8772a4..0000000 --- a/tests/Mock/TestHandler.php +++ /dev/null @@ -1,35 +0,0 @@ -getUri()->getPath(); - - if ($path == '/to/exception') { - throw new Exception('Pest for testz!'); - } elseif (in_array($path, ['/to/post/and/check/json', '/to/post/and/check/urlencoded', '/to/post/and/check/xml'])) { - return new JsonResponse($request->getParsedBody()); - } elseif ($path == '/to/route/result') { - return new JsonResponse([ - 'route_result_is_success' => $request->getAttribute(RouteResultInterface::class)?->isSuccess() ?? false - ]); - } - - return new TextResponse(__METHOD__); - } -} diff --git a/tests/Pest.php b/tests/Pest.php deleted file mode 100644 index 879562e..0000000 --- a/tests/Pest.php +++ /dev/null @@ -1,135 +0,0 @@ -beforeEach(function () { - $this->log_file = __DIR__.'/app.log'; - $this->logger = new Logger('Borsch'); - $this->logger->pushHandler(new StreamHandler($this->log_file)); - - $this->listener = new MonologListener($this->logger); - - $this->server_request = (new ServerRequestFactory())->createServerRequest( - 'GET', - 'https://example.com/to/dispatch' - ); - }) - ->afterEach(function () { - if (file_exists($this->log_file)) { - @unlink($this->log_file); - } - }) - ->in( - 'Unit/Listener/MonologListenerTest.php', - 'Unit/Middleware/ErrorHandlerMiddlewareTest.php' - ); - -uses() - ->beforeEach(function () { - $this->log_file = __DIR__.'/app.log'; - - $container = new Container(); - $container->set(RouteMiddleware::class); - $container->set(DispatchMiddleware::class); - $container->set(NotFoundHandlerMiddleware::class); - $container->set(FastRouteRouter::class); - $container->set(RouterInterface::class, FastRouteRouter::class)->cache(true); - $container->set( - Logger::class, - fn() => (new Logger(env('APP_NAME', 'App'))) - ->pushHandler(new StreamHandler(__DIR__.'/app.log')) - ); - $container - ->set(ErrorHandlerMiddleware::class) - ->addMethod('addListener', [$container->get(MonologListener::class)]); - $container - ->set(TemplateRendererInterface::class, LatteEngine::class); - - $this->container = $container; - $this->app = new class(new RequestHandler(), $container->get(RouterInterface::class), $container) extends BorschApp { - public function runAndGetResponse(ServerRequestInterface $server_request): ResponseInterface - { - return $this->request_handler->handle($server_request); - } - public function getContainer(): Container - { - return $this->container; - } - }; - - // Middlewares pipeline - $this->app->pipe(ErrorHandlerMiddleware::class); - $this->app->pipe(TrailingSlashMiddleware::class); - $this->app->pipe(ContentLengthMiddleware::class); - $this->app->pipe('/to/post/and/check', BodyParserMiddleware::class); - $this->app->pipe(RouteMiddleware::class); - $this->app->pipe(ImplicitHeadMiddleware::class); - $this->app->pipe(ImplicitOptionsMiddleware::class); - $this->app->pipe(MethodNotAllowedMiddleware::class); - $this->app->pipe(DispatchMiddleware::class); - $this->app->pipe(NotFoundHandlerMiddleware::class); - - // Routes - $this->app->get('/to/dispatch', TestHandler::class); - $this->app->get('/to/exception', TestHandler::class); - $this->app->post('/to/post/and/check/json', TestHandler::class); - $this->app->post('/to/post/and/check/urlencoded', TestHandler::class); - $this->app->post('/to/post/and/check/xml', TestHandler::class); - $this->app->post('/to/head/without/post', TestHandler::class); - $this->app->head('/to/head', TestHandler::class); - $this->app->options('/to/options', TestHandler::class); - $this->app->get('/to/route/result', TestHandler::class); - - // Server Requests - $factory = new ServerRequestFactory(); - $this->server_request = $factory->createServerRequest('GET', 'https://example.com/to/dispatch'); - $this->server_request_not_found = $factory->createServerRequest('GET', 'https://example.com/to/not/dispatch'); - $this->server_request_to_exception = $factory->createServerRequest('GET', 'https://example.com/to/exception'); - }) - ->in('Unit/Middleware'); - -uses() - ->beforeEach(function () { - $_ENV['TEST1'] = 'true'; - $_ENV['TEST2'] = 'yes'; - $_ENV['TEST3'] = 'false'; - $_ENV['TEST4'] = 'no'; - $_ENV['TEST5'] = 'empty'; - $_ENV['TEST6'] = 'null'; - $_ENV['TEST7'] = 'a value '; - }) - ->in('Unit/Bootstrap/HelpersTest.php'); diff --git a/tests/TestCase.php b/tests/TestCase.php deleted file mode 100644 index fdb4604..0000000 --- a/tests/TestCase.php +++ /dev/null @@ -1,10 +0,0 @@ -toBeTrue() - ->and(env('TEST2'))->toBeTrue(); -}); - -test('env can deal with FALSE', function() { - expect(env('TEST3'))->toBeFalse() - ->and(env('TEST4'))->toBeFalse(); -}); - -test('env can deal with EMPTY', function() { - expect(env('TEST5'))->toBe(''); -}); - -test('env can deal with NULL', function() { - expect(env('TEST6'))->toBe(null); -}); - -test('env can deal with DEFAULT', function() { - expect(env('TEST7'))->toBe('a value'); -}); diff --git a/tests/Unit/Listener/MonologListenerTest.php b/tests/Unit/Listener/MonologListenerTest.php deleted file mode 100644 index 035ed49..0000000 --- a/tests/Unit/Listener/MonologListenerTest.php +++ /dev/null @@ -1,139 +0,0 @@ -listener)(new InvalidArgumentException('Not Found', 404), $this->server_request); - expect($this->log_file)->toBeFile() - ->and(file_get_contents($this->log_file))->toContain( - 'Borsch.CRITICAL: GET https://example.com/to/dispatch => Not Found' - ); -}); - -it('can handle error exception with severity E_ERROR', function () { - ($this->listener)(new ErrorException('Not Found', 404, E_ERROR), $this->server_request); - expect($this->log_file)->toBeFile() - ->and(file_get_contents($this->log_file))->toContain( - 'Borsch.ERROR: GET https://example.com/to/dispatch => Not Found' - ); -}); - -it('can handle error exception with severity E_RECOVERABLE_ERROR', function () { - ($this->listener)(new ErrorException('Not Found', 404, E_RECOVERABLE_ERROR), $this->server_request); - expect($this->log_file)->toBeFile() - ->and(file_get_contents($this->log_file))->toContain( - 'Borsch.ERROR: GET https://example.com/to/dispatch => Not Found' - ); -}); - -it('can handle error exception with severity E_CORE_ERROR', function () { - ($this->listener)(new ErrorException('Not Found', 404, E_RECOVERABLE_ERROR), $this->server_request); - expect($this->log_file)->toBeFile() - ->and(file_get_contents($this->log_file))->toContain( - 'Borsch.ERROR: GET https://example.com/to/dispatch => Not Found' - ); -}); - -it('can handle error exception with severity E_COMPILE_ERROR', function () { - ($this->listener)(new ErrorException('Not Found', 404, E_RECOVERABLE_ERROR), $this->server_request); - expect($this->log_file)->toBeFile() - ->and(file_get_contents($this->log_file))->toContain( - 'Borsch.ERROR: GET https://example.com/to/dispatch => Not Found' - ); -}); - -it('can handle error exception with severity E_USER_ERROR', function () { - ($this->listener)(new ErrorException('Not Found', 404, E_USER_ERROR), $this->server_request); - expect($this->log_file)->toBeFile() - ->and(file_get_contents($this->log_file))->toContain( - 'Borsch.ERROR: GET https://example.com/to/dispatch => Not Found' - ); -}); - -it('can handle error exception with severity E_PARSE', function () { - ($this->listener)(new ErrorException('Not Found', 404, E_PARSE), $this->server_request); - expect($this->log_file)->toBeFile() - ->and(file_get_contents($this->log_file))->toContain( - 'Borsch.ERROR: GET https://example.com/to/dispatch => Not Found' - ); -}); - -it('can handle error exception with severity E_WARNING', function () { - ($this->listener)(new ErrorException('Not Found', 404, E_WARNING), $this->server_request); - expect($this->log_file)->toBeFile() - ->and(file_get_contents($this->log_file))->toContain( - 'Borsch.WARNING: GET https://example.com/to/dispatch => Not Found' - ); -}); - -it('can handle error exception with severity E_USER_WARNING', function () { - ($this->listener)(new ErrorException('Not Found', 404, E_USER_WARNING), $this->server_request); - expect($this->log_file)->toBeFile() - ->and(file_get_contents($this->log_file))->toContain( - 'Borsch.WARNING: GET https://example.com/to/dispatch => Not Found' - ); -}); - -it('can handle error exception with severity E_CORE_WARNING', function () { - ($this->listener)(new ErrorException('Not Found', 404, E_CORE_WARNING), $this->server_request); - expect($this->log_file)->toBeFile() - ->and(file_get_contents($this->log_file))->toContain( - 'Borsch.WARNING: GET https://example.com/to/dispatch => Not Found' - ); -}); - -it('can handle error exception with severity E_COMPILE_WARNING', function () { - ($this->listener)(new ErrorException('Not Found', 404, E_COMPILE_WARNING), $this->server_request); - expect($this->log_file)->toBeFile() - ->and(file_get_contents($this->log_file))->toContain( - 'Borsch.WARNING: GET https://example.com/to/dispatch => Not Found' - ); -}); - -it('can handle error exception with severity E_NOTICE', function () { - ($this->listener)(new ErrorException('Not Found', 404, E_NOTICE), $this->server_request); - expect($this->log_file)->toBeFile() - ->and(file_get_contents($this->log_file))->toContain( - 'Borsch.NOTICE: GET https://example.com/to/dispatch => Not Found' - ); -}); - -it('can handle error exception with severity E_USER_NOTICE', function () { - ($this->listener)(new ErrorException('Not Found', 404, E_USER_NOTICE), $this->server_request); - expect($this->log_file)->toBeFile() - ->and(file_get_contents($this->log_file))->toContain( - 'Borsch.NOTICE: GET https://example.com/to/dispatch => Not Found' - ); -}); - -if (version_compare(PHP_VERSION, '8.4', '<')) { - it('can handle error exception with severity E_STRICT', function () { - ($this->listener)(new ErrorException('Not Found', 404, E_STRICT), $this->server_request); - expect($this->log_file)->toBeFile() - ->and(file_get_contents($this->log_file))->toContain( - 'Borsch.INFO: GET https://example.com/to/dispatch => Not Found' - ); - }); -} - -it('can handle error exception with severity E_DEPRECATED', function () { - ($this->listener)(new ErrorException('Not Found', 404, E_DEPRECATED), $this->server_request); - expect($this->log_file)->toBeFile() - ->and(file_get_contents($this->log_file))->toContain( - 'Borsch.INFO: GET https://example.com/to/dispatch => Not Found' - ); -}); - -it('can handle error exception with severity E_USER_DEPRECATED', function () { - ($this->listener)(new ErrorException('Not Found', 404, E_USER_DEPRECATED), $this->server_request); - expect($this->log_file)->toBeFile() - ->and(file_get_contents($this->log_file))->toContain( - 'Borsch.INFO: GET https://example.com/to/dispatch => Not Found' - ); -}); - -it('can handle error exception with default DEBUG severity', function () { - ($this->listener)(new ErrorException('Not Found', 404, 9999999), $this->server_request); - expect($this->log_file)->toBeFile() - ->and(file_get_contents($this->log_file))->toContain( - 'Borsch.DEBUG: GET https://example.com/to/dispatch => Not Found' - ); -}); From eb79c97f579bafd02905188ecc09d8bdf0891a21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Debussch=C3=A8re?= Date: Mon, 4 Aug 2025 16:58:33 +0200 Subject: [PATCH 06/20] tests(pest): reset tests --- tests/Feature/ExampleTest.php | 7 ++++++ tests/Pest.php | 45 +++++++++++++++++++++++++++++++++++ tests/TestCase.php | 10 ++++++++ tests/Unit/ExampleTest.php | 7 ++++++ 4 files changed, 69 insertions(+) create mode 100644 tests/Feature/ExampleTest.php create mode 100644 tests/Pest.php create mode 100644 tests/TestCase.php create mode 100644 tests/Unit/ExampleTest.php diff --git a/tests/Feature/ExampleTest.php b/tests/Feature/ExampleTest.php new file mode 100644 index 0000000..e99d96d --- /dev/null +++ b/tests/Feature/ExampleTest.php @@ -0,0 +1,7 @@ +toBeTrue(); +}); diff --git a/tests/Pest.php b/tests/Pest.php new file mode 100644 index 0000000..b239048 --- /dev/null +++ b/tests/Pest.php @@ -0,0 +1,45 @@ +extend(Tests\TestCase::class)->in('Feature'); + +/* +|-------------------------------------------------------------------------- +| Expectations +|-------------------------------------------------------------------------- +| +| When you're writing tests, you often need to check that values meet certain conditions. The +| "expect()" function gives you access to a set of "expectations" methods that you can use +| to assert different things. Of course, you may extend the Expectation API at any time. +| +*/ + +expect()->extend('toBeOne', function () { + return $this->toBe(1); +}); + +/* +|-------------------------------------------------------------------------- +| Functions +|-------------------------------------------------------------------------- +| +| While Pest is very powerful out-of-the-box, you may have some testing code specific to your +| project that you don't want to repeat in every file. Here you can also expose helpers as +| global functions to help you to reduce the number of lines of code in your test files. +| +*/ + +function something() +{ + // .. +} diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 0000000..cfb05b6 --- /dev/null +++ b/tests/TestCase.php @@ -0,0 +1,10 @@ +toBeTrue(); +}); From 4afeebf8c75b7ab1cbcf11ee9ad1103fce3e0e5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Debussch=C3=A8re?= Date: Mon, 4 Aug 2025 23:19:34 +0200 Subject: [PATCH 07/20] feat: switched to borsch-psr7 --- composer.json | 2 +- config/container.php | 7 +++++-- src/Application/Handler/AlbumHandler.php | 13 ++++++++++--- src/Application/Handler/ArtistHandler.php | 13 ++++++++++--- src/Application/Handler/HomeHandler.php | 4 ++-- src/Application/Handler/OpenApiHandler.php | 6 +++--- src/Application/Handler/RedocHandler.php | 2 +- 7 files changed, 32 insertions(+), 15 deletions(-) diff --git a/composer.json b/composer.json index 182e8c3..7eeb38b 100644 --- a/composer.json +++ b/composer.json @@ -29,7 +29,7 @@ "borschphp/requesthandler": "^2", "borschphp/container": "^2", "borschphp/router": "^3.1", - "laminas/laminas-diactoros": "^3", + "borschphp/psr7": "^1.0", "laminas/laminas-db": "^2.20", "monolog/monolog": "^2 || ^3", "vlucas/phpdotenv": "^v5.1", diff --git a/config/container.php b/config/container.php index 691dd52..cb7eb31 100644 --- a/config/container.php +++ b/config/container.php @@ -1,6 +1,8 @@ middleware($container->get(NotFoundHandlerMiddleware::class)); }); -$container->set(ServerRequestInterface::class, static fn() => ServerRequestFactory::fromGlobals())->cache(false); +$container->set(ServerRequestInterface::class, static function () { + return (new ServerRequestFactory())->createServerRequest($_SERVER['REQUEST_METHOD'], $_SERVER['REQUEST_URI'], $_SERVER); +})->cache(false); $container->set(ResponseFactoryInterface::class, ResponseFactory::class); diff --git a/src/Application/Handler/AlbumHandler.php b/src/Application/Handler/AlbumHandler.php index 3ad6af6..5cc632b 100644 --- a/src/Application/Handler/AlbumHandler.php +++ b/src/Application/Handler/AlbumHandler.php @@ -2,10 +2,11 @@ namespace Application\Handler; +use Borsch\Http\Response\{EmptyResponse, JsonResponse}; use Borsch\Router\Attribute\{Controller, Delete, Get, Patch, Post, Put}; use Domain\AlbumService; -use Laminas\Diactoros\Response\{EmptyResponse, JsonResponse}; use OpenApi\Attributes as OA; +use ProblemDetails\ProblemDetailsException; use Psr\Http\Message\{ResponseInterface, ServerRequestInterface}; use Psr\Http\Server\RequestHandlerInterface; @@ -92,7 +93,10 @@ private function getAlbums(?int $id = null): ResponseInterface ); } - /** @param array{"title": string, "artist_id": int} $body */ + /** + * @param array{"title": string, "artist_id": int} $body + * @throws ProblemDetailsException + */ #[OA\Post( path: '/albums', description: 'Create a new album (there is no check on the `artist_id` existence)', @@ -124,7 +128,10 @@ private function createAlbum(array $body): ResponseInterface return new JsonResponse($new_album, 201); } - /** @param array{title?: string, artist_id?: int} $body */ + /** + * @param array{title?: string, artist_id?: int} $body + * @throws ProblemDetailsException + */ #[OA\Put( path: '/albums/{id}', description: 'Update an album by ID', diff --git a/src/Application/Handler/ArtistHandler.php b/src/Application/Handler/ArtistHandler.php index 5be75d9..829552e 100644 --- a/src/Application/Handler/ArtistHandler.php +++ b/src/Application/Handler/ArtistHandler.php @@ -2,10 +2,11 @@ namespace Application\Handler; +use Borsch\Http\Response\{EmptyResponse, JsonResponse}; use Borsch\Router\Attribute\{Controller, Delete, Get, Patch, Post, Put}; use Domain\ArtistService; -use Laminas\Diactoros\Response\{EmptyResponse, JsonResponse}; use OpenApi\Attributes as OA; +use ProblemDetails\ProblemDetailsException; use Psr\Http\Message\{ResponseInterface, ServerRequestInterface}; use Psr\Http\Server\RequestHandlerInterface; @@ -92,7 +93,10 @@ private function getArtists(?int $id = null): ResponseInterface ); } - /** @param array{name: string} $body */ + /** + * @param array{name: string} $body + * @throws ProblemDetailsException + */ #[OA\Post( path: '/artists', description: 'Create a new artist', @@ -121,7 +125,10 @@ private function createArtist(array $body): ResponseInterface return new JsonResponse($new_artist, 201); } - /** @param array{name: string} $body */ + /** + * @param array{name: string} $body + * @throws ProblemDetailsException + */ #[OA\Put( path: '/artists/{id}', description: 'Update an artist', diff --git a/src/Application/Handler/HomeHandler.php b/src/Application/Handler/HomeHandler.php index 8e400dd..f8866f6 100644 --- a/src/Application/Handler/HomeHandler.php +++ b/src/Application/Handler/HomeHandler.php @@ -2,9 +2,9 @@ namespace Application\Handler; +use Borsch\Http\Response\HtmlResponse; use Borsch\Template\TemplateRendererInterface; use Borsch\Router\Attribute\{Controller, Get}; -use Laminas\Diactoros\Response\HtmlResponse; use Psr\Http\Message\{ResponseInterface, ServerRequestInterface}; use Psr\Http\Server\RequestHandlerInterface; @@ -25,4 +25,4 @@ public function handle(ServerRequestInterface $request): ResponseInterface return new HtmlResponse($this->engine->render('home.tpl')); } -} \ No newline at end of file +} diff --git a/src/Application/Handler/OpenApiHandler.php b/src/Application/Handler/OpenApiHandler.php index 890050a..16a9c0d 100644 --- a/src/Application/Handler/OpenApiHandler.php +++ b/src/Application/Handler/OpenApiHandler.php @@ -3,10 +3,10 @@ namespace Application\Handler; use OpenApi\{Attributes as OA, Generator}; +use Borsch\Http\Factory\StreamFactory; +use Borsch\Http\Response; use Borsch\Router\Attribute\Controller; use Borsch\Router\Attribute\Get; -use Laminas\Diactoros\Response; -use Laminas\Diactoros\StreamFactory; use Psr\Http\Message\{ResponseInterface, ServerRequestInterface}; use Psr\Http\Server\RequestHandlerInterface; @@ -34,8 +34,8 @@ public function handle(ServerRequestInterface $request): ResponseInterface $stream_factory = new StreamFactory(); return new Response( - $stream_factory->createStream($definition), 200, + $stream_factory->createStream($definition), ['Content-Type' => 'text/'.$format] ); } diff --git a/src/Application/Handler/RedocHandler.php b/src/Application/Handler/RedocHandler.php index 8bda4e8..1385ab1 100644 --- a/src/Application/Handler/RedocHandler.php +++ b/src/Application/Handler/RedocHandler.php @@ -2,10 +2,10 @@ namespace Application\Handler; +use Borsch\Http\Response\HtmlResponse; use Borsch\Router\Attribute\Controller; use Borsch\Router\Attribute\Get; use Borsch\Router\Contract\RouterInterface; -use Laminas\Diactoros\Response\HtmlResponse; use Psr\Http\Message\{ResponseInterface, ServerRequestInterface}; use Psr\Http\Server\RequestHandlerInterface; From 7e553a6af3ca08e2486fdece5374e12e71aafe32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Debussch=C3=A8re?= Date: Mon, 4 Aug 2025 23:47:14 +0200 Subject: [PATCH 08/20] upd: Skeletorfile.php --- Skeletorfile.php | 20 ++++++++++---------- config/container.php | 3 +-- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/Skeletorfile.php b/Skeletorfile.php index e7b8b91..e5b3fbe 100644 --- a/Skeletorfile.php +++ b/Skeletorfile.php @@ -14,27 +14,26 @@ if ($installation_type === 'MINIMAL') { $skeletor->spin('Removing front handlers', function () use ($skeletor) { - $skeletor->removeFile('src/Handler/HomeHandler.php'); - $skeletor->pregReplaceInFile( - '/\s\$app->[\s\S].+/', - '', - 'config/routes.php' - ); - + $skeletor->removeFile('src/Application/Handler/HomeHandler.php'); return true; }, 'Removed front handlers', 'Unable to completely remove front handlers'); - + /* // For now, it only removes the handlers, not the templates (because used in other container definitions). $skeletor->spin('Removing template files and configuration', function () use ($skeletor) { - $skeletor->removeFile('config/containers/template.container.php'); $skeletor->pregReplaceInFile( - '/\n[\s\S].+template[\s\S].+;/', + '/\n[\s\S].+TemplateRendererInterface[\s\S].+;/', + '', + 'config/container.php' + ); + $skeletor->pregReplaceInFile( + '/\$engine->render(\'500.tpl\')/', '', 'config/container.php' ); $skeletor->removeFile('storage/views/404.tpl'); $skeletor->removeFile('storage/views/500.tpl'); $skeletor->removeFile('storage/views/home.tpl'); + $skeletor->removeFile('storage/views/layout.tpl'); // There is an issue with `Skeletor::removeDirectory(string $filename);` because it internally uses `rmdir` // which has a parameter named `$path` and not `$filename`. // Because of the use of `get_defined_vars()`, `rmdir` receives a parameter named `$filename` instead of @@ -56,6 +55,7 @@ return true; }, 'Removing Latte template engine from composer.json', 'Unable to completely remove Latte template engine from composer.json'); + */ $skeletor->spin('Creating environment file', function () use ($skeletor, $app_name) { if (!$skeletor->exists('.env')) { diff --git a/config/container.php b/config/container.php index cb7eb31..a0bbcd7 100644 --- a/config/container.php +++ b/config/container.php @@ -26,8 +26,7 @@ use Borsch\Router\Contract\RouterInterface; use Borsch\Router\FastRouteRouter; use Borsch\Router\Loader\AttributeRouteLoader; -use Laminas\Db\Adapter\Adapter; -use Laminas\Db\Adapter\AdapterInterface; +use Laminas\Db\Adapter\{Adapter, AdapterInterface}; use Monolog\Handler\StreamHandler; use Monolog\Level; use Monolog\Logger; From 76f216c879c8756228b4d3d7a216c456438419d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Debussch=C3=A8re?= Date: Tue, 5 Aug 2025 14:08:57 +0200 Subject: [PATCH 09/20] upd(openapi): format in url, generator folders --- src/Application/Handler/OpenApiHandler.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Application/Handler/OpenApiHandler.php b/src/Application/Handler/OpenApiHandler.php index 16a9c0d..4c391e2 100644 --- a/src/Application/Handler/OpenApiHandler.php +++ b/src/Application/Handler/OpenApiHandler.php @@ -21,11 +21,14 @@ readonly class OpenApiHandler implements RequestHandlerInterface { - #[Get(path: '/openapi', name: 'openapi')] + #[Get(path: '/openapi[.{format:json|yaml|yml}]', name: 'openapi')] public function handle(ServerRequestInterface $request): ResponseInterface { $format = $request->getAttribute('format', 'yaml'); - $openapi = Generator::scan([__ROOT_DIR__.'/src']); + $openapi = (new Generator())->generate([ + __ROOT_DIR__.'/src/Application/Handler', + __ROOT_DIR__.'/src/Domain', + ]); $definition = match ($format) { 'json' => $openapi->toJson(), default => $openapi->toYaml(), @@ -36,7 +39,7 @@ public function handle(ServerRequestInterface $request): ResponseInterface return new Response( 200, $stream_factory->createStream($definition), - ['Content-Type' => 'text/'.$format] + ['Content-Type' => ['text/'.$format]] ); } } From 331cf6bd1d61ac2ae420d3fae4311f6012d71fbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Debussch=C3=A8re?= Date: Tue, 5 Aug 2025 20:38:25 +0200 Subject: [PATCH 10/20] upd: middlewares v2 --- config/container.php | 63 ++++++++++++++++++++++++++++++++------------ 1 file changed, 46 insertions(+), 17 deletions(-) diff --git a/config/container.php b/config/container.php index a0bbcd7..99b9839 100644 --- a/config/container.php +++ b/config/container.php @@ -2,11 +2,11 @@ use Borsch\Container\Container; use Borsch\Http\Response\HtmlResponse; -use Borsch\Http\Factory\{ResponseFactory, ServerRequestFactory}; +use Borsch\Http\Response\JsonResponse; +use Borsch\Http\Factory\{ResponseFactory, ServerRequestFactory, StreamFactory, UploadedFileFactory}; use Borsch\Latte\LatteRenderer; use Borsch\Router\Contract\RouteInterface; use Borsch\Template\TemplateRendererInterface; -use Borsch\Formatter\{FormatterInterface, HtmlFormatter, JsonFormatter}; use Borsch\Middleware\{BodyParserMiddleware, ContentLengthMiddleware, DispatchMiddleware, @@ -33,7 +33,12 @@ use Monolog\Processor\PsrLogMessageProcessor; use ProblemDetails\{ProblemDetails, ProblemDetailsException, ProblemDetailsMiddleware}; use Psr\Container\ContainerInterface; -use Psr\Http\Message\{RequestInterface, ResponseFactoryInterface, ResponseInterface, ServerRequestInterface}; +use Psr\Http\Message\{RequestInterface, + ResponseFactoryInterface, + ResponseInterface, + ServerRequestInterface, + StreamFactoryInterface, + UploadedFileFactoryInterface}; $container = new Container(); $container->setCacheByDefault(true); @@ -70,10 +75,48 @@ static function() use ($container) { ->middleware($container->get(NotFoundHandlerMiddleware::class)); }); +$container->set(ErrorHandlerMiddleware::class, static fn(TemplateRendererInterface $renderer) => new ErrorHandlerMiddleware( + static function (Throwable $throwable, ServerRequestInterface $request) use ($renderer): ResponseInterface { + if (str_starts_with($request->getUri()->getPath(), '/api')) { + return new JsonResponse(new ProblemDetails( + type: '://problem/internal-server-error', + title: 'Internal server error.', + status: 500, + detail: $throwable->getMessage() + ), 500); + } + + return new HtmlResponse( + $renderer->render('500.tpl'), + 500 + ); + } +)); + +$container->set(NotFoundHandlerMiddleware::class, static function (TemplateRendererInterface $renderer) { + return new NotFoundHandlerMiddleware(static function (ServerRequestInterface $request) use ($renderer): ResponseInterface { + if (str_starts_with($request->getUri()->getPath(), '/api')) { + throw new ProblemDetailsException(new ProblemDetails( + type: '://problem/not-found', + title: 'Not found.', + status: 404, + detail: "The requested uri ({$request->getUri()->getPath()}) could not be found." + )); + } + + return new HtmlResponse( + $renderer->render('404.tpl'), + 404 + ); + }); +}); + $container->set(ServerRequestInterface::class, static function () { return (new ServerRequestFactory())->createServerRequest($_SERVER['REQUEST_METHOD'], $_SERVER['REQUEST_URI'], $_SERVER); })->cache(false); +$container->set(UploadedFileFactoryInterface::class, UploadedFileFactory::class); +$container->set(StreamFactoryInterface::class, StreamFactory::class); $container->set(ResponseFactoryInterface::class, ResponseFactory::class); $container->set( @@ -126,20 +169,6 @@ static function() use ($container) { }); }); -$container->set(FormatterInterface::class, function () { - return new class implements FormatterInterface { - - public function format(ResponseInterface $response, Throwable $throwable, RequestInterface $request): ResponseInterface - { - $formatter = str_starts_with($request->getUri()->getPath(), '/api') ? - new JsonFormatter() : - new HtmlFormatter(isProduction()); - - return $formatter->format($response, $throwable, $request); - } - }; -}); - $container->set(TemplateRendererInterface::class, fn() => new LatteRenderer(storage_path('views'), cache_path('views'), !isProduction())); $container->set(Logger::class, function (): Logger { From b495af4b4ea4be04351a5d04e155377d985f48f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Debussch=C3=A8re?= Date: Tue, 5 Aug 2025 20:40:03 +0200 Subject: [PATCH 11/20] chore: clean namespace use --- config/container.php | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/config/container.php b/config/container.php index 99b9839..f884691 100644 --- a/config/container.php +++ b/config/container.php @@ -1,11 +1,12 @@ Date: Wed, 6 Aug 2025 14:12:32 +0200 Subject: [PATCH 12/20] clean: deletion of chef cli --- chef | 9 --------- composer.json | 1 - 2 files changed, 10 deletions(-) delete mode 100755 chef diff --git a/chef b/chef deleted file mode 100755 index 3a4e4a7..0000000 --- a/chef +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/php -run(); diff --git a/composer.json b/composer.json index 7eeb38b..45e754c 100644 --- a/composer.json +++ b/composer.json @@ -64,7 +64,6 @@ } }, "scripts": { - "pre-install-cmd": "App\\Package\\Installer::install", "post-create-project-cmd": [ "NiftyCo\\Skeletor\\Runner::execute" ], From 8249d987a1b64c78c8b8b1c92cc2ca048ae6bcac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Debussch=C3=A8re?= Date: Wed, 6 Aug 2025 14:40:39 +0200 Subject: [PATCH 13/20] clean: container definitions --- config/container.database.php | 15 +++ config/container.handler.php | 56 +++++++++ config/container.http.php | 24 ++++ config/container.logs.php | 29 +++++ config/container.middlewares.php | 48 ++++++++ config/container.php | 188 +------------------------------ config/container.routes.php | 43 +++++++ config/container.views.php | 14 +++ 8 files changed, 235 insertions(+), 182 deletions(-) create mode 100644 config/container.database.php create mode 100644 config/container.handler.php create mode 100644 config/container.http.php create mode 100644 config/container.logs.php create mode 100644 config/container.middlewares.php create mode 100644 config/container.routes.php create mode 100644 config/container.views.php diff --git a/config/container.database.php b/config/container.database.php new file mode 100644 index 0000000..407748b --- /dev/null +++ b/config/container.database.php @@ -0,0 +1,15 @@ +set(AdapterInterface::class, Adapter::class) + ->addParameter([ + 'driver' => 'Pdo_Sqlite', + 'dsn' => 'sqlite:'.storage_path('database.sqlite') + ]); + +}; diff --git a/config/container.handler.php b/config/container.handler.php new file mode 100644 index 0000000..aca5dc9 --- /dev/null +++ b/config/container.handler.php @@ -0,0 +1,56 @@ +set(RequestHandlerRunnerInterface::class, static function (ContainerInterface $container) { + return new RequestHandlerRunner( + $container->get(RequestHandlerInterface::class), + new Emitter(), + static fn() => $container->get(ServerRequestInterface::class), + static function() use ($container) { + $engine = $container->get(TemplateRendererInterface::class); + $response = ($container->get(ResponseFactoryInterface::class))->createResponse(500); + + $response->getBody()->write($engine->render('500.tpl')); + + return $response; + } + ); + }); + + $container->set(RequestHandlerInterface::class, static function (ContainerInterface $container) { + return (new RequestHandler()) + ->middleware($container->get(ErrorHandlerMiddleware::class)) + ->middleware($container->get(ProblemDetailsMiddleware::class)) + ->middleware($container->get(TrailingSlashMiddleware::class)) + ->middleware($container->get(ContentLengthMiddleware::class)) + ->middleware($container->get(RouteMiddleware::class)) + ->middleware($container->get(ImplicitHeadMiddleware::class)) + ->middleware($container->get(ImplicitOptionsMiddleware::class)) + ->middleware($container->get(MethodNotAllowedMiddleware::class)) + ->middleware($container->get(BodyParserMiddleware::class)) + ->middleware($container->get(UploadedFilesParserMiddleware::class)) + ->middleware($container->get(DispatchMiddleware::class)) + ->middleware($container->get(NotFoundHandlerMiddleware::class)); + }); + +}; diff --git a/config/container.http.php b/config/container.http.php new file mode 100644 index 0000000..04d8ef0 --- /dev/null +++ b/config/container.http.php @@ -0,0 +1,24 @@ +set(ServerRequestInterface::class, static function () { + $scheme = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http'; + $host = $_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME']; + $uri = "$scheme://$host" . ($_SERVER['REQUEST_URI'] ?? ''); + + return (new ServerRequestFactory())->createServerRequest($_SERVER['REQUEST_METHOD'], $uri, $_SERVER); + })->cache(false); + + $container->set(UploadedFileFactoryInterface::class, UploadedFileFactory::class); + $container->set(StreamFactoryInterface::class, StreamFactory::class); + $container->set(ResponseFactoryInterface::class, ResponseFactory::class); + +}; diff --git a/config/container.logs.php b/config/container.logs.php new file mode 100644 index 0000000..49e5bfb --- /dev/null +++ b/config/container.logs.php @@ -0,0 +1,29 @@ +set(LoggerInterface::class, Logger::class); + + $container->set(Logger::class, function (): Logger { + $name = env('APP_NAME', 'App'); + + $handlers = [ + new StreamHandler( + logs_path(env('LOG_CHANNEL', 'app').'.log'), + Level::fromName(env('LOG_LEVEL', 'Debug')) + ) + ]; + + $processors = [new PsrLogMessageProcessor(removeUsedContextFields: true)]; + $datetime_zone = new DateTimeZone(env('TIMEZONE', 'UTC')); + + return new Logger($name, $handlers, $processors, $datetime_zone); + }); + +}; diff --git a/config/container.middlewares.php b/config/container.middlewares.php new file mode 100644 index 0000000..0fbe458 --- /dev/null +++ b/config/container.middlewares.php @@ -0,0 +1,48 @@ +set(ErrorHandlerMiddleware::class, static fn(TemplateRendererInterface $renderer) => new ErrorHandlerMiddleware( + static function (Throwable $throwable, ServerRequestInterface $request) use ($renderer): ResponseInterface { + if (str_starts_with($request->getUri()->getPath(), '/api')) { + return new JsonResponse(new ProblemDetails( + type: '://problem/internal-server-error', + title: 'Internal server error.', + status: 500, + detail: $throwable->getMessage() + ), 500); + } + + return new HtmlResponse( + $renderer->render('500.tpl'), + 500 + ); + } + )); + + $container->set(NotFoundHandlerMiddleware::class, static function (TemplateRendererInterface $renderer) { + return new NotFoundHandlerMiddleware(static function (ServerRequestInterface $request) use ($renderer): ResponseInterface { + if (str_starts_with($request->getUri()->getPath(), '/api')) { + throw new ProblemDetailsException(new ProblemDetails( + type: '://problem/not-found', + title: 'Not found.', + status: 404, + detail: "The requested uri ({$request->getUri()->getPath()}) could not be found." + )); + } + + return new HtmlResponse( + $renderer->render('404.tpl'), + 404 + ); + }); + }); + +}; diff --git a/config/container.php b/config/container.php index f884691..c8a684d 100644 --- a/config/container.php +++ b/config/container.php @@ -1,191 +1,15 @@ setCacheByDefault(true); -$container->set(RequestHandlerRunnerInterface::class, static function (ContainerInterface $container) { - return new RequestHandlerRunner( - $container->get(RequestHandlerInterface::class), - new Emitter(), - static fn() => $container->get(ServerRequestInterface::class), - static function() use ($container) { - $engine = $container->get(TemplateRendererInterface::class); - $response = ($container->get(ResponseFactoryInterface::class))->createResponse(500); - - $response->getBody()->write($engine->render('500.tpl')); - - return $response; - } - ); -}); - -$container->set(RequestHandlerInterface::class, static function (ContainerInterface $container) { - return (new RequestHandler()) - ->middleware($container->get(ErrorHandlerMiddleware::class)) - ->middleware($container->get(ProblemDetailsMiddleware::class)) - ->middleware($container->get(TrailingSlashMiddleware::class)) - ->middleware($container->get(ContentLengthMiddleware::class)) - ->middleware($container->get(RouteMiddleware::class)) - ->middleware($container->get(ImplicitHeadMiddleware::class)) - ->middleware($container->get(ImplicitOptionsMiddleware::class)) - ->middleware($container->get(MethodNotAllowedMiddleware::class)) - ->middleware($container->get(BodyParserMiddleware::class)) - ->middleware($container->get(UploadedFilesParserMiddleware::class)) - ->middleware($container->get(DispatchMiddleware::class)) - ->middleware($container->get(NotFoundHandlerMiddleware::class)); -}); - -$container->set(ErrorHandlerMiddleware::class, static fn(TemplateRendererInterface $renderer) => new ErrorHandlerMiddleware( - static function (Throwable $throwable, ServerRequestInterface $request) use ($renderer): ResponseInterface { - if (str_starts_with($request->getUri()->getPath(), '/api')) { - return new JsonResponse(new ProblemDetails( - type: '://problem/internal-server-error', - title: 'Internal server error.', - status: 500, - detail: $throwable->getMessage() - ), 500); - } - - return new HtmlResponse( - $renderer->render('500.tpl'), - 500 - ); - } -)); - -$container->set(NotFoundHandlerMiddleware::class, static function (TemplateRendererInterface $renderer) { - return new NotFoundHandlerMiddleware(static function (ServerRequestInterface $request) use ($renderer): ResponseInterface { - if (str_starts_with($request->getUri()->getPath(), '/api')) { - throw new ProblemDetailsException(new ProblemDetails( - type: '://problem/not-found', - title: 'Not found.', - status: 404, - detail: "The requested uri ({$request->getUri()->getPath()}) could not be found." - )); - } - - return new HtmlResponse( - $renderer->render('404.tpl'), - 404 - ); - }); -}); - -$container->set(ServerRequestInterface::class, static function () { - return (new ServerRequestFactory())->createServerRequest($_SERVER['REQUEST_METHOD'], $_SERVER['REQUEST_URI'], $_SERVER); -})->cache(false); - -$container->set(UploadedFileFactoryInterface::class, UploadedFileFactory::class); -$container->set(StreamFactoryInterface::class, StreamFactory::class); -$container->set(ResponseFactoryInterface::class, ResponseFactory::class); - -$container->set( - AttributeRouteLoader::class, - static fn(ContainerInterface $container) => ( - new AttributeRouteLoader( - [__ROOT_DIR__ . '/src/Application'], - $container, - cache_path('loader.routes.cache.php'), - !isProduction() - ))->load() -); - -$container->set(RouterInterface::class, static function (AttributeRouteLoader $loader) { - $routes = $loader->getRoutes(); - - if (isProduction()) { - return new FastRouteRouter( - array_combine( - array_map(fn(RouteInterface $route) => $route->getName(), $routes), - $routes - ), - cache_path('router.routes.cache.php') - ); - } - - $router = new FastRouteRouter(); - foreach ($routes as $route) { - $router->addRoute($route); - } - - return $router; -}); - -$container->set(NotFoundHandlerMiddleware::class, static function (TemplateRendererInterface $renderer) { - return new NotFoundHandlerMiddleware(static function (ServerRequestInterface $request) use ($renderer): ResponseInterface { - if (str_starts_with($request->getUri()->getPath(), '/api')) { - throw new ProblemDetailsException(new ProblemDetails( - type: '://problem/not-found', - title: 'Not found.', - status: 404, - detail: "The requested uri ({$request->getUri()->getPath()}) could not be found." - )); - } - - return new HtmlResponse( - $renderer->render('404.tpl'), - 404 - ); - }); -}); - -$container->set(TemplateRendererInterface::class, fn() => new LatteRenderer(storage_path('views'), cache_path('views'), !isProduction())); - -$container->set(Logger::class, function (): Logger { - $name = env('APP_NAME', 'App'); - - $handlers = [ - new StreamHandler( - logs_path(env('LOG_CHANNEL', 'app').'.log'), - Level::fromName(env('LOG_LEVEL', 'Debug')) - ) - ]; - - $processors = [new PsrLogMessageProcessor(removeUsedContextFields: true)]; - $datetime_zone = new DateTimeZone(env('TIMEZONE', 'UTC')); - - return new Logger($name, $handlers, $processors, $datetime_zone); -}); - -$container - ->set(AdapterInterface::class, Adapter::class) - ->addParameter([ - 'driver' => 'Pdo_Sqlite', - 'dsn' => 'sqlite:'.storage_path('database.sqlite') - ]); +(require_once __DIR__.'/container.handler.php')($container); +(require_once __DIR__.'/container.middlewares.php')($container); +(require_once __DIR__.'/container.http.php')($container); +(require_once __DIR__.'/container.routes.php')($container); +(require_once __DIR__.'/container.logs.php')($container); +(require_once __DIR__.'/container.views.php')($container); return $container; diff --git a/config/container.routes.php b/config/container.routes.php new file mode 100644 index 0000000..07655ca --- /dev/null +++ b/config/container.routes.php @@ -0,0 +1,43 @@ +set(AttributeRouteLoader::class, static function (ContainerInterface $container) { + $loader = new AttributeRouteLoader( + [__ROOT_DIR__ . '/src/Application'], + $container, + cache_path('loader.routes.cache.php'), + !isProduction() + ); + + return $loader->load(); + }); + + $container->set(RouterInterface::class, static function (AttributeRouteLoader $loader) { + $routes = $loader->getRoutes(); + + if (isProduction()) { + return new FastRouteRouter( + array_combine( + array_map(fn(RouteInterface $route) => $route->getName(), $routes), + $routes + ), + cache_path('router.routes.cache.php') + ); + } + + $router = new FastRouteRouter(); + foreach ($routes as $route) { + $router->addRoute($route); + } + + return $router; + }); + +}; diff --git a/config/container.views.php b/config/container.views.php new file mode 100644 index 0000000..fba60fa --- /dev/null +++ b/config/container.views.php @@ -0,0 +1,14 @@ +set( + TemplateRendererInterface::class, + fn() => new LatteRenderer(storage_path('views'), cache_path('views'), !isProduction()) + ); + +}; From 2792fa99997739379aa551572af6504c7c2f81f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Debussch=C3=A8re?= Date: Thu, 7 Aug 2025 13:03:02 +0200 Subject: [PATCH 14/20] upd(container): comments --- config/container.php | 13 +++++++------ config/{ => containers}/container.database.php | 8 ++++++++ config/{ => containers}/container.handler.php | 15 +++++++++++++++ config/{ => containers}/container.http.php | 9 +++++++++ config/{ => containers}/container.logs.php | 6 ++++++ config/{ => containers}/container.middlewares.php | 15 +++++++++++++++ config/{ => containers}/container.routes.php | 12 ++++++++++++ config/{ => containers}/container.views.php | 5 +++++ 8 files changed, 77 insertions(+), 6 deletions(-) rename config/{ => containers}/container.database.php (53%) rename config/{ => containers}/container.handler.php (77%) rename config/{ => containers}/container.http.php (73%) rename config/{ => containers}/container.logs.php (77%) rename config/{ => containers}/container.middlewares.php (72%) rename config/{ => containers}/container.routes.php (73%) rename config/{ => containers}/container.views.php (59%) diff --git a/config/container.php b/config/container.php index c8a684d..315e2c5 100644 --- a/config/container.php +++ b/config/container.php @@ -5,11 +5,12 @@ $container = new Container(); $container->setCacheByDefault(true); -(require_once __DIR__.'/container.handler.php')($container); -(require_once __DIR__.'/container.middlewares.php')($container); -(require_once __DIR__.'/container.http.php')($container); -(require_once __DIR__.'/container.routes.php')($container); -(require_once __DIR__.'/container.logs.php')($container); -(require_once __DIR__.'/container.views.php')($container); +(require_once __DIR__ . '/containers/container.handler.php')($container); +(require_once __DIR__ . '/containers/container.middlewares.php')($container); +(require_once __DIR__ . '/containers/container.http.php')($container); +(require_once __DIR__ . '/containers/container.routes.php')($container); +(require_once __DIR__ . '/containers/container.logs.php')($container); +(require_once __DIR__ . '/containers/container.views.php')($container); +(require_once __DIR__ . '/containers/container.database.php')($container); return $container; diff --git a/config/container.database.php b/config/containers/container.database.php similarity index 53% rename from config/container.database.php rename to config/containers/container.database.php index 407748b..ad6e044 100644 --- a/config/container.database.php +++ b/config/containers/container.database.php @@ -5,6 +5,14 @@ return static function (Container $container) { + /* + * An adapter for SQLite database. + * + * This adapter uses the `Pdo_Sqlite` driver and connects to an SQLite database file located at + * `storage/database.sqlite`. + * + * It is used by Repositories (in `Infrastructure` namespace) to interact with the SQLite database. + */ $container ->set(AdapterInterface::class, Adapter::class) ->addParameter([ diff --git a/config/container.handler.php b/config/containers/container.handler.php similarity index 77% rename from config/container.handler.php rename to config/containers/container.handler.php index aca5dc9..6100a98 100644 --- a/config/container.handler.php +++ b/config/containers/container.handler.php @@ -21,6 +21,15 @@ return static function (Container $container) { + /* + * The RequestHandlerRunner is responsible for running the RequestHandler and emit a response. + * + * As parameters, it takes: + * - A RequestHandlerInterface instance that will handle the request + * - An Emitter instance that will emit the response + * - A callable that returns the ServerRequestInterface instance + * - A callable that returns a fallback response in case of an error + */ $container->set(RequestHandlerRunnerInterface::class, static function (ContainerInterface $container) { return new RequestHandlerRunner( $container->get(RequestHandlerInterface::class), @@ -37,6 +46,12 @@ static function() use ($container) { ); }); + /* + * The RequestHandler is responsible for handling the request and returning a response. + * + * It is composed of several middlewares that will be executed in the order they are added (FIFO). + * Predefined middlewares are included to handle common tasks, feel free to add your own. + */ $container->set(RequestHandlerInterface::class, static function (ContainerInterface $container) { return (new RequestHandler()) ->middleware($container->get(ErrorHandlerMiddleware::class)) diff --git a/config/container.http.php b/config/containers/container.http.php similarity index 73% rename from config/container.http.php rename to config/containers/container.http.php index 04d8ef0..9147664 100644 --- a/config/container.http.php +++ b/config/containers/container.http.php @@ -9,6 +9,12 @@ return static function (Container $container) { + /* + * This is a factory for the PSR-7 ServerRequestInterface. + * + * It creates a ServerRequest object based on the current PHP environment. + * It uses the $_SERVER superglobal to determine the request method, URI, and other relevant information. + */ $container->set(ServerRequestInterface::class, static function () { $scheme = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http'; $host = $_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME']; @@ -17,6 +23,9 @@ return (new ServerRequestFactory())->createServerRequest($_SERVER['REQUEST_METHOD'], $uri, $_SERVER); })->cache(false); + /* + * Necessary factories for PSR-7, used in middlewares and controllers. + */ $container->set(UploadedFileFactoryInterface::class, UploadedFileFactory::class); $container->set(StreamFactoryInterface::class, StreamFactory::class); $container->set(ResponseFactoryInterface::class, ResponseFactory::class); diff --git a/config/container.logs.php b/config/containers/container.logs.php similarity index 77% rename from config/container.logs.php rename to config/containers/container.logs.php index 49e5bfb..8425310 100644 --- a/config/container.logs.php +++ b/config/containers/container.logs.php @@ -10,6 +10,12 @@ $container->set(LoggerInterface::class, Logger::class); + /* + * A simple PSR-3 compliant logger instance. + * + * Logs are written to the `storage/logs/app.log` file in the application root by default. + * The log level and channel can be configured via environment variables. + */ $container->set(Logger::class, function (): Logger { $name = env('APP_NAME', 'App'); diff --git a/config/container.middlewares.php b/config/containers/container.middlewares.php similarity index 72% rename from config/container.middlewares.php rename to config/containers/container.middlewares.php index 0fbe458..4132306 100644 --- a/config/container.middlewares.php +++ b/config/containers/container.middlewares.php @@ -9,6 +9,15 @@ return static function (Container $container) { + /* + * The ErrorHandlerMiddleware is responsible for handling exceptions and returning appropriate responses. + * + * If the request is for an API endpoint, it returns a JSON response with a ProblemDetails object. + * Otherwise, it returns an HTML response with a 500 error page. + * + * It should be registered before any other middleware so that it can catch exceptions and handle them + * appropriately. + */ $container->set(ErrorHandlerMiddleware::class, static fn(TemplateRendererInterface $renderer) => new ErrorHandlerMiddleware( static function (Throwable $throwable, ServerRequestInterface $request) use ($renderer): ResponseInterface { if (str_starts_with($request->getUri()->getPath(), '/api')) { @@ -27,6 +36,12 @@ static function (Throwable $throwable, ServerRequestInterface $request) use ($re } )); + /* + * The NotFoundHandlerMiddleware is responsible for handling 404 Not Found errors. + * + * If the request is for an API endpoint, it returns a JSON response with a ProblemDetails object. + * Otherwise, it returns an HTML response with a 404 error page. + */ $container->set(NotFoundHandlerMiddleware::class, static function (TemplateRendererInterface $renderer) { return new NotFoundHandlerMiddleware(static function (ServerRequestInterface $request) use ($renderer): ResponseInterface { if (str_starts_with($request->getUri()->getPath(), '/api')) { diff --git a/config/container.routes.php b/config/containers/container.routes.php similarity index 73% rename from config/container.routes.php rename to config/containers/container.routes.php index 07655ca..f052a98 100644 --- a/config/container.routes.php +++ b/config/containers/container.routes.php @@ -8,6 +8,12 @@ return static function (Container $container) { + /* + * Load routes from attributes in controllers. + * + * The routes will be cached in the specified file when in production environment. + * In development, the routes will be loaded from the source files directly. + */ $container->set(AttributeRouteLoader::class, static function (ContainerInterface $container) { $loader = new AttributeRouteLoader( [__ROOT_DIR__ . '/src/Application'], @@ -19,6 +25,12 @@ return $loader->load(); }); + /* + * Register the router service. + * + * In production, it uses a cached version of the routes for performance. + * In development, it loads routes directly from the source files. + */ $container->set(RouterInterface::class, static function (AttributeRouteLoader $loader) { $routes = $loader->getRoutes(); diff --git a/config/container.views.php b/config/containers/container.views.php similarity index 59% rename from config/container.views.php rename to config/containers/container.views.php index fba60fa..fb69c29 100644 --- a/config/container.views.php +++ b/config/containers/container.views.php @@ -6,6 +6,11 @@ return static function (Container $container) { + /* + * A Template renderer is used to render views. + * This one uses Latte as the template engine. + * It is configured to use the 'storage/views' directory for templates and `storage/cache/views` for caching. + */ $container->set( TemplateRendererInterface::class, fn() => new LatteRenderer(storage_path('views'), cache_path('views'), !isProduction()) From 71321a1914c466e04b4aa45d496726dfb1671b5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Debussch=C3=A8re?= Date: Thu, 7 Aug 2025 13:10:33 +0200 Subject: [PATCH 15/20] upd(container): static fn and parameters --- config/containers/container.handler.php | 31 ++++++++++++++----------- config/containers/container.logs.php | 2 +- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/config/containers/container.handler.php b/config/containers/container.handler.php index 6100a98..1d72387 100644 --- a/config/containers/container.handler.php +++ b/config/containers/container.handler.php @@ -30,20 +30,25 @@ * - A callable that returns the ServerRequestInterface instance * - A callable that returns a fallback response in case of an error */ - $container->set(RequestHandlerRunnerInterface::class, static function (ContainerInterface $container) { - return new RequestHandlerRunner( - $container->get(RequestHandlerInterface::class), - new Emitter(), - static fn() => $container->get(ServerRequestInterface::class), - static function() use ($container) { - $engine = $container->get(TemplateRendererInterface::class); - $response = ($container->get(ResponseFactoryInterface::class))->createResponse(500); + $container->set( + RequestHandlerRunnerInterface::class, + static function ( + RequestHandlerInterface $handler, + ServerRequestInterface $request, + TemplateRendererInterface $renderer, + ResponseFactoryInterface $factory + ) { + return new RequestHandlerRunner( + $handler, + new Emitter(), + static fn() => $request, + static function() use ($renderer, $factory) { + $response = $factory->createResponse(500); + $response->getBody()->write($renderer->render('500.tpl')); - $response->getBody()->write($engine->render('500.tpl')); - - return $response; - } - ); + return $response; + } + ); }); /* diff --git a/config/containers/container.logs.php b/config/containers/container.logs.php index 8425310..5a4a910 100644 --- a/config/containers/container.logs.php +++ b/config/containers/container.logs.php @@ -16,7 +16,7 @@ * Logs are written to the `storage/logs/app.log` file in the application root by default. * The log level and channel can be configured via environment variables. */ - $container->set(Logger::class, function (): Logger { + $container->set(Logger::class, static function (): Logger { $name = env('APP_NAME', 'App'); $handlers = [ From 333f69de958aaa2cf5ae7be7310f3375e22d8b51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Debussch=C3=A8re?= Date: Thu, 7 Aug 2025 13:18:54 +0200 Subject: [PATCH 16/20] fix(services): warning on return type --- src/Domain/AlbumService.php | 12 ++++++++++++ src/Domain/ArtistService.php | 12 ++++++++++++ 2 files changed, 24 insertions(+) diff --git a/src/Domain/AlbumService.php b/src/Domain/AlbumService.php index 6ec6bcb..427df64 100644 --- a/src/Domain/AlbumService.php +++ b/src/Domain/AlbumService.php @@ -28,6 +28,7 @@ public function all(): array public function find(int $id): ?Album { $album = $this->repository->find($id); + if ($album === null) { $this->logger->error('Album with ID #{id} not found', ['{id}' => $id]); @@ -39,6 +40,17 @@ public function find(int $id): ?Album )); } + if (!$album instanceof Album) { + $this->logger->error('Album with ID #{id} is not an instance of Album', ['{id}' => $id]); + + throw new ProblemDetailsException(new ProblemDetails( + type: '://problem/invalid-type', + title: 'Invalid album type.', + status: 500, + detail: "The album with ID {$id} is not a valid Album instance." + )); + } + return $album; } diff --git a/src/Domain/ArtistService.php b/src/Domain/ArtistService.php index fb7d08f..6dc9288 100644 --- a/src/Domain/ArtistService.php +++ b/src/Domain/ArtistService.php @@ -28,6 +28,7 @@ public function all(): array public function find(int $id): ?Artist { $artist = $this->repository->find($id); + if ($artist === null) { $this->logger->error('Artist with ID #{id} not found', ['{id}' => $id]); @@ -39,6 +40,17 @@ public function find(int $id): ?Artist )); } + if (!$artist instanceof Artist) { + $this->logger->error('Artist with ID #{id} is not an instance of Artist', ['{id}' => $id]); + + throw new ProblemDetailsException(new ProblemDetails( + type: '://problem/invalid-artist', + title: 'Invalid artist data.', + status: 500, + detail: "The artist with ID {$id} is not a valid Artist instance." + )); + } + return $artist; } From 7fb18da71dd74821cfb53c0729c94c2bc68b5eee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Debussch=C3=A8re?= Date: Fri, 8 Aug 2025 16:44:51 +0200 Subject: [PATCH 17/20] upd(README): description --- .env.example | 5 ++--- README.md | 11 ++++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.env.example b/.env.example index 62a5eb1..b58e560 100644 --- a/.env.example +++ b/.env.example @@ -6,9 +6,8 @@ APP_VERSION=1.0.0 # Environment # ----------- -# Development and debug environment. +# Development environment. APP_ENV=development -APP_DEBUG=true # URL # --- @@ -39,4 +38,4 @@ FALLBACK_LOCALE=en # LOG_CHANNEL: name usually used in Monolog, see file config/container.php, at the ErrorHandlerMiddleware definition. # LOG_LEVEL: the minimum logging level at which the handler will be triggered, MUST be one of Level::class constant from Monolog. LOG_CHANNEL=app -LOG_LEVEL=Debug \ No newline at end of file +LOG_LEVEL=Debug diff --git a/README.md b/README.md index 245b299..7eee3f3 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Sometimes, you don't need an overkill solution like [Laravel](https://laravel.com/) or [Symfony](https://symfony.com/). -Borsch is a simple and efficient [PSR-15](https://www.php-fig.org/psr/psr-15/) micro framework made to kick start your +Borsch is a simple, real fast and efficient [PSR-15](https://www.php-fig.org/psr/psr-15/) micro framework made to kick-start your web app or API development by using the tools you prefer, and provides minimal structure and facilities to ease your development. @@ -22,8 +22,7 @@ It natively features : * [Router](https://github.com/borschphp/borsch-router) * [Request Handlers and Middlewares](https://github.com/borschphp/borsch-requesthandler) * [Environment Variables](https://github.com/vlucas/phpdotenv) -* [Error Handling](https://github.com/borschphp/borsch-skeleton/blob/master/src/Middleware/ErrorHandlerMiddleware.php) -* [Listeners](https://github.com/borschphp/borsch-skeleton/blob/master/src/Listener/MonologListener.php) +* [Error Handling](https://github.com/borschphp/borsch-middlewares/blob/main/src/Middleware/ErrorHandlerMiddleware.php) Can be enriched with : @@ -41,7 +40,7 @@ Via [composer](https://getcomposer.org/) : ## Web servers -Instructions below will start a server on http://0.0.0.0:8080. +Instructions below will start a server on http://0.0.0.0:8080 (or https://localhost if you use FrankenPHP). ### PHP Built-in web server @@ -74,6 +73,8 @@ docker run \ -v $PWD:/app \ -p 80:8080 -p 443:443 -p 443:443/udp \ dunglas/frankenphp +# or use the shortcut +composer franken ``` ## Documentation @@ -89,4 +90,4 @@ Do not hesitate to check [Mezzio](https://docs.mezzio.dev/mezzio/) and [Laravel] ## License -The package is licensed under the MIT license. See [License File](https://github.com/borschphp/borsch-skeleton/blob/master/LICENSE.md) for more information. \ No newline at end of file +The package is licensed under the MIT license. See [License File](https://github.com/borschphp/borsch-skeleton/blob/master/LICENSE.md) for more information. From e28f8bbfeb4e026579d2d7028135943743ff25d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Debussch=C3=A8re?= Date: Fri, 8 Aug 2025 16:56:11 +0200 Subject: [PATCH 18/20] upd(welcome): template + docs URL --- storage/views/home.tpl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/storage/views/home.tpl b/storage/views/home.tpl index b3e2ef7..89769ce 100644 --- a/storage/views/home.tpl +++ b/storage/views/home.tpl @@ -4,8 +4,8 @@ {block content}
- Logo -

Fuel Your Code with Flavor

+ Logo +

Fuel Your Code with Flavor

@@ -20,7 +20,7 @@

Get started quickly with clear, well-structured documentation covering everything you need to build and scale.

From 872e5c89ad5b6731bde40ca615fd5e2a847bbc77 Mon Sep 17 00:00:00 2001 From: debuss Date: Sat, 9 Aug 2025 23:33:03 +0200 Subject: [PATCH 19/20] chore(comment): added missing throws comments --- src/Application/Handler/AlbumHandler.php | 9 +++++++++ src/Application/Handler/ArtistHandler.php | 9 +++++++++ src/Domain/AlbumService.php | 16 ++++++++++++++-- src/Domain/ArtistService.php | 16 ++++++++++++++-- 4 files changed, 46 insertions(+), 4 deletions(-) diff --git a/src/Application/Handler/AlbumHandler.php b/src/Application/Handler/AlbumHandler.php index 5cc632b..8ba59b2 100644 --- a/src/Application/Handler/AlbumHandler.php +++ b/src/Application/Handler/AlbumHandler.php @@ -18,6 +18,9 @@ public function __construct( private AlbumService $service ) {} + /** + * @throws ProblemDetailsException + */ #[Get(path: '[/{id}]', name: 'albums')] #[Post] #[Put(path: '/{id:\d+}')] @@ -37,6 +40,9 @@ public function handle(ServerRequestInterface $request): ResponseInterface }; } + /** + * @throws ProblemDetailsException + */ #[OA\Get( path: '/albums', description: 'Get all albums', @@ -174,6 +180,9 @@ private function updateAlbum(int $id, array $body): ResponseInterface return new JsonResponse($updated_album); } + /** + * @throws ProblemDetailsException + */ #[OA\Delete( path: '/albums/{id}', description: 'Delete an album by ID', diff --git a/src/Application/Handler/ArtistHandler.php b/src/Application/Handler/ArtistHandler.php index 829552e..ccfbb70 100644 --- a/src/Application/Handler/ArtistHandler.php +++ b/src/Application/Handler/ArtistHandler.php @@ -18,6 +18,9 @@ public function __construct( private ArtistService $service ) {} + /** + * @throws ProblemDetailsException + */ #[Get(path: '[/{id}]', name: 'artists')] #[Post] #[Put(path: '/{id:\d+}')] @@ -37,6 +40,9 @@ public function handle(ServerRequestInterface $request): ResponseInterface }; } + /** + * @throws ProblemDetailsException + */ #[OA\Get( path: '/artists', description: 'Get all artists', @@ -168,6 +174,9 @@ private function updateArtist(int $id, array $body): ResponseInterface return new JsonResponse($updated_artist); } + /** + * @throws ProblemDetailsException + */ #[OA\Delete( path: '/artists/{id}', description: 'Delete an artist', diff --git a/src/Domain/AlbumService.php b/src/Domain/AlbumService.php index 427df64..3f23269 100644 --- a/src/Domain/AlbumService.php +++ b/src/Domain/AlbumService.php @@ -25,6 +25,9 @@ public function all(): array return $this->repository->all(); } + /** + * @throws ProblemDetailsException + */ public function find(int $id): ?Album { $album = $this->repository->find($id); @@ -54,7 +57,10 @@ public function find(int $id): ?Album return $album; } - /** @param array{title: string, artist_id: int} $data */ + /** + * @param array{title: string, artist_id: int} $data + * @throws ProblemDetailsException + */ public function create(array $data): Album { if (!isset($data['title'], $data['artist_id'])) { @@ -83,7 +89,10 @@ public function create(array $data): Album return $this->find($id); } - /** @param array{title?: string, artist_id?: int} $data */ + /** + * @param array{title?: string, artist_id?: int} $data + * @throws ProblemDetailsException + */ public function update(int $id, array $data): Album { if (!isset($data['title']) && !isset($data['artist_id'])) { @@ -119,6 +128,9 @@ public function update(int $id, array $data): Album return $this->find($id); } + /** + * @throws ProblemDetailsException + */ public function delete(int $id): bool { // Making sure it exists diff --git a/src/Domain/ArtistService.php b/src/Domain/ArtistService.php index 6dc9288..d65e5a4 100644 --- a/src/Domain/ArtistService.php +++ b/src/Domain/ArtistService.php @@ -25,6 +25,9 @@ public function all(): array return $this->repository->all(); } + /** + * @throws ProblemDetailsException + */ public function find(int $id): ?Artist { $artist = $this->repository->find($id); @@ -54,7 +57,10 @@ public function find(int $id): ?Artist return $artist; } - /** @param array{name: string} $data */ + /** + * @param array{name: string} $data + * @throws ProblemDetailsException + */ public function create(array $data): Artist { if (!isset($data['name'])) { @@ -83,7 +89,10 @@ public function create(array $data): Artist return $this->find($id); } - /** @param array{name: string} $data */ + /** + * @param array{name: string} $data + * @throws ProblemDetailsException + */ public function update(int $id, array $data): Artist { if (!isset($data['name'])) { @@ -114,6 +123,9 @@ public function update(int $id, array $data): Artist return $this->find($id); } + /** + * @throws ProblemDetailsException + */ public function delete(int $id): bool { // Making sure it exists From 8dd85dbfe7eff4f31f5426c4e48ed4595b435fc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Debussch=C3=A8re?= Date: Fri, 29 Aug 2025 16:02:55 +0200 Subject: [PATCH 20/20] clean: replace README with gitignore files --- storage/cache/.gitignore | 0 storage/cache/README | 1 - storage/cache/views/.gitignore | 0 storage/logs/.gitignore | 0 storage/logs/README | 1 - 5 files changed, 2 deletions(-) create mode 100644 storage/cache/.gitignore delete mode 100644 storage/cache/README create mode 100644 storage/cache/views/.gitignore create mode 100644 storage/logs/.gitignore delete mode 100644 storage/logs/README diff --git a/storage/cache/.gitignore b/storage/cache/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/storage/cache/README b/storage/cache/README deleted file mode 100644 index 83d2153..0000000 --- a/storage/cache/README +++ /dev/null @@ -1 +0,0 @@ -This folder can be used to cache everything you need from any part of your code. diff --git a/storage/cache/views/.gitignore b/storage/cache/views/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/storage/logs/.gitignore b/storage/logs/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/storage/logs/README b/storage/logs/README deleted file mode 100644 index 0d03dd3..0000000 --- a/storage/logs/README +++ /dev/null @@ -1 +0,0 @@ -This folder can be used to log everything you need from any part of your code.