Skip to content

EE-282: Support Symfony 7.4 - #10

Merged
mSprunskas merged 16 commits into
paysera:masterfrom
vinayak-iyer-paysera:EE-282-symfony-7-support
Sep 30, 2026
Merged

mSprunskas merged 16 commits into
paysera:masterfrom
vinayak-iyer-paysera:EE-282-symfony-7-support

Conversation

@vinayak-iyer-paysera

@vinayak-iyer-paysera vinayak-iyer-paysera commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Summary

This lets the bundle install and work on Symfony 7.4. It also removes the deprecation notices Symfony 5.4 and later report for the bundle's classes, and the one Symfony 7.4 reports for its XML service files. The workflow step that gets the Symfony 3 and 4 jobs installing again came in separately through #11, which is merged. I propose releasing it as 1.4.0. It needed paysera/lib-dependency-injection 1.5.0 (paysera/lib-dependency-injection#8), because this bundle requires that library and its earlier versions stop at Symfony 6. 1.5.0 is now released, so I have taken this out of draft.

Why

  • symfony/framework-bundle stops at 6.x, so no Symfony 7 application can install the bundle.
  • On Symfony 7 the container build is a fatal error. ConfigurationInterface declares getConfigTreeBuilder(): TreeBuilder, and Configuration overrides it untyped.
  • The code uses symfony/config, symfony/dependency-injection and symfony/http-kernel without requiring them. symfony/framework-bundle 7.4 accepts their 8.x versions, so on PHP 8.4 Composer takes http-kernel 8.0.
  • With Symfony's debug class loader on (dev environment, phpunit-bridge), Symfony 5.4 and later report notices against the bundle's classes: load() and getConfigTreeBuilder() "might add a native return type", and the parent HttpKernel\DependencyInjection\Extension is internal since 7.1.
  • Symfony 7.4 deprecates XML service configuration: loading the bundle's two XML files reports "XML configuration format is deprecated" in every container build of every application that uses the bundle.

Changes

  • composer.json:
    • symfony/framework-bundle gains ^7.4 and moves to require-dev: nothing in src/ uses it, only the test application.
    • symfony/config, symfony/dependency-injection and symfony/http-kernel, which the code uses, are required at the same versions.
    • paysera/lib-normalization starts at 1.3.1. In earlier releases DateTimeNormalizer fails on PHP 8.2 and later, where date_get_last_errors() returns false.
    • Dev symfony/yaml gains ^6.0|^7.4. Dev phpunit/phpunit 9 starts at 9.6.33: 9.6.21 is the first to handle PHP 8.4's E_STRICT deprecation, and 9.6.33 the first outside CVE-2026-24765.
    • PHP stays ^7.0 || ^8.0.
  • Configuration::getConfigTreeBuilder(): TreeBuilder. A class return type parses on PHP 7.0.
  • The extension extends DependencyInjection\Extension\Extension, the parent of the HttpKernel class. load() gets a @return void docblock, as 1.3.1 did for build().
  • The services are defined in the extension instead of Resources/config/services.xml and Resources/config/services/date_time_normalizer.xml, which are removed, as on EE-288: Add Symfony 7.4 support lib-lock-bundle#13. Their ids, classes, arguments, factory, lazy flags, tags and aliases are unchanged: they compare identical to the XML-loaded ones on Symfony 3.4, 4.4, 6.4 and 7.4. A new test pins what load() registers, with and without the date_time normalizers, and another checks that loading raises no deprecation.
  • A test loads the bundle's classes with the debug class loader and fails on any notice naming them. It is skipped below Symfony 4.4.
    • It runs in its own process, and first checks that the classes are not loaded yet, so they load after the debug class
      loader is enabled.
    • It collects notices before anything loads: on PHP 8.4, compiling Symfony 4.4's own files raises deprecation notices,
      and one that reached the child process's output would fail the test whatever it said.
    • Its error handler takes only deprecations (E_DEPRECATED | E_USER_DEPRECATED), so a warning or a notice raised
      while the classes load goes to PHP's standard handler, and PHPUnit fails the test on it.
  • Workflow: Symfony 7.* on PHP 8.2–8.4. The advisories step went in through EE-282: Let the Symfony 3 and 4 jobs install with current Composer #11.
  • Workflow, after the second review and your notes on EE-281: Add Symfony 7.4 and psr/log 3 support lib-pagination#18 and EE-280: Add Symfony 7.4 support lib-serializer#14: every job runs with lowest and highest dependencies, and only combinations Composer cannot install are excluded: 70 jobs, 35 of them lowest. The lowest jobs that failed on PHP 8 now pass. PHPUnit 9.6 no longer turns the old releases' PHP 8 deprecation notices into errors, and lib-normalization 1.3.1 fixes the one real failure. PHP 7.0 with Symfony 4, 5 and 6 is now excluded: those versions need PHP 7.1.3 or later, and because Flex does not install on PHP 7.0, those jobs were running Symfony 3.4.
  • A 1.4.0 CHANGELOG.md entry.

Backward compatibility

  • Breaking only for subclasses:
    • an untyped getConfigTreeBuilder() override needs : TreeBuilder. It fails on Symfony 7 anyway;
    • a subclass of the extension loses the HttpKernel class-cache methods (deprecated in Symfony 7.1): calls fail and an override of the getter is ignored.
  • Neither class is in the public API the README defines, so this is a minor release, like Symfony 5.4 and 6 support (1.2.0, 1.3.0). I found no such subclass in 115 of our applications or in the public dependents.
  • On Symfony 3.4, the three new requirements start at 3.4.26.
  • paysera/lib-normalization 1.3.1 is the new minimum. An application locked below it updates the two together (composer update paysera/lib-normalization-bundle -w). 26 of our 56 applications that use the bundle are below 1.3.1.
  • The bundle no longer requires symfony/framework-bundle. Every Symfony application requires it itself; none of our 56 gets it only through this bundle.
  • The two XML files are removed. They are not part of the public API the README defines, and none of our 115 applications imports them.
  • A backward-compatibility check from 1.3.1 reports two changes in the bundle's own classes: the return type and the parent class above. The other items it lists are Symfony's own classes, which differ between the Symfony versions each side installs.

Test plan

  • The suite (17 tests) passes on Symfony 3.4 to 7.4. Coverage is 88 of 89 lines (52 of 53 before); the uncovered line is Configuration's fallback for Symfony below 4.2. Against 1.3.1, the debug-class-loader test fails on 5.4, 6.0 and 6.4, and 7.4 is a fatal error. Against the XML version, the no-deprecation test fails on 7.4 with two notices, one per file.
  • All 70 workflow jobs pass, run locally with the workflow's own commands: 17 tests each, one skipped where Symfony's error handler is absent. The lowest jobs install every floor the package declares, including Symfony 7.4.0 and lib-normalization 1.3.1.
  • The debug-class-loader test fails when a warning or a notice is raised while the bundle's classes load, on PHPUnit 6.0, 6.5 and 9.6 (PHP 7.1, 7.2 and 8.4). Before your review it passed.
  • The new test fails when the extension drops a lazy flag, a tag or an alias, passes a service id where a reference belongs, or registers the date_time normalizers without their configuration (on Symfony 7.4 and 3.4).
  • A separate Symfony 4.4, 6.4 and 7.4 application uses every tag and the core services. It passes 8 tests on each, also with this round's changes. With 1.3.1, 6.4 fails on the two notices and the 7.4 install is refused.
  • A Symfony 6.4 production application's full test suite gives the same result before and after: 2,195 tests, the same set, only this package moved in the lock.
  • CI on this pull request: 70 of 70 jobs pass with 17 tests each (https://github.com/paysera/lib-normalization-bundle/actions/runs/36686126909). The Symfony 7 lowest jobs install framework-bundle 7.4.0.

🤖 Generated with Claude Code

Symfony 7 declares getConfigTreeBuilder(): TreeBuilder on ConfigurationInterface, so the untyped
Configuration::getConfigTreeBuilder() is a fatal error there. A class return type parses on PHP 7.0,
so the PHP requirement stays as it is.

symfony/framework-bundle allows ^7.4. The bundle's code uses symfony/config, symfony/dependency-injection
and symfony/http-kernel without requiring them; they are now required at the framework-bundle constraint,
because framework-bundle 7.4 accepts the Symfony 8 versions of all three. The test-only symfony/yaml
follows the same lines.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
With Symfony's DebugClassLoader on (an application's dev environment, symfony/phpunit-bridge), Symfony 6.4
and 7.4 report two notices against this bundle: ExtensionInterface::load() "might add void as a native
return type" in PayseraNormalizationExtension, and, on 7.4, that the HttpKernel Extension it extends is
internal since Symfony 7.1 (deprecated in 8.1). The extension now extends
Symfony\Component\DependencyInjection\Extension\Extension, the parent of the HttpKernel class, which it
already used for everything (the HttpKernel class adds only the annotated-classes cache this extension
never fills), and load() documents its void return the way 1.3.1 did for the bundle's build(). The native
void type waits for Symfony 8, where it would also require PHP 7.1.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… install again

The matrix gains Symfony 7.* on PHP 8.2 to 8.4; Symfony 7.4 needs PHP 8.2, so the older PHP lines exclude it.

Composer 2.10, which setup-php installs for PHP 7.2 and later, refuses packages with security advisories,
and every symfony/cache release of Symfony 3.4 and 4.4 has one, so those jobs failed at install. The step
paysera/lib-api-bundle#18 added turns the blocking off for this test install only; PHP 7.0 and 7.1 get
Composer 2.2, which neither blocks advisories nor knows the setting, so the step skips them.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
On Symfony 6.4 one of the two notices is about getConfigTreeBuilder(), which the return type removes, not the
extension change; the entry now credits both.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… the bundle

The suite did not assert what the previous commits are for: with the HttpKernel parent put back or the
@return void docblock removed, all 13 tests stayed green. The new test loads the bundle's three classes in a
separate process with Symfony's DebugClassLoader enabled and fails on any deprecation notice that names them.
Against the pre-change code it fails on Symfony 6.4 (load() "might add void", getConfigTreeBuilder() "might add
TreeBuilder") and on 7.4 (the internal HttpKernel parent, load()); it is skipped below Symfony 4.4, where
symfony/error-handler does not exist.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… step is needed

- CHANGELOG: the breaking note now covers both classes (the extension's subclasses lose the HttpKernel
  class-cache methods too) and says why this is still a minor: neither class is in the public API the README
  defines. It also says that Symfony 7.4's notice about XML service definitions remains.
- load() documents its $configs element type, as ExtensionInterface does.
- The workflow comment names every job the advisories step is for, including Symfony 6.0 on PHP 8.0 and
  PHPUnit 6.5 on PHP 7.2, and the Composer version that started blocking (2.9).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
On PHP 8.4, compiling Symfony 4.4's DebugClassLoader raises an "implicitly nullable parameter" notice. The
test loaded that class before installing its error handler, so the notice reached the separate process's
output and PHPUnit failed the test (workflow job PHP 8.4 x Symfony 4.*). The handler now goes in first.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…s, wording made exact

- The deprecation test now asserts that the bundle's classes are not loaded before the debug class loader is
  enabled and that they load inside it. Without that, dropping `@preserveGlobalState disabled` let the child
  process re-include classes the parent had loaded, and the test passed with the fix reverted.
- The test's closures are typed.
- load()'s `$configs` may hold null entries: on Symfony 3.4 and 4.4 a `paysera_normalization: ~` block arrives
  as null.
- The workflow comment no longer lists four packages where more are affected, and says the setting it writes
  exists from Composer 2.10.
- CHANGELOG: the notices removed are reported from Symfony 5.4 on, not only on 6.4 and 7.4; the class-cache
  methods are named per Symfony line, and an override of their getter is ignored as well.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@vinayak-iyer-paysera

Copy link
Copy Markdown
Contributor Author

Following your note on lib-dependency-injection#8, every job here also runs with lowest dependencies where that combination can run (dabe1ef): 55 jobs, the 38 highest plus 17 lowest. I ran all 38 lowest combinations locally with the workflow's own commands. 17 pass. The other 21 fail inside vendor/, because the oldest releases the constraints allow don't run on PHP 8, so I excluded them with 6 entries:

  • PHP 8.0–8.4 with Symfony 3 or 4: Symfony 3.4.26 and 4.4.9 call libxml_disable_entity_loader(), which PHP 8 deprecates.
  • PHP 8.1–8.4: paysera/lib-normalization 1.2.0 installs paysera/lib-object-wrapper 0.3.0, whose ArrayAccess methods have no return types. That is a fatal error from PHP 8.1.
  • PHP 8.4: PHPUnit 9.3.0 can't run there (E_STRICT is deprecated).

Unlike #8, this workflow turns Composer's advisory blocking off, so its lowest jobs install the real floors. One consequence: the Symfony 7.4.0 floor isn't tested at lowest. Raising the floors (paysera/lib-normalization ^1.2.1, a newer PHPUnit) would let lowest run on PHP 8.1 and later, but it changes the package's requirements, so I left it out of this pull request. Tell me if you want it here.

The matrix gains dependency-versions lowest next to highest, and the single
PHP 7.0 lowest entry became one of those jobs. Six exclude entries take out
the 21 lowest combinations whose oldest allowed dependencies cannot run on
PHP 8: Symfony 3.4.26 and 4.4.9 on PHP 8.0 and later (libxml deprecation),
lib-object-wrapper 0.3.0 on PHP 8.1 and later (ArrayAccess return types), and
PHPUnit 9.3.0 on PHP 8.4. The 17 lowest jobs that remain pass.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@vinayak-iyer-paysera
vinayak-iyer-paysera marked this pull request as ready for review September 29, 2026 07:45
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@vinayak-iyer-paysera

Copy link
Copy Markdown
Contributor Author

Before your review I applied here the points you raised on lib-lock-bundle#13, lib-pagination#18, lib-serializer#14 and lib-dependency-injection#8 (b6f8902, aabd352):

  • The services are defined in the extension instead of the two XML files, so Symfony 7.4 no longer reports "XML configuration format is deprecated". The definitions compare identical to the XML ones on Symfony 3.4, 4.4, 6.4 and 7.4, and a new test pins them.
  • symfony/framework-bundle moves to require-dev. Nothing in src/ uses it; only the test application does.
  • The lowest exclusions are gone: every lowest job now runs, 35 of 70. This answers the question in my earlier comment. Most of those failures were PHP 8 deprecation notices that PHPUnit 9.3 turned into errors, so dev PHPUnit now starts at 9.6.33. One was a real bug: lib-normalization before 1.3.1 fails on PHP 8.2 in DateTimeNormalizer, so the bundle now requires ^1.3.1.
  • PHP 7.0 with Symfony 4, 5 and 6 is excluded. Composer cannot install those versions on PHP 7.0, and because Flex does not install there, those jobs were running Symfony 3.4.

CI: 70 of 70 jobs pass, 17 tests each (https://github.com/paysera/lib-normalization-bundle/actions/runs/36607653494). The description is updated with the details.

@mSprunskas mSprunskas left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The deprecation test hides warnings and notices while the bundle classes load
  
  - Location: tests/Functional/FunctionalDeprecationNoticesTest.php:23-29
  - Problem: The error handler records only E_USER_DEPRECATED and E_DEPRECATED. It returns true for all error types. It has no $error_levels mask, so it also catches E_WARNING, E_NOTICE and E_USER_WARNING, and then discards
    them.
  - Impact: phpunit.xml.dist sets convertWarningsToExceptions="true" and convertNoticesToExceptions="true". While DebugClassLoader loads the three bundle classes, this handler replaces the PHPUnit handler. If a class load
    causes a warning or a notice, the test passes and gives no signal. The effect is limited to this test.
  - Fix: Return false for error types that are not deprecations, so that the error goes to the standard handler. Another option is to give set_error_handler() the mask E_DEPRECATED | E_USER_DEPRECATED.

The test's error handler took every error type and returned true, so a warning or a notice raised while the
bundle's classes load was discarded and the test passed. It now handles E_USER_DEPRECATED and E_DEPRECATED only.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@vinayak-iyer-paysera

Copy link
Copy Markdown
Contributor Author

Fixed in 1df96f0: the handler now takes only E_DEPRECATED | E_USER_DEPRECATED, so warnings and notices go to PHP's standard handler.

I checked it by raising an E_USER_WARNING and an E_USER_NOTICE while Configuration loads. Before the change the test passed with both; now it fails with both, on PHPUnit 6.0, 6.5 and 9.6 (PHP 7.1, 7.2 and 8.4). This holds whatever php.ini says: the test runs in its own process, PHPUnit sends that process's error output to stderr, and it fails the test on any stderr output.

CI: 70 of 70 jobs pass, 17 tests each (https://github.com/paysera/lib-normalization-bundle/actions/runs/36686126909).

@mSprunskas
mSprunskas merged commit cff0ce7 into paysera:master Sep 30, 2026
70 checks passed
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.

2 participants