IBX-12204: SiteAccessService owns a scope-change stack, SiteAccessAware deprecated - #798
Draft
Steveb-p wants to merge 11 commits into
Draft
IBX-12204: SiteAccessService owns a scope-change stack, SiteAccessAware deprecated#798Steveb-p wants to merge 11 commits into
Steveb-p wants to merge 11 commits into
Conversation
…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.
SiteAccessService no longer takes the shared, container-wide SiteAccess as a constructor dependency, and getCurrent() goes back to being null until a real request match or an explicit changeSiteAccess() call establishes one. Its "uninitialized" matcher (SiteAccess::MATCHING_TYPE_UNINITIALIZED) is treated the same way defensively, should it ever end up on the stack. ConsoleCommandListener no longer mutates the shared SiteAccess singleton in place either: it now constructs a fresh SiteAccess for the CLI invocation and hands it to SiteAccessService::changeSiteAccess(), which it already called. IO/repository integration tests (IbexaTestKernel and LegacyTestContainerBuilder) never dispatch a request or console command, so they now explicitly seed SiteAccessService via changeSiteAccess() with a freshly-constructed default SiteAccess — the same mechanism CLI uses, not a shared/mutated singleton.
ConsoleCommandListener no longer mutates the shared SiteAccess singleton in place; it only calls SiteAccessService::changeSiteAccess() now. Any command that type-hints the shared SiteAccess service directly to read the CLI-resolved siteaccess (like the ibexa:behat:test-siteaccess fixture command used by this scenario) no longer sees it updated. This is an intentional, accepted BC break, documented in the PR description; tagging the scenario @broken until the fixture command migrates to SiteAccessServiceInterface::getCurrent().
…vice Every DI reference to SiteAccessService (YAML service arguments, a Reference() in LegacyTestContainerBuilder, and ComplexConfigProcessor's constructor type-hint) now points at SiteAccessServiceInterface instead of the concrete class, consistent with how it's already injected elsewhere (e.g. SiteAccessLimitationType). The concrete class is only still referenced where structurally required: its own service definition, the interface alias's target, and IbexaTestKernel's compiler pass (Symfony aliases have no Definition object to fetch via getDefinition()/hasDefinition()) or a call to onSiteAccessMatch(), a subscriber-only method not part of the interface.
Same bug as ibexa/behat's TestSiteaccessCommand: constructor-injected the bare, shared SiteAccess singleton to display the CLI-resolved siteaccess name, which ConsoleCommandListener no longer mutates in place. Switched to SiteAccessServiceInterface::getCurrent(), matching the pattern already used elsewhere in this PR.
The CLI SiteAccess BC break this scenario caught is now fixed on both known affected consumers: this repo's own DebugConfigResolverCommand, and ibexa/behat's TestSiteaccessCommand (fixed in a companion PR, cross-tested here via a temporary dependencies.json in the next commit).
Points Behat CI at the branch that fixes TestSiteaccessCommand, so this PR's "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.
There was a problem hiding this comment.
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 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 | ||
| } | ||
| ] | ||
| } |
This was referenced Aug 10, 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.
|
This was referenced Aug 12, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Warning
This branch includes a temporary commit (
dependencies.json) that points the Behat CI job atibexa/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.
Description:
A
ConfigScopeListeneralready reacts toMVCEvents::CONFIG_SCOPE_CHANGE/CONFIG_SCOPE_RESTOREand broadcasts the new SiteAccess to everyVersatileScopeInterfaceconfig resolver and everySiteAccessAwareview manager/view provider — butSiteAccessServiceitself was never one of those broadcast targets. It kept a single value, injected once viaSiteAccessAware::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 aScopeChangeEventand dispatch it itself, with no supported API for it.This PR makes
SiteAccessServicethe single owner of "what is the current SiteAccess right now":SiteAccessService::changeSiteAccess(SiteAccess $siteAccess): SiteAccessandrestoreSiteAccess(): ?SiteAccess, which wrap the existingScopeChangeEventdispatch underMVCEvents::CONFIG_SCOPE_CHANGE/CONFIG_SCOPE_RESTORE— no new event types, just a supported API instead of hand-dispatching.SiteAccessServicenow maintains a real LIFO stack ofSiteAccesschanges instead of a single injected value:EventSubscriberInterfaceand subscribes toMVCEvents::SITEACCESS: on aMAIN_REQUESTmatch it resets the stack to[$siteAccess]; on aSUB_REQUESTmatch (fragments, ESI, content-preview's internal sub-request) it pushes onto the stack.KernelEvents::FINISH_REQUEST, which pops the stack — but never below 1 remaining entry. That single "floor guard" is shared withrestoreSiteAccess(), so a sub-request's own push is always safely undone by its own finish, and an unbalancedrestoreSiteAccess()call (or CLI's one-timechangeSiteAccess()floor) can never strip the stack down to empty.getCurrent()returns the top of the stack, ornullif it's empty — it does not fall back to the shared, container-wideSiteAccesssingleton.SiteAccessService(andConsoleCommandListener, 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.SiteAccessAwareis marked@deprecated(no runtime deprecation notice — it's still legitimately used by consumers not migrated here) in favor of callingSiteAccessService::getCurrent()directly.SiteAccessAwareconsumers to constructor-injectSiteAccessServiceInterfaceand callgetCurrent()instead of storing a value viasetSiteAccess():ContentPreviewHelper,ConsoleCommandListener,HttpUtils,DefaultRouter,Generator(andUrlAliasGenerator),AliasGeneratorDecorator,DecoratedFragmentRenderer/InlineFragmentRenderer.ConsoleCommandListenerno longer mutates the sharedSiteAccesssingleton at all — it constructs a freshSiteAccessfor the CLI invocation and hands it tochangeSiteAccess().ConfigResolverand friends still read that singleton directly for theMATCHING_TYPE_UNINITIALIZEDsentinel (see IBX-12192) and are intentionally not migrated in this PR (would require a circular DI dependency back ontoSiteAccessService, plus non-trivial caching/broadcast semantics). Same reasoning excludesSiteAccess\Router,View\Manager/Provider\Configured.IbexaTestKernelandLegacyTestContainerBuilder's test settings now explicitly seedSiteAccessServiceviachangeSiteAccess()with a freshly-constructed defaultSiteAccess, the same mechanism CLI uses.ComplexConfigProcessorwhile migrating it: it read$this->siteAccessService->getCurrent()->namewith no null check. Added agetCurrentSiteAccessName()guard that throwsInvalidArgumentException, matching the precedent already set inSiteAccessService::getSiteAccessesRelation().DecoratedFragmentRenderer/InlineFragmentRendererpreviously only ever saw the original request's SiteAccess when rewriting fragment paths (they weren't part ofConfigScopeListener's scope-change broadcast). They now correctly reflect whateverSiteAccessService::getCurrent()says, including during an active content-preview scope change — this is a correctness fix, not a regression.routing.yml,services.yml,helpers.yml,image.yml, and theSecurityPass/ChainRoutingPass/FragmentPasscompiler passes now passSiteAccessServiceInterfaceas a constructor argument instead of the oldsetSiteAccess()calls;SiteAccessServiceitself gained anEventDispatcherInterfaceconstructor argument and akernel.event_subscribertag. ForHttpUtilsandDefaultRouter— 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.phpstan-baseline.neonentries left behind by removingSiteAccessAware/setSiteAccess()from the migrated classes.Known BC break (intentional): any custom Symfony console command that type-hints the shared
Ibexa\Core\MVC\Symfony\SiteAccessservice directly to read the CLI-resolved siteaccess will no longer see it updated —ConsoleCommandListenerused to mutate that shared singleton in place specifically for such consumers, but no longer does (see above). Commands relying on this must switch to injectingSiteAccessServiceInterfaceand callinggetCurrent()instead. Audited the rest of the publicibexa/*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'sTestSiteaccessCommandfixture command (used bysrc/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 temporarydependencies.json(see warning at the top).Verified:
phpstan analyseclean, and theunit_core/bundle_core/bundle_ioPHPUnit 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:
--siteaccess=<name>and confirmSiteAccessService::getCurrent()(andConfigResolver) reflect that siteaccess for the whole command run.New unit test coverage lives in
SiteAccessServiceTestfor the stack semantics specifically: nestedchangeSiteAccess()/restoreSiteAccess()pairs behaving as LIFO,MAIN_REQUESTresetting vs.SUB_REQUESTpushing, the finish_request floor guard never popping below 1 entry, and a full sub-request-nesting scenario.Documentation:
SiteAccessAwareis now@deprecated— worth a mention in the deprecation notes for this release, pointing integrators atSiteAccessServiceInterface::getCurrent()/changeSiteAccess()/restoreSiteAccess()instead of the oldsetSiteAccess()+ hand-dispatchedScopeChangeEventpattern.The BC break above should also be called out explicitly in the release/upgrade notes: custom console commands reading the shared
SiteAccessservice to determine the CLI-resolved siteaccess must switch toSiteAccessServiceInterface::getCurrent().