From 6838fc2f89f3827af7c37b64fedc0b10ca217fac Mon Sep 17 00:00:00 2001 From: Neil Daniels Date: Fri, 30 Jan 2026 12:52:46 -0800 Subject: [PATCH 01/11] Updated dependencies, include newer Symfony versions --- composer.json | 16 ++--- lib/Tmdb/Client.php | 148 ++++++++++++++++++++++++++++++-------------- 2 files changed, 110 insertions(+), 54 deletions(-) diff --git a/composer.json b/composer.json index 79a56e52..25439119 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", + "symfony/options-resolver": "^4.4 || ^5 || ^6 || ^7 || ^8", "psr/cache": "^1 || ^2 || ^3", "psr/simple-cache": "^1 || ^2 || ^3", "psr/event-dispatcher": "^1", @@ -44,22 +44,22 @@ "psr/http-client-implementation": "^1", "psr/http-factory": "^1", "psr/http-factory-implementation": "^1", - "psr/http-message": "^1" + "psr/http-message": "^1 || ^2" }, "require-dev": { "nyholm/psr7": "^1.2", "php-http/mock-client": "^1.2", - "slevomat/coding-standard": "^8.8", - "squizlabs/php_codesniffer": "^3.5.8", - "symfony/cache": "^4.4 || ^5 || ^6", - "symfony/event-dispatcher": "^4.4 || ^5 || ^6", + "slevomat/coding-standard": "^8.27.1", + "squizlabs/php_codesniffer": "^4.0.1", + "symfony/cache": "^4.4 || ^5 || ^6 || ^7 || ^8", + "symfony/event-dispatcher": "^4.4 || ^5 || ^6 || ^7 || ^8", "phpstan/phpstan": "^1.8.1", "phpstan/phpstan-deprecation-rules": "^1.1", "spaze/phpstan-disallowed-calls": "^2.11", - "phpunit/phpunit": "^9.6.3", + "phpunit/phpunit": "^12.5.8", "php-http/guzzle7-adapter": "^1.0", "monolog/monolog": "^2.9.1 || ^3.0", - "php-http/cache-plugin": "^1.7", + "php-http/cache-plugin": "^1.7 || ^2.0", "jeroen/psr-log-test-doubles": "^2.1 || ^3" }, "scripts": { diff --git a/lib/Tmdb/Client.php b/lib/Tmdb/Client.php index 36468168..300560b0 100644 --- a/lib/Tmdb/Client.php +++ b/lib/Tmdb/Client.php @@ -22,6 +22,8 @@ use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\StreamFactoryInterface; use Psr\Http\Message\UriFactoryInterface; +use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; +use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; use Tmdb\HttpClient\HttpClient; use Tmdb\Token\Api\ApiToken; @@ -64,6 +66,11 @@ class Client */ private $options = []; + private static function debugType($value): string + { + return is_object($value) ? get_class($value) : gettype($value); + } + /** * Construct our client * @@ -95,52 +102,25 @@ protected function configureOptions(array $options) 'base_uri' => null, 'api_token' => null, 'guest_session_token' => null, - 'http' => function (OptionsResolver $optionsResolver) { - $optionsResolver->setDefaults( - [ - 'client' => null, - 'request_factory' => null, - 'response_factory' => null, - 'stream_factory' => null, - 'uri_factory' => null, - ] - ); - $optionsResolver->setRequired( - [ - 'client', - 'request_factory', - 'response_factory', - 'stream_factory', - 'uri_factory' - ] - ); - $optionsResolver->setAllowedTypes('client', [ClientInterface::class, 'null']); - $optionsResolver->setAllowedTypes('request_factory', [RequestFactoryInterface::class, 'null']); - $optionsResolver->setAllowedTypes('response_factory', [ResponseFactoryInterface::class, 'null']); - $optionsResolver->setAllowedTypes('stream_factory', [StreamFactoryInterface::class, 'null']); - $optionsResolver->setAllowedTypes('uri_factory', [UriFactoryInterface::class, 'null']); - }, - 'hydration' => function (OptionsResolver $optionsResolver) { - $optionsResolver->setDefaults( - [ - 'event_listener_handles_hydration' => false, - 'only_for_specified_models' => [] - ] - ); - $optionsResolver->setAllowedTypes('event_listener_handles_hydration', ['bool']); - // @todo 4.1 validate these are actually models - $optionsResolver->setAllowedTypes('only_for_specified_models', ['array']); - }, - 'event_dispatcher' => function (OptionsResolver $optionsResolver) { - $optionsResolver->setDefaults( - [ - 'adapter' => null - ] - ); - - $optionsResolver->setRequired(['adapter']); - $optionsResolver->setAllowedTypes('adapter', [EventDispatcherInterface::class]); - } + 'http' => [ + 'client' => null, + 'request_factory' => null, + 'response_factory' => null, + 'stream_factory' => null, + 'uri_factory' => null, + ], + 'hydration' => [ + 'event_listener_handles_hydration' => false, + 'only_for_specified_models' => [], + ], + 'event_dispatcher' => [ + 'adapter' => new class implements EventDispatcherInterface { + public function dispatch(object $event) + { + return $event; + } + }, + ], ] ); @@ -161,6 +141,82 @@ protected function configureOptions(array $options) $resolver->setAllowedTypes('secure', ['bool']); $resolver->setAllowedTypes('http', ['array']); $resolver->setAllowedTypes('event_dispatcher', ['array']); + $resolver->setAllowedTypes('hydration', ['array']); + + $resolver->setNormalizer('http', function (Options $options, $value) { + if (!is_array($value)) { + throw new InvalidOptionsException(sprintf('The option "http" is expected to be of type "array", but is of type "%s".', self::debugType($value))); + } + + $allowedKeys = ['client', 'request_factory', 'response_factory', 'stream_factory', 'uri_factory']; + $unknownKeys = array_diff(array_keys($value), $allowedKeys); + if (!empty($unknownKeys)) { + throw new InvalidOptionsException(sprintf('The option "http" has unknown keys: %s.', implode(', ', $unknownKeys))); + } + + foreach ($allowedKeys as $key) { + if (!array_key_exists($key, $value)) { + throw new InvalidOptionsException(sprintf('The option "http" must define the key "%s".', $key)); + } + } + + if ($value['client'] !== null && !$value['client'] instanceof ClientInterface) { + throw new InvalidOptionsException(sprintf('The option "http[client]" is expected to be an instance of %s or null, but got %s.', ClientInterface::class, self::debugType($value['client']))); + } + if ($value['request_factory'] !== null && !$value['request_factory'] instanceof RequestFactoryInterface) { + throw new InvalidOptionsException(sprintf('The option "http[request_factory]" is expected to be an instance of %s or null, but got %s.', RequestFactoryInterface::class, self::debugType($value['request_factory']))); + } + if ($value['response_factory'] !== null && !$value['response_factory'] instanceof ResponseFactoryInterface) { + throw new InvalidOptionsException(sprintf('The option "http[response_factory]" is expected to be an instance of %s or null, but got %s.', ResponseFactoryInterface::class, self::debugType($value['response_factory']))); + } + if ($value['stream_factory'] !== null && !$value['stream_factory'] instanceof StreamFactoryInterface) { + throw new InvalidOptionsException(sprintf('The option "http[stream_factory]" is expected to be an instance of %s or null, but got %s.', StreamFactoryInterface::class, self::debugType($value['stream_factory']))); + } + if ($value['uri_factory'] !== null && !$value['uri_factory'] instanceof UriFactoryInterface) { + throw new InvalidOptionsException(sprintf('The option "http[uri_factory]" is expected to be an instance of %s or null, but got %s.', UriFactoryInterface::class, self::debugType($value['uri_factory']))); + } + + return $value; + }); + + $resolver->setNormalizer('hydration', function (Options $options, $value) { + if (!is_array($value)) { + throw new InvalidOptionsException(sprintf('The option "hydration" is expected to be of type "array", but is of type "%s".', self::debugType($value))); + } + + $allowedKeys = ['event_listener_handles_hydration', 'only_for_specified_models']; + $unknownKeys = array_diff(array_keys($value), $allowedKeys); + if (!empty($unknownKeys)) { + throw new InvalidOptionsException(sprintf('The option "hydration" has unknown keys: %s.', implode(', ', $unknownKeys))); + } + + if (!array_key_exists('event_listener_handles_hydration', $value) || !is_bool($value['event_listener_handles_hydration'])) { + throw new InvalidOptionsException('The option "hydration[event_listener_handles_hydration]" is expected to be of type "bool".'); + } + if (!array_key_exists('only_for_specified_models', $value) || !is_array($value['only_for_specified_models'])) { + throw new InvalidOptionsException('The option "hydration[only_for_specified_models]" is expected to be of type "array".'); + } + + return $value; + }); + + $resolver->setNormalizer('event_dispatcher', function (Options $options, $value) { + if (!is_array($value)) { + throw new InvalidOptionsException(sprintf('The option "event_dispatcher" is expected to be of type "array", but is of type "%s".', self::debugType($value))); + } + + $allowedKeys = ['adapter']; + $unknownKeys = array_diff(array_keys($value), $allowedKeys); + if (!empty($unknownKeys)) { + throw new InvalidOptionsException(sprintf('The option "event_dispatcher" has unknown keys: %s.', implode(', ', $unknownKeys))); + } + + if (!array_key_exists('adapter', $value) || !$value['adapter'] instanceof EventDispatcherInterface) { + throw new InvalidOptionsException(sprintf('The option "event_dispatcher[adapter]" is expected to be an instance of %s.', EventDispatcherInterface::class)); + } + + return $value; + }); // @todo 4.1 fix smelly stuff $resolver->setAllowedTypes( From f83cc5d4e0a31f6553f637adde1f4313fcf1f577 Mon Sep 17 00:00:00 2001 From: Neil Daniels Date: Fri, 30 Jan 2026 12:57:09 -0800 Subject: [PATCH 02/11] Update PHPStan version --- composer.json | 6 +++--- phpstan.neon.dist | 3 --- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/composer.json b/composer.json index 25439119..84c58bc8 100644 --- a/composer.json +++ b/composer.json @@ -53,9 +53,9 @@ "squizlabs/php_codesniffer": "^4.0.1", "symfony/cache": "^4.4 || ^5 || ^6 || ^7 || ^8", "symfony/event-dispatcher": "^4.4 || ^5 || ^6 || ^7 || ^8", - "phpstan/phpstan": "^1.8.1", - "phpstan/phpstan-deprecation-rules": "^1.1", - "spaze/phpstan-disallowed-calls": "^2.11", + "phpstan/phpstan": "^2.1.38", + "phpstan/phpstan-deprecation-rules": "^2.0.3", + "spaze/phpstan-disallowed-calls": "^4.7.0", "phpunit/phpunit": "^12.5.8", "php-http/guzzle7-adapter": "^1.0", "monolog/monolog": "^2.9.1 || ^3.0", diff --git a/phpstan.neon.dist b/phpstan.neon.dist index bcbfb51d..6ccd72ae 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -9,9 +9,6 @@ includes: parameters: level: 5 - checkAlwaysTrueCheckTypeFunctionCall: true - checkAlwaysTrueInstanceof: true - checkAlwaysTrueStrictComparison: true checkExplicitMixedMissingReturn: true checkFunctionNameCase: true checkInternalClassCaseSensitivity: true From f45c6b780321dbfda9e976d585408d9ef0ac7b4f Mon Sep 17 00:00:00 2001 From: Neil Daniels Date: Fri, 30 Jan 2026 13:02:06 -0800 Subject: [PATCH 03/11] More flexible PHPUnit dependency --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 84c58bc8..bf9dab41 100644 --- a/composer.json +++ b/composer.json @@ -56,7 +56,7 @@ "phpstan/phpstan": "^2.1.38", "phpstan/phpstan-deprecation-rules": "^2.0.3", "spaze/phpstan-disallowed-calls": "^4.7.0", - "phpunit/phpunit": "^12.5.8", + "phpunit/phpunit": "^9 || ^10 || ^ 11 || ^12", "php-http/guzzle7-adapter": "^1.0", "monolog/monolog": "^2.9.1 || ^3.0", "php-http/cache-plugin": "^1.7 || ^2.0", From 7a0294c75606c4c1f79b55c8d4abcf62f56ffe3d Mon Sep 17 00:00:00 2001 From: Neil Daniels Date: Fri, 30 Jan 2026 13:03:08 -0800 Subject: [PATCH 04/11] Composer.json typo fix --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index bf9dab41..1d4a803b 100644 --- a/composer.json +++ b/composer.json @@ -56,7 +56,7 @@ "phpstan/phpstan": "^2.1.38", "phpstan/phpstan-deprecation-rules": "^2.0.3", "spaze/phpstan-disallowed-calls": "^4.7.0", - "phpunit/phpunit": "^9 || ^10 || ^ 11 || ^12", + "phpunit/phpunit": "^9 || ^10 || ^11 || ^12", "php-http/guzzle7-adapter": "^1.0", "monolog/monolog": "^2.9.1 || ^3.0", "php-http/cache-plugin": "^1.7 || ^2.0", From b00f91a049c13dcc138bb953e0dba48bdac2f71f Mon Sep 17 00:00:00 2001 From: Neil Daniels Date: Fri, 30 Jan 2026 13:16:31 -0800 Subject: [PATCH 05/11] Use PHPUnit 11 because it still supports test annotations --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 1d4a803b..d13803a8 100644 --- a/composer.json +++ b/composer.json @@ -56,7 +56,7 @@ "phpstan/phpstan": "^2.1.38", "phpstan/phpstan-deprecation-rules": "^2.0.3", "spaze/phpstan-disallowed-calls": "^4.7.0", - "phpunit/phpunit": "^9 || ^10 || ^11 || ^12", + "phpunit/phpunit": "^9 || ^10 || ^11", "php-http/guzzle7-adapter": "^1.0", "monolog/monolog": "^2.9.1 || ^3.0", "php-http/cache-plugin": "^1.7 || ^2.0", From a0a9635aec8c211821b3e364526b3629212dda1e Mon Sep 17 00:00:00 2001 From: Neil Daniels Date: Fri, 30 Jan 2026 13:40:33 -0800 Subject: [PATCH 06/11] Fixes for dependency updates --- composer.json | 2 +- lib/Tmdb/Api/AbstractApi.php | 7 +- lib/Tmdb/Client.php | 148 ++++++++++--------------------- test/Tmdb/Tests/Api/TestCase.php | 2 +- test/Tmdb/Tests/TestCase.php | 4 +- 5 files changed, 57 insertions(+), 106 deletions(-) diff --git a/composer.json b/composer.json index d13803a8..634cdc8f 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 || ^8", + "symfony/options-resolver": "^4.4 || ^5 || ^6 || ^7", "psr/cache": "^1 || ^2 || ^3", "psr/simple-cache": "^1 || ^2 || ^3", "psr/event-dispatcher": "^1", diff --git a/lib/Tmdb/Api/AbstractApi.php b/lib/Tmdb/Api/AbstractApi.php index aba79069..0b195430 100644 --- a/lib/Tmdb/Api/AbstractApi.php +++ b/lib/Tmdb/Api/AbstractApi.php @@ -196,7 +196,12 @@ private function decodeResponse(ResponseInterface $response) { try { if ($response->getBody() instanceof StreamInterface) { - return json_decode((string)$response->getBody(), true, 512, JSON_THROW_ON_ERROR); + $body = (string)$response->getBody(); + if (trim($body) === '') { + return []; + } + + return json_decode($body, true, 512, JSON_THROW_ON_ERROR); } return []; diff --git a/lib/Tmdb/Client.php b/lib/Tmdb/Client.php index 300560b0..36468168 100644 --- a/lib/Tmdb/Client.php +++ b/lib/Tmdb/Client.php @@ -22,8 +22,6 @@ use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\StreamFactoryInterface; use Psr\Http\Message\UriFactoryInterface; -use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; -use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; use Tmdb\HttpClient\HttpClient; use Tmdb\Token\Api\ApiToken; @@ -66,11 +64,6 @@ class Client */ private $options = []; - private static function debugType($value): string - { - return is_object($value) ? get_class($value) : gettype($value); - } - /** * Construct our client * @@ -102,25 +95,52 @@ protected function configureOptions(array $options) 'base_uri' => null, 'api_token' => null, 'guest_session_token' => null, - 'http' => [ - 'client' => null, - 'request_factory' => null, - 'response_factory' => null, - 'stream_factory' => null, - 'uri_factory' => null, - ], - 'hydration' => [ - 'event_listener_handles_hydration' => false, - 'only_for_specified_models' => [], - ], - 'event_dispatcher' => [ - 'adapter' => new class implements EventDispatcherInterface { - public function dispatch(object $event) - { - return $event; - } - }, - ], + 'http' => function (OptionsResolver $optionsResolver) { + $optionsResolver->setDefaults( + [ + 'client' => null, + 'request_factory' => null, + 'response_factory' => null, + 'stream_factory' => null, + 'uri_factory' => null, + ] + ); + $optionsResolver->setRequired( + [ + 'client', + 'request_factory', + 'response_factory', + 'stream_factory', + 'uri_factory' + ] + ); + $optionsResolver->setAllowedTypes('client', [ClientInterface::class, 'null']); + $optionsResolver->setAllowedTypes('request_factory', [RequestFactoryInterface::class, 'null']); + $optionsResolver->setAllowedTypes('response_factory', [ResponseFactoryInterface::class, 'null']); + $optionsResolver->setAllowedTypes('stream_factory', [StreamFactoryInterface::class, 'null']); + $optionsResolver->setAllowedTypes('uri_factory', [UriFactoryInterface::class, 'null']); + }, + 'hydration' => function (OptionsResolver $optionsResolver) { + $optionsResolver->setDefaults( + [ + 'event_listener_handles_hydration' => false, + 'only_for_specified_models' => [] + ] + ); + $optionsResolver->setAllowedTypes('event_listener_handles_hydration', ['bool']); + // @todo 4.1 validate these are actually models + $optionsResolver->setAllowedTypes('only_for_specified_models', ['array']); + }, + 'event_dispatcher' => function (OptionsResolver $optionsResolver) { + $optionsResolver->setDefaults( + [ + 'adapter' => null + ] + ); + + $optionsResolver->setRequired(['adapter']); + $optionsResolver->setAllowedTypes('adapter', [EventDispatcherInterface::class]); + } ] ); @@ -141,82 +161,6 @@ public function dispatch(object $event) $resolver->setAllowedTypes('secure', ['bool']); $resolver->setAllowedTypes('http', ['array']); $resolver->setAllowedTypes('event_dispatcher', ['array']); - $resolver->setAllowedTypes('hydration', ['array']); - - $resolver->setNormalizer('http', function (Options $options, $value) { - if (!is_array($value)) { - throw new InvalidOptionsException(sprintf('The option "http" is expected to be of type "array", but is of type "%s".', self::debugType($value))); - } - - $allowedKeys = ['client', 'request_factory', 'response_factory', 'stream_factory', 'uri_factory']; - $unknownKeys = array_diff(array_keys($value), $allowedKeys); - if (!empty($unknownKeys)) { - throw new InvalidOptionsException(sprintf('The option "http" has unknown keys: %s.', implode(', ', $unknownKeys))); - } - - foreach ($allowedKeys as $key) { - if (!array_key_exists($key, $value)) { - throw new InvalidOptionsException(sprintf('The option "http" must define the key "%s".', $key)); - } - } - - if ($value['client'] !== null && !$value['client'] instanceof ClientInterface) { - throw new InvalidOptionsException(sprintf('The option "http[client]" is expected to be an instance of %s or null, but got %s.', ClientInterface::class, self::debugType($value['client']))); - } - if ($value['request_factory'] !== null && !$value['request_factory'] instanceof RequestFactoryInterface) { - throw new InvalidOptionsException(sprintf('The option "http[request_factory]" is expected to be an instance of %s or null, but got %s.', RequestFactoryInterface::class, self::debugType($value['request_factory']))); - } - if ($value['response_factory'] !== null && !$value['response_factory'] instanceof ResponseFactoryInterface) { - throw new InvalidOptionsException(sprintf('The option "http[response_factory]" is expected to be an instance of %s or null, but got %s.', ResponseFactoryInterface::class, self::debugType($value['response_factory']))); - } - if ($value['stream_factory'] !== null && !$value['stream_factory'] instanceof StreamFactoryInterface) { - throw new InvalidOptionsException(sprintf('The option "http[stream_factory]" is expected to be an instance of %s or null, but got %s.', StreamFactoryInterface::class, self::debugType($value['stream_factory']))); - } - if ($value['uri_factory'] !== null && !$value['uri_factory'] instanceof UriFactoryInterface) { - throw new InvalidOptionsException(sprintf('The option "http[uri_factory]" is expected to be an instance of %s or null, but got %s.', UriFactoryInterface::class, self::debugType($value['uri_factory']))); - } - - return $value; - }); - - $resolver->setNormalizer('hydration', function (Options $options, $value) { - if (!is_array($value)) { - throw new InvalidOptionsException(sprintf('The option "hydration" is expected to be of type "array", but is of type "%s".', self::debugType($value))); - } - - $allowedKeys = ['event_listener_handles_hydration', 'only_for_specified_models']; - $unknownKeys = array_diff(array_keys($value), $allowedKeys); - if (!empty($unknownKeys)) { - throw new InvalidOptionsException(sprintf('The option "hydration" has unknown keys: %s.', implode(', ', $unknownKeys))); - } - - if (!array_key_exists('event_listener_handles_hydration', $value) || !is_bool($value['event_listener_handles_hydration'])) { - throw new InvalidOptionsException('The option "hydration[event_listener_handles_hydration]" is expected to be of type "bool".'); - } - if (!array_key_exists('only_for_specified_models', $value) || !is_array($value['only_for_specified_models'])) { - throw new InvalidOptionsException('The option "hydration[only_for_specified_models]" is expected to be of type "array".'); - } - - return $value; - }); - - $resolver->setNormalizer('event_dispatcher', function (Options $options, $value) { - if (!is_array($value)) { - throw new InvalidOptionsException(sprintf('The option "event_dispatcher" is expected to be of type "array", but is of type "%s".', self::debugType($value))); - } - - $allowedKeys = ['adapter']; - $unknownKeys = array_diff(array_keys($value), $allowedKeys); - if (!empty($unknownKeys)) { - throw new InvalidOptionsException(sprintf('The option "event_dispatcher" has unknown keys: %s.', implode(', ', $unknownKeys))); - } - - if (!array_key_exists('adapter', $value) || !$value['adapter'] instanceof EventDispatcherInterface) { - throw new InvalidOptionsException(sprintf('The option "event_dispatcher[adapter]" is expected to be an instance of %s.', EventDispatcherInterface::class)); - } - - return $value; - }); // @todo 4.1 fix smelly stuff $resolver->setAllowedTypes( diff --git a/test/Tmdb/Tests/Api/TestCase.php b/test/Tmdb/Tests/Api/TestCase.php index d7f963b7..862164cc 100644 --- a/test/Tmdb/Tests/Api/TestCase.php +++ b/test/Tmdb/Tests/Api/TestCase.php @@ -67,7 +67,7 @@ protected function getMockedApi(array $methods = [], array $clientMethods = [], } return $this->_api = $this->getMockBuilder($this->getApiClass()) - ->setMethods($methods) + ->onlyMethods($methods) ->setConstructorArgs([$this->_client]) ->getMock(); } diff --git a/test/Tmdb/Tests/TestCase.php b/test/Tmdb/Tests/TestCase.php index cc62ae8a..063259cd 100644 --- a/test/Tmdb/Tests/TestCase.php +++ b/test/Tmdb/Tests/TestCase.php @@ -90,6 +90,8 @@ protected function getClientWithMockedHttpClient(array $options = array()) $options['api_token'] = new ApiToken('abcdef'); $options['http']['client'] = new \Http\Mock\Client(); $response = $this->createMock('Psr\Http\Message\ResponseInterface'); + $streamFactory = Psr17FactoryDiscovery::findStreamFactory(); + $response->method('getBody')->willReturn($streamFactory->createStream('{}')); $options['http']['client']->setDefaultResponse($response); $client = new Client($options); @@ -170,7 +172,7 @@ protected function getMockedHttpClient(array $methods = []) $methods[] = 'send'; } - return $this->getMockBuilder('Guzzle\Http\Client')->setMethods($methods)->getMock(); + return $this->getMockBuilder('Guzzle\Http\Client')->onlyMethods($methods)->getMock(); } /** From 253f09a52f0d8a480fbc094bcaea30fddbacb2ee Mon Sep 17 00:00:00 2001 From: Neil Daniels Date: Fri, 30 Jan 2026 13:44:25 -0800 Subject: [PATCH 07/11] Adopt new Symfony options-resolver behavior --- composer.json | 2 +- lib/Tmdb/Client.php | 98 ++++++++++++++++++++++++--------------------- 2 files changed, 53 insertions(+), 47 deletions(-) diff --git a/composer.json b/composer.json index 634cdc8f..d13803a8 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..9b668ba4 100644 --- a/lib/Tmdb/Client.php +++ b/lib/Tmdb/Client.php @@ -95,55 +95,61 @@ protected function configureOptions(array $options) 'base_uri' => null, 'api_token' => null, 'guest_session_token' => null, - 'http' => function (OptionsResolver $optionsResolver) { - $optionsResolver->setDefaults( - [ - 'client' => null, - 'request_factory' => null, - 'response_factory' => null, - 'stream_factory' => null, - 'uri_factory' => null, - ] - ); - $optionsResolver->setRequired( - [ - 'client', - 'request_factory', - 'response_factory', - 'stream_factory', - 'uri_factory' - ] - ); - $optionsResolver->setAllowedTypes('client', [ClientInterface::class, 'null']); - $optionsResolver->setAllowedTypes('request_factory', [RequestFactoryInterface::class, 'null']); - $optionsResolver->setAllowedTypes('response_factory', [ResponseFactoryInterface::class, 'null']); - $optionsResolver->setAllowedTypes('stream_factory', [StreamFactoryInterface::class, 'null']); - $optionsResolver->setAllowedTypes('uri_factory', [UriFactoryInterface::class, 'null']); - }, - 'hydration' => function (OptionsResolver $optionsResolver) { - $optionsResolver->setDefaults( - [ - 'event_listener_handles_hydration' => false, - 'only_for_specified_models' => [] - ] - ); - $optionsResolver->setAllowedTypes('event_listener_handles_hydration', ['bool']); - // @todo 4.1 validate these are actually models - $optionsResolver->setAllowedTypes('only_for_specified_models', ['array']); - }, - 'event_dispatcher' => function (OptionsResolver $optionsResolver) { - $optionsResolver->setDefaults( - [ - 'adapter' => null - ] - ); - - $optionsResolver->setRequired(['adapter']); - $optionsResolver->setAllowedTypes('adapter', [EventDispatcherInterface::class]); - } + 'http' => [], + 'hydration' => [], + 'event_dispatcher' => [], ] ); + $resolver->setOptions('http', function (OptionsResolver $optionsResolver) { + $optionsResolver->setDefaults( + [ + 'client' => null, + 'request_factory' => null, + 'response_factory' => null, + 'stream_factory' => null, + 'uri_factory' => null, + ] + ); + $optionsResolver->setRequired( + [ + 'client', + 'request_factory', + 'response_factory', + 'stream_factory', + 'uri_factory' + ] + ); + $optionsResolver->setAllowedTypes('client', [ClientInterface::class, 'null']); + $optionsResolver->setAllowedTypes('request_factory', [RequestFactoryInterface::class, 'null']); + $optionsResolver->setAllowedTypes('response_factory', [ResponseFactoryInterface::class, 'null']); + $optionsResolver->setAllowedTypes('stream_factory', [StreamFactoryInterface::class, 'null']); + $optionsResolver->setAllowedTypes('uri_factory', [UriFactoryInterface::class, 'null']); + }); + + $resolver->setOptions('hydration', function (OptionsResolver $optionsResolver) { + $optionsResolver->setDefaults( + [ + 'event_listener_handles_hydration' => false, + 'only_for_specified_models' => [] + ] + ); + $optionsResolver->setAllowedTypes('event_listener_handles_hydration', ['bool']); + // @todo 4.1 validate these are actually models + $optionsResolver->setAllowedTypes('only_for_specified_models', ['array']); + }); + + $resolver->setOptions('event_dispatcher', function (OptionsResolver $optionsResolver) { + $optionsResolver->setDefaults( + [ + 'adapter' => null + ] + ); + + $optionsResolver->setRequired(['adapter']); + $optionsResolver->setAllowedTypes('adapter', [EventDispatcherInterface::class]); + }); + $resolver->setRequired( [ 'host', From 81b83173dda7ecd519b57942c62ed1fdcc5dabe9 Mon Sep 17 00:00:00 2001 From: Neil Daniels Date: Fri, 30 Jan 2026 13:55:55 -0800 Subject: [PATCH 08/11] Update phpunit.xml.dist --- phpunit.xml.dist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 7b2d6040..77c4c070 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -2,7 +2,7 @@ - lib + lib From 275860c1be40fe42882b2862468fe805d1e751a9 Mon Sep 17 00:00:00 2001 From: Neil Daniels Date: Fri, 30 Jan 2026 14:03:38 -0800 Subject: [PATCH 09/11] Update phpstan-baseline.neon --- phpstan-baseline.neon | 69 +++++++++++++++++++++++++++---------------- 1 file changed, 44 insertions(+), 25 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 7538c107..50dbd489 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,78 +1,97 @@ parameters: ignoreErrors: - - message: "#^Instanceof between Psr\\\\Http\\\\Message\\\\StreamInterface and Psr\\\\Http\\\\Message\\\\StreamInterface will always evaluate to true\\.$#" + rawMessage: Instanceof between Psr\Http\Message\StreamInterface and Psr\Http\Message\StreamInterface will always evaluate to true. + identifier: instanceof.alwaysTrue count: 1 path: lib/Tmdb/Api/AbstractApi.php - - message: "#^Unreachable statement \\- code above always terminates\\.$#" + rawMessage: Unreachable statement - code above always terminates. + identifier: deadCode.unreachable count: 1 path: lib/Tmdb/Api/AbstractApi.php - - message: "#^Strict comparison using \\!\\=\\= between null and Psr\\\\Http\\\\Message\\\\StreamInterface will always evaluate to true\\.$#" + rawMessage: 'Strict comparison using !== between null and Psr\Http\Message\StreamInterface will always evaluate to true.' + identifier: notIdentical.alwaysTrue count: 1 path: lib/Tmdb/Event/Listener/Logger/LogHttpMessageListener.php - - message: "#^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\CachePlugin::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 - - message: "#^Property Tmdb\\\\Event\\\\Listener\\\\Psr6CachedRequestListener\\:\\:\\$options is never read, only written\\.$#" + rawMessage: 'Property Tmdb\Event\Listener\Psr6CachedRequestListener::$options is never read, only written.' + identifier: property.onlyWritten count: 1 path: lib/Tmdb/Event/Listener/Psr6CachedRequestListener.php - - message: """ - #^Call to deprecated method setReleases\\(\\) of class Tmdb\\\\Model\\\\Movie\\: - Use the setReleaseDates instead\\.$# - """ + rawMessage: ''' + Call to deprecated method setReleases() of class Tmdb\Model\Movie: + Use the setReleaseDates instead. + ''' + identifier: method.deprecated count: 1 path: lib/Tmdb/Factory/MovieFactory.php - - message: """ - #^Instantiation of deprecated class Tmdb\\\\Model\\\\Movie\\\\Release\\: - Use ReleaseDate instead$# - """ + rawMessage: ''' + Instantiation of deprecated class Tmdb\Model\Movie\Release: + Use ReleaseDate instead + ''' + identifier: new.deprecatedClass count: 1 path: lib/Tmdb/Factory/MovieFactory.php - - message: "#^Return type \\(Tmdb\\\\Model\\\\Collection\\\\People\\) of method Tmdb\\\\Factory\\\\PeopleFactory\\:\\:createCollection\\(\\) should be compatible with return type \\(Tmdb\\\\Model\\\\Common\\\\GenericCollection\\\\) of method Tmdb\\\\Factory\\\\AbstractFactory\\\\:\\:createCollection\\(\\)$#" + rawMessage: 'Return type (Tmdb\Model\Collection\People) of method Tmdb\Factory\PeopleFactory::createCollection() should be compatible with return type (Tmdb\Model\Common\GenericCollection) of method Tmdb\Factory\AbstractFactory::createCollection()' + identifier: method.childReturnType count: 1 path: lib/Tmdb/Factory/PeopleFactory.php - - message: "#^Property Tmdb\\\\HttpClient\\\\HttpClient\\:\\:\\$sessionToken is never written, only read\\.$#" + rawMessage: 'Property Tmdb\HttpClient\HttpClient::$sessionToken (Tmdb\Token\Session\SessionToken|null) is never assigned Tmdb\Token\Session\SessionToken so it can be removed from the property type.' + identifier: property.unusedType count: 1 path: lib/Tmdb/HttpClient/HttpClient.php - - message: """ - #^Access to deprecated property \\$releases of class Tmdb\\\\Model\\\\Movie\\: - Use \\$release_dates instead$# - """ + rawMessage: 'Property Tmdb\HttpClient\HttpClient::$sessionToken is never written, only read.' + identifier: property.onlyRead + count: 1 + path: lib/Tmdb/HttpClient/HttpClient.php + + - + rawMessage: ''' + Access to deprecated property $releases of class Tmdb\Model\Movie: + Use $release_dates instead + ''' + identifier: property.deprecated count: 1 path: lib/Tmdb/Model/Movie.php - - message: """ - #^Return type of method Tmdb\\\\Repository\\\\MovieRepository\\:\\:getReleases\\(\\) has typehint with deprecated class Tmdb\\\\Model\\\\Movie\\\\Release\\: - Use ReleaseDate instead$# - """ + rawMessage: ''' + Return type of method Tmdb\Repository\MovieRepository::getReleases() has typehint with deprecated class Tmdb\Model\Movie\Release: + Use ReleaseDate instead + ''' + identifier: return.deprecatedClass count: 1 path: lib/Tmdb/Repository/MovieRepository.php - - message: "#^Result of \\|\\| is always false\\.$#" + rawMessage: Result of || is always false. + identifier: booleanOr.alwaysFalse count: 1 path: lib/Tmdb/Repository/TvSeasonRepository.php - - message: "#^Strict comparison using \\=\\=\\= between null and int will always evaluate to false\\.$#" + rawMessage: 'Strict comparison using === between null and int will always evaluate to false.' + identifier: identical.alwaysFalse count: 2 path: lib/Tmdb/Repository/TvSeasonRepository.php From 013ad35ad91aa1481335766befc2a8c0ebf93344 Mon Sep 17 00:00:00 2001 From: Neil Daniels Date: Fri, 30 Jan 2026 14:11:19 -0800 Subject: [PATCH 10/11] Revert to older Symfony OptionsResolver --- composer.json | 6 +-- lib/Tmdb/Client.php | 98 +++++++++++++++++++++------------------------ 2 files changed, 49 insertions(+), 55 deletions(-) diff --git a/composer.json b/composer.json index d13803a8..f360a678 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 || ^8", + "symfony/options-resolver": "^4.4 || ^5 || ^6 || ^7", "psr/cache": "^1 || ^2 || ^3", "psr/simple-cache": "^1 || ^2 || ^3", "psr/event-dispatcher": "^1", @@ -51,8 +51,8 @@ "php-http/mock-client": "^1.2", "slevomat/coding-standard": "^8.27.1", "squizlabs/php_codesniffer": "^4.0.1", - "symfony/cache": "^4.4 || ^5 || ^6 || ^7 || ^8", - "symfony/event-dispatcher": "^4.4 || ^5 || ^6 || ^7 || ^8", + "symfony/cache": "^4.4 || ^5 || ^6 || ^7", + "symfony/event-dispatcher": "^4.4 || ^5 || ^6 || ^7", "phpstan/phpstan": "^2.1.38", "phpstan/phpstan-deprecation-rules": "^2.0.3", "spaze/phpstan-disallowed-calls": "^4.7.0", diff --git a/lib/Tmdb/Client.php b/lib/Tmdb/Client.php index 9b668ba4..36468168 100644 --- a/lib/Tmdb/Client.php +++ b/lib/Tmdb/Client.php @@ -95,61 +95,55 @@ protected function configureOptions(array $options) 'base_uri' => null, 'api_token' => null, 'guest_session_token' => null, - 'http' => [], - 'hydration' => [], - 'event_dispatcher' => [], + 'http' => function (OptionsResolver $optionsResolver) { + $optionsResolver->setDefaults( + [ + 'client' => null, + 'request_factory' => null, + 'response_factory' => null, + 'stream_factory' => null, + 'uri_factory' => null, + ] + ); + $optionsResolver->setRequired( + [ + 'client', + 'request_factory', + 'response_factory', + 'stream_factory', + 'uri_factory' + ] + ); + $optionsResolver->setAllowedTypes('client', [ClientInterface::class, 'null']); + $optionsResolver->setAllowedTypes('request_factory', [RequestFactoryInterface::class, 'null']); + $optionsResolver->setAllowedTypes('response_factory', [ResponseFactoryInterface::class, 'null']); + $optionsResolver->setAllowedTypes('stream_factory', [StreamFactoryInterface::class, 'null']); + $optionsResolver->setAllowedTypes('uri_factory', [UriFactoryInterface::class, 'null']); + }, + 'hydration' => function (OptionsResolver $optionsResolver) { + $optionsResolver->setDefaults( + [ + 'event_listener_handles_hydration' => false, + 'only_for_specified_models' => [] + ] + ); + $optionsResolver->setAllowedTypes('event_listener_handles_hydration', ['bool']); + // @todo 4.1 validate these are actually models + $optionsResolver->setAllowedTypes('only_for_specified_models', ['array']); + }, + 'event_dispatcher' => function (OptionsResolver $optionsResolver) { + $optionsResolver->setDefaults( + [ + 'adapter' => null + ] + ); + + $optionsResolver->setRequired(['adapter']); + $optionsResolver->setAllowedTypes('adapter', [EventDispatcherInterface::class]); + } ] ); - $resolver->setOptions('http', function (OptionsResolver $optionsResolver) { - $optionsResolver->setDefaults( - [ - 'client' => null, - 'request_factory' => null, - 'response_factory' => null, - 'stream_factory' => null, - 'uri_factory' => null, - ] - ); - $optionsResolver->setRequired( - [ - 'client', - 'request_factory', - 'response_factory', - 'stream_factory', - 'uri_factory' - ] - ); - $optionsResolver->setAllowedTypes('client', [ClientInterface::class, 'null']); - $optionsResolver->setAllowedTypes('request_factory', [RequestFactoryInterface::class, 'null']); - $optionsResolver->setAllowedTypes('response_factory', [ResponseFactoryInterface::class, 'null']); - $optionsResolver->setAllowedTypes('stream_factory', [StreamFactoryInterface::class, 'null']); - $optionsResolver->setAllowedTypes('uri_factory', [UriFactoryInterface::class, 'null']); - }); - - $resolver->setOptions('hydration', function (OptionsResolver $optionsResolver) { - $optionsResolver->setDefaults( - [ - 'event_listener_handles_hydration' => false, - 'only_for_specified_models' => [] - ] - ); - $optionsResolver->setAllowedTypes('event_listener_handles_hydration', ['bool']); - // @todo 4.1 validate these are actually models - $optionsResolver->setAllowedTypes('only_for_specified_models', ['array']); - }); - - $resolver->setOptions('event_dispatcher', function (OptionsResolver $optionsResolver) { - $optionsResolver->setDefaults( - [ - 'adapter' => null - ] - ); - - $optionsResolver->setRequired(['adapter']); - $optionsResolver->setAllowedTypes('adapter', [EventDispatcherInterface::class]); - }); - $resolver->setRequired( [ 'host', From 03c02d1cf10ac5b60083617fe86157caaf60ac0a Mon Sep 17 00:00:00 2001 From: Neil Daniels Date: Fri, 30 Jan 2026 14:33:03 -0800 Subject: [PATCH 11/11] Migrate PHPUnit config --- composer.json | 2 +- phpunit.xml.dist | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/composer.json b/composer.json index f360a678..8fedf3fa 100644 --- a/composer.json +++ b/composer.json @@ -64,7 +64,7 @@ }, "scripts": { "test": "vendor/bin/phpunit", - "test-ci": "vendor/bin/phpunit --coverage-text --coverage-clover=build/coverage.xml coverage", + "test-ci": "vendor/bin/phpunit --coverage-text --coverage-clover=build/coverage.xml", "test-coverage": "php -d xdebug.mode=coverage vendor/bin/phpunit --coverage-html build/coverage", "test-cs": "vendor/bin/phpcs", "test-phpstan": "vendor/bin/phpstan analyse" diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 77c4c070..7cba710b 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,13 +1,13 @@ - - - - lib - - + ./test/ + + + lib + +