From 19bdb846dd5ef20e071d0e9975db084816ddd152 Mon Sep 17 00:00:00 2001 From: QuickWrite Date: Fri, 2 Oct 2026 18:36:22 +0200 Subject: [PATCH 1/6] Add a barebones translation system Adding the basis for a very simple translation system --- src/Manager/TranslationManager.php | 37 ++++++++++++++++++++++++++++ src/Translations/Language.php | 8 ++++++ src/Translations/TranslationKeys.php | 7 ++++++ src/Translations/en.php | 4 +++ 4 files changed, 56 insertions(+) create mode 100644 src/Manager/TranslationManager.php create mode 100644 src/Translations/Language.php create mode 100644 src/Translations/TranslationKeys.php create mode 100644 src/Translations/en.php diff --git a/src/Manager/TranslationManager.php b/src/Manager/TranslationManager.php new file mode 100644 index 0000000..76e5022 --- /dev/null +++ b/src/Manager/TranslationManager.php @@ -0,0 +1,37 @@ + 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. + $translation = $this->translations[$key->value]; + + if (is_string($translation)) { + return $translation; + } + + return $translation(...$values); + } +} diff --git a/src/Translations/Language.php b/src/Translations/Language.php new file mode 100644 index 0000000..7d179d0 --- /dev/null +++ b/src/Translations/Language.php @@ -0,0 +1,8 @@ + Date: Fri, 9 Oct 2026 20:23:12 +0200 Subject: [PATCH 2/6] Add English translations for 404 page --- src/Http/Router.php | 3 +++ src/Template/error/404.php | 15 ++++++++++----- src/Translations/TranslationKeys.php | 4 ++++ src/Translations/en.php | 6 ++++++ 4 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/Http/Router.php b/src/Http/Router.php index 28f3301..6dba088 100644 --- a/src/Http/Router.php +++ b/src/Http/Router.php @@ -4,6 +4,8 @@ use KittyShare\Model\Dependencies; use KittyShare\Manager\ConfigManager; +use KittyShare\Manager\TranslationManager; +use KittyShare\Translations\Language; /** * The router is responsible for dispatching requests to the correct handler @@ -95,6 +97,7 @@ public function dispatch(Method $method, string $path): Response parameters: [ 'path' => $path, 'baseUrl' => ConfigManager::get()->baseUrl, + 'translationManager' => new TranslationManager(Language::EN), // TODO: Do not hardcode ], statusCode: 404, ); 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\".", ]; From bbe2fd83b8bd60b03a0cac118c6a5926fde49509 Mon Sep 17 00:00:00 2001 From: QuickWrite Date: Mon, 12 Oct 2026 16:49:22 +0200 Subject: [PATCH 3/6] Fix broken tests --- tests/Http/TemplateResponseTest.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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, ); From 9d89250f55b9fc45815b609bb44a5408e96e050d Mon Sep 17 00:00:00 2001 From: QuickWrite Date: Thu, 15 Oct 2026 20:13:55 +0200 Subject: [PATCH 4/6] Add explicit out of bounds check. Even though the array should NEVER be out of bounds, it could theoretically happen. As such a check to harden the application is being added. --- src/Manager/TranslationManager.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Manager/TranslationManager.php b/src/Manager/TranslationManager.php index 76e5022..1cfc326 100644 --- a/src/Manager/TranslationManager.php +++ b/src/Manager/TranslationManager.php @@ -4,6 +4,7 @@ use KittyShare\Translations\Language; use KittyShare\Translations\TranslationKeys; +use OutOfBoundsException; /** * Manages the translation values the application uses @@ -26,6 +27,12 @@ public function __construct(Language $language) 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)) { From a6b50c15ae29012e2584b8db920b111ec64040f7 Mon Sep 17 00:00:00 2001 From: QuickWrite Date: Sat, 17 Oct 2026 16:34:09 +0200 Subject: [PATCH 5/6] Refactor: Use a NotFoundController instead of using the router directly It seems like the router itself was sending the response when a request failed instead of the router. This is by itself fine. However this is normally the job of the controller which can do more logic at the end to ensure that the response is correct. --- src/Controller/NotFoundController.php | 26 ++++ src/Http/Router.php | 21 ++-- tests/Controller/NotFoundControllerTest.php | 124 ++++++++++++++++++++ 3 files changed, 159 insertions(+), 12 deletions(-) create mode 100644 src/Controller/NotFoundController.php create mode 100644 tests/Controller/NotFoundControllerTest.php 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 6dba088..517b3cb 100644 --- a/src/Http/Router.php +++ b/src/Http/Router.php @@ -2,10 +2,8 @@ namespace KittyShare\Http; +use KittyShare\Controller\NotFoundController; use KittyShare\Model\Dependencies; -use KittyShare\Manager\ConfigManager; -use KittyShare\Manager\TranslationManager; -use KittyShare\Translations\Language; /** * The router is responsible for dispatching requests to the correct handler @@ -92,15 +90,14 @@ public function dispatch(Method $method, string $path): Response ); } - return new TemplateResponse( - template: "error/404", - parameters: [ - 'path' => $path, - 'baseUrl' => ConfigManager::get()->baseUrl, - 'translationManager' => new TranslationManager(Language::EN), // TODO: Do not hardcode - ], - statusCode: 404, - ); + return new NotFoundController($this->dependencies) + ->handle( + new Request( + method: $method, + uri: $path, + parameters: [], + ) + ); } /** 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); + } +} From 010b90f26f6e92de514d5ebded083963a3175825 Mon Sep 17 00:00:00 2001 From: QuickWrite Date: Sat, 17 Oct 2026 16:35:11 +0200 Subject: [PATCH 6/6] Display phpunit notices Especially with CI it is important that ALL the information is being printed. as such it should be on by default. --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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" } }