From dcbd9a6803c992b076bcaa76d5222673042108bf Mon Sep 17 00:00:00 2001 From: brandonkelly Date: Thu, 21 Mar 2024 10:45:45 -0700 Subject: [PATCH] `language` element query param Resolves #14631 --- CHANGELOG-WIP.md | 2 + src/elements/db/ElementQuery.php | 31 +++++ src/elements/db/ElementQueryInterface.php | 38 +++++ src/gql/base/ElementArguments.php | 5 + src/services/Sites.php | 14 ++ tests/unit/services/SitesTest.php | 161 ++++++++++++++++++++++ 6 files changed, 251 insertions(+) create mode 100644 tests/unit/services/SitesTest.php diff --git a/CHANGELOG-WIP.md b/CHANGELOG-WIP.md index 519d52f9c9d..e769faa8994 100644 --- a/CHANGELOG-WIP.md +++ b/CHANGELOG-WIP.md @@ -5,9 +5,11 @@ - Element conditions can now include condition rules for Time fields. ([#14616](https://github.com/craftcms/cms/discussions/14616)) ### Development +- Added the `language` element query param, which filters the resulting elements based on their sites’ languages. ([#14631](https://github.com/craftcms/cms/discussions/14631)) - GraphQL responses now include full exception details, when Dev Mode is enabled or an admin is signed in with the “Show full exception views when Dev Mode is disabled” preference enabled. ([#14527](https://github.com/craftcms/cms/issues/14527)) ### Extensibility +- Added `craft\services\Sites::getSitesByLanguage()`. - Added `craft\web\ErrorHandler::exceptionAsArray()`. - Added `craft\web\ErrorHandler::showExceptionDetails()`. diff --git a/src/elements/db/ElementQuery.php b/src/elements/db/ElementQuery.php index 34252fdc7d5..9ca7e73395c 100644 --- a/src/elements/db/ElementQuery.php +++ b/src/elements/db/ElementQuery.php @@ -976,6 +976,37 @@ public function siteId($value): self return $this; } + /** + * @inheritdoc + * @uses $siteId + * @return static + */ + public function language($value): self + { + if (is_string($value)) { + $sites = Craft::$app->getSites()->getSitesByLanguage($value); + if (empty($sites)) { + throw new InvalidArgumentException("Invalid language: $value"); + } + $this->siteId = array_map(fn(Site $site) => $site->id, $sites); + } else { + if ($not = (strtolower(reset($value)) === 'not')) { + array_shift($value); + } + $this->siteId = []; + foreach (Craft::$app->getSites()->getAllSites() as $site) { + if (in_array($site->language, $value, true) === !$not) { + $this->siteId[] = $site->id; + } + } + if (empty($this->siteId)) { + throw new InvalidArgumentException('Invalid language param: [' . ($not ? 'not, ' : '') . implode(', ', $value) . ']'); + } + } + + return $this; + } + /** * @inheritdoc * @return static diff --git a/src/elements/db/ElementQueryInterface.php b/src/elements/db/ElementQueryInterface.php index 31e459b65a5..41a110bcf58 100644 --- a/src/elements/db/ElementQueryInterface.php +++ b/src/elements/db/ElementQueryInterface.php @@ -729,6 +729,44 @@ public function site(mixed $value): self; */ public function siteId(mixed $value): self; + /** + * Determines which site(s) the {elements} should be queried in, based on their language. + * + * Possible values include: + * + * | Value | Fetches {elements}… + * | - | - + * | `'en'` | from sites with a language of `en`. + * | `['en-GB', 'en-US']` | from sites with a language of `en-GB` or `en-US`. + * | `['not', 'en-GB', 'en-US']` | not in sites with a language of `en-GB` or `en-US`. + * + * ::: tip + * Elements that belong to multiple sites will be returned multiple times by default. If you + * only want unique elements to be returned, use [[unique()]] in conjunction with this. + * ::: + * + * --- + * + * ```twig + * {# Fetch {elements} from English sites #} + * {% set {elements-var} = {twig-method} + * .language('en') + * .all() %} + * ``` + * + * ```php + * // Fetch {elements} from English sites + * ${elements-var} = {php-method} + * ->language('en') + * ->all(); + * ``` + * + * @param mixed $value The property value + * @return static + * @since 4.9.0 + */ + public function language(mixed $value): self; + /** * Determines whether only elements with unique IDs should be returned by the query. * diff --git a/src/gql/base/ElementArguments.php b/src/gql/base/ElementArguments.php index 95737166062..dd25decaf63 100644 --- a/src/gql/base/ElementArguments.php +++ b/src/gql/base/ElementArguments.php @@ -135,6 +135,11 @@ public static function getArguments(): array 'type' => Type::int(), 'description' => 'Sets the offset for paginated results.', ], + 'language' => [ + 'name' => 'language', + 'type' => Type::listOf(Type::string()), + 'description' => 'Determines which site(s) the elements should be queried in, based on their language.', + ], 'limit' => [ 'name' => 'limit', 'type' => Type::int(), diff --git a/src/services/Sites.php b/src/services/Sites.php index 36709df988d..02a1697e653 100644 --- a/src/services/Sites.php +++ b/src/services/Sites.php @@ -633,6 +633,19 @@ public function getSiteByHandle(string $siteHandle, ?bool $withDisabled = null): return ArrayHelper::firstWhere($this->_allSites($withDisabled), 'handle', $siteHandle, true); } + /** + * Returns sites by their language. + * + * @param string $language + * @param bool|null $withDisabled + * @return Site[] + * @since 4.9.0 + */ + public function getSitesByLanguage(string $language, ?bool $withDisabled = null): array + { + return ArrayHelper::where($this->_allSites($withDisabled), 'language', $language, true); + } + /** * Saves a site. * @@ -1122,6 +1135,7 @@ public function refreshSites(): void { $this->_allSitesById = null; $this->_enabledSitesById = null; + $this->_editableSiteIds = null; $this->_loadAllSites(); Craft::$app->getIsMultiSite(true); } diff --git a/tests/unit/services/SitesTest.php b/tests/unit/services/SitesTest.php new file mode 100644 index 00000000000..f9ea473609b --- /dev/null +++ b/tests/unit/services/SitesTest.php @@ -0,0 +1,161 @@ + + * @since 4.9.0 + */ +class SitesTest extends TestCase +{ + /** + * @var UnitTester + */ + protected UnitTester $tester; + + /** + * @return array + */ + public function _fixtures(): array + { + return [ + 'sites' => [ + 'class' => SitesFixture::class, + ], + ]; + } + + /** + * @dataProvider getSitesByGroupIdDataProvider + * @param int $expectedCount + * @param int $groupId + */ + public function testGetSitesByGroupId(int $expectedCount, int $groupId): void + { + $sites = Craft::$app->getSites()->getSitesByGroupId($groupId); + self::assertEquals($expectedCount, count($sites)); + } + + public function getSitesByGroupIdDataProvider(): array + { + return [ + [4, 1], + [0, 9999], + ]; + } + + /** + * + */ + public function testGetTotalSites(): void + { + self::assertEquals(4, Craft::$app->getSites()->getTotalSites()); + } + + /** + * + */ + public function testGetTotalEditableSites(): void + { + $userSession = Craft::$app->getUser(); + $sitesService = Craft::$app->getSites(); + $originalUser = $userSession->getIdentity(false); + + $userSession->setIdentity(null); + $sitesService->refreshSites(); + self::assertEquals(0, Craft::$app->getSites()->getTotalEditableSites()); + + $admin = User::find()->admin()->one(); + $userSession->setIdentity($admin); + $sitesService->refreshSites(); + self::assertEquals(4, Craft::$app->getSites()->getTotalEditableSites()); + + $userSession->setIdentity($originalUser); + $sitesService->refreshSites(); + } + + /** + * @dataProvider getSiteByIdDataProvider + * @param bool $expectedNotEmpty + * @param int $id + */ + public function testGetSiteById(bool $expectedNotEmpty, int $id): void + { + $sites = Craft::$app->getSites()->getSiteById($id); + self::assertEquals($expectedNotEmpty, !empty($sites)); + } + + /** + * @return array + */ + public function getSiteByIdDataProvider(): array + { + return [ + [true, 1000], + [true, 1001], + [true, 1002], + [false, 999999], + ]; + } + + /** + * @dataProvider getSiteByHandleDataProvider + * @param bool $expectedNotEmpty + * @param string $handle + */ + public function testGetSiteByHandle(bool $expectedNotEmpty, string $handle): void + { + $sites = Craft::$app->getSites()->getSiteByHandle($handle); + self::assertEquals($expectedNotEmpty, !empty($sites)); + } + + /** + * @return array + */ + public function getSiteByHandleDataProvider(): array + { + return [ + [true, 'testSite1'], + [true, 'testSite2'], + [true, 'testSite3'], + [false, 'fakeSiteHandle'], + ]; + } + + /** + * @dataProvider getSitesByLanguageDataProvider + * @param int $expectedCount + * @param string $language + */ + public function testGetSitesByLanguage(int $expectedCount, string $language): void + { + $sites = Craft::$app->getSites()->getSitesByLanguage($language); + self::assertEquals($expectedCount, count($sites)); + } + + /** + * @return array + */ + public function getSitesByLanguageDataProvider(): array + { + return [ + [2, 'en-US'], + [2, 'nl'], + [0, 'en-us'], + [0, 'en'], + ]; + } +}