Skip to content

fix: reconcile the config mapping callback type between BuilderStack and BuilderConfigurator - #292

Open
marilenaRM wants to merge 1 commit into
sensiolabs:1.xfrom
marilenaRM:marilenMR/fix-config-mapping-callback-type
Open

fix: reconcile the config mapping callback type between BuilderStack and BuilderConfigurator#292
marilenaRM wants to merge 1 commit into
sensiolabs:1.xfrom
marilenaRM:marilenMR/fix-config-mapping-callback-type

Conversation

@marilenaRM

@marilenaRM marilenaRM commented Aug 28, 2026

Copy link
Copy Markdown
Contributor
Q A
Gotenberg API version ? 8.x
Bug fix ? yes
New feature ? no
BC break ? no
Issues Fix #291

Description

BuilderStack and BuilderConfigurator described the same array — the config mapping the extension
dumps into the container — with two incompatible types for its callback entry: array<array-key, string>|null on one side, (\Closure(mixed): mixed)|null on the other. See #291 for the full
write-up.

The \Closure side was the lie

BuilderStack stores array callables ([$enumClass, 'from'], [Unit::class, 'parse']), and a
closure could not reach BuilderConfigurator even if we wanted it to — the mapping goes through
Definition::replaceArgument() into the compiled container, and PhpDumper refuses:

RuntimeException: Unable to dump a service container if a parameter is an object or a resource, got "Closure".

I checked this before picking a direction, since "build real closures in BuilderStack" is the
obvious first idea and it is a dead end.

Why not just write callable(mixed): mixed on both sides

PHPStan collapses the two assignments into array{class-string<BackedEnum>, 'from'|'parse'} and
rejects it as a callable, because parse is not declared on \BackedEnum. I also tried
callable-array, an intersection array{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 BuilderConfigurationMapping declared on
BuilderConfigurator and imported by BuilderStack — same producer/consumer idiom already used for
WebhookDefinition in WebhookConfigurationRegistryInterface. One declaration, so they cannot drift
apart again.

The callback is typed array{class-string<\BackedEnum>, string}|null, which is exactly what ends up
in the compiled container:

'pdf_format' => ['method' => 'pdfFormat', 'mustUseVariadic' => false,
                 'callback' => ['Sensiolabs\\GotenbergBundle\\Enumeration\\PdfFormat', 'from']]

(read out of the generated getSensiolabsGotenberg_BuilderConfiguratorService.php after booting the
test 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 @var or an ignore:

[$callbackClass, $callbackMethod] = $configurationMap['callback'];
$value = $callbackClass::$callbackMethod($value);

Behaviour is identical — PHP resolved [$class, $method]($value) to the same static call.

I considered replacing the callback with a discriminator enum plus a match dispatch. Rejected: it
forces $value (genuinely mixed) to be narrowed at the call site, which means adding validation
that changes behaviour. Not what a type-hint fix should do.

Test

tests/Configurator/BuilderConfiguratorTest.php builds the mapping with BuilderStack and feeds it
straight into BuilderConfigurator, so the two shapes now meet in analysed code and a regression
becomes a PHPStan error rather than a silent divergence. It covers both conversions — enum
(pdf_format, emulated_media_type) and Unit (paper_width, margin_top, including the variadic
spread). 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 issues
  • php-cs-fixer — clean

…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
marilenaRM force-pushed the marilenMR/fix-config-mapping-callback-type branch from 5d5f23a to 68aed51 Compare August 28, 2026 10:34

@Jean-Beru Jean-Beru 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.

Good catch! Thank you @marilenaRM

@Jean-Beru Jean-Beru added this to the v1.4 milestone Aug 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] BuilderStack and BuilderConfigurator declare incompatible types for the same config mapping

2 participants