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

Skip to content

Commit 955517f

Browse files
authored
feature #13398 [Documentation][CatalogPromotion] Add cookbook about creating 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
2 parents 551d34d + e45593d commit 955517f

6 files changed

Lines changed: 414 additions & 68 deletions

File tree

docs/cookbook/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ Promotions
7474
promotions/custom-cart-promotion-rule
7575
promotions/custom-cart-promotion-action
7676
promotions/custom-catalog-promotion-scope
77+
promotions/custom-catalog-promotion-action
7778

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

Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,316 @@
1+
How to add a custom catalog promotion action?
2+
=============================================
3+
4+
Adding a new, custom catalog promotion action to your shop may become a quite helpful extension to your own Catalog Promotions.
5+
You can create your own calculator tailored to your product catalog to attract as many people as possible.
6+
7+
Let's try to implement the new **Catalog Promotion Action** in this cookbook that will lower the price of the product
8+
or product variant to a specific value.
9+
10+
.. note::
11+
12+
If you are familiar with **Cart Promotions** and you know how **Cart Promotion Actions** work,
13+
then the Catalog Promotion Action should look familiar, as the concept of them is quite similar.
14+
15+
Create a new catalog promotion action
16+
-------------------------------------
17+
18+
The new action needs to be declared somewhere, the first step would be to extend the base interface:
19+
20+
.. code-block:: php
21+
22+
<?php
23+
24+
declare(strict_types=1);
25+
26+
namespace App\Model;
27+
28+
use Sylius\Component\Promotion\Model\CatalogPromotionActionInterface as BaseCatalogPromotionActionInterface;
29+
30+
interface CatalogPromotionActionInterface extends BaseCatalogPromotionActionInterface
31+
{
32+
public const TYPE_FIXED_PRICE = 'fixed_price';
33+
}
34+
35+
Now let's declare the parameter with action types, with our additional custom action added as the last one:
36+
37+
.. code-block:: yaml
38+
39+
# config/services.yaml
40+
41+
parameters:
42+
sylius.catalog_promotion.actions:
43+
- !php/const Sylius\Component\Promotion\Model\CatalogPromotionActionInterface::TYPE_FIXED_DISCOUNT
44+
- !php/const Sylius\Component\Promotion\Model\CatalogPromotionActionInterface::TYPE_PERCENTAGE_DISCOUNT
45+
- !php/const App\Model\CatalogPromotionActionInterface::TYPE_FIXED_PRICE
46+
47+
We should now create a calculator that will return a proper price for given channel pricing. We can start with configuration:
48+
49+
.. code-block:: yaml
50+
51+
# config/services.yaml
52+
53+
App\Calculator\FixedPriceCalculator:
54+
tags:
55+
- { name: 'sylius.catalog_promotion.price_calculator' }
56+
57+
.. note::
58+
59+
Please take a note on a declared tag of calculator, it is necessary for this service to be taken into account.
60+
61+
And the code for the calculator itself:
62+
63+
.. code-block:: php
64+
65+
<?php
66+
67+
declare(strict_types=1);
68+
69+
namespace App\Calculator;
70+
71+
use App\Model\CatalogPromotionActionInterface;
72+
use Sylius\Bundle\CoreBundle\Calculator\ActionBasedPriceCalculatorInterface;
73+
use Sylius\Component\Core\Model\ChannelPricingInterface;
74+
use Sylius\Component\Promotion\Model\CatalogPromotionActionInterface as BaseCatalogPromotionActionInterface;
75+
76+
final class FixedPriceCalculator implements ActionBasedPriceCalculatorInterface
77+
{
78+
public function supports(BaseCatalogPromotionActionInterface $action): bool
79+
{
80+
return $action->getType() === CatalogPromotionActionInterface::TYPE_FIXED_PRICE;
81+
}
82+
83+
public function calculate(ChannelPricingInterface $channelPricing, BaseCatalogPromotionActionInterface $action): int
84+
{
85+
if (!isset($action->getConfiguration()[$channelPricing->getChannelCode()])) {
86+
return $channelPricing->getPrice();
87+
}
88+
89+
$price = $action->getConfiguration()[$channelPricing->getChannelCode()]['price'];
90+
91+
$minimumPrice = $this->provideMinimumPrice($channelPricing);
92+
if ($price < $minimumPrice) {
93+
return $minimumPrice;
94+
}
95+
96+
return $price;
97+
}
98+
99+
private function provideMinimumPrice(ChannelPricingInterface $channelPricing): int
100+
{
101+
if ($channelPricing->getMinimumPrice() <= 0) {
102+
return 0;
103+
}
104+
105+
return $channelPricing->getMinimumPrice();
106+
}
107+
}
108+
109+
Now the catalog promotion should work with your new action for resources created both programmatically and via API.
110+
Let's now prepare a custom validator for the newly created action.
111+
112+
Prepare a custom validator for the new action
113+
---------------------------------------------
114+
115+
We can start with configuration, declare our basic validator for this particular action:
116+
117+
.. code-block:: yaml
118+
119+
# config/services.yaml
120+
121+
App\Validator\CatalogPromotionAction\FixedPriceActionValidator:
122+
arguments:
123+
- '@sylius.repository.channel'
124+
tags:
125+
- { name: 'sylius.catalog_promotion.action_validator', key: 'fixed_price' }
126+
127+
In this validator, we will check the provided configuration for necessary data and if the configured channels exist.
128+
129+
.. code-block:: php
130+
131+
<?php
132+
133+
declare(strict_types=1);
134+
135+
namespace App\Validator\CatalogPromotionAction;
136+
137+
use Sylius\Bundle\PromotionBundle\Validator\CatalogPromotionAction\ActionValidatorInterface;
138+
use Sylius\Bundle\PromotionBundle\Validator\Constraints\CatalogPromotionAction;
139+
use Sylius\Component\Channel\Repository\ChannelRepositoryInterface;
140+
use Symfony\Component\Validator\Constraint;
141+
use Symfony\Component\Validator\Context\ExecutionContextInterface;
142+
use Webmozart\Assert\Assert;
143+
144+
final class FixedPriceActionValidator implements ActionValidatorInterface
145+
{
146+
private ChannelRepositoryInterface $channelRepository;
147+
148+
public function __construct(ChannelRepositoryInterface $channelRepository)
149+
{
150+
$this->channelRepository = $channelRepository;
151+
}
152+
153+
public function validate(array $configuration, Constraint $constraint, ExecutionContextInterface $context): void
154+
{
155+
/** @var CatalogPromotionAction $constraint */
156+
Assert::isInstanceOf($constraint, CatalogPromotionAction::class);
157+
158+
if (empty($configuration)) {
159+
$context->buildViolation('There is no configuration provided.')->atPath('configuration')->addViolation();
160+
161+
return;
162+
}
163+
164+
foreach ($configuration as $channelCode => $channelConfiguration) {
165+
if (null === $this->channelRepository->findOneBy(['code' => $channelCode])) {
166+
$context->buildViolation('The provided channel is not valid.')->atPath('configuration')->addViolation();
167+
168+
return;
169+
}
170+
171+
if (!array_key_exists('price', $channelConfiguration) || !is_integer($channelConfiguration['price']) || $channelConfiguration['price'] < 0) {
172+
$context->buildViolation('The provided configuration for channel is not valid.')->atPath('configuration')->addViolation();
173+
174+
return;
175+
}
176+
}
177+
}
178+
}
179+
180+
Alright, we have a working basic validation, and our new type of action exists, can be created, and edited
181+
programmatically or by API. Let's now prepare the UI part of this new feature.
182+
183+
Prepare a configuration form type for the new action
184+
----------------------------------------------------
185+
186+
To be able to configure a catalog promotion with your new action you will need a form type for the admin panel.
187+
And with the current implementation, as our action is channel-based, you need to create 2 form types as below:
188+
189+
.. code-block:: yaml
190+
191+
# config/services.yaml
192+
193+
App\Form\Type\CatalogPromotionAction\ChannelBasedFixedPriceActionConfigurationType:
194+
tags:
195+
- { name: 'sylius.catalog_promotion.action_configuration_type', key: 'fixed_price' }
196+
- { name: 'form.type' }
197+
198+
.. code-block:: php
199+
200+
<?php
201+
202+
declare(strict_types=1);
203+
204+
namespace App\Form\Type\CatalogPromotionAction;
205+
206+
use Sylius\Bundle\MoneyBundle\Form\Type\MoneyType;
207+
use Symfony\Component\Form\AbstractType;
208+
use Symfony\Component\Form\FormBuilderInterface;
209+
use Symfony\Component\OptionsResolver\OptionsResolver;
210+
211+
final class FixedPriceActionConfigurationType extends AbstractType
212+
{
213+
public function buildForm(FormBuilderInterface $builder, array $options): void
214+
{
215+
$builder
216+
->add('price', MoneyType::class, [
217+
'label' => 'Price',
218+
'currency' => $options['currency'],
219+
])
220+
;
221+
}
222+
223+
public function configureOptions(OptionsResolver $resolver): void
224+
{
225+
$resolver
226+
->setRequired('currency')
227+
->setAllowedTypes('currency', 'string')
228+
;
229+
}
230+
231+
public function getBlockPrefix(): string
232+
{
233+
return 'app_catalog_promotion_action_fixed_price_configuration';
234+
}
235+
}
236+
237+
.. code-block:: php
238+
239+
<?php
240+
241+
declare(strict_types=1);
242+
243+
namespace App\Form\Type\CatalogPromotionAction;
244+
245+
use Sylius\Bundle\CoreBundle\Form\Type\ChannelCollectionType;
246+
use Sylius\Component\Core\Model\ChannelInterface;
247+
use Symfony\Component\Form\AbstractType;
248+
use Symfony\Component\OptionsResolver\OptionsResolver;
249+
250+
final class ChannelBasedFixedPriceActionConfigurationType extends AbstractType
251+
{
252+
public function configureOptions(OptionsResolver $resolver): void
253+
{
254+
$resolver->setDefaults([
255+
'entry_type' => FixedPriceActionConfigurationType::class,
256+
'entry_options' => function (ChannelInterface $channel) {
257+
return [
258+
'label' => $channel->getName(),
259+
'currency' => $channel->getBaseCurrency()->getCode(),
260+
];
261+
},
262+
]);
263+
}
264+
265+
public function getParent(): string
266+
{
267+
return ChannelCollectionType::class;
268+
}
269+
}
270+
271+
And define the translation for our new action type:
272+
273+
.. code-block:: yaml
274+
275+
# translations/messages.en.yaml
276+
277+
sylius:
278+
form:
279+
catalog_promotion:
280+
action:
281+
fixed_price: 'Fixed price'
282+
283+
Prepare an action template for show page of catalog promotion
284+
-------------------------------------------------------------
285+
286+
The last thing is to create a template to display our new action properly. Remember to name it the same as the action type.
287+
288+
.. code-block:: html+twig
289+
290+
{# templates/bundles/SyliusAdminBundle/CatalogPromotion/Show/Action/fixed_price.html.twig #}
291+
292+
{% import "@SyliusAdmin/Common/Macro/money.html.twig" as money %}
293+
294+
<table class="ui very basic celled table">
295+
<tbody>
296+
<tr>
297+
<td class="five wide"><strong class="gray text">Type</strong></td>
298+
<td>Fixed price</td>
299+
</tr>
300+
{% set currencies = sylius_channels_currencies() %}
301+
{% for channelCode, channelConfiguration in action.configuration %}
302+
<tr>
303+
<td class="five wide"><strong class="gray text">{{ channelCode }}</strong></td>
304+
<td>{{ money.format(channelConfiguration.price, currencies[channelCode]) }}</td>
305+
</tr>
306+
{% endfor %}
307+
</tbody>
308+
</table>
309+
310+
That's all. You will now be able to choose the new action while creating or editing a catalog promotion.
311+
312+
Learn more
313+
----------
314+
315+
* :doc:`Customization Guide </customization/index>`
316+
* :doc:`Catalog Promotion Concept Book </book/products/catalog_promotions>`

0 commit comments

Comments
 (0)