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/.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/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. 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/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 97a9773..45e754c 100644 --- a/composer.json +++ b/composer.json @@ -24,16 +24,16 @@ "ext-libxml": "*", "ext-pdo": "*", "ext-simplexml": "*", - "borschphp/application": "^2", - "borschphp/middlewares": "^1", + "borschphp/middlewares": "^2", "borschphp/latte": "^1", - "borschphp/chef": "^1", - "laminas/laminas-diactoros": "^3", + "borschphp/requesthandler": "^2", + "borschphp/container": "^2", + "borschphp/router": "^3.1", + "borschphp/psr7": "^1.0", + "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 +43,9 @@ }, "autoload": { "psr-4": { - "App\\": "src/" + "Application\\": "src/Application/", + "Domain\\": "src/Domain/", + "Infrastructure\\": "src/Infrastructure/" }, "files": [ "bootstrap/defines.inc.php", @@ -53,7 +55,7 @@ }, "autoload-dev": { "psr-4": { - "AppTest\\": "tests/" + "Tests\\": "tests/" } }, "config": { @@ -62,7 +64,6 @@ } }, "scripts": { - "pre-install-cmd": "App\\Package\\Installer::install", "post-create-project-cmd": [ "NiftyCo\\Skeletor\\Runner::execute" ], 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 72d5d84..315e2c5 100644 --- a/config/container.php +++ b/config/container.php @@ -1,16 +1,16 @@ setCacheByDefault(true); -$container->defaultToShared(); -$container->delegate(new ReflectionContainer(true)); - -(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); +(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/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/container.database.php b/config/containers/container.database.php new file mode 100644 index 0000000..ad6e044 --- /dev/null +++ b/config/containers/container.database.php @@ -0,0 +1,23 @@ +set(AdapterInterface::class, Adapter::class) + ->addParameter([ + 'driver' => 'Pdo_Sqlite', + 'dsn' => 'sqlite:'.storage_path('database.sqlite') + ]); + +}; diff --git a/config/containers/container.handler.php b/config/containers/container.handler.php new file mode 100644 index 0000000..1d72387 --- /dev/null +++ b/config/containers/container.handler.php @@ -0,0 +1,76 @@ +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')); + + return $response; + } + ); + }); + + /* + * 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)) + ->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/containers/container.http.php b/config/containers/container.http.php new file mode 100644 index 0000000..9147664 --- /dev/null +++ b/config/containers/container.http.php @@ -0,0 +1,33 @@ +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); + + /* + * 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/containers/container.logs.php b/config/containers/container.logs.php new file mode 100644 index 0000000..5a4a910 --- /dev/null +++ b/config/containers/container.logs.php @@ -0,0 +1,35 @@ +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, static 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/container.middlewares.php b/config/containers/container.middlewares.php new file mode 100644 index 0000000..4132306 --- /dev/null +++ b/config/containers/container.middlewares.php @@ -0,0 +1,63 @@ +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 + ); + } + )); + + /* + * 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')) { + 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/containers/container.routes.php b/config/containers/container.routes.php new file mode 100644 index 0000000..f052a98 --- /dev/null +++ b/config/containers/container.routes.php @@ -0,0 +1,55 @@ +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(); + }); + + /* + * 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(); + + 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/containers/container.views.php b/config/containers/container.views.php new file mode 100644 index 0000000..fb69c29 --- /dev/null +++ b/config/containers/container.views.php @@ -0,0 +1,19 @@ +set( + TemplateRendererInterface::class, + fn() => new LatteRenderer(storage_path('views'), cache_path('views'), !isProduction()) + ); + +}; 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/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/public/index.php b/public/index.php index 5a386eb..8f4cab1 100644 --- a/public/index.php +++ b/public/index.php @@ -2,9 +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 @@ -14,13 +13,6 @@ /** @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); - - $request = $container->get(ServerRequestInterface::class); - - $app->run($request); + $runner = $container->get(RequestHandlerRunnerInterface::class); + $runner->run(); })(); 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 88% rename from src/Handler/AlbumHandler.php rename to src/Application/Handler/AlbumHandler.php index 0269ff3..8ba59b2 100644 --- a/src/Handler/AlbumHandler.php +++ b/src/Application/Handler/AlbumHandler.php @@ -1,17 +1,16 @@ getAttribute('id'); @@ -33,6 +40,9 @@ public function handle(ServerRequestInterface $request): ResponseInterface }; } + /** + * @throws ProblemDetailsException + */ #[OA\Get( path: '/albums', description: 'Get all albums', @@ -89,7 +99,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)', @@ -121,7 +134,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', @@ -164,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/Handler/ArtistHandler.php b/src/Application/Handler/ArtistHandler.php similarity index 88% rename from src/Handler/ArtistHandler.php rename to src/Application/Handler/ArtistHandler.php index 66b9039..ccfbb70 100644 --- a/src/Handler/ArtistHandler.php +++ b/src/Application/Handler/ArtistHandler.php @@ -1,17 +1,16 @@ getAttribute('id'); @@ -33,6 +40,9 @@ public function handle(ServerRequestInterface $request): ResponseInterface }; } + /** + * @throws ProblemDetailsException + */ #[OA\Get( path: '/artists', description: 'Get all artists', @@ -89,7 +99,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', @@ -118,7 +131,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', @@ -158,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/Handler/HomeHandler.php b/src/Application/Handler/HomeHandler.php similarity index 51% rename from src/Handler/HomeHandler.php rename to src/Application/Handler/HomeHandler.php index 8a0d98d..f8866f6 100644 --- a/src/Handler/HomeHandler.php +++ b/src/Application/Handler/HomeHandler.php @@ -1,32 +1,22 @@ engine->assign([ diff --git a/src/Handler/OpenApiHandler.php b/src/Application/Handler/OpenApiHandler.php similarity index 66% rename from src/Handler/OpenApiHandler.php rename to src/Application/Handler/OpenApiHandler.php index 32df909..4c391e2 100644 --- a/src/Handler/OpenApiHandler.php +++ b/src/Application/Handler/OpenApiHandler.php @@ -1,10 +1,12 @@ 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(), @@ -30,9 +37,9 @@ public function handle(ServerRequestInterface $request): ResponseInterface $stream_factory = new StreamFactory(); return new Response( - $stream_factory->createStream($definition), 200, - ['Content-Type' => 'text/'.$format] + $stream_factory->createStream($definition), + ['Content-Type' => ['text/'.$format]] ); } } diff --git a/src/Handler/RedocHandler.php b/src/Application/Handler/RedocHandler.php similarity index 78% rename from src/Handler/RedocHandler.php rename to src/Application/Handler/RedocHandler.php index be3ac67..1385ab1 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/Service/AlbumService.php b/src/Domain/AlbumService.php similarity index 78% rename from src/Service/AlbumService.php rename to src/Domain/AlbumService.php index f3bc17d..3f23269 100644 --- a/src/Service/AlbumService.php +++ b/src/Domain/AlbumService.php @@ -1,13 +1,11 @@ AlbumMapper::toAlbum($album), - $this->repository->all() - ); + return $this->repository->all(); } + /** + * @throws ProblemDetailsException + */ 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]); @@ -44,10 +43,24 @@ public function find(int $id): ?Album )); } - return AlbumMapper::toAlbum($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; } - /** @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'])) { @@ -76,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'])) { @@ -112,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/Service/ArtistService.php b/src/Domain/ArtistService.php similarity index 77% rename from src/Service/ArtistService.php rename to src/Domain/ArtistService.php index c6871ae..d65e5a4 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(); } + /** + * @throws ProblemDetailsException + */ 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]); @@ -46,10 +43,24 @@ public function find(int $id): ?Artist )); } - return ArtistMapper::toArtist($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; } - /** @param array{name: string} $data */ + /** + * @param array{name: string} $data + * @throws ProblemDetailsException + */ public function create(array $data): Artist { if (!isset($data['name'])) { @@ -78,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'])) { @@ -109,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 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 @@ +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/src/Repository/AlbumRepository.php b/src/Repository/AlbumRepository.php deleted file mode 100644 index d4aac8d..0000000 --- a/src/Repository/AlbumRepository.php +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - 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..89769ce 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} +
+ + 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/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 index 879562e..b239048 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -1,30 +1,5 @@ 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'; +pest()->extend(Tests\TestCase::class)->in('Feature'); - $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); +/* +|-------------------------------------------------------------------------- +| 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. +| +*/ - // 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); +expect()->extend('toBeOne', function () { + return $this->toBe(1); +}); - // 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'); +/* +|-------------------------------------------------------------------------- +| 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. +| +*/ -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'); +function something() +{ + // .. +} diff --git a/tests/TestCase.php b/tests/TestCase.php index fdb4604..cfb05b6 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -1,6 +1,6 @@ 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/ExampleTest.php b/tests/Unit/ExampleTest.php new file mode 100644 index 0000000..d92d58a --- /dev/null +++ b/tests/Unit/ExampleTest.php @@ -0,0 +1,7 @@ +toBeTrue(); +}); 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' - ); -});