Merge pull request #6713 from lchrusciel/product-variant-generation

[Product] Product variant generation
This commit is contained in:
Paweł Jędrzejewski 2016-11-16 11:25:37 +01:00 committed by GitHub
commit 13c74f2ae2
29 changed files with 750 additions and 110 deletions

View file

@ -0,0 +1,47 @@
@managing_product_variants
Feature: Generating product variants
In order to sell different variations of a single product
As an Administrator
I want to generate all possible product variants
Background:
Given the store is available in "English (United States)"
And the store has a "Wyborowa Vodka" configurable product
And this product has option "Taste" with values "Orange" and "Melon"
And I am logged in as an administrator
@ui
Scenario: Generating a product variant for product without variants
Given I want to generate new variants for this product
When I specify that the 1st variant is identified by "WYBOROWA_ORANGE" code and costs "$90"
And I specify that the 2nd variant is identified by "WYBOROWA_MELON" code and costs "$95"
And I generate it
Then I should be notified that it has been successfully generated
And I should see 2 variants in the list
@ui
Scenario: Generating the rest of product variants for product with at least one
Given this product is available in "Melon" taste priced at "$95.00"
And I want to generate new variants for this product
When I specify that the 2nd variant is identified by "WYBOROWA_ORANGE" code and costs "$90"
And I generate it
Then I should be notified that it has been successfully generated
And I should see 2 variants in the list
@ui
Scenario: Generating the rest of product variants for product with at least one
Given this product is available in "Orange" taste priced at "$90.00"
And I want to generate new variants for this product
When I specify that the 2nd variant is identified by "WYBOROWA_MELON" code and costs "$95"
And I generate it
Then I should be notified that it has been successfully generated
And I should see 2 variants in the list
@ui @javascript
Scenario: Generating only a part of product variants
Given I want to generate new variants for this product
When I specify that the 1st variant is identified by "WYBOROWA_ORANGE" code and costs "$90"
And I remove 2nd variant from the list
And I generate it
Then I should be notified that it has been successfully generated
And I should see 1 variants in the list

View file

@ -0,0 +1,27 @@
@managing_product_variants
Feature: Generating product variant generation
In order to avoid making mistakes when generating variants
As an Administrator
I want to be prevented from generating it without specifying required fields
Background:
Given the store is available in "English (United States)"
And the store has a "Wyborowa Vodka" configurable product
And this product has option "Taste" with values "Orange" and "Melon"
And I am logged in as an administrator
@ui
Scenario: Generating a product variant without price
Given I want to generate new variants for this product
When I specify that the 1st variant is identified by "WYBOROWA_ORANGE" code
And I try to generate it
Then I should be notified that price is required for the 1st variant
And I should see 0 variants in the list
@ui
Scenario: Generating a product variant without code
Given I want to generate new variants for this product
When I specify that the 1st variant costs "$90"
And I try to generate it
Then I should be notified that code is required for the 1st variant
And I should see 0 variants in the list

View file

@ -14,6 +14,7 @@ namespace Sylius\Behat\Context\Ui\Admin;
use Behat\Behat\Context\Context;
use Sylius\Behat\NotificationType;
use Sylius\Behat\Page\Admin\ProductVariant\CreatePageInterface;
use Sylius\Behat\Page\Admin\ProductVariant\GeneratePageInterface;
use Sylius\Behat\Page\Admin\ProductVariant\IndexPageInterface;
use Sylius\Behat\Page\Admin\ProductVariant\UpdatePageInterface;
use Sylius\Behat\Service\NotificationCheckerInterface;
@ -56,6 +57,11 @@ final class ManagingProductVariantsContext implements Context
*/
private $updatePage;
/**
* @var GeneratePageInterface
*/
private $generatePage;
/**
* @var CurrentPageResolverInterface
*/
@ -72,6 +78,7 @@ final class ManagingProductVariantsContext implements Context
* @param CreatePageInterface $createPage
* @param IndexPageInterface $indexPage
* @param UpdatePageInterface $updatePage
* @param GeneratePageInterface $generatePage
* @param CurrentPageResolverInterface $currentPageResolver
* @param NotificationCheckerInterface $notificationChecker
*/
@ -81,6 +88,7 @@ final class ManagingProductVariantsContext implements Context
CreatePageInterface $createPage,
IndexPageInterface $indexPage,
UpdatePageInterface $updatePage,
GeneratePageInterface $generatePage,
CurrentPageResolverInterface $currentPageResolver,
NotificationCheckerInterface $notificationChecker
) {
@ -89,6 +97,7 @@ final class ManagingProductVariantsContext implements Context
$this->createPage = $createPage;
$this->indexPage = $indexPage;
$this->updatePage = $updatePage;
$this->generatePage = $generatePage;
$this->currentPageResolver = $currentPageResolver;
$this->notificationChecker = $notificationChecker;
}
@ -209,7 +218,7 @@ final class ManagingProductVariantsContext implements Context
* @Then I should see :numberOfProductVariants variants in the list
* @Then I should see :numberOfProductVariants variant in the list
*/
public function iShouldSeeProductsInTheList($numberOfProductVariants)
public function iShouldSeeProductVariantsInTheList($numberOfProductVariants)
{
$foundRows = $this->indexPage->countItems();
@ -291,6 +300,22 @@ final class ManagingProductVariantsContext implements Context
$this->assertValidationMessage('price', 'Please enter the price.');
}
/**
* @Then /^I should be notified that price is required for the (\d)(?:st|nd|rd|th) variant$/
*/
public function iShouldBeNotifiedThatPriceIsRequiredForVariant($position)
{
Assert::same($this->generatePage->getValidationMessage('price', $position), 'Please enter the price.');
}
/**
* @Then /^I should be notified that code is required for the (\d)(?:st|nd|rd|th) variant$/
*/
public function iShouldBeNotifiedThatCodeIsRequiredForVariant($position)
{
Assert::same($this->generatePage->getValidationMessage('code', $position), 'Please enter variant code.');
}
/**
* @When I save my changes
* @When I try to save my changes
@ -467,6 +492,64 @@ final class ManagingProductVariantsContext implements Context
);
}
/**
* @Given /^I want to generate new variants for (this product)$/
*/
public function iWantToGenerateNewVariantsForThisProduct(ProductInterface $product)
{
$this->generatePage->open(['productId' => $product->getId()]);
}
/**
* @When I generate it
* @When I try to generate it
*/
public function iClickGenerate()
{
$this->generatePage->generate();
}
/**
* @When /^I specify that the (\d)(?:st|nd|rd|th) variant is identified by ("[^"]+") code and costs "(?:|£|\$)([^"]+)"$/
*/
public function iSpecifyThereAreVariantsIdentifiedByCodeWithCost($nthVariant, $code, $price)
{
$this->generatePage->nameCode($nthVariant - 1, $code);
$this->generatePage->specifyPrice($nthVariant - 1, $price);
}
/**
* @When /^I specify that the (\d)(?:st|nd|rd|th) variant is identified by ("[^"]+") code$/
*/
public function iSpecifyThereAreVariantsIdentifiedByCode($nthVariant, $code)
{
$this->generatePage->nameCode($nthVariant, $code - 1);
}
/**
* @When /^I specify that the (\d)(?:st|nd|rd|th) variant costs "(?:€|£|\$)([^"]+)"$/
*/
public function iSpecifyThereAreVariantsWithCost($nthVariant, $price)
{
$this->generatePage->specifyPrice($nthVariant, $price - 1);
}
/**
* @When /^I remove (\d)(?:st|nd|rd|th) variant from the list$/
*/
public function iRemoveVariantFromTheList($nthVariant)
{
$this->generatePage->removeVariant($nthVariant - 1);
}
/**
* @Then I should be notified that it has been successfully generated
*/
public function iShouldBeNotifiedThatItHasBeenSuccessfullyGenerated()
{
$this->notificationChecker->checkNotification('Success Product variants have been successfully generated.', NotificationType::success());
}
/**
* @param string $element
* @param $message

View file

@ -0,0 +1,104 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Paweł Jędrzejewski
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Sylius\Behat\Page\Admin\ProductVariant;
use Behat\Mink\Element\NodeElement;
use Behat\Mink\Exception\ElementNotFoundException;
use Sylius\Behat\Page\SymfonyPage;
/**
* @author Łukasz Chruściel <lukasz.chrusciel@lakion.com>
*/
class GeneratePage extends SymfonyPage implements GeneratePageInterface
{
public function generate()
{
$this->getDocument()->pressButton('Generate');
}
/**
* {@inheritdoc}
*/
public function specifyPrice($nth, $price)
{
$this->getDocument()->fillField(sprintf('sylius_product_generate_variants_variants_%s_price', $nth), $price);
}
/**
* {@inheritdoc}
*/
public function nameCode($nth, $code)
{
$this->getDocument()->fillField(sprintf('sylius_product_generate_variants_variants_%s_code', $nth), $code);
}
/**
* {@inheritdoc}
*/
public function removeVariant($nth)
{
$item = $this->getDocument()->find('css', sprintf('div[data-form-collection-index="%s"]', $nth));
$item->clickLink('Delete');
}
/**
* {@inheritdoc}
*/
public function getRouteName()
{
return 'sylius_admin_product_variant_generate';
}
/**
* {@inheritdoc}
*
* @throws ElementNotFoundException
*/
public function getValidationMessage($element, $position)
{
$foundElement = $this->getElement($element, ['%position%' => $position]);
$validatedField = $this->getValidatedField($foundElement);
if (null === $validatedField) {
throw new ElementNotFoundException($this->getSession(), 'Element', 'css', $foundElement);
}
return $validatedField->find('css', '.sylius-validation-error')->getText();
}
/**
* {@inheritdoc}
*/
protected function getDefinedElements()
{
return array_merge(parent::getDefinedElements(), [
'code' => '#sylius_product_generate_variants_variants_%position%_code',
'price' => '#sylius_product_generate_variants_variants_%position%_price',
]);
}
/**
* @param NodeElement $element
*
* @return NodeElement
*
* @throws ElementNotFoundException
*/
private function getValidatedField(NodeElement $element)
{
while (null !== $element && !$element->hasClass('field')) {
$element = $element->getParent();
}
return $element;
}
}

View file

@ -0,0 +1,47 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Paweł Jędrzejewski
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Sylius\Behat\Page\Admin\ProductVariant;
use Sylius\Behat\Page\SymfonyPageInterface;
/**
* @author Łukasz Chruściel <lukasz.chrusciel@lakion.com>
*/
interface GeneratePageInterface extends SymfonyPageInterface
{
public function generate();
/**
* @param int $nth
* @param int $price
*/
public function specifyPrice($nth, $price);
/**
* @param int $nth
* @param string $code
*/
public function nameCode($nth, $code);
/**
* @param int $nth
*/
public function removeVariant($nth);
/**
* @param string $element
* @param int $position
*
* @return string
*/
public function getValidationMessage($element, $position);
}

View file

@ -157,6 +157,7 @@
<argument type="service" id="sylius.behat.page.admin.product_variant.create" />
<argument type="service" id="sylius.behat.page.admin.product_variant.index" />
<argument type="service" id="sylius.behat.page.admin.product_variant.update" />
<argument type="service" id="sylius.behat.page.admin.product_variant.generate" />
<argument type="service" id="sylius.behat.current_page_resolver" />
<argument type="service" id="sylius.behat.notification_checker" />
<tag name="fob.context_service" />

View file

@ -14,6 +14,8 @@
<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">
<parameters>
<parameter key="sylius.behat.page.admin.product_variant.create.class">Sylius\Behat\Page\Admin\ProductVariant\CreatePage</parameter>
<parameter key="sylius.behat.page.admin.product_variant.generate.class">Sylius\Behat\Page\Admin\ProductVariant\GeneratePage</parameter>
<parameter key="sylius.behat.page.admin.product_variant.index.class">Sylius\Behat\Page\Admin\ProductVariant\IndexPage</parameter>
<parameter key="sylius.behat.page.admin.product_variant.update.class">Sylius\Behat\Page\Admin\ProductVariant\UpdatePage</parameter>
</parameters>
@ -21,7 +23,10 @@
<service id="sylius.behat.page.admin.product_variant.create" class="%sylius.behat.page.admin.product_variant.create.class%" parent="sylius.behat.page.admin.crud.create" public="false">
<argument type="string">product_variant</argument>
</service>
<service id="sylius.behat.page.admin.product_variant.index" class="Sylius\Behat\Page\Admin\ProductVariant\IndexPage" parent="sylius.behat.page.admin.crud.index" public="false">
<service id="sylius.behat.page.admin.product_variant.generate" class="%sylius.behat.page.admin.product_variant.generate.class%" parent="sylius.behat.symfony_page" public="false">
<argument type="string">product_variant</argument>
</service>
<service id="sylius.behat.page.admin.product_variant.index" class="%sylius.behat.page.admin.product_variant.index.class%" parent="sylius.behat.page.admin.crud.index" public="false">
<argument type="string">product_variant</argument>
</service>
<service id="sylius.behat.page.admin.product_variant.update" class="%sylius.behat.page.admin.product_variant.update.class%" parent="sylius.behat.page.admin.crud.update" public="false">

View file

@ -73,3 +73,21 @@ sylius_admin_product_variant_delete:
_sylius:
redirect: referer
permission: true
sylius_admin_product_variant_generate:
path: /generate
methods: [GET, POST]
defaults:
_controller: sylius.controller.product:updateAction
_sylius:
template: "@SyliusAdmin/ProductVariant/generate.html.twig"
section: admin
redirect:
route: sylius_admin_product_variant_index
parameters: { productId: $productId }
form:
type: sylius_product_generate_variants
repository:
method: find
arguments: [$productId]
flash: sylius.product_variant.generation_completed

View file

@ -0,0 +1,12 @@
{% import '@SyliusAdmin/Macro/breadcrumb.html.twig' as breadcrumb %}
{% set breadcrumbs = [
{ label: 'sylius.ui.administration'|trans, url: path('sylius_admin_dashboard') },
{ label: 'sylius.ui.products'|trans, url: path('sylius_admin_product_index') },
{ label: product.name, url: path('sylius_admin_product_update', {'id': product.id}) },
{ label: 'sylius.ui.variants'|trans, url: path('sylius_admin_product_variant_index', {'productId': product.id}) },
{ label: 'sylius.ui.generate'|trans }
]
%}
{{ breadcrumb.crumble(breadcrumbs) }}

View file

@ -0,0 +1,5 @@
<div class="ui grid">
<div class="column">
{{- form_row(form.variants) -}}
</div>
</div>

View file

@ -0,0 +1,29 @@
{% extends '@SyliusAdmin/layout.html.twig' %}
{% import 'SyliusUiBundle:Macro:headers.html.twig' as headers %}
{% set header = 'sylius.ui.generate_variants' %}
{% block title %}{{ parent() }} {{ header|trans }}{% endblock %}
{% form_theme form 'SyliusUiBundle:Form:theme.html.twig' %}
{% block content %}
{{ headers.default(header|trans, 'random', 'sylius.ui.manage_products'|trans) }}
{% include '@SyliusAdmin/ProductVariant/Generate/_breadcrumb.html.twig' %}
{{ form_start(form, {'action': path('sylius_admin_product_variant_generate', {'productId': product.id}), 'attr': {'class': 'ui loadable form', 'novalidate': 'novalidate'}}) }}
<div class="ui segment">
{% include '@SyliusAdmin/ProductVariant/Generate/_form.html.twig' %}
<div class="ui basic segment">
<div class="ui buttons">
<button class="ui labeled icon primary button" type="submit"><i class="random icon"></i>{{- 'sylius.ui.generate'|trans -}}</button>
<div class="or" data-text="{{ 'sylius.ui.or'|trans }}"></div>
{% include '@SyliusUi/Form/Buttons/_cancel.html.twig' with {'path': path(configuration.getRouteName('index'), {'productId': product.id})} %}
</div>
</div>
</div>
{{ form_row(form._token) }}
{{ form_end(form, {'render_rest': false}) }}
{% endblock %}

View file

@ -0,0 +1,35 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Paweł Jędrzejewski
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Sylius\Bundle\CoreBundle\Form\Type\Product;
use Sylius\Bundle\ProductBundle\Form\Type\ProductVariantGenerationType as BaseProductVariantGenerationType;
use Symfony\Component\Form\FormBuilderInterface;
/**
* @author Łukasz Chruściel <lukasz.chrusciel@lakion.com>
*/
class ProductVariantGenerationType extends BaseProductVariantGenerationType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
parent::buildForm($builder, $options);
$builder
->add('price', 'sylius_money', [
'label' => 'sylius.form.variant.price',
])
;
}
}

View file

@ -20,6 +20,7 @@ sylius_product:
repository: Sylius\Bundle\ProductBundle\Doctrine\ORM\ProductVariantRepository
form:
default: Sylius\Bundle\CoreBundle\Form\Type\Product\ProductVariantType
generation: Sylius\Bundle\CoreBundle\Form\Type\Product\ProductVariantGenerationType
product_option:
classes:
repository: Sylius\Bundle\ProductBundle\Doctrine\ORM\ProductOptionRepository

View file

@ -16,3 +16,5 @@ sylius:
failed: Your payment was declined.
unknown: Unknown payment error.
out_of_stock: There is only %quantity% %name% in the stock.
product_variant:
generation_completed: Product variants have been successfully generated.

View file

@ -19,6 +19,7 @@ use Sylius\Bundle\ProductBundle\Form\Type\ProductOptionValueTranslationType;
use Sylius\Bundle\ProductBundle\Form\Type\ProductOptionValueType;
use Sylius\Bundle\ProductBundle\Form\Type\ProductTranslationType;
use Sylius\Bundle\ProductBundle\Form\Type\ProductType;
use Sylius\Bundle\ProductBundle\Form\Type\ProductGenerateVariantsType;
use Sylius\Bundle\ProductBundle\Form\Type\ProductVariantGenerationType;
use Sylius\Bundle\ProductBundle\Form\Type\ProductVariantType;
use Sylius\Bundle\ResourceBundle\Controller\ResourceController;
@ -103,7 +104,7 @@ final class Configuration implements ConfigurationInterface
->addDefaultsIfNotSet()
->children()
->scalarNode('default')->defaultValue(ProductType::class)->cannotBeEmpty()->end()
->scalarNode('variant_generation')->defaultValue(ProductVariantGenerationType::class)->cannotBeEmpty()->end()
->scalarNode('generate_variants')->defaultValue(ProductGenerateVariantsType::class)->cannotBeEmpty()->end()
->scalarNode('choice')->defaultValue(ResourceChoiceType::class)->cannotBeEmpty()->end()
->scalarNode('from_identifier')->defaultValue(ResourceFromIdentifierType::class)->cannotBeEmpty()->end()
->end()
@ -118,7 +119,7 @@ final class Configuration implements ConfigurationInterface
->defaultValue(['sylius'])
->cannotBeEmpty()
->end()
->arrayNode('variant_generation')
->arrayNode('generate_variants')
->prototype('scalar')->end()
->defaultValue(['sylius'])
->cannotBeEmpty()
@ -175,6 +176,7 @@ final class Configuration implements ConfigurationInterface
->addDefaultsIfNotSet()
->children()
->scalarNode('default')->defaultValue(ProductVariantType::class)->cannotBeEmpty()->end()
->scalarNode('generation')->defaultValue(ProductVariantGenerationType::class)->cannotBeEmpty()->end()
->scalarNode('from_identifier')->defaultValue(ResourceFromIdentifierType::class)->cannotBeEmpty()->end()
->end()
->end()

View file

@ -43,7 +43,7 @@ final class SyliusProductExtension extends AbstractResourceExtension implements
$loader->load('services.xml');
$formDefinition = $container->getDefinition('sylius.form.type.product_variant_generation');
$formDefinition = $container->getDefinition('sylius.form.type.product_generate_variants');
$formDefinition->addArgument($container->getDefinition('sylius.form.event_subscriber.product_variant_generator'));
$container->getDefinition('sylius.form.type.product')->addArgument(new Reference('sylius.product_variant_resolver.default'));

View file

@ -19,6 +19,7 @@ use Symfony\Component\Form\FormFactoryInterface;
/**
* @author Paweł Jędrzejewski <pawel@sylius.org>
* @author Łukasz Chruściel <lukasz.chrusciel@lakion.com>
*/
final class BuildProductVariantFormSubscriber implements EventSubscriberInterface
{
@ -28,11 +29,18 @@ final class BuildProductVariantFormSubscriber implements EventSubscriberInterfac
private $factory;
/**
* @param FormFactoryInterface $factory
* @var bool
*/
public function __construct(FormFactoryInterface $factory)
private $disabled;
/**
* @param FormFactoryInterface $factory
* @param bool $disabled
*/
public function __construct(FormFactoryInterface $factory, $disabled = false)
{
$this->factory = $factory;
$this->disabled = $disabled;
}
/**
@ -65,6 +73,7 @@ final class BuildProductVariantFormSubscriber implements EventSubscriberInterfac
}
$form->add($this->factory->createNamed('optionValues', 'sylius_product_option_value_collection', $productVariant->getOptionValues(), [
'disabled' => $this->disabled,
'options' => $product->getOptions(),
'auto_initialize' => false,
]));

View file

@ -0,0 +1,63 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Paweł Jędrzejewski
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Sylius\Bundle\ProductBundle\Form\Type;
use Sylius\Bundle\ResourceBundle\Form\Type\AbstractResourceType;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\FormBuilderInterface;
/**
* @author Łukasz Chruściel <lukasz.chrusciel@lakion.com>
*/
class ProductGenerateVariantsType extends AbstractResourceType
{
/**
* @var EventSubscriberInterface
*/
private $generateProductVariantsSubscriber;
/**
* @param string $dataClass
* @param string[] $validationGroups
* @param EventSubscriberInterface $generateProductVariants
*/
public function __construct($dataClass, $validationGroups, EventSubscriberInterface $generateProductVariants)
{
parent::__construct($dataClass, $validationGroups);
$this->generateProductVariantsSubscriber = $generateProductVariants;
}
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('variants', 'collection', [
'type' => 'sylius_product_variant_generation',
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
])
->addEventSubscriber($this->generateProductVariantsSubscriber);
;
}
/**
* {@inheritdoc}
*/
public function getName()
{
return 'sylius_product_generate_variants';
}
}

View file

@ -11,8 +11,9 @@
namespace Sylius\Bundle\ProductBundle\Form\Type;
use Sylius\Bundle\ResourceBundle\Form\EventSubscriber\AddCodeFormSubscriber;
use Sylius\Bundle\ResourceBundle\Form\Type\AbstractResourceType;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Sylius\Bundle\ProductBundle\Form\EventSubscriber\BuildProductVariantFormSubscriber;
use Symfony\Component\Form\FormBuilderInterface;
/**
@ -20,37 +21,20 @@ use Symfony\Component\Form\FormBuilderInterface;
*/
class ProductVariantGenerationType extends AbstractResourceType
{
/**
* @var EventSubscriberInterface
*/
private $generateProductVariantsSubscriber;
/**
* @param string $dataClass
* @param string[] $validationGroups
* @param EventSubscriberInterface $generateProductVariants
*/
public function __construct($dataClass, $validationGroups, EventSubscriberInterface $generateProductVariants)
{
parent::__construct($dataClass, $validationGroups);
$this->generateProductVariantsSubscriber = $generateProductVariants;
}
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('variants', 'collection', [
'type' => 'sylius_product_variant',
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
->add('name', 'text', [
'required' => false,
'label' => 'sylius.form.variant.name',
])
->addEventSubscriber($this->generateProductVariantsSubscriber);
->addEventSubscriber(new AddCodeFormSubscriber())
;
$builder->addEventSubscriber(new BuildProductVariantFormSubscriber($builder->getFormFactory(), true));
}
/**

View file

@ -24,12 +24,15 @@
</service>
<service id="sylius.validator.product_variant_combination" class="Sylius\Bundle\ProductBundle\Validator\ProductVariantCombinationValidator">
<argument type="service" id="sylius.checker.product_variants_parity" />
<tag name="validator.constraint_validator" alias="sylius.validator.product_variant_combination" />
</service>
<service id="sylius.generator.product_variant" class="Sylius\Component\Product\Generator\ProductVariantGenerator">
<argument type="service" id="sylius.factory.product_variant" />
<argument type="service" id="sylius.checker.product_variants_parity" />
</service>
<service id="sylius.checker.product_variants_parity" class="Sylius\Component\Product\Checker\ProductVariantsParityChecker" />
<service id="sylius.generator.slug" class="Sylius\Component\Product\Generator\SlugGenerator" />
<service id="sylius.form.type.sylius_product_associations" class="Sylius\Bundle\ProductBundle\Form\Type\ProductAssociationsType">

View file

@ -15,7 +15,7 @@
<class name="Sylius\Component\Product\Model\ProductVariant">
<property name="code">
<constraint name="NotBlank">
<option name="message">sylius.option.code.not_blank</option>
<option name="message">sylius.variant.code.not_blank</option>
<option name="groups">sylius</option>
</constraint>
</property>

View file

@ -13,10 +13,9 @@ sylius:
slug:
not_blank: Please enter product slug.
variant:
sku:
not_blank: Please enter variant SKU.
unique: Variant SKU must be unique.
combination: Variant with this option set already exists.
code:
not_blank: Please enter variant code.
option:
code:
not_blank: Please enter option code.

View file

@ -11,7 +11,7 @@
namespace Sylius\Bundle\ProductBundle\Validator;
use Sylius\Component\Product\Model\ProductInterface;
use Sylius\Component\Product\Checker\ProductVariantsParityCheckerInterface;
use Sylius\Component\Product\Model\ProductVariantInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
@ -22,6 +22,19 @@ use Symfony\Component\Validator\Exception\UnexpectedTypeException;
*/
class ProductVariantCombinationValidator extends ConstraintValidator
{
/**
* @var ProductVariantsParityCheckerInterface
*/
private $variantsParityChecker;
/**
* @param ProductVariantsParityCheckerInterface $variantsParityChecker
*/
public function __construct(ProductVariantsParityCheckerInterface $variantsParityChecker)
{
$this->variantsParityChecker = $variantsParityChecker;
}
/**
* {@inheritdoc}
*/
@ -36,37 +49,8 @@ class ProductVariantCombinationValidator extends ConstraintValidator
return;
}
if ($this->matches($value, $product)) {
if ($this->variantsParityChecker->checkParity($value, $product)) {
$this->context->addViolation($constraint->message);
}
}
/**
* @param ProductVariantInterface $variant
* @param ProductInterface $variable
*
* @return bool
*/
private function matches(ProductVariantInterface $variant, ProductInterface $variable)
{
foreach ($variable->getVariants() as $existingVariant) {
if ($variant === $existingVariant || !$variant->getOptionValues()->count()) {
continue;
}
$matches = true;
foreach ($variant->getOptionValues() as $optionValue) {
if (!$existingVariant->hasOptionValue($optionValue)) {
$matches = false;
}
}
if ($matches) {
return true;
}
}
return false;
}
}

View file

@ -41,7 +41,7 @@ final class BuildProductVariantFormSubscriberSpec extends ObjectBehavior
);
}
function it_adds_options_on_pre_set_data_event(
function it_adds_options_on_pre_set_data_event_with_configurable_options(
FormEvent $event,
FormFactoryInterface $factory,
FormInterface $form,
@ -66,6 +66,43 @@ final class BuildProductVariantFormSubscriberSpec extends ObjectBehavior
[
'options' => [$options],
'auto_initialize' => false,
'disabled' => false,
]
)->willReturn($optionsForm);
$form->add($optionsForm)->shouldBeCalled();
$this->preSetData($event);
}
function it_adds_options_on_pre_set_data_event_without_configurable_options(
FormEvent $event,
FormFactoryInterface $factory,
FormInterface $form,
FormInterface $optionsForm,
ProductInterface $variable,
ProductOptionInterface $options,
ProductOptionValueInterface $optionValue,
ProductVariantInterface $variant
) {
$this->beConstructedWith($factory, true);
$event->getForm()->willReturn($form);
$event->getData()->willReturn($variant);
$variant->getProduct()->willReturn($variable);
$variant->getOptionValues()->willReturn([$optionValue]);
$variable->getOptions()->willReturn([$options]);
$variable->hasOptions()->willReturn(true);
$factory->createNamed(
'optionValues',
'sylius_product_option_value_collection',
[$optionValue],
[
'options' => [$options],
'auto_initialize' => false,
'disabled' => true,
]
)->willReturn($optionsForm);

View file

@ -11,11 +11,11 @@
namespace spec\Sylius\Bundle\ProductBundle\Validator;
use Doctrine\Common\Collections\ArrayCollection;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
use Sylius\Bundle\ProductBundle\Validator\Constraint\ProductVariantCombination;
use Sylius\Bundle\ProductBundle\Validator\ProductVariantCombinationValidator;
use Sylius\Component\Product\Checker\ProductVariantsParityCheckerInterface;
use Sylius\Component\Product\Model\ProductOptionValueInterface;
use Sylius\Component\Product\Model\ProductInterface;
use Sylius\Component\Product\Model\ProductVariantInterface;
@ -24,8 +24,9 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
final class ProductVariantCombinationValidatorSpec extends ObjectBehavior
{
function let(ExecutionContextInterface $context)
function let(ExecutionContextInterface $context, ProductVariantsParityCheckerInterface $variantsParityChecker)
{
$this->beConstructedWith($variantsParityChecker);
$this->initialize($context);
}
@ -39,10 +40,33 @@ final class ProductVariantCombinationValidatorSpec extends ObjectBehavior
$this->shouldImplement(ConstraintValidator::class);
}
function it_does_not_add_violation_if_variable_does_not_have_options(
function it_does_not_add_violation_if_product_does_not_have_options(
ExecutionContextInterface $context,
ProductInterface $product,
ProductVariantInterface $variant
ProductVariantInterface $variant,
ProductVariantsParityCheckerInterface $variantsParityChecker
) {
$constraint = new ProductVariantCombination([
'message' => 'Variant with given options already exists',
]);
$variant->getProduct()->willReturn($product);
$product->hasVariants()->willReturn(true);
$product->hasOptions()->willReturn(false);
$variantsParityChecker->checkParity($variant, $product)->willReturn(false);
$context->addViolation(Argument::any())->shouldNotBeCalled();
$this->validate($variant, $constraint);
}
function it_does_not_add_violation_if_product_does_not_have_variants(
ExecutionContextInterface $context,
ProductInterface $product,
ProductVariantInterface $variant,
ProductVariantsParityCheckerInterface $variantsParityChecker
) {
$constraint = new ProductVariantCombination([
'message' => 'Variant with given options already exists',
@ -55,48 +79,27 @@ final class ProductVariantCombinationValidatorSpec extends ObjectBehavior
$context->addViolation(Argument::any())->shouldNotBeCalled();
$this->validate($variant, $constraint);
}
function it_does_not_add_violation_if_product_does_not_have_variants(
ExecutionContextInterface $context,
ProductInterface $variable,
ProductVariantInterface $variant
) {
$constraint = new ProductVariantCombination([
'message' => 'Variant with given options already exists',
]);
$variant->getProduct()->willReturn($variable);
$variable->hasVariants()->willReturn(true);
$variable->hasOptions()->willReturn(false);
$context->addViolation(Argument::any())->shouldNotBeCalled();
$variantsParityChecker->checkParity($variant, $product)->willReturn(false);
$this->validate($variant, $constraint);
}
function it_adds_violation_if_variant_with_given_same_options_already_exists(
ExecutionContextInterface $context,
ProductInterface $variable,
ProductOptionValueInterface $optionValue,
ProductVariantInterface $existingVariant,
ProductVariantInterface $variant
ProductInterface $product,
ProductVariantInterface $variant,
ProductVariantsParityCheckerInterface $variantsParityChecker
) {
$constraint = new ProductVariantCombination([
'message' => 'Variant with given options already exists',
]);
$variant->getProduct()->willReturn($variable);
$variant->getProduct()->willReturn($product);
$variant->getOptionValues()->willReturn(new ArrayCollection([$optionValue->getWrappedObject()]));
$product->hasVariants()->willReturn(true);
$product->hasOptions()->willReturn(true);
$existingVariant->hasOptionValue($optionValue)->willReturn(true);
$variable->hasVariants()->willReturn(true);
$variable->hasOptions()->willReturn(true);
$variable->getVariants()->willReturn([$existingVariant]);
$variantsParityChecker->checkParity($variant, $product)->willReturn(true);
$context->addViolation('Variant with given options already exists', Argument::any())->shouldBeCalled();

View file

@ -0,0 +1,58 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Paweł Jędrzejewski
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Sylius\Component\Product\Checker;
use Sylius\Component\Product\Model\ProductInterface;
use Sylius\Component\Product\Model\ProductVariantInterface;
use Webmozart\Assert\Assert;
/**
* @author Łukasz Chruściel <lukasz.chrusciel@lakion.com>
*/
final class ProductVariantsParityChecker implements ProductVariantsParityCheckerInterface
{
/**
* {@inheritdoc}
*/
public function checkParity(ProductVariantInterface $variant, ProductInterface $product)
{
foreach ($product->getVariants() as $existingVariant) {
// This check is require, because this function has to look for any other different variant with same option values set
if ($variant === $existingVariant || count($variant->getOptionValues()) !== count($product->getOptions())) {
continue;
}
if ($this->matchOptions($variant, $existingVariant)) {
return true;
}
}
return false;
}
/**
* @param ProductVariantInterface $variant
* @param ProductVariantInterface $existingVariant
*
* @return bool
*/
private function matchOptions(ProductVariantInterface $variant, ProductVariantInterface $existingVariant)
{
foreach ($variant->getOptionValues() as $option) {
if (!$existingVariant->hasOptionValue($option)) {
return false;
}
}
return true;
}
}

View file

@ -0,0 +1,29 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Paweł Jędrzejewski
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Sylius\Component\Product\Checker;
use Sylius\Component\Product\Model\ProductInterface;
use Sylius\Component\Product\Model\ProductVariantInterface;
/**
* @author Łukasz Chruściel <lukasz.chrusciel@lakion.com>
*/
interface ProductVariantsParityCheckerInterface
{
/**
* @param ProductVariantInterface $variant
* @param ProductInterface $product
*
* @return bool
*/
public function checkParity(ProductVariantInterface $variant, ProductInterface $product);
}

View file

@ -11,6 +11,7 @@
namespace Sylius\Component\Product\Generator;
use Sylius\Component\Product\Checker\ProductVariantsParityCheckerInterface;
use Sylius\Component\Product\Factory\ProductVariantFactoryInterface;
use Sylius\Component\Product\Model\ProductInterface;
use Sylius\Component\Product\Model\ProductVariantInterface;
@ -18,13 +19,14 @@ use Webmozart\Assert\Assert;
/**
* @author Paweł Jędrzejewski <pawel@sylius.org>
* @author Łukasz Chruściel <lukasz.chrusciel@lakion.com>
*/
final class ProductVariantGenerator implements ProductVariantGeneratorInterface
{
/**
* @var ProductVariantFactoryInterface
*/
protected $productVariantFactory;
private $productVariantFactory;
/**
* @var CartesianSetBuilder
@ -32,12 +34,21 @@ final class ProductVariantGenerator implements ProductVariantGeneratorInterface
private $setBuilder;
/**
* @param ProductVariantFactoryInterface $productVariantFactory
* @var ProductVariantsParityCheckerInterface
*/
public function __construct(ProductVariantFactoryInterface $productVariantFactory)
{
private $variantsParityChecker;
/**
* @param ProductVariantFactoryInterface $productVariantFactory
* @param ProductVariantsParityCheckerInterface $variantsParityChecker
*/
public function __construct(
ProductVariantFactoryInterface $productVariantFactory,
ProductVariantsParityCheckerInterface $variantsParityChecker
) {
$this->productVariantFactory = $productVariantFactory;
$this->setBuilder = new CartesianSetBuilder();
$this->variantsParityChecker = $variantsParityChecker;
}
/**
@ -61,7 +72,10 @@ final class ProductVariantGenerator implements ProductVariantGeneratorInterface
foreach ($permutations as $permutation) {
$variant = $this->createVariant($product, $optionMap, $permutation);
$product->addVariant($variant);
if (!$this->variantsParityChecker->checkParity($variant, $product)) {
$product->addVariant($variant);
}
}
}

View file

@ -13,6 +13,7 @@ namespace spec\Sylius\Component\Product\Generator;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
use Sylius\Component\Product\Checker\ProductVariantsParityCheckerInterface;
use Sylius\Component\Product\Factory\ProductVariantFactoryInterface;
use Sylius\Component\Product\Generator\ProductVariantGenerator;
use Sylius\Component\Product\Generator\ProductVariantGeneratorInterface;
@ -26,9 +27,11 @@ use Sylius\Component\Product\Model\ProductVariantInterface;
*/
final class ProductVariantGeneratorSpec extends ObjectBehavior
{
function let(ProductVariantFactoryInterface $productVariantFactory)
{
$this->beConstructedWith($productVariantFactory);
function let(
ProductVariantFactoryInterface $productVariantFactory,
ProductVariantsParityCheckerInterface $variantsParityChecker
) {
$this->beConstructedWith($productVariantFactory, $variantsParityChecker);
}
function it_is_initializable()
@ -55,7 +58,8 @@ final class ProductVariantGeneratorSpec extends ObjectBehavior
ProductOptionValueInterface $redColor,
ProductOptionValueInterface $whiteColor,
ProductVariantFactoryInterface $productVariantFactory,
ProductVariantInterface $permutationVariant
ProductVariantInterface $permutationVariant,
ProductVariantsParityCheckerInterface $variantsParityChecker
) {
$productVariable->hasOptions()->willReturn(true);
@ -67,6 +71,8 @@ final class ProductVariantGeneratorSpec extends ObjectBehavior
$whiteColor->getId()->willReturn('white2');
$redColor->getId()->willReturn('red3');
$variantsParityChecker->checkParity($permutationVariant, $productVariable)->willReturn(false);
$productVariantFactory->createForProduct($productVariable)->willReturn($permutationVariant);
$permutationVariant->addOptionValue(Argument::type(ProductOptionValueInterface::class))->shouldBeCalled();
@ -75,6 +81,36 @@ final class ProductVariantGeneratorSpec extends ObjectBehavior
$this->generate($productVariable);
}
function it_does_not_generate_variant_if_given_variant_exists(
ProductInterface $productVariable,
ProductOptionInterface $colorOption,
ProductOptionValueInterface $blackColor,
ProductOptionValueInterface $redColor,
ProductOptionValueInterface $whiteColor,
ProductVariantFactoryInterface $productVariantFactory,
ProductVariantInterface $permutationVariant,
ProductVariantsParityCheckerInterface $variantsParityChecker
) {
$productVariable->hasOptions()->willReturn(true);
$productVariable->getOptions()->willReturn([$colorOption]);
$colorOption->getValues()->willReturn([$blackColor, $whiteColor, $redColor]);
$blackColor->getId()->willReturn('black1');
$whiteColor->getId()->willReturn('white2');
$redColor->getId()->willReturn('red3');
$variantsParityChecker->checkParity($permutationVariant, $productVariable)->willReturn(true);
$productVariantFactory->createForProduct($productVariable)->willReturn($permutationVariant);
$permutationVariant->addOptionValue(Argument::type(ProductOptionValueInterface::class))->shouldBeCalled();
$productVariable->addVariant($permutationVariant)->shouldNotBeCalled();
$this->generate($productVariable);
}
function it_generates_variants_for_every_possible_permutation_of_an_objects_options_and_option_values(
ProductInterface $productVariable,
ProductOptionInterface $colorOption,
@ -86,7 +122,8 @@ final class ProductVariantGeneratorSpec extends ObjectBehavior
ProductOptionValueInterface $smallSize,
ProductOptionValueInterface $whiteColor,
ProductVariantFactoryInterface $productVariantFactory,
ProductVariantInterface $permutationVariant
ProductVariantInterface $permutationVariant,
ProductVariantsParityCheckerInterface $variantsParityChecker
) {
$productVariable->hasOptions()->willReturn(true);
@ -102,6 +139,8 @@ final class ProductVariantGeneratorSpec extends ObjectBehavior
$mediumSize->getId()->willReturn('medium5');
$largeSize->getId()->willReturn('large6');
$variantsParityChecker->checkParity($permutationVariant, $productVariable)->willReturn(false);
$productVariantFactory->createForProduct($productVariable)->willReturn($permutationVariant);
$permutationVariant->addOptionValue(Argument::type(ProductOptionValueInterface::class))->shouldBeCalled();