diff --git a/config/app.php b/config/app.php index bfb66cf3..970f80a5 100644 --- a/config/app.php +++ b/config/app.php @@ -26,6 +26,7 @@ return [ \Engelsystem\Middleware\RequestHandlerServiceProvider::class, \Engelsystem\Middleware\SessionHandlerServiceProvider::class, \Engelsystem\Http\Validation\ValidationServiceProvider::class, + \Engelsystem\Http\RedirectServiceProvider::class, // Additional services \Engelsystem\Helpers\VersionServiceProvider::class, diff --git a/src/Http/RedirectServiceProvider.php b/src/Http/RedirectServiceProvider.php new file mode 100644 index 00000000..70238d91 --- /dev/null +++ b/src/Http/RedirectServiceProvider.php @@ -0,0 +1,13 @@ +app->bind('redirect', Redirector::class); + } +} diff --git a/src/Http/Redirector.php b/src/Http/Redirector.php new file mode 100644 index 00000000..4149a8ec --- /dev/null +++ b/src/Http/Redirector.php @@ -0,0 +1,55 @@ +request = $request; + $this->response = $response; + } + + /** + * @param string $path + * @param int $status + * @param array $headers + * @return Response + */ + public function to(string $path, int $status = 302, array $headers = []): Response + { + return $this->response->redirectTo($path, $status, $headers); + } + + /** + * @param int $status + * @param array $headers + * @return Response + */ + public function back(int $status = 302, array $headers = []): Response + { + return $this->to($this->getPreviousUrl(), $status, $headers); + } + + /** + * @return string + */ + protected function getPreviousUrl(): string + { + if ($header = $this->request->getHeader('referer')) { + return array_pop($header); + } + + return '/'; + } +} diff --git a/src/Http/Response.php b/src/Http/Response.php index a6b4ab74..43bcb5d8 100644 --- a/src/Http/Response.php +++ b/src/Http/Response.php @@ -6,27 +6,37 @@ use Engelsystem\Renderer\Renderer; use InvalidArgumentException; use Psr\Http\Message\ResponseInterface; use Symfony\Component\HttpFoundation\Response as SymfonyResponse; +use Symfony\Component\HttpFoundation\Session\SessionInterface; class Response extends SymfonyResponse implements ResponseInterface { use MessageTrait; + /** + * @var SessionInterface + */ + protected $session; + /** @var Renderer */ protected $renderer; /** - * @param string $content - * @param int $status - * @param array $headers - * @param Renderer $renderer + * @param string $content + * @param int $status + * @param array $headers + * @param Renderer $renderer + * @param SessionInterface $session */ public function __construct( $content = '', int $status = 200, array $headers = [], - Renderer $renderer = null + Renderer $renderer = null, + SessionInterface $session = null ) { $this->renderer = $renderer; + $this->session = $session; + parent::__construct($content, $status, $headers); } @@ -155,4 +165,44 @@ class Response extends SymfonyResponse implements ResponseInterface { $this->renderer = $renderer; } + + /** + * Sets a session attribute (which is mutable) + * + * @param string $key + * @param mixed|mixed[] $value + * @return Response + */ + public function with(string $key, $value) + { + if (!$this->session instanceof SessionInterface) { + throw new InvalidArgumentException('Session not defined'); + } + + $data = $this->session->get($key); + if (is_array($data) && is_array($value)) { + $value = array_merge_recursive($data, $value); + } + + $this->session->set($key, $value); + + return $this; + } + + /** + * Sets form data to the mutable session + * + * @param array $input + * @return Response + */ + public function withInput(array $input) + { + if (!$this->session instanceof SessionInterface) { + throw new InvalidArgumentException('Session not defined'); + } + + $this->session->set('form-data', $input); + + return $this; + } } diff --git a/src/Middleware/ErrorHandler.php b/src/Middleware/ErrorHandler.php index 46e6e5a8..85f315d4 100644 --- a/src/Middleware/ErrorHandler.php +++ b/src/Middleware/ErrorHandler.php @@ -63,17 +63,11 @@ class ErrorHandler implements MiddlewareInterface } catch (HttpException $e) { $response = $this->createResponse($e->getMessage(), $e->getStatusCode(), $e->getHeaders()); } catch (ValidationException $e) { - $response = $this->createResponse('', 302, ['Location' => $this->getPreviousUrl($request)]); + $response = $this->redirectBack(); + $response->with('errors', ['validation' => $e->getValidator()->getErrors()]); if ($request instanceof Request) { - $session = $request->getSession(); - $errors = array_merge_recursive( - $session->get('errors', []), - ['validation' => $e->getValidator()->getErrors()] - ); - $session->set('errors', $errors); - - $session->set('form-data', Arr::except($request->request->all(), $this->formIgnore)); + $response->withInput(Arr::except($request->request->all(), $this->formIgnore)); } } @@ -140,15 +134,12 @@ class ErrorHandler implements MiddlewareInterface } /** - * @param ServerRequestInterface $request - * @return string + * Create a redirect back response + * + * @return Response */ - protected function getPreviousUrl(ServerRequestInterface $request) + protected function redirectBack() { - if ($header = $request->getHeader('referer')) { - return array_pop($header); - } - - return '/'; + return back(); } } diff --git a/src/helpers.php b/src/helpers.php index de140c4e..7cb17ea9 100644 --- a/src/helpers.php +++ b/src/helpers.php @@ -4,6 +4,7 @@ use Engelsystem\Application; use Engelsystem\Config\Config; use Engelsystem\Helpers\Authenticator; use Engelsystem\Helpers\Translation\Translator; +use Engelsystem\Http\Redirector; use Engelsystem\Http\Request; use Engelsystem\Http\Response; use Engelsystem\Http\UrlGeneratorInterface; @@ -28,7 +29,7 @@ function app($id = null) /** * @return Authenticator */ -function auth() +function auth(): Authenticator { return app('authenticator'); } @@ -37,11 +38,24 @@ function auth() * @param string $path * @return string */ -function base_path($path = '') +function base_path($path = ''): string { return app('path') . (empty($path) ? '' : DIRECTORY_SEPARATOR . $path); } +/** + * @param int $status + * @param array $headers + * @return Response + */ +function back($status = 302, $headers = []): Response +{ + /** @var Redirector $redirect */ + $redirect = app('redirect'); + + return $redirect->back($status, $headers); +} + /** * Get or set config values * @@ -70,11 +84,25 @@ function config($key = null, $default = null) * @param string $path * @return string */ -function config_path($path = '') +function config_path($path = ''): string { return app('path.config') . (empty($path) ? '' : DIRECTORY_SEPARATOR . $path); } +/** + * @param string $path + * @param int $status + * @param array $headers + * @return Response + */ +function redirect(string $path, $status = 302, $headers = []): Response +{ + /** @var Redirector $redirect */ + $redirect = app('redirect'); + + return $redirect->to($path, $status, $headers); +} + /** * @param string $key * @param mixed $default @@ -97,7 +125,7 @@ function request($key = null, $default = null) * @param array $headers * @return Response */ -function response($content = '', $status = 200, $headers = []) +function response($content = '', $status = 200, $headers = []): Response { /** @var Response $response */ $response = app('psr7.response'); @@ -155,7 +183,7 @@ function trans($key = null, $replace = []) * @param array $replace * @return string */ -function __($key, $replace = []) +function __($key, $replace = []): string { /** @var Translator $translator */ $translator = app('translator'); @@ -172,7 +200,7 @@ function __($key, $replace = []) * @param array $replace * @return string */ -function _e($key, $keyPlural, $number, $replace = []) +function _e($key, $keyPlural, $number, $replace = []): string { /** @var Translator $translator */ $translator = app('translator'); diff --git a/tests/Unit/HelpersTest.php b/tests/Unit/HelpersTest.php index 09362a90..710deff5 100644 --- a/tests/Unit/HelpersTest.php +++ b/tests/Unit/HelpersTest.php @@ -7,6 +7,7 @@ use Engelsystem\Config\Config; use Engelsystem\Container\Container; use Engelsystem\Helpers\Authenticator; use Engelsystem\Helpers\Translation\Translator; +use Engelsystem\Http\Redirector; use Engelsystem\Http\Request; use Engelsystem\Http\Response; use Engelsystem\Http\UrlGeneratorInterface; @@ -98,6 +99,29 @@ class HelpersTest extends TestCase $this->assertEquals(['user' => 'FooBar'], config('mail')); } + /** + * @covers \back + */ + public function testBack() + { + $response = new Response(); + /** @var Redirector|MockObject $redirect */ + $redirect = $this->createMock(Redirector::class); + $redirect->expects($this->exactly(2)) + ->method('back') + ->withConsecutive([302, []], [303, ['test' => 'ing']]) + ->willReturn($response); + + $app = new Application(); + $app->instance('redirect', $redirect); + + $return = back(); + $this->assertEquals($response, $return); + + $return = back(303, ['test' => 'ing']); + $this->assertEquals($response, $return); + } + /** * @covers \config_path */ @@ -117,6 +141,29 @@ class HelpersTest extends TestCase $this->assertEquals('/foo/conf/bar.php', config_path('bar.php')); } + /** + * @covers \redirect + */ + public function testRedirect() + { + $response = new Response(); + /** @var Redirector|MockObject $redirect */ + $redirect = $this->createMock(Redirector::class); + $redirect->expects($this->exactly(2)) + ->method('to') + ->withConsecutive(['/lorem', 302, []], ['/ipsum', 303, ['test' => 'er']]) + ->willReturn($response); + + $app = new Application(); + $app->instance('redirect', $redirect); + + $return = redirect('/lorem'); + $this->assertEquals($response, $return); + + $return = redirect('/ipsum', 303, ['test' => 'er']); + $this->assertEquals($response, $return); + } + /** * @covers \request */ diff --git a/tests/Unit/Http/RedirectServiceProviderTest.php b/tests/Unit/Http/RedirectServiceProviderTest.php new file mode 100644 index 00000000..9c2a07fc --- /dev/null +++ b/tests/Unit/Http/RedirectServiceProviderTest.php @@ -0,0 +1,23 @@ +register(); + + $this->assertTrue($app->has('redirect')); + } +} diff --git a/tests/Unit/Http/RedirectorTest.php b/tests/Unit/Http/RedirectorTest.php new file mode 100644 index 00000000..300b0180 --- /dev/null +++ b/tests/Unit/Http/RedirectorTest.php @@ -0,0 +1,53 @@ +to('/test'); + $this->assertEquals(['/test'], $return->getHeader('location')); + $this->assertEquals(302, $return->getStatusCode()); + + $return = $redirector->to('/foo', 303, ['test' => 'data']); + $this->assertEquals(['/foo'], $return->getHeader('location')); + $this->assertEquals(303, $return->getStatusCode()); + $this->assertEquals(['data'], $return->getHeader('test')); + } + + /** + * @covers \Engelsystem\Http\Redirector::back + * @covers \Engelsystem\Http\Redirector::getPreviousUrl + */ + public function testBack() + { + $request = new Request(); + $response = new Response(); + $redirector = new Redirector($request, $response); + + $return = $redirector->back(); + $this->assertEquals(['/'], $return->getHeader('location')); + $this->assertEquals(302, $return->getStatusCode()); + + $request = $request->withHeader('referer', '/old-page'); + $redirector = new Redirector($request, $response); + $return = $redirector->back(303, ['foo' => 'bar']); + $this->assertEquals(303, $return->getStatusCode()); + $this->assertEquals(['/old-page'], $return->getHeader('location')); + $this->assertEquals(['bar'], $return->getHeader('foo')); + } +} diff --git a/tests/Unit/Http/ResponseTest.php b/tests/Unit/Http/ResponseTest.php index b8e6e527..56d8410f 100644 --- a/tests/Unit/Http/ResponseTest.php +++ b/tests/Unit/Http/ResponseTest.php @@ -10,6 +10,8 @@ use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Psr\Http\Message\ResponseInterface; use Symfony\Component\HttpFoundation\Response as SymfonyResponse; +use Symfony\Component\HttpFoundation\Session\Session; +use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage; class ResponseTest extends TestCase { @@ -116,4 +118,59 @@ class ResponseTest extends TestCase $newResponse->getHeaders() ); } + + /** + * @covers \Engelsystem\Http\Response::with + */ + public function testWith() + { + $session = new Session(new MockArraySessionStorage()); + $response = new Response('', 200, [], null, $session); + + $response->with('foo', 'bar'); + $this->assertEquals('bar', $session->get('foo')); + + $response->with('lorem', ['ipsum', 'dolor' => ['foo' => 'bar']]); + $this->assertEquals(['ipsum', 'dolor' => ['foo' => 'bar']], $session->get('lorem')); + + $response->with('lorem', ['dolor' => ['test' => 'er']]); + $this->assertEquals(['ipsum', 'dolor' => ['foo' => 'bar', 'test' => 'er']], $session->get('lorem')); + } + + /** + * @covers \Engelsystem\Http\Response::with + */ + public function testWithNoSession() + { + $this->expectException(InvalidArgumentException::class); + + $response = new Response(); + $response->with('foo', 'bar'); + } + + /** + * @covers \Engelsystem\Http\Response::withInput + */ + public function testWithInput() + { + $session = new Session(new MockArraySessionStorage()); + $response = new Response('', 200, [], null, $session); + + $response->withInput(['some' => 'value']); + $this->assertEquals(['some' => 'value'], $session->get('form-data')); + + $response->withInput(['lorem' => 'ipsum']); + $this->assertEquals(['lorem' => 'ipsum'], $session->get('form-data')); + } + + /** + * @covers \Engelsystem\Http\Response::withInput + */ + public function testWithInputNoSession() + { + $this->expectException(InvalidArgumentException::class); + + $response = new Response(); + $response->withInput(['some' => 'value']); + } } diff --git a/tests/Unit/Middleware/ErrorHandlerTest.php b/tests/Unit/Middleware/ErrorHandlerTest.php index ef2f7be0..91b59507 100644 --- a/tests/Unit/Middleware/ErrorHandlerTest.php +++ b/tests/Unit/Middleware/ErrorHandlerTest.php @@ -6,6 +6,7 @@ use Engelsystem\Application; use Engelsystem\Http\Exceptions\HttpException; use Engelsystem\Http\Exceptions\ValidationException; use Engelsystem\Http\Psr7ServiceProvider; +use Engelsystem\Http\RedirectServiceProvider; use Engelsystem\Http\Request; use Engelsystem\Http\Response; use Engelsystem\Http\ResponseServiceProvider; @@ -18,6 +19,7 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; use Symfony\Component\HttpFoundation\Session\Session; +use Symfony\Component\HttpFoundation\Session\SessionInterface; use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage; use Twig\Loader\LoaderInterface as TwigLoader; @@ -155,7 +157,7 @@ class ErrorHandlerTest extends TestCase /** * @covers \Engelsystem\Middleware\ErrorHandler::process - * @covers \Engelsystem\Middleware\ErrorHandler::getPreviousUrl + * @covers \Engelsystem\Middleware\ErrorHandler::redirectBack */ public function testProcessValidationException() { @@ -185,11 +187,13 @@ class ErrorHandlerTest extends TestCase /** @var Application $app */ $app = app(); + $app->instance(Session::class, $session); + $app->bind(SessionInterface::class, Session::class); (new ResponseServiceProvider($app))->register(); (new Psr7ServiceProvider($app))->register(); + (new RedirectServiceProvider($app))->register(); $errorHandler = new ErrorHandler($twigLoader); - $return = $errorHandler->process($request, $handler); $this->assertEquals(302, $return->getStatusCode()); @@ -209,6 +213,7 @@ class ErrorHandlerTest extends TestCase ], $session->all()); $request = $request->withAddedHeader('referer', '/foo/batz'); + $app->instance(Request::class, $request); $return = $errorHandler->process($request, $handler); $this->assertEquals('/foo/batz', $return->getHeaderLine('location'));