Skip to content

Commit

Permalink
[Documentation][CatalogPromotion] Add cookbook about creating a custo…
Browse files Browse the repository at this point in the history
…m action
  • Loading branch information
GSadee committed Dec 16, 2021
1 parent 7f33db6 commit 08e9a0c
Show file tree
Hide file tree
Showing 3 changed files with 319 additions and 1 deletion.
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 should 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 added our additional custom action 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 programmatically and API created resources.
Let's now prepare a custom validator for a created action.

Prepare a custom validator for a 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 if necessary data is provided and 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 and can be created, and edited
programmatically or by API. Let's now prepare a UI part of this new feature.

Prepare a configuration form type for a 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 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 should 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>`
3 changes: 2 additions & 1 deletion docs/cookbook/promotions/map.rst.inc
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
* :doc:`/cookbook/promotions/custom-cart-promotion-action`
* :doc:`/cookbook/promotions/custom-cart-promotion-rule`
* :doc:`/cookbook/promotions/custom-cart-promotion-action`
* :doc:`/cookbook/promotions/custom-catalog-promotion-scope`
* :doc:`/cookbook/promotions/custom-catalog-promotion-action`

0 comments on commit 08e9a0c

Please sign in to comment.