summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--config/app.php1
-rw-r--r--src/Http/Response.php41
-rw-r--r--src/Middleware/ErrorHandler.php80
-rw-r--r--src/Middleware/LegacyMiddleware.php10
-rw-r--r--src/Renderer/RendererServiceProvider.php2
-rw-r--r--templates/errors/default.twig7
-rw-r--r--tests/Unit/Http/ResponseTest.php37
-rw-r--r--tests/Unit/Middleware/ErrorHandlerTest.php88
-rw-r--r--tests/Unit/Middleware/Stub/ReturnResponseMiddlewareHandler.php10
-rw-r--r--tests/Unit/Renderer/RendererServiceProviderTest.php4
10 files changed, 272 insertions, 8 deletions
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 @@
+<?php
+
+namespace Engelsystem\Middleware;
+
+use Engelsystem\Http\Response;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Server\MiddlewareInterface;
+use Psr\Http\Server\RequestHandlerInterface;
+use Twig_LoaderInterface as TwigLoader;
+
+class ErrorHandler implements MiddlewareInterface
+{
+ /** @var TwigLoader */
+ protected $loader;
+
+ /** @var string */
+ protected $viewPrefix = 'errors/';
+
+ /**
+ * @param TwigLoader $loader
+ */
+ public function __construct(TwigLoader $loader)
+ {
+ $this->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) . ' <br />'
- ]), $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 %}
+ <div class="alert alert-info">{{ content }}</div>
+{% 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 @@
+<?php
+
+namespace Engelsystem\Test\Unit\Middleware;
+
+use Engelsystem\Http\Response;
+use Engelsystem\Middleware\ErrorHandler;
+use Engelsystem\Test\Unit\Middleware\Stub\ReturnResponseMiddlewareHandler;
+use PHPUnit\Framework\MockObject\MockObject;
+use PHPUnit\Framework\TestCase;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use Twig_LoaderInterface as TwigLoader;
+
+class ErrorHandlerTest extends TestCase
+{
+ /**
+ * @covers \Engelsystem\Middleware\ErrorHandler::__construct
+ * @covers \Engelsystem\Middleware\ErrorHandler::process
+ * @covers \Engelsystem\Middleware\ErrorHandler::selectView
+ */
+ public function testProcess()
+ {
+ /** @var TwigLoader|MockObject $twigLoader */
+ $twigLoader = $this->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]
);