Skip to content

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

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

Steveb-p wants to merge 2 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

Warning

This branch includes a temporary commit (dependencies.json) that points the Behat CI job at
ibexa/behat#191 so the "Commands use the default
siteaccess if not specified" scenario can run (and pass) before that companion PR merges.
Must be removed before merging this PR.

🎫 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, or null if it's empty — it does not fall back to the shared, container-wide SiteAccess singleton. SiteAccessService (and ConsoleCommandListener, see below) intentionally stop relying on that singleton altogether; a SiteAccess with an "uninitialized" matcher (SiteAccess::MATCHING_TYPE_UNINITIALIZED) is treated as null defensively even if one somehow ends up on the stack.
  • 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 no longer mutates the shared SiteAccess singleton at all — it constructs a fresh SiteAccess for the CLI invocation and hands it to 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.
    • IO/repository integration tests never dispatch a request or console command, so IbexaTestKernel and LegacyTestContainerBuilder's test settings now explicitly seed SiteAccessService via changeSiteAccess() with a freshly-constructed default SiteAccess, the same mechanism CLI uses.
    • 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.

Known BC break (intentional): any custom Symfony console command that type-hints the shared Ibexa\Core\MVC\Symfony\SiteAccess service directly to read the CLI-resolved siteaccess will no longer see it updated — ConsoleCommandListener used to mutate that shared singleton in place specifically for such consumers, but no longer does (see above). Commands relying on this must switch to injecting SiteAccessServiceInterface and calling getCurrent() instead. Audited the rest of the public ibexa/* org for the same pattern and found exactly two affected spots, both now fixed:

  • Ibexa\Bundle\Core\Command\DebugConfigResolverCommand (ibexa:debug:config-resolver) in this repo — fixed in this PR.
  • ibexa/behat's TestSiteaccessCommand fixture command (used by src/bundle/Core/Features/Console/console.feature's "Commands use the default siteaccess if not specified" scenario) — fixed in companion PR ibexa/behat#191, cross-tested here via the temporary dependencies.json (see warning at the top).

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.

The BC break above should also be called out explicitly in the release/upgrade notes: custom console commands reading the shared SiteAccess service to determine the CLI-resolved siteaccess must switch to SiteAccessServiceInterface::getCurrent().

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Centralizes current SiteAccess and scope-change management in SiteAccessService, migrating consumers away from SiteAccessAware.

Changes:

  • Adds request-aware SiteAccess stack operations and scope events.
  • Migrates routing, preview, console, fragment, security, and image services.
  • Updates dependency injection and tests.

Reviewed changes

Copilot reviewed 43 out of 43 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/lib/MVC/Symfony/SiteAccess/SiteAccessServiceTest.php Tests stack and request semantics.
tests/lib/MVC/Symfony/Security/HttpUtilsTest.php Tests service-based SiteAccess lookup.
tests/lib/MVC/Symfony/Routing/UrlAliasRouterTest.php Supplies the new service dependency.
tests/lib/MVC/Symfony/Routing/UrlAliasGeneratorTest.php Updates generator setup and mocks.
tests/lib/MVC/Symfony/Routing/GeneratorTest.php Tests dynamic current SiteAccess lookup.
tests/lib/Helper/ContentPreviewHelperTest.php Tests delegated scope changes.
tests/integration/Core/Resources/settings/common.yml Seeds integration-test SiteAccess state.
tests/integration/Core/LegacyTestContainerBuilder.php Uses the service interface.
tests/bundle/IO/DependencyInjection/IbexaIOExtensionTest.php Initializes SiteAccess for IO tests.
tests/bundle/Core/SiteAccess/Config/IOConfigResolverTest.php Mocks the service interface.
tests/bundle/Core/Routing/DefaultRouterTest.php Tests constructor-injected service usage.
tests/bundle/Core/Fragment/DecoratedFragmentRendererTest.php Tests current-scope fragment rewriting.
tests/bundle/Core/EventListener/ConsoleCommandListenerTest.php Tests CLI scope changes.
tests/bundle/Core/DependencyInjection/Compiler/SecurityPassTest.php Verifies HttpUtils injection.
tests/bundle/Core/DependencyInjection/Compiler/FragmentPassTest.php Verifies fragment dependencies.
tests/bundle/Core/DependencyInjection/Compiler/ChainRoutingPassTest.php Verifies router injection.
src/lib/Resources/settings/roles.yml Injects the service interface.
src/lib/MVC/Symfony/SiteAccess/SiteAccessServiceInterface.php Exposes change and restore APIs.
src/lib/MVC/Symfony/SiteAccess/SiteAccessService.php Implements stack ownership and events.
src/lib/MVC/Symfony/SiteAccess/SiteAccessAware.php Deprecates the legacy interface.
src/lib/MVC/Symfony/Security/HttpUtils.php Reads SiteAccess dynamically.
src/lib/MVC/Symfony/Routing/Generator/UrlAliasGenerator.php Injects SiteAccessService.
src/lib/MVC/Symfony/Routing/Generator.php Uses the current SiteAccess for URLs.
src/lib/MVC/Symfony/Controller/Content/PreviewController.php Guards missing SiteAccess state.
src/lib/Helper/ContentPreviewHelper.php Delegates scope stack operations.
src/contracts/Test/IbexaTestKernel.php Seeds test-kernel SiteAccess state.
src/bundle/Core/SiteAccess/Config/ComplexConfigProcessor.php Handles absent current SiteAccess.
src/bundle/Core/Routing/DefaultRouter.php Uses dynamic SiteAccess lookup.
src/bundle/Core/Resources/config/services.yml Rewires console and fragment services.
src/bundle/Core/Resources/config/routing.yml Configures generator and service dependencies.
src/bundle/Core/Resources/config/image.yml Rewires image alias generation.
src/bundle/Core/Resources/config/helpers.yml Rewires preview and config helpers.
src/bundle/Core/Resources/config/commands.yml Updates debug-command dependency.
src/bundle/Core/Imagine/Cache/AliasGeneratorDecorator.php Uses current SiteAccess in cache metadata.
src/bundle/Core/Fragment/InlineFragmentRenderer.php Removes obsolete SiteAccess storage.
src/bundle/Core/Fragment/DecoratedFragmentRenderer.php Rewrites paths using current SiteAccess.
src/bundle/Core/EventListener/ConsoleCommandListener.php Establishes fresh CLI SiteAccess state.
src/bundle/Core/DependencyInjection/Compiler/SecurityPass.php Constructor-injects HttpUtils dependency.
src/bundle/Core/DependencyInjection/Compiler/FragmentPass.php Injects fragment renderer dependency.
src/bundle/Core/DependencyInjection/Compiler/ChainRoutingPass.php Injects default-router dependency.
src/bundle/Core/Command/DebugConfigResolverCommand.php Reads CLI SiteAccess from the service.
phpstan-baseline.neon Removes obsolete suppressions.
dependencies.json Temporarily pins the companion Behat branch.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +61 to +63
public function onKernelFinishRequest(FinishRequestEvent $event): void
{
$this->popSiteAccessStack();
Comment thread dependencies.json
Comment on lines +1 to +11
{
"recipesEndpoint": "",
"packages": [
{
"requirement": "dev-IBX-12204-siteaccessservice-scope-stack as 6.0.x-dev",
"repositoryUrl": "https://github.com/ibexa/behat.git",
"package": "ibexa/behat",
"shouldBeAddedAsVCS": false
}
]
}
Steveb-p added a commit that referenced this pull request Aug 12, 2026
previewContentAction() only called restoreConfigScope()/setPreviewActive(false)
on the success path; both catch blocks for the forwarded sub-request returned
early, skipping them. This pre-existing gap didn't matter before, since
SiteAccessService never tracked scope changes at all — but now that
changeConfigScope() pushes onto its stack, skipping the restore leaves
SiteAccessService and ConfigResolver disagreeing on the current scope: the
stack silently unwinds on request finish (no CONFIG_SCOPE_RESTORE dispatch),
so SiteAccessService::getCurrent() reverts while ConfigResolver, updated by
ConfigScopeListener reacting to the earlier CONFIG_SCOPE_CHANGE, stays on the
preview scope.

Wrapped the sub-request handling in try/finally so both calls always run,
regardless of how the method exits. Calling restoreConfigScope() even when
the scope was never changed is safe: restoreSiteAccess()'s floor guard never
pops past the request-matched entry, so it's a no-op in that case (matching
the pre-existing test expectations, which already assumed an unconditional
call).

Flagged by GitHub Copilot's review on PR #798.
@sonarqubecloud

Copy link
Copy Markdown

@Steveb-p

Copy link
Copy Markdown
Contributor Author

Backported the reactive part of this (SiteAccessService listening to CONFIG_SCOPE_CHANGE/RESTORE, without the stack/API/SiteAccessAware deprecation work) to the older branches: #805 (5.0) and #806 (4.6).

@tbialcz

tbialcz commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

IBX-12102 (#802) landed on 6.0 via merge-up — feel free to drop SiteAccessRestoreListener in this PR, the stack pop covers it.

…ss() the supported way to switch SiteAccess scope

Manually dispatching ScopeChangeEvent under MVCEvents::CONFIG_SCOPE_CHANGE/
CONFIG_SCOPE_RESTORE was previously the only documented way to switch the
current SiteAccess for content preview, CLI --siteaccess, and sub-requests.
SiteAccessService now tracks the current SiteAccess as a stack and exposes
changeSiteAccess()/restoreSiteAccess() as the primary API; every internal
caller (SiteAccessMatchListener, SiteAccessRestoreListener, DefaultRouter,
ContentPreviewHelper, ConsoleCommandListener, route/fragment/image generators,
HttpUtils) now goes through it instead of constructing SiteAccess\Aware
setters or dispatching the event by hand.

Dispatching the event manually still works, but is deprecated: SiteAccessService
keeps reacting to it for not-yet-migrated/third-party listeners, emitting a
runtime deprecation notice whenever the event wasn't dispatched internally by
changeSiteAccess()/restoreSiteAccess() themselves.

Reconciled against the 79 commits that landed on 6.0 since this branch was
opened, including three PRs that independently touched the same area
(SiteAccess stack handling, SiteAccess\Aware removal, DefaultRouter's
decorator rewrite) - this is a from-scratch re-implementation against that
new baseline rather than a mechanical rebase.
@Steveb-p
Steveb-p force-pushed the IBX-12204-siteaccessservice-scope-stack branch from d536264 to ef50d06 Compare October 2, 2026 12:28
@sonarqubecloud

sonarqubecloud Bot commented Oct 2, 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.

3 participants