Merge branch '1.12' into 1.13

* 1.12:
  Refactor to provide a product translation instead of generate product shop url
  Provide post-CR fixes
  Allow passing a desired locale code to DefaultChannelFactory
  Remove unknown localeCode named paramter
  Add support for secure and unsecured URLs for generating product show page
  Refactor logic behind a button redirecting to the product's shop page
  Adding try logic to get the shop's slug
  Adding default if product has no slug
  [Bunnyshell] Adjust the workflows for Fork PRs
  [Maintenance] Improve ProportionalIntegerDistributor to keep original index
This commit is contained in:
Grzegorz Sadowski 2023-07-07 11:20:44 +02:00
commit 7035db1945
No known key found for this signature in database
GPG key ID: EF87FF4E6E3BD364
21 changed files with 500 additions and 39 deletions

View file

@ -10,7 +10,7 @@ In order to check a product in shop in all channels it is available in
And the store has a product "Bugatti" available in "United States" channel
And I am logged in as an administrator
@ui
@ui @no-api
Scenario: Accessing product show page in shop from the product variant edit page where product is available in more than one channel
Given this product is also available in the "Europe" channel
And this product has "Red" variant priced at "$220000.00" in "Europe" channel
@ -18,14 +18,14 @@ In order to check a product in shop in all channels it is available in
And I choose to show this product in the "Europe" channel
Then I should see this product in the "Europe" channel in the shop
@ui
@ui @no-api
Scenario: Accessing product show page in shop from the product variant edit page where product is available in one channel
Given this product has "Red" variant priced at "$220000.00" in "United States" channel
When I want to modify the "Bugatti" product variant
And I choose to show this product in this channel
Then I should see this product in the "United States" channel in the shop
@ui
@ui @no-api
Scenario: Being unable to access product show page in shop when the product is disabled
Given this product has been disabled
When I want to modify the "Bugatti" product variant

View file

@ -0,0 +1,42 @@
@managing_products
Feature: Accessing a store's product page
In order to access a store's product page from the admin panel
As an Administrator
I want to have a way to be redirected to the store's product page from the admin panel
Background:
Given the store operates on a single channel in "German (Germany)" locale
And the store has a product "Französischer Bulldoggen T-Shirt"
And I am logged in as an administrator
And I am using "Polish (Poland)" locale for my panel
@ui @no-api
Scenario: Opening a product's page from the admin panel when the product has a translation with a defined slug in the administrator's chosen language
Given the locale "Polish (Poland)" is enabled
And this product is named "Bulldog francuski T-Shirt" in the "Polish (Poland)" locale
When I want to edit this product
Then the show product's page button should be enabled
And it should be leading to the product's page in the "Polish (Poland)" locale
@ui @no-api
Scenario: Opening a product's page from the admin panel when the product has a translation with a defined slug in the default channel's language
Given the locale "French (France)" is enabled
And this product is named "Tee-shirt bouledogue français" in the "French (France)" locale
When I want to edit this product
Then the show product's page button should be enabled
And it should be leading to the product's page in the "German (Germany)" locale
@ui @no-api
Scenario: Opening a product's page from the admin panel with using first available locale with slug and enabled in the channel
Given the locale "French (France)" is enabled
And the store also operates in "French (France)" locale
And this product has no slug in the "German (Germany)" locale
And this product is named "T-shirt bouledogue français" in the "French (France)" locale
When I want to edit this product
Then it should be leading to the product's page in the "French (France)" locale
@ui @no-api
Scenario: Not being able to open a product's page from the admin panel when the product has no translations meeting the criteria
Given this product has no translations with a defined slug
When I want to edit this product
Then the show product's page button should be disabled

View file

@ -11,20 +11,20 @@ Feature: Checking products in the shop while viewing them
And I am logged in as an administrator
And I am browsing products
@ui
@ui @no-api
Scenario: Accessing product show page in shop from the product show page where product is available in more than one channel
Given this product is available in the "Europe" channel
When I access "Bugatti" product page
And I show this product in the "Europe" channel
Then I should see this product in the "Europe" channel in the shop
@ui
@ui @no-api
Scenario: Accessing product show page in shop from the product show page where product is available in one channel
When I access "Bugatti" product page
And I show this product in this channel
Then I should see this product in the "Europe" channel in the shop
@ui
@ui @no-api
Scenario: Being unable to access product show page in shop when the product is disabled
Given this product has been disabled
When I access "Bugatti" product page

View file

@ -25,6 +25,8 @@ use Sylius\Component\Core\Model\ChannelInterface;
use Sylius\Component\Core\Model\ShopBillingData;
use Sylius\Component\Core\Model\TaxonInterface;
use Sylius\Component\Core\Test\Services\DefaultChannelFactoryInterface;
use Sylius\Component\Locale\Model\LocaleInterface;
use Sylius\Component\Resource\Repository\RepositoryInterface;
final class ChannelContext implements Context
{
@ -35,6 +37,7 @@ final class ChannelContext implements Context
private DefaultChannelFactoryInterface $defaultChannelFactory,
private ChannelRepositoryInterface $channelRepository,
private ObjectManager $channelManager,
private RepositoryInterface $localeRepository,
) {
}
@ -98,6 +101,17 @@ final class ChannelContext implements Context
$this->sharedStorage->set('channel', $defaultData['channel']);
}
/**
* @Given the store operates on a single channel in :localeCode locale
*/
public function storeOperatesOnASingleChannelInLocale(string $localeCode): void
{
$defaultData = $this->defaultChannelFactory->create(localeCode: $localeCode);
$this->sharedStorage->setClipboard($defaultData);
$this->sharedStorage->set('channel', $defaultData['channel']);
}
/**
* @Given /^the store(?:| also) operates on (?:a|another) channel named "([^"]+)"$/
* @Given /^the store(?:| also) operates on (?:a|another) channel named "([^"]+)" in "([^"]+)" currency$/
@ -320,6 +334,18 @@ final class ChannelContext implements Context
$channel->getChannelPriceHistoryConfig()->setLowestPriceForDiscountedProductsVisible(false);
}
/**
* @Given the store also operates in :locale locale
*/
public function theStoreAlsoOperatesInLocale(LocaleInterface $locale): void
{
/** @var ChannelInterface $channel */
$channel = $this->sharedStorage->get('channel');
$channel->addLocale($locale);
$this->channelManager->flush();
}
/**
* @param bool $state
*/

View file

@ -1314,6 +1314,30 @@ final class ProductContext implements Context
return $channelPricing;
}
/**
* @Given /^(this product) has no slug in the ("[^"]+" locale)$/
*/
public function thisProductHasNoSlugInTheLocale(ProductInterface $product, string $localeCode): void
{
$productTranslation = $product->getTranslation($localeCode);
$productTranslation->setSlug('');
$this->saveProduct($product);
}
/**
* @Given /^(this product) has no translations with a defined slug$/
*/
public function thisProductHasNoTranslationsWithADefinedSlug(ProductInterface $product): void
{
/** @var ProductTranslationInterface $productTranslation */
foreach ($product->getTranslations() as $productTranslation) {
$productTranslation->setSlug('');
}
$this->saveProduct($product);
}
private function getPriceFromString(string $price): int
{
return (int) round((float) str_replace(['€', '£', '$'], '', $price) * 100, 2);

View file

@ -370,6 +370,7 @@ final class ManagingProductsContext implements Context
/**
* @When I want to modify the :product product
* @When /^I want to modify (this product)$/
* @When /^I want to edit (this product)$/
* @When I modify the :product product
*/
public function iWantToModifyAProduct(ProductInterface $product): void
@ -1199,6 +1200,36 @@ final class ManagingProductsContext implements Context
Assert::eq($this->indexPage->getPageNumber(), $page);
}
/**
* @Then the show product's page button should be enabled
*/
public function theShowProductsPageButtonShouldBeEnabled(): void
{
Assert::false($this->updateSimpleProductPage->isShowInShopButtonDisabled());
}
/**
* @Then the show product's page button should be disabled
*/
public function theShowProductsPageButtonShouldBeDisabled(): void
{
Assert::true($this->updateSimpleProductPage->isShowInShopButtonDisabled());
}
/**
* @Then /^it should be leading to (the product)'s page in the ("[^"]+" locale)$/
*/
public function itShouldBeLeadingToTheProductPageInTheLocale(ProductInterface $product, string $localeCode): void
{
$productTranslation = $product->getTranslation($localeCode);
$showProductPageUrl = $this->updateSimpleProductPage->getShowProductInSingleChannelUrl();
Assert::contains(
$showProductPageUrl,
sprintf('/%s/products/%s', $localeCode, $productTranslation->getSlug()),
);
}
/**
* @param string $element
* @param string $value

View file

@ -413,6 +413,11 @@ class UpdateSimpleProductPage extends BaseUpdatePage implements UpdateSimpleProd
return null !== $this->getDocument()->find('css', '.tab > h3:contains("Inventory")');
}
public function getShowProductInSingleChannelUrl(): string
{
return $this->getElement('show_product_single_button')->getAttribute('href');
}
public function isShowInShopButtonDisabled(): bool
{
return $this->getElement('show_product_single_button')->hasClass('disabled');
@ -487,7 +492,7 @@ class UpdateSimpleProductPage extends BaseUpdatePage implements UpdateSimpleProd
'product_taxons' => '#sylius_product_productTaxons',
'shipping_required' => '#sylius_product_variant_shippingRequired',
'show_product_dropdown' => '.scrolling.menu',
'show_product_single_button' => 'a:contains("Show product in shop page")',
'show_product_single_button' => '[data-test-show-product-in-shop-page]',
'slug' => '#sylius_product_translations_%locale%_slug',
'tab' => '.menu [data-tab="%name%"]',
'taxonomy' => 'a[data-tab="taxonomy"]',

View file

@ -112,6 +112,8 @@ interface UpdateSimpleProductPageInterface extends BaseUpdatePageInterface
public function hasInventoryTab(): bool;
public function getShowProductInSingleChannelUrl(): string;
public function isShowInShopButtonDisabled(): bool;
public function showProductInChannel(string $channel): void;

View file

@ -53,6 +53,7 @@
<argument type="service" id="sylius.behat.factory.default_channel" />
<argument type="service" id="sylius.repository.channel" />
<argument type="service" id="sylius.manager.channel" />
<argument type="service" id="sylius.repository.locale" />
</service>
<service id="sylius.behat.context.setup.currency" class="Sylius\Behat\Context\Setup\CurrencyContext">

View file

@ -2,12 +2,12 @@
{% if sylius_bundle_loaded_checker('SyliusShopBundle') %}
{% if not product.enabled or enabledChannels|length < 1 %}
<a class="ui labeled icon button disabled" href="#">
<a class="ui labeled icon button disabled" href="#" {{ sylius_test_html_attribute('show-product-in-shop-page') }}>
<i class="angle right icon"></i>
{{ 'sylius.ui.show_product_in_shop_page'|trans }}
</a>
{% elseif enabledChannels|length > 1 %}
<div class="ui floating dropdown labeled icon button">
<div class="ui floating dropdown labeled icon button" {{ sylius_test_html_attribute('show-product-in-shop-page') }}>
<i class="share alternate icon"></i>
<span class="text">
{{ 'sylius.ui.show_product_in_shop_page'|trans }}
@ -15,8 +15,16 @@
<div class="menu">
<div class="scrolling menu">
{% for channel in enabledChannels %}
{% set url = sylius_channel_url(path('sylius_shop_product_show', {'slug': product.slug, '_locale': channel.defaultLocale.code}), channel) %}
<a href="{{ url|raw }}" class="item" target="_blank">
{% set product_translation = sylius_product_translation(product, channel) %}
{% if product_translation is not null %}
{% set url = sylius_channel_url(path('sylius_shop_product_show', {'slug': product_translation.slug, '_locale': product_translation.locale}), channel) %}
{% endif %}
<a
href="{{ (url is not defined) ? '#' : url|raw }}"
class="item {% if url is not defined %}disabled{% endif %}"
target="_blank"
>
<i class="angle right icon"></i>
{{ 'sylius.ui.show_in'|trans }}
{{ channel.name }} ({{ channel.code }})
@ -27,8 +35,17 @@
</div>
{% else %}
{% for channel in enabledChannels %}
{% set url = sylius_channel_url(path('sylius_shop_product_show', {'slug': product.slug, '_locale': channel.defaultLocale.code}), channel) %}
<a class="ui labeled icon button" href="{{ url|raw }}" target="_blank">
{% set product_translation = sylius_product_translation(product, channel) %}
{% if product_translation is not null %}
{% set url = sylius_channel_url(path('sylius_shop_product_show', {'slug': product_translation.slug, '_locale': product_translation.locale}), channel) %}
{% endif %}
<a
class="ui labeled icon button {% if url is not defined %}disabled{% endif %}"
href="{{ (url is not defined) ? '#' : url|raw }}"
target="_blank"
{{ sylius_test_html_attribute('show-product-in-shop-page') }}
>
<i class="angle right icon"></i>
{{ 'sylius.ui.show_product_in_shop_page'|trans }}
</a>

View file

@ -0,0 +1,80 @@
<?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\CoreBundle\Provider;
use Doctrine\Common\Collections\Collection;
use Sylius\Component\Core\Model\ChannelInterface;
use Sylius\Component\Core\Model\ProductInterface;
use Sylius\Component\Core\Model\ProductTranslationInterface;
use Sylius\Component\Locale\Context\LocaleContextInterface;
use Sylius\Component\Locale\Model\LocaleInterface;
final class ChannelBasedProductTranslationProvider implements ChannelBasedProductTranslationProviderInterface
{
public function __construct(private LocaleContextInterface $localeContext)
{
}
public function provide(ProductInterface $product, ChannelInterface $channel): ?ProductTranslationInterface
{
/** @var Collection<array-key, ProductTranslationInterface> $productTranslations */
$productTranslations = $product->getTranslations();
$contextLocaleCode = $this->localeContext->getLocaleCode();
$productTranslation = $this->findTranslationWithSlugForLocales($productTranslations, [$contextLocaleCode]);
if (null !== $productTranslation) {
return $productTranslation;
}
/** @var string $channelDefaultLocaleCode */
$channelDefaultLocaleCode = $channel->getDefaultLocale()->getCode();
$productTranslation = $this->findTranslationWithSlugForLocales($productTranslations, [$channelDefaultLocaleCode]);
if (null !== $productTranslation) {
return $productTranslation;
}
$localesEnabledInChannel = $this->getLocalesCodesEnabledInChannel($channel);
$productTranslation = $this->findTranslationWithSlugForLocales($productTranslations, $localesEnabledInChannel);
return $productTranslation;
}
/**
* @param Collection<array-key, ProductTranslationInterface> $productTranslations
* @param array<string> $localeCodes
*/
private function findTranslationWithSlugForLocales(Collection $productTranslations, array $localeCodes): ?ProductTranslationInterface
{
foreach ($productTranslations as $productTranslation) {
$isLocaleCodeMatching = in_array($productTranslation->getLocale(), $localeCodes);
$isSlugPresent = '' !== $productTranslation->getSlug() && null !== $productTranslation->getSlug();
if ($isLocaleCodeMatching && $isSlugPresent) {
return $productTranslation;
}
}
return null;
}
/** @return array<array-key, string> */
private function getLocalesCodesEnabledInChannel(ChannelInterface $channel): array
{
return $channel->getLocales()->map(function (LocaleInterface $locale): string {
return $locale->getCode();
})->toArray();
}
}

View file

@ -0,0 +1,23 @@
<?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\CoreBundle\Provider;
use Sylius\Component\Core\Model\ChannelInterface;
use Sylius\Component\Core\Model\ProductInterface;
use Sylius\Component\Core\Model\ProductTranslationInterface;
interface ChannelBasedProductTranslationProviderInterface
{
public function provide(ProductInterface $product, ChannelInterface $channel): ?ProductTranslationInterface;
}

View file

@ -12,12 +12,12 @@ parameters:
framework:
messenger:
transports:
main:
main:
dsn: '%sylius_messenger_transport_main_dsn%'
failure_transport: 'main_failed'
main_failed:
main_failed:
dsn: '%sylius_messenger_transport_main_failed_dsn%'
catalog_promotion_removal:
catalog_promotion_removal:
dsn: '%sylius_messenger_transport_catalog_promotion_removal_dsn%'
failure_transport: 'catalog_promotion_removal_failed'
catalog_promotion_removal_failed:
@ -42,4 +42,4 @@ framework:
default_middleware: allow_no_handlers
sylius_default.bus: *command_bus
sylius_event.bus: *event_bus

View file

@ -35,6 +35,7 @@
<import resource="services/price_history.xml" />
<import resource="services/product_variant_map.xml" />
<import resource="services/promotion.xml" />
<import resource="services/providers.xml" />
<import resource="services/shipping.xml" />
<import resource="services/state_resolvers.xml" />
<import resource="services/taxation.xml" />
@ -52,12 +53,6 @@
<services>
<defaults public="true" />
<service id="sylius.provider.channel_based_default_zone_provider" class="Sylius\Bundle\CoreBundle\Provider\ChannelBasedDefaultTaxZoneProvider" />
<service id="Sylius\Bundle\CoreBundle\Provider\CustomerProviderInterface" class="Sylius\Bundle\CoreBundle\Provider\CustomerProvider" >
<argument type="service" id="sylius.repository.customer" />
<argument type="service" id="sylius.canonicalizer" />
</service>
<service id="sylius.integer_distributor" class="Sylius\Component\Core\Distributor\IntegerDistributor" />
<service id="Sylius\Component\Core\Distributor\IntegerDistributorInterface" alias="sylius.integer_distributor" />
@ -168,11 +163,6 @@
</service>
<service id="Sylius\Component\Core\Factory\CustomerAfterCheckoutFactoryInterface" alias="sylius.factory.customer_after_checkout" />
<service id="sylius.provider.product_variants_prices" class="Sylius\Component\Core\Provider\ProductVariantsPricesProvider">
<argument type="service" id="sylius.calculator.product_variant_price" />
</service>
<service id="Sylius\Component\Core\Provider\ProductVariantsPricesProviderInterface" alias="sylius.provider.product_variants_prices" />
<service id="sylius.twig.extension.product_variants_prices" class="Sylius\Bundle\CoreBundle\Twig\ProductVariantsPricesExtension" public="false">
<argument type="service" id="sylius.templating.helper.product_variants_prices" />
<tag name="twig.extension" />
@ -219,11 +209,6 @@
</service>
<service id="Sylius\Component\Core\Calculator\ProductVariantPriceCalculatorInterface" alias="sylius.calculator.product_variant_price" />
<service id="sylius.translation_locale_provider.admin" class="Sylius\Component\Core\Provider\TranslationLocaleProvider">
<argument type="service" id="sylius.repository.locale" />
<argument type="string">%locale%</argument>
</service>
<service id="sylius.section_resolver.uri_based_section_resolver" class="Sylius\Bundle\CoreBundle\SectionResolver\UriBasedSectionProvider">
<argument type="service" id="request_stack" />
<argument type="collection" />

View file

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<defaults public="true" />
<service id="sylius.provider.channel_based_default_zone_provider" class="Sylius\Bundle\CoreBundle\Provider\ChannelBasedDefaultTaxZoneProvider" />
<service id="Sylius\Bundle\CoreBundle\Provider\ChannelBasedProductTranslationProviderInterface" class="Sylius\Bundle\CoreBundle\Provider\ChannelBasedProductTranslationProvider">
<argument type="service" id="sylius.context.locale" />
</service>
<service id="Sylius\Bundle\CoreBundle\Provider\CustomerProviderInterface" class="Sylius\Bundle\CoreBundle\Provider\CustomerProvider" >
<argument type="service" id="sylius.repository.customer" />
<argument type="service" id="sylius.canonicalizer" />
</service>
<service id="sylius.provider.product_variants_prices" class="Sylius\Component\Core\Provider\ProductVariantsPricesProvider">
<argument type="service" id="sylius.calculator.product_variant_price" />
</service>
<service id="Sylius\Component\Core\Provider\ProductVariantsPricesProviderInterface" alias="sylius.provider.product_variants_prices" />
<service id="sylius.translation_locale_provider.admin" class="Sylius\Component\Core\Provider\TranslationLocaleProvider">
<argument type="service" id="sylius.repository.locale" />
<argument type="string">%locale%</argument>
</service>
</services>
</container>

View file

@ -57,5 +57,10 @@
<argument>%sylius.unsecured_urls%</argument>
<tag name="twig.extension" />
</service>
<service id="sylius.twig.extension.product_translation" class="Sylius\Bundle\CoreBundle\Twig\ProductTranslationExtension" public="false">
<argument type="service" id="Sylius\Bundle\CoreBundle\Provider\ChannelBasedProductTranslationProviderInterface" />
<tag name="twig.extension" />
</service>
</services>
</container>

View file

@ -0,0 +1,143 @@
<?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\CoreBundle\Tests\Provider;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Sylius\Bundle\CoreBundle\Provider\ChannelBasedProductTranslationProvider;
use Sylius\Bundle\CoreBundle\Provider\ChannelBasedProductTranslationProviderInterface;
use Sylius\Component\Core\Model\Channel;
use Sylius\Component\Core\Model\Product;
use Sylius\Component\Core\Model\ProductInterface;
use Sylius\Component\Core\Model\ProductTranslation;
use Sylius\Component\Core\Model\ProductTranslationInterface;
use Sylius\Component\Locale\Context\LocaleContextInterface;
use Sylius\Component\Locale\Model\Locale;
final class ChannelBasedProductTranslationProviderTest extends TestCase
{
private LocaleContextInterface|MockObject $localeContext;
protected function setUp(): void
{
$this->localeContext = $this->createMock(LocaleContextInterface::class);
}
/** @test */
public function it_provides_product_translation_using_administrator_locale(): void
{
$product = new Product();
$product->addTranslation($this->createProductTranslation($product, 'pl_PL', 'polish-product-slug'));
$product->addTranslation($this->createProductTranslation($product, 'en_US', 'english-product-slug'));
$channelLocale = new Locale();
$channelLocale->setCode('en_US');
$channel = new Channel();
$channel->setDefaultLocale($channelLocale);
$this->localeContext->expects($this->once())->method('getLocaleCode')->willReturn('pl_PL');
$provider = $this->createProvider();
$productTranslation = $provider->provide($product, $channel);
$this->assertSame('polish-product-slug', $productTranslation->getSlug());
$this->assertSame('pl_PL', $productTranslation->getLocale());
}
/** @test */
public function it_generates_url_using_channel_default_locale_translations(): void
{
$product = new Product();
$product->addTranslation($this->createProductTranslation($product, 'pl_PL', ''));
$product->addTranslation($this->createProductTranslation($product, 'en_US', 'english-product-slug'));
$channelLocale = new Locale();
$channelLocale->setCode('en_US');
$channel = new Channel();
$channel->setDefaultLocale($channelLocale);
$this->localeContext->expects($this->once())->method('getLocaleCode')->willReturn('de_DE');
$provider = $this->createProvider();
$productTranslation = $provider->provide($product, $channel);
$this->assertSame('english-product-slug', $productTranslation->getSlug());
$this->assertSame('en_US', $productTranslation->getLocale());
}
/** @test */
public function it_generates_url_using_any_translation_enabled_in_channel_if_administrator_and_channel_default_translations_does_not_exist(): void
{
$product = new Product();
$product->addTranslation($this->createProductTranslation($product, 'pl_PL', ''));
$product->addTranslation($this->createProductTranslation($product, 'en_US', ''));
$product->addTranslation($this->createProductTranslation($product, 'de_DE', 'german-product-slug'));
$product->addTranslation($this->createProductTranslation($product, 'fr_FR', 'french-product-slug'));
$enUsLocale = new Locale();
$enUsLocale->setCode('en_US');
$deDeLocale = new Locale();
$deDeLocale->setCode('de_DE');
$channel = new Channel();
$channel->setDefaultLocale($enUsLocale);
$channel->addLocale($enUsLocale);
$channel->addLocale($deDeLocale);
$this->localeContext->expects($this->once())->method('getLocaleCode')->willReturn('es_ES');
$provider = $this->createProvider();
$productTranslation = $provider->provide($product, $channel);
$this->assertSame('german-product-slug', $productTranslation->getSlug());
$this->assertSame('de_DE', $productTranslation->getLocale());
}
/** @test */
public function it_generates_null_when_no_translations_available(): void
{
$product = new Product();
$channelLocale = new Locale();
$channelLocale->setCode('en_US');
$channel = new Channel();
$channel->setDefaultLocale($channelLocale);
$this->localeContext->expects($this->once())->method('getLocaleCode')->willReturn('fr_FR');
$provider = $this->createProvider();
$productTranslation = $provider->provide($product, $channel);
$this->assertNull($productTranslation);
}
private function createProvider(): ChannelBasedProductTranslationProviderInterface
{
return new ChannelBasedProductTranslationProvider($this->localeContext);
}
private function createProductTranslation(ProductInterface $product, string $localeCode, string $slug = ''): ProductTranslationInterface
{
$productTranslation = new ProductTranslation();
$productTranslation->setTranslatable($product);
$productTranslation->setLocale($localeCode);
$productTranslation->setSlug($slug);
return $productTranslation;
}
}

View file

@ -0,0 +1,33 @@
<?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\CoreBundle\Twig;
use Sylius\Bundle\CoreBundle\Provider\ChannelBasedProductTranslationProviderInterface;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
final class ProductTranslationExtension extends AbstractExtension
{
public function __construct(
private ChannelBasedProductTranslationProviderInterface $channelBasedProductTranslationProvider,
) {
}
public function getFunctions(): array
{
return [
new TwigFunction('sylius_product_translation', [$this->channelBasedProductTranslationProvider, 'provide']),
];
}
}

View file

@ -39,10 +39,10 @@ final class DefaultChannelFactory implements DefaultChannelFactoryInterface
) {
}
public function create(?string $code = null, ?string $name = null, ?string $currencyCode = null): array
public function create(?string $code = null, ?string $name = null, ?string $currencyCode = null, ?string $localeCode = null): array
{
$currency = $this->provideCurrency($currencyCode);
$locale = $this->provideLocale();
$locale = $this->provideLocale($localeCode);
/** @var ChannelInterface $channel */
$channel = $this->channelFactory->createNamed($name ?: self::DEFAULT_CHANNEL_NAME);
@ -82,7 +82,7 @@ final class DefaultChannelFactory implements DefaultChannelFactoryInterface
return $currency;
}
private function provideLocale(): LocaleInterface
private function provideLocale(?string $localeCode = null): LocaleInterface
{
/** @var LocaleInterface|null $locale */
$locale = $this->localeRepository->findOneBy(['code' => $this->defaultLocaleCode]);
@ -90,7 +90,7 @@ final class DefaultChannelFactory implements DefaultChannelFactoryInterface
if (null === $locale) {
/** @var LocaleInterface $locale */
$locale = $this->localeFactory->createNew();
$locale->setCode($this->defaultLocaleCode);
$locale->setCode($localeCode ?? $this->defaultLocaleCode);
$this->localeRepository->add($locale);
}

View file

@ -15,5 +15,10 @@ namespace Sylius\Component\Core\Test\Services;
interface DefaultChannelFactoryInterface
{
public function create(?string $code = null, ?string $name = null, ?string $currencyCode = null): array;
public function create(
?string $code = null,
?string $name = null,
?string $currencyCode = null,
?string $localeCode = null,
): array;
}

View file

@ -52,7 +52,7 @@ final class DefaultUnitedStatesChannelFactory implements DefaultChannelFactoryIn
) {
}
public function create(?string $code = null, ?string $name = null, ?string $currencyCode = null): array
public function create(?string $code = null, ?string $name = null, ?string $currencyCode = null, ?string $localeCode = null): array
{
$currency = $this->provideCurrency($currencyCode);
$locale = $this->provideLocale();