From 8a4b4c53254f315688ebd78edaf2c492698c8d5d Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Thu, 23 Jul 2026 01:25:49 +0100 Subject: [PATCH] feat: add support for Guzzle 8 --- composer.json | 6 ++--- phpstan.neon.dist | 4 +++ src/AuthHandler/AuthHandlerFactory.php | 12 ++------- src/AuthHandler/Guzzle6AuthHandler.php | 2 +- src/Client.php | 33 +++---------------------- src/Http/REST.php | 7 +++--- tests/Google/ClientTest.php | 7 +++--- tests/Google/Http/RESTTest.php | 34 ++++++++++++++++++++++++++ 8 files changed, 56 insertions(+), 49 deletions(-) diff --git a/composer.json b/composer.json index 962f61dd60..0394550b44 100644 --- a/composer.json +++ b/composer.json @@ -9,12 +9,12 @@ "license": "Apache-2.0", "require": { "php": "^8.1", - "google/auth": "^1.37", + "google/auth": "^1.53", "google/apiclient-services": "~0.350", "firebase/php-jwt": "^6.0||^7.0", "monolog/monolog": "^2.9||^3.0", - "guzzlehttp/guzzle": "^7.4.5", - "guzzlehttp/psr7": "^2.6" + "guzzlehttp/guzzle": "^7.8.2||^8.0", + "guzzlehttp/psr7": "^2.6.3||^3.0" }, "require-dev": { "squizlabs/php_codesniffer": "^3.8", diff --git a/phpstan.neon.dist b/phpstan.neon.dist index dcef11342c..cc6ff402fb 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -3,3 +3,7 @@ parameters: level: 5 paths: - src + excludePaths: + # Legacy, unused Guzzle 6 auth handler retained for backwards + # compatibility only; it relies on APIs removed in Guzzle 8. + - src/AuthHandler/Guzzle6AuthHandler.php diff --git a/src/AuthHandler/AuthHandlerFactory.php b/src/AuthHandler/AuthHandlerFactory.php index 98a0ab1668..2c16f52153 100644 --- a/src/AuthHandler/AuthHandlerFactory.php +++ b/src/AuthHandler/AuthHandlerFactory.php @@ -30,17 +30,9 @@ class AuthHandlerFactory */ public static function build($cache = null, array $cacheConfig = []) { - $guzzleVersion = null; - if (defined('\GuzzleHttp\ClientInterface::MAJOR_VERSION')) { - $guzzleVersion = ClientInterface::MAJOR_VERSION; - } elseif (defined('\GuzzleHttp\ClientInterface::VERSION')) { - $guzzleVersion = (int) substr(ClientInterface::VERSION, 0, 1); - } - - switch ($guzzleVersion) { - case 6: - return new Guzzle6AuthHandler($cache, $cacheConfig); + switch (ClientInterface::MAJOR_VERSION) { case 7: + case 8: return new Guzzle7AuthHandler($cache, $cacheConfig); default: throw new Exception('Version not supported'); diff --git a/src/AuthHandler/Guzzle6AuthHandler.php b/src/AuthHandler/Guzzle6AuthHandler.php index 05fe1b2b9b..56fda356c3 100644 --- a/src/AuthHandler/Guzzle6AuthHandler.php +++ b/src/AuthHandler/Guzzle6AuthHandler.php @@ -13,7 +13,7 @@ use Psr\Cache\CacheItemPoolInterface; /** - * This supports Guzzle 6 + * @deprecated Guzzle 6 is no longer supported; use Guzzle7AuthHandler. */ class Guzzle6AuthHandler { diff --git a/src/Client.php b/src/Client.php index c859fe6e60..5e57a87123 100644 --- a/src/Client.php +++ b/src/Client.php @@ -35,7 +35,6 @@ use Google\Http\REST; use GuzzleHttp\Client as GuzzleClient; use GuzzleHttp\ClientInterface; -use GuzzleHttp\Ring\Client\StreamHandler; use InvalidArgumentException; use LogicException; use Monolog\Handler\StreamHandler as MonologStreamHandler; @@ -1239,34 +1238,10 @@ public function setApiFormatV2($value) protected function createDefaultHttpClient() { - $guzzleVersion = null; - if (defined('\GuzzleHttp\ClientInterface::MAJOR_VERSION')) { - $guzzleVersion = ClientInterface::MAJOR_VERSION; - } elseif (defined('\GuzzleHttp\ClientInterface::VERSION')) { - $guzzleVersion = (int)substr(ClientInterface::VERSION, 0, 1); - } - - if (5 === $guzzleVersion) { - $options = [ - 'base_url' => $this->config['base_path'], - 'defaults' => ['exceptions' => false], - ]; - if ($this->isAppEngine()) { - if (class_exists(StreamHandler::class)) { - // set StreamHandler on AppEngine by default - $options['handler'] = new StreamHandler(); - $options['defaults']['verify'] = '/etc/ca-certificates.crt'; - } - } - } elseif (6 === $guzzleVersion || 7 === $guzzleVersion) { - // guzzle 6 or 7 - $options = [ - 'base_uri' => $this->config['base_path'], - 'http_errors' => false, - ]; - } else { - throw new LogicException('Could not find supported version of Guzzle.'); - } + $options = [ + 'base_uri' => $this->config['base_path'], + 'http_errors' => false, + ]; return new GuzzleClient($options); } diff --git a/src/Http/REST.php b/src/Http/REST.php index ea6eb668bd..f975259655 100644 --- a/src/Http/REST.php +++ b/src/Http/REST.php @@ -84,11 +84,12 @@ public static function doExecute(ClientInterface $client, RequestInterface $requ $response = $httpHandler($request); } catch (RequestException $e) { // if Guzzle throws an exception, catch it and handle the response - if (!$e->hasResponse()) { + // (on Guzzle 7 the response is on RequestException, but on Guzzle 8 + // it is only on its ResponseException subclass) + $response = method_exists($e, 'getResponse') ? $e->getResponse() : null; + if (null === $response) { throw $e; } - - $response = $e->getResponse(); } return self::decodeHttpResponse($response, $request, $expectedClass); diff --git a/tests/Google/ClientTest.php b/tests/Google/ClientTest.php index 4a336705d7..a8da391271 100644 --- a/tests/Google/ClientTest.php +++ b/tests/Google/ClientTest.php @@ -68,12 +68,13 @@ private function checkAuthHandler($http, $className) $property = $class->getProperty('stack'); $property->setAccessible(true); $middlewares = $property->getValue($stack); - $middleware = array_pop($middlewares); if (null === $className) { // only the default middlewares have been added - $this->assertCount(3, $middlewares); + $defaultStack = (new GuzzleClient())->getConfig('handler'); + $this->assertCount(count($property->getValue($defaultStack)), $middlewares); } else { + $middleware = array_pop($middlewares); $authClass = sprintf('Google\Auth\Middleware\%sMiddleware', $className); $this->assertInstanceOf($authClass, $middleware[0]); } @@ -904,7 +905,7 @@ public function testCredentialsOptionWithFetchAuthTokenInterface() $callable(new Request('GET', '/fake-uri'), ['auth' => 'google_auth']); }); - $httpClient = $this->prophesize('GuzzleHttp\ClientInterface'); + $httpClient = $this->prophesize('GuzzleHttp\Client'); $httpClient->getConfig() ->shouldBeCalled() ->willReturn(['handler' => $handler->reveal()]); diff --git a/tests/Google/Http/RESTTest.php b/tests/Google/Http/RESTTest.php index 7b05b0cb46..600c288883 100644 --- a/tests/Google/Http/RESTTest.php +++ b/tests/Google/Http/RESTTest.php @@ -21,9 +21,12 @@ use Google\Service\Exception as ServiceException; use Google\Tests\BaseTest; use GuzzleHttp\Client as GuzzleClient; +use GuzzleHttp\Exception\ClientException; +use GuzzleHttp\Exception\RequestException; use GuzzleHttp\Psr7; use GuzzleHttp\Psr7\Request; use GuzzleHttp\Psr7\Response; +use Prophecy\Argument; class RESTTest extends BaseTest { @@ -86,6 +89,37 @@ public function testExceptionResponse() $response = $this->rest->doExecute($http, $request); } + public function testRequestExceptionWithResponseIsHandled() + { + $this->expectException(ServiceException::class); + $this->expectExceptionCode(400); + + $request = new Request('GET', 'http://www.example.com'); + $response = new Response(400, [], Psr7\Utils::streamFor('{"error": "bad request"}')); + + $http = $this->prophesize('GuzzleHttp\ClientInterface'); + $http->send(Argument::type('Psr\Http\Message\RequestInterface'), []) + ->shouldBeCalledTimes(1) + ->willThrow(new ClientException('Bad Request', $request, $response)); + + $this->rest->doExecute($http->reveal(), $request); + } + + public function testRequestExceptionWithoutResponseIsRethrown() + { + $this->expectException(RequestException::class); + $this->expectExceptionMessage('Request failed'); + + $request = new Request('GET', 'http://www.example.com'); + + $http = $this->prophesize('GuzzleHttp\ClientInterface'); + $http->send(Argument::type('Psr\Http\Message\RequestInterface'), []) + ->shouldBeCalledTimes(1) + ->willThrow(new RequestException('Request failed', $request)); + + $this->rest->doExecute($http->reveal(), $request); + } + public function testDecodeEmptyResponse() { $stream = Psr7\Utils::streamFor('{}');