From c0ae60a5bd09b494bf88cb09c945d381fc547c7c Mon Sep 17 00:00:00 2001 From: Louis Charette Date: Sun, 22 Sep 2024 19:51:53 -0400 Subject: [PATCH 1/2] Replace LocaleMiddleware with ServerRequestMiddleware --- app/src/Core.php | 4 +- app/src/I18n/SiteLocale.php | 33 ++--- app/src/I18n/SiteLocaleInterface.php | 8 -- ...leware.php => ServerRequestMiddleware.php} | 10 +- app/src/Util/RequestContainer.php | 48 +++++++ app/tests/Integration/I18n/SiteLocaleTest.php | 122 ++++++++++-------- 6 files changed, 130 insertions(+), 95 deletions(-) rename app/src/Middlewares/{LocaleMiddleware.php => ServerRequestMiddleware.php} (74%) create mode 100644 app/src/Util/RequestContainer.php diff --git a/app/src/Core.php b/app/src/Core.php index 29436369..566936e1 100644 --- a/app/src/Core.php +++ b/app/src/Core.php @@ -62,7 +62,7 @@ use UserFrosting\Sprinkle\Core\Listeners\ResourceLocatorInitiated; use UserFrosting\Sprinkle\Core\Listeners\SetRouteCaching; use UserFrosting\Sprinkle\Core\Middlewares\FilePermissionMiddleware; -use UserFrosting\Sprinkle\Core\Middlewares\LocaleMiddleware; +use UserFrosting\Sprinkle\Core\Middlewares\ServerRequestMiddleware; use UserFrosting\Sprinkle\Core\Middlewares\SessionMiddleware; use UserFrosting\Sprinkle\Core\Middlewares\URIMiddleware; use UserFrosting\Sprinkle\Core\Routes\AlertsRoutes; @@ -227,7 +227,7 @@ public function getServices(): array public function getMiddlewares(): array { return [ - LocaleMiddleware::class, + ServerRequestMiddleware::class, CsrfGuardMiddleware::class, SessionMiddleware::class, URIMiddleware::class, diff --git a/app/src/I18n/SiteLocale.php b/app/src/I18n/SiteLocale.php index 9a2eeed4..ded7e894 100644 --- a/app/src/I18n/SiteLocale.php +++ b/app/src/I18n/SiteLocale.php @@ -12,25 +12,21 @@ namespace UserFrosting\Sprinkle\Core\I18n; -use Psr\Http\Message\ServerRequestInterface; use UserFrosting\Config\Config; use UserFrosting\I18n\Locale; +use UserFrosting\Sprinkle\Core\Util\RequestContainer; /** * Helper methods for the locale system. */ class SiteLocale implements SiteLocaleInterface { - /** - * @var string|null - */ - protected ?string $browserLocale = null; - /** * @param Config $config */ public function __construct( protected Config $config, + protected RequestContainer $request, ) { } @@ -127,8 +123,6 @@ public function getDefaultLocale(string $fallback = 'en_US'): string * Returns the locale identifier (ie. en_US) to use. * * @return string Locale identifier - * - * @todo This should accept the request service as argument, or null, in which case the `getBrowserLocale` method would be skipped */ public function getLocaleIdentifier(): string { @@ -150,21 +144,14 @@ public function getLocaleIdentifier(): string */ protected function getBrowserLocale(): ?string { - return $this->browserLocale; - } + // Stop if there's no request + if (is_null($request = $this->request->getRequest())) { + return null; + } - /** - * Define the browser locale from the header present in the request. - * - * @param ServerRequestInterface $request - */ - public function defineBrowserLocale(ServerRequestInterface $request): void - { // Stop if request doesn't have the header if (!$request->hasHeader('Accept-Language')) { - $this->browserLocale = null; - - return; + return null; } // Get available locales @@ -207,9 +194,7 @@ public function defineBrowserLocale(ServerRequestInterface $request): void // if no $foundLocales, return null if (count($foundLocales) === 0) { - $this->browserLocale = null; - - return; + return null; } // Sort by preference (value) @@ -218,6 +203,6 @@ public function defineBrowserLocale(ServerRequestInterface $request): void // Return first element reset($foundLocales); - $this->browserLocale = key($foundLocales); + return key($foundLocales); } } diff --git a/app/src/I18n/SiteLocaleInterface.php b/app/src/I18n/SiteLocaleInterface.php index 6f78ced6..c25e805b 100644 --- a/app/src/I18n/SiteLocaleInterface.php +++ b/app/src/I18n/SiteLocaleInterface.php @@ -12,7 +12,6 @@ namespace UserFrosting\Sprinkle\Core\I18n; -use Psr\Http\Message\ServerRequestInterface; use UserFrosting\Config\Config; use UserFrosting\I18n\Locale; @@ -70,11 +69,4 @@ public function getDefaultLocale(): string; * @todo This should accept the request service as argument, or null, in which case the `getBrowserLocale` method would be skipped */ public function getLocaleIdentifier(): string; - - /** - * Define the browser locale from the header present in the request. - * - * @param ServerRequestInterface $request - */ - public function defineBrowserLocale(ServerRequestInterface $request): void; } diff --git a/app/src/Middlewares/LocaleMiddleware.php b/app/src/Middlewares/ServerRequestMiddleware.php similarity index 74% rename from app/src/Middlewares/LocaleMiddleware.php rename to app/src/Middlewares/ServerRequestMiddleware.php index 58f2c1b3..ef54a36a 100644 --- a/app/src/Middlewares/LocaleMiddleware.php +++ b/app/src/Middlewares/ServerRequestMiddleware.php @@ -16,15 +16,15 @@ use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; -use UserFrosting\Sprinkle\Core\I18n\SiteLocaleInterface; +use UserFrosting\Sprinkle\Core\Util\RequestContainer; -class LocaleMiddleware implements MiddlewareInterface +class ServerRequestMiddleware implements MiddlewareInterface { /** - * @param SiteLocaleInterface $siteLocale Inject SiteLocale service + * @param RequestContainer $requestContainer */ public function __construct( - protected SiteLocaleInterface $siteLocale, + protected RequestContainer $requestContainer, ) { } @@ -33,7 +33,7 @@ public function __construct( */ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { - $this->siteLocale->defineBrowserLocale($request); + $this->requestContainer->setRequest($request); return $handler->handle($request); } diff --git a/app/src/Util/RequestContainer.php b/app/src/Util/RequestContainer.php new file mode 100644 index 00000000..3b0adb66 --- /dev/null +++ b/app/src/Util/RequestContainer.php @@ -0,0 +1,48 @@ +request; + } + + /** + * Store the server request. + * + * @param ServerRequestInterface $request + */ + public function setRequest(ServerRequestInterface $request): void + { + $this->request = $request; + } +} diff --git a/app/tests/Integration/I18n/SiteLocaleTest.php b/app/tests/Integration/I18n/SiteLocaleTest.php index aa2cc8e1..4a5ea767 100644 --- a/app/tests/Integration/I18n/SiteLocaleTest.php +++ b/app/tests/Integration/I18n/SiteLocaleTest.php @@ -21,6 +21,7 @@ use UserFrosting\Sprinkle\Core\I18n\SiteLocale; use UserFrosting\Sprinkle\Core\I18n\SiteLocaleInterface; use UserFrosting\Sprinkle\Core\Tests\CoreTestCase as TestCase; +use UserFrosting\Sprinkle\Core\Util\RequestContainer; use UserFrosting\UniformResourceLocator\ResourceLocator; use UserFrosting\UniformResourceLocator\ResourceStream; @@ -51,17 +52,15 @@ public function setUp(): void { parent::setUp(); - // Set alias - $this->config = $this->ci->get(Config::class); - $this->locale = $this->ci->get(SiteLocale::class); - // Set test config + $this->config = $this->ci->get(Config::class); $this->config->set('site.locales.available', $this->testLocale); } public function testService(): void { - $this->assertInstanceOf(SiteLocale::class, $this->locale); // @phpstan-ignore-line + $locale = $this->ci->get(SiteLocale::class); + $this->assertInstanceOf(SiteLocale::class, $locale); // @phpstan-ignore-line } public function testFakeConfig(): void @@ -75,10 +74,11 @@ public function testFakeConfig(): void */ public function testGetAvailableIdentifiers(): void { + $locale = $this->ci->get(SiteLocale::class); $this->assertSame([ 'fr_FR', 'en_US', - ], $this->locale->getAvailableIdentifiers()); + ], $locale->getAvailableIdentifiers()); } /** @@ -87,7 +87,8 @@ public function testGetAvailableIdentifiers(): void */ public function testGetAvailable(): void { - $locales = $this->locale->getAvailable(); + $locale = $this->ci->get(SiteLocale::class); + $locales = $locale->getAvailable(); $this->assertCount(2, $locales); $this->assertSame('fr_FR', $locales[0]->getIdentifier()); @@ -98,6 +99,8 @@ public function testGetAvailable(): void */ public function testGetAvailableOptions(): void { + $locale = $this->ci->get(SiteLocale::class); + // Implement fake locale file location & locator $locator = new ResourceLocator(__DIR__); $stream = new ResourceStream('locale', 'data', true); @@ -108,7 +111,7 @@ public function testGetAvailableOptions(): void 'en_US' => 'English', 'fr_FR' => 'Tomato', // Just to be sure the fake locale are loaded ;) ]; - $options = $this->locale->getAvailableOptions(); + $options = $locale->getAvailableOptions(); $this->assertSame($expected, $options); } @@ -117,9 +120,10 @@ public function testGetAvailableOptions(): void */ public function testIsAvailable(): void { - $this->assertFalse($this->locale->isAvailable('ZZ_zz')); - $this->assertFalse($this->locale->isAvailable('es_ES')); - $this->assertTrue($this->locale->isAvailable('en_US')); + $locale = $this->ci->get(SiteLocale::class); + $this->assertFalse($locale->isAvailable('ZZ_zz')); + $this->assertFalse($locale->isAvailable('es_ES')); + $this->assertTrue($locale->isAvailable('en_US')); } /** @@ -127,8 +131,9 @@ public function testIsAvailable(): void */ public function testGetLocaleIdentifier(): void { + $locale = $this->ci->get(SiteLocale::class); $this->config->set('site.locales.default', 'fr_FR'); - $this->assertSame('fr_FR', $this->locale->getLocaleIdentifier()); + $this->assertSame('fr_FR', $locale->getLocaleIdentifier()); } /** @@ -137,8 +142,10 @@ public function testGetLocaleIdentifier(): void */ public function testGetLocaleIdentifierAndTranslator(): void { + $locale = $this->ci->get(SiteLocale::class); + $this->config->set('site.locales.default', 'fr_FR'); - $this->assertSame('fr_FR', $this->locale->getLocaleIdentifier()); + $this->assertSame('fr_FR', $locale->getLocaleIdentifier()); $translator = $this->ci->get(Translator::class); $this->assertInstanceOf(Translator::class, $translator); @@ -150,8 +157,9 @@ public function testGetLocaleIdentifierAndTranslator(): void */ public function testGetLocaleIdentifierWithDefaultIdentifier(): void { + $locale = $this->ci->get(SiteLocale::class); $this->config->set('site.locales.default', ''); - $this->assertSame('en_US', $this->locale->getLocaleIdentifier()); + $this->assertSame('en_US', $locale->getLocaleIdentifier()); } /** @@ -159,8 +167,9 @@ public function testGetLocaleIdentifierWithDefaultIdentifier(): void */ public function testGetLocaleIdentifierWithCommaSeparatedString(): void { + $locale = $this->ci->get(SiteLocale::class); $this->config->set('site.locales.default', 'fr_FR, en_US'); - $this->assertSame('fr_FR, en_US', $this->locale->getLocaleIdentifier()); + $this->assertSame('fr_FR, en_US', $locale->getLocaleIdentifier()); } /** @@ -168,27 +177,26 @@ public function testGetLocaleIdentifierWithCommaSeparatedString(): void */ public function testGetLocaleIdentifierWithCommaSeparatedStringReverseOrder(): void { + $locale = $this->ci->get(SiteLocale::class); $this->config->set('site.locales.default', 'en_US,fr_FR'); - $this->assertSame('en_US,fr_FR', $this->locale->getLocaleIdentifier()); + $this->assertSame('en_US,fr_FR', $locale->getLocaleIdentifier()); } public function testGetLocaleIdentifierWithBrowserAndNoHeader(): void { - // Define mock + // Define request mock $request = m::mock(\Psr\Http\Message\ServerRequestInterface::class); $request->shouldReceive('hasHeader')->with('Accept-Language')->once()->andReturn(false); + $requestContainer = new RequestContainer(); + $requestContainer->setRequest($request); + $this->ci->set(RequestContainer::class, $requestContainer); // Define default locale $this->config['site.locales.default'] = 'fr_FR'; - // Define browser locale - $this->locale->defineBrowserLocale($request); - - // Get locale - $locale = $this->locale->getLocaleIdentifier(); - // Assertions - $this->assertSame('fr_FR', $locale); + $locale = $this->ci->get(SiteLocale::class); + $this->assertSame('fr_FR', $locale->getLocaleIdentifier()); } public function testGetLocaleIdentifierWithBrowserAndComplexLocale(): void @@ -197,19 +205,18 @@ public function testGetLocaleIdentifierWithBrowserAndComplexLocale(): void $request = m::mock(\Psr\Http\Message\ServerRequestInterface::class); $request->shouldReceive('hasHeader')->with('Accept-Language')->once()->andReturn(true); $request->shouldReceive('getHeaderLine')->with('Accept-Language')->once()->andReturn('en-US, en;q=0.9, fr;q=0.8, de;q=0.7, *;q=0.5'); + $requestContainer = new RequestContainer(); + $requestContainer->setRequest($request); + $this->ci->set(RequestContainer::class, $requestContainer); // Define default locale $this->config['site.locales.default'] = 'fr_FR'; - // Define browser locale - $this->locale->defineBrowserLocale($request); - - // Get locale - $locale = $this->locale->getLocaleIdentifier(); - // Assertions - $this->assertSame('en_US', $locale); - $this->assertTrue($this->locale->isAvailable($locale)); + $locale = $this->ci->get(SiteLocale::class); + $identifier = $locale->getLocaleIdentifier(); + $this->assertSame('en_US', $identifier); + $this->assertTrue($locale->isAvailable($identifier)); } public function testGetLocaleIdentifierWithBrowserAndComplexLocaleInLowerCase(): void @@ -218,19 +225,18 @@ public function testGetLocaleIdentifierWithBrowserAndComplexLocaleInLowerCase(): $request = m::mock(\Psr\Http\Message\ServerRequestInterface::class); $request->shouldReceive('hasHeader')->with('Accept-Language')->once()->andReturn(true); $request->shouldReceive('getHeaderLine')->with('Accept-Language')->once()->andReturn('en-us, en;q=0.9, fr;q=0.8, de;q=0.7, *;q=0.5'); + $requestContainer = new RequestContainer(); + $requestContainer->setRequest($request); + $this->ci->set(RequestContainer::class, $requestContainer); // Define default locale $this->config['site.locales.default'] = 'fr_FR'; - // Define browser locale - $this->locale->defineBrowserLocale($request); - - // Get locale - $locale = $this->locale->getLocaleIdentifier(); - // Assertions - $this->assertSame('en_US', $locale); - $this->assertTrue($this->locale->isAvailable($locale)); + $locale = $this->ci->get(SiteLocale::class); + $identifier = $locale->getLocaleIdentifier(); + $this->assertSame('en_US', $identifier); + $this->assertTrue($locale->isAvailable($identifier)); } public function testGetLocaleIdentifierWithBrowserAndMultipleLocale(): void @@ -239,15 +245,16 @@ public function testGetLocaleIdentifierWithBrowserAndMultipleLocale(): void $request = m::mock(\Psr\Http\Message\ServerRequestInterface::class); $request->shouldReceive('hasHeader')->with('Accept-Language')->once()->andReturn(true); $request->shouldReceive('getHeaderLine')->with('Accept-Language')->once()->andReturn('es-ES, fr-FR;q=0.7, fr-CA;q=0.9, en-US;q=0.8, *;q=0.5'); + $requestContainer = new RequestContainer(); + $requestContainer->setRequest($request); + $this->ci->set(RequestContainer::class, $requestContainer); // Define default locale $this->config['site.locales.default'] = 'fr_FR'; - // Define browser locale - $this->locale->defineBrowserLocale($request); - // Assertions - $this->assertSame('en_US', $this->locale->getLocaleIdentifier()); + $locale = $this->ci->get(SiteLocale::class); + $this->assertSame('en_US', $locale->getLocaleIdentifier()); } public function testGetLocaleIdentifierWithBrowserAndLocaleInSecondPlace(): void @@ -256,15 +263,16 @@ public function testGetLocaleIdentifierWithBrowserAndLocaleInSecondPlace(): void $request = m::mock(\Psr\Http\Message\ServerRequestInterface::class); $request->shouldReceive('hasHeader')->with('Accept-Language')->once()->andReturn(true); $request->shouldReceive('getHeaderLine')->with('Accept-Language')->once()->andReturn('zz-ZZ, en-US;q=0.9, fr;q=0.8, de;q=0.7, *;q=0.5'); + $requestContainer = new RequestContainer(); + $requestContainer->setRequest($request); + $this->ci->set(RequestContainer::class, $requestContainer); // Define default locale $this->config['site.locales.default'] = 'fr_FR'; - // Define browser locale - $this->locale->defineBrowserLocale($request); - // Assertions - $this->assertSame('en_US', $this->locale->getLocaleIdentifier()); + $locale = $this->ci->get(SiteLocale::class); + $this->assertSame('en_US', $locale->getLocaleIdentifier()); } public function testGetLocaleIdentifierWithBrowserAndInvalidLocale(): void @@ -273,15 +281,16 @@ public function testGetLocaleIdentifierWithBrowserAndInvalidLocale(): void $request = m::mock(\Psr\Http\Message\ServerRequestInterface::class); $request->shouldReceive('hasHeader')->with('Accept-Language')->once()->andReturn(true); $request->shouldReceive('getHeaderLine')->with('Accept-Language')->once()->andReturn('fo,oba;;;r,'); + $requestContainer = new RequestContainer(); + $requestContainer->setRequest($request); + $this->ci->set(RequestContainer::class, $requestContainer); // Define default locale $this->config['site.locales.default'] = 'fr_FR'; - // Define browser locale - $this->locale->defineBrowserLocale($request); - // Assertions - $this->assertSame('fr_FR', $this->locale->getLocaleIdentifier()); + $locale = $this->ci->get(SiteLocale::class); + $this->assertSame('fr_FR', $locale->getLocaleIdentifier()); } public function testGetLocaleIdentifierWithBrowserAndNonExistingLocale(): void @@ -290,15 +299,16 @@ public function testGetLocaleIdentifierWithBrowserAndNonExistingLocale(): void $request = m::mock(\Psr\Http\Message\ServerRequestInterface::class); $request->shouldReceive('hasHeader')->with('Accept-Language')->once()->andReturn(true); $request->shouldReceive('getHeaderLine')->with('Accept-Language')->once()->andReturn('fr-ca'); + $requestContainer = new RequestContainer(); + $requestContainer->setRequest($request); + $this->ci->set(RequestContainer::class, $requestContainer); // Define default locale $this->config['site.locales.default'] = 'fr_FR'; - // Define browser locale - $this->locale->defineBrowserLocale($request); - // Assertions - $this->assertSame('fr_FR', $this->locale->getLocaleIdentifier()); + $locale = $this->ci->get(SiteLocale::class); + $this->assertSame('fr_FR', $locale->getLocaleIdentifier()); } public function testMiddlewareWithSimulatedBrowserLocaleControl(): void From e8404296154d225ead1bc2295143c949a29f495c Mon Sep 17 00:00:00 2001 From: Louis Charette Date: Sun, 22 Sep 2024 20:22:53 -0400 Subject: [PATCH 2/2] Add changelog --- CHANGELOG.md | 3 +++ app/src/I18n/SiteLocale.php | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c92ac87d..c5fcc971 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [5.1.2](https://github.com/userfrosting/sprinkle-core/compare/5.1.1...5.1.2) +- Replace `LocaleMiddleware` with `ServerRequestMiddleware`. A new class, `RequestContainer`, can be injected or retrieved from the container to get the server request. It will be `null` if the request is not defined (called before it is injected into the container by Middleware or if there's no request, e.g., a Bakery command). + ## [5.1.2](https://github.com/userfrosting/sprinkle-core/compare/5.1.1...5.1.2) - Fix [#1264](https://github.com/userfrosting/UserFrosting/issues/1264) - The browser locale is not applied automatically diff --git a/app/src/I18n/SiteLocale.php b/app/src/I18n/SiteLocale.php index ded7e894..fef682bd 100644 --- a/app/src/I18n/SiteLocale.php +++ b/app/src/I18n/SiteLocale.php @@ -22,7 +22,8 @@ class SiteLocale implements SiteLocaleInterface { /** - * @param Config $config + * @param Config $config + * @param RequestContainer $request */ public function __construct( protected Config $config,