Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[API] Disallow removing the translation in default locale #15517

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?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\DataPersister;

use ApiPlatform\Core\DataPersister\ContextAwareDataPersisterInterface;
use ApiPlatform\Core\DataPersister\ResumableDataPersisterInterface;
use Sylius\Bundle\ApiBundle\Exception\TranslationInDefaultLocaleCannotBeRemoved;
use Sylius\Component\Resource\Model\TranslatableInterface;
use Sylius\Component\Resource\Translation\Provider\TranslationLocaleProviderInterface;

final class TranslatableDataPersister implements ContextAwareDataPersisterInterface, ResumableDataPersisterInterface
{
public function __construct(private TranslationLocaleProviderInterface $localeProvider)
{
}

/**
* @param array<array-key, mixed> $context
*/
public function supports($data, array $context = []): bool
{
return $data instanceof TranslatableInterface;
}

/**
* @param TranslatableInterface $data
* @param array<array-key, mixed> $context
*/
public function persist($data, array $context = []): object
{
$defaultLocaleCode = $this->localeProvider->getDefaultLocaleCode();

if (!$data->getTranslations()->containsKey($defaultLocaleCode)) {
throw new TranslationInDefaultLocaleCannotBeRemoved(
sprintf('Translation in the default locale "%s" cannot be removed.', $defaultLocaleCode),
);
}

return $data;
}

/**
* @param TranslatableInterface $data
* @param array<array-key, mixed> $context
*/
public function remove($data, array $context = []): mixed
{
return $data;
}

/** @param array<array-key, mixed> $context */
public function resumable(array $context = []): bool
{
return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?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\Exception;

/** @experimental */
final class TranslationInDefaultLocaleCannotBeRemoved extends \RuntimeException
{
public function __construct(
string $message = 'Translation in the default locale cannot be removed.',
int $code = 0,
\Throwable $previous = null,
) {
parent::__construct($message, $code, $previous);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ api_platform:
Sylius\Bundle\ApiBundle\Exception\ProvinceCannotBeRemoved: 422
Sylius\Bundle\ApiBundle\Exception\ShippingMethodCannotBeRemoved: 422
Sylius\Bundle\ApiBundle\Exception\TaxonCannotBeRemoved: 422
Sylius\Bundle\ApiBundle\Exception\TranslationInDefaultLocaleCannotBeRemoved: 422
Sylius\Bundle\ApiBundle\Exception\ZoneCannotBeRemoved: 422
Sylius\Bundle\ApiBundle\Exception\CannotRemoveMenuTaxonException: 409
Sylius\Bundle\LocaleBundle\Checker\Exception\LocaleIsUsedException: 422
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,11 @@
<tag name="api_platform.data_persister" />
</service>

<service id="Sylius\Bundle\ApiBundle\DataPersister\TranslatableDataPersister">
<argument type="service" id="sylius.translation_locale_provider" />
<tag name="api_platform.data_persister" priority="128" />
</service>

<service id="Sylius\Bundle\ApiBundle\DataPersister\ZoneDataPersister">
<argument type="service" id="api_platform.doctrine.orm.data_persister" />
<argument type="service" id="Sylius\Component\Addressing\Checker\ZoneDeletionCheckerInterface" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?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\DataPersister;

use ApiPlatform\Core\DataPersister\ContextAwareDataPersisterInterface;
use ApiPlatform\Core\DataPersister\ResumableDataPersisterInterface;
use Doctrine\Common\Collections\ArrayCollection;
use PhpSpec\ObjectBehavior;
use Sylius\Bundle\ApiBundle\Exception\TranslationInDefaultLocaleCannotBeRemoved;
use Sylius\Component\Resource\Model\TranslatableInterface;
use Sylius\Component\Resource\Translation\Provider\TranslationLocaleProviderInterface;

final class TranslatableDataPersisterSpec extends ObjectBehavior
{
function let(TranslationLocaleProviderInterface $localeProvider): void
{
$this->beConstructedWith($localeProvider);
}

function it_is_a_context_aware_data_persister(): void
{
$this->shouldImplement(ContextAwareDataPersisterInterface::class);
}

function it_is_a_resumable_data_persister(): void
{
$this->shouldImplement(ResumableDataPersisterInterface::class);
}

function it_supports_only_translatable(TranslatableInterface $translatable): void
{
$this->supports(new \stdClass())->shouldReturn(false);
$this->supports($translatable)->shouldReturn(true);
}

function it_does_nothing_if_there_is_a_translation_in_default_locale(
TranslationLocaleProviderInterface $localeProvider,
TranslatableInterface $translatable,
TranslatableInterface $translation,
): void {
$localeProvider->getDefaultLocaleCode()->willReturn('en_US');
$translatable->getTranslations()->willReturn(new ArrayCollection(['en_US' => $translation]));

$this->persist($translatable)->shouldReturn($translatable);
}

function it_throws_an_exception_if_there_is_no_translation_in_default_locale(
TranslationLocaleProviderInterface $localeProvider,
TranslatableInterface $translatable,
TranslatableInterface $translation,
): void {
$localeProvider->getDefaultLocaleCode()->willReturn('en_US');
$translatable->getTranslations()->willReturn(new ArrayCollection(['de_DE' => $translation]));

$this->shouldThrow(TranslationInDefaultLocaleCannotBeRemoved::class)->during('persist', [$translatable]);
}

function it_does_nothing_during_removing_object(TranslatableInterface $translatable): void
{
$this->remove($translatable)->shouldReturn($translatable);
}

function it_is_resumable(): void
{
$this->resumable()->shouldReturn(true);
}
}
14 changes: 10 additions & 4 deletions tests/Api/Admin/ProductAssociationTypesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -136,10 +136,16 @@ public function it_updates_product_association_type(): void
server: $header,
content: json_encode([
'code' => 'TEST',
'translations' => ['de_DE' => [
'name' => 'test',
'description' => 'test description'
]]
'translations' => [
'en_US' => [
'@id' => sprintf('/api/v2/admin/product-association-type-translations/%s', $associationType->getTranslation('en_US')->getId()),
'name' => 'Similar products',
],
'de_DE' => [
'name' => 'test',
'description' => 'test description'
],
]
], JSON_THROW_ON_ERROR),
);

Expand Down
47 changes: 40 additions & 7 deletions tests/Api/Admin/ProductVariantsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ public function it_creates_a_product_variant_with_all_optional_data(): void
}

/** @test */
public function it_creates_a_product_variant_enabled_by_default(): void
public function it_creates_a_product_variant_enabled_by_default_with_translation_in_default_locale(): void
{
$this->loadFixturesFromFiles([
'authentication/api_administrator.yaml',
Expand All @@ -168,7 +168,7 @@ public function it_creates_a_product_variant_enabled_by_default(): void

$this->assertResponse(
$this->client->getResponse(),
'admin/product_variant/post_product_variant_enabled_by_default_response',
'admin/product_variant/post_product_variant_enabled_by_default_with_translation_in_default_locale_response',
Response::HTTP_CREATED,
);
}
Expand Down Expand Up @@ -325,13 +325,11 @@ public function it_updates_the_existing_product_variant(): void
'minimumPrice' => 500,
]],
'translations' => [
'pl_PL' => [
'@id' => sprintf('/api/v2/admin/product-variant-translations/%s', $productVariant->getTranslation('pl_PL')->getId()),
'locale' => 'pl_PL',
'name' => 'Czerwony kubek',
'en_US' => [
'@id' => sprintf('/api/v2/admin/product-variant-translations/%s', $productVariant->getTranslation('en_US')->getId()),
'name' => 'Red mug',
],
'de_DE' => [
'locale' => 'de_DE',
'name' => 'Rote Tasse',
],
],
Expand Down Expand Up @@ -396,6 +394,41 @@ public function it_does_not_allow_to_update_product_variant_with_invalid_locale_
$this->assertResponseCode($this->client->getResponse(), Response::HTTP_UNPROCESSABLE_ENTITY);
}

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

/** @var ProductVariantInterface $productVariant */
$productVariant = $fixtures['product_variant'];

$this->client->request(
method: 'PUT',
uri: sprintf('/api/v2/admin/product-variants/%s', $productVariant->getCode()),
server: $header,
content: json_encode([
'translations' => [
'de_DE' => [
'name' => 'Tasse',
],
],
], JSON_THROW_ON_ERROR),
);

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

/** @test */
public function it_deletes_the_product_variant(): void
{
Expand Down
4 changes: 2 additions & 2 deletions tests/Api/Admin/ProductsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ public function it_does_not_create_a_product_without_required_data(): void
}

/** @test */
public function it_does_not_create_a_product_without_translation_locale(): void
public function it_does_not_create_a_product_with_invalid_translation_locale(): void
{
$this->loadFixturesFromFile('authentication/api_administrator.yaml');
$header = array_merge($this->logInAdminUser('api@example.com'), self::CONTENT_TYPE_HEADER);
Expand All @@ -172,7 +172,7 @@ public function it_does_not_create_a_product_without_translation_locale(): void

$this->assertResponse(
$this->client->getResponse(),
'admin/product/post_product_without_translation_locale',
'admin/product/post_product_with_invalid_translation_locale',
Response::HTTP_UNPROCESSABLE_ENTITY,
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"@type": "ProductAssociationType",
"id": @integer@,
"code": "similar_products",
"name": null,
"name": "Similar products",
"createdAt": "@string@",
"updatedAt": "@string@",
"translations": {
Expand All @@ -14,11 +14,11 @@
"id": @integer@,
"name": "test"
},
"en": {
"@id": "\/api\/v2\/admin\/product-association-type-translations\/",
"en_US": {
"@id": "\/api\/v2\/admin\/product-association-type-translations\/@integer@",
"@type": "ProductAssociationTypeTranslation",
"id": null,
"name": null
"id": @integer@,
"name": "Similar products"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,12 @@
"name": "Rote Tasse",
"locale": "de_DE"
},
"pl_PL": {
"en_US": {
"@id": "\/api\/v2\/admin\/product-variant-translations\/@integer@",
"@type": "ProductVariantTranslation",
"id": @integer@,
"name": "Czerwony kubek",
"locale": "pl_PL"
"name": "Red mug",
"locale": "en_US"
}
},
"enabled": false,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"@context": "\/api\/v2\/contexts\/Error",
"@type": "hydra:Error",
"hydra:title": "An error occurred",
"hydra:description": "Translation in the default locale \"en_US\" cannot be removed."
}