diff --git a/composer.json b/composer.json index 53225f0..62a62ba 100644 --- a/composer.json +++ b/composer.json @@ -26,7 +26,7 @@ "@check:migrations", "@test" ], - "test": "vendor/bin/phpunit --bootstrap vendor/autoload.php tests", + "test": "vendor/bin/phpunit --bootstrap vendor/autoload.php tests --display-phpunit-notices", "start": "php -S localhost:8000 -t public" } } diff --git a/src/Controller/NotFoundController.php b/src/Controller/NotFoundController.php new file mode 100644 index 0000000..2713ebc --- /dev/null +++ b/src/Controller/NotFoundController.php @@ -0,0 +1,26 @@ + $request->getUri(), + 'baseUrl' => ConfigManager::get()->baseUrl, + 'translationManager' => new TranslationManager(Language::EN), // TODO: Do not hardcode + ], + statusCode: 404, + ); + } +} diff --git a/src/Http/Router.php b/src/Http/Router.php index 28f3301..517b3cb 100644 --- a/src/Http/Router.php +++ b/src/Http/Router.php @@ -2,8 +2,8 @@ namespace KittyShare\Http; +use KittyShare\Controller\NotFoundController; use KittyShare\Model\Dependencies; -use KittyShare\Manager\ConfigManager; /** * The router is responsible for dispatching requests to the correct handler @@ -90,14 +90,14 @@ public function dispatch(Method $method, string $path): Response ); } - return new TemplateResponse( - template: "error/404", - parameters: [ - 'path' => $path, - 'baseUrl' => ConfigManager::get()->baseUrl, - ], - statusCode: 404, - ); + return new NotFoundController($this->dependencies) + ->handle( + new Request( + method: $method, + uri: $path, + parameters: [], + ) + ); } /** diff --git a/src/Manager/TranslationManager.php b/src/Manager/TranslationManager.php new file mode 100644 index 0000000..1cfc326 --- /dev/null +++ b/src/Manager/TranslationManager.php @@ -0,0 +1,44 @@ + The key-value store for the translations + */ + private readonly array $translations; + + public function __construct(Language $language) + { + /** @var array $translations */ + $translations = require __DIR__ . '/../Translations/' . $language->value . '.php'; + + $this->translations = $translations; + } + + public function get(TranslationKeys $key, string ...$values): string + { + // If the key does not exist, something went wrong... badly. + if (!array_key_exists($key->value, $this->translations)) { + throw new OutOfBoundsException( + sprintf('Translation key "%s" does not exist.', $key->value), + ); + } + + $translation = $this->translations[$key->value]; + + if (is_string($translation)) { + return $translation; + } + + return $translation(...$values); + } +} diff --git a/src/Template/error/404.php b/src/Template/error/404.php index 0c15ada..b7131f5 100644 --- a/src/Template/error/404.php +++ b/src/Template/error/404.php @@ -1,18 +1,23 @@ get(TranslationKeys::PAGE_404_TITLE)); ?>
-

404 Page not found

-

Hey! It seems like the site that you've tried to access does not exist.

+

get(TranslationKeys::PAGE_404_TITLE) ?>

+

get(TranslationKeys::PAGE_404_SUBTITLE) ?>

-

Your requested path was "".

+

get(TranslationKeys::PAGE_404_PATHDESC, $path)) ?>

value => 'KittyShare Logo', + TranslationKeys::PAGE_404_TITLE->value => '404 Page not found', + TranslationKeys::PAGE_404_SUBTITLE->value => 'Hey! It seems like the site that you\'ve tried to access does not exist.', + TranslationKeys::PAGE_404_PATHDESC->value => fn(string $path) => "Your requested path was \"$path\".", +]; diff --git a/tests/Controller/NotFoundControllerTest.php b/tests/Controller/NotFoundControllerTest.php new file mode 100644 index 0000000..79a4be1 --- /dev/null +++ b/tests/Controller/NotFoundControllerTest.php @@ -0,0 +1,124 @@ +createStub(UserRepository::class); + $sessionRepository = $this->createStub(SessionRepository::class); + + $dependencies = new Dependencies( + userRepository: $userRepository, + setupRepository: $this->createStub(SetupRepository::class), + sessionRepository: $sessionRepository, + shareRepository: $this->createStub(ShareRepository::class), + authenticationManager: new AuthenticationManager($userRepository, $sessionRepository), + ); + + $this->controller = new NotFoundController($dependencies); + } + + public function testHandleReturnsTemplateResponse(): void + { + $request = $this->createMock(Request::class); + $request->expects(self::once()) + ->method('getUri') + ->willReturn('/missing-page'); + + $response = $this->controller->handle($request); + + self::assertInstanceOf(TemplateResponse::class, $response); + } + + public function testHandleReturns404StatusCode(): void + { + $request = $this->createStub(Request::class); + $request->method('getUri')->willReturn('/missing-page'); + + $response = $this->controller->handle($request); + + self::assertSame(404, $this->getResponseProperty($response, 'statusCode')); + } + + public function testHandleUses404Template(): void + { + $request = $this->createStub(Request::class); + $request->method('getUri')->willReturn('/missing-page'); + + $response = $this->controller->handle($request); + + self::assertSame('error/404', $this->getResponseProperty($response, 'template')); + } + + public function testHandlePassesRequestUriToTemplate(): void + { + $uri = '/some/nonexistent/page?foo=bar'; + + $request = $this->createMock(Request::class); + $request->expects(self::once()) + ->method('getUri') + ->willReturn($uri); + + $response = $this->controller->handle($request); + + $parameters = $this->getResponseProperty($response, 'parameters'); + + self::assertSame($uri, $parameters['path']); + } + + public function testHandlePassesConfiguredBaseUrlToTemplate(): void + { + $request = $this->createStub(Request::class); + $request->method('getUri')->willReturn('/missing-page'); + + $response = $this->controller->handle($request); + + $parameters = $this->getResponseProperty($response, 'parameters'); + + self::assertSame(ConfigManager::get()->baseUrl, $parameters['baseUrl']); + } + + public function testHandlePassesTranslationManagerToTemplate(): void + { + $request = $this->createStub(Request::class); + $request->method('getUri')->willReturn('/missing-page'); + + $response = $this->controller->handle($request); + + $parameters = $this->getResponseProperty($response, 'parameters'); + + self::assertInstanceOf( + TranslationManager::class, + $parameters['translationManager'], + ); + } + + /** + * Reads a private property from TemplateResponse. + */ + private function getResponseProperty( + TemplateResponse $response, + string $property, + ): mixed { + return (new ReflectionProperty(TemplateResponse::class, $property)) + ->getValue($response); + } +} diff --git a/tests/Http/TemplateResponseTest.php b/tests/Http/TemplateResponseTest.php index 05b3c4d..7b2cd88 100644 --- a/tests/Http/TemplateResponseTest.php +++ b/tests/Http/TemplateResponseTest.php @@ -2,6 +2,8 @@ use KittyShare\Http\Response; use KittyShare\Http\TemplateResponse; +use KittyShare\Manager\TranslationManager; +use KittyShare\Translations\Language; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; @@ -51,7 +53,7 @@ public function rendersTemplateWithStatusCode(): void { $response = new TemplateResponse( 'error/404', - ['path' => '/missing', 'baseUrl' => ''], + ['path' => '/missing', 'baseUrl' => '', 'translationManager' => new TranslationManager(Language::EN)], 404, ); @@ -72,7 +74,7 @@ public function escapesTemplateParameters(): void { $response = new TemplateResponse( 'error/404', - ['path' => '/a&"c"', 'baseUrl' => ''], + ['path' => '/a&"c"', 'baseUrl' => '', 'translationManager' => new TranslationManager(Language::EN)], 404, );