fix: reconcile the config mapping callback type between BuilderStack and BuilderConfigurator - #292
Open
marilenaRM wants to merge 1 commit into
Conversation
…and BuilderConfigurator BuilderStack declared the callback as `array<array-key, string>|null` while BuilderConfigurator declared it as `(\Closure(mixed): mixed)|null`. Both describe the same array, dumped into the container by SensiolabsGotenbergExtension, so one of them had to be wrong — the Closure one was: BuilderStack stores array callables (`[$enumClass, 'from']`, `[Unit::class, 'parse']`), and the container cannot dump a closure at all (PhpDumper throws on any object argument). PHPStan never noticed because the two types only met at compile time, outside analysed code. Both sides now share a single `BuilderConfigurationMapping` phpstan-type declared on BuilderConfigurator, honest about what is stored: a static method target on a backed enum. The invocation destructures it so PHPStan can follow the static call instead of having to prove an array literal is callable — behaviour is identical, PHP resolved `[$class, $method]($value)` the same way. BuilderConfiguratorTest builds the mapping with BuilderStack and feeds it straight into BuilderConfigurator, so the two shapes now meet in analysed code and cannot silently drift apart again. Assisted-by: Claude Code:Opus-5
marilenaRM
force-pushed
the
marilenMR/fix-config-mapping-callback-type
branch
from
August 28, 2026 10:34
5d5f23a to
68aed51
Compare
Jean-Beru
approved these changes
Aug 31, 2026
Jean-Beru
left a comment
Contributor
There was a problem hiding this comment.
Good catch! Thank you @marilenaRM
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.
Description
BuilderStackandBuilderConfiguratordescribed the same array — the config mapping the extensiondumps into the container — with two incompatible types for its
callbackentry:array<array-key, string>|nullon one side,(\Closure(mixed): mixed)|nullon the other. See #291 for the fullwrite-up.
The
\Closureside was the lieBuilderStackstores array callables ([$enumClass, 'from'],[Unit::class, 'parse']), and aclosure could not reach
BuilderConfiguratoreven if we wanted it to — the mapping goes throughDefinition::replaceArgument()into the compiled container, andPhpDumperrefuses:I checked this before picking a direction, since "build real closures in
BuilderStack" is theobvious first idea and it is a dead end.
Why not just write
callable(mixed): mixedon both sidesPHPStan collapses the two assignments into
array{class-string<BackedEnum>, 'from'|'parse'}andrejects it as a callable, because
parseis not declared on\BackedEnum. I also triedcallable-array, an intersectionarray{class-string<\BackedEnum>, string}&callable(mixed): mixed,per-branch assignment to dodge the variable merge, and a union of two precise tuples. The union one
is the instructive failure: the assignments pass, then PHPStan re-collapses the type at the call site
and reports
Trying to invoke array{class-string<BackedEnum>, 'from'|'parse'} but it might not be a callable. PHPStan will not prove these array literals callable, whatever you declare.What this PR does
Both sides now share a single
@phpstan-type BuilderConfigurationMappingdeclared onBuilderConfiguratorand imported byBuilderStack— same producer/consumer idiom already used forWebhookDefinitioninWebhookConfigurationRegistryInterface. One declaration, so they cannot driftapart again.
The callback is typed
array{class-string<\BackedEnum>, string}|null, which is exactly what ends upin the compiled container:
(read out of the generated
getSensiolabsGotenberg_BuilderConfiguratorService.phpafter booting thetest kernel, to confirm the shape survives dumping.)
The call site destructures instead of invoking, so PHPStan can follow the static call without an
assert(), an inline@varor an ignore:Behaviour is identical — PHP resolved
[$class, $method]($value)to the same static call.I considered replacing the callback with a discriminator enum plus a
matchdispatch. Rejected: itforces
$value(genuinelymixed) to be narrowed at the call site, which means adding validationthat changes behaviour. Not what a type-hint fix should do.
Test
tests/Configurator/BuilderConfiguratorTest.phpbuilds the mapping withBuilderStackand feeds itstraight into
BuilderConfigurator, so the two shapes now meet in analysed code and a regressionbecomes a PHPStan error rather than a silent divergence. It covers both conversions — enum
(
pdf_format,emulated_media_type) andUnit(paper_width,margin_top, including the variadicspread). I mutation-checked it by neutering the callback line: both tests fail with a
TypeError.Checks
Run on
1.x(8ceaa92) with PHP 8.3 / Symfony 7.4:./vendor/bin/phpstan analyse— no errors./vendor/bin/phpunit— 1097 tests, 2221 assertions, OK./vendor/bin/composer-dependency-analyser— no issuesphp-cs-fixer— clean