From 1b98f7a86d7556e6eeef21f077cf72a947efdc47 Mon Sep 17 00:00:00 2001 From: Neil Daniels Date: Mon, 18 May 2026 20:17:59 -0700 Subject: [PATCH 1/5] Support symfony/options-resolver 8 (fixes #290) 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) --- composer.json | 2 +- lib/Tmdb/Client.php | 14 +++++++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/composer.json b/composer.json index 8fedf3fa..9f91ec5f 100644 --- a/composer.json +++ b/composer.json @@ -33,7 +33,7 @@ "require": { "php": "^7.3 || ^8.0", "ext-json": "*", - "symfony/options-resolver": "^4.4 || ^5 || ^6 || ^7", + "symfony/options-resolver": "^4.4 || ^5 || ^6 || ^7 || ^8", "psr/cache": "^1 || ^2 || ^3", "psr/simple-cache": "^1 || ^2 || ^3", "psr/event-dispatcher": "^1", diff --git a/lib/Tmdb/Client.php b/lib/Tmdb/Client.php index 36468168..54092e18 100644 --- a/lib/Tmdb/Client.php +++ b/lib/Tmdb/Client.php @@ -95,7 +95,7 @@ protected function configureOptions(array $options) 'base_uri' => null, 'api_token' => null, 'guest_session_token' => null, - 'http' => function (OptionsResolver $optionsResolver) { + 'http' => $http = function (OptionsResolver $optionsResolver) { $optionsResolver->setDefaults( [ 'client' => null, @@ -120,7 +120,7 @@ protected function configureOptions(array $options) $optionsResolver->setAllowedTypes('stream_factory', [StreamFactoryInterface::class, 'null']); $optionsResolver->setAllowedTypes('uri_factory', [UriFactoryInterface::class, 'null']); }, - 'hydration' => function (OptionsResolver $optionsResolver) { + 'hydration' => $hydration = function (OptionsResolver $optionsResolver) { $optionsResolver->setDefaults( [ 'event_listener_handles_hydration' => false, @@ -131,7 +131,7 @@ protected function configureOptions(array $options) // @todo 4.1 validate these are actually models $optionsResolver->setAllowedTypes('only_for_specified_models', ['array']); }, - 'event_dispatcher' => function (OptionsResolver $optionsResolver) { + 'event_dispatcher' => $eventDispatcher = function (OptionsResolver $optionsResolver) { $optionsResolver->setDefaults( [ 'adapter' => null @@ -144,6 +144,14 @@ protected function configureOptions(array $options) ] ); + // symfony/options-resolver 8.0 removed nested-options-via-Closure passed to setDefaults(); + // setOptions() is the replacement, available from 7.3. + if (method_exists($resolver, 'setOptions')) { + $resolver->setOptions('http', $http); + $resolver->setOptions('hydration', $hydration); + $resolver->setOptions('event_dispatcher', $eventDispatcher); + } + $resolver->setRequired( [ 'host', From 4b4d5edac22b46de4843a41acb673b014ef1a2f8 Mon Sep 17 00:00:00 2001 From: Neil Daniels Date: Mon, 18 May 2026 20:38:10 -0700 Subject: [PATCH 2/5] Silence PHPStan tautology on setOptions() guard 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) --- lib/Tmdb/Client.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/Tmdb/Client.php b/lib/Tmdb/Client.php index 54092e18..0f65fcc6 100644 --- a/lib/Tmdb/Client.php +++ b/lib/Tmdb/Client.php @@ -145,7 +145,9 @@ protected function configureOptions(array $options) ); // symfony/options-resolver 8.0 removed nested-options-via-Closure passed to setDefaults(); - // setOptions() is the replacement, available from 7.3. + // setOptions() is the replacement, available from 7.3. The runtime guard supports older + // versions still allowed by composer.json; PHPStan resolves against the installed version. + // @phpstan-ignore function.alreadyNarrowedType if (method_exists($resolver, 'setOptions')) { $resolver->setOptions('http', $http); $resolver->setOptions('hydration', $hydration); From 0e66d7d72bd6ccee73cca5c30177e7bc6c4bb565 Mon Sep 17 00:00:00 2001 From: Neil Daniels Date: Mon, 18 May 2026 20:44:26 -0700 Subject: [PATCH 3/5] Refresh PHPStan baseline for php-http/cache-plugin update 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) --- phpstan-baseline.neon | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 50dbd489..7ed649bb 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -19,11 +19,17 @@ parameters: path: lib/Tmdb/Event/Listener/Logger/LogHttpMessageListener.php - - rawMessage: 'Parameter #3 $first of method Http\Client\Common\Plugin\CachePlugin::handleRequest() expects callable(Psr\Http\Message\RequestInterface): Http\Promise\Promise, Closure(): void given.' + rawMessage: 'Parameter #3 $first of method Http\Client\Common\Plugin\AbstractCachePlugin::handleRequest() expects callable(Psr\Http\Message\RequestInterface): Http\Promise\Promise, Closure(): void given.' identifier: argument.type count: 1 path: lib/Tmdb/Event/Listener/Psr6CachedRequestListener.php + - + rawMessage: 'Call to method handleRequest() of internal class Http\Client\Common\Plugin\AbstractCachePlugin from outside its root namespace Http.' + identifier: method.internalClass + count: 1 + path: lib/Tmdb/Event/Listener/Psr6CachedRequestListener.php + - rawMessage: 'Property Tmdb\Event\Listener\Psr6CachedRequestListener::$options is never read, only written.' identifier: property.onlyWritten From 4802c970f54dfc03f8af8aa97ca165907c1954e2 Mon Sep 17 00:00:00 2001 From: Neil Daniels Date: Mon, 18 May 2026 20:51:27 -0700 Subject: [PATCH 4/5] Cover both PHPStan identifiers on method_exists guard 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) --- lib/Tmdb/Client.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Tmdb/Client.php b/lib/Tmdb/Client.php index 0f65fcc6..ed9af57a 100644 --- a/lib/Tmdb/Client.php +++ b/lib/Tmdb/Client.php @@ -147,7 +147,7 @@ protected function configureOptions(array $options) // symfony/options-resolver 8.0 removed nested-options-via-Closure passed to setDefaults(); // setOptions() is the replacement, available from 7.3. The runtime guard supports older // versions still allowed by composer.json; PHPStan resolves against the installed version. - // @phpstan-ignore function.alreadyNarrowedType + // @phpstan-ignore function.alreadyNarrowedType, function.impossibleType if (method_exists($resolver, 'setOptions')) { $resolver->setOptions('http', $http); $resolver->setOptions('hydration', $hydration); From 5396b858366cc7f5de94128138eddb953079b9b3 Mon Sep 17 00:00:00 2001 From: Neil Daniels Date: Mon, 18 May 2026 20:57:35 -0700 Subject: [PATCH 5/5] Use baseline reportUnmatched for both method_exists outcomes 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) --- lib/Tmdb/Client.php | 5 +++-- phpstan-baseline.neon | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/lib/Tmdb/Client.php b/lib/Tmdb/Client.php index ed9af57a..3e231265 100644 --- a/lib/Tmdb/Client.php +++ b/lib/Tmdb/Client.php @@ -146,8 +146,9 @@ protected function configureOptions(array $options) // symfony/options-resolver 8.0 removed nested-options-via-Closure passed to setDefaults(); // setOptions() is the replacement, available from 7.3. The runtime guard supports older - // versions still allowed by composer.json; PHPStan resolves against the installed version. - // @phpstan-ignore function.alreadyNarrowedType, function.impossibleType + // versions still allowed by composer.json; PHPStan tautology errors are silenced in + // phpstan-baseline.neon for both true and false outcomes (analysis runs against a single + // installed OptionsResolver version, but the guard is genuine across the supported range). if (method_exists($resolver, 'setOptions')) { $resolver->setOptions('http', $http); $resolver->setOptions('hydration', $hydration); diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 7ed649bb..5318f2fd 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -18,6 +18,22 @@ parameters: count: 1 path: lib/Tmdb/Event/Listener/Logger/LogHttpMessageListener.php + # The method_exists(setOptions) guard supports symfony/options-resolver versions both + # with and without the method. PHPStan resolves against the installed version on each + # CI job, so exactly one of these two outcomes fires per job; reportUnmatched silences + # the other. + - + rawMessage: "Call to function method_exists() with Symfony\\Component\\OptionsResolver\\OptionsResolver and 'setOptions' will always evaluate to true." + identifier: function.alreadyNarrowedType + reportUnmatched: false + path: lib/Tmdb/Client.php + + - + rawMessage: "Call to function method_exists() with Symfony\\Component\\OptionsResolver\\OptionsResolver and 'setOptions' will always evaluate to false." + identifier: function.impossibleType + reportUnmatched: false + path: lib/Tmdb/Client.php + - rawMessage: 'Parameter #3 $first of method Http\Client\Common\Plugin\AbstractCachePlugin::handleRequest() expects callable(Psr\Http\Message\RequestInterface): Http\Promise\Promise, Closure(): void given.' identifier: argument.type