How to optimize background processes
when Sylius meets Black
fi
re
Photo by Laura Ockel on Unsplash
Introduction
01
Photo by Mikhail Vasilyev on Unsplash
Photo by Ryan Quintal on Unsplash
Feature overview
sylius.com
4
Product variants


Always have Y Channel Pricings,


Where Y equals amount of Channels in shop
Product


May have from 1 to N variants
Channel Pricing


Contains end pricing for customer
Sylius pricing model
sylius.com
Discounting rules set
5
Main con
fi
guration


Dates


Channels


Enabled


Priority


Exclusiveness
Processing scope


Variants


Products


Taxonomy
Resulting actions


Fixed discount


Percentage discount
sylius.com
6
1 - 10
To process updates of


Products through UI interface
Minimal processable scope


As a business rule
10k - 100k
Default behaviour should


Be consider for these values
Typical Sylius project


Based on the clients we are working the most
Over 1kk
Feature should be possible


To be used with this scope
Maximal processable scope


From the projects we know
Dataset size
Photo by Brendan Church on Unsplash
Initial dilema
sylius.com
8
Take all eligible catalog promotions


This step can be outsourced to DB
Fetch variants for given promotion


To ensure that only correct ones applies
Apply promotion for given variants


Go to the next promotion


Execute second step
Catalog Promotion
Processing order
Take all product variants


This step can be outsourced to DB
Take all eligible catalog promotions


This step can be outsourced to DB
Apply promotion for given variant


Product Variant
Performance test setup
Photo by Loubna Aggoun on Unsplash
sylius.com
?
Dataset


7000 Product Variants


3 Catalog Promotions covering all of products at least once


2 Channels
Execution on single catalog promotion update


Triggering CatalogPromotionUpdated event
Environment


Production environment


No debug option


Removal of cache and cache warmup before each execution
<
E
10
sylius.com
11
Command Batching
Doctrine


Fetching only codes to avoid hydration


Flushing & transaction out-of-the-box


Clearance of entity manager
Symfony Messenger


No need for custom command


Flushing & transaction out-of-the-box


Sync & async switch is piece of cake
Photo by Shane Aldendorff on Unsplash
Let’s improve!
02
Photo by Bastien Plu on Unsplash
First round - bug
sylius.com
23
public function assignLocale(
TranslatableInterface $translatableEntity
): void {
$fallbackLocale = $this
->translationLocaleProvider
->getDefaultLocaleCode()
;
try {
$currentLocale = $this->localeContext->getLocaleCode();
} catch (LocaleNotFoundException) {
$currentLocale = $fallbackLocale;
}
$translatableEntity->setCurrentLocale($currentLocale);
$translatableEntity->setFallbackLocale($fallbackLocale);
}
sylius.com
24
public function assignLocale(
TranslatableInterface $translatableEntity
): void {
$fallbackLocale = $this
->translationLocaleProvider
->getDefaultLocaleCode()
;
try {
$currentLocale = $this->localeContext->getLocaleCode();
} catch (LocaleNotFoundException) {
$currentLocale = $fallbackLocale;
}
$translatableEntity->setCurrentLocale($currentLocale);
$translatableEntity->setFallbackLocale($fallbackLocale);
}
sylius.com
25
public function assignLocale(
TranslatableInterface $translatableEntity
): void {
$fallbackLocale = $this
->translationLocaleProvider
->getDefaultLocaleCode()
;
try {
$currentLocale = $this->localeContext->getLocaleCode();
} catch (LocaleNotFoundException) {
$currentLocale = $fallbackLocale;
}
$translatableEntity->setCurrentLocale($currentLocale);
$translatableEntity->setFallbackLocale($fallbackLocale);
}
sylius.com
26
public function assignLocale(
TranslatableInterface $translatableEntity
): void {
$fallbackLocale = $this
->translationLocaleProvider
->getDefaultLocaleCode()
;
try {
$currentLocale = $this->localeContext->getLocaleCode();
} catch (LocaleNotFoundException) {
$currentLocale = $fallbackLocale;
}
$translatableEntity->setCurrentLocale($currentLocale);
$translatableEntity->setFallbackLocale($fallbackLocale);
}
sylius.com
27
public function assignLocale(
TranslatableInterface $translatableEntity
): void {
$fallbackLocale = $this
->translationLocaleProvider
->getDefaultLocaleCode()
;
try {
$currentLocale = $this->localeContext->getLocaleCode();
} catch (LocaleNotFoundException) {
$currentLocale = $fallbackLocale;
}
$translatableEntity->setCurrentLocale($currentLocale);
$translatableEntity->setFallbackLocale($fallbackLocale);
}
sylius.com
28
if ($this->commandBasedChecker->isExecutedFromCLI()) {
$translatableEntity->setCurrentLocale($fallbackLocale);
return;
}
try {
$currentLocale = $this->localeContext->getLocaleCode();
} catch (LocaleNotFoundException) {
$currentLocale = $fallbackLocale;
}
sylius.com
34
Conclusion?
Photo by Pietro Mattia on Unsplash
Second round - N+1
sylius.com
41
public function findByCodes(array $codes): array
{
return $this->createQueryBuilder('o')
->andWhere('o.code IN (:codes)')
->setParameter('codes', $codes)
->getQuery()
->getResult()
;
}
sylius.com
42
public function findByCodes(array $codes): array
{
return $this->createQueryBuilder('o')
->addSelect('product')
->addSelect('channelPricings')
->addSelect('appliedPromotions')
->addSelect('productTaxon')
->addSelect('taxon')
->leftJoin('o.channelPricings', 'channelPricings')
->leftJoin('channelPricings.appliedPromotions', 'appliedPromotions')
->leftJoin('o.product', 'product')
->leftJoin('product.productTaxons', 'productTaxon')
->leftJoin('productTaxon.taxon', 'taxon')
->andWhere('o.code IN (:codes)')
->setParameter('codes', $codes)
->getQuery()
->getResult()
;
}
sylius.com
43
public function findByCodes(array $codes): array
{
return $this->createQueryBuilder('o')
->addSelect('product')
->addSelect('channelPricings')
->addSelect('appliedPromotions')
->addSelect('productTaxon')
->addSelect('taxon')
->leftJoin('o.channelPricings', 'channelPricings')
->leftJoin('channelPricings.appliedPromotions', 'appliedPromotions')
->leftJoin('o.product', 'product')
->leftJoin('product.productTaxons', 'productTaxon')
->leftJoin('productTaxon.taxon', 'taxon')
->andWhere('o.code IN (:codes)')
->setParameter('codes', $codes)
->getQuery()
->getResult()
;
}
sylius.com
50
Conclusion?
Photo by Ralfs Blumbergs on Unsplash
Third round - EM Clear
sylius.com
52
Up to this moment


All operations were executed synchronously
sylius.com
54
After implementation of EntityManagerClearer and
switching to async processing
sylius.com
56
Conclusion?
Photo by Bastien Plu on Unsplash
Summary
Photo by Glen Rushton on Unsplash
Final round - native query
sylius.com
64
private function clearChannelPricing(
ChannelPricingInterface $channelPricing
): void {
if ($channelPricing->getAppliedPromotions()->isEmpty()) {
return;
}
if ($channelPricing->getOriginalPrice() !== null) {
$channelPricing->setPrice($channelPricing->getOriginalPrice());
}
$channelPricing->clearAppliedPromotions();
}
sylius.com
65
private function clearChannelPricing(
ChannelPricingInterface $channelPricing
): void {
if ($channelPricing->getAppliedPromotions()->isEmpty()) {
return;
}
if ($channelPricing->getOriginalPrice() !== null) {
$channelPricing->setPrice($channelPricing->getOriginalPrice());
}
$channelPricing->clearAppliedPromotions();
}
sylius.com
66
private function clearChannelPricing(
ChannelPricingInterface $channelPricing
): void {
if ($channelPricing->getAppliedPromotions()->isEmpty()) {
return;
}
if ($channelPricing->getOriginalPrice() !== null) {
$channelPricing->setPrice($channelPricing->getOriginalPrice());
}
$channelPricing->clearAppliedPromotions();
}
sylius.com
67
private function clearChannelPricing(
ChannelPricingInterface $channelPricing
): void {
if ($channelPricing->getAppliedPromotions()->isEmpty()) {
return;
}
if ($channelPricing->getOriginalPrice() !== null) {
$channelPricing->setPrice($channelPricing->getOriginalPrice());
}
$channelPricing->clearAppliedPromotions();
}
sylius.com
68
public function __invoke(
ApplyCatalogPromotionsOnVariants $updateVariants
): void {
$catalogPromotions = $this->catalogPromotionsProvider->provide();
$variants = $this->productVariantRepository->findByCodes(
$updateVariants->variantsCodes
);
foreach ($variants as $variant) {
$this->clearer->clearVariant($variant);
foreach ($catalogPromotions as $promotion) {
$this->catalogPromotionApplicator->applyOnVariant(
$variant,
$promotion
);
}
}
}
sylius.com
69
public function __invoke(
ApplyCatalogPromotionsOnVariants $updateVariants
): void {
$catalogPromotions = $this->catalogPromotionsProvider->provide();
$variants = $this->productVariantRepository->findByCodes(
$updateVariants->variantsCodes
);
foreach ($variants as $variant) {
$this->clearer->clearVariant($variant);
foreach ($catalogPromotions as $promotion) {
$this->catalogPromotionApplicator->applyOnVariant(
$variant,
$promotion
);
}
}
}
sylius.com
70
public function __invoke(
ApplyCatalogPromotionsOnVariants $updateVariants
): void {
$catalogPromotions = $this->catalogPromotionsProvider->provide();
$variants = $this->productVariantRepository->findByCodes(
$updateVariants->variantsCodes
);
foreach ($variants as $variant) {
$this->clearer->clearVariant($variant);
foreach ($catalogPromotions as $promotion) {
$this->catalogPromotionApplicator->applyOnVariant(
$variant,
$promotion
);
}
}
}
sylius.com
71
public function __invoke(
ApplyCatalogPromotionsOnVariants $updateVariants
): void {
$catalogPromotions = $this->catalogPromotionsProvider->provide();
$variants = $this->productVariantRepository->findByCodes(
$updateVariants->variantsCodes
);
foreach ($variants as $variant) {
$this->clearer->clearVariant($variant);
foreach ($catalogPromotions as $promotion) {
$this->catalogPromotionApplicator->applyOnVariant(
$variant,
$promotion
);
}
}
}
sylius.com
72
public function __invoke(
ApplyCatalogPromotionsOnVariants $updateVariants
): void {
$catalogPromotions = $this->catalogPromotionsProvider->provide();
$variants = $this->productVariantRepository->findByCodes(
$updateVariants->variantsCodes
);
foreach ($variants as $variant) {
$this->clearer->clearVariant($variant);
foreach ($catalogPromotions as $promotion) {
$this->catalogPromotionApplicator->applyOnVariant(
$variant,
$promotion
);
}
}
}
sylius.com
73
public function __invoke(
ApplyCatalogPromotionsOnVariants $updateVariants
): void {
$catalogPromotions = $this->catalogPromotionsProvider->provide();
$variants = $this->productVariantRepository->findByCodes(
$updateVariants->variantsCodes
);
foreach ($variants as $variant) {
$this->clearer->clearVariant($variant);
foreach ($catalogPromotions as $promotion) {
$this->catalogPromotionApplicator->applyOnVariant(
$variant,
$promotion
);
}
}
}
sylius.com
74
public function __invoke(
ApplyCatalogPromotionsOnVariants $updateVariants
): void {
$catalogPromotions = $this->catalogPromotionsProvider->provide();
$variants = $this->productVariantRepository->findByCodes(
$updateVariants->variantsCodes
);
foreach ($variants as $variant) {
$this->clearer->clearVariant($variant);
foreach ($catalogPromotions as $promotion) {
$this->catalogPromotionApplicator->applyOnVariant(
$variant,
$promotion
);
}
}
}
sylius.com
75
foreach ($variants as $variant) {
$this->clearer->clearVariant($variant);
}
$codes = array_column($updateVariants->variantsCodes, 'code');
$connection = $this->entityManager->getConnection();
$connection->executeQuery('
DELETE scpcp FROM sylius_channel_pricing_catalog_promotions scpcp
JOIN sylius_channel_pricing scp ON scpcp.channel_pricing_id = scp.id
JOIN sylius_product_variant spv ON scp.product_variant_id = spv.id
WHERE spv.code IN (?)’,
[$codes],
[Connection::PARAM_STR_ARRAY]
);
foreach ($variants as $variant) {
// Apply Catalog promotion
}
sylius.com
76
foreach ($variants as $variant) {
$this->clearer->clearVariant($variant);
}
$codes = array_column($updateVariants->variantsCodes, 'code');
$connection = $this->entityManager->getConnection();
$connection->executeQuery('
DELETE scpcp FROM sylius_channel_pricing_catalog_promotions scpcp
JOIN sylius_channel_pricing scp ON scpcp.channel_pricing_id = scp.id
JOIN sylius_product_variant spv ON scp.product_variant_id = spv.id
WHERE spv.code IN (?)’,
[$codes],
[Connection::PARAM_STR_ARRAY]
);
foreach ($variants as $variant) {
// Apply Catalog promotion
}
sylius.com
77
foreach ($variants as $variant) {
$this->clearer->clearVariant($variant);
}
$codes = array_column($updateVariants->variantsCodes, 'code');
$connection = $this->entityManager->getConnection();
$connection->executeQuery('
DELETE scpcp FROM sylius_channel_pricing_catalog_promotions scpcp
JOIN sylius_channel_pricing scp ON scpcp.channel_pricing_id = scp.id
JOIN sylius_product_variant spv ON scp.product_variant_id = spv.id
WHERE spv.code IN (?)’,
[$codes],
[Connection::PARAM_STR_ARRAY]
);
foreach ($variants as $variant) {
// Apply Catalog promotion
}
sylius.com
78
foreach ($variants as $variant) {
$this->clearer->clearVariant($variant);
}
$codes = array_column($updateVariants->variantsCodes, 'code');
$connection = $this->entityManager->getConnection();
$connection->executeQuery('
DELETE scpcp FROM sylius_channel_pricing_catalog_promotions scpcp
JOIN sylius_channel_pricing scp ON scpcp.channel_pricing_id = scp.id
JOIN sylius_product_variant spv ON scp.product_variant_id = spv.id
WHERE spv.code IN (?)’,
[$codes],
[Connection::PARAM_STR_ARRAY]
);
foreach ($variants as $variant) {
// Apply Catalog promotion
}
sylius.com
79
foreach ($variants as $variant) {
$this->clearer->clearVariant($variant);
}
$codes = array_column($updateVariants->variantsCodes, 'code');
$connection = $this->entityManager->getConnection();
$connection->executeQuery('
DELETE scpcp FROM sylius_channel_pricing_catalog_promotions scpcp
JOIN sylius_channel_pricing scp ON scpcp.channel_pricing_id = scp.id
JOIN sylius_product_variant spv ON scp.product_variant_id = spv.id
WHERE spv.code IN (?)’,
[$codes],
[Connection::PARAM_STR_ARRAY]
);
foreach ($variants as $variant) {
// Apply Catalog promotion
}
sylius.com
80
foreach ($variants as $variant) {
$this->clearer->clearVariant($variant);
}
$codes = array_column($updateVariants->variantsCodes, 'code');
$connection = $this->entityManager->getConnection();
$connection->executeQuery('
DELETE scpcp FROM sylius_channel_pricing_catalog_promotions scpcp
JOIN sylius_channel_pricing scp ON scpcp.channel_pricing_id = scp.id
JOIN sylius_product_variant spv ON scp.product_variant_id = spv.id
WHERE spv.code IN (?)’,
[$codes],
[Connection::PARAM_STR_ARRAY]
);
foreach ($variants as $variant) {
// Apply Catalog promotion
}
sylius.com
81
foreach ($variants as $variant) {
$this->clearer->clearVariant($variant);
}
$codes = array_column($updateVariants->variantsCodes, 'code');
$connection = $this->entityManager->getConnection();
$connection->executeQuery('
DELETE scpcp FROM sylius_channel_pricing_catalog_promotions scpcp
JOIN sylius_channel_pricing scp ON scpcp.channel_pricing_id = scp.id
JOIN sylius_product_variant spv ON scp.product_variant_id = spv.id
WHERE spv.code IN (?)’,
[$codes],
[Connection::PARAM_STR_ARRAY]
);
foreach ($variants as $variant) {
// Apply Catalog promotion
}
sylius.com
82
foreach ($variants as $variant) {
$this->clearer->clearVariant($variant);
}
$codes = array_column($updateVariants->variantsCodes, 'code');
$connection = $this->entityManager->getConnection();
$connection->executeQuery('
DELETE scpcp FROM sylius_channel_pricing_catalog_promotions scpcp
JOIN sylius_channel_pricing scp ON scpcp.channel_pricing_id = scp.id
JOIN sylius_product_variant spv ON scp.product_variant_id = spv.id
WHERE spv.code IN (?)’,
[$codes],
[Connection::PARAM_STR_ARRAY]
);
foreach ($variants as $variant) {
// Apply Catalog promotion
}
sylius.com
83
foreach ($variants as $variant) {
$this->clearer->clearVariant($variant);
}
$codes = array_column($updateVariants->variantsCodes, 'code');
$connection = $this->entityManager->getConnection();
$connection->executeQuery('
DELETE scpcp FROM sylius_channel_pricing_catalog_promotions scpcp
JOIN sylius_channel_pricing scp ON scpcp.channel_pricing_id = scp.id
JOIN sylius_product_variant spv ON scp.product_variant_id = spv.id
WHERE spv.code IN (?)’,
[$codes],
[Connection::PARAM_STR_ARRAY]
);
foreach ($variants as $variant) {
// Apply Catalog promotion
}
sylius.com
88
It didn’t work according to acceptance tests!
sylius.com
92
public function __invoke(
ClearCatalogPromotionsOnVariants $clearCatalogPromotionsOnVariants
): void {
$codes = array_column($clearCatalogPromotionsOnVariants->variantsCodes, ‘code');
$connection = $this->entityManager->getConnection();
$connection->executeQuery('
UPDATE sylius_channel_pricing scp
INNER JOIN sylius_channel_pricing_catalog_promotions scpcp ON
scpcp.channel_pricing_id = scp.id
JOIN sylius_product_variant spv ON scp.product_variant_id = spv.id
SET scp.price=scp.original_price
WHERE spv.code IN (?);', [$codes], [Connection::PARAM_STR_ARRAY]);
$connection->executeQuery('
DELETE scpcp FROM sylius_channel_pricing_catalog_promotions scpcp
JOIN sylius_channel_pricing scp ON scpcp.channel_pricing_id = scp.id
JOIN sylius_product_variant spv ON scp.product_variant_id = spv.id
WHERE spv.code IN (?)', [$codes], [Connection::PARAM_STR_ARRAY]);
}
sylius.com
93
public function __invoke(
ClearCatalogPromotionsOnVariants $clearCatalogPromotionsOnVariants
): void {
$codes = array_column($clearCatalogPromotionsOnVariants->variantsCodes, ‘code');
$connection = $this->entityManager->getConnection();
$connection->executeQuery('
UPDATE sylius_channel_pricing scp
INNER JOIN sylius_channel_pricing_catalog_promotions scpcp ON
scpcp.channel_pricing_id = scp.id
JOIN sylius_product_variant spv ON scp.product_variant_id = spv.id
SET scp.price=scp.original_price
WHERE spv.code IN (?);', [$codes], [Connection::PARAM_STR_ARRAY]);
$connection->executeQuery('
DELETE scpcp FROM sylius_channel_pricing_catalog_promotions scpcp
JOIN sylius_channel_pricing scp ON scpcp.channel_pricing_id = scp.id
JOIN sylius_product_variant spv ON scp.product_variant_id = spv.id
WHERE spv.code IN (?)', [$codes], [Connection::PARAM_STR_ARRAY]);
}
sylius.com
94
public function __invoke(
ClearCatalogPromotionsOnVariants $clearCatalogPromotionsOnVariants
): void {
$codes = array_column($clearCatalogPromotionsOnVariants->variantsCodes, ‘code');
$connection = $this->entityManager->getConnection();
$connection->executeQuery('
UPDATE sylius_channel_pricing scp
INNER JOIN sylius_channel_pricing_catalog_promotions scpcp ON
scpcp.channel_pricing_id = scp.id
JOIN sylius_product_variant spv ON scp.product_variant_id = spv.id
SET scp.price=scp.original_price
WHERE spv.code IN (?);', [$codes], [Connection::PARAM_STR_ARRAY]);
$connection->executeQuery('
DELETE scpcp FROM sylius_channel_pricing_catalog_promotions scpcp
JOIN sylius_channel_pricing scp ON scpcp.channel_pricing_id = scp.id
JOIN sylius_product_variant spv ON scp.product_variant_id = spv.id
WHERE spv.code IN (?)', [$codes], [Connection::PARAM_STR_ARRAY]);
}
sylius.com
95
public function __invoke(
ClearCatalogPromotionsOnVariants $clearCatalogPromotionsOnVariants
): void {
$codes = array_column($clearCatalogPromotionsOnVariants->variantsCodes, ‘code');
$connection = $this->entityManager->getConnection();
$connection->executeQuery('
UPDATE sylius_channel_pricing scp
INNER JOIN sylius_channel_pricing_catalog_promotions scpcp ON
scpcp.channel_pricing_id = scp.id
JOIN sylius_product_variant spv ON scp.product_variant_id = spv.id
SET scp.price=scp.original_price
WHERE spv.code IN (?);', [$codes], [Connection::PARAM_STR_ARRAY]);
$connection->executeQuery('
DELETE scpcp FROM sylius_channel_pricing_catalog_promotions scpcp
JOIN sylius_channel_pricing scp ON scpcp.channel_pricing_id = scp.id
JOIN sylius_product_variant spv ON scp.product_variant_id = spv.id
WHERE spv.code IN (?)', [$codes], [Connection::PARAM_STR_ARRAY]);
}
sylius.com
96
public function __invoke(
ClearCatalogPromotionsOnVariants $clearCatalogPromotionsOnVariants
): void {
$codes = array_column($clearCatalogPromotionsOnVariants->variantsCodes, ‘code');
$connection = $this->entityManager->getConnection();
$connection->executeQuery('
UPDATE sylius_channel_pricing scp
INNER JOIN sylius_channel_pricing_catalog_promotions scpcp ON
scpcp.channel_pricing_id = scp.id
JOIN sylius_product_variant spv ON scp.product_variant_id = spv.id
SET scp.price=scp.original_price
WHERE spv.code IN (?);', [$codes], [Connection::PARAM_STR_ARRAY]);
$connection->executeQuery('
DELETE scpcp FROM sylius_channel_pricing_catalog_promotions scpcp
JOIN sylius_channel_pricing scp ON scpcp.channel_pricing_id = scp.id
JOIN sylius_product_variant spv ON scp.product_variant_id = spv.id
WHERE spv.code IN (?)', [$codes], [Connection::PARAM_STR_ARRAY]);
}
sylius.com
?
101
What if we change


the catalog promotion


con
fi
guration?
sylius.com
106
Conclusion?
Photo by Mikhail Vasilyev on Unsplash
03
Takeaways
sylius.com
109
Check the highest class in your callgraph


And look for bugs
Remember to clear Entity Manager when batch processing


Unless you are executing asynchronously messages with Symfony Messenger
Load all data required to perform operation


But not more
Pure DQL may bring bene
fi
ts


But is hard to do it properly for complicated objects or mix it with ORM
sylius.com
110
SyliusCon
This autumn - at 27th of October


In a Sylius hometown: Łódź
sylius.com
111
Thank you!
Photo by Maksym Harbar on Unsplash

How to optimize background processes - when Sylius meets Blackfire

  • 1.
    How to optimizebackground processes when Sylius meets Black fi re Photo by Laura Ockel on Unsplash
  • 2.
  • 3.
    Photo by RyanQuintal on Unsplash Feature overview
  • 4.
    sylius.com 4 Product variants Always haveY Channel Pricings, Where Y equals amount of Channels in shop Product May have from 1 to N variants Channel Pricing Contains end pricing for customer Sylius pricing model
  • 5.
    sylius.com Discounting rules set 5 Maincon fi guration Dates Channels Enabled Priority Exclusiveness Processing scope Variants Products Taxonomy Resulting actions Fixed discount Percentage discount
  • 6.
    sylius.com 6 1 - 10 Toprocess updates of Products through UI interface Minimal processable scope As a business rule 10k - 100k Default behaviour should Be consider for these values Typical Sylius project Based on the clients we are working the most Over 1kk Feature should be possible To be used with this scope Maximal processable scope From the projects we know Dataset size
  • 7.
    Photo by BrendanChurch on Unsplash Initial dilema
  • 8.
    sylius.com 8 Take all eligiblecatalog promotions This step can be outsourced to DB Fetch variants for given promotion To ensure that only correct ones applies Apply promotion for given variants Go to the next promotion Execute second step Catalog Promotion Processing order Take all product variants This step can be outsourced to DB Take all eligible catalog promotions This step can be outsourced to DB Apply promotion for given variant Product Variant
  • 9.
    Performance test setup Photoby Loubna Aggoun on Unsplash
  • 10.
    sylius.com ? Dataset 7000 Product Variants 3Catalog Promotions covering all of products at least once 2 Channels Execution on single catalog promotion update Triggering CatalogPromotionUpdated event Environment Production environment No debug option Removal of cache and cache warmup before each execution < E 10
  • 11.
    sylius.com 11 Command Batching Doctrine Fetching onlycodes to avoid hydration Flushing & transaction out-of-the-box Clearance of entity manager Symfony Messenger No need for custom command Flushing & transaction out-of-the-box Sync & async switch is piece of cake
  • 12.
    Photo by ShaneAldendorff on Unsplash Let’s improve! 02
  • 13.
    Photo by BastienPlu on Unsplash First round - bug
  • 23.
    sylius.com 23 public function assignLocale( TranslatableInterface$translatableEntity ): void { $fallbackLocale = $this ->translationLocaleProvider ->getDefaultLocaleCode() ; try { $currentLocale = $this->localeContext->getLocaleCode(); } catch (LocaleNotFoundException) { $currentLocale = $fallbackLocale; } $translatableEntity->setCurrentLocale($currentLocale); $translatableEntity->setFallbackLocale($fallbackLocale); }
  • 24.
    sylius.com 24 public function assignLocale( TranslatableInterface$translatableEntity ): void { $fallbackLocale = $this ->translationLocaleProvider ->getDefaultLocaleCode() ; try { $currentLocale = $this->localeContext->getLocaleCode(); } catch (LocaleNotFoundException) { $currentLocale = $fallbackLocale; } $translatableEntity->setCurrentLocale($currentLocale); $translatableEntity->setFallbackLocale($fallbackLocale); }
  • 25.
    sylius.com 25 public function assignLocale( TranslatableInterface$translatableEntity ): void { $fallbackLocale = $this ->translationLocaleProvider ->getDefaultLocaleCode() ; try { $currentLocale = $this->localeContext->getLocaleCode(); } catch (LocaleNotFoundException) { $currentLocale = $fallbackLocale; } $translatableEntity->setCurrentLocale($currentLocale); $translatableEntity->setFallbackLocale($fallbackLocale); }
  • 26.
    sylius.com 26 public function assignLocale( TranslatableInterface$translatableEntity ): void { $fallbackLocale = $this ->translationLocaleProvider ->getDefaultLocaleCode() ; try { $currentLocale = $this->localeContext->getLocaleCode(); } catch (LocaleNotFoundException) { $currentLocale = $fallbackLocale; } $translatableEntity->setCurrentLocale($currentLocale); $translatableEntity->setFallbackLocale($fallbackLocale); }
  • 27.
    sylius.com 27 public function assignLocale( TranslatableInterface$translatableEntity ): void { $fallbackLocale = $this ->translationLocaleProvider ->getDefaultLocaleCode() ; try { $currentLocale = $this->localeContext->getLocaleCode(); } catch (LocaleNotFoundException) { $currentLocale = $fallbackLocale; } $translatableEntity->setCurrentLocale($currentLocale); $translatableEntity->setFallbackLocale($fallbackLocale); }
  • 28.
    sylius.com 28 if ($this->commandBasedChecker->isExecutedFromCLI()) { $translatableEntity->setCurrentLocale($fallbackLocale); return; } try{ $currentLocale = $this->localeContext->getLocaleCode(); } catch (LocaleNotFoundException) { $currentLocale = $fallbackLocale; }
  • 34.
  • 35.
    Photo by PietroMattia on Unsplash Second round - N+1
  • 41.
    sylius.com 41 public function findByCodes(array$codes): array { return $this->createQueryBuilder('o') ->andWhere('o.code IN (:codes)') ->setParameter('codes', $codes) ->getQuery() ->getResult() ; }
  • 42.
    sylius.com 42 public function findByCodes(array$codes): array { return $this->createQueryBuilder('o') ->addSelect('product') ->addSelect('channelPricings') ->addSelect('appliedPromotions') ->addSelect('productTaxon') ->addSelect('taxon') ->leftJoin('o.channelPricings', 'channelPricings') ->leftJoin('channelPricings.appliedPromotions', 'appliedPromotions') ->leftJoin('o.product', 'product') ->leftJoin('product.productTaxons', 'productTaxon') ->leftJoin('productTaxon.taxon', 'taxon') ->andWhere('o.code IN (:codes)') ->setParameter('codes', $codes) ->getQuery() ->getResult() ; }
  • 43.
    sylius.com 43 public function findByCodes(array$codes): array { return $this->createQueryBuilder('o') ->addSelect('product') ->addSelect('channelPricings') ->addSelect('appliedPromotions') ->addSelect('productTaxon') ->addSelect('taxon') ->leftJoin('o.channelPricings', 'channelPricings') ->leftJoin('channelPricings.appliedPromotions', 'appliedPromotions') ->leftJoin('o.product', 'product') ->leftJoin('product.productTaxons', 'productTaxon') ->leftJoin('productTaxon.taxon', 'taxon') ->andWhere('o.code IN (:codes)') ->setParameter('codes', $codes) ->getQuery() ->getResult() ; }
  • 50.
  • 51.
    Photo by RalfsBlumbergs on Unsplash Third round - EM Clear
  • 52.
    sylius.com 52 Up to thismoment All operations were executed synchronously
  • 54.
    sylius.com 54 After implementation ofEntityManagerClearer and switching to async processing
  • 56.
  • 57.
    Photo by BastienPlu on Unsplash Summary
  • 60.
    Photo by GlenRushton on Unsplash Final round - native query
  • 64.
    sylius.com 64 private function clearChannelPricing( ChannelPricingInterface$channelPricing ): void { if ($channelPricing->getAppliedPromotions()->isEmpty()) { return; } if ($channelPricing->getOriginalPrice() !== null) { $channelPricing->setPrice($channelPricing->getOriginalPrice()); } $channelPricing->clearAppliedPromotions(); }
  • 65.
    sylius.com 65 private function clearChannelPricing( ChannelPricingInterface$channelPricing ): void { if ($channelPricing->getAppliedPromotions()->isEmpty()) { return; } if ($channelPricing->getOriginalPrice() !== null) { $channelPricing->setPrice($channelPricing->getOriginalPrice()); } $channelPricing->clearAppliedPromotions(); }
  • 66.
    sylius.com 66 private function clearChannelPricing( ChannelPricingInterface$channelPricing ): void { if ($channelPricing->getAppliedPromotions()->isEmpty()) { return; } if ($channelPricing->getOriginalPrice() !== null) { $channelPricing->setPrice($channelPricing->getOriginalPrice()); } $channelPricing->clearAppliedPromotions(); }
  • 67.
    sylius.com 67 private function clearChannelPricing( ChannelPricingInterface$channelPricing ): void { if ($channelPricing->getAppliedPromotions()->isEmpty()) { return; } if ($channelPricing->getOriginalPrice() !== null) { $channelPricing->setPrice($channelPricing->getOriginalPrice()); } $channelPricing->clearAppliedPromotions(); }
  • 68.
    sylius.com 68 public function __invoke( ApplyCatalogPromotionsOnVariants$updateVariants ): void { $catalogPromotions = $this->catalogPromotionsProvider->provide(); $variants = $this->productVariantRepository->findByCodes( $updateVariants->variantsCodes ); foreach ($variants as $variant) { $this->clearer->clearVariant($variant); foreach ($catalogPromotions as $promotion) { $this->catalogPromotionApplicator->applyOnVariant( $variant, $promotion ); } } }
  • 69.
    sylius.com 69 public function __invoke( ApplyCatalogPromotionsOnVariants$updateVariants ): void { $catalogPromotions = $this->catalogPromotionsProvider->provide(); $variants = $this->productVariantRepository->findByCodes( $updateVariants->variantsCodes ); foreach ($variants as $variant) { $this->clearer->clearVariant($variant); foreach ($catalogPromotions as $promotion) { $this->catalogPromotionApplicator->applyOnVariant( $variant, $promotion ); } } }
  • 70.
    sylius.com 70 public function __invoke( ApplyCatalogPromotionsOnVariants$updateVariants ): void { $catalogPromotions = $this->catalogPromotionsProvider->provide(); $variants = $this->productVariantRepository->findByCodes( $updateVariants->variantsCodes ); foreach ($variants as $variant) { $this->clearer->clearVariant($variant); foreach ($catalogPromotions as $promotion) { $this->catalogPromotionApplicator->applyOnVariant( $variant, $promotion ); } } }
  • 71.
    sylius.com 71 public function __invoke( ApplyCatalogPromotionsOnVariants$updateVariants ): void { $catalogPromotions = $this->catalogPromotionsProvider->provide(); $variants = $this->productVariantRepository->findByCodes( $updateVariants->variantsCodes ); foreach ($variants as $variant) { $this->clearer->clearVariant($variant); foreach ($catalogPromotions as $promotion) { $this->catalogPromotionApplicator->applyOnVariant( $variant, $promotion ); } } }
  • 72.
    sylius.com 72 public function __invoke( ApplyCatalogPromotionsOnVariants$updateVariants ): void { $catalogPromotions = $this->catalogPromotionsProvider->provide(); $variants = $this->productVariantRepository->findByCodes( $updateVariants->variantsCodes ); foreach ($variants as $variant) { $this->clearer->clearVariant($variant); foreach ($catalogPromotions as $promotion) { $this->catalogPromotionApplicator->applyOnVariant( $variant, $promotion ); } } }
  • 73.
    sylius.com 73 public function __invoke( ApplyCatalogPromotionsOnVariants$updateVariants ): void { $catalogPromotions = $this->catalogPromotionsProvider->provide(); $variants = $this->productVariantRepository->findByCodes( $updateVariants->variantsCodes ); foreach ($variants as $variant) { $this->clearer->clearVariant($variant); foreach ($catalogPromotions as $promotion) { $this->catalogPromotionApplicator->applyOnVariant( $variant, $promotion ); } } }
  • 74.
    sylius.com 74 public function __invoke( ApplyCatalogPromotionsOnVariants$updateVariants ): void { $catalogPromotions = $this->catalogPromotionsProvider->provide(); $variants = $this->productVariantRepository->findByCodes( $updateVariants->variantsCodes ); foreach ($variants as $variant) { $this->clearer->clearVariant($variant); foreach ($catalogPromotions as $promotion) { $this->catalogPromotionApplicator->applyOnVariant( $variant, $promotion ); } } }
  • 75.
    sylius.com 75 foreach ($variants as$variant) { $this->clearer->clearVariant($variant); } $codes = array_column($updateVariants->variantsCodes, 'code'); $connection = $this->entityManager->getConnection(); $connection->executeQuery(' DELETE scpcp FROM sylius_channel_pricing_catalog_promotions scpcp JOIN sylius_channel_pricing scp ON scpcp.channel_pricing_id = scp.id JOIN sylius_product_variant spv ON scp.product_variant_id = spv.id WHERE spv.code IN (?)’, [$codes], [Connection::PARAM_STR_ARRAY] ); foreach ($variants as $variant) { // Apply Catalog promotion }
  • 76.
    sylius.com 76 foreach ($variants as$variant) { $this->clearer->clearVariant($variant); } $codes = array_column($updateVariants->variantsCodes, 'code'); $connection = $this->entityManager->getConnection(); $connection->executeQuery(' DELETE scpcp FROM sylius_channel_pricing_catalog_promotions scpcp JOIN sylius_channel_pricing scp ON scpcp.channel_pricing_id = scp.id JOIN sylius_product_variant spv ON scp.product_variant_id = spv.id WHERE spv.code IN (?)’, [$codes], [Connection::PARAM_STR_ARRAY] ); foreach ($variants as $variant) { // Apply Catalog promotion }
  • 77.
    sylius.com 77 foreach ($variants as$variant) { $this->clearer->clearVariant($variant); } $codes = array_column($updateVariants->variantsCodes, 'code'); $connection = $this->entityManager->getConnection(); $connection->executeQuery(' DELETE scpcp FROM sylius_channel_pricing_catalog_promotions scpcp JOIN sylius_channel_pricing scp ON scpcp.channel_pricing_id = scp.id JOIN sylius_product_variant spv ON scp.product_variant_id = spv.id WHERE spv.code IN (?)’, [$codes], [Connection::PARAM_STR_ARRAY] ); foreach ($variants as $variant) { // Apply Catalog promotion }
  • 78.
    sylius.com 78 foreach ($variants as$variant) { $this->clearer->clearVariant($variant); } $codes = array_column($updateVariants->variantsCodes, 'code'); $connection = $this->entityManager->getConnection(); $connection->executeQuery(' DELETE scpcp FROM sylius_channel_pricing_catalog_promotions scpcp JOIN sylius_channel_pricing scp ON scpcp.channel_pricing_id = scp.id JOIN sylius_product_variant spv ON scp.product_variant_id = spv.id WHERE spv.code IN (?)’, [$codes], [Connection::PARAM_STR_ARRAY] ); foreach ($variants as $variant) { // Apply Catalog promotion }
  • 79.
    sylius.com 79 foreach ($variants as$variant) { $this->clearer->clearVariant($variant); } $codes = array_column($updateVariants->variantsCodes, 'code'); $connection = $this->entityManager->getConnection(); $connection->executeQuery(' DELETE scpcp FROM sylius_channel_pricing_catalog_promotions scpcp JOIN sylius_channel_pricing scp ON scpcp.channel_pricing_id = scp.id JOIN sylius_product_variant spv ON scp.product_variant_id = spv.id WHERE spv.code IN (?)’, [$codes], [Connection::PARAM_STR_ARRAY] ); foreach ($variants as $variant) { // Apply Catalog promotion }
  • 80.
    sylius.com 80 foreach ($variants as$variant) { $this->clearer->clearVariant($variant); } $codes = array_column($updateVariants->variantsCodes, 'code'); $connection = $this->entityManager->getConnection(); $connection->executeQuery(' DELETE scpcp FROM sylius_channel_pricing_catalog_promotions scpcp JOIN sylius_channel_pricing scp ON scpcp.channel_pricing_id = scp.id JOIN sylius_product_variant spv ON scp.product_variant_id = spv.id WHERE spv.code IN (?)’, [$codes], [Connection::PARAM_STR_ARRAY] ); foreach ($variants as $variant) { // Apply Catalog promotion }
  • 81.
    sylius.com 81 foreach ($variants as$variant) { $this->clearer->clearVariant($variant); } $codes = array_column($updateVariants->variantsCodes, 'code'); $connection = $this->entityManager->getConnection(); $connection->executeQuery(' DELETE scpcp FROM sylius_channel_pricing_catalog_promotions scpcp JOIN sylius_channel_pricing scp ON scpcp.channel_pricing_id = scp.id JOIN sylius_product_variant spv ON scp.product_variant_id = spv.id WHERE spv.code IN (?)’, [$codes], [Connection::PARAM_STR_ARRAY] ); foreach ($variants as $variant) { // Apply Catalog promotion }
  • 82.
    sylius.com 82 foreach ($variants as$variant) { $this->clearer->clearVariant($variant); } $codes = array_column($updateVariants->variantsCodes, 'code'); $connection = $this->entityManager->getConnection(); $connection->executeQuery(' DELETE scpcp FROM sylius_channel_pricing_catalog_promotions scpcp JOIN sylius_channel_pricing scp ON scpcp.channel_pricing_id = scp.id JOIN sylius_product_variant spv ON scp.product_variant_id = spv.id WHERE spv.code IN (?)’, [$codes], [Connection::PARAM_STR_ARRAY] ); foreach ($variants as $variant) { // Apply Catalog promotion }
  • 83.
    sylius.com 83 foreach ($variants as$variant) { $this->clearer->clearVariant($variant); } $codes = array_column($updateVariants->variantsCodes, 'code'); $connection = $this->entityManager->getConnection(); $connection->executeQuery(' DELETE scpcp FROM sylius_channel_pricing_catalog_promotions scpcp JOIN sylius_channel_pricing scp ON scpcp.channel_pricing_id = scp.id JOIN sylius_product_variant spv ON scp.product_variant_id = spv.id WHERE spv.code IN (?)’, [$codes], [Connection::PARAM_STR_ARRAY] ); foreach ($variants as $variant) { // Apply Catalog promotion }
  • 88.
    sylius.com 88 It didn’t workaccording to acceptance tests!
  • 92.
    sylius.com 92 public function __invoke( ClearCatalogPromotionsOnVariants$clearCatalogPromotionsOnVariants ): void { $codes = array_column($clearCatalogPromotionsOnVariants->variantsCodes, ‘code'); $connection = $this->entityManager->getConnection(); $connection->executeQuery(' UPDATE sylius_channel_pricing scp INNER JOIN sylius_channel_pricing_catalog_promotions scpcp ON scpcp.channel_pricing_id = scp.id JOIN sylius_product_variant spv ON scp.product_variant_id = spv.id SET scp.price=scp.original_price WHERE spv.code IN (?);', [$codes], [Connection::PARAM_STR_ARRAY]); $connection->executeQuery(' DELETE scpcp FROM sylius_channel_pricing_catalog_promotions scpcp JOIN sylius_channel_pricing scp ON scpcp.channel_pricing_id = scp.id JOIN sylius_product_variant spv ON scp.product_variant_id = spv.id WHERE spv.code IN (?)', [$codes], [Connection::PARAM_STR_ARRAY]); }
  • 93.
    sylius.com 93 public function __invoke( ClearCatalogPromotionsOnVariants$clearCatalogPromotionsOnVariants ): void { $codes = array_column($clearCatalogPromotionsOnVariants->variantsCodes, ‘code'); $connection = $this->entityManager->getConnection(); $connection->executeQuery(' UPDATE sylius_channel_pricing scp INNER JOIN sylius_channel_pricing_catalog_promotions scpcp ON scpcp.channel_pricing_id = scp.id JOIN sylius_product_variant spv ON scp.product_variant_id = spv.id SET scp.price=scp.original_price WHERE spv.code IN (?);', [$codes], [Connection::PARAM_STR_ARRAY]); $connection->executeQuery(' DELETE scpcp FROM sylius_channel_pricing_catalog_promotions scpcp JOIN sylius_channel_pricing scp ON scpcp.channel_pricing_id = scp.id JOIN sylius_product_variant spv ON scp.product_variant_id = spv.id WHERE spv.code IN (?)', [$codes], [Connection::PARAM_STR_ARRAY]); }
  • 94.
    sylius.com 94 public function __invoke( ClearCatalogPromotionsOnVariants$clearCatalogPromotionsOnVariants ): void { $codes = array_column($clearCatalogPromotionsOnVariants->variantsCodes, ‘code'); $connection = $this->entityManager->getConnection(); $connection->executeQuery(' UPDATE sylius_channel_pricing scp INNER JOIN sylius_channel_pricing_catalog_promotions scpcp ON scpcp.channel_pricing_id = scp.id JOIN sylius_product_variant spv ON scp.product_variant_id = spv.id SET scp.price=scp.original_price WHERE spv.code IN (?);', [$codes], [Connection::PARAM_STR_ARRAY]); $connection->executeQuery(' DELETE scpcp FROM sylius_channel_pricing_catalog_promotions scpcp JOIN sylius_channel_pricing scp ON scpcp.channel_pricing_id = scp.id JOIN sylius_product_variant spv ON scp.product_variant_id = spv.id WHERE spv.code IN (?)', [$codes], [Connection::PARAM_STR_ARRAY]); }
  • 95.
    sylius.com 95 public function __invoke( ClearCatalogPromotionsOnVariants$clearCatalogPromotionsOnVariants ): void { $codes = array_column($clearCatalogPromotionsOnVariants->variantsCodes, ‘code'); $connection = $this->entityManager->getConnection(); $connection->executeQuery(' UPDATE sylius_channel_pricing scp INNER JOIN sylius_channel_pricing_catalog_promotions scpcp ON scpcp.channel_pricing_id = scp.id JOIN sylius_product_variant spv ON scp.product_variant_id = spv.id SET scp.price=scp.original_price WHERE spv.code IN (?);', [$codes], [Connection::PARAM_STR_ARRAY]); $connection->executeQuery(' DELETE scpcp FROM sylius_channel_pricing_catalog_promotions scpcp JOIN sylius_channel_pricing scp ON scpcp.channel_pricing_id = scp.id JOIN sylius_product_variant spv ON scp.product_variant_id = spv.id WHERE spv.code IN (?)', [$codes], [Connection::PARAM_STR_ARRAY]); }
  • 96.
    sylius.com 96 public function __invoke( ClearCatalogPromotionsOnVariants$clearCatalogPromotionsOnVariants ): void { $codes = array_column($clearCatalogPromotionsOnVariants->variantsCodes, ‘code'); $connection = $this->entityManager->getConnection(); $connection->executeQuery(' UPDATE sylius_channel_pricing scp INNER JOIN sylius_channel_pricing_catalog_promotions scpcp ON scpcp.channel_pricing_id = scp.id JOIN sylius_product_variant spv ON scp.product_variant_id = spv.id SET scp.price=scp.original_price WHERE spv.code IN (?);', [$codes], [Connection::PARAM_STR_ARRAY]); $connection->executeQuery(' DELETE scpcp FROM sylius_channel_pricing_catalog_promotions scpcp JOIN sylius_channel_pricing scp ON scpcp.channel_pricing_id = scp.id JOIN sylius_product_variant spv ON scp.product_variant_id = spv.id WHERE spv.code IN (?)', [$codes], [Connection::PARAM_STR_ARRAY]); }
  • 101.
    sylius.com ? 101 What if wechange the catalog promotion con fi guration?
  • 106.
  • 108.
    Photo by MikhailVasilyev on Unsplash 03 Takeaways
  • 109.
    sylius.com 109 Check the highestclass in your callgraph And look for bugs Remember to clear Entity Manager when batch processing Unless you are executing asynchronously messages with Symfony Messenger Load all data required to perform operation But not more Pure DQL may bring bene fi ts But is hard to do it properly for complicated objects or mix it with ORM
  • 110.
    sylius.com 110 SyliusCon This autumn -at 27th of October In a Sylius hometown: Łódź
  • 111.
    sylius.com 111 Thank you! Photo byMaksym Harbar on Unsplash