Skip to content

Commit

Permalink
minor #15501 [API] Automatically add a default translation to transla…
Browse files Browse the repository at this point in the history
…tables (NoResponseMate)

This PR was merged into the 1.13 branch.

Discussion
----------

| Q               | A                                                            |
|-----------------|--------------------------------------------------------------|
| Branch?         | 1.13 |
| Bug fix?        | kinda                                                       |
| New feature?    | no?                                                       |
| BC breaks?      | no                                                       |
| Deprecations?   | no |
| Related tickets | -                      |
| License         | MIT                                                          |

Commits
-------
  [API] Automatically add a default translation to translatables
  [API] Adjust tests after missing translation validation kicked in
  [API] Less naive default translation checking
  • Loading branch information
jakubtobiasz committed Nov 8, 2023
2 parents 072bf87 + 9012465 commit 68e776b
Show file tree
Hide file tree
Showing 18 changed files with 376 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Feature: Seeing correct select attribute values in different locale than default
Given the store is available in "French (France)"
And I am logged in as an administrator

@ui @javascript @api
@ui @javascript @no-api
Scenario: Seeing correct attribute values in different locale than default one
When I want to create a new select product attribute
And I specify its code as "mug_material"
Expand Down
5 changes: 5 additions & 0 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -1840,6 +1840,11 @@ parameters:
count: 1
path: src/Sylius/Bundle/ApiBundle/Serializer/ShippingMethodNormalizer.php

-
message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Serializer\\\\TranslatableDenormalizer\\:\\:hasDefaultTranslation\\(\\) has parameter \\$translations with no value type specified in iterable type array\\.$#"
count: 1
path: src/Sylius/Bundle/ApiBundle/Serializer/TranslatableDenormalizer.php

-
message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Serializer\\\\ZoneDenormalizer\\:\\:getZoneMemberByCode\\(\\) has parameter \\$zoneMembers with generic interface Doctrine\\\\Common\\\\Collections\\\\Selectable but does not specify its types\\: TKey, T$#"
count: 1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,11 @@
<tag name="serializer.normalizer" priority="64" />
</service>

<service id="Sylius\Bundle\ApiBundle\Serializer\TranslatableDenormalizer">
<argument type="service" id="sylius.translation_locale_provider" />
<tag name="serializer.normalizer" priority="64" />
</service>

<service id="date_time_normalizer" class="Symfony\Component\Serializer\Normalizer\DateTimeNormalizer">
<argument type="collection">
<argument key="datetime_format">Y-m-d H:i:s</argument>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?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\Serializer;

use Sylius\Component\Resource\Model\TranslatableInterface;
use Sylius\Component\Resource\Translation\Provider\TranslationLocaleProviderInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Serializer\Normalizer\ContextAwareDenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait;

/** @experimental */
final class TranslatableDenormalizer implements ContextAwareDenormalizerInterface, DenormalizerAwareInterface
{
use DenormalizerAwareTrait;

private const ALREADY_CALLED = 'sylius_translatable_denormalizer_already_called_for_%s';

public function __construct(
private TranslationLocaleProviderInterface $localeProvider,
) {
}

public function denormalize(mixed $data, string $type, string $format = null, array $context = [])
{
$context[self::getAlreadyCalledKey($type)] = true;

$defaultLocaleCode = $this->localeProvider->getDefaultLocaleCode();

if (!$this->hasDefaultTranslation($data['translations'] ?? [], $defaultLocaleCode)) {
$data['translations'][$defaultLocaleCode] = [
'locale' => $defaultLocaleCode,
];
}

return $this->denormalizer->denormalize($data, $type, $format, $context);
}

public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = []): bool
{
return
Request::METHOD_POST === ($context[ContextKeys::HTTP_REQUEST_METHOD_TYPE] ?? null) &&
!isset($context[self::getAlreadyCalledKey($type)]) &&
is_a($type, TranslatableInterface::class, true)
;
}

private static function getAlreadyCalledKey(string $class): string
{
return sprintf(self::ALREADY_CALLED, $class);
}

private function hasDefaultTranslation(array $translations, string $defaultLocale): bool
{
return
isset($translations[$defaultLocale]['locale']) &&
$defaultLocale === $translations[$defaultLocale]['locale']
;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
<?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 spec\Sylius\Bundle\ApiBundle\Serializer;

use PhpSpec\ObjectBehavior;
use Sylius\Bundle\ApiBundle\Serializer\ContextKeys;
use Sylius\Component\Resource\Model\TranslatableInterface;
use Sylius\Component\Resource\Translation\Provider\TranslationLocaleProviderInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;

final class TranslatableDenormalizerSpec extends ObjectBehavior
{
function let(
DenormalizerInterface $denormalizer,
TranslationLocaleProviderInterface $localeProvider,
): void {
$this->beConstructedWith($localeProvider);

$this->setDenormalizer($denormalizer);
}

function it_only_supports_translatable_resource(): void
{
$this->supportsDenormalization([], TranslatableInterface::class, null, [
ContextKeys::HTTP_REQUEST_METHOD_TYPE => 'PUT',
])->shouldReturn(false);

$this->supportsDenormalization([], TranslatableInterface::class, null, [
'sylius_translatable_denormalizer_already_called_for_Sylius\Component\Resource\Model\TranslatableInterface' => true,
ContextKeys::HTTP_REQUEST_METHOD_TYPE => 'POST',
])->shouldReturn(false);

$this->supportsDenormalization([], \stdClass::class, null, [
ContextKeys::HTTP_REQUEST_METHOD_TYPE => 'POST',
])->shouldReturn(false);
}

function it_does_nothing_when_data_contains_a_translation_in_default_locale(
DenormalizerInterface $denormalizer,
TranslationLocaleProviderInterface $localeProvider,
): void {
$data = ['translations' => ['en' => ['locale' => 'en']]];

$localeProvider->getDefaultLocaleCode()->willReturn('en');

$denormalizer->denormalize($data, TranslatableInterface::class, null, [
'sylius_translatable_denormalizer_already_called_for_Sylius\Component\Resource\Model\TranslatableInterface' => true,
ContextKeys::HTTP_REQUEST_METHOD_TYPE => 'POST',
])->shouldBeCalled()->willReturn($data);

$this
->denormalize($data, TranslatableInterface::class, null, [ContextKeys::HTTP_REQUEST_METHOD_TYPE => 'POST'])
->shouldReturn($data)
;
}

function it_adds_default_translation_when_no_translations_passed_in_data(
DenormalizerInterface $denormalizer,
TranslationLocaleProviderInterface $localeProvider,
): void {
$localeProvider->getDefaultLocaleCode()->willReturn('en');

$updatedData = ['translations' => ['en' => ['locale' => 'en']]];

$denormalizer->denormalize($updatedData, TranslatableInterface::class, null, [
'sylius_translatable_denormalizer_already_called_for_Sylius\Component\Resource\Model\TranslatableInterface' => true,
ContextKeys::HTTP_REQUEST_METHOD_TYPE => 'POST',
])->shouldBeCalled()->willReturn($updatedData);

$this
->denormalize([], TranslatableInterface::class, null, [ContextKeys::HTTP_REQUEST_METHOD_TYPE => 'POST'])
->shouldReturn($updatedData)
;
}

function it_adds_default_translation_when_no_translation_passed_for_default_locale_in_data(
DenormalizerInterface $denormalizer,
TranslationLocaleProviderInterface $localeProvider,
): void {
$localeProvider->getDefaultLocaleCode()->willReturn('en');

$originalData = ['translations' => ['en' => []]];
$updatedData = ['translations' => ['en' => ['locale' => 'en']]];

$denormalizer->denormalize($updatedData, TranslatableInterface::class, null, [
'sylius_translatable_denormalizer_already_called_for_Sylius\Component\Resource\Model\TranslatableInterface' => true,
ContextKeys::HTTP_REQUEST_METHOD_TYPE => 'POST',
])->shouldBeCalled()->willReturn($updatedData);

$this
->denormalize($originalData, TranslatableInterface::class, null, [ContextKeys::HTTP_REQUEST_METHOD_TYPE => 'POST'])
->shouldReturn($updatedData)
;
}

function it_adds_default_translation_when_passed_default_translation_has_empty_locale(
DenormalizerInterface $denormalizer,
TranslationLocaleProviderInterface $localeProvider,
): void {
$localeProvider->getDefaultLocaleCode()->willReturn('en');

$originalData = ['translations' => ['en' => ['locale' => '']]];
$updatedData = ['translations' => ['en' => ['locale' => 'en']]];

$denormalizer->denormalize($updatedData, TranslatableInterface::class, null, [
'sylius_translatable_denormalizer_already_called_for_Sylius\Component\Resource\Model\TranslatableInterface' => true,
ContextKeys::HTTP_REQUEST_METHOD_TYPE => 'POST',
])->shouldBeCalled()->willReturn($updatedData);

$this
->denormalize($originalData, TranslatableInterface::class, null, [ContextKeys::HTTP_REQUEST_METHOD_TYPE => 'POST'])
->shouldReturn($updatedData)
;
}

function it_adds_default_translation_when_passed_default_translation_has_null_locale(
DenormalizerInterface $denormalizer,
TranslationLocaleProviderInterface $localeProvider,
): void {
$localeProvider->getDefaultLocaleCode()->willReturn('en');

$originalData = ['translations' => ['en' => ['locale' => null]]];
$updatedData = ['translations' => ['en' => ['locale' => 'en']]];

$denormalizer->denormalize($updatedData, TranslatableInterface::class, null, [
'sylius_translatable_denormalizer_already_called_for_Sylius\Component\Resource\Model\TranslatableInterface' => true,
ContextKeys::HTTP_REQUEST_METHOD_TYPE => 'POST',
])->shouldBeCalled()->willReturn($updatedData);

$this
->denormalize($originalData, TranslatableInterface::class, null, [ContextKeys::HTTP_REQUEST_METHOD_TYPE => 'POST'])
->shouldReturn($updatedData)
;
}

function it_adds_default_translation_when_passed_default_translation_has_mismatched_locale(
DenormalizerInterface $denormalizer,
TranslationLocaleProviderInterface $localeProvider,
): void {
$localeProvider->getDefaultLocaleCode()->willReturn('en');

$originalData = ['translations' => ['en' => ['locale' => 'fr']]];
$updatedData = ['translations' => ['en' => ['locale' => 'en']]];

$denormalizer->denormalize($updatedData, TranslatableInterface::class, null, [
'sylius_translatable_denormalizer_already_called_for_Sylius\Component\Resource\Model\TranslatableInterface' => true,
ContextKeys::HTTP_REQUEST_METHOD_TYPE => 'POST',
])->shouldBeCalled()->willReturn($updatedData);

$this
->denormalize($originalData, TranslatableInterface::class, null, [ContextKeys::HTTP_REQUEST_METHOD_TYPE => 'POST'])
->shouldReturn($updatedData)
;
}

function it_adds_default_translation_when_no_translation_in_default_locale_passed_in_data(
DenormalizerInterface $denormalizer,
TranslationLocaleProviderInterface $localeProvider,
): void {
$localeProvider->getDefaultLocaleCode()->willReturn('en');

$originalData = ['translations' => ['pl' => ['locale' => 'pl']]];
$updatedData = ['translations' => ['en' => ['locale' => 'en'], 'pl' => ['locale' => 'pl']]];

$denormalizer->denormalize($updatedData, TranslatableInterface::class, null, [
'sylius_translatable_denormalizer_already_called_for_Sylius\Component\Resource\Model\TranslatableInterface' => true,
ContextKeys::HTTP_REQUEST_METHOD_TYPE => 'POST',
])->shouldBeCalled()->willReturn($updatedData);

$this
->denormalize($originalData, TranslatableInterface::class, null, [ContextKeys::HTTP_REQUEST_METHOD_TYPE => 'POST'])
->shouldReturn($updatedData)
;
}
}
28 changes: 24 additions & 4 deletions tests/Api/Admin/ProductAssociationTypesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public function it_gets_product_association_type(): void

$this->assertResponse(
$this->client->getResponse(),
'admin/get_product_association_type_response',
'admin/product_association_type/get_product_association_type_response',
Response::HTTP_OK,
);
}
Expand Down Expand Up @@ -70,7 +70,7 @@ public function it_returns_product_association_type_collection(): void

$this->assertResponse(
$this->client->getResponse(),
'admin/get_product_association_type_collection_response',
'admin/product_association_type/get_product_association_type_collection_response',
Response::HTTP_OK,
);
}
Expand All @@ -97,11 +97,31 @@ public function it_creates_product_association_type(): void

$this->assertResponse(
$this->client->getResponse(),
'admin/post_product_association_type_response',
'admin/product_association_type/post_product_association_type_response',
Response::HTTP_CREATED,
);
}

/** @test */
public function it_does_not_create_product_association_type_without_required_data(): void
{
$this->loadFixturesFromFiles(['product/product_with_many_locales.yaml', 'authentication/api_administrator.yaml']);
$header = array_merge($this->logInAdminUser('api@example.com'), self::CONTENT_TYPE_HEADER);

$this->client->request(
method: 'POST',
uri: '/api/v2/admin/product-association-types',
server: $header,
content: '{}',
);

$this->assertResponse(
$this->client->getResponse(),
'admin/product_association_type/post_product_association_type_without_required_data_response',
Response::HTTP_UNPROCESSABLE_ENTITY,
);
}

/** @test */
public function it_updates_product_association_type(): void
{
Expand All @@ -126,7 +146,7 @@ public function it_updates_product_association_type(): void

$this->assertResponse(
$this->client->getResponse(),
'admin/put_product_association_type_response',
'admin/product_association_type/put_product_association_type_response',
Response::HTTP_OK,
);
}
Expand Down
14 changes: 7 additions & 7 deletions tests/Api/Admin/ProductAttributesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -394,13 +394,7 @@ public function it_does_not_create_a_product_attribute_without_required_data():
method: 'POST',
uri: '/api/v2/admin/product-attributes',
server: $header,
content: json_encode([
'translations' => [
'en_US' => [
'locale' => 'en_US',
],
],
], JSON_THROW_ON_ERROR),
content: '{}',
);

$this->assertResponse(
Expand All @@ -423,6 +417,12 @@ public function it_does_not_create_a_product_attribute_with_unregistered_type():
content: json_encode([
'code' => 'test',
'type' => 'foobar',
'translations' => [
'en_US' => [
'locale' => 'en_US',
'name' => 'Test',
],
],
], JSON_THROW_ON_ERROR),
);

Expand Down

0 comments on commit 68e776b

Please sign in to comment.