chore: allow phpunit ^13.0 - #8537
Conversation
f96d0bd to
6f57bd2
Compare
… surfaced Three behaviours change on the wire, all accepted rather than worked around since nothing is deployed yet: - the JSON-LD content type loses its "; charset=utf-8" suffix (27 assertions); - an operation declaring an output DTO now reports the resource short name in @type ("Stage", not "StageResponse"); @context still points at the DTO. No client keys on the value — it is an untyped string in core/schema.d.ts; - responses carry the LDP Allow / Accept-Post headers. ApiTestCase::$alwaysBootKernel flipped from null (boot on every createClient) to false (reuse a booted kernel). Reusing it keeps the Doctrine connection open across tests, so Foundry's per-test migrate reset has to terminate that live connection to drop the database — which kills the migration run itself and leaves every table missing. There was no shared base class to fix this in, so this adds App\Tests\ApiTestCase; the 45 test files move onto it in the same pass as the api-platform/test namespace split. PHPUnit stays at ^12.5 in api/ only: api-platform/test caps it at ^12.2 even though its classes run unchanged on 13 (api-platform/core#8537). provisioner/ is an independent Composer project with no API Platform dependency and keeps 13. See ADR-070. Flipping ignoreIndirectDeprecations to false is what made the rest visible — vendor deprecations were simply not being counted before: - doctrine/orm wants SortDirection instead of "ASC"/"DESC", in one #[ORM\OrderBy] and three QueryBuilder::orderBy() calls; - zenstruck/foundry 2.9 deprecated the Factories/ResetDatabase traits in favour of the PHPUnit extension (already registered) plus #[ResetDatabase]: 62 files; - framework.profiler.collect_serializer_data and doctrine.orm.controller_resolver.auto_mapping are removed in the next majors and already equalled the only accepted value. Dropping collect_serializer_data also dropped the only framework.profiler declaration in dev, which silently disabled the profiler and every service it brings (PHPStan caught it via a missing mailer.message_logger_listener). The key is restored as `profiler: null` with a comment saying it is load-bearing. Rector 2.6 removes the version-numbered PHPUnit sets; withComposerBased(phpunit) was already configured and derives them from composer.lock. The two undocumented pins — rector/rector ~2.5.9 and an exact playwright-core override — are lifted; nothing broke, which is the usual fate of a pin nobody can explain.
…cure 1.0) (#1298) * ci: pin every floating image and close the Dependabot gaps Four images ran with no tag at all (hadolint, markdownlint-cli2, alpine, oskarstark/php-cs-fixer-ga), so two runs of the same commit could use different tool versions — and the Makefile drifted from CI, which pins hadolint via the action. All are now pinned. The third-party `esoledad/php:8.5` backing all ten PHP jobs is digest-pinned rather than replaced: swapping the job container is a CI change no local run can validate, so it stays a follow-up. Freezing the digest is the part that is safe to do now. docker/bake-action moves to v7.4.0 for its workflow-command injection fix. Dependabot was watching neither the compose files (the `docker` ecosystem only reads Dockerfiles, which is how traefik and cloudflared aged unnoticed), nor `provisioner/`, `core/`, `mobile/`, the root npm manifest holding the security overrides, nor the vendor-bin namespaces where php-cs-fixer lives. * build(deps): bump the ops images Traefik v3.3 -> v3.7.13 closes five security advisories; cloudflared was over a year behind at 2025.7.0. The provisioner Dockerfile was the only one pinned by tag alone, so Dependabot could not see it while php and pwa were tracked: php and composer are now digest-pinned, and install-php-extensions goes 2.11.12 -> 2.11.27 with its checksum recomputed. Side stacks: glitchtip 6.1.6 -> 6.2.6, its redis:7-alpine aligned onto the redis:8-alpine the app stack already runs, and uptime-kuma 1.23.16 -> 2.5.5. Note that uptime-kuma 1.x -> 2.x is a major with its own data migration; it is deployed out of band (.docker/uptime-kuma/README.md), so the bump lands here but the migration is a manual step. * feat(mercure)!: speak the Mercure 1.0 protocol end to end The hub is embedded in the FrankenPHP binary (ADR-037) and upstream pins dunglas/mercure/caddy 0.24.2, which only speaks 0.x. A frankenphp_builder stage now recompiles it at v1.0.0. Two facts make that cheap: mercure/caddy v1.0.0 wants Caddy v2.11.4, exactly what the upstream image embeds, and frankenphp is built from the sources vendored in the builder image so it links against its own libphp. The only obstacle was the toolchain — the module declares go 1.27 while the builder ships Go 1.26 under GOTOOLCHAIN=local — which GOTOOLCHAIN=auto resolves. Runtime and builder images must now be bumped together. Protocol 1.0 is a breaking change on three axes, each of which fails silently (the stream simply never delivers): - subscriptions move from ?topic= to ?match=, verified against a live hub: ?match= returns 200 text/event-stream, ?topic= returns 400. - tokens become RFC 9068 access tokens (typ: at+jwt, authorization_details). MercureTokenIssuer is rewritten on top of symfony/mercure 0.8 instead of hand-rolling the claim with lcobucci, which also removes the risk of it drifting from the tokens the bundle mints when publishing. - the subscription API gains a match-type segment: /subscriptions/exact/<topic> answers 401 (route present, token required) while the 0.x path is now 404. The cookie is renamed to __Secure-mercure_access_token. Its Secure flag is load-bearing, not defensive: browsers reject the prefix without it. aud is pinned on both sides rather than derived, because the backend publishes through the internal MERCURE_URL while browsers subscribe on the public one, and an unpinned hub expects a different audience per origin. compose.dev.yaml passed the "demo" directive, renamed "playground" in 1.0 — and an unknown directive is now fatal rather than a silent no-op, so this alone stopped the dev hub from booting. The mocked E2E suite fakes SSE with a CustomEvent and never touches the hub, so it stays green whatever the protocol does. The integration smoke test now asserts ?match= and the cookie name explicitly. * feat(api)!: upgrade to API Platform 5.0 and purge the deprecations it surfaced Three behaviours change on the wire, all accepted rather than worked around since nothing is deployed yet: - the JSON-LD content type loses its "; charset=utf-8" suffix (27 assertions); - an operation declaring an output DTO now reports the resource short name in @type ("Stage", not "StageResponse"); @context still points at the DTO. No client keys on the value — it is an untyped string in core/schema.d.ts; - responses carry the LDP Allow / Accept-Post headers. ApiTestCase::$alwaysBootKernel flipped from null (boot on every createClient) to false (reuse a booted kernel). Reusing it keeps the Doctrine connection open across tests, so Foundry's per-test migrate reset has to terminate that live connection to drop the database — which kills the migration run itself and leaves every table missing. There was no shared base class to fix this in, so this adds App\Tests\ApiTestCase; the 45 test files move onto it in the same pass as the api-platform/test namespace split. PHPUnit stays at ^12.5 in api/ only: api-platform/test caps it at ^12.2 even though its classes run unchanged on 13 (api-platform/core#8537). provisioner/ is an independent Composer project with no API Platform dependency and keeps 13. See ADR-070. Flipping ignoreIndirectDeprecations to false is what made the rest visible — vendor deprecations were simply not being counted before: - doctrine/orm wants SortDirection instead of "ASC"/"DESC", in one #[ORM\OrderBy] and three QueryBuilder::orderBy() calls; - zenstruck/foundry 2.9 deprecated the Factories/ResetDatabase traits in favour of the PHPUnit extension (already registered) plus #[ResetDatabase]: 62 files; - framework.profiler.collect_serializer_data and doctrine.orm.controller_resolver.auto_mapping are removed in the next majors and already equalled the only accepted value. Dropping collect_serializer_data also dropped the only framework.profiler declaration in dev, which silently disabled the profiler and every service it brings (PHPStan caught it via a missing mailer.message_logger_listener). The key is restored as `profiler: null` with a comment saying it is load-bearing. Rector 2.6 removes the version-numbered PHPUnit sets; withComposerBased(phpunit) was already configured and derives them from composer.lock. The two undocumented pins — rector/rector ~2.5.9 and an exact playwright-core override — are lifted; nothing broke, which is the usual fate of a pin nobody can explain. * chore(deps): bump the web and mobile toolchains Web: Next 16.3.5, Sentry 10.75, maplibre-gl 6.10, next-intl 4.14.5, vite 8.3, vitest 5.0.1, zod 4.6.5 (pwa and core), prettier 3.9.8 — whose reformatting of nine pre-existing files is the bulk of the diff. Mobile stays on Expo SDK 57 / RN 0.86 (SDK 58 is preview-only), with every expo-* patch, RN 0.86.3 and maplibre-react-native 11.4. Three upgrades were tried and backed out; all three are recorded in ADR-070 rather than left as folklore: - ESLint 10: eslint-plugin-react, pulled in by eslint-config-next, still calls context.getFilename(), removed in v10. The peer ranges allow ESLint 10 — they are simply wrong. - TypeScript 7 on mobile: typescript-eslint refuses it outright, and npm hoists a single TypeScript for the whole workspace, so mobile's choice decided what eslint-config-next parsed with in pwa. - React 19.3: Expo 57 pins React 19.2 and jest-expo ships react-test-renderer 19.2.3, so 19.3 broke every mobile render test. The constraint is tightened to ~19.2.8 — ^19.2.8 admitted 19.3, and only the lockfile was holding it back. Jest 30 needed two fixes its types surfaced: `global` is no longer re-exported (use globalThis) and setImmediate is typed as taking no argument. lucide-react-native 1.x moved the per-icon files to .mjs and gated dist/ behind its exports map, so the deep imports the tree-shaking relies on (#1176) had to become the public icons/* subpath. jest-expo's resolver picks the react-native condition there, i.e. ESM, so the module mapper stays — retargeted at the CJS build through a resolved filesystem path, which exports cannot refuse. `trash-2` is gone from the package (though lucide-react still ships it as an alias of `trash`); the two have identical path data, so Trash2 now maps to trash. * docs: record the dependency version policy and the ceilings we hit There was no ADR on versioning at all, which is how two pins ended up with no explanation behind them and how four images ended up untagged. ADR-070 states the rule (a pin without an ADR is a pin to lift) and tabulates each ceiling with the condition that lifts it, so the next pass re-attempts on a fact rather than on a hunch. Three of those ceilings share a shape worth naming: the constraint that bites is not the one you declare. npm hoists one React and one TypeScript for the whole workspace, so a bump in mobile silently changes what pwa builds against. ADR-037 gets an addendum: the PHP image is no longer the upstream FrankenPHP image but a recompilation of it, because the Mercure hub lives inside that binary. * feat(mercure): add the MERCURE_ISSUER dev default Mercure 1.0 access tokens are RFC 9068, so the backend has to mint an iss that matches the Caddyfile issuer block. compose.yaml derives it from DOMAIN for both php and worker, but anything running outside Compose — bin/console, PHPStan's cache warmup, a throwaway PHPUnit container — reads api/.env instead and would die on a missing environment variable. Verified by running the Mercure functional test with no MERCURE_ISSUER in the environment: it still mints a valid at+jwt, so the dotenv value is what the configured hub picks up. * chore(core): regenerate schema.d.ts after the Mercure docblock change An #[ApiResource] class docblock becomes the resource description in the exported OpenAPI, so renaming the cookie in MercureToken's PHPDoc drifts the generated types and fails the OpenAPI -> TS drift job. Worth recording that this two-line docblock is the *only* drift: API Platform 5 changed nothing in the exported schema, even though it now emits OpenAPI 3.2. * fix(ci): declare eslint and typescript at the root, and repair three pins Four red jobs, three causes. **ESLint and typegen** shared one: when two workspaces ask for different versions of a package, npm stops hoisting it and nests a copy in each — and anything that *is* hoisted can then no longer resolve it. `typescript` lived only in pwa (5.9) and mobile (6.0), so a clean `npm ci` left the root without one and the hoisted `openapi-typescript` died with `Cannot find package 'typescript'`. Same story for `eslint`, declared only in pwa, which the hoisted `eslint-plugin-react` could not find. This is unstable rather than plainly broken: whether npm hoists one of the two or neither depends on resolution order, which is why an incremental `npm install` locally and a clean `npm ci` in CI disagreed. Both are now declared in the root devDependencies at the version the hoisted tooling should see; mobile keeps its nested TypeScript 6. Verified by reproducing the CI layout (`npm ci` from a removed node_modules), not by an incremental install. **PHP-CS-Fixer** was my own regression: pinning `oskarstark/php-cs-fixer-ga` to its newest *tag* (3.26.0) downgraded the bundled PHP below what the config file needs, so it died on a parse error at the `new Config()->...` chain. `latest` carries PHP 8.5.10; it is now pinned by digest, which keeps the reproducibility the tag was meant to buy without the downgrade. **hadolint** flagged the new builder stage: xcaddy is pinned to v0.4.7 (DL3062), and the build-info verification writes to a temp file instead of piping into grep (DL4006). Rebuilt to confirm the binary still reports mercure/caddy v1.0.0. ADR-070 gains the hoisting rule: if a package is consumed by something npm hoists, declare it at the root, and reproduce with `npm ci` rather than an incremental install. * fix(tests): give each test class its own $alwaysBootKernel slot A static property a subclass does not redeclare is the ancestor's storage. The 23 Functional classes that want kernel reuse assigned `self::$alwaysBootKernel = false` in setUpBeforeClass(), so every one of those writes landed in App\Tests\ApiTestCase's single slot — and since PHPUnit runs the suite in one process with no reset, whichever class ran first flipped the flag off for every other Functional test for the rest of the run. That is order-dependent rather than reliably broken, which is the worst shape: it silently re-introduces, suite-wide, the kernel-reuse/Foundry-migrate failure the base class exists to prevent. Redeclaring the property in each class gives it real per-class storage. Also from review: - docker/bake-action goes back to a full commit SHA. Replacing the previous SHA pin with a floating tag contradicted the very commit that introduced it ("pin every floating image"), and a tag can be re-pointed by the action owner. - the Caddyfile keeps its `log` directive. Removing the redaction filter was right — the `authorization` query parameter no longer exists in Mercure 1.0 — but removing the whole block turned access logging off, taking the X-Request-Id correlation trail with it. * build(deps): require symfony/polyfill-php86 explicitly Four files name \SortDirection directly — PHP's native global enum, which ships in 8.6, so on this project's 8.5 it comes from symfony/polyfill-php86. That polyfill was only present transitively, via doctrine/orm and doctrine/collections. Relying on a transitive dependency for a class we reference by name is a latent break: the day Doctrine stops needing it, four call sites fatal with nothing in composer.json to explain why. Declaring it also settles a review finding that has now been raised twice. \SortDirection resolves under the production autoloader — verified in the built prod image (classmap-authoritative, --no-dev, PHP 8.5.10), where it loads from vendor/symfony/polyfill-php86/Resources/stubs/SortDirection.php, and where doctrine:mapping:info reports [OK] App\ApiResource\TripRequest, i.e. Doctrine reads the #[ORM\OrderBy] attribute the finding predicted would be fatal. The suggested alternative, Doctrine\Common\Collections\Order, belongs to the Collections criteria API; Doctrine's own deprecation message asks for SortDirection by name, and doctrine/orm imports that same global enum in ClassMetadata. A comment at the TripRequest call site now records all of this, so the next reader does not re-litigate it. * docs: renumber the version-policy ADR to 071 main landed its own ADR-070 (freshness is dispatch completeness) while this branch was open, so the dependency-version ADR takes the next free number. * docs: drop a sentence duplicated into the compose-layout paragraph Inserting the Mercure 1.0 note ended on the same sentence the original text resumed with, so "in prod that edge serves plain HTTP behind Traefik + Cloudflare Tunnel" appeared twice back to back. CLAUDE.md is loaded as project instructions for every session, so the redundancy is not free.
6f57bd2 to
74b812d
Compare
|
Hey guys, As Please keep in mind that this is my first contribution to API Platform - I've tried my best and hopefully there will not be many problems. As such I would like to thank all the maintainers (and @soyuka @dunglas) for amazing work they have done over the years and let you know that if there are any issues, I will be monitoring these PRs closely until merged. If something doesn't makes sense, if for example Mercure failed pipelines get fixed upstream - I would be happy to rebase all of them (so we're 100% nothing is breaking), the same about discussing / fixing if anything else is found or recommended. I appreciate any suggestions, critique etc - I currently have several large production systems running 4.3 "bleeding edge" with PHPUnit 13 and am more than looking forward to 5.0 soon, so any feedback is more than welcome! Thank you, |
74b812d to
1af3b2b
Compare
1af3b2b to
37f1dc8
Compare
37f1dc8 to
b5e7af2
Compare
|
Thanks @mislavjakopovic for the patches! |
api-platform/core#8537 widened api-platform/test to "^11.5 || ^12.2 || ^13.0", but no release carries it yet: Packagist still serves v5.0.0 with "^11.5 || ^12.2", which caps the demo at PHPUnit 12. Widening the constraint here changes nothing today, the lock stays on 12.5.35. It only means the next `composer update` picks PHPUnit 13 on its own once api-platform/test ships the change, instead of needing another manual bump.
Since #7887
api-platform/testsplit requiresphpunit/phpunit ^11.5 || ^12.2, it cannot be installed next to PHPUnit 13, although the classes run unchanged on it.Before the split those classes shipped inside
api-platform/symfony, which lists PHPUnit only inrequire-dev, a section Composer never enforces for dependencies, so PHPUnit 13 worked - the extraction moved that constraint intorequire, and it now blocks "bleeding edge" migration to API Platform 5.This PR widens the constraint to
^11.5 || ^12.2 || ^13.0in the rootcomposer.jsonand in every component manifest that pins it, same shape as c0886c1 (^11.5) and #7623 (^12.2).The serializer component's
sebastian/exporterpin (#7663) gains^8.2the same way, since PHPUnit 13 requires exporter 8.After initial commit on this pull request and failed pipelines, the rabbit hole went moderately deep.
In the end several preparation PRs were needed for the suite to stay green under 11, 12 and 13:
ApiPlatform\Symfony\Bundle\Test\ApiTestCase; loading it triggers a deprecation that 13 counts, which would turnphpunit-no-deprecationsredwith()withoutexpects()on mocks, deprecated in 13 and rejected in 14, plus the other deprecations and warnings 13 reportsapi:upgrade-filterusedUnifiedDiffOutputBuilder, removed in sebastian/diff 9, which PHPUnit 13.2 requiresPlease visit those if you're interested in more details.
PHPUnit 13 requires PHP 8.4.1, so the 8.2 and 8.3 jobs will keep resolving 11.5 and 12, while the 8.4 and 8.5 jobs move to PHPUnit 13 without a workflow change.
The separate pull requests run against the currently supported PHPUnit versions, showing that these changes should remain backwards compatible.
For that very reason I've cherry-picked their commits into this pull request as well, so CI on here runs the same changes together on PHPUnit 13.3.x.
I've chosen this separation into several PRs for cleaner readability and easier reviews from maintainers, hope that's okay.
I would suggest keeping this pull request as a draft until all related preparation PRs are merged; then I will adjust description of it and rebase branch to the dependencies bump commit alone.
One last note which might be of concern - the root
conflictentry<9.5,phpunit.baseline.xmlanddocs/composer.json(^10, only reachable viarequire-devsince #8527) are left untouched.