-
-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feature #13398 [Documentation][CatalogPromotion] Add cookbook about c…
…reating a custom action + minor improvements (GSadee) This PR was merged into the 1.11 branch. Discussion ---------- | Q | A | --------------- | ----- | Branch? | 1.11 | Bug fix? | no | New feature? | no | BC breaks? | no | Deprecations? | no | Related tickets | partially fixes ##13325 (comment) | License | MIT This PR contains a minor refactor of parameters with types of actions/scopes passed to validators. <!-- - Bug fixes must be submitted against the 1.10 or 1.11 branch(the lowest possible) - Features and deprecations must be submitted against the master branch - Make sure that the correct base branch is set To be sure you are not breaking any Backward Compatibilities, check the documentation: https://docs.sylius.com/en/latest/book/organization/backward-compatibility-promise.html --> Commits ------- 7f33db6 [CatalogPromotion] Create parameters with collections of scopes and actions types 08e9a0c [Documentation][CatalogPromotion] Add cookbook about creating a custom action c4816cd [Documentation][CatalogPromotion] Minor improvements to cookbook about creating a custom scope e45593d [Documentation][CatalogPromotion] Fixes to cookbooks about creating custom action and scope
- Loading branch information
Showing
6 changed files
with
414 additions
and
68 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
316 changes: 316 additions & 0 deletions
316
docs/cookbook/promotions/custom-catalog-promotion-action.rst
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,316 @@ | ||
How to add a custom catalog promotion action? | ||
============================================= | ||
|
||
Adding a new, custom catalog promotion action to your shop may become a quite helpful extension to your own Catalog Promotions. | ||
You can create your own calculator tailored to your product catalog to attract as many people as possible. | ||
|
||
Let's try to implement the new **Catalog Promotion Action** in this cookbook that will lower the price of the product | ||
or product variant to a specific value. | ||
|
||
.. note:: | ||
|
||
If you are familiar with **Cart Promotions** and you know how **Cart Promotion Actions** work, | ||
then the Catalog Promotion Action should look familiar, as the concept of them is quite similar. | ||
|
||
Create a new catalog promotion action | ||
------------------------------------- | ||
|
||
The new action needs to be declared somewhere, the first step would be to extend the base interface: | ||
|
||
.. code-block:: php | ||
<?php | ||
declare(strict_types=1); | ||
namespace App\Model; | ||
use Sylius\Component\Promotion\Model\CatalogPromotionActionInterface as BaseCatalogPromotionActionInterface; | ||
interface CatalogPromotionActionInterface extends BaseCatalogPromotionActionInterface | ||
{ | ||
public const TYPE_FIXED_PRICE = 'fixed_price'; | ||
} | ||
Now let's declare the parameter with action types, with our additional custom action added as the last one: | ||
|
||
.. code-block:: yaml | ||
# config/services.yaml | ||
parameters: | ||
sylius.catalog_promotion.actions: | ||
- !php/const Sylius\Component\Promotion\Model\CatalogPromotionActionInterface::TYPE_FIXED_DISCOUNT | ||
- !php/const Sylius\Component\Promotion\Model\CatalogPromotionActionInterface::TYPE_PERCENTAGE_DISCOUNT | ||
- !php/const App\Model\CatalogPromotionActionInterface::TYPE_FIXED_PRICE | ||
We should now create a calculator that will return a proper price for given channel pricing. We can start with configuration: | ||
|
||
.. code-block:: yaml | ||
# config/services.yaml | ||
App\Calculator\FixedPriceCalculator: | ||
tags: | ||
- { name: 'sylius.catalog_promotion.price_calculator' } | ||
.. note:: | ||
|
||
Please take a note on a declared tag of calculator, it is necessary for this service to be taken into account. | ||
|
||
And the code for the calculator itself: | ||
|
||
.. code-block:: php | ||
<?php | ||
declare(strict_types=1); | ||
namespace App\Calculator; | ||
use App\Model\CatalogPromotionActionInterface; | ||
use Sylius\Bundle\CoreBundle\Calculator\ActionBasedPriceCalculatorInterface; | ||
use Sylius\Component\Core\Model\ChannelPricingInterface; | ||
use Sylius\Component\Promotion\Model\CatalogPromotionActionInterface as BaseCatalogPromotionActionInterface; | ||
final class FixedPriceCalculator implements ActionBasedPriceCalculatorInterface | ||
{ | ||
public function supports(BaseCatalogPromotionActionInterface $action): bool | ||
{ | ||
return $action->getType() === CatalogPromotionActionInterface::TYPE_FIXED_PRICE; | ||
} | ||
public function calculate(ChannelPricingInterface $channelPricing, BaseCatalogPromotionActionInterface $action): int | ||
{ | ||
if (!isset($action->getConfiguration()[$channelPricing->getChannelCode()])) { | ||
return $channelPricing->getPrice(); | ||
} | ||
$price = $action->getConfiguration()[$channelPricing->getChannelCode()]['price']; | ||
$minimumPrice = $this->provideMinimumPrice($channelPricing); | ||
if ($price < $minimumPrice) { | ||
return $minimumPrice; | ||
} | ||
return $price; | ||
} | ||
private function provideMinimumPrice(ChannelPricingInterface $channelPricing): int | ||
{ | ||
if ($channelPricing->getMinimumPrice() <= 0) { | ||
return 0; | ||
} | ||
return $channelPricing->getMinimumPrice(); | ||
} | ||
} | ||
Now the catalog promotion should work with your new action for resources created both programmatically and via API. | ||
Let's now prepare a custom validator for the newly created action. | ||
|
||
Prepare a custom validator for the new action | ||
--------------------------------------------- | ||
|
||
We can start with configuration, declare our basic validator for this particular action: | ||
|
||
.. code-block:: yaml | ||
# config/services.yaml | ||
App\Validator\CatalogPromotionAction\FixedPriceActionValidator: | ||
arguments: | ||
- '@sylius.repository.channel' | ||
tags: | ||
- { name: 'sylius.catalog_promotion.action_validator', key: 'fixed_price' } | ||
In this validator, we will check the provided configuration for necessary data and if the configured channels exist. | ||
|
||
.. code-block:: php | ||
<?php | ||
declare(strict_types=1); | ||
namespace App\Validator\CatalogPromotionAction; | ||
use Sylius\Bundle\PromotionBundle\Validator\CatalogPromotionAction\ActionValidatorInterface; | ||
use Sylius\Bundle\PromotionBundle\Validator\Constraints\CatalogPromotionAction; | ||
use Sylius\Component\Channel\Repository\ChannelRepositoryInterface; | ||
use Symfony\Component\Validator\Constraint; | ||
use Symfony\Component\Validator\Context\ExecutionContextInterface; | ||
use Webmozart\Assert\Assert; | ||
final class FixedPriceActionValidator implements ActionValidatorInterface | ||
{ | ||
private ChannelRepositoryInterface $channelRepository; | ||
public function __construct(ChannelRepositoryInterface $channelRepository) | ||
{ | ||
$this->channelRepository = $channelRepository; | ||
} | ||
public function validate(array $configuration, Constraint $constraint, ExecutionContextInterface $context): void | ||
{ | ||
/** @var CatalogPromotionAction $constraint */ | ||
Assert::isInstanceOf($constraint, CatalogPromotionAction::class); | ||
if (empty($configuration)) { | ||
$context->buildViolation('There is no configuration provided.')->atPath('configuration')->addViolation(); | ||
return; | ||
} | ||
foreach ($configuration as $channelCode => $channelConfiguration) { | ||
if (null === $this->channelRepository->findOneBy(['code' => $channelCode])) { | ||
$context->buildViolation('The provided channel is not valid.')->atPath('configuration')->addViolation(); | ||
return; | ||
} | ||
if (!array_key_exists('price', $channelConfiguration) || !is_integer($channelConfiguration['price']) || $channelConfiguration['price'] < 0) { | ||
$context->buildViolation('The provided configuration for channel is not valid.')->atPath('configuration')->addViolation(); | ||
return; | ||
} | ||
} | ||
} | ||
} | ||
Alright, we have a working basic validation, and our new type of action exists, can be created, and edited | ||
programmatically or by API. Let's now prepare the UI part of this new feature. | ||
|
||
Prepare a configuration form type for the new action | ||
---------------------------------------------------- | ||
|
||
To be able to configure a catalog promotion with your new action you will need a form type for the admin panel. | ||
And with the current implementation, as our action is channel-based, you need to create 2 form types as below: | ||
|
||
.. code-block:: yaml | ||
# config/services.yaml | ||
App\Form\Type\CatalogPromotionAction\ChannelBasedFixedPriceActionConfigurationType: | ||
tags: | ||
- { name: 'sylius.catalog_promotion.action_configuration_type', key: 'fixed_price' } | ||
- { name: 'form.type' } | ||
.. code-block:: php | ||
<?php | ||
declare(strict_types=1); | ||
namespace App\Form\Type\CatalogPromotionAction; | ||
use Sylius\Bundle\MoneyBundle\Form\Type\MoneyType; | ||
use Symfony\Component\Form\AbstractType; | ||
use Symfony\Component\Form\FormBuilderInterface; | ||
use Symfony\Component\OptionsResolver\OptionsResolver; | ||
final class FixedPriceActionConfigurationType extends AbstractType | ||
{ | ||
public function buildForm(FormBuilderInterface $builder, array $options): void | ||
{ | ||
$builder | ||
->add('price', MoneyType::class, [ | ||
'label' => 'Price', | ||
'currency' => $options['currency'], | ||
]) | ||
; | ||
} | ||
public function configureOptions(OptionsResolver $resolver): void | ||
{ | ||
$resolver | ||
->setRequired('currency') | ||
->setAllowedTypes('currency', 'string') | ||
; | ||
} | ||
public function getBlockPrefix(): string | ||
{ | ||
return 'app_catalog_promotion_action_fixed_price_configuration'; | ||
} | ||
} | ||
.. code-block:: php | ||
<?php | ||
declare(strict_types=1); | ||
namespace App\Form\Type\CatalogPromotionAction; | ||
use Sylius\Bundle\CoreBundle\Form\Type\ChannelCollectionType; | ||
use Sylius\Component\Core\Model\ChannelInterface; | ||
use Symfony\Component\Form\AbstractType; | ||
use Symfony\Component\OptionsResolver\OptionsResolver; | ||
final class ChannelBasedFixedPriceActionConfigurationType extends AbstractType | ||
{ | ||
public function configureOptions(OptionsResolver $resolver): void | ||
{ | ||
$resolver->setDefaults([ | ||
'entry_type' => FixedPriceActionConfigurationType::class, | ||
'entry_options' => function (ChannelInterface $channel) { | ||
return [ | ||
'label' => $channel->getName(), | ||
'currency' => $channel->getBaseCurrency()->getCode(), | ||
]; | ||
}, | ||
]); | ||
} | ||
public function getParent(): string | ||
{ | ||
return ChannelCollectionType::class; | ||
} | ||
} | ||
And define the translation for our new action type: | ||
|
||
.. code-block:: yaml | ||
# translations/messages.en.yaml | ||
sylius: | ||
form: | ||
catalog_promotion: | ||
action: | ||
fixed_price: 'Fixed price' | ||
Prepare an action template for show page of catalog promotion | ||
------------------------------------------------------------- | ||
|
||
The last thing is to create a template to display our new action properly. Remember to name it the same as the action type. | ||
|
||
.. code-block:: html+twig | ||
|
||
{# templates/bundles/SyliusAdminBundle/CatalogPromotion/Show/Action/fixed_price.html.twig #} | ||
|
||
{% import "@SyliusAdmin/Common/Macro/money.html.twig" as money %} | ||
|
||
<table class="ui very basic celled table"> | ||
<tbody> | ||
<tr> | ||
<td class="five wide"><strong class="gray text">Type</strong></td> | ||
<td>Fixed price</td> | ||
</tr> | ||
{% set currencies = sylius_channels_currencies() %} | ||
{% for channelCode, channelConfiguration in action.configuration %} | ||
<tr> | ||
<td class="five wide"><strong class="gray text">{{ channelCode }}</strong></td> | ||
<td>{{ money.format(channelConfiguration.price, currencies[channelCode]) }}</td> | ||
</tr> | ||
{% endfor %} | ||
</tbody> | ||
</table> | ||
|
||
That's all. You will now be able to choose the new action while creating or editing a catalog promotion. | ||
|
||
Learn more | ||
---------- | ||
|
||
* :doc:`Customization Guide </customization/index>` | ||
* :doc:`Catalog Promotion Concept Book </book/products/catalog_promotions>` |
Oops, something went wrong.