Repository navigation
Conversation
bmack
force-pushed
the
composer-mode-standalone
branch
from
September 8, 2026 10:49
465ac8e to
b1cefe1
Compare
TYPO3 ships two installation modes and only one of them was covered by
functional tests: every test instance was a classic mode installation, even
though composer mode is what most projects run. Set
TYPO3_TESTING_INSTANCE_MODE=composer
to provision composer mode instances instead. Classic mode stays the default,
and the mode is part of the instance identifier, so a test case's classic and
composer instances never collide.
A composer mode instance is not its own composer installation. One shared
installation containing every system extension and every fixture extension is
built once per run by Build/Scripts/setupFunctionalComposerSuperset.php, and
each instance borrows its vendor tree and its package artifact by symlink:
<instance>/vendor -> <shared>/vendor
<instance>/public/_assets -> <shared>/public/_assets
<instance>/config/system own configuration
<instance>/public own document root, fileadmin, typo3temp/assets
<instance>/var own caches and logs
Per test case only the *active* package set differs, which is expressed by
narrowing the artifact in memory. Building a real installation per test
configuration was measured at about five seconds each, which for the roughly
two hundred configurations in this repository would cost far more than the
provisioning this saves.
Classes are deliberately not loaded from the shared installation. The root
autoloader already maps every system extension and every fixture extension, so
no second autoloader is registered and no second copy of any third party
package is ever loaded. One consequence: class alias maps that the shared
installation registered at install time are not active, because the root
installation never processed those packages.
Notable details, each of which was a bug on the way and none of which fails
loudly:
* TYPO3 decides between the classic layout (typo3conf, typo3temp/var) and the
composer layout (config, var) by whether the project root and the document
root differ - not by composer mode. Test instances therefore set
TYPO3_PATH_ROOT to the public directory in composer mode.
* Package::getPackagePath() resolves the artifact's relative path against
Environment::getProjectPath() once and writes the result back. Caching one
PackageCacheEntry for the process therefore binds every package to whichever
instance touched it first, and every later instance looks for its packages
inside that instance. Only the raw artifact data is cached; the entry is
rebuilt whenever the instance changes.
* On the command line in composer mode TYPO3 reports the console binary as the
entry script, because in a real installation it sits outside the document
root. Functional tests are command line processes simulating web requests,
and NormalizedParams derives the site URL by subtracting the entry script's
directory from the request - so it subtracted typo3/sysext/core/bin/ from
every request, the site path collapsed to an empty string, and every
generated link silently lost its leading slash. The front end entry script
is reported instead, which is what a web request has.
* A console command run in a sub process cannot use EXT:core/bin/typo3: that
script derives its autoloader from its own directory, and since the shared
installation's packages are symlinks into the checkout, it resolves back to
the root autoloader, never defines TYPO3_COMPOSER_MODE and runs in classic
mode. Composer mode instances get a generated entry point instead, which
getCliEntryPoint() hands to the test.
* Applications bootstrapped by a test - the install tool, notably - must boot
through bootTestInstance(), because Bootstrap resolves the artifact from the
root installation, and the mono repo has none.
* fileadmin and typo3temp/assets are web accessible and belong below public/.
Getting that wrong makes the default file storage point at nothing.
* Frontend sub requests bootstrap a second time and need the same narrowed
package set. Their server parameters also have to name the real document
root, or the site path is derived from a script outside the public path.
* The sites configuration cache removal in tearDown() has to use the layout's
variable path, or it silently does nothing and the stale configuration leaks
into the next test.
* Paths declared by test cases are given relative to the document root, which
only carries the extension tree in classic mode; they now fall back to the
original root.
A "PKG:typo3/app:" resource identifier carries a path relative to the project
root, which is the document root only in classic mode, so
getAppRelativePublicPath() is provided for tests naming such resources.
TestingBootstrapRunner overrides Bootstrap::createPackageCache() through late
static binding, so no change to typo3/cms is required.
Composer mode is opt-in, and everyone who does not opt in - TYPO3 v14 and v15 extensions tested from the same branch included - must not notice it. A few parts of the first version reached into classic mode, or into API that extensions build on: * The instance identifier, and with it the instance path and database name, included the mode for classic instances too. It is unchanged now, only composer instances get a suffix. * Paths to link or provide fell back to the original root in classic mode as well, so a wrong path no longer failed. The fallback is limited to composer mode. * Testbase::setUpLocalConfiguration() and setUpBasicTypo3Bootstrap() gained parameters, which is fatal for subclasses overriding them. Both keep their signatures and read the mode themselves. * FunctionalTestCase gained four final protected methods, which are fatal for test cases that already have a method of the same name. The mode check is private now, and the helpers tests need are moved to the new TestInstance class. * TestingBootstrapRunner extends Bootstrap as a readonly class, which is a fatal error before TYPO3 v14.3. Enabling composer mode on an older version now fails with a message instead. Each instance's package set is now written to its own package artifact, the composer mode counterpart of PackageStates.php, and read by TYPO3's ComposerPackageArtifact. That replaces the in-memory package cache and the static state it needed, and the console entry point no longer has to carry the extension list. Package objects are unserialized freshly on every boot, so they can no longer be bound to the wrong instance. The narrowed set keeps the dependency order of the shared installation's artifact. Also: the duplicate exception code is replaced, and the default extension list exists once.
bmack
force-pushed
the
composer-mode-standalone
branch
from
November 14, 2026 14:09
b1cefe1 to
ac1e364
Compare
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.
Functional test instances can now be provisioned as composer mode installations. Until
now every instance is a classic mode installation, although composer mode is what most
projects run, so a whole installation layout -
config/andvar/beside apublic/document root, packages resolved from a package artifact, assets in
_assets/- nevergets tested.
Nothing changes without the switch
Classic mode stays the default and behaves exactly as before: same instance path, same
database name, same setup steps, same API. In particular:
TestbaseorFunctionalTestCasechanges its signatureprotectedmethods onFunctionalTestCasethat could collide with methods oftest cases in extensions
Composer mode needs TYPO3 v14.3 or later, and says so with a clear message when it is
enabled on an older version.
How a composer mode instance is built
An instance is not its own composer installation. A shared installation containing every
extension the test cases load is built once per run (TYPO3 Core does that with
Build/Scripts/setupFunctionalComposerSuperset.php,TYPO3_TESTING_SUPERSET_PATHcanpoint elsewhere). Each instance borrows its vendor tree:
The package set of a test case is written into the instance as its own package artifact,
derived from the one the installer plugin built for the shared installation: every
package stays known, only the ones the test case loads are active. It is the composer mode
counterpart of the
PackageStates.phpof a classic instance, and TYPO3 reads it with itsown
ComposerPackageArtifact.TestingBootstrapRunnerpointsBootstrapat it byoverriding
createPackageCache(), so TYPO3 itself needs no change.Classes are not loaded from the shared installation. The root autoloader already maps
every extension, so no second autoloader is registered and no third party package is
loaded twice.
What to look at
and active are always the same set. Here the shared installation installs everything
and each instance activates a subset. This is the one place the model departs from
reality.
SystemEnvironmentBuilder::determineCurrentScript(). On the CLI, composer modereports the console binary as the entry script. Functional tests are CLI processes
simulating web requests, and
NormalizedParamsderives the site path from the entryscript, so every generated link lost its leading slash. In composer mode the entry
script is
public/index.phpnow, as for a web request.EXT:core/bin/typo3finds its autoloaderrelative to itself, which resolves outside the instance. Composer mode instances get a
generated entry point instead, with the instance paths written in, because Symfony
Processdoes not pass onputenv()values.For tests that run in both modes
TestInstance(internal, used by TYPO3 Core) has three helpers for tests that mustwork in either layout:
boot(), for tests that bootstrap an application themselvesgetCliEntryPoint(), for console commands in a sub processgetAppRelativePublicPath(), forPKG:typo3/app:resource identifiers, which arerelative to the project root and not the document root
Verification
TYPO3 Core functional suite on SQLite, in 4 chunks as in CI:
every chunk. The only differences come from the Core companion patch's use of
TestInstance, which does not exist without this change.requests. The remaining failures are in Core tests added or changed since the companion
patch was prepared, which assume the classic layout. These tests call
Bootstrap::init()directly, link into
typo3conf/sites, or use the new file storage configuration. Thecompanion patch is being updated for them.
Lint (PHP 8.2), CGL and the unit tests of this package are green.