Replacement flow internals
This page documents the runtime behaviour for readers who want to understand or extend the plugin. None of this is needed to configure it.
Event subscribers
Section titled “Event subscribers”The plugin listens to three storefront page-loaded events and two entity-loaded events:
| Event | Handler | Page kind |
|---|---|---|
ProductPageLoadedEvent | onProductPageLoaded | Product detail |
NavigationPageLoadedEvent | onNavigationPageLoaded | Category / listing |
LandingPageLoadedEvent | onLandingPageLoaded | Landing page |
CategoryEvents::CATEGORY_LOADED_EVENT | onCategoryEntityLoaded | CMS navigation widget (XHR) |
LandingPageEvents::LANDING_PAGE_LOADED_EVENT | onLandingPageEntityLoaded | Landing page entity load |
All five live in Subscriber\PageLoadedSubscriber.
The page-loaded handlers cover normal storefront navigation. The two entity-loaded handlers cover the CMS navigation widget XHR (the call your storefront makes to lazy-load category content) and direct landing-page entity loads, so the replacement behaves consistently whether the layout is rendered as part of the main page or as part of a widget XHR.
The matching loop
Section titled “The matching loop”For every relevant request, the handler does roughly:
salesChannelId = resolve from request1. Has the base layout passed the replacement-mode gate? - Product page → ProductPageReplaceModeEnum (4 values) - Category page → NavigationPageReplaceModeEnum (3 values) - Landing page → LandingPageReplaceModeEnum (3 values) If no → return.
2. alternativeTypes = SettingsService::getAlternativeReplaceTypes(baseType, salesChannelId) If empty → return.
3. Walk every p2lab_cms_assigner_cms_page_to_rule whose cmsPage.type IN alternativeTypes: - Ordered by rule.priority ASC (Shopware's stock ordering). - In pages of 100 (LIMIT_PER_REQUEST), up to 500 total (LIMIT_TOTAL). - Group rows by cmsPageId — a single layout may have multiple rules attached.
4. For each cmsPageId, call RuleConditionService::match(rules, page, salesChannelContext): - All attached rules must match (AND semantics). - A rule is built from its conditions, filtered to ConditionNameEnum::CONDITIONS and CONTAINERS. - The matcher dispatches on page type → ProductRuleScope / CategoryRuleScope / LandingRuleScope.
5. First match wins. The plugin loads the CmsPage via SalesChannelCmsPageLoaderInterface with an EntityResolverContext built around the product / category / landingPage, then assigns it back to the page object.The p2lab_cms_assigner_cms_page_to_rule table
Section titled “The p2lab_cms_assigner_cms_page_to_rule table”The plugin stores rule ↔ layout assignments in a single junction table. Each row holds:
id— primary key.cms_page_id+cms_page_version_id— the alternative layout.rule_id— the rule it depends on.
A layout with N attached rules has N rows; a rule attached to M layouts has M rows. Read in either direction.
Rule scopes
Section titled “Rule scopes”The plugin defines three custom rule scopes (under Rule\Scope\) so its custom conditions (and Shopware’s stock ones) can introspect what’s being viewed:
ProductRuleScope— holds theSalesChannelProductEntityplus the sales-channel context. Used by product page handlers and shared byInCategoryRule/InCategoryWithChildrenRulewhen they evaluate against a product’scategoryIds.CategoryRuleScope— holds thecategoryIdplus the sales-channel context. ExposesisHomepage()(used byIsHomepageRule).LandingRuleScope— holds theLandingPagepage plus the sales-channel context.
All three inherit from AbstractRuleScope, which holds the SalesChannelContext and the category repository (so rules that traverse the tree can search).
Custom rule classes
Section titled “Custom rule classes”The plugin ships three custom rule classes (under Rule\):
IsHomepageRule— boolean check againstCategoryRuleScope::isHomepage(). Returnsfalseon non-category scopes.InCategoryRule— UUID comparison against the product’scategoryIdsor the category’s ownid.InCategoryWithChildrenRuleextendsInCategoryRule— additionally walks the categorypath(Shopware stores the ancestor chain as|id1|id2|id3|) to match descendants.
There is also Rule\Container\MatchAllRule — a stand-in for Shopware’s “All line items container”, whose product-only semantics make no sense in a CMS layout selection context. The plugin substitutes its own implementation that returns true only when every child rule matches against the current scope.
Conditions explicitly excluded
Section titled “Conditions explicitly excluded”ConditionNameEnum defines two arrays: CONTAINERS and CONDITIONS. Anything not in those arrays is silently ignored when a rule is built. The exclusion list is wide on purpose — many of Shopware’s stock conditions (cart totals, line item attributes, shipping method) target the checkout flow and have no meaning when picking a layout for a page that hasn’t been built yet.
To find out exactly what is supported, see Custom rule conditions.
When the entity-loaded handlers kick in
Section titled “When the entity-loaded handlers kick in”onCategoryEntityLoaded is the most subtle. It runs on category.loaded events and only acts when the request looks like a CMS navigation widget XHR — the path matches ^/widgets/cms/navigation/<32-hex>$ and the query string carries navigationId + cms-page-id. This is how the plugin keeps replacement consistent when the storefront fetches a category’s content via the widget XHR (in addition to the initial page render).
It does the same mode check as the main onNavigationPageLoaded handler before overriding the entity’s cmsPageId.
onLandingPageEntityLoaded runs on landing_page.loaded events and applies the default landing page layout if the landing page has no layout of its own. This is the entry point for the Landing page card’s default-layout setting.
What the plugin does NOT do
Section titled “What the plugin does NOT do”- It does not modify the CMS page itself — just which
CmsPageEntityis rendered. - It does not touch the category / product / landing-page entity in the database — the override happens in memory, per request.
- It does not cache its decisions; every request re-evaluates the rules. The repository search hits Shopware’s standard caches, but the rule evaluation is per-request.
Related
Section titled “Related”- Performance — limits and what to watch.
- Custom rule conditions — the supported list with semantics.