Skip to content

Support symfony/options-resolver 8 (fixes #290)#291

Merged
neildaniels merged 5 commits into
4.1from
fix/options-resolver-8-compat
May 19, 2026
Merged

Support symfony/options-resolver 8 (fixes #290)#291
neildaniels merged 5 commits into
4.1from
fix/options-resolver-8-compat

Conversation

@neildaniels
Copy link
Copy Markdown
Contributor

@neildaniels neildaniels commented May 19, 2026

Summary

Fixes #290. The library crashed on symfony/options-resolver 8 with Cannot use object of type Closure as array in Client.php:216 because v8.0 removed support for defining nested options by passing a Closure to setDefaults() (deprecated in 7.3, removed in 8.0). The closures stayed as raw default values and $options['hydration'] arrived in postResolve() as a Closure.

Changes

Options-resolver fix (composer.json, lib/Tmdb/Client.php):

  • Widen the symfony/options-resolver constraint to ^4.4 || ^5 || ^6 || ^7 || ^8.
  • Bind the three nested resolvers (http, hydration, event_dispatcher) via OptionsResolver::setOptions() when available (introduced in 7.3), falling back to the legacy closure-as-default pattern on older versions. The original setDefaults([...]) block is preserved almost verbatim — each nested closure is captured into a local via 'key' => $var = function (...) and then re-bound afterwards.

PHPStan baseline updates (phpstan-baseline.neon):

  • The method_exists($resolver, 'setOptions') guard is tautological from PHPStan's perspective on a given CI job (always-true when the installed OptionsResolver has the method, always-false otherwise), with different identifiers per outcome. Both outcomes are baselined with reportUnmatched: false so whichever fires per job gets silenced and the other entry sits idle without tripping the unmatched-ignore check.
  • Unrelated drift: a php-http/cache-plugin release between Update composer dependencies #289's last-green CI on 4.1 (2026-01-30) and now moved handleRequest() to the new @internal AbstractCachePlugin parent, invalidating the existing argument.type ignore on Psr6CachedRequestListener.php:106 and adding a new method.internalClass error. The repo has no composer.lock, so every CI run pulls the latest cache-plugin — 4.1 would also fail PHPStan on a fresh re-run today. Baseline refreshed accordingly. Long-term fix is to stop calling the internal parent method, tracked separately.

Why a runtime branch instead of bumping the floor

setOptions() only exists from options-resolver 7.3, which requires PHP ≥ 8.2. Bumping the constraint to ^7.3 || ^8 would implicitly force the library to PHP 8.2+, breaking installs on the lib's currently-declared "php": "^7.3 || ^8.0" floor. The method_exists() guard preserves that floor at the cost of the two-entry baseline silencing.

Coverage

The CI matrix in .github/workflows/continuous-integration.yml runs PHP 7.4–8.5 × {normal, low, dev}. With the constraint widened to ^8, jobs on PHP ≥ 8.4 + normal/dev will now install options-resolver 8 and exercise the setOptions() path — the previous matrix passed only because the old constraint excluded v8 entirely, which is how #290 escaped detection. low jobs pin v4.4 and exercise the legacy fallback. All 59 PR checks green.

Reviewer notes

  • The reporter's other two items in Does not work in PHP 8.4 and symfony/options-resolver 8 #290 are non-issues: symfony/options-resolver is already in require (composer.json:36), and the PHP 8.4 implicit-nullable deprecation in HttpClient::getOptions was fixed in 5a9ea4c.
  • Happy to split the cache-plugin baseline refresh into its own PR if you'd prefer the options-resolver diff isolated — it's bundled here only because CI was unmergeable without it.

Test plan

  • CI green across the PHP 7.4–8.5 × {normal, low, dev} matrix
  • Manual smoke: instantiate Tmdb\Client on PHP 8.4 with symfony/options-resolver:^8 installed
  • Manual smoke: instantiate Tmdb\Client on PHP 7.4 with symfony/options-resolver:4.4 installed (legacy path)

🤖 Generated with Claude Code

neildaniels and others added 5 commits May 18, 2026 20:17
Nested-options-via-Closure passed to setDefaults() was deprecated in
options-resolver 7.3 and removed in 8.0, leaving the closures stored as
raw defaults and causing "Cannot use object of type Closure as array" in
Client::postResolve().

Bind each nested resolver explicitly via setOptions() when available,
falling back to the legacy closure-default pattern on older versions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PHPStan resolves OptionsResolver against the installed version (>= 7.3
in CI), where setOptions() exists, so the runtime method_exists() guard
appears tautological. Ignore the function.alreadyNarrowedType identifier
on that line so the guard remains in place for older OptionsResolver
versions allowed by the composer constraint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A recent php-http/cache-plugin release moved handleRequest() from
CachePlugin to the new @internal AbstractCachePlugin parent. Update the
existing argument.type ignore to the new class name and add a baseline
entry for the resulting method.internalClass error on the same call
site, so CI unblocks. Long-term fix is to stop calling the internal
parent method, tracked separately.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The method_exists() guard evaluates to true on PHP >= 8.2 (installs
options-resolver 7.3+ where setOptions exists) and false on PHP < 8.2
(installs 4.4-7.2 where it doesn't). PHPStan reports different
identifiers per outcome (function.alreadyNarrowedType vs
function.impossibleType). List both in the inline ignore so the
relevant one matches on each PHP version in the CI matrix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PHPStan analyzes against the single installed OptionsResolver version on
each CI job, so the method_exists() guard appears tautological as either
"always true" (>= 7.3 on PHP 8.2+) or "always false" (< 7.3 on PHP 7.4-
8.1) with different identifiers per outcome. Inline @phpstan-ignore
treats every listed identifier as required, so the non-firing one trips
reportUnmatchedIgnoredErrors.

Use the baseline form with reportUnmatched: false on both entries, so
whichever fires gets silenced and the other entry quietly does nothing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@neildaniels neildaniels merged commit b9e47f7 into 4.1 May 19, 2026
59 checks passed
@neildaniels neildaniels deleted the fix/options-resolver-8-compat branch May 19, 2026 04:04
neildaniels added a commit that referenced this pull request May 19, 2026
Allow the dev-side Symfony components to install on v8, matching the
constraint widened for symfony/options-resolver in #291.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

Does not work in PHP 8.4 and symfony/options-resolver 8

1 participant