diff --git a/config/app.php b/config/app.php index ea394b8e..19118f1d 100644 --- a/config/app.php +++ b/config/app.php @@ -26,6 +26,7 @@ return [ \Engelsystem\Middleware\SendResponseHandler::class, \Engelsystem\Middleware\ExceptionHandler::class, \Engelsystem\Middleware\SetLocale::class, + \Engelsystem\Middleware\ErrorHandler::class, \Engelsystem\Middleware\RouteDispatcher::class, \Engelsystem\Middleware\RequestHandler::class, ], diff --git a/src/Http/Response.php b/src/Http/Response.php index 9db6fa83..d79ab98b 100644 --- a/src/Http/Response.php +++ b/src/Http/Response.php @@ -2,6 +2,7 @@ namespace Engelsystem\Http; +use Engelsystem\Renderer\Renderer; use Psr\Http\Message\ResponseInterface; use Symfony\Component\HttpFoundation\Response as SymfonyResponse; @@ -9,6 +10,25 @@ class Response extends SymfonyResponse implements ResponseInterface { use MessageTrait; + /** @var Renderer */ + protected $view; + + /** + * @param string $content + * @param int $status + * @param array $headers + * @param Renderer $view + */ + public function __construct( + $content = '', + int $status = 200, + array $headers = array(), + Renderer $view = null + ) { + $this->view = $view; + parent::__construct($content, $status, $headers); + } + /** * Return an instance with the specified status code and, optionally, reason phrase. * @@ -72,4 +92,25 @@ class Response extends SymfonyResponse implements ResponseInterface return $new; } + + /** + * Return an instance with the rendered content. + * + * THis method retains the immutability of the message and returns + * an instance with the updated status and headers + * + * @param string $view + * @param array $data + * @param int $status + * @param string[]|string[][] $headers + * @return Response + */ + public function withView($view, $data = [], $status = 200, $headers = []) + { + if (!$this->view instanceof Renderer) { + throw new \InvalidArgumentException('Renderer not defined'); + } + + return $this->create($this->view->render($view, $data), $status, $headers); + } } diff --git a/src/Middleware/ErrorHandler.php b/src/Middleware/ErrorHandler.php new file mode 100644 index 00000000..a7c4cfe6 --- /dev/null +++ b/src/Middleware/ErrorHandler.php @@ -0,0 +1,80 @@ +loader = $loader; + } + + /** + * Handles any error messages + * + * Should be added at the beginning + * + * @param ServerRequestInterface $request + * @param RequestHandlerInterface $handler + * @return ResponseInterface + */ + public function process( + ServerRequestInterface $request, + RequestHandlerInterface $handler + ): ResponseInterface { + $response = $handler->handle($request); + + $statusCode = $response->getStatusCode(); + if ($statusCode < 400 || !$response instanceof Response) { + return $response; + } + + $view = $this->selectView($statusCode); + + return $response->withView( + $this->viewPrefix . $view, + [ + 'status' => $statusCode, + 'content' => $response->getContent(), + ], + $statusCode, + $response->getHeaders() + ); + } + + /** + * Select a view based on the given status code + * + * @param int $statusCode + * @return string + */ + protected function selectView(int $statusCode): string + { + $hundreds = intdiv($statusCode, 100); + + $viewsList = [$statusCode, $hundreds, $hundreds * 100]; + foreach ($viewsList as $view) { + if ($this->loader->exists($this->viewPrefix . $view)) { + return $view; + } + } + + return 'default'; + } +} diff --git a/src/Middleware/LegacyMiddleware.php b/src/Middleware/LegacyMiddleware.php index bf849611..78132815 100644 --- a/src/Middleware/LegacyMiddleware.php +++ b/src/Middleware/LegacyMiddleware.php @@ -83,7 +83,7 @@ class LegacyMiddleware implements MiddlewareInterface } if (empty($title) and empty($content)) { - $page = '404'; + $page = 404; $title = _('Page not found'); $content = _('This page could not be found or you don\'t have permission to view it. You probably have to sign in or register in order to gain access!'); } @@ -277,10 +277,8 @@ class LegacyMiddleware implements MiddlewareInterface $parameters['meetings'] = 1; } - $status = 200; - if ($page == '404') { - $status = 404; - $content = info($content, true); + if (!empty($page) && is_int($page)) { + return response($content, (int)$page); } return response(view('layouts/app', [ @@ -290,6 +288,6 @@ class LegacyMiddleware implements MiddlewareInterface 'content' => msg() . $content, 'header_toolbar' => header_toolbar(), 'event_info' => EventConfig_info($event_config) . '
' - ]), $status); + ]), 200); } } diff --git a/src/Renderer/RendererServiceProvider.php b/src/Renderer/RendererServiceProvider.php index 3e8d69bc..2e41837b 100644 --- a/src/Renderer/RendererServiceProvider.php +++ b/src/Renderer/RendererServiceProvider.php @@ -24,12 +24,14 @@ class RendererServiceProvider extends ServiceProvider protected function registerRenderer() { $renderer = $this->app->make(Renderer::class); + $this->app->instance(Renderer::class, $renderer); $this->app->instance('renderer', $renderer); } protected function registerHtmlEngine() { $htmlEngine = $this->app->make(HtmlEngine::class); + $this->app->instance(HtmlEngine::class, $htmlEngine); $this->app->instance('renderer.htmlEngine', $htmlEngine); $this->app->tag('renderer.htmlEngine', ['renderer.engine']); } diff --git a/templates/errors/default.twig b/templates/errors/default.twig new file mode 100644 index 00000000..a04afc4e --- /dev/null +++ b/templates/errors/default.twig @@ -0,0 +1,7 @@ +{% extends "layouts/app.twig" %} + +{% block title %}Error {{ status }}{% endblock %} + +{% block content %} +
{{ content }}
+{% endblock %} diff --git a/tests/Unit/Http/ResponseTest.php b/tests/Unit/Http/ResponseTest.php index f6c24767..d7dc37c0 100644 --- a/tests/Unit/Http/ResponseTest.php +++ b/tests/Unit/Http/ResponseTest.php @@ -3,6 +3,8 @@ namespace Engelsystem\Test\Unit\Http; use Engelsystem\Http\Response; +use Engelsystem\Renderer\Renderer; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Psr\Http\Message\ResponseInterface; use Symfony\Component\HttpFoundation\Response as SymfonyResponse; @@ -46,4 +48,37 @@ class ResponseTest extends TestCase $this->assertNotEquals($response, $newResponse); $this->assertEquals('Lorem Ipsum?', $newResponse->getContent()); } -} + + /** + * @covers \Engelsystem\Http\Response::withView + */ + public function testWithView() + { + /** @var REnderer|MockObject $renderer */ + $renderer = $this->createMock(Renderer::class); + + $renderer->expects($this->once()) + ->method('render') + ->with('foo', ['lorem' => 'ipsum']) + ->willReturn('Foo ipsum!'); + + $response = new Response('', 200, [], $renderer); + $newResponse = $response->withView('foo', ['lorem' => 'ipsum'], 505, ['test' => 'er']); + + $this->assertNotEquals($response, $newResponse); + $this->assertEquals('Foo ipsum!', $newResponse->getContent()); + $this->assertEquals(505, $newResponse->getStatusCode()); + $this->assertArraySubset(['test' => ['er']], $newResponse->getHeaders()); + } + + /** + * @covers \Engelsystem\Http\Response::withView + */ + public function testWithViewNoRenderer() + { + $this->expectException(\InvalidArgumentException::class); + + $response = new Response(); + $response->withView('foo'); + } +} \ No newline at end of file diff --git a/tests/Unit/Middleware/ErrorHandlerTest.php b/tests/Unit/Middleware/ErrorHandlerTest.php new file mode 100644 index 00000000..abf9c52f --- /dev/null +++ b/tests/Unit/Middleware/ErrorHandlerTest.php @@ -0,0 +1,88 @@ +createMock(TwigLoader::class); + /** @var ServerRequestInterface|MockObject $request */ + $request = $this->createMock(ServerRequestInterface::class); + /** @var ResponseInterface|MockObject $psrResponse */ + $psrResponse = $this->getMockForAbstractClass(ResponseInterface::class); + $returnResponseHandler = new ReturnResponseMiddlewareHandler($psrResponse); + + $psrResponse->expects($this->once()) + ->method('getStatusCode') + ->willReturn(505); + + $errorHandler = new ErrorHandler($twigLoader); + + $return = $errorHandler->process($request, $returnResponseHandler); + $this->assertEquals($psrResponse, $return, 'Plain PSR-7 Response should be passed directly'); + + /** @var Response|MockObject $response */ + $response = $this->createMock(Response::class); + + $response->expects($this->exactly(3)) + ->method('getStatusCode') + ->willReturnOnConsecutiveCalls( + 200, + 418, + 505 + ); + + $returnResponseHandler->setResponse($response); + $return = $errorHandler->process($request, $returnResponseHandler); + $this->assertEquals($response, $return, 'Only Responses >= 400 should be processed'); + + $twigLoader->expects($this->exactly(4)) + ->method('exists') + ->withConsecutive( + ['errors/418'], + ['errors/4'], + ['errors/400'], + ['errors/505'] + ) + ->willReturnOnConsecutiveCalls( + false, + false, + false, + true + ); + + $response->expects($this->exactly(2)) + ->method('getContent') + ->willReturnOnConsecutiveCalls( + 'Teapot', + 'Internal Error!' + ); + + $response->expects($this->exactly(2)) + ->method('withView') + ->withConsecutive( + ['errors/default', ['status' => 418, 'content' => 'Teapot'], 418], + ['errors/505', ['status' => 505, 'content' => 'Internal Error!'], 505] + ) + ->willReturn($response); + + $errorHandler->process($request, $returnResponseHandler); + $errorHandler->process($request, $returnResponseHandler); + } +} diff --git a/tests/Unit/Middleware/Stub/ReturnResponseMiddlewareHandler.php b/tests/Unit/Middleware/Stub/ReturnResponseMiddlewareHandler.php index 323e07b4..370187dd 100644 --- a/tests/Unit/Middleware/Stub/ReturnResponseMiddlewareHandler.php +++ b/tests/Unit/Middleware/Stub/ReturnResponseMiddlewareHandler.php @@ -27,4 +27,14 @@ class ReturnResponseMiddlewareHandler implements RequestHandlerInterface { return $this->response; } + + /** + * Set the response + * + * @param ResponseInterface $response + */ + public function setResponse(ResponseInterface $response) + { + $this->response = $response; + } } diff --git a/tests/Unit/Renderer/RendererServiceProviderTest.php b/tests/Unit/Renderer/RendererServiceProviderTest.php index 3826da7e..6cdf4363 100644 --- a/tests/Unit/Renderer/RendererServiceProviderTest.php +++ b/tests/Unit/Renderer/RendererServiceProviderTest.php @@ -37,10 +37,12 @@ class RendererServiceProviderTest extends ServiceProviderTest $htmlEngine ); - $app->expects($this->exactly(2)) + $app->expects($this->exactly(4)) ->method('instance') ->withConsecutive( + [Renderer::class, $renderer], ['renderer', $renderer], + [HtmlEngine::class, $htmlEngine], ['renderer.htmlEngine', $htmlEngine] );