From 0f8af860c2a2a97fb0eb5c5736ab32054bc88baa Mon Sep 17 00:00:00 2001 From: Grzegorz Sadowski Date: Wed, 7 Aug 2024 15:59:03 +0200 Subject: [PATCH 1/3] [Documentation] Update the dates on the release cycle page --- docs/book/organization/release-cycle.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/book/organization/release-cycle.rst b/docs/book/organization/release-cycle.rst index 02560eed16..5bc0a9764d 100644 --- a/docs/book/organization/release-cycle.rst +++ b/docs/book/organization/release-cycle.rst @@ -59,9 +59,9 @@ Planned releases +---------+----------------------+------------------------+--------------------+ | Version | Development starts | Stabilization starts | Release date | +=========+======================+========================+====================+ -| 2.0 | Sep 15, 2023 | Q2 2024 | Q3 2024 | +| 2.0 | Sep 15, 2023 | Q3 2024 | Q4 2024 | +---------+----------------------+------------------------+--------------------+ -| 1.14 | Apr 26, 2024 | Q2 2024 | Q3 2024 | +| 1.14 | Apr 26, 2024 | Q3 2024 | Q4 2024 | +---------+----------------------+------------------------+--------------------+ Supported versions @@ -70,7 +70,7 @@ Supported versions +---------+--------------------+--------------------+--------------------+---------------------+ | Version | Release date | End of maintenance | End of life | Status | +=========+====================+====================+====================+=====================+ -| 1.13 | Apr 23, 2024 | Oct 23, 2024 | Apr 23, 2025 | Fully supported | +| 1.13 | Apr 23, 2024 | Jan 23, 2025 | Apr 23, 2025 | Fully supported | +---------+--------------------+--------------------+--------------------+---------------------+ | 1.12 | Oct 31, 2022 | Jun 30, 2024 | Dec 31, 2024 | Security fixes only | +---------+--------------------+--------------------+--------------------+---------------------+ From f929c6c5c3ccf8ecb82d262fbf1e1d063cfa8212 Mon Sep 17 00:00:00 2001 From: Aymeric LAFONTAINE Date: Thu, 18 Jul 2024 12:05:15 +0200 Subject: [PATCH 2/3] [Core] Fix #16603 SyliusCollector do not display API tag properly --- src/Sylius/Bundle/CoreBundle/Collector/SyliusCollector.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Sylius/Bundle/CoreBundle/Collector/SyliusCollector.php b/src/Sylius/Bundle/CoreBundle/Collector/SyliusCollector.php index 007eee7b33..f845eb1214 100644 --- a/src/Sylius/Bundle/CoreBundle/Collector/SyliusCollector.php +++ b/src/Sylius/Bundle/CoreBundle/Collector/SyliusCollector.php @@ -37,7 +37,7 @@ final class SyliusCollector extends DataCollector 'default_locale_code' => $defaultLocaleCode, 'locale_code' => null, 'extensions' => [ - 'SyliusAdminApiBundle' => ['name' => 'API', 'enabled' => false], + 'SyliusApiBundle' => ['name' => 'API', 'enabled' => false], 'SyliusAdminBundle' => ['name' => 'Admin', 'enabled' => false], 'SyliusShopBundle' => ['name' => 'Shop', 'enabled' => false], ], From ff625108260ad0ee3f3c9af3519d8cccbabb82ff Mon Sep 17 00:00:00 2001 From: Jan Goralski Date: Fri, 9 Aug 2024 14:25:34 +0200 Subject: [PATCH 3/3] [API] Fix translation.name filters --- .../TranslationOrderLocaleExtension.php | 74 ++++++ .../TranslationOrderNameAndLocaleFilter.php | 34 +-- .../Resources/config/services/extensions.xml | 4 + .../TranslationOrderLocaleExtensionTest.php | 240 ++++++++++++++++++ 4 files changed, 326 insertions(+), 26 deletions(-) create mode 100644 src/Sylius/Bundle/ApiBundle/Doctrine/QueryCollectionExtension/TranslationOrderLocaleExtension.php create mode 100644 src/Sylius/Bundle/ApiBundle/Tests/Doctrine/QueryCollectionExtension/TranslationOrderLocaleExtensionTest.php diff --git a/src/Sylius/Bundle/ApiBundle/Doctrine/QueryCollectionExtension/TranslationOrderLocaleExtension.php b/src/Sylius/Bundle/ApiBundle/Doctrine/QueryCollectionExtension/TranslationOrderLocaleExtension.php new file mode 100644 index 0000000000..74131eec77 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Doctrine/QueryCollectionExtension/TranslationOrderLocaleExtension.php @@ -0,0 +1,74 @@ + $context */ + public function applyToCollection( + QueryBuilder $queryBuilder, + LegacyQueryNameGeneratorInterface|QueryNameGeneratorInterface $queryNameGenerator, + string $resourceClass, + string $operationName = null, + array $context = [], + ): void { + if (!is_a($resourceClass, TranslatableInterface::class, true)) { + return; + } + /* @see \Sylius\Bundle\ApiBundle\Filter\Doctrine\TranslationOrderNameAndLocaleFilter */ + if (!isset($context['filters']['order']['translation.name'])) { + return; + } + if (!$queryBuilder->getEntityManager()->getClassMetadata($resourceClass)->hasAssociation('translations')) { + return; + } + + $localeCode = $this->resolveContextLocaleCode($context); + if (empty($localeCode)) { + return; + } + + $rootAlias = $queryBuilder->getRootAliases()[0]; + $localeCodeParameterName = $queryNameGenerator->generateParameterName('localeCode'); + + $queryBuilder + ->addSelect('translation') + ->leftJoin( + sprintf('%s.translations', $rootAlias), + 'translation', + Join::WITH, + sprintf('translation.locale = :%s', $localeCodeParameterName), + ) + ->setParameter($localeCodeParameterName, $localeCode) + ; + } + + /** @param array $context */ + private function resolveContextLocaleCode(array $context): ?string + { + return + $context['filters']['localeCode for order']['translation.name'] ?? + $context['filters']['order']['localeCode']['translation.name'] ?? + $context['filters']['order']['localeCode'] ?? + null + ; + } +} diff --git a/src/Sylius/Bundle/ApiBundle/Filter/Doctrine/TranslationOrderNameAndLocaleFilter.php b/src/Sylius/Bundle/ApiBundle/Filter/Doctrine/TranslationOrderNameAndLocaleFilter.php index 33b2c96cd1..6947339846 100644 --- a/src/Sylius/Bundle/ApiBundle/Filter/Doctrine/TranslationOrderNameAndLocaleFilter.php +++ b/src/Sylius/Bundle/ApiBundle/Filter/Doctrine/TranslationOrderNameAndLocaleFilter.php @@ -15,7 +15,8 @@ namespace Sylius\Bundle\ApiBundle\Filter\Doctrine; use ApiPlatform\Core\Bridge\Doctrine\Common\Filter\OrderFilterInterface; use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\AbstractContextAwareFilter; -use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface; +use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface as LegacyQueryNameGeneratorInterface; +use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; use Doctrine\ORM\QueryBuilder; final class TranslationOrderNameAndLocaleFilter extends AbstractContextAwareFilter @@ -24,38 +25,18 @@ final class TranslationOrderNameAndLocaleFilter extends AbstractContextAwareFilt string $property, $value, QueryBuilder $queryBuilder, - QueryNameGeneratorInterface $queryNameGenerator, + LegacyQueryNameGeneratorInterface|QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?string $operationName = null, ): void { - if ('order' === $property) { - if (!isset($value['translation.name'])) { - return; - } - - $direction = $value['translation.name']; - $rootAlias = $queryBuilder->getRootAliases()[0]; - - if (isset($value['localeCode'])) { - $localeParameterName = $queryNameGenerator->generateParameterName('locale'); - - $queryBuilder - ->addSelect('translation') - ->leftJoin( - sprintf('%s.translations', $rootAlias), - 'translation', - 'WITH', - sprintf('translation.locale = :%s', $localeParameterName), - ) - ->orderBy('translation.name', $direction) - ->setParameter($localeParameterName, $value['localeCode']) - ; - + if ('order' === $property && isset($value['translation.name'])) { + /** @phpstan-ignore-next-line */ + if (!$queryBuilder->getEntityManager()->getClassMetadata($resourceClass)->hasAssociation('translations')) { return; } $queryBuilder - ->orderBy('translation.name', $direction) + ->orderBy('translation.name', $value['translation.name']) ; } } @@ -75,6 +56,7 @@ final class TranslationOrderNameAndLocaleFilter extends AbstractContextAwareFilt ], ], ], + /* @see \Sylius\Bundle\ApiBundle\Doctrine\QueryCollectionExtension\TranslationOrderLocaleExtension */ 'localeCode for order[translation.name]' => [ 'type' => 'string', 'required' => false, diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/services/extensions.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/services/extensions.xml index aea90a057a..1483d48581 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/services/extensions.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/services/extensions.xml @@ -18,6 +18,10 @@ + + + + %sylius.model.shipping_method.class% diff --git a/src/Sylius/Bundle/ApiBundle/Tests/Doctrine/QueryCollectionExtension/TranslationOrderLocaleExtensionTest.php b/src/Sylius/Bundle/ApiBundle/Tests/Doctrine/QueryCollectionExtension/TranslationOrderLocaleExtensionTest.php new file mode 100644 index 0000000000..5a1be07055 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Tests/Doctrine/QueryCollectionExtension/TranslationOrderLocaleExtensionTest.php @@ -0,0 +1,240 @@ +queryNameGenerator = $this->createMock(QueryNameGeneratorInterface::class); + $this->queryBuilder = $this->createMock(QueryBuilder::class); + $this->entityManager = $this->createMock(EntityManagerInterface::class); + $this->classMetadata = $this->createMock(ClassMetadata::class); + } + + /** @test */ + public function it_does_nothing_when_resource_class_is_not_translatable(): void + { + $this->queryBuilder + ->expects($this->never()) + ->method('leftJoin') + ; + + $this->doApplyToCollection(\stdClass::class); + } + + /** @test */ + public function it_does_nothing_when_the_resource_is_not_sorted_by_translation_name(): void + { + $this->queryBuilder + ->expects($this->never()) + ->method('leftJoin') + ; + + $this->doApplyToCollection(ProductInterface::class); + } + + /** @test */ + public function it_does_nothing_when_the_resource_does_not_have_a_translations_association(): void + { + $this->queryBuilder + ->expects($this->once()) + ->method('getEntityManager') + ->willReturn($this->entityManager) + ; + + $this->entityManager + ->expects($this->once()) + ->method('getClassMetadata') + ->with(ProductInterface::class) + ->willReturn($this->classMetadata) + ; + + $this->classMetadata + ->expects($this->once()) + ->method('hasAssociation') + ->with('translations') + ->willReturn(false) + ; + + $this->queryBuilder + ->expects($this->never()) + ->method('leftJoin') + ; + + $this->doApplyToCollection(ProductInterface::class, [ + 'filters' => [ + 'order' => [ + 'translation.name' => 'test', + ], + ], + ]); + } + + /** @test */ + public function it_does_nothing_when_no_locale_code_has_been_resolved_from_filters(): void + { + $this->queryBuilder + ->expects($this->once()) + ->method('getEntityManager') + ->willReturn($this->entityManager) + ; + + $this->entityManager + ->expects($this->once()) + ->method('getClassMetadata') + ->with(ProductInterface::class) + ->willReturn($this->classMetadata) + ; + + $this->classMetadata + ->expects($this->once()) + ->method('hasAssociation') + ->with('translations') + ->willReturn(true) + ; + + $this->queryBuilder + ->expects($this->never()) + ->method('leftJoin') + ->withAnyParameters() + ; + + $this->doApplyToCollection(ProductInterface::class, [ + 'filters' => [ + 'order' => [ + 'translation.name' => 'test', + ], + ], + ]); + } + + /** + * @test + * @dataProvider getLocaleCodeContexts + */ + public function it_joins_on_a_specific_translation_when_locale_code_has_been_resolved_from_filters( + array $contextWithLocaleCode, + ): void { + $this->queryBuilder + ->expects($this->once()) + ->method('getEntityManager') + ->willReturn($this->entityManager) + ; + + $this->entityManager + ->expects($this->once()) + ->method('getClassMetadata') + ->with(ProductInterface::class) + ->willReturn($this->classMetadata) + ; + + $this->classMetadata + ->expects($this->once()) + ->method('hasAssociation') + ->with('translations') + ->willReturn(true) + ; + + $this->queryBuilder + ->expects($this->once()) + ->method('getRootAliases') + ->willReturn(['alias']) + ; + + $this->queryNameGenerator + ->method('generateParameterName') + ->with('localeCode') + ->willReturn('param') + ; + + $this->queryBuilder + ->expects($this->once()) + ->method('addSelect') + ->with('translation') + ->willReturnSelf() + ; + + $this->queryBuilder + ->expects($this->once()) + ->method('leftJoin') + ->with('alias.translations', 'translation', Join::WITH, 'translation.locale = :param') + ->willReturnSelf() + ; + + $this->queryBuilder + ->expects($this->once()) + ->method('setParameter') + ->with('param', 'en_US') + ->willReturnSelf() + ; + + $this->doApplyToCollection(ProductInterface::class, array_merge_recursive([ + 'filters' => [ + 'order' => [ + 'translation.name' => 'test', + ], + ], + ], $contextWithLocaleCode)); + } + + /** @param array $context */ + private function doApplyToCollection(string $resourceClass, array $context = []): void + { + (new TranslationOrderLocaleExtension())->applyToCollection( + queryBuilder: $this->queryBuilder, + queryNameGenerator: $this->queryNameGenerator, + resourceClass: $resourceClass, + context: $context, + ); + } + + /** @return iterable> */ + private function getLocaleCodeContexts(): iterable + { + yield 'locale code in documentation filter' => [[ + 'filters' => [ + 'localeCode for order' => ['translation.name' => 'en_US'], + ], + ]]; + yield 'locale code in localeCode translation.name filter' => [[ + 'filters' => [ + 'order' => ['localeCode' => ['translation.name' => 'en_US']], + ], + ]]; + yield 'locale code in localeCode filter' => [[ + 'filters' => [ + 'order' => ['localeCode' => 'en_US'], + ], + ]]; + } +}