vendor/shopware/core/Framework/Adapter/Cache/CacheInvalidationSubscriber.php line 267

  1. <?php declare(strict_types=1);
  2. namespace Shopware\Core\Framework\Adapter\Cache;
  3. use Doctrine\DBAL\ArrayParameterType;
  4. use Doctrine\DBAL\Connection;
  5. use Shopware\Core\Checkout\Cart\CachedRuleLoader;
  6. use Shopware\Core\Checkout\Customer\Aggregate\CustomerGroup\CustomerGroupDefinition;
  7. use Shopware\Core\Checkout\Payment\PaymentMethodDefinition;
  8. use Shopware\Core\Checkout\Payment\SalesChannel\CachedPaymentMethodRoute;
  9. use Shopware\Core\Checkout\Shipping\SalesChannel\CachedShippingMethodRoute;
  10. use Shopware\Core\Checkout\Shipping\ShippingMethodDefinition;
  11. use Shopware\Core\Content\Category\CategoryDefinition;
  12. use Shopware\Core\Content\Category\Event\CategoryIndexerEvent;
  13. use Shopware\Core\Content\Category\SalesChannel\CachedCategoryRoute;
  14. use Shopware\Core\Content\Category\SalesChannel\CachedNavigationRoute;
  15. use Shopware\Core\Content\Cms\CmsPageDefinition;
  16. use Shopware\Core\Content\LandingPage\Event\LandingPageIndexerEvent;
  17. use Shopware\Core\Content\LandingPage\SalesChannel\CachedLandingPageRoute;
  18. use Shopware\Core\Content\Product\Aggregate\ProductCategory\ProductCategoryDefinition;
  19. use Shopware\Core\Content\Product\Aggregate\ProductCrossSelling\ProductCrossSellingDefinition;
  20. use Shopware\Core\Content\Product\Aggregate\ProductManufacturer\ProductManufacturerDefinition;
  21. use Shopware\Core\Content\Product\Aggregate\ProductProperty\ProductPropertyDefinition;
  22. use Shopware\Core\Content\Product\Events\ProductChangedEventInterface;
  23. use Shopware\Core\Content\Product\Events\ProductIndexerEvent;
  24. use Shopware\Core\Content\Product\Events\ProductNoLongerAvailableEvent;
  25. use Shopware\Core\Content\Product\ProductDefinition;
  26. use Shopware\Core\Content\Product\SalesChannel\CrossSelling\CachedProductCrossSellingRoute;
  27. use Shopware\Core\Content\Product\SalesChannel\Detail\CachedProductDetailRoute;
  28. use Shopware\Core\Content\Product\SalesChannel\Listing\CachedProductListingRoute;
  29. use Shopware\Core\Content\Product\SalesChannel\Review\CachedProductReviewRoute;
  30. use Shopware\Core\Content\ProductStream\ProductStreamDefinition;
  31. use Shopware\Core\Content\Property\Aggregate\PropertyGroupOption\PropertyGroupOptionDefinition;
  32. use Shopware\Core\Content\Property\Aggregate\PropertyGroupOptionTranslation\PropertyGroupOptionTranslationDefinition;
  33. use Shopware\Core\Content\Property\Aggregate\PropertyGroupTranslation\PropertyGroupTranslationDefinition;
  34. use Shopware\Core\Content\Property\PropertyGroupDefinition;
  35. use Shopware\Core\Content\Rule\Event\RuleIndexerEvent;
  36. use Shopware\Core\Content\Seo\CachedSeoResolver;
  37. use Shopware\Core\Content\Seo\Event\SeoUrlUpdateEvent;
  38. use Shopware\Core\Content\Sitemap\Event\SitemapGeneratedEvent;
  39. use Shopware\Core\Content\Sitemap\SalesChannel\CachedSitemapRoute;
  40. use Shopware\Core\Defaults;
  41. use Shopware\Core\Framework\Adapter\Translation\Translator;
  42. use Shopware\Core\Framework\DataAbstractionLayer\Cache\EntityCacheKeyGenerator;
  43. use Shopware\Core\Framework\DataAbstractionLayer\Event\EntityWrittenContainerEvent;
  44. use Shopware\Core\Framework\Log\Package;
  45. use Shopware\Core\Framework\Plugin\Event\PluginPostActivateEvent;
  46. use Shopware\Core\Framework\Plugin\Event\PluginPostDeactivateEvent;
  47. use Shopware\Core\Framework\Plugin\Event\PluginPostInstallEvent;
  48. use Shopware\Core\Framework\Plugin\Event\PluginPostUninstallEvent;
  49. use Shopware\Core\Framework\Plugin\Event\PluginPostUpdateEvent;
  50. use Shopware\Core\Framework\Uuid\Uuid;
  51. use Shopware\Core\System\Country\Aggregate\CountryState\CountryStateDefinition;
  52. use Shopware\Core\System\Country\CountryDefinition;
  53. use Shopware\Core\System\Country\SalesChannel\CachedCountryRoute;
  54. use Shopware\Core\System\Country\SalesChannel\CachedCountryStateRoute;
  55. use Shopware\Core\System\Currency\CurrencyDefinition;
  56. use Shopware\Core\System\Currency\SalesChannel\CachedCurrencyRoute;
  57. use Shopware\Core\System\Language\LanguageDefinition;
  58. use Shopware\Core\System\Language\SalesChannel\CachedLanguageRoute;
  59. use Shopware\Core\System\SalesChannel\Aggregate\SalesChannelCountry\SalesChannelCountryDefinition;
  60. use Shopware\Core\System\SalesChannel\Aggregate\SalesChannelCurrency\SalesChannelCurrencyDefinition;
  61. use Shopware\Core\System\SalesChannel\Aggregate\SalesChannelLanguage\SalesChannelLanguageDefinition;
  62. use Shopware\Core\System\SalesChannel\Aggregate\SalesChannelPaymentMethod\SalesChannelPaymentMethodDefinition;
  63. use Shopware\Core\System\SalesChannel\Aggregate\SalesChannelShippingMethod\SalesChannelShippingMethodDefinition;
  64. use Shopware\Core\System\SalesChannel\Context\CachedBaseContextFactory;
  65. use Shopware\Core\System\SalesChannel\Context\CachedSalesChannelContextFactory;
  66. use Shopware\Core\System\SalesChannel\SalesChannelDefinition;
  67. use Shopware\Core\System\Salutation\SalesChannel\CachedSalutationRoute;
  68. use Shopware\Core\System\Salutation\SalutationDefinition;
  69. use Shopware\Core\System\Snippet\SnippetDefinition;
  70. use Shopware\Core\System\StateMachine\Loader\InitialStateIdLoader;
  71. use Shopware\Core\System\StateMachine\StateMachineDefinition;
  72. use Shopware\Core\System\SystemConfig\CachedSystemConfigLoader;
  73. use Shopware\Core\System\SystemConfig\Event\SystemConfigChangedEvent;
  74. use Shopware\Core\System\SystemConfig\SystemConfigService;
  75. use Shopware\Core\System\Tax\TaxDefinition;
  76. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  77. /**
  78.  * @internal - The functions inside this class are no public-api and can be changed without previous deprecation
  79.  */
  80. #[Package('core')]
  81. class CacheInvalidationSubscriber implements EventSubscriberInterface
  82. {
  83.     public function __construct(
  84.         private readonly CacheInvalidator $cacheInvalidator,
  85.         private readonly Connection $connection
  86.     ) {
  87.     }
  88.     /**
  89.      * @return array<string, string|array{0: string, 1: int}|list<array{0: string, 1?: int}>>
  90.      */
  91.     public static function getSubscribedEvents(): array
  92.     {
  93.         return [
  94.             CategoryIndexerEvent::class => [
  95.                 ['invalidateCategoryRouteByCategoryIds'2000],
  96.                 ['invalidateListingRouteByCategoryIds'2001],
  97.             ],
  98.             LandingPageIndexerEvent::class => [
  99.                 ['invalidateIndexedLandingPages'2000],
  100.             ],
  101.             ProductIndexerEvent::class => [
  102.                 ['invalidateSearch'2000],
  103.                 ['invalidateListings'2001],
  104.                 ['invalidateProductIds'2002],
  105.                 ['invalidateDetailRoute'2004],
  106.                 ['invalidateStreamsAfterIndexing'2005],
  107.                 ['invalidateReviewRoute'2006],
  108.             ],
  109.             ProductNoLongerAvailableEvent::class => [
  110.                 ['invalidateSearch'2000],
  111.                 ['invalidateListings'2001],
  112.                 ['invalidateProductIds'2002],
  113.                 ['invalidateDetailRoute'2004],
  114.                 ['invalidateStreamsAfterIndexing'2005],
  115.                 ['invalidateReviewRoute'2006],
  116.             ],
  117.             EntityWrittenContainerEvent::class => [
  118.                 ['invalidateCmsPageIds'2001],
  119.                 ['invalidateCurrencyRoute'2002],
  120.                 ['invalidateLanguageRoute'2003],
  121.                 ['invalidateNavigationRoute'2004],
  122.                 ['invalidatePaymentMethodRoute'2005],
  123.                 ['invalidateProductAssignment'2006],
  124.                 ['invalidateManufacturerFilters'2007],
  125.                 ['invalidatePropertyFilters'2008],
  126.                 ['invalidateCrossSellingRoute'2009],
  127.                 ['invalidateContext'2010],
  128.                 ['invalidateShippingMethodRoute'2011],
  129.                 ['invalidateSnippets'2012],
  130.                 ['invalidateStreamsBeforeIndexing'2013],
  131.                 ['invalidateStreamIds'2014],
  132.                 ['invalidateCountryRoute'2015],
  133.                 ['invalidateSalutationRoute'2016],
  134.                 ['invalidateInitialStateIdLoader'2017],
  135.                 ['invalidateCountryStateRoute'2018],
  136.             ],
  137.             SeoUrlUpdateEvent::class => [
  138.                 ['invalidateSeoUrls'2000],
  139.             ],
  140.             RuleIndexerEvent::class => [
  141.                 ['invalidateRules'2000],
  142.             ],
  143.             PluginPostInstallEvent::class => [
  144.                 ['invalidateRules'2000],
  145.                 ['invalidateConfig'2001],
  146.             ],
  147.             PluginPostActivateEvent::class => [
  148.                 ['invalidateRules'2000],
  149.                 ['invalidateConfig'2001],
  150.             ],
  151.             PluginPostUpdateEvent::class => [
  152.                 ['invalidateRules'2000],
  153.                 ['invalidateConfig'2001],
  154.             ],
  155.             PluginPostDeactivateEvent::class => [
  156.                 ['invalidateRules'2000],
  157.                 ['invalidateConfig'2001],
  158.             ],
  159.             PluginPostUninstallEvent::class => [
  160.                 ['invalidateRules'2000],
  161.                 ['invalidateConfig'2001],
  162.             ],
  163.             SystemConfigChangedEvent::class => [
  164.                 ['invalidateConfigKey'2000],
  165.             ],
  166.             SitemapGeneratedEvent::class => [
  167.                 ['invalidateSitemap'2000],
  168.             ],
  169.         ];
  170.     }
  171.     public function invalidateInitialStateIdLoader(EntityWrittenContainerEvent $event): void
  172.     {
  173.         if (!$event->getPrimaryKeys(StateMachineDefinition::ENTITY_NAME)) {
  174.             return;
  175.         }
  176.         $this->cacheInvalidator->invalidate([InitialStateIdLoader::CACHE_KEY]);
  177.     }
  178.     public function invalidateSitemap(SitemapGeneratedEvent $event): void
  179.     {
  180.         $this->cacheInvalidator->invalidate([
  181.             CachedSitemapRoute::buildName($event->getSalesChannelContext()->getSalesChannelId()),
  182.         ]);
  183.     }
  184.     public function invalidateConfig(): void
  185.     {
  186.         // invalidates the complete cached config
  187.         $this->cacheInvalidator->invalidate([
  188.             CachedSystemConfigLoader::CACHE_TAG,
  189.         ]);
  190.     }
  191.     public function invalidateConfigKey(SystemConfigChangedEvent $event): void
  192.     {
  193.         // invalidates the complete cached config and routes which access a specific key
  194.         $this->cacheInvalidator->invalidate([
  195.             SystemConfigService::buildName($event->getKey()),
  196.             CachedSystemConfigLoader::CACHE_TAG,
  197.         ]);
  198.     }
  199.     public function invalidateSnippets(EntityWrittenContainerEvent $event): void
  200.     {
  201.         // invalidates all http cache items where the snippets used
  202.         $snippets $event->getEventByEntityName(SnippetDefinition::ENTITY_NAME);
  203.         if (!$snippets) {
  204.             return;
  205.         }
  206.         $tags = [];
  207.         foreach ($snippets->getPayloads() as $payload) {
  208.             if (isset($payload['translationKey'])) {
  209.                 $tags[] = Translator::buildName($payload['translationKey']);
  210.             }
  211.         }
  212.         $this->cacheInvalidator->invalidate($tags);
  213.     }
  214.     public function invalidateShippingMethodRoute(EntityWrittenContainerEvent $event): void
  215.     {
  216.         // checks if a shipping method changed or the assignment between shipping method and sales channel
  217.         $logs = [...$this->getChangedShippingMethods($event), ...$this->getChangedShippingAssignments($event)];
  218.         $this->cacheInvalidator->invalidate($logs);
  219.     }
  220.     public function invalidateSeoUrls(SeoUrlUpdateEvent $event): void
  221.     {
  222.         // invalidates the cache for the seo url resolver based on the path infos which used for the new seo urls
  223.         $urls $event->getSeoUrls();
  224.         $pathInfo array_column($urls'pathInfo');
  225.         $this->cacheInvalidator->invalidate(array_map([CachedSeoResolver::class, 'buildName'], $pathInfo));
  226.     }
  227.     public function invalidateRules(): void
  228.     {
  229.         // invalidates the rule loader each time a rule changed or a plugin install state changed
  230.         $this->cacheInvalidator->invalidate([CachedRuleLoader::CACHE_KEY]);
  231.     }
  232.     public function invalidateCmsPageIds(EntityWrittenContainerEvent $event): void
  233.     {
  234.         // invalidates all routes and http cache pages where a cms page was loaded, the id is assigned as tag
  235.         $this->cacheInvalidator->invalidate(
  236.             array_map(EntityCacheKeyGenerator::buildCmsTag(...), $event->getPrimaryKeys(CmsPageDefinition::ENTITY_NAME))
  237.         );
  238.     }
  239.     public function invalidateProductIds(ProductChangedEventInterface $event): void
  240.     {
  241.         // invalidates all routes which loads products in nested unknown objects, like cms listing elements or cross selling elements
  242.         $this->cacheInvalidator->invalidate(
  243.             array_map(EntityCacheKeyGenerator::buildProductTag(...), $event->getIds())
  244.         );
  245.     }
  246.     public function invalidateStreamIds(EntityWrittenContainerEvent $event): void
  247.     {
  248.         // invalidates all routes which are loaded based on a stream (e.G. category listing and cross selling)
  249.         $this->cacheInvalidator->invalidate(
  250.             array_map(EntityCacheKeyGenerator::buildStreamTag(...), $event->getPrimaryKeys(ProductStreamDefinition::ENTITY_NAME))
  251.         );
  252.     }
  253.     public function invalidateCategoryRouteByCategoryIds(CategoryIndexerEvent $event): void
  254.     {
  255.         // invalidates the category route cache when a category changed
  256.         $this->cacheInvalidator->invalidate(
  257.             array_map([CachedCategoryRoute::class, 'buildName'], $event->getIds())
  258.         );
  259.     }
  260.     public function invalidateListingRouteByCategoryIds(CategoryIndexerEvent $event): void
  261.     {
  262.         // invalidates the product listing route each time a category changed
  263.         $this->cacheInvalidator->invalidate(
  264.             array_map([CachedProductListingRoute::class, 'buildName'], $event->getIds())
  265.         );
  266.     }
  267.     public function invalidateIndexedLandingPages(LandingPageIndexerEvent $event): void
  268.     {
  269.         // invalidates the landing page route, if the corresponding landing page changed
  270.         $this->cacheInvalidator->invalidate(
  271.             array_map([CachedLandingPageRoute::class, 'buildName'], $event->getIds())
  272.         );
  273.     }
  274.     public function invalidateCurrencyRoute(EntityWrittenContainerEvent $event): void
  275.     {
  276.         // invalidates the currency route when a currency changed or an assignment between the sales channel and currency changed
  277.         $this->cacheInvalidator->invalidate([...$this->getChangedCurrencyAssignments($event), ...$this->getChangedCurrencies($event)]);
  278.     }
  279.     public function invalidateLanguageRoute(EntityWrittenContainerEvent $event): void
  280.     {
  281.         // invalidates the language route when a language changed or an assignment between the sales channel and language changed
  282.         $this->cacheInvalidator->invalidate([...$this->getChangedLanguageAssignments($event), ...$this->getChangedLanguages($event)]);
  283.     }
  284.     public function invalidateCountryRoute(EntityWrittenContainerEvent $event): void
  285.     {
  286.         // invalidates the country route when a country changed or an assignment between the sales channel and country changed
  287.         $this->cacheInvalidator->invalidate([...$this->getChangedCountryAssignments($event), ...$this->getChangedCountries($event)]);
  288.     }
  289.     public function invalidateCountryStateRoute(EntityWrittenContainerEvent $event): void
  290.     {
  291.         $tags = [];
  292.         if (
  293.             $event->getDeletedPrimaryKeys(CountryStateDefinition::ENTITY_NAME)
  294.             || $event->getPrimaryKeysWithPropertyChange(CountryStateDefinition::ENTITY_NAME, ['countryId'])
  295.         ) {
  296.             $tags[] = CachedCountryStateRoute::ALL_TAG;
  297.         }
  298.         if (empty($tags)) {
  299.             // invalidates the country-state route when a state changed or an assignment between the state and country changed
  300.             $tags array_map(
  301.                 [CachedCountryStateRoute::class, 'buildName'],
  302.                 $event->getPrimaryKeys(CountryDefinition::ENTITY_NAME)
  303.             );
  304.         }
  305.         $this->cacheInvalidator->invalidate($tags);
  306.     }
  307.     public function invalidateSalutationRoute(EntityWrittenContainerEvent $event): void
  308.     {
  309.         // invalidates the salutation route when a salutation changed
  310.         $this->cacheInvalidator->invalidate([...$this->getChangedSalutations($event)]);
  311.     }
  312.     public function invalidateNavigationRoute(EntityWrittenContainerEvent $event): void
  313.     {
  314.         // invalidates the navigation route when a category changed or the entry point configuration of an sales channel changed
  315.         $logs = [...$this->getChangedCategories($event), ...$this->getChangedEntryPoints($event)];
  316.         $this->cacheInvalidator->invalidate($logs);
  317.     }
  318.     public function invalidatePaymentMethodRoute(EntityWrittenContainerEvent $event): void
  319.     {
  320.         // invalidates the payment method route when a payment method changed or an assignment between the sales channel and payment method changed
  321.         $logs = [...$this->getChangedPaymentMethods($event), ...$this->getChangedPaymentAssignments($event)];
  322.         $this->cacheInvalidator->invalidate($logs);
  323.     }
  324.     public function invalidateSearch(): void
  325.     {
  326.         // invalidates the search and suggest route each time a product changed
  327.         $this->cacheInvalidator->invalidate([
  328.             'product-suggest-route',
  329.             'product-search-route',
  330.         ]);
  331.     }
  332.     public function invalidateDetailRoute(ProductChangedEventInterface $event): void
  333.     {
  334.         //invalidates the product detail route each time a product changed or if the product is no longer available (because out of stock)
  335.         $this->cacheInvalidator->invalidate(
  336.             array_map([CachedProductDetailRoute::class, 'buildName'], $event->getIds())
  337.         );
  338.     }
  339.     public function invalidateProductAssignment(EntityWrittenContainerEvent $event): void
  340.     {
  341.         //invalidates the product listing route, each time a product - category assignment changed
  342.         $ids $event->getPrimaryKeys(ProductCategoryDefinition::ENTITY_NAME);
  343.         $ids array_column($ids'categoryId');
  344.         $this->cacheInvalidator->invalidate(
  345.             array_map([CachedProductListingRoute::class, 'buildName'], $ids)
  346.         );
  347.     }
  348.     public function invalidateContext(EntityWrittenContainerEvent $event): void
  349.     {
  350.         //invalidates the context cache - each time one of the entities which are considered inside the context factory changed
  351.         $ids $event->getPrimaryKeys(SalesChannelDefinition::ENTITY_NAME);
  352.         $keys array_map([CachedSalesChannelContextFactory::class, 'buildName'], $ids);
  353.         $keys array_merge($keysarray_map([CachedBaseContextFactory::class, 'buildName'], $ids));
  354.         if ($event->getEventByEntityName(CurrencyDefinition::ENTITY_NAME)) {
  355.             $keys[] = CachedSalesChannelContextFactory::ALL_TAG;
  356.         }
  357.         if ($event->getEventByEntityName(PaymentMethodDefinition::ENTITY_NAME)) {
  358.             $keys[] = CachedSalesChannelContextFactory::ALL_TAG;
  359.         }
  360.         if ($event->getEventByEntityName(ShippingMethodDefinition::ENTITY_NAME)) {
  361.             $keys[] = CachedSalesChannelContextFactory::ALL_TAG;
  362.         }
  363.         if ($event->getEventByEntityName(TaxDefinition::ENTITY_NAME)) {
  364.             $keys[] = CachedSalesChannelContextFactory::ALL_TAG;
  365.         }
  366.         if ($event->getEventByEntityName(CountryDefinition::ENTITY_NAME)) {
  367.             $keys[] = CachedSalesChannelContextFactory::ALL_TAG;
  368.         }
  369.         if ($event->getEventByEntityName(CustomerGroupDefinition::ENTITY_NAME)) {
  370.             $keys[] = CachedSalesChannelContextFactory::ALL_TAG;
  371.         }
  372.         if ($event->getEventByEntityName(LanguageDefinition::ENTITY_NAME)) {
  373.             $keys[] = CachedSalesChannelContextFactory::ALL_TAG;
  374.         }
  375.         $keys array_filter(array_unique($keys));
  376.         if (empty($keys)) {
  377.             return;
  378.         }
  379.         $this->cacheInvalidator->invalidate($keys);
  380.     }
  381.     public function invalidateManufacturerFilters(EntityWrittenContainerEvent $event): void
  382.     {
  383.         // invalidates the product listing route, each time a manufacturer changed
  384.         $ids $event->getPrimaryKeys(ProductManufacturerDefinition::ENTITY_NAME);
  385.         if (empty($ids)) {
  386.             return;
  387.         }
  388.         $ids $this->connection->fetchFirstColumn(
  389.             'SELECT DISTINCT LOWER(HEX(category_id)) as category_id
  390.              FROM product_category_tree
  391.                 INNER JOIN product ON product.id = product_category_tree.product_id AND product_category_tree.product_version_id = product.version_id
  392.              WHERE product.product_manufacturer_id IN (:ids)
  393.              AND product.version_id = :version',
  394.             ['ids' => Uuid::fromHexToBytesList($ids), 'version' => Uuid::fromHexToBytes(Defaults::LIVE_VERSION)],
  395.             ['ids' => ArrayParameterType::STRING]
  396.         );
  397.         $this->cacheInvalidator->invalidate(
  398.             array_map([CachedProductListingRoute::class, 'buildName'], $ids)
  399.         );
  400.     }
  401.     public function invalidatePropertyFilters(EntityWrittenContainerEvent $event): void
  402.     {
  403.         $this->cacheInvalidator->invalidate([...$this->getChangedPropertyFilterTags($event), ...$this->getDeletedPropertyFilterTags($event)]);
  404.     }
  405.     public function invalidateReviewRoute(ProductChangedEventInterface $event): void
  406.     {
  407.         $this->cacheInvalidator->invalidate(
  408.             array_map([CachedProductReviewRoute::class, 'buildName'], $event->getIds())
  409.         );
  410.     }
  411.     public function invalidateListings(ProductChangedEventInterface $event): void
  412.     {
  413.         // invalidates product listings which are based on the product category assignment
  414.         $this->cacheInvalidator->invalidate(
  415.             array_map([CachedProductListingRoute::class, 'buildName'], $this->getProductCategoryIds($event->getIds()))
  416.         );
  417.     }
  418.     public function invalidateStreamsBeforeIndexing(EntityWrittenContainerEvent $event): void
  419.     {
  420.         // invalidates all stream based pages and routes before the product indexer changes product_stream_mapping
  421.         $ids $event->getPrimaryKeys(ProductDefinition::ENTITY_NAME);
  422.         if (empty($ids)) {
  423.             return;
  424.         }
  425.         // invalidates product listings which are based on a product stream
  426.         $ids $this->connection->fetchFirstColumn(
  427.             'SELECT DISTINCT LOWER(HEX(product_stream_id))
  428.              FROM product_stream_mapping
  429.              WHERE product_stream_mapping.product_id IN (:ids)
  430.              AND product_stream_mapping.product_version_id = :version',
  431.             ['ids' => Uuid::fromHexToBytesList($ids), 'version' => Uuid::fromHexToBytes(Defaults::LIVE_VERSION)],
  432.             ['ids' => ArrayParameterType::STRING]
  433.         );
  434.         $this->cacheInvalidator->invalidate(
  435.             array_map(EntityCacheKeyGenerator::buildStreamTag(...), $ids)
  436.         );
  437.     }
  438.     public function invalidateStreamsAfterIndexing(ProductChangedEventInterface $event): void
  439.     {
  440.         // invalidates all stream based pages and routes after the product indexer changes product_stream_mapping
  441.         $ids $this->connection->fetchFirstColumn(
  442.             'SELECT DISTINCT LOWER(HEX(product_stream_id))
  443.              FROM product_stream_mapping
  444.              WHERE product_stream_mapping.product_id IN (:ids)
  445.              AND product_stream_mapping.product_version_id = :version',
  446.             ['ids' => Uuid::fromHexToBytesList($event->getIds()), 'version' => Uuid::fromHexToBytes(Defaults::LIVE_VERSION)],
  447.             ['ids' => ArrayParameterType::STRING]
  448.         );
  449.         $this->cacheInvalidator->invalidate(
  450.             array_map(EntityCacheKeyGenerator::buildStreamTag(...), $ids)
  451.         );
  452.     }
  453.     public function invalidateCrossSellingRoute(EntityWrittenContainerEvent $event): void
  454.     {
  455.         // invalidates the product detail route for the changed cross selling definitions
  456.         $ids $event->getPrimaryKeys(ProductCrossSellingDefinition::ENTITY_NAME);
  457.         if (empty($ids)) {
  458.             return;
  459.         }
  460.         $ids $this->connection->fetchFirstColumn(
  461.             'SELECT DISTINCT LOWER(HEX(product_id)) FROM product_cross_selling WHERE id IN (:ids)',
  462.             ['ids' => Uuid::fromHexToBytesList($ids)],
  463.             ['ids' => ArrayParameterType::STRING]
  464.         );
  465.         $this->cacheInvalidator->invalidate(
  466.             array_map([CachedProductCrossSellingRoute::class, 'buildName'], $ids)
  467.         );
  468.     }
  469.     /**
  470.      * @return list<string>
  471.      */
  472.     private function getDeletedPropertyFilterTags(EntityWrittenContainerEvent $event): array
  473.     {
  474.         // invalidates the product listing route, each time a property changed
  475.         $ids $event->getDeletedPrimaryKeys(ProductPropertyDefinition::ENTITY_NAME);
  476.         if (empty($ids)) {
  477.             return [];
  478.         }
  479.         $productIds array_column($ids'productId');
  480.         return array_merge(
  481.             array_map([CachedProductDetailRoute::class, 'buildName'], array_unique($productIds)),
  482.             array_map([CachedProductListingRoute::class, 'buildName'], $this->getProductCategoryIds($productIds))
  483.         );
  484.     }
  485.     /**
  486.      * @return list<string>
  487.      */
  488.     private function getChangedPropertyFilterTags(EntityWrittenContainerEvent $event): array
  489.     {
  490.         // invalidates the product listing route and detail rule, each time a property group changed
  491.         $propertyGroupIds array_unique(array_merge(
  492.             $event->getPrimaryKeysWithPayloadIgnoringFields(PropertyGroupDefinition::ENTITY_NAME, ['id''updatedAt']),
  493.             array_column($event->getPrimaryKeysWithPayloadIgnoringFields(PropertyGroupTranslationDefinition::ENTITY_NAME, ['propertyGroupId''languageId''updatedAt']), 'propertyGroupId')
  494.         ));
  495.         // invalidates the product listing route and detail rule, each time a property option changed
  496.         $propertyOptionIds array_unique(array_merge(
  497.             $event->getPrimaryKeysWithPayloadIgnoringFields(PropertyGroupOptionDefinition::ENTITY_NAME, ['id''updatedAt']),
  498.             array_column($event->getPrimaryKeysWithPayloadIgnoringFields(PropertyGroupOptionTranslationDefinition::ENTITY_NAME, ['propertyGroupOptionId''languageId''updatedAt']), 'propertyGroupOptionId')
  499.         ));
  500.         if (empty($propertyGroupIds) && empty($propertyOptionIds)) {
  501.             return [];
  502.         }
  503.         $productIds $this->connection->fetchFirstColumn(
  504.             'SELECT product_property.product_id
  505.              FROM product_property
  506.                 LEFT JOIN property_group_option productProperties ON productProperties.id = product_property.property_group_option_id
  507.              WHERE productProperties.property_group_id IN (:ids) OR productProperties.id IN (:optionIds)
  508.              AND product_property.product_version_id = :version',
  509.             ['ids' => Uuid::fromHexToBytesList($propertyGroupIds), 'optionIds' => Uuid::fromHexToBytesList($propertyOptionIds), 'version' => Uuid::fromHexToBytes(Defaults::LIVE_VERSION)],
  510.             ['ids' => ArrayParameterType::STRING'optionIds' => ArrayParameterType::STRING]
  511.         );
  512.         $productIds array_unique([...$productIds, ...$this->connection->fetchFirstColumn(
  513.             'SELECT product_option.product_id
  514.                  FROM product_option
  515.                     LEFT JOIN property_group_option productOptions ON productOptions.id = product_option.property_group_option_id
  516.                  WHERE productOptions.property_group_id IN (:ids) OR productOptions.id IN (:optionIds)
  517.                  AND product_option.product_version_id = :version',
  518.             ['ids' => Uuid::fromHexToBytesList($propertyGroupIds), 'optionIds' => Uuid::fromHexToBytesList($propertyOptionIds), 'version' => Uuid::fromHexToBytes(Defaults::LIVE_VERSION)],
  519.             ['ids' => ArrayParameterType::STRING'optionIds' => ArrayParameterType::STRING]
  520.         )]);
  521.         if (empty($productIds)) {
  522.             return [];
  523.         }
  524.         $parentIds $this->connection->fetchFirstColumn(
  525.             'SELECT DISTINCT LOWER(HEX(COALESCE(parent_id, id)))
  526.             FROM product
  527.             WHERE id in (:productIds) AND version_id = :version',
  528.             ['productIds' => $productIds'version' => Uuid::fromHexToBytes(Defaults::LIVE_VERSION)],
  529.             ['productIds' => ArrayParameterType::STRING]
  530.         );
  531.         $categoryIds $this->connection->fetchFirstColumn(
  532.             'SELECT DISTINCT LOWER(HEX(category_id))
  533.             FROM product_category_tree
  534.             WHERE product_id in (:productIds) AND product_version_id = :version',
  535.             ['productIds' => $productIds'version' => Uuid::fromHexToBytes(Defaults::LIVE_VERSION)],
  536.             ['productIds' => ArrayParameterType::STRING]
  537.         );
  538.         return [...array_map([CachedProductDetailRoute::class, 'buildName'], array_filter($parentIds)), ...array_map([CachedProductListingRoute::class, 'buildName'], array_filter($categoryIds))];
  539.     }
  540.     /**
  541.      * @param list<string> $ids
  542.      *
  543.      * @return list<string>
  544.      */
  545.     private function getProductCategoryIds(array $ids): array
  546.     {
  547.         return $this->connection->fetchFirstColumn(
  548.             'SELECT DISTINCT LOWER(HEX(category_id)) as category_id
  549.              FROM product_category_tree
  550.              WHERE product_id IN (:ids)
  551.              AND product_version_id = :version
  552.              AND category_version_id = :version',
  553.             ['ids' => Uuid::fromHexToBytesList($ids), 'version' => Uuid::fromHexToBytes(Defaults::LIVE_VERSION)],
  554.             ['ids' => ArrayParameterType::STRING]
  555.         );
  556.     }
  557.     /**
  558.      * @return list<string>
  559.      */
  560.     private function getChangedShippingMethods(EntityWrittenContainerEvent $event): array
  561.     {
  562.         $ids $event->getPrimaryKeys(ShippingMethodDefinition::ENTITY_NAME);
  563.         if (empty($ids)) {
  564.             return [];
  565.         }
  566.         $ids $this->connection->fetchFirstColumn(
  567.             'SELECT DISTINCT LOWER(HEX(sales_channel_id)) as id FROM sales_channel_shipping_method WHERE shipping_method_id IN (:ids)',
  568.             ['ids' => Uuid::fromHexToBytesList($ids)],
  569.             ['ids' => ArrayParameterType::STRING]
  570.         );
  571.         $tags = [];
  572.         if ($event->getDeletedPrimaryKeys(ShippingMethodDefinition::ENTITY_NAME)) {
  573.             $tags[] = CachedShippingMethodRoute::ALL_TAG;
  574.         }
  575.         return array_merge($tagsarray_map([CachedShippingMethodRoute::class, 'buildName'], $ids));
  576.     }
  577.     /**
  578.      * @return list<string>
  579.      */
  580.     private function getChangedShippingAssignments(EntityWrittenContainerEvent $event): array
  581.     {
  582.         //Used to detect changes to the shipping assignment of a sales channel
  583.         $ids $event->getPrimaryKeys(SalesChannelShippingMethodDefinition::ENTITY_NAME);
  584.         $ids array_column($ids'salesChannelId');
  585.         return array_map([CachedShippingMethodRoute::class, 'buildName'], $ids);
  586.     }
  587.     /**
  588.      * @return list<string>
  589.      */
  590.     private function getChangedPaymentMethods(EntityWrittenContainerEvent $event): array
  591.     {
  592.         $ids $event->getPrimaryKeys(PaymentMethodDefinition::ENTITY_NAME);
  593.         if (empty($ids)) {
  594.             return [];
  595.         }
  596.         $ids $this->connection->fetchFirstColumn(
  597.             'SELECT DISTINCT LOWER(HEX(sales_channel_id)) as id FROM sales_channel_payment_method WHERE payment_method_id IN (:ids)',
  598.             ['ids' => Uuid::fromHexToBytesList($ids)],
  599.             ['ids' => ArrayParameterType::STRING]
  600.         );
  601.         $tags = [];
  602.         if ($event->getDeletedPrimaryKeys(PaymentMethodDefinition::ENTITY_NAME)) {
  603.             $tags[] = CachedPaymentMethodRoute::ALL_TAG;
  604.         }
  605.         return array_merge($tagsarray_map([CachedPaymentMethodRoute::class, 'buildName'], $ids));
  606.     }
  607.     /**
  608.      * @return list<string>
  609.      */
  610.     private function getChangedPaymentAssignments(EntityWrittenContainerEvent $event): array
  611.     {
  612.         //Used to detect changes to the language assignment of a sales channel
  613.         $ids $event->getPrimaryKeys(SalesChannelPaymentMethodDefinition::ENTITY_NAME);
  614.         $ids array_column($ids'salesChannelId');
  615.         return array_map([CachedPaymentMethodRoute::class, 'buildName'], $ids);
  616.     }
  617.     /**
  618.      * @return list<string>
  619.      */
  620.     private function getChangedCategories(EntityWrittenContainerEvent $event): array
  621.     {
  622.         $ids $event->getPrimaryKeysWithPayload(CategoryDefinition::ENTITY_NAME);
  623.         if (empty($ids)) {
  624.             return [];
  625.         }
  626.         $ids array_map([CachedNavigationRoute::class, 'buildName'], $ids);
  627.         $ids[] = CachedNavigationRoute::BASE_NAVIGATION_TAG;
  628.         return $ids;
  629.     }
  630.     /**
  631.      * @return list<string>
  632.      */
  633.     private function getChangedEntryPoints(EntityWrittenContainerEvent $event): array
  634.     {
  635.         $ids $event->getPrimaryKeysWithPropertyChange(
  636.             SalesChannelDefinition::ENTITY_NAME,
  637.             ['navigationCategoryId''navigationCategoryDepth''serviceCategoryId''footerCategoryId']
  638.         );
  639.         if (empty($ids)) {
  640.             return [];
  641.         }
  642.         return [CachedNavigationRoute::ALL_TAG];
  643.     }
  644.     /**
  645.      * @return list<string>
  646.      */
  647.     private function getChangedCountries(EntityWrittenContainerEvent $event): array
  648.     {
  649.         $ids $event->getPrimaryKeys(CountryDefinition::ENTITY_NAME);
  650.         if (empty($ids)) {
  651.             return [];
  652.         }
  653.         //Used to detect changes to the country itself and invalidate the route for all sales channels in which the country is assigned.
  654.         $ids $this->connection->fetchFirstColumn(
  655.             'SELECT DISTINCT LOWER(HEX(sales_channel_id)) as id FROM sales_channel_country WHERE country_id IN (:ids)',
  656.             ['ids' => Uuid::fromHexToBytesList($ids)],
  657.             ['ids' => ArrayParameterType::STRING]
  658.         );
  659.         $tags = [];
  660.         if ($event->getDeletedPrimaryKeys(CountryDefinition::ENTITY_NAME)) {
  661.             $tags[] = CachedCountryRoute::ALL_TAG;
  662.         }
  663.         return array_merge($tagsarray_map([CachedCountryRoute::class, 'buildName'], $ids));
  664.     }
  665.     /**
  666.      * @return list<string>
  667.      */
  668.     private function getChangedCountryAssignments(EntityWrittenContainerEvent $event): array
  669.     {
  670.         //Used to detect changes to the country assignment of a sales channel
  671.         $ids $event->getPrimaryKeys(SalesChannelCountryDefinition::ENTITY_NAME);
  672.         $ids array_column($ids'salesChannelId');
  673.         return array_map([CachedCountryRoute::class, 'buildName'], $ids);
  674.     }
  675.     /**
  676.      * @return list<string>
  677.      */
  678.     private function getChangedSalutations(EntityWrittenContainerEvent $event): array
  679.     {
  680.         $ids $event->getPrimaryKeys(SalutationDefinition::ENTITY_NAME);
  681.         if (empty($ids)) {
  682.             return [];
  683.         }
  684.         return [CachedSalutationRoute::ALL_TAG];
  685.     }
  686.     /**
  687.      * @return list<string>
  688.      */
  689.     private function getChangedLanguages(EntityWrittenContainerEvent $event): array
  690.     {
  691.         $ids $event->getPrimaryKeys(LanguageDefinition::ENTITY_NAME);
  692.         if (empty($ids)) {
  693.             return [];
  694.         }
  695.         //Used to detect changes to the language itself and invalidate the route for all sales channels in which the language is assigned.
  696.         $ids $this->connection->fetchFirstColumn(
  697.             'SELECT DISTINCT LOWER(HEX(sales_channel_id)) as id FROM sales_channel_language WHERE language_id IN (:ids)',
  698.             ['ids' => Uuid::fromHexToBytesList($ids)],
  699.             ['ids' => ArrayParameterType::STRING]
  700.         );
  701.         $tags = [];
  702.         if ($event->getDeletedPrimaryKeys(LanguageDefinition::ENTITY_NAME)) {
  703.             $tags[] = CachedLanguageRoute::ALL_TAG;
  704.         }
  705.         return array_merge($tagsarray_map([CachedLanguageRoute::class, 'buildName'], $ids));
  706.     }
  707.     /**
  708.      * @return list<string>
  709.      */
  710.     private function getChangedLanguageAssignments(EntityWrittenContainerEvent $event): array
  711.     {
  712.         //Used to detect changes to the language assignment of a sales channel
  713.         $ids $event->getPrimaryKeys(SalesChannelLanguageDefinition::ENTITY_NAME);
  714.         $ids array_column($ids'salesChannelId');
  715.         return array_map([CachedLanguageRoute::class, 'buildName'], $ids);
  716.     }
  717.     /**
  718.      * @return list<string>
  719.      */
  720.     private function getChangedCurrencies(EntityWrittenContainerEvent $event): array
  721.     {
  722.         $ids $event->getPrimaryKeys(CurrencyDefinition::ENTITY_NAME);
  723.         if (empty($ids)) {
  724.             return [];
  725.         }
  726.         //Used to detect changes to the currency itself and invalidate the route for all sales channels in which the currency is assigned.
  727.         $ids $this->connection->fetchFirstColumn(
  728.             'SELECT DISTINCT LOWER(HEX(sales_channel_id)) as id FROM sales_channel_currency WHERE currency_id IN (:ids)',
  729.             ['ids' => Uuid::fromHexToBytesList($ids)],
  730.             ['ids' => ArrayParameterType::STRING]
  731.         );
  732.         $tags = [];
  733.         if ($event->getDeletedPrimaryKeys(CurrencyDefinition::ENTITY_NAME)) {
  734.             $tags[] = CachedCurrencyRoute::ALL_TAG;
  735.         }
  736.         return array_merge($tagsarray_map([CachedCurrencyRoute::class, 'buildName'], $ids));
  737.     }
  738.     /**
  739.      * @return list<string>
  740.      */
  741.     private function getChangedCurrencyAssignments(EntityWrittenContainerEvent $event): array
  742.     {
  743.         //Used to detect changes to the currency assignment of a sales channel
  744.         $ids $event->getPrimaryKeys(SalesChannelCurrencyDefinition::ENTITY_NAME);
  745.         $ids array_column($ids'salesChannelId');
  746.         return array_map([CachedCurrencyRoute::class, 'buildName'], $ids);
  747.     }
  748. }