src/Core/Framework/Adapter/Cache/CacheInvalidationSubscriber.php line 454

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