Merge branch '2.0' into api-platform-3

* 2.0:
  [API] Fix translation.name filters
  [Core] Fix #16603 SyliusCollector do not display API tag properly
  [Documentation] Update the dates on the release cycle page
This commit is contained in:
Grzegorz Sadowski 2024-08-12 11:32:53 +02:00
commit df6f0c867f
No known key found for this signature in database
GPG key ID: EF87FF4E6E3BD364
6 changed files with 329 additions and 30 deletions

View file

@ -59,9 +59,9 @@ Planned releases
+---------+----------------------+------------------------+--------------------+ +---------+----------------------+------------------------+--------------------+
| Version | Development starts | Stabilization starts | Release date | | 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 Supported versions
@ -70,7 +70,7 @@ Supported versions
+---------+--------------------+--------------------+--------------------+---------------------+ +---------+--------------------+--------------------+--------------------+---------------------+
| Version | Release date | End of maintenance | End of life | Status | | 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 | | 1.12 | Oct 31, 2022 | Jun 30, 2024 | Dec 31, 2024 | Security fixes only |
+---------+--------------------+--------------------+--------------------+---------------------+ +---------+--------------------+--------------------+--------------------+---------------------+

View file

@ -0,0 +1,74 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\ApiBundle\Doctrine\ORM\QueryExtension\Common;
use ApiPlatform\Doctrine\Orm\Extension\QueryCollectionExtensionInterface;
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use ApiPlatform\Metadata\Operation;
use Doctrine\ORM\Query\Expr\Join;
use Doctrine\ORM\QueryBuilder;
use Sylius\Component\Resource\Model\TranslatableInterface;
final class TranslationOrderLocaleExtension implements QueryCollectionExtensionInterface
{
/** @param array<string, mixed> $context */
public function applyToCollection(
QueryBuilder $queryBuilder,
QueryNameGeneratorInterface $queryNameGenerator,
string $resourceClass,
?Operation $operation = 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<string, mixed> $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
;
}
}

View file

@ -13,7 +13,7 @@ declare(strict_types=1);
namespace Sylius\Bundle\ApiBundle\Filter\Doctrine; namespace Sylius\Bundle\ApiBundle\Filter\Doctrine;
use ApiPlatform\Doctrine\Common\Filter\OrderFilterInterface; use ApiPlatform\Core\Bridge\Doctrine\Common\Filter\OrderFilterInterface;
use ApiPlatform\Doctrine\Orm\Filter\AbstractFilter; use ApiPlatform\Doctrine\Orm\Filter\AbstractFilter;
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use ApiPlatform\Metadata\Operation; use ApiPlatform\Metadata\Operation;
@ -23,41 +23,21 @@ final class TranslationOrderNameAndLocaleFilter extends AbstractFilter
{ {
protected function filterProperty( protected function filterProperty(
string $property, string $property,
$value, $value,
QueryBuilder $queryBuilder, QueryBuilder $queryBuilder,
QueryNameGeneratorInterface $queryNameGenerator, QueryNameGeneratorInterface $queryNameGenerator,
string $resourceClass, string $resourceClass,
?Operation $operation = null, ?Operation $operation = null,
array $context = [], array $context = [],
): void { ): void {
if ('order' === $property) { if ('order' === $property && isset($value['translation.name'])) {
if (!isset($value['translation.name'])) { /** @phpstan-ignore-next-line */
return; if (!$queryBuilder->getEntityManager()->getClassMetadata($resourceClass)->hasAssociation('translations')) {
}
$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'])
;
return; return;
} }
$queryBuilder $queryBuilder
->orderBy('translation.name', $direction) ->orderBy('translation.name', $value['translation.name'])
; ;
} }
} }
@ -77,6 +57,7 @@ final class TranslationOrderNameAndLocaleFilter extends AbstractFilter
], ],
], ],
], ],
/* @see \Sylius\Bundle\ApiBundle\Doctrine\ORM\QueryExtension\Common\TranslationOrderLocaleExtension */
'localeCode for order[translation.name]' => [ 'localeCode for order[translation.name]' => [
'type' => 'string', 'type' => 'string',
'required' => false, 'required' => false,

View file

@ -21,6 +21,10 @@
<tag name="api_platform.doctrine.orm.query_extension.collection" /> <tag name="api_platform.doctrine.orm.query_extension.collection" />
</service> </service>
<service id="sylius_api.doctrine.orm.query_extension.common.translation_order_locale" class="Sylius\Bundle\ApiBundle\Doctrine\ORM\QueryExtension\Common\TranslationOrderLocaleExtension">
<tag name="api_platform.doctrine.orm.query_extension.collection" />
</service>
<service id="sylius_api.doctrine.orm.query_extension.shop.product_review.accepted" class="Sylius\Bundle\ApiBundle\Doctrine\ORM\QueryExtension\Shop\ProductReview\AcceptedExtension"> <service id="sylius_api.doctrine.orm.query_extension.shop.product_review.accepted" class="Sylius\Bundle\ApiBundle\Doctrine\ORM\QueryExtension\Shop\ProductReview\AcceptedExtension">
<argument type="service" id="sylius.section_resolver.uri_based_section_resolver" /> <argument type="service" id="sylius.section_resolver.uri_based_section_resolver" />
<tag name="api_platform.doctrine.orm.query_extension.collection" /> <tag name="api_platform.doctrine.orm.query_extension.collection" />

View file

@ -0,0 +1,240 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\ApiBundle\Tests\Doctrine\ORM\QueryExtension\Common;
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Query\Expr\Join;
use Doctrine\ORM\QueryBuilder;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Sylius\Bundle\ApiBundle\Doctrine\ORM\QueryExtension\Common\TranslationOrderLocaleExtension;
use Sylius\Component\Core\Model\ProductInterface;
final class TranslationOrderLocaleExtensionTest extends TestCase
{
private QueryNameGeneratorInterface&MockObject $queryNameGenerator;
private QueryBuilder&MockObject $queryBuilder;
private EntityManagerInterface&MockObject $entityManager;
private ClassMetadata&MockObject $classMetadata;
protected function setUp(): void
{
$this->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<string, mixed> $context */
private function doApplyToCollection(string $resourceClass, array $context = []): void
{
(new TranslationOrderLocaleExtension())->applyToCollection(
queryBuilder: $this->queryBuilder,
queryNameGenerator: $this->queryNameGenerator,
resourceClass: $resourceClass,
context: $context,
);
}
/** @return iterable<array<string, mixed>> */
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'],
],
]];
}
}

View file

@ -37,7 +37,7 @@ final class SyliusCollector extends DataCollector
'default_locale_code' => $defaultLocaleCode, 'default_locale_code' => $defaultLocaleCode,
'locale_code' => null, 'locale_code' => null,
'extensions' => [ 'extensions' => [
'SyliusAdminApiBundle' => ['name' => 'API', 'enabled' => false], 'SyliusApiBundle' => ['name' => 'API', 'enabled' => false],
'SyliusAdminBundle' => ['name' => 'Admin', 'enabled' => false], 'SyliusAdminBundle' => ['name' => 'Admin', 'enabled' => false],
'SyliusShopBundle' => ['name' => 'Shop', 'enabled' => false], 'SyliusShopBundle' => ['name' => 'Shop', 'enabled' => false],
], ],