Bugfix Issue 481 PHP84 Symfony Translation Implicit Nullable - sebiboga/testlink-upgraded GitHub Wiki
Issue 481 — PHP 8.4+ E_DEPRECATED "Implicitly marking parameter $domain as nullable" in symfony/translation on every page load
Issue: #481
Branch: fix/issue-481
Status: FIXED & VERIFIED (2026-08-24)
Symptom
On PHP ≥ 8.4 every page load prints:
Deprecated: Symfony\Component\Translation\t(): Implicitly marking parameter $domain as nullable is deprecated, the explicit nullable type must be used instead in vendor/symfony/translation/Resources/functions.php on line 18
The reporter's quickfix (string $domain = null → ?string $domain = null) worked
but was expected to be reverted by any later composer install.
Root cause chain
vendor/symfony/translation/Resources/functions.php:18— helpert()declaredfunction t(string $message, array $parameters = [], string $domain = null). A non-nullable type with an implicit-null default is implicitly nullable; PHP ≥ 8.4 emits the E_DEPRECATED above when compiling the file.vendor/composer/autoload_files.php:15(+autoload_static.php:16) — Composer's files autoloader includes that functions file unconditionally whenevervendor/autoload.phpis required. Every entry point (index.php, alllib/**.php, XML-RPC/REST APIs, CLI) requires autoload first → the warning prints on every request, not only when translation is used.- Installed
symfony/translationis v6.4.0, predating Symfony's Nov-2024 explicit-nullable backports (~v6.4.8+). It is a transitive dependency — no direct constraint incomposer.json.
Why this fix approach
| Option | Verdict |
|---|---|
Full composer update of symfony/* |
rejected — needs network, risks unrelated dependency drift, not minimal |
cweagans/composer-patches plugin |
rejected — new dependency + network for a one-line hunk |
Suppress deprecations via error_reporting |
rejected — hides real problems |
| Direct committed-vendor patch + composer guard hook | chosen — vendor/ IS committed to this repo (fresh clones get it), and the guard keeps it alive across installs |
Fix
Code review caught that the direct callee of the patched t() —
TranslatableMessage::__construct() — carried the exact same implicit-nullable
signature, and the scanner then found a third one on the actual localization
path (TranslatableMessage::trans(), $locale). Patching only functions.php
would have fixed the autoload-time warning but left every page that uses
translation deprecating on PHP ≥ 8.4. Final patch set (all explicit nullable):
-
vendor/symfony/translation/Resources/functions.php:18—t()$domain -
vendor/symfony/translation/TranslatableMessage.php:26—__construct()$domain -
vendor/symfony/translation/TranslatableMessage.php:53—trans()$locale -
Composer-overwrite guard — new idempotent script
tools/patch_vendor_php84.php, wired intocomposer.jsonscripts.post-install-cmd+post-update-cmd, re-applying all three signatures after anycomposer install/update; fail-safe: missing file → skip exit 0; upstream signature changed → skip exit 0; already patched → no-op exit 0; write failure → exit 1 (loud, per code-review finding). Review hardening applied: strictstrpos !== falsechecks, checkedfile_put_contents, per-entry parameter labels in output.
Verification (reproduction & regression matrix)
Because the deprecation fires only on PHP ≥ 8.4 and the dev machine runs 8.3.33,
reproduction used a static scanner (tmp/repro-issue-481.php) that walks tokens
and applies exactly the engine's condition, emitting the byte-identical message:
$ php tmp/repro-issue-481.php vendor/symfony/translation/Resources/functions.php
Deprecated: Implicitly marking parameter $domain as nullable is deprecated, the explicit nullable type must be used instead in vendor/symfony/translation/Resources/functions.php on line 18
Post-fix matrix (suite 481 in tmp/TLU_Test_Cases.md, 6/6 PASS):
| # | Case | Result |
|---|---|---|
| R1 | scanner on all 3 patched signatures | 0 findings, exit 0 |
| R2 | php -l | clean |
| R3 | behavioral: autoload + call t() |
TranslatableMessage returned, domain kept |
| R4 | overwrite simulation: revert signature → guard re-patch → idempotent second run | PASS |
| R5 | app boot: HTTP 200 /login.php + browser login admin/admin through the index shell (navBar / asideMenu / mainframe) | all render, zero Deprecated text |
| R6 | Event Viewer delta | only audit row log_level=16 (login_succeeded); no new Error/Warning |
Blast radius note (out of scope, documented)
Scanner measured 51 further implicit-nullables across symfony translation +
translation-contracts (103 files), e.g. TranslatorBagInterface.php:28. Those
surface lazily (only when their classes load) and would be fixed wholesale by a
future dependency upgrade to ~v6.4.8+. Other vendors (ldaprecord, …) carry more.
This issue's scope was the every-page-load warning; that is fixed and verified.
Commits
d0b5d13ae— fix(vendor): symfony/translation t() implicit nullable -> explicit ?string (PHP 8.4 deprecation)1a4552569— fix(tools): composer post-install/update guard re-applies PHP 8.4 symfony/translation patch- review round: TranslatableMessage __construct/trans signatures added to vendor patch + guard; guard hardened (strict strpos, write-failure exit 1)