Skip to content

Commit

Permalink
feature #13398 [Documentation][CatalogPromotion] Add cookbook about c…
Browse files Browse the repository at this point in the history
…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
AdamKasp committed Dec 16, 2021
2 parents 551d34d + e45593d commit 955517f
Show file tree
Hide file tree
Showing 6 changed files with 414 additions and 68 deletions.
1 change: 1 addition & 0 deletions docs/cookbook/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ Promotions
promotions/custom-cart-promotion-rule
promotions/custom-cart-promotion-action
promotions/custom-catalog-promotion-scope
promotions/custom-catalog-promotion-action

.. include:: /cookbook/promotions/map.rst.inc

Expand Down
316 changes: 316 additions & 0 deletions docs/cookbook/promotions/custom-catalog-promotion-action.rst
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>`
Loading

0 comments on commit 955517f

Please sign in to comment.