diff --git a/composer.json b/composer.json index 41e1911b6e..5295ee17ca 100644 --- a/composer.json +++ b/composer.json @@ -188,7 +188,7 @@ "url": "src/Symfony/Contracts", "options": { "versions": { - "symfony/contracts": "2.3.x-dev" + "symfony/contracts": "2.4.x-dev" } } } diff --git a/src/Symfony/Component/HttpClient/AsyncDecoratorTrait.php b/src/Symfony/Component/HttpClient/AsyncDecoratorTrait.php index c5d40a251d..2e6267300d 100644 --- a/src/Symfony/Component/HttpClient/AsyncDecoratorTrait.php +++ b/src/Symfony/Component/HttpClient/AsyncDecoratorTrait.php @@ -51,4 +51,15 @@ trait AsyncDecoratorTrait return new ResponseStream(AsyncResponse::stream($responses, $timeout, static::class)); } + + /** + * {@inheritdoc} + */ + public function withOptions(array $options): self + { + $clone = clone $this; + $clone->client = $this->client->withOptions($options); + + return $clone; + } } diff --git a/src/Symfony/Component/HttpClient/CHANGELOG.md b/src/Symfony/Component/HttpClient/CHANGELOG.md index f25989e168..3b97488c93 100644 --- a/src/Symfony/Component/HttpClient/CHANGELOG.md +++ b/src/Symfony/Component/HttpClient/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +5.3 +--- + + * Implement `HttpClientInterface::withOptions()` from `symfony/contracts` v2.4 + 5.2.0 ----- diff --git a/src/Symfony/Component/HttpClient/CurlHttpClient.php b/src/Symfony/Component/HttpClient/CurlHttpClient.php index 48ecf773d4..3a75e675e2 100644 --- a/src/Symfony/Component/HttpClient/CurlHttpClient.php +++ b/src/Symfony/Component/HttpClient/CurlHttpClient.php @@ -341,30 +341,8 @@ final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface, public function reset() { - if ($this->logger) { - foreach ($this->multi->pushedResponses as $url => $response) { - $this->logger->debug(sprintf('Unused pushed response: "%s"', $url)); - } - } - - $this->multi->pushedResponses = []; - $this->multi->dnsCache->evictions = $this->multi->dnsCache->evictions ?: $this->multi->dnsCache->removals; - $this->multi->dnsCache->removals = $this->multi->dnsCache->hostnames = []; - - if (\is_resource($this->multi->handle) || $this->multi->handle instanceof \CurlMultiHandle) { - if (\defined('CURLMOPT_PUSHFUNCTION')) { - curl_multi_setopt($this->multi->handle, \CURLMOPT_PUSHFUNCTION, null); - } - - $active = 0; - while (\CURLM_CALL_MULTI_PERFORM === curl_multi_exec($this->multi->handle, $active)); - } - - foreach ($this->multi->openHandles as [$ch]) { - if (\is_resource($ch) || $ch instanceof \CurlHandle) { - curl_setopt($ch, \CURLOPT_VERBOSE, false); - } - } + $this->multi->logger = $this->logger; + $this->multi->reset(); } public function __sleep() @@ -379,7 +357,7 @@ final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface, public function __destruct() { - $this->reset(); + $this->multi->logger = $this->logger; } private function handlePush($parent, $pushed, array $requestHeaders, int $maxPendingPushes): int diff --git a/src/Symfony/Component/HttpClient/EventSourceHttpClient.php b/src/Symfony/Component/HttpClient/EventSourceHttpClient.php index d6f27a7fae..81d9f4bfc8 100644 --- a/src/Symfony/Component/HttpClient/EventSourceHttpClient.php +++ b/src/Symfony/Component/HttpClient/EventSourceHttpClient.php @@ -26,8 +26,9 @@ use Symfony\Contracts\HttpClient\ResponseInterface; */ final class EventSourceHttpClient implements HttpClientInterface { - use AsyncDecoratorTrait; - use HttpClientTrait; + use AsyncDecoratorTrait, HttpClientTrait { + AsyncDecoratorTrait::withOptions insteadof HttpClientTrait; + } private $reconnectionTime; diff --git a/src/Symfony/Component/HttpClient/HttpClientTrait.php b/src/Symfony/Component/HttpClient/HttpClientTrait.php index 545b1aad28..9d68f9a61f 100644 --- a/src/Symfony/Component/HttpClient/HttpClientTrait.php +++ b/src/Symfony/Component/HttpClient/HttpClientTrait.php @@ -17,7 +17,7 @@ use Symfony\Component\HttpClient\Exception\TransportException; /** * Provides the common logic from writing HttpClientInterface implementations. * - * All methods are static to prevent implementers from creating memory leaks via circular references. + * All private methods are static to prevent implementers from creating memory leaks via circular references. * * @author Nicolas Grekas */ @@ -25,6 +25,17 @@ trait HttpClientTrait { private static $CHUNK_SIZE = 16372; + /** + * {@inheritdoc} + */ + public function withOptions(array $options): self + { + $clone = clone $this; + $clone->defaultOptions = self::mergeDefaultOptions($options, $this->defaultOptions); + + return $clone; + } + /** * Validates and normalizes method, URL and options, and merges them with defaults. * diff --git a/src/Symfony/Component/HttpClient/Internal/CurlClientState.php b/src/Symfony/Component/HttpClient/Internal/CurlClientState.php index f381968d14..40ed0f1fc8 100644 --- a/src/Symfony/Component/HttpClient/Internal/CurlClientState.php +++ b/src/Symfony/Component/HttpClient/Internal/CurlClientState.php @@ -11,6 +11,8 @@ namespace Symfony\Component\HttpClient\Internal; +use Psr\Log\LoggerInterface; + /** * Internal representation of the cURL client's state. * @@ -29,10 +31,55 @@ final class CurlClientState extends ClientState /** @var float[] */ public $pauseExpiries = []; public $execCounter = \PHP_INT_MIN; + /** @var LoggerInterface|null */ + public $logger; public function __construct() { $this->handle = curl_multi_init(); $this->dnsCache = new DnsCache(); } + + public function reset() + { + if ($this->logger) { + foreach ($this->pushedResponses as $url => $response) { + $this->logger->debug(sprintf('Unused pushed response: "%s"', $url)); + } + } + + $this->pushedResponses = []; + $this->dnsCache->evictions = $this->dnsCache->evictions ?: $this->dnsCache->removals; + $this->dnsCache->removals = $this->dnsCache->hostnames = []; + + if (\is_resource($this->handle) || $this->handle instanceof \CurlMultiHandle) { + if (\defined('CURLMOPT_PUSHFUNCTION')) { + curl_multi_setopt($this->handle, \CURLMOPT_PUSHFUNCTION, null); + } + + $active = 0; + while (\CURLM_CALL_MULTI_PERFORM === curl_multi_exec($this->handle, $active)); + } + + foreach ($this->openHandles as [$ch]) { + if (\is_resource($ch) || $ch instanceof \CurlHandle) { + curl_setopt($ch, \CURLOPT_VERBOSE, false); + } + } + } + + public function __sleep() + { + throw new \BadMethodCallException('Cannot serialize '.__CLASS__); + } + + public function __wakeup() + { + throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); + } + + public function __destruct() + { + $this->reset(); + } } diff --git a/src/Symfony/Component/HttpClient/MockHttpClient.php b/src/Symfony/Component/HttpClient/MockHttpClient.php index 08571f07c2..a794faff6e 100644 --- a/src/Symfony/Component/HttpClient/MockHttpClient.php +++ b/src/Symfony/Component/HttpClient/MockHttpClient.php @@ -28,8 +28,8 @@ class MockHttpClient implements HttpClientInterface use HttpClientTrait; private $responseFactory; - private $baseUri; private $requestsCount = 0; + private $defaultOptions = []; /** * @param callable|callable[]|ResponseInterface|ResponseInterface[]|iterable|null $responseFactory @@ -47,7 +47,7 @@ class MockHttpClient implements HttpClientInterface } $this->responseFactory = $responseFactory; - $this->baseUri = $baseUri; + $this->defaultOptions['base_uri'] = $baseUri; } /** @@ -55,7 +55,7 @@ class MockHttpClient implements HttpClientInterface */ public function request(string $method, string $url, array $options = []): ResponseInterface { - [$url, $options] = $this->prepareRequest($method, $url, $options, ['base_uri' => $this->baseUri], true); + [$url, $options] = $this->prepareRequest($method, $url, $options, $this->defaultOptions, true); $url = implode('', $url); if (null === $this->responseFactory) { @@ -96,4 +96,15 @@ class MockHttpClient implements HttpClientInterface { return $this->requestsCount; } + + /** + * {@inheritdoc} + */ + public function withOptions(array $options): self + { + $clone = clone $this; + $clone->defaultOptions = self::mergeDefaultOptions($options, $this->defaultOptions, true); + + return $clone; + } } diff --git a/src/Symfony/Component/HttpClient/NoPrivateNetworkHttpClient.php b/src/Symfony/Component/HttpClient/NoPrivateNetworkHttpClient.php index d4c69cabce..b9db846992 100644 --- a/src/Symfony/Component/HttpClient/NoPrivateNetworkHttpClient.php +++ b/src/Symfony/Component/HttpClient/NoPrivateNetworkHttpClient.php @@ -110,4 +110,15 @@ final class NoPrivateNetworkHttpClient implements HttpClientInterface, LoggerAwa $this->client->setLogger($logger); } } + + /** + * {@inheritdoc} + */ + public function withOptions(array $options): self + { + $clone = clone $this; + $clone->client = $this->client->withOptions($options); + + return $clone; + } } diff --git a/src/Symfony/Component/HttpClient/ScopingHttpClient.php b/src/Symfony/Component/HttpClient/ScopingHttpClient.php index 66dcccf0e9..2a6e70e15d 100644 --- a/src/Symfony/Component/HttpClient/ScopingHttpClient.php +++ b/src/Symfony/Component/HttpClient/ScopingHttpClient.php @@ -110,4 +110,15 @@ class ScopingHttpClient implements HttpClientInterface, ResetInterface, LoggerAw $this->client->setLogger($logger); } } + + /** + * {@inheritdoc} + */ + public function withOptions(array $options): self + { + $clone = clone $this; + $clone->client = $this->client->withOptions($options); + + return $clone; + } } diff --git a/src/Symfony/Component/HttpClient/TraceableHttpClient.php b/src/Symfony/Component/HttpClient/TraceableHttpClient.php index 34dc01ad25..bc84211590 100644 --- a/src/Symfony/Component/HttpClient/TraceableHttpClient.php +++ b/src/Symfony/Component/HttpClient/TraceableHttpClient.php @@ -105,4 +105,15 @@ final class TraceableHttpClient implements HttpClientInterface, ResetInterface, $this->client->setLogger($logger); } } + + /** + * {@inheritdoc} + */ + public function withOptions(array $options): self + { + $clone = clone $this; + $clone->client = $this->client->withOptions($options); + + return $clone; + } } diff --git a/src/Symfony/Component/HttpClient/composer.json b/src/Symfony/Component/HttpClient/composer.json index 880798e83b..be66a8f5c2 100644 --- a/src/Symfony/Component/HttpClient/composer.json +++ b/src/Symfony/Component/HttpClient/composer.json @@ -18,12 +18,12 @@ "php-http/async-client-implementation": "*", "php-http/client-implementation": "*", "psr/http-client-implementation": "1.0", - "symfony/http-client-implementation": "2.2" + "symfony/http-client-implementation": "2.4" }, "require": { "php": ">=7.2.5", "psr/log": "^1.0", - "symfony/http-client-contracts": "^2.2", + "symfony/http-client-contracts": "^2.4", "symfony/polyfill-php73": "^1.11", "symfony/polyfill-php80": "^1.15", "symfony/service-contracts": "^1.0|^2" diff --git a/src/Symfony/Contracts/CHANGELOG.md b/src/Symfony/Contracts/CHANGELOG.md index b62029adb5..25efba02b9 100644 --- a/src/Symfony/Contracts/CHANGELOG.md +++ b/src/Symfony/Contracts/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +2.4 +--- + + * Add `HttpClientInterface::withOptions()` + 2.3.0 ----- diff --git a/src/Symfony/Contracts/Cache/composer.json b/src/Symfony/Contracts/Cache/composer.json index 2b83ae24bc..0f6989201a 100644 --- a/src/Symfony/Contracts/Cache/composer.json +++ b/src/Symfony/Contracts/Cache/composer.json @@ -28,7 +28,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-main": "2.3-dev" + "dev-main": "2.4-dev" }, "thanks": { "name": "symfony/contracts", diff --git a/src/Symfony/Contracts/Deprecation/composer.json b/src/Symfony/Contracts/Deprecation/composer.json index 88d3055927..3884889637 100644 --- a/src/Symfony/Contracts/Deprecation/composer.json +++ b/src/Symfony/Contracts/Deprecation/composer.json @@ -25,7 +25,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-main": "2.3-dev" + "dev-main": "2.4-dev" }, "thanks": { "name": "symfony/contracts", diff --git a/src/Symfony/Contracts/EventDispatcher/composer.json b/src/Symfony/Contracts/EventDispatcher/composer.json index 8eaf0354fd..9f6dc413a0 100644 --- a/src/Symfony/Contracts/EventDispatcher/composer.json +++ b/src/Symfony/Contracts/EventDispatcher/composer.json @@ -28,7 +28,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-main": "2.3-dev" + "dev-main": "2.4-dev" }, "thanks": { "name": "symfony/contracts", diff --git a/src/Symfony/Contracts/HttpClient/HttpClientInterface.php b/src/Symfony/Contracts/HttpClient/HttpClientInterface.php index 4388eb84ce..ea793ba394 100644 --- a/src/Symfony/Contracts/HttpClient/HttpClientInterface.php +++ b/src/Symfony/Contracts/HttpClient/HttpClientInterface.php @@ -19,6 +19,8 @@ use Symfony\Contracts\HttpClient\Test\HttpClientTestCase; * * @see HttpClientTestCase for a reference test suite * + * @method static withOptions(array $options) Returns a new instance of the client with new default options + * * @author Nicolas Grekas */ interface HttpClientInterface diff --git a/src/Symfony/Contracts/HttpClient/Test/HttpClientTestCase.php b/src/Symfony/Contracts/HttpClient/Test/HttpClientTestCase.php index 74997eaafc..38b845438d 100644 --- a/src/Symfony/Contracts/HttpClient/Test/HttpClientTestCase.php +++ b/src/Symfony/Contracts/HttpClient/Test/HttpClientTestCase.php @@ -1038,4 +1038,20 @@ abstract class HttpClientTestCase extends TestCase $this->assertLessThan(10, $duration); } + + public function testWithOptions() + { + $client = $this->getHttpClient(__FUNCTION__); + if (!method_exists($client, 'withOptions')) { + $this->markTestSkipped(sprintf('Not implementing "%s::withOptions()" is deprecated.', get_debug_type($client))); + } + + $client2 = $client->withOptions(['base_uri' => 'http://localhost:8057/']); + + $this->assertNotSame($client, $client2); + $this->assertSame(\get_class($client), \get_class($client2)); + + $response = $client2->request('GET', '/'); + $this->assertSame(200, $response->getStatusCode()); + } } diff --git a/src/Symfony/Contracts/HttpClient/composer.json b/src/Symfony/Contracts/HttpClient/composer.json index 9ab9eaf76c..2633785448 100644 --- a/src/Symfony/Contracts/HttpClient/composer.json +++ b/src/Symfony/Contracts/HttpClient/composer.json @@ -27,7 +27,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-main": "2.3-dev" + "dev-main": "2.4-dev" }, "thanks": { "name": "symfony/contracts", diff --git a/src/Symfony/Contracts/Service/composer.json b/src/Symfony/Contracts/Service/composer.json index 524f6a6c6c..ec22b07db6 100644 --- a/src/Symfony/Contracts/Service/composer.json +++ b/src/Symfony/Contracts/Service/composer.json @@ -28,7 +28,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-main": "2.3-dev" + "dev-main": "2.4-dev" }, "thanks": { "name": "symfony/contracts", diff --git a/src/Symfony/Contracts/Translation/composer.json b/src/Symfony/Contracts/Translation/composer.json index 907d28f0a8..00e27f836a 100644 --- a/src/Symfony/Contracts/Translation/composer.json +++ b/src/Symfony/Contracts/Translation/composer.json @@ -27,7 +27,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-main": "2.3-dev" + "dev-main": "2.4-dev" }, "thanks": { "name": "symfony/contracts", diff --git a/src/Symfony/Contracts/composer.json b/src/Symfony/Contracts/composer.json index 595e744af5..b424c33fc8 100644 --- a/src/Symfony/Contracts/composer.json +++ b/src/Symfony/Contracts/composer.json @@ -49,7 +49,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-main": "2.3-dev" + "dev-main": "2.4-dev" } } }