The tools of a Nextcloud app run on a newer PHP than its code, which is checked on the oldest¶
- Status: accepted
- Date: 2026-10-18
Context¶
A Nextcloud app of the blueprint runs on PHP 8.2 and newer, as <php min-version="8.2"/> of appinfo/info.xml and require.php of composer.json say; Nextcloud 33 and 34 accept PHP 8.2 to 8.5. Its tools ran on PHP 8.2 too: the CI set up PHP 8.2, and config.platform.php of composer.json, for which Composer locks the tools, was 8.2.27.
- PHPUnit stayed at an end of its life. PHPUnit 10.5 has had no bug fixes since February 2025 and 11.5 none since February 2026; PHPUnit 12 needs PHP 8.3 and 13 PHP 8.4. paperless-sync and paperless-unified-search still ran PHPUnit 10.5.
- Renovate proposed what could never pass. Its filter by the PHP of the platform,
constraintsFiltering: "strict", let PHPUnit 13 through: the pull requests Dennis-Otto/paperless-sync#80 and Dennis-Otto/paperless-unified-search#87 replaced^10.5with^13.0, failed twice and closed themselves unseen, and with them went the step to PHPUnit 11 or 12. - The order of the tests was always the same.
phpunit.xmlran them in the order of their files, so a test that needs what another one left behind, such as a stream wrapper or the static server of Nextcloud, passed by luck. With thephpunit.xmlof this decision, the 1124 tests of paperless-sync and the 387 of paperless-unified-search passed with their PHPUnit 10.5 on PHP 8.3, in a random order with five seeds and in reverse.
Options¶
- Raise the oldest PHP of the apps to 8.3. Simple, and allowed in this rebuild, but it drops the servers that still run PHP 8.2, for nothing that the code needs.
- Run the tools on PHP 8.3 and check the code on PHP 8.2 as well. The tools can move on, and what the app ships keeps running where it ran.
- Keep PHP 8.2 and PHPUnit 11.5, and only stop Renovate from proposing PHPUnit 13.
Decision¶
Option 2:
- The tools run on the PHP of
config.platform.phpincomposer.json,8.3.16for a new app, the oldest PHP 8.3 that Psalm 6 accepts, with PHPUnit 12. The CI and the Variants read that PHP and the oldest PHP ofappinfo/info.xmlfrom the files of the app, set up the oldest one first and the PHP of the tools last, so thatphpruns the tools, and set up only one when both are the same. An app made before keeps its tools on PHP 8.2 until its owncomposer.jsonmoves them. - The new step Oldest PHP of
scripts/check.shparses what the app ships,appinfo/,lib/andtemplates/, with that PHP, asphp8.2or the like, and runscomposer check-platform-reqs --lock --no-devwith it, so that a package of the run time that needs a newer PHP fails as well. The CI fails without that PHP; on a computer without it, the step says that the CI checks it. Psalm checks the code for PHP 8.2, asphpVersionofpsalm.xmlsays, so a function or class of a newer PHP fails there. - Renovate filters the Composer packages by PHP 8.3, proposes each major version in a pull request of its own,
separateMultipleMajor, so that one that needs a newer PHP holds back none that the app can take, and holds PHPUnit below 13 until the tools run on PHP 8.4. phpunit.xmlruns the tests in a new random order each time, each after the tests that it depends on (executionOrder="depends,random",resolveDependencies="true"), and fails a test that prints (beStrictAboutOutputDuringTests) or that uses what PHPUnit deprecates (failOnPhpunitDeprecation): attributes that PHPUnit 10.5, 11.5, 12 and 13 all know, so that the file of the blueprint fits every app, whichever major version itscomposer.jsoninstalls.
---
config:
flowchart:
wrappingWidth: 400
---
flowchart TB
accTitle: Which PHP checks a Nextcloud app
accDescr: Renovate keeps the tools of composer.json current, with only the releases that run on the PHP of the tools, each major version on its own and PHPUnit below 13. The CI reads two versions of PHP from the files of the app: config.platform.php of composer.json, for which Composer locks the tools, and the min-version of the PHP in appinfo/info.xml, the oldest PHP that the app runs on. It sets up the oldest PHP first and the PHP of the tools last, so that php runs the tools. php-cs-fixer, Psalm for PHP 8.2 and PHPUnit in a new random order run on the PHP of the tools; then the step Oldest PHP parses the code that the app ships with php8.2 and checks that its packages install there.
renovate["Renovate: tools for PHP 8.3, each major<br>version on its own, PHPUnit below 13"]
composer(["composer.json<br>config.platform.php 8.3.16"])
info(["appinfo/info.xml<br>php min-version 8.2"])
setup["The CI sets up PHP 8.2,<br>then PHP 8.3 for php"]
tools["PHP 8.3: php-cs-fixer, Psalm for PHP 8.2,<br>PHPUnit 12 in random order"]
oldest["php8.2: the syntax of what the app ships,<br>composer check-platform-reqs --no-dev"]
app["An app that runs on PHP 8.2 and newer"]
renovate --> composer
composer --> setup
info --> setup
setup --> tools --> oldest --> app
classDef you fill:#f59e0b2e,stroke:#f59e0b,stroke-width:2px
classDef bot fill:#526cfe2e,stroke:#526cfe,stroke-width:2px
classDef done fill:#16a34a2e,stroke:#16a34a,stroke-width:2px
class composer,info you
class renovate,setup,tools,oldest bot
class app done
Consequences¶
- A new app starts on PHPUnit 12. An app made before moves its tools in its own
composer.json, which the update never touches:config.platform.phpto8.3.16, thencomposer update --lock, and Renovate brings PHPUnit 11 and 12 in pull requests of their own. Until the app moves, Renovate already looks for releases for PHP 8.3, the PHP of its filter: its pull request for PHPUnit 12 fails, because Composer cannot install PHPUnit 12 forconfig.platform.php8.2. The move, not that pull request, brings PHPUnit 12, so it follows the update of the blueprint soon, which keeps that time short. - The dev container runs PHP 8.3, the PHP of the tools, without PHP 8.2: there, the step Oldest PHP leaves the oldest PHP to the CI.
- A test that depends on the order of the tests fails now and then, with the seed of its order in the log;
vendor/bin/phpunit --random-order-seed SEEDruns that order again, asCONTRIBUTING.mdof the app says. - When the tools move to PHP 8.4, the platform of
stacks/php/composer.json, the PHP of Renovate's filter and the limit of PHPUnit move together; a test of the template fails until they agree.