Merge branch '2.2' into fix/discriminator-subclass-iri-generation

This commit is contained in:
Mateusz 2026-06-18 15:03:54 +02:00 committed by GitHub
commit cafc7ff9e9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
37 changed files with 421 additions and 77 deletions

View file

@ -1,4 +1,4 @@
# UPGRADE FROM `2.6.0` TO `2.7.0`
# UPGRADE FROM `2.2.6` TO `2.2.7`
## Constructor Signature Changes
@ -14,6 +14,14 @@
)
```
## Bahavior changes
1. The `LiveComponentTagPass` and `TwigComponentTagPass` in `SyliusUiBundle` were registered with a priority of `500`,
which caused them to run before Symfony's autoconfiguration passes (priority `100`).
As a result, services tagged via `#[AutoconfigureTag]` or `registerForAutoconfiguration()` with the `sylius.twig_component`
or `sylius.live_component.*` tag did not receive the `twig.component` tag.
The priority has been lowered to `50` to ensure Symfony's autoconfiguration runs first.
# UPGRADE FROM `2.1` TO `2.2`
## Telemetry

View file

@ -1,3 +1,14 @@
# UPGRADE FROM `2.2.6` TO `2.2.7`
## Shop API
1. The `Sylius\Bundle\ApiBundle\Doctrine\ORM\QueryExtension\Shop\Product\ChannelAndLocaleBasedExtension` service (`sylius_api.doctrine.orm.query_extension.shop.product.channel_and_locale_based`)
now implements `QueryItemExtensionInterface` in addition to `QueryCollectionExtensionInterface`.
The service is tagged with `api_platform.doctrine.orm.query_extension.item`.
If you decorate this service, make sure your decorator also implements
`ApiPlatform\Doctrine\Orm\Extension\QueryItemExtensionInterface` and proxies the `applyToItem` method.
# UPGRADE FROM `2.1` TO `2.2`
## Modified routes

View file

@ -93,7 +93,7 @@ interface ResponseCheckerInterface
public function hasViolationWithMessage(Response $response, string $message, ?string $property = null): bool;
public function appendError(Response $response): ResponseCheckerInterface;
public function appendError(Response $response): self;
public function cleanErrors(): void;

View file

@ -228,6 +228,17 @@ sylius_twig_hooks:
template: '@SyliusAdmin/order/show/content/sections/shipments/item/actions.html.twig'
priority: 0
'sylius_admin.order.show.content.sections.shipments.item.general':
method:
template: '@SyliusAdmin/order/show/content/sections/shipments/item/general/method.html.twig'
priority: 200
shipped_at:
template: '@SyliusAdmin/order/show/content/sections/shipments/item/general/shipped_at.html.twig'
priority: 100
tracking:
template: '@SyliusAdmin/order/show/content/sections/shipments/item/general/tracking.html.twig'
priority: 0
'sylius_admin.order.show.content.sections.shipments.item.actions':
ship:
component: 'sylius_admin:shipment:ship_form'

View file

@ -1,20 +1,3 @@
{% set shipment = hookable_metadata.context.shipment %}
<td class="px-3 py-2 text-nowrap">
<div class="py-2">
<strong {{ sylius_test_html_attribute('shipment-method') }}>{{ shipment.method.name }}</strong>
<div class="text-secondary">{{ ux_icon('tabler:world') }} {{ shipment.method.zone }}</div>
</div>
{% if shipment.shippedAt is not empty %}
<div class="text-secondary-emphasis">
<strong>{{ 'sylius.ui.shipped_at'|trans }}:</strong>
<span {{ sylius_test_html_attribute('shipped-at-date') }}>{{ shipment.shippedAt|date('d-m-Y H:i:s') }}</span>
</div>
{% endif %}
{% if shipment.state == 'shipped' and shipment.tracking is not empty %}
<div class="text-secondary-emphasis">
<strong>{{ 'sylius.ui.tracking_code'|trans }}:</strong>
<span>{{ shipment.tracking }}</span>
</div>
{% endif %}
{% hook 'general' %}
</td>

View file

@ -0,0 +1,6 @@
{% set shipment = hookable_metadata.context.shipment %}
<div class="py-2">
<strong {{ sylius_test_html_attribute('shipment-method') }}>{{ shipment.method.name }}</strong>
<div class="text-secondary">{{ ux_icon('tabler:world') }} {{ shipment.method.zone }}</div>
</div>

View file

@ -0,0 +1,8 @@
{% set shipment = hookable_metadata.context.shipment %}
{% if shipment.shippedAt is not empty %}
<div class="text-secondary-emphasis">
<strong>{{ 'sylius.ui.shipped_at'|trans }}:</strong>
<span {{ sylius_test_html_attribute('shipped-at-date') }}>{{ shipment.shippedAt|date('d-m-Y H:i:s') }}</span>
</div>
{% endif %}

View file

@ -0,0 +1,8 @@
{% set shipment = hookable_metadata.context.shipment %}
{% if shipment.state == 'shipped' and shipment.tracking is not empty %}
<div class="text-secondary-emphasis">
<strong>{{ 'sylius.ui.tracking_code'|trans }}:</strong>
<span>{{ shipment.tracking }}</span>
</div>
{% endif %}

View file

@ -14,6 +14,7 @@ declare(strict_types=1);
namespace Sylius\Bundle\ApiBundle\Doctrine\ORM\QueryExtension\Shop\Product;
use ApiPlatform\Doctrine\Orm\Extension\QueryCollectionExtensionInterface;
use ApiPlatform\Doctrine\Orm\Extension\QueryItemExtensionInterface;
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use ApiPlatform\Metadata\Operation;
use Doctrine\ORM\QueryBuilder;
@ -21,9 +22,8 @@ use Sylius\Bundle\ApiBundle\SectionResolver\ShopApiSection;
use Sylius\Bundle\ApiBundle\Serializer\ContextKeys;
use Sylius\Bundle\CoreBundle\SectionResolver\SectionProviderInterface;
use Sylius\Component\Core\Model\ProductInterface;
use Webmozart\Assert\Assert;
final readonly class ChannelAndLocaleBasedExtension implements QueryCollectionExtensionInterface
final readonly class ChannelAndLocaleBasedExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
{
public function __construct(private SectionProviderInterface $sectionProvider)
{
@ -38,6 +38,33 @@ final readonly class ChannelAndLocaleBasedExtension implements QueryCollectionEx
string $resourceClass,
?Operation $operation = null,
array $context = [],
): void {
$this->apply($queryBuilder, $queryNameGenerator, $resourceClass, $context);
}
/**
* @param array<array-key, mixed> $identifiers
* @param array<array-key, mixed> $context
*/
public function applyToItem(
QueryBuilder $queryBuilder,
QueryNameGeneratorInterface $queryNameGenerator,
string $resourceClass,
array $identifiers,
?Operation $operation = null,
array $context = [],
): void {
$this->apply($queryBuilder, $queryNameGenerator, $resourceClass, $context);
}
/**
* @param array<array-key, mixed> $context
*/
private function apply(
QueryBuilder $queryBuilder,
QueryNameGeneratorInterface $queryNameGenerator,
string $resourceClass,
array $context,
): void {
if (!is_a($resourceClass, ProductInterface::class, true)) {
return;
@ -47,8 +74,9 @@ final readonly class ChannelAndLocaleBasedExtension implements QueryCollectionEx
return;
}
Assert::keyExists($context, ContextKeys::CHANNEL);
Assert::keyExists($context, ContextKeys::LOCALE_CODE);
if (!isset($context[ContextKeys::CHANNEL]) || !isset($context[ContextKeys::LOCALE_CODE])) {
return;
}
$channel = $context[ContextKeys::CHANNEL];
$localeCode = $context[ContextKeys::LOCALE_CODE];

View file

@ -56,6 +56,7 @@
>
<argument type="service" id="sylius.section_resolver.uri_based" />
<tag name="api_platform.doctrine.orm.query_extension.collection" />
<tag name="api_platform.doctrine.orm.query_extension.item" />
</service>
<service

View file

@ -105,8 +105,8 @@ final readonly class ItemProvider implements ProviderInterface
$customer = $order->getCustomer();
return null === $customer
|| null === $customer->getUser()
|| $order->isCreatedByGuest();
return null === $customer ||
null === $customer->getUser() ||
$order->isCreatedByGuest();
}
}

View file

@ -16,7 +16,6 @@ namespace Tests\Sylius\Bundle\ApiBundle\Doctrine\ORM\QueryExtension\Shop\Product
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use ApiPlatform\Metadata\Get;
use Doctrine\ORM\QueryBuilder;
use InvalidArgumentException;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Sylius\Bundle\ApiBundle\Doctrine\ORM\QueryExtension\Shop\Product\ChannelAndLocaleBasedExtension;
@ -73,11 +72,12 @@ final class ChannelAndLocaleBasedExtensionTest extends TestCase
);
}
public function test_it_throws_exception_if_context_has_no_channel(): void
public function test_it_does_not_apply_conditions_to_collection_when_context_has_no_channel(): void
{
$this->sectionProvider->method('getSection')->willReturn(new ShopApiSection());
$this->expectException(InvalidArgumentException::class);
$this->queryBuilder->expects(self::never())->method('getRootAliases');
$this->queryBuilder->expects(self::never())->method('andWhere');
$this->extension->applyToCollection(
$this->queryBuilder,
@ -87,13 +87,14 @@ final class ChannelAndLocaleBasedExtensionTest extends TestCase
);
}
public function test_it_throws_exception_if_context_has_no_locale(): void
public function test_it_does_not_apply_conditions_to_collection_when_context_has_no_locale(): void
{
$this->sectionProvider->method('getSection')->willReturn(new ShopApiSection());
$channel = $this->createMock(ChannelInterface::class);
$this->expectException(InvalidArgumentException::class);
$this->queryBuilder->expects(self::never())->method('getRootAliases');
$this->queryBuilder->expects(self::never())->method('andWhere');
$this->extension->applyToCollection(
$this->queryBuilder,
@ -167,4 +168,132 @@ final class ChannelAndLocaleBasedExtensionTest extends TestCase
],
);
}
public function test_it_does_not_apply_conditions_to_item_for_unsupported_resource(): void
{
$this->queryBuilder->expects(self::never())->method('getRootAliases');
$this->queryBuilder->expects(self::never())->method('andWhere');
$this->extension->applyToItem(
$this->queryBuilder,
$this->queryNameGenerator,
\stdClass::class,
[],
);
}
public function test_it_does_not_apply_conditions_to_item_for_admin_api_section(): void
{
$this->sectionProvider->method('getSection')->willReturn(new AdminApiSection());
$this->queryBuilder->expects(self::never())->method('getRootAliases');
$this->queryBuilder->expects(self::never())->method('andWhere');
$this->extension->applyToItem(
$this->queryBuilder,
$this->queryNameGenerator,
AddressInterface::class,
[],
);
}
public function test_it_does_not_apply_conditions_to_item_when_context_has_no_channel(): void
{
$this->sectionProvider->method('getSection')->willReturn(new ShopApiSection());
$this->queryBuilder->expects(self::never())->method('getRootAliases');
$this->queryBuilder->expects(self::never())->method('andWhere');
$this->extension->applyToItem(
$this->queryBuilder,
$this->queryNameGenerator,
ProductInterface::class,
['code' => 'MUG'],
new Get(),
);
}
public function test_it_does_not_apply_conditions_to_item_when_context_has_no_locale(): void
{
$this->sectionProvider->method('getSection')->willReturn(new ShopApiSection());
$channel = $this->createMock(ChannelInterface::class);
$this->queryBuilder->expects(self::never())->method('getRootAliases');
$this->queryBuilder->expects(self::never())->method('andWhere');
$this->extension->applyToItem(
$this->queryBuilder,
$this->queryNameGenerator,
ProductInterface::class,
['code' => 'MUG'],
new Get(),
[ContextKeys::CHANNEL => $channel],
);
}
public function test_it_filters_item_by_channel_and_locale(): void
{
$this->sectionProvider->method('getSection')->willReturn(new ShopApiSection());
$channel = $this->createMock(ChannelInterface::class);
$this->queryNameGenerator->expects(self::exactly(2))
->method('generateParameterName')
->with($this->callback(function ($param) {
return $param === 'channel' || $param === 'localeCode';
}))
->willReturnCallback(function ($param) {
return $param;
});
$this->queryBuilder->method('getRootAliases')->willReturn(['o']);
$this->queryBuilder->expects(self::once())
->method('addSelect')
->with('translation')
->willReturnSelf();
$this->queryBuilder->expects(self::once())
->method('innerJoin')
->with('o.translations', 'translation', 'WITH', 'translation.locale = :localeCode')
->willReturnSelf();
$this->queryBuilder->expects(self::once())
->method('andWhere')
->with(':channel MEMBER OF o.channels')
->willReturnSelf();
$expectedParams = [
['channel', $channel],
['localeCode', 'en_US'],
];
$callIndex = 0;
$this->queryBuilder->expects(self::exactly(2))
->method('setParameter')
->with(
$this->callback(function ($name) use (&$expectedParams, &$callIndex) {
return $name === $expectedParams[$callIndex][0];
}),
$this->callback(function ($value) use (&$expectedParams, &$callIndex) {
$result = $value === $expectedParams[$callIndex][1];
++$callIndex;
return $result;
}),
)
->willReturnSelf();
$this->extension->applyToItem(
$this->queryBuilder,
$this->queryNameGenerator,
ProductInterface::class,
['code' => 'MUG'],
new Get(),
[
ContextKeys::CHANNEL => $channel,
ContextKeys::LOCALE_CODE => 'en_US',
],
);
}
}

View file

@ -175,6 +175,69 @@ sylius_twig_hooks:
template: '@SyliusShop/shared/layout/base/footer/content/copy.html.twig'
priority: 0
'sylius_shop.base.footer.content.menu':
informational_links:
template: '@SyliusShop/shared/layout/base/footer/content/menu/informational_links.html.twig'
priority: 100
help_links:
template: '@SyliusShop/shared/layout/base/footer/content/menu/help_links.html.twig'
priority: 0
'sylius_shop.base.footer.content.menu.informational_links':
about:
template: '@SyliusShop/shared/layout/base/footer/content/menu/informational_links/about.html.twig'
priority: 300
terms_and_conditions:
template: '@SyliusShop/shared/layout/base/footer/content/menu/informational_links/terms_and_conditions.html.twig'
priority: 200
privacy_policy:
template: '@SyliusShop/shared/layout/base/footer/content/menu/informational_links/privacy_policy.html.twig'
priority: 100
contact_us:
template: '@SyliusShop/shared/layout/base/footer/content/menu/informational_links/contact_us.html.twig'
priority: 0
'sylius_shop.base.footer.content.menu.help_links':
faq:
template: '@SyliusShop/shared/layout/base/footer/content/menu/help_links/faq.html.twig'
priority: 200
delivery_and_shipping:
template: '@SyliusShop/shared/layout/base/footer/content/menu/help_links/delivery_and_shipping.html.twig'
priority: 100
returns_policy:
template: '@SyliusShop/shared/layout/base/footer/content/menu/help_links/returns_policy.html.twig'
priority: 0
'sylius_shop.base.footer.content.payment_methods':
providers:
template: '@SyliusShop/shared/layout/base/footer/content/payment_methods/providers.html.twig'
priority: 100
social_media:
template: '@SyliusShop/shared/layout/base/footer/content/payment_methods/social_media.html.twig'
priority: 0
'sylius_shop.base.footer.content.payment_methods.providers':
paypal:
template: '@SyliusShop/shared/layout/base/footer/content/payment_methods/providers/paypal.html.twig'
priority: 200
adyen:
template: '@SyliusShop/shared/layout/base/footer/content/payment_methods/providers/adyen.html.twig'
priority: 100
mollie:
template: '@SyliusShop/shared/layout/base/footer/content/payment_methods/providers/mollie.html.twig'
priority: 0
'sylius_shop.base.footer.content.payment_methods.social_media':
instagram:
template: '@SyliusShop/shared/layout/base/footer/content/payment_methods/social_media/instagram.html.twig'
priority: 200
facebook:
template: '@SyliusShop/shared/layout/base/footer/content/payment_methods/social_media/facebook.html.twig'
priority: 100
x:
template: '@SyliusShop/shared/layout/base/footer/content/payment_methods/social_media/x.html.twig'
priority: 0
'sylius_shop.base.offcanvas':
cart:
component: 'sylius_shop:cart:widget:offcanvas'

View file

@ -102,7 +102,7 @@
id="sylius_shop.twig.component.product.summary"
class="Sylius\Bundle\ShopBundle\Twig\Component\Product\SummaryComponent"
>
<argument type="service" id="sylius.resolver.product_variant.default" />
<argument type="service" id="sylius.resolver.product_variant" />
<argument type="service" id="sylius.repository.product" />
<argument type="service" id="sylius.repository.product_variant" />

View file

@ -67,8 +67,8 @@ class FormComponent
/** @var OrderInterface|null $order */
$order = $this->repository->find($value);
if (!$order instanceof OrderInterface
|| $order->getCheckoutState() === OrderCheckoutStates::STATE_COMPLETED
if (!$order instanceof OrderInterface ||
$order->getCheckoutState() === OrderCheckoutStates::STATE_COMPLETED
) {
return $this->createResource();
}

View file

@ -1,20 +1,5 @@
<div class="col-12 col-lg-6 order-lg-2">
<div class="row">
<div class="col-12 col-sm-6">
<nav aria-label="{{ 'sylius.ui.footer_informational_links'|trans }}" class="nav flex-column">
<a class="nav-link" href="#">About</a>
<a class="nav-link" href="#">Terms and Conditions</a>
<a class="nav-link" href="#">Privacy Policy</a>
<a class="nav-link" href="{{ path('sylius_shop_contact_request') }}">Contact us</a>
</nav>
</div>
<div class="col-12 col-sm-6">
<nav aria-label="{{ 'sylius.ui.footer_help_links'|trans }}" class="nav flex-column mb-5">
<a class="nav-link" href="#">FAQ</a>
<a class="nav-link" href="#">Delivery & Shipping</a>
<a class="nav-link" href="#">Returns Policy</a>
</nav>
</div>
{% hook 'menu' %}
</div>
</div>

View file

@ -0,0 +1,5 @@
<div class="col-12 col-sm-6">
<nav aria-label="{{ 'sylius.ui.footer_help_links'|trans }}" class="nav flex-column mb-5">
{% hook 'help_links' %}
</nav>
</div>

View file

@ -0,0 +1 @@
<a class="nav-link" href="#">Delivery & Shipping</a>

View file

@ -0,0 +1 @@
<a class="nav-link" href="#">FAQ</a>

View file

@ -0,0 +1 @@
<a class="nav-link" href="#">Returns Policy</a>

View file

@ -0,0 +1,5 @@
<div class="col-12 col-sm-6">
<nav aria-label="{{ 'sylius.ui.footer_informational_links'|trans }}" class="nav flex-column">
{% hook 'informational_links' %}
</nav>
</div>

View file

@ -0,0 +1 @@
<a class="nav-link" href="#">About</a>

View file

@ -0,0 +1 @@
<a class="nav-link" href="{{ path('sylius_shop_contact_request') }}">Contact us</a>

View file

@ -0,0 +1 @@
<a class="nav-link" href="#">Privacy Policy</a>

View file

@ -0,0 +1 @@
<a class="nav-link" href="#">Terms and Conditions</a>

View file

@ -1,27 +1,4 @@
<div class="col-12 text-center col-md-6 order-md-4 text-md-start col-lg-3 order-lg-4 text-lg-center">
<div class="mb-3 fw-medium">Payment methods:</div>
<div class="row g-2 mb-4">
<div class="col-4">
<a href="https://sylius.com/partner/paypal/" target="_blank" class="d-block border rounded p-2">
<img class="img-fluid" src="{{ asset('build/shop/images/paypal-logo.svg', 'shop') }}" width="100%" height="100%" alt="PayPal" />
</a>
</div>
<div class="col-4">
<a href="https://sylius.com/partner/adyen/" target="_blank" class="d-block border rounded p-2">
<img class="img-fluid" src="{{ asset('build/shop/images/adyen-logo.svg', 'shop') }}" width="100%" height="100%" alt="Adyen" />
</a>
</div>
<div class="col-4">
<a href="https://sylius.com/partner/mollie/" target="_blank" class="d-block border rounded p-2">
<img class="img-fluid" src="{{ asset('build/shop/images/mollie-logo.svg', 'shop') }}" width="100%" height="100%" alt="Mollie" />
</a>
</div>
</div>
<div class="d-flex justify-content-center justify-content-md-start justify-content-lg-center gap-3 mb-5">
<a class="link-reset" href="https://www.instagram.com/sylius.team/" target="_blank" aria-label="instagram logo">{{ ux_icon('tabler:brand-instagram', { class: 'icon icon-md' }) }}</a>
<a class="link-reset" href="https://www.facebook.com/SyliusEcommerce/" target="_blank" aria-label="facebook logo">{{ ux_icon('tabler:brand-facebook', { class: 'icon icon-md' }) }}</a>
<a class="link-reset" href="https://x.com/Sylius" target="_blank" aria-label="x logo">{{ ux_icon('tabler:brand-x', { class: 'icon icon-md' }) }}</a>
</div>
{% hook 'payment_methods' %}
</div>

View file

@ -0,0 +1,3 @@
<div class="row g-2 mb-4">
{% hook 'providers' %}
</div>

View file

@ -0,0 +1,5 @@
<div class="col-4">
<a href="https://sylius.com/partner/adyen/" target="_blank" class="d-block border rounded p-2">
<img class="img-fluid" src="{{ asset('build/shop/images/adyen-logo.svg', 'shop') }}" width="100%" height="100%" alt="Adyen" />
</a>
</div>

View file

@ -0,0 +1,5 @@
<div class="col-4">
<a href="https://sylius.com/partner/mollie/" target="_blank" class="d-block border rounded p-2">
<img class="img-fluid" src="{{ asset('build/shop/images/mollie-logo.svg', 'shop') }}" width="100%" height="100%" alt="Mollie" />
</a>
</div>

View file

@ -0,0 +1,5 @@
<div class="col-4">
<a href="https://sylius.com/partner/paypal/" target="_blank" class="d-block border rounded p-2">
<img class="img-fluid" src="{{ asset('build/shop/images/paypal-logo.svg', 'shop') }}" width="100%" height="100%" alt="PayPal" />
</a>
</div>

View file

@ -0,0 +1,3 @@
<div class="d-flex justify-content-center justify-content-md-start justify-content-lg-center gap-3 mb-5">
{% hook 'social_media' %}
</div>

View file

@ -0,0 +1 @@
<a class="link-reset" href="https://www.facebook.com/SyliusEcommerce/" target="_blank" aria-label="facebook logo">{{ ux_icon('tabler:brand-facebook', { class: 'icon icon-md' }) }}</a>

View file

@ -0,0 +1 @@
<a class="link-reset" href="https://www.instagram.com/sylius.team/" target="_blank" aria-label="instagram logo">{{ ux_icon('tabler:brand-instagram', { class: 'icon icon-md' }) }}</a>

View file

@ -0,0 +1 @@
<a class="link-reset" href="https://x.com/Sylius" target="_blank" aria-label="x logo">{{ ux_icon('tabler:brand-x', { class: 'icon icon-md' }) }}</a>

View file

@ -25,7 +25,7 @@ final class SyliusUiBundle extends Bundle
{
public function build(ContainerBuilder $container): void
{
$container->addCompilerPass(new LiveComponentTagPass(), priority: 500);
$container->addCompilerPass(new TwigComponentTagPass(), priority: 500);
$container->addCompilerPass(new LiveComponentTagPass(), priority: 50);
$container->addCompilerPass(new TwigComponentTagPass(), priority: 50);
}
}

View file

@ -0,0 +1,57 @@
Sylius\Component\Core\Model\Channel:
channel_web:
code: 'WEB'
name: 'Web Channel'
hostname: 'localhost'
description: 'Lorem ipsum'
baseCurrency: '@currency_usd'
defaultLocale: '@locale_en'
locales: ['@locale_en']
color: 'black'
enabled: true
taxCalculationStrategy: 'order_items_based'
channel_mobile:
code: 'MOBILE'
name: 'Mobile Channel'
hostname: 'mobile.localhost'
description: 'Lorem ipsum'
baseCurrency: '@currency_usd'
defaultLocale: '@locale_en'
locales: ['@locale_en']
color: 'blue'
enabled: true
taxCalculationStrategy: 'order_items_based'
Sylius\Component\Currency\Model\Currency:
currency_usd:
code: 'USD'
Sylius\Component\Locale\Model\Locale:
locale_en:
code: 'en_US'
Sylius\Component\Core\Model\Product:
product_in_web_channel:
code: 'MUG'
channels: ['@channel_web']
currentLocale: 'en_US'
product_in_mobile_channel_only:
code: 'T_SHIRT'
channels: ['@channel_mobile']
currentLocale: 'en_US'
Sylius\Component\Core\Model\ProductTranslation:
product_translation_mug_en:
slug: 'mug'
locale: 'en_US'
name: 'Mug'
description: 'A mug'
shortDescription: 'Mug'
translatable: '@product_in_web_channel'
product_translation_tshirt_en:
slug: 't-shirt'
locale: 'en_US'
name: 'T-Shirt'
description: 'A t-shirt'
shortDescription: 'T-Shirt'
translatable: '@product_in_mobile_channel_only'

View file

@ -37,6 +37,34 @@ final class ProductsTest extends JsonApiTestCase
$this->assertResponseCode($response, Response::HTTP_MOVED_PERMANENTLY);
}
#[Test]
public function it_returns_not_found_response_when_getting_product_not_in_current_channel(): void
{
$this->loadFixturesFromFile('product/product_not_in_channel.yaml');
$this->client->request(
method: 'GET',
uri: '/api/v2/shop/products/T_SHIRT',
server: self::CONTENT_TYPE_HEADER,
);
$this->assertResponseCode($this->client->getResponse(), Response::HTTP_NOT_FOUND);
}
#[Test]
public function it_returns_product_from_current_channel(): void
{
$this->loadFixturesFromFile('product/product_not_in_channel.yaml');
$this->client->request(
method: 'GET',
uri: '/api/v2/shop/products/MUG',
server: self::CONTENT_TYPE_HEADER,
);
$this->assertResponseCode($this->client->getResponse(), Response::HTTP_OK);
}
#[Test]
public function it_returns_product_with_translations_in_default_locale(): void
{