Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[AttributeValue][Product] Translatable attribute values#7219

Merged
pjedrzejewski merged 10 commits into
Sylius:masterfrom
GSadee:translatable-attribute-value
Jan 18, 2017
Merged

[AttributeValue][Product] Translatable attribute values#7219
pjedrzejewski merged 10 commits into
Sylius:masterfrom
GSadee:translatable-attribute-value

Conversation

@GSadee
Copy link
Copy Markdown
Member

@GSadee GSadee commented Jan 9, 2017

Q A
Bug fix? no
New feature? yes
BC breaks? yes
Related tickets
License MIT

@GSadee GSadee force-pushed the translatable-attribute-value branch from 5248da2 to 2a3884c Compare January 16, 2017 07:16
@GSadee GSadee force-pushed the translatable-attribute-value branch 2 times, most recently from b23149d to b2b764c Compare January 17, 2017 07:14
@GSadee GSadee changed the title [WIP][AttributeValue] Translatable attribute values [AttributeValue][Product] Translatable attribute values Jan 17, 2017
Comment thread UPGRADE.md Outdated

### Attribute / AttributeBundle

* `AttributeValue::$localeCode` property was added to make it translatable. Now, every attribute value has a locale code to be properly displaying in different locales. All attribute values are migrated to new concept with migration `Version20170109143010`. Look at [this PR](https://github.com/Sylius/Sylius/pull/7219) if you have any problems with upgrade.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to be displayed properly in different locales; to the new concept;

When I switch to the "Polish (Poland)" locale
When I view product "T-shirt banana"
Then I should see the product attribute "T-shirt material" with value "Skórka banana"
Then I should also see the product attribute "T-shirt details" with value "Banana is very good material."
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Banana is a very good material."

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Banana is a fruit, not a material :( 🍌

@GSadee GSadee force-pushed the translatable-attribute-value branch from bd4155b to ec752f3 Compare January 17, 2017 09:21
* {@inheritdoc}
*/
public function hasAttributeByCode($attributeCode)
public function hasAttributeByCodeAndLocale($attributeCode, $localeCode)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could perhaps improve the DX here a bit and make the 2nd parameter optional. If not provided, the entity would use the currentLocale stored on it at the moment of call. WDYT?

$defaultLocale = $this->container->getParameter('locale');

$this->addSql('ALTER TABLE sylius_product_attribute_value ADD locale_code VARCHAR(255) NOT NULL');
$this->addSql('UPDATE sylius_product_attribute_value SET locale_code = "'.$defaultLocale.'"');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just use SET DEFAULT in the ALTER clause instead of this?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would add this default locale to the column definition in the DB. I'd prefer to avoid that.

$defaultLocale = $this->container->getParameter('locale');

$this->addSql('ALTER TABLE sylius_product_attribute_value ADD locale_code VARCHAR(255) NOT NULL');
$this->addSql('UPDATE sylius_product_attribute_value SET locale_code = "'.$defaultLocale.'"');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure it works? The first SQL query will probably fail if there are any records in the database.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I am sure :tested:

And the store has a product "T-shirt banana"
And this product has text attribute "T-shirt material" with value "Banana skin" in "English (United States)" locale
And this product has text attribute "T-shirt material" with value "Skórka banana" in "Polish (Poland)" locale
And this product has textarea attribute "T-shirt details" with value "Banana is a very good material." in "English (United States)" locale
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it needed?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, because I want to test if using a fallback locale when attribute has no value in current locale is working correctly ; )


/**
* @Given /^(this product) has ([^"]+) attribute "([^"]+)" with value "([^"]+)"$/
* @Given /^(this product) has ([^"]+) attribute "([^"]+)" with value "([^"]+)" in ("[^"]+" locale)$/
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

([^"]+) attribute -> (.+?) attribute

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why? What is the difference?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Matching every character except " does not make any sense here, let's match all the characters instead.

* @param string|null $code
*
* @return ProductAttributeInterface
*/
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getOrCreate -> provide?

sprintf('ProductAttribute "%s" should have value "%s" but it does not.', $attribute, $value)
$this->updateSimpleProductPage->getAttributeValue($attributeName, $language),
sprintf(
'Product attribute "%s" should have value "%s" in language "%s" but it does not.',
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but it does not

* @param string[] $localeCodes
*
* @return FormView
* @return array
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FormView[]

@GSadee GSadee force-pushed the translatable-attribute-value branch from ec752f3 to dea6b21 Compare January 17, 2017 14:58
@GSadee GSadee force-pushed the translatable-attribute-value branch from dea6b21 to 111f9ce Compare January 18, 2017 07:34
{
$attribute = $this->createProductAttribute($productAttributeType, $productAttributeName);
$attributeValue = $this->createProductAttributeValue($value, $attribute);
public function thisProductHasAttributeWithValue(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it be worthy to extract ProductAttributes to separate context?

* @return ProductAttributeInterface
*/
private function provideProductAttribute($type, $name, $code = null)
{
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$code = $code?:StringInflector::nameToCode($name);

if (null !== $productAttribute) {
return $productAttribute;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure about it. Shouldn't we create an attribute before first usage of it?

public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('localeCode')
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't it be

->add('localeCode', LocaleChoiceType::class, [
    'constraints' => [
        new NotBlank(['groups' => ['sylius']]),
    ],
])

And then we will use a transformer:

$builder->get('localeCode')->addModelTransformer(
    new ReversedTransformer(new ResourceToIdentifierTransformer($this->localeRepository, 'code'))
);

With this solution we will be sure, that locale exist in the repository

);

foreach ($attributes as $attribute) {
foreach ($localeCodes as $localeCode) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we extract this foreach at least to another private method?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any idea? When I extract this to another method, this method will have to return array and then I will have to merge arrays.. I know that current solution isn't clear, but I have no simple idea how to do it clearer.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

    /**
     * @param FormEvent $event
     */
    public function preSetData(FormEvent $event)
    {
        /** @var ProductInterface $product */
        $product = $event->getData();

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

        $attributes = $product->getAttributes()->filter(
            function (ProductAttributeValueInterface $attribute) use ($defaultLocaleCode) {
                return $attribute->getLocaleCode() === $defaultLocaleCode;
            }
        );

        foreach ($attributes as $attribute) {
            $this->resolveLocalizedAttributes($localeCodes, $product, $attribute);
        }
    }

    /**
     * @param array $localeCodes
     * @param ProductInterface $product
     * @param ProductAttributeInterface $productAttribute
     */
    private function resolveLocalizedAttributes(array $localeCodes, ProductInterface $product, ProductAttributeInterface $productAttribute)
    {
        foreach ($localeCodes as $localeCode) {
            if (!$product->hasAttributeByCodeAndLocale($productAttribute->getCode(), $localeCode)) {
                $attributeValue = $this->createProductAttributeValue($productAttribute->getAttribute(), $localeCode);
                $product->addAttribute($attributeValue);
            }
        }
    }

or

    /**
     * @param FormEvent $event
     */
    public function preSetData(FormEvent $event)
    {
        /** @var ProductInterface $product */
        $product = $event->getData();

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

        $attributes = $product->getAttributes()->filter(
            function (ProductAttributeValueInterface $attribute) use ($defaultLocaleCode) {
                return $attribute->getLocaleCode() === $defaultLocaleCode;
            }
        );

        foreach ($attributes as $attribute) {
            $this->resolveLocalizedAttributes($product, $attribute);
        }
    }

    /**
     * @param ProductInterface $product
     * @param ProductAttributeInterface $productAttribute
     */
    private function resolveLocalizedAttributes(ProductInterface $product, ProductAttributeInterface $productAttribute)
    {
        $localeCodes = $this->localeProvider->getDefinedLocalesCodes();

        foreach ($localeCodes as $localeCode) {
            if (!$product->hasAttributeByCodeAndLocale($productAttribute->getCode(), $localeCode)) {
                $attributeValue = $this->createProductAttributeValue($productAttribute->getAttribute(), $localeCode);
                $product->addAttribute($attributeValue);
            }
        }
    }

Anyway, we should add Assert::isInstanceOf(ProductInterface) just after $product = $event->getData();

if ($attribute->getAttribute()->getCode() === $attributeCode) {
if ($attribute->getAttribute()->getCode() === $attributeCode &&
$attribute->getLocaleCode() === $localeCode) {
return true;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing blank line

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't notice that the if above has and... My bad. Anyway, the if above will fit one line. And also we have to decide for some convention with multiple conditions in on if statement. Here you have && at the end of first line, and a few lines below the || is at the beginning of a second one...

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 for convention

foreach ($this->attributes as $attribute) {
if ($attributeCode === $attribute->getAttribute()->getCode()) {
if ($attribute->getAttribute()->getCode() === $attributeCode &&
$attribute->getLocaleCode() === $localeCode) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing blank line

public function getAttributeByCode($attributeCode)
public function getAttributeByCodeAndLocale($attributeCode, $localeCode = null)
{
if (null === $localeCode) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$localeCode = $localeCode:?$this->getTranslation()->getLocale();


$attributeValueInDifferentLocale = $this->getAttributeByCodeAndLocale($attributeValue->getCode(), $localeCode);
if ('' === $attributeValueInDifferentLocale->getValue()
|| null === $attributeValueInDifferentLocale->getValue()) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

empty($attributeValueInDifferentLocale->getValue()) will be equivalent to this if. And we will remove an or condition

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

empty('0') === true, so I guess it won't.


$attributeValueInDifferentLocale = $this->getAttributeByCodeAndLocale($attributeValue->getCode(), $localeCode);
if ('' === $attributeValueInDifferentLocale->getValue()
|| null === $attributeValueInDifferentLocale->getValue()) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

empty($attributeValueInDifferentLocale->getValue()) will be equivalent to this if. And we will remove an or condition

Copy link
Copy Markdown
Contributor

@pamil pamil Jan 18, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

empty('0') === true, so I guess it won't.

@pjedrzejewski pjedrzejewski merged commit 5008336 into Sylius:master Jan 18, 2017
@pjedrzejewski
Copy link
Copy Markdown
Contributor

Nice work Grzesiu, thank you! Apply @lchrusciel's comments in a separate PR if you agree, please.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants