From aece17f42eb9de6d997726df2c11717e6642d95d Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Wed, 5 Aug 2026 15:48:37 +0100 Subject: [PATCH 1/4] Add support for transport sharing --- .changes/nextrelease/transport-sharing.json | 7 ++ phpstan.neon | 3 + src/AwsClient.php | 7 ++ src/ClientResolver.php | 24 ++++- src/Handler/Guzzle/GuzzleHandler.php | 21 +++- src/Handler/HttpTransportSharing.php | 95 ++++++++++++++++++ src/Sdk.php | 5 +- src/functions.php | 10 +- tests/ClientResolverTest.php | 88 ++++++++++++++++ tests/FunctionsTest.php | 15 +++ tests/Handler/Guzzle/HandlerTest.php | 42 ++++++++ tests/Handler/HttpTransportSharingTest.php | 106 ++++++++++++++++++++ tests/SdkTest.php | 31 ++++++ 13 files changed, 446 insertions(+), 8 deletions(-) create mode 100644 .changes/nextrelease/transport-sharing.json create mode 100644 src/Handler/HttpTransportSharing.php create mode 100644 tests/Handler/HttpTransportSharingTest.php diff --git a/.changes/nextrelease/transport-sharing.json b/.changes/nextrelease/transport-sharing.json new file mode 100644 index 0000000000..cca313bc02 --- /dev/null +++ b/.changes/nextrelease/transport-sharing.json @@ -0,0 +1,7 @@ +[ + { + "type": "feature", + "category": "Handler", + "description": "Adds support for Guzzle transport sharing (persistent connections) via the new transport_sharing client option." + } +] diff --git a/phpstan.neon b/phpstan.neon index 12ceabca64..b4b1d7ec52 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -28,4 +28,7 @@ parameters: # These exception classes only exist in Guzzle 8, but instances cannot occur under Guzzle 7 - '#Class GuzzleHttp\\Exception\\(NetworkException|ResponseException|ResponseTransferException) not found\.#' + # The transport sharing class only exists in Guzzle 7.11+ and 8; usage is guarded by feature detection + - '#Class GuzzleHttp\\TransportSharing not found\.#' + reportUnmatchedIgnoredErrors: false diff --git a/src/AwsClient.php b/src/AwsClient.php index 682f5f9e86..eab287c041 100644 --- a/src/AwsClient.php +++ b/src/AwsClient.php @@ -204,6 +204,13 @@ public static function getArguments() * signature version to use with a service (e.g., v4). Note that * per/operation signature version MAY override this requested signature * version. + * - transport_sharing: (string) Set to a transport sharing mode ("none", + * "handler_prefer", "handler_require", "persistent_prefer", or + * "persistent_require") to enable connection sharing on the default + * HTTP handler. The "*_prefer" modes degrade gracefully when the + * installed version of Guzzle or the runtime cannot honor them, and + * the "*_require" modes throw. This option only applies when the SDK + * creates the default HTTP handler. * - use_aws_shared_config_files: (bool, default=bool(true)) Set to false to * disable checking for shared config file in '~/.aws/config' and * '~/.aws/credentials'. This will override the AWS_CONFIG_FILE diff --git a/src/ClientResolver.php b/src/ClientResolver.php index 707827c7d3..ac22d20f0f 100644 --- a/src/ClientResolver.php +++ b/src/ClientResolver.php @@ -30,6 +30,7 @@ use Aws\EndpointV2\EndpointProviderV2; use Aws\Exception\AwsException; use Aws\Exception\InvalidRegionException; +use Aws\Handler\HttpTransportSharing; use Aws\Retry\ConfigurationInterface as RetryConfigInterface; use Aws\Retry\ConfigurationProvider as RetryConfigProvider; use Aws\Retry\V3\OptIn as NewRetriesOptIn; @@ -293,6 +294,12 @@ class ClientResolver 'default' => [], 'doc' => 'Set to an array of SDK request options to apply to each request (e.g., proxy, verify, etc.).', ], + 'transport_sharing' => [ + 'type' => 'value', + 'valid' => ['string'], + 'doc' => 'Set to a transport sharing mode ("none", "handler_prefer", "handler_require", "persistent_prefer", or "persistent_require") to enable connection sharing on the default HTTP handler. The "*_prefer" modes degrade gracefully when the installed version of Guzzle or the runtime cannot honor them, and the "*_require" modes throw. This option only applies when the SDK creates the default HTTP handler, and the "*_require" modes throw when combined with a custom "handler" or "http_handler" option.', + 'fn' => [__CLASS__, '_apply_transport_sharing'], + ], 'http_handler' => [ 'type' => 'value', 'valid' => ['callable'], @@ -988,7 +995,7 @@ public static function _apply_handler($value, array &$args, HandlerList $list) public static function _default_handler(array &$args) { return new WrappedHttpHandler( - default_http_handler(), + default_http_handler($args['transport_sharing'] ?? null), $args['parser'], $args['error_parser'], $args['exception_class'], @@ -1007,6 +1014,21 @@ public static function _apply_http_handler($value, array &$args, HandlerList $li ); } + public static function _apply_transport_sharing($value, array &$args) + { + if ((isset($args['http_handler']) || isset($args['handler'])) + && HttpTransportSharing::isRequired($value) + ) { + throw new IAE('The "transport_sharing" option can only' + . ' require transport sharing when the SDK creates the' + . ' default HTTP handler. Remove the "handler" or' + . ' "http_handler" option, or configure transport sharing' + . ' on the custom handler instead.'); + } + + $args['transport_sharing'] = HttpTransportSharing::resolve($value); + } + public static function _apply_app_id($value, array &$args) { // AppId should not be longer than 50 chars diff --git a/src/Handler/Guzzle/GuzzleHandler.php b/src/Handler/Guzzle/GuzzleHandler.php index ab5fcea08c..6bf9e81c54 100644 --- a/src/Handler/Guzzle/GuzzleHandler.php +++ b/src/Handler/Guzzle/GuzzleHandler.php @@ -2,6 +2,7 @@ namespace Aws\Handler\Guzzle; use Aws\Handler\HttpHandlerError; +use Aws\Handler\HttpTransportSharing; use GuzzleHttp\Utils; use GuzzleHttp\Promise; use GuzzleHttp\Client; @@ -18,11 +19,23 @@ class GuzzleHandler private $client; /** - * @param ClientInterface $client + * @param ClientInterface|null $client + * @param string|null $transportSharing */ - public function __construct(?ClientInterface $client = null) - { - $this->client = $client ?: new Client(); + public function __construct( + ?ClientInterface $client = null, + ?string $transportSharing = null + ) { + if ($client !== null && HttpTransportSharing::isRequired($transportSharing)) { + throw new \InvalidArgumentException('The provided transport' + . ' sharing mode cannot require sharing when a client is' + . ' provided. Configure the "transport_sharing" option on' + . ' the provided client instead.'); + } + + $this->client = $client ?: new Client( + HttpTransportSharing::toClientConfig($transportSharing) + ); } /** diff --git a/src/Handler/HttpTransportSharing.php b/src/Handler/HttpTransportSharing.php new file mode 100644 index 0000000000..7fcf076dc2 --- /dev/null +++ b/src/Handler/HttpTransportSharing.php @@ -0,0 +1,95 @@ + $mode]; + } +} diff --git a/src/Sdk.php b/src/Sdk.php index e2a6f19b61..abe41f60af 100644 --- a/src/Sdk.php +++ b/src/Sdk.php @@ -884,7 +884,10 @@ public function __construct(array $args = []) $this->args = $args; if (!isset($args['handler']) && !isset($args['http_handler'])) { - $this->args['http_handler'] = default_http_handler(); + $this->args['http_handler'] = default_http_handler( + $args['transport_sharing'] ?? null + ); + unset($this->args['transport_sharing']); } } diff --git a/src/functions.php b/src/functions.php index 6f4ed46acc..5baf72176d 100644 --- a/src/functions.php +++ b/src/functions.php @@ -270,11 +270,17 @@ function describe_type($input) /** * Creates a default HTTP handler based on the available clients. * + * @param string|null $transportSharing Optional transport sharing mode + * ("none", "handler_prefer", "handler_require", "persistent_prefer", + * or "persistent_require") to apply to the underlying HTTP client. + * The "*_prefer" modes degrade gracefully when the installed version + * of Guzzle cannot honor them, and the "*_require" modes throw. + * * @return callable */ -function default_http_handler() +function default_http_handler(?string $transportSharing = null) { - return new \Aws\Handler\Guzzle\GuzzleHandler(); + return new \Aws\Handler\Guzzle\GuzzleHandler(null, $transportSharing); } /** diff --git a/tests/ClientResolverTest.php b/tests/ClientResolverTest.php index bf84cfa406..f9c725dcca 100644 --- a/tests/ClientResolverTest.php +++ b/tests/ClientResolverTest.php @@ -20,6 +20,7 @@ use Aws\Result; use Generator; use GuzzleHttp\Psr7\Response; +use GuzzleHttp\TransportSharing; use PHPUnit\Framework\Attributes\DoesNotPerformAssertions; use Psr\Http\Message\RequestInterface; use Yoast\PHPUnitPolyfills\TestCases\TestCase; @@ -749,6 +750,93 @@ public function testCanAddHttpClientDefaultOptions() $this->assertSame('bar', $conf['http']['foo']); } + public function testAppliesTransportSharingToDefaultHandler() + { + $r = new ClientResolver(ClientResolver::getDefaultArguments()); + $conf = $r->resolve([ + 'service' => 'sqs', + 'region' => 'x', + 'version' => 'latest', + 'transport_sharing' => 'handler_prefer', + ], new HandlerList()); + + $expected = class_exists(TransportSharing::class) ? 'handler_prefer' : null; + $this->assertSame($expected, $conf['transport_sharing']); + } + + public function testDegradesPersistentPreferTransportSharing() + { + $r = new ClientResolver(ClientResolver::getDefaultArguments()); + $conf = $r->resolve([ + 'service' => 'sqs', + 'region' => 'x', + 'version' => 'latest', + 'transport_sharing' => 'persistent_prefer', + ], new HandlerList()); + + if (defined(TransportSharing::class . '::PERSISTENT_PREFER')) { + $expected = 'persistent_prefer'; + } elseif (class_exists(TransportSharing::class)) { + $expected = 'handler_prefer'; + } else { + $expected = null; + } + $this->assertSame($expected, $conf['transport_sharing']); + } + + public function testTransportSharingCannotRequireWithCustomHandler() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('can only require transport sharing'); + $r = new ClientResolver(ClientResolver::getDefaultArguments()); + $r->resolve([ + 'service' => 'sqs', + 'region' => 'x', + 'version' => 'latest', + 'http_handler' => function () {}, + 'transport_sharing' => 'handler_require', + ], new HandlerList()); + } + + #[DoesNotPerformAssertions] + public function testTransportSharingPreferIsIgnoredWithCustomHandler() + { + $r = new ClientResolver(ClientResolver::getDefaultArguments()); + $r->resolve([ + 'service' => 'sqs', + 'region' => 'x', + 'version' => 'latest', + 'http_handler' => function () {}, + 'transport_sharing' => 'persistent_prefer', + ], new HandlerList()); + } + + public function testTransportSharingRejectsInvalidMode() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The provided transport sharing mode "always" is invalid.'); + $r = new ClientResolver(ClientResolver::getDefaultArguments()); + $r->resolve([ + 'service' => 'sqs', + 'region' => 'x', + 'version' => 'latest', + 'transport_sharing' => 'always', + ], new HandlerList()); + } + + public function testTransportSharingRejectsNonStringValue() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid configuration value provided for "transport_sharing". Expected string, but got int(1)'); + $r = new ClientResolver(ClientResolver::getDefaultArguments()); + $r->resolve([ + 'service' => 'sqs', + 'region' => 'x', + 'version' => 'latest', + 'transport_sharing' => 1, + ], new HandlerList()); + } + public function testCanAddConfigOptions() { $c = new S3Client([ diff --git a/tests/FunctionsTest.php b/tests/FunctionsTest.php index e96231c002..bd7e4fe01d 100644 --- a/tests/FunctionsTest.php +++ b/tests/FunctionsTest.php @@ -201,6 +201,21 @@ public function testGuzzleHttpHandler() ); } + public function testGuzzleHttpHandlerWithTransportSharing() + { + if (!class_exists('GuzzleHttp\Handler\StreamHandler')) { + $this->markTestSkipped(); + } + $this->assertInstanceOf( + Aws\Handler\Guzzle\GuzzleHandler::class, + Aws\default_http_handler('none') + ); + $this->assertInstanceOf( + Aws\Handler\Guzzle\GuzzleHandler::class, + Aws\default_http_handler('persistent_prefer') + ); + } + public function testSerializesHttpRequests() { $mock = new MockHandler([new Result([])]); diff --git a/tests/Handler/Guzzle/HandlerTest.php b/tests/Handler/Guzzle/HandlerTest.php index 1fa921814b..0b8a12db15 100644 --- a/tests/Handler/Guzzle/HandlerTest.php +++ b/tests/Handler/Guzzle/HandlerTest.php @@ -14,6 +14,7 @@ use GuzzleHttp\Psr7\Request; use GuzzleHttp\Psr7\Response; use GuzzleHttp\TransferStats; +use GuzzleHttp\TransportSharing; use Yoast\PHPUnitPolyfills\TestCases\TestCase; use PHPUnit\Framework\Attributes\CoversClass; @@ -204,4 +205,45 @@ public function testHandlerWillStillInvokeOnStatsCallback() $this->assertTrue($wasCalled); } + + public function testTransportSharingCannotRequireWithProvidedClient() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('cannot require sharing when a client is provided'); + + new GuzzleHandler(new Client(), 'handler_require'); + } + + public function testTransportSharingPreferIsIgnoredWithProvidedClient() + { + $mock = new MockHandler([new Response(200)]); + $client = new Client(['handler' => $mock]); + $handler = new GuzzleHandler($client, 'persistent_prefer'); + + $response = $handler(new Request('GET', 'http://example.com'))->wait(); + $this->assertSame(200, $response->getStatusCode()); + } + + public function testCreatesClientWithTransportSharing() + { + $this->assertInstanceOf( + GuzzleHandler::class, + new GuzzleHandler(null, 'persistent_prefer') + ); + } + + public function testGuzzleEnforcesPersistentRequireEagerly() + { + if (!defined(TransportSharing::class . '::PERSISTENT_REQUIRE')) { + $this->markTestSkipped('Persistent transport sharing is only available in Guzzle 8.'); + } + if (PHP_VERSION_ID >= 80500) { + $this->markTestSkipped('Persistent share handles may be supported on PHP 8.5+.'); + } + + // Proves the mode is forwarded to Guzzle: only Guzzle itself throws here. + $this->expectException(\RuntimeException::class); + + new GuzzleHandler(null, 'persistent_require'); + } } diff --git a/tests/Handler/HttpTransportSharingTest.php b/tests/Handler/HttpTransportSharingTest.php new file mode 100644 index 0000000000..1c80079319 --- /dev/null +++ b/tests/Handler/HttpTransportSharingTest.php @@ -0,0 +1,106 @@ +assertNull(HttpTransportSharing::resolve(null)); + $this->assertNull(HttpTransportSharing::resolve('none')); + $this->assertSame([], HttpTransportSharing::toClientConfig(null)); + $this->assertSame([], HttpTransportSharing::toClientConfig('none')); + } + + public function testRejectsInvalidMode() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The provided transport sharing mode "always" is invalid.'); + + HttpTransportSharing::resolve('always'); + } + + public function testDetectsRequiredModes() + { + $this->assertTrue(HttpTransportSharing::isRequired('handler_require')); + $this->assertTrue(HttpTransportSharing::isRequired('persistent_require')); + $this->assertFalse(HttpTransportSharing::isRequired('handler_prefer')); + $this->assertFalse(HttpTransportSharing::isRequired('persistent_prefer')); + $this->assertFalse(HttpTransportSharing::isRequired('none')); + $this->assertFalse(HttpTransportSharing::isRequired(null)); + } + + public function testPassesHandlerModesThroughWhenSupported() + { + if (!class_exists(TransportSharing::class)) { + $this->markTestSkipped('Transport sharing requires Guzzle 7.11+.'); + } + + $this->assertSame('handler_prefer', HttpTransportSharing::resolve('handler_prefer')); + $this->assertSame('handler_require', HttpTransportSharing::resolve('handler_require')); + $this->assertSame( + ['transport_sharing' => 'handler_prefer'], + HttpTransportSharing::toClientConfig('handler_prefer') + ); + } + + public function testPreferModesDegradeToNullWhenUnsupported() + { + if (class_exists(TransportSharing::class)) { + $this->markTestSkipped('Transport sharing is supported by the installed Guzzle.'); + } + + $this->assertNull(HttpTransportSharing::resolve('handler_prefer')); + $this->assertNull(HttpTransportSharing::resolve('persistent_prefer')); + } + + public function testHandlerRequireThrowsWhenUnsupported() + { + if (class_exists(TransportSharing::class)) { + $this->markTestSkipped('Transport sharing is supported by the installed Guzzle.'); + } + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('requires guzzlehttp/guzzle ^7.11 || ^8.0'); + + HttpTransportSharing::resolve('handler_require'); + } + + public function testPersistentPreferDegradesToHandlerPreferOnGuzzle7() + { + if (!class_exists(TransportSharing::class) + || defined(TransportSharing::class . '::PERSISTENT_PREFER') + ) { + $this->markTestSkipped('Requires Guzzle 7.11 through 7.15.'); + } + + $this->assertSame('handler_prefer', HttpTransportSharing::resolve('persistent_prefer')); + } + + public function testPersistentModesPassThroughOnGuzzle8() + { + if (!defined(TransportSharing::class . '::PERSISTENT_PREFER')) { + $this->markTestSkipped('Persistent transport sharing is only available in Guzzle 8.'); + } + + $this->assertSame('persistent_prefer', HttpTransportSharing::resolve('persistent_prefer')); + $this->assertSame('persistent_require', HttpTransportSharing::resolve('persistent_require')); + } + + public function testPersistentRequireThrowsWithoutGuzzle8() + { + if (defined(TransportSharing::class . '::PERSISTENT_PREFER')) { + $this->markTestSkipped('Persistent transport sharing is available in the installed Guzzle.'); + } + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('requires guzzlehttp/guzzle ^8.0'); + + HttpTransportSharing::resolve('persistent_require'); + } +} diff --git a/tests/SdkTest.php b/tests/SdkTest.php index 53266adbd6..d497b7c85c 100644 --- a/tests/SdkTest.php +++ b/tests/SdkTest.php @@ -5,6 +5,7 @@ use Aws\MultiRegionClient; use Aws\S3\S3MultiRegionClient; use Aws\Sdk; +use GuzzleHttp\TransportSharing; use Yoast\PHPUnitPolyfills\TestCases\TestCase; use PHPUnit\Framework\Attributes\CoversClass; @@ -88,4 +89,34 @@ public function testClonesWithExtraArgs() (string) $copy->createDynamoDb()->getEndpoint() ); } + + public function testAppliesTransportSharingToSharedHttpHandler() + { + $sdk = new Sdk([ + 'region' => 'us-east-1', + 'version' => 'latest', + 'transport_sharing' => 'persistent_prefer', + ]); + + $this->assertInstanceOf( + AwsClientInterface::class, + $sdk->createDynamoDb() + ); + } + + public function testSdkConsumesTransportSharingWhenCreatingSharedHandler() + { + if (!class_exists(TransportSharing::class)) { + $this->markTestSkipped('Transport sharing requires Guzzle 7.11+.'); + } + + $sdk = new Sdk([ + 'region' => 'us-east-1', + 'version' => 'latest', + 'transport_sharing' => 'handler_prefer', + ]); + $client = $sdk->createDynamoDb(); + + $this->assertNull($client->getConfig('transport_sharing')); + } } From cc9379fa4ba9a86396434da9e1c00f6708a9318f Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Wed, 5 Aug 2026 15:48:38 +0100 Subject: [PATCH 2/4] Test against Guzzle 7.11-7.15 in CI --- .github/workflows/tests.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 84ce20ba5a..c1ffe2b43c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -35,8 +35,11 @@ jobs: composer-options: '--prefer-lowest' - php-versions: '8.5' composer-options: '' + - php-versions: '8.4' + composer-options: '' + guzzle-version: '>=7.11 <8' # set the name for each job - name: PHP ${{ matrix.php-versions }} ${{ matrix.composer-options }} + name: PHP ${{ matrix.php-versions }} ${{ matrix.composer-options }} ${{ matrix.guzzle-version }} # set up environment variables used by unit tests env: AWS_ACCESS_KEY_ID: foo @@ -80,6 +83,9 @@ jobs: else composer update ${{ matrix.composer-options }} --no-interaction --prefer-dist fi + if [[ -n "${{ matrix.guzzle-version }}" ]]; then + composer require "guzzlehttp/guzzle:${{ matrix.guzzle-version }}" --no-interaction --with-all-dependencies + fi # run tests - name: Run test suite From c3b5264b79f23a17560a19dba2cea41321794021 Mon Sep 17 00:00:00 2001 From: Sean O'Brien Date: Sat, 8 Aug 2026 16:36:28 -0400 Subject: [PATCH 3/4] update exception type in testGuzzleEnforcesPersistentRequireEagerly --- tests/Handler/Guzzle/HandlerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Handler/Guzzle/HandlerTest.php b/tests/Handler/Guzzle/HandlerTest.php index 0b8a12db15..bddbb90c49 100644 --- a/tests/Handler/Guzzle/HandlerTest.php +++ b/tests/Handler/Guzzle/HandlerTest.php @@ -242,7 +242,7 @@ public function testGuzzleEnforcesPersistentRequireEagerly() } // Proves the mode is forwarded to Guzzle: only Guzzle itself throws here. - $this->expectException(\RuntimeException::class); + $this->expectException(\InvalidArgumentException::class); new GuzzleHandler(null, 'persistent_require'); } From 041c5b961f023090d93c517ce5b23623fd8d05ea Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Thu, 13 Aug 2026 00:07:20 +0100 Subject: [PATCH 4/4] Memoize Guzzle feature detection and only resolve transport sharing in the handler --- src/ClientResolver.php | 4 +-- src/Handler/HttpTransportSharing.php | 41 ++++++++++++++++++++++------ tests/ClientResolverTest.php | 15 ++-------- 3 files changed, 37 insertions(+), 23 deletions(-) diff --git a/src/ClientResolver.php b/src/ClientResolver.php index ac22d20f0f..e017ab895a 100644 --- a/src/ClientResolver.php +++ b/src/ClientResolver.php @@ -1016,6 +1016,8 @@ public static function _apply_http_handler($value, array &$args, HandlerList $li public static function _apply_transport_sharing($value, array &$args) { + HttpTransportSharing::validate($value); + if ((isset($args['http_handler']) || isset($args['handler'])) && HttpTransportSharing::isRequired($value) ) { @@ -1025,8 +1027,6 @@ public static function _apply_transport_sharing($value, array &$args) . ' "http_handler" option, or configure transport sharing' . ' on the custom handler instead.'); } - - $args['transport_sharing'] = HttpTransportSharing::resolve($value); } public static function _apply_app_id($value, array &$args) diff --git a/src/Handler/HttpTransportSharing.php b/src/Handler/HttpTransportSharing.php index 7fcf076dc2..e5fd0d18bd 100644 --- a/src/Handler/HttpTransportSharing.php +++ b/src/Handler/HttpTransportSharing.php @@ -28,6 +28,20 @@ public static function isRequired(?string $mode): bool || $mode === self::PERSISTENT_REQUIRE; } + /** + * Validates a requested transport sharing mode without resolving it + * against the capabilities of the installed version of Guzzle. + */ + public static function validate(?string $mode): void + { + if ($mode !== null && !in_array($mode, self::MODES, true)) { + throw new \InvalidArgumentException('The provided transport' + . ' sharing mode "' . $mode . '" is invalid. Valid modes are:' + . ' "none", "handler_prefer", "handler_require",' + . ' "persistent_prefer", "persistent_require".'); + } + } + /** * Resolves a requested transport sharing mode to the mode that should be * passed to the installed version of Guzzle, or null when no mode should @@ -36,25 +50,20 @@ public static function isRequired(?string $mode): bool */ public static function resolve(?string $mode): ?string { + self::validate($mode); + if ($mode === null || $mode === self::NONE) { return null; } - if (!in_array($mode, self::MODES, true)) { - throw new \InvalidArgumentException('The provided transport' - . ' sharing mode "' . $mode . '" is invalid. Valid modes are:' - . ' "none", "handler_prefer", "handler_require",' - . ' "persistent_prefer", "persistent_require".'); - } - // Guzzle 8: all modes are understood, and Guzzle enforces the // runtime requirements of the "*_require" modes itself. - if (defined(TransportSharing::class . '::PERSISTENT_PREFER')) { + if (self::supportsPersistentSharing()) { return $mode; } // Guzzle 7.11+: handler-lifetime sharing only. - if (class_exists(TransportSharing::class)) { + if (self::supportsHandlerSharing()) { if ($mode === self::PERSISTENT_PREFER) { return self::HANDLER_PREFER; } @@ -92,4 +101,18 @@ public static function toClientConfig(?string $mode): array return $mode === null ? [] : ['transport_sharing' => $mode]; } + + private static function supportsPersistentSharing(): bool + { + static $supported; + + return $supported ??= defined(TransportSharing::class . '::PERSISTENT_PREFER'); + } + + private static function supportsHandlerSharing(): bool + { + static $supported; + + return $supported ??= class_exists(TransportSharing::class); + } } diff --git a/tests/ClientResolverTest.php b/tests/ClientResolverTest.php index f9c725dcca..49f12b571e 100644 --- a/tests/ClientResolverTest.php +++ b/tests/ClientResolverTest.php @@ -20,7 +20,6 @@ use Aws\Result; use Generator; use GuzzleHttp\Psr7\Response; -use GuzzleHttp\TransportSharing; use PHPUnit\Framework\Attributes\DoesNotPerformAssertions; use Psr\Http\Message\RequestInterface; use Yoast\PHPUnitPolyfills\TestCases\TestCase; @@ -760,11 +759,10 @@ public function testAppliesTransportSharingToDefaultHandler() 'transport_sharing' => 'handler_prefer', ], new HandlerList()); - $expected = class_exists(TransportSharing::class) ? 'handler_prefer' : null; - $this->assertSame($expected, $conf['transport_sharing']); + $this->assertSame('handler_prefer', $conf['transport_sharing']); } - public function testDegradesPersistentPreferTransportSharing() + public function testPreservesRequestedTransportSharingMode() { $r = new ClientResolver(ClientResolver::getDefaultArguments()); $conf = $r->resolve([ @@ -774,14 +772,7 @@ public function testDegradesPersistentPreferTransportSharing() 'transport_sharing' => 'persistent_prefer', ], new HandlerList()); - if (defined(TransportSharing::class . '::PERSISTENT_PREFER')) { - $expected = 'persistent_prefer'; - } elseif (class_exists(TransportSharing::class)) { - $expected = 'handler_prefer'; - } else { - $expected = null; - } - $this->assertSame($expected, $conf['transport_sharing']); + $this->assertSame('persistent_prefer', $conf['transport_sharing']); } public function testTransportSharingCannotRequireWithCustomHandler()