Skip to content

IBX-12204: SiteAccessService owns a scope-change stack, SiteAccessAware deprecated - #798

Draft
Steveb-p wants to merge 4 commits into
6.0from
IBX-12204-siteaccessservice-scope-stack
Draft

IBX-12204: SiteAccessService owns a scope-change stack, SiteAccessAware deprecated#798
Steveb-p wants to merge 4 commits into
6.0from
IBX-12204-siteaccessservice-scope-stack

Conversation

@Steveb-p

@Steveb-p Steveb-p commented Aug 5, 2026

Copy link
Copy Markdown
Contributor
🎫 Issue IBX-12204

Description:

A ConfigScopeListener already reacts to MVCEvents::CONFIG_SCOPE_CHANGE/CONFIG_SCOPE_RESTORE and broadcasts the new SiteAccess to every VersatileScopeInterface config resolver and every SiteAccessAware view manager/view provider — but SiteAccessService itself was never one of those broadcast targets. It kept a single value, injected once via SiteAccessAware::setSiteAccess() from request matching, that never updated when a scope change was dispatched (see IBX-8074). Every scope-change call site (ContentPreviewHelper, ConsoleCommandListener) also had to hand-build a ScopeChangeEvent and dispatch it itself, with no supported API for it.

This PR makes SiteAccessService the single owner of "what is the current SiteAccess right now":

  • Added SiteAccessService::changeSiteAccess(SiteAccess $siteAccess): SiteAccess and restoreSiteAccess(): ?SiteAccess, which wrap the existing ScopeChangeEvent dispatch under MVCEvents::CONFIG_SCOPE_CHANGE/CONFIG_SCOPE_RESTORE — no new event types, just a supported API instead of hand-dispatching.
  • SiteAccessService now maintains a real LIFO stack of SiteAccess changes instead of a single injected value:
    • It implements EventSubscriberInterface and subscribes to MVCEvents::SITEACCESS: on a MAIN_REQUEST match it resets the stack to [$siteAccess]; on a SUB_REQUEST match (fragments, ESI, content-preview's internal sub-request) it pushes onto the stack.
    • It also subscribes to KernelEvents::FINISH_REQUEST, which pops the stack — but never below 1 remaining entry. That single "floor guard" is shared with restoreSiteAccess(), so a sub-request's own push is always safely undone by its own finish, and an unbalanced restoreSiteAccess() call (or CLI's one-time changeSiteAccess() floor) can never strip the stack down to empty.
    • getCurrent() returns the top of the stack. The constructor now also takes the shared, container-wide default SiteAccess singleton (the same one SiteAccessListener mutates in place) and seeds the stack with it directly, with no event dispatch — this preserves the pre-existing guarantee that getCurrent() is never null once the container is built, which code such as ComplexConfigProcessor relies on outside of an HTTP request cycle (CLI warm-up, integration tests, etc.).
  • SiteAccessAware is marked @deprecated (no runtime deprecation notice — it's still legitimately used by consumers not migrated here) in favor of calling SiteAccessService::getCurrent() directly.
  • Migrated the straightforward "read-only" SiteAccessAware consumers to constructor-inject SiteAccessServiceInterface and call getCurrent() instead of storing a value via setSiteAccess(): ContentPreviewHelper, ConsoleCommandListener, HttpUtils, DefaultRouter, Generator (and UrlAliasGenerator), AliasGeneratorDecorator, DecoratedFragmentRenderer/InlineFragmentRenderer.
    • ConsoleCommandListener still mutates the shared SiteAccess singleton in place in addition to calling changeSiteAccess()ConfigResolver and friends still read that singleton directly for the MATCHING_TYPE_UNINITIALIZED sentinel (see IBX-12192) and are intentionally not migrated in this PR (would require a circular DI dependency back onto SiteAccessService, plus non-trivial caching/broadcast semantics). Same reasoning excludes SiteAccess\Router, View\Manager/Provider\Configured.
    • Found and fixed a latent null-dereference bug in ComplexConfigProcessor while migrating it: it read $this->siteAccessService->getCurrent()->name with no null check. Added a getCurrentSiteAccessName() guard that throws InvalidArgumentException, matching the precedent already set in SiteAccessService::getSiteAccessesRelation().
    • Small, intentional behavior change worth flagging: DecoratedFragmentRenderer/InlineFragmentRenderer previously only ever saw the original request's SiteAccess when rewriting fragment paths (they weren't part of ConfigScopeListener's scope-change broadcast). They now correctly reflect whatever SiteAccessService::getCurrent() says, including during an active content-preview scope change — this is a correctness fix, not a regression.
  • Updated DI wiring accordingly: routing.yml, services.yml, helpers.yml, image.yml, and the SecurityPass/ChainRoutingPass/FragmentPass compiler passes now pass SiteAccessServiceInterface as a constructor argument instead of the old setSiteAccess() calls; SiteAccessService itself gained an EventDispatcherInterface constructor argument and a kernel.event_subscriber tag. For HttpUtils and DefaultRouter — which extend Symfony framework base classes with their own evolving constructors — the new parameter is appended as a nullable, named-argument-bound constructor parameter ($definition->setArgument('$siteAccessService', ...)), so the existing positional arguments coming from Symfony's own service definitions are left untouched.
  • Pruned the now-stale phpstan-baseline.neon entries left behind by removing SiteAccessAware/setSiteAccess() from the migrated classes.

Verified: phpstan analyse clean, and the unit_core/bundle_core/bundle_io PHPUnit suites pass (6567/864/27 tests respectively, no failures — only pre-existing PHPUnit-deprecation warnings unrelated to this change).

For QA:

Two scenarios worth exercising manually:

  1. CLI scope change: run any console command with --siteaccess=<name> and confirm SiteAccessService::getCurrent() (and ConfigResolver) reflect that siteaccess for the whole command run.
  2. Nested scope change (content preview): open content preview for a Location under a SiteAccess different from the admin one, and confirm both the previewed page's rendering and any generated fragment/ESI URLs inside it reflect the previewed SiteAccess (this exercises the sub-request push/pop path plus the fragment-renderer behavior fix above).

New unit test coverage lives in SiteAccessServiceTest for the stack semantics specifically: nested changeSiteAccess()/restoreSiteAccess() pairs behaving as LIFO, MAIN_REQUEST resetting vs. SUB_REQUEST pushing, the finish_request floor guard never popping below 1 entry, and a full sub-request-nesting scenario.

Documentation:

SiteAccessAware is now @deprecated — worth a mention in the deprecation notes for this release, pointing integrators at SiteAccessServiceInterface::getCurrent()/changeSiteAccess()/restoreSiteAccess() instead of the old setSiteAccess() + hand-dispatched ScopeChangeEvent pattern.

…re deprecated

SiteAccessService now maintains a real LIFO stack of SiteAccess changes and
exposes changeSiteAccess()/restoreSiteAccess(), wrapping the existing
ScopeChangeEvent dispatch instead of requiring callers to hand-build it.
getCurrent() reflects the active scope correctly, including through nested
sub-requests (content preview, fragments, ESI).

SiteAccessAware is marked @deprecated; straightforward "read current
SiteAccess" consumers (ContentPreviewHelper, ConsoleCommandListener,
HttpUtils, DefaultRouter, Generator, AliasGeneratorDecorator,
DecoratedFragmentRenderer/InlineFragmentRenderer) are migrated to
SiteAccessService::getCurrent() instead.
…etter

Generator (and UrlAliasGenerator), HttpUtils, and DefaultRouter now receive
SiteAccessServiceInterface as a constructor dependency instead of through a
setSiteAccessService() call. For the two classes extending Symfony framework
base classes (HttpUtils, DefaultRouter), the new parameter is appended as a
nullable, named-argument-bound constructor parameter so the existing
positional arguments coming from Symfony's own service definitions are left
untouched.
…ctor arg

UrlAliasGenerator::__construct() now requires SiteAccessServiceInterface as
its 4th argument; the test built the generator via getMockBuilder() with
only 3 constructor args, causing an ArgumentCountError on both PHP 8.3 and
8.4 CI jobs.
Removing SiteAccessAware from SiteAccessService also dropped the old
setSiteAccess() call that seeded it with the shared, container-wide default
SiteAccess singleton at construction time. That meant getCurrent() went from
"never null once the container is built" to genuinely null until the first
PostSiteAccessMatchEvent, breaking any code that reads it outside of an HTTP
request cycle (integration tests, CLI warm-up, etc.) — surfaced by
ComplexConfigProcessor/IOConfigResolver now throwing on IO-related
integration tests.

SiteAccessService now takes that shared SiteAccess singleton as a 4th
constructor argument and seeds the stack with it directly (no event dispatch,
matching the old setSiteAccess() semantics exactly), restoring the
pre-existing guarantee.
@sonarqubecloud

sonarqubecloud Bot commented Aug 6, 2026

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant