Merge branch '1.13' into 1.14

* 1.13:
  [API] Fix translation.name filters
  [Documentation] Update the dates on the release cycle page
This commit is contained in:
Grzegorz Sadowski 2024-08-12 11:10:25 +02:00
commit cc099118cd
No known key found for this signature in database
GPG key ID: EF87FF4E6E3BD364
5 changed files with 329 additions and 29 deletions

View file

@ -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 |
+---------+--------------------+--------------------+--------------------+---------------------+

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\QueryCollectionExtension;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\ContextAwareQueryCollectionExtensionInterface;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface as LegacyQueryNameGeneratorInterface;
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use Doctrine\ORM\Query\Expr\Join;
use Doctrine\ORM\QueryBuilder;
use Sylius\Component\Resource\Model\TranslatableInterface;
final class TranslationOrderLocaleExtension implements ContextAwareQueryCollectionExtensionInterface
{
/** @param array<string, mixed> $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<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

@ -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,

View file

@ -18,6 +18,10 @@
<services>
<defaults public="true" />
<service id="Sylius\Bundle\ApiBundle\Doctrine\QueryCollectionExtension\TranslationOrderLocaleExtension">
<tag name="api_platform.doctrine.orm.query_extension.collection" />
</service>
<service id="Sylius\Bundle\ApiBundle\Doctrine\QueryCollectionExtension\HideArchivedShippingMethodExtension">
<argument>%sylius.model.shipping_method.class%</argument>
<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\QueryCollectionExtension;
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\QueryCollectionExtension\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'],
],
]];
}
}