diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php index 2788cf019f..1e52ce9322 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -17,10 +17,12 @@ use Doctrine\DBAL\Connection; use Symfony\Bundle\FullStack; use Symfony\Component\Asset\Package; use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; +use Symfony\Component\Config\Definition\Builder\NodeBuilder; use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; use Symfony\Component\DependencyInjection\Exception\LogicException; use Symfony\Component\Form\Form; +use Symfony\Component\HttpClient\HttpClient; use Symfony\Component\HttpFoundation\Cookie; use Symfony\Component\Lock\Lock; use Symfony\Component\Lock\Store\SemaphoreStore; @@ -109,6 +111,7 @@ class Configuration implements ConfigurationInterface $this->addLockSection($rootNode); $this->addMessengerSection($rootNode); $this->addRobotsIndexSection($rootNode); + $this->addHttpClientSection($rootNode); return $treeBuilder; } @@ -1170,4 +1173,151 @@ class Configuration implements ConfigurationInterface ->end() ; } + + private function addHttpClientSection(ArrayNodeDefinition $rootNode) + { + $subNode = $rootNode + ->children() + ->arrayNode('http_client') + ->info('HTTP Client configuration') + ->{!class_exists(FullStack::class) && class_exists(HttpClient::class) ? 'canBeDisabled' : 'canBeEnabled'}() + ->fixXmlConfig('client') + ->children(); + + $this->addHttpClientOptionsSection($subNode); + + $subNode = $subNode + ->arrayNode('clients') + ->useAttributeAsKey('name') + ->normalizeKeys(false) + ->arrayPrototype() + ->children(); + + $this->addHttpClientOptionsSection($subNode); + + $subNode = $subNode + ->end() + ->end() + ->end() + ->end() + ->end() + ->end() + ; + } + + private function addHttpClientOptionsSection(NodeBuilder $rootNode) + { + $rootNode + ->integerNode('max_host_connections') + ->info('The maximum number of connections to a single host.') + ->end() + ->arrayNode('default_options') + ->fixXmlConfig('header') + ->children() + ->scalarNode('auth_basic') + ->info('An HTTP Basic authentication "username:password".') + ->end() + ->scalarNode('auth_bearer') + ->info('A token enabling HTTP Bearer authorization.') + ->end() + ->arrayNode('query') + ->info('Associative array of query string values merged with URL parameters.') + ->useAttributeAsKey('key') + ->beforeNormalization() + ->always(function ($config) { + if (!\is_array($config)) { + return []; + } + if (!isset($config['key'])) { + return $config; + } + + return [$config['key'] => $config['value']]; + }) + ->end() + ->normalizeKeys(false) + ->scalarPrototype()->end() + ->end() + ->arrayNode('headers') + ->info('Associative array: header => value(s).') + ->useAttributeAsKey('name') + ->normalizeKeys(false) + ->variablePrototype()->end() + ->end() + ->integerNode('max_redirects') + ->info('The maximum number of redirects to follow.') + ->end() + ->scalarNode('http_version') + ->info('The default HTTP version, typically 1.1 or 2.0. Leave to null for the best version.') + ->end() + ->scalarNode('base_uri') + ->info('The URI to resolve relative URLs, following rules in RFC 3986, section 2.') + ->end() + ->arrayNode('resolve') + ->info('Associative array: domain => IP.') + ->useAttributeAsKey('host') + ->beforeNormalization() + ->always(function ($config) { + if (!\is_array($config)) { + return []; + } + if (!isset($config['host'])) { + return $config; + } + + return [$config['host'] => $config['value']]; + }) + ->end() + ->normalizeKeys(false) + ->scalarPrototype()->end() + ->end() + ->scalarNode('proxy') + ->info('The URL of the proxy to pass requests through or null for automatic detection.') + ->end() + ->scalarNode('no_proxy') + ->info('A comma separated list of hosts that do not require a proxy to be reached.') + ->end() + ->floatNode('timeout') + ->info('Defaults to "default_socket_timeout" ini parameter.') + ->end() + ->scalarNode('bindto') + ->info('A network interface name, IP address, a host name or a UNIX socket to bind to.') + ->end() + ->booleanNode('verify_peer') + ->info('Indicates if the peer should be verified in a SSL/TLS context.') + ->end() + ->booleanNode('verify_host') + ->info('Indicates if the host should exist as a certificate common name.') + ->end() + ->scalarNode('cafile') + ->info('A certificate authority file.') + ->end() + ->scalarNode('capath') + ->info('A directory that contains multiple certificate authority files.') + ->end() + ->scalarNode('local_cert') + ->info('A PEM formatted certificate file.') + ->end() + ->scalarNode('local_pk') + ->info('A private key file.') + ->end() + ->scalarNode('passphrase') + ->info('The passphrase used to encrypt the "local_pk" file.') + ->end() + ->scalarNode('ciphers') + ->info('A list of SSL/TLS ciphers separated by colons, commas or spaces (e.g. "RC4-SHA:TLS13-AES-128-GCM-SHA256"...)') + ->end() + ->arrayNode('peer_fingerprint') + ->info('Associative array: hashing algorithm => hash(es).') + ->normalizeKeys(false) + ->children() + ->variableNode('sha1')->end() + ->variableNode('pin-sha256')->end() + ->variableNode('md5')->end() + ->end() + ->end() + ->end() + ->end() + ; + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 391408a348..673c89f9fe 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -15,6 +15,7 @@ use Doctrine\Common\Annotations\AnnotationRegistry; use Doctrine\Common\Annotations\Reader; use Psr\Cache\CacheItemPoolInterface; use Psr\Container\ContainerInterface as PsrContainerInterface; +use Psr\Http\Client\ClientInterface; use Psr\Log\LoggerAwareInterface; use Symfony\Bridge\Monolog\Processor\DebugProcessor; use Symfony\Bridge\Twig\Extension\CsrfExtension; @@ -57,6 +58,9 @@ use Symfony\Component\Finder\Finder; use Symfony\Component\Form\FormTypeExtensionInterface; use Symfony\Component\Form\FormTypeGuesserInterface; use Symfony\Component\Form\FormTypeInterface; +use Symfony\Component\HttpClient\HttpClient; +use Symfony\Component\HttpClient\HttpClientTrait; +use Symfony\Component\HttpClient\Psr18Client; use Symfony\Component\HttpKernel\CacheClearer\CacheClearerInterface; use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface; use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface; @@ -110,6 +114,8 @@ use Symfony\Component\Workflow\WorkflowInterface; use Symfony\Component\Yaml\Command\LintCommand as BaseYamlLintCommand; use Symfony\Component\Yaml\Yaml; use Symfony\Contracts\Cache\CacheInterface; +use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; +use Symfony\Contracts\HttpClient\HttpClientInterface; use Symfony\Contracts\Service\ResetInterface; use Symfony\Contracts\Service\ServiceSubscriberInterface; @@ -301,6 +307,10 @@ class FrameworkExtension extends Extension $this->registerLockConfiguration($config['lock'], $container, $loader); } + if ($this->isConfigEnabled($container, $config['http_client'])) { + $this->registerHttpClientConfiguration($config['http_client'], $container, $loader); + } + if ($this->isConfigEnabled($container, $config['web_link'])) { if (!class_exists(HttpHeaderSerializer::class)) { throw new LogicException('WebLink support cannot be enabled as the WebLink component is not installed. Try running "composer require symfony/weblink".'); @@ -1747,6 +1757,63 @@ class FrameworkExtension extends Extension } } + private function registerHttpClientConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader) + { + if (!class_exists(HttpClient::class)) { + throw new LogicException('HttpClient support cannot be enabled as the component is not installed. Try running "composer require symfony/http-client".'); + } + + $loader->load('http_client.xml'); + + $merger = new class() { + use HttpClientTrait; + + public function merge(array $options, array $defaultOptions) + { + try { + [, $mergedOptions] = $this->prepareRequest(null, null, $options, $defaultOptions); + + foreach ($mergedOptions as $k => $v) { + if (!isset($options[$k]) && !isset($defaultOptions[$k])) { + // Remove options added by prepareRequest() + unset($mergedOptions[$k]); + } + } + + return $mergedOptions; + } catch (TransportExceptionInterface $e) { + throw new InvalidArgumentException($e->getMessage(), 0, $e); + } + } + }; + + $defaultOptions = $merger->merge($config['default_options'] ?? [], []); + $container->getDefinition('http_client')->setArguments([$defaultOptions, $config['max_host_connections'] ?? 6]); + + if (!$hasPsr18 = interface_exists(ClientInterface::class)) { + $container->removeDefinition('psr18.http_client'); + $container->removeAlias(ClientInterface::class); + } + + foreach ($config['clients'] as $name => $clientConfig) { + $options = $merger->merge($clientConfig['default_options'] ?? [], $defaultOptions); + + $container->register($name, HttpClientInterface::class) + ->setFactory([HttpClient::class, 'create']) + ->setArguments([$options, $clientConfig['max_host_connections'] ?? $config['max_host_connections'] ?? 6]); + + $container->registerAliasForArgument($name, HttpClientInterface::class); + + if ($hasPsr18) { + $container->register('psr18.'.$name, Psr18Client::class) + ->setAutowired(true) + ->setArguments([new Reference($name)]); + + $container->registerAliasForArgument('psr18.'.$name, ClientInterface::class, $name); + } + } + } + /** * Returns the base path for the XSD files. * diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/http_client.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/http_client.xml new file mode 100644 index 0000000000..c21d115828 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/http_client.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd index 4c48fe0a58..c1242a1e08 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd @@ -32,6 +32,7 @@ + @@ -444,4 +445,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php index 589ddf50a6..56be70050c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php @@ -17,6 +17,7 @@ use Symfony\Bundle\FrameworkBundle\DependencyInjection\Configuration; use Symfony\Bundle\FullStack; use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; use Symfony\Component\Config\Definition\Processor; +use Symfony\Component\HttpClient\HttpClient; use Symfony\Component\Lock\Store\SemaphoreStore; use Symfony\Component\Messenger\MessageBusInterface; @@ -331,6 +332,10 @@ class ConfigurationTest extends TestCase 'buses' => ['messenger.bus.default' => ['default_middleware' => true, 'middleware' => []]], ], 'disallow_search_engine_index' => true, + 'http_client' => [ + 'enabled' => !class_exists(FullStack::class) && class_exists(HttpClient::class), + 'clients' => [], + ], ]; } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/http_client_default_options.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/http_client_default_options.php new file mode 100644 index 0000000000..bd36ab1f03 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/http_client_default_options.php @@ -0,0 +1,13 @@ +loadFromExtension('framework', [ + 'http_client' => [ + 'max_host_connections' => 4, + 'default_options' => null, + 'clients' => [ + 'foo' => [ + 'default_options' => null, + ], + ], + ], +]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/http_client_full_default_options.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/http_client_full_default_options.php new file mode 100644 index 0000000000..59e7f85d03 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/http_client_full_default_options.php @@ -0,0 +1,30 @@ +loadFromExtension('framework', [ + 'http_client' => [ + 'default_options' => [ + 'auth_basic' => 'foo:bar', + 'query' => ['foo' => 'bar', 'bar' => 'baz'], + 'headers' => ['X-powered' => 'PHP'], + 'max_redirects' => 2, + 'http_version' => '2.0', + 'base_uri' => 'http://example.com', + 'resolve' => ['localhost' => '127.0.0.1'], + 'proxy' => 'proxy.org', + 'timeout' => 3.5, + 'bindto' => '127.0.0.1', + 'verify_peer' => true, + 'verify_host' => true, + 'cafile' => '/etc/ssl/cafile', + 'capath' => '/etc/ssl', + 'local_cert' => '/etc/ssl/cert.pem', + 'local_pk' => '/etc/ssl/private_key.pem', + 'passphrase' => 'password123456', + 'ciphers' => 'RC4-SHA:TLS13-AES-128-GCM-SHA256', + 'peer_fingerprint' => [ + 'pin-sha256' => ['14s5erg62v1v8471g2revg48r7==', 'jsda84hjtyd4821bgfesd215bsfg5412='], + 'md5' => 'sdhtb481248721thbr=', + ], + ], + ], +]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/http_client_override_default_options.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/http_client_override_default_options.php new file mode 100644 index 0000000000..5482f2903e --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/http_client_override_default_options.php @@ -0,0 +1,16 @@ +loadFromExtension('framework', [ + 'http_client' => [ + 'default_options' => [ + 'headers' => ['foo' => 'bar'], + ], + 'clients' => [ + 'foo' => [ + 'default_options' => [ + 'headers' => ['bar' => 'baz'], + ], + ], + ], + ], +]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/http_client_default_options.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/http_client_default_options.xml new file mode 100644 index 0000000000..5a16c54914 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/http_client_default_options.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/http_client_full_default_options.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/http_client_full_default_options.xml new file mode 100644 index 0000000000..6f889ba6e8 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/http_client_full_default_options.xml @@ -0,0 +1,39 @@ + + + + + + + bar + baz + PHP + 127.0.0.1 + + 14s5erg62v1v8471g2revg48r7== + jsda84hjtyd4821bgfesd215bsfg5412= + sdhtb481248721thbr= + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/http_client_override_default_options.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/http_client_override_default_options.xml new file mode 100644 index 0000000000..33c201ef9f --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/http_client_override_default_options.xml @@ -0,0 +1,20 @@ + + + + + + + bar + + + + baz + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/http_client_default_options.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/http_client_default_options.yml new file mode 100644 index 0000000000..4abf1b8973 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/http_client_default_options.yml @@ -0,0 +1,7 @@ +framework: + http_client: + max_host_connections: 4 + default_options: ~ + clients: + foo: + default_options: ~ diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/http_client_full_default_options.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/http_client_full_default_options.yml new file mode 100644 index 0000000000..3d18286820 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/http_client_full_default_options.yml @@ -0,0 +1,25 @@ +framework: + http_client: + default_options: + auth_basic: foo:bar + query: {'foo': 'bar', 'bar': 'baz'} + headers: + X-powered: PHP + max_redirects: 2 + http_version: 2.0 + base_uri: 'http://example.com' + resolve: {'localhost': '127.0.0.1'} + proxy: proxy.org + timeout: 3.5 + bindto: 127.0.0.1 + verify_peer: true + verify_host: true + cafile: /etc/ssl/cafile + capath: /etc/ssl + local_cert: /etc/ssl/cert.pem + local_pk: /etc/ssl/private_key.pem + passphrase: password123456 + ciphers: 'RC4-SHA:TLS13-AES-128-GCM-SHA256' + peer_fingerprint: + pin-sha256: ['14s5erg62v1v8471g2revg48r7==', 'jsda84hjtyd4821bgfesd215bsfg5412='] + md5: 'sdhtb481248721thbr=' diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/http_client_override_default_options.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/http_client_override_default_options.yml new file mode 100644 index 0000000000..3751644172 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/http_client_override_default_options.yml @@ -0,0 +1,8 @@ +framework: + http_client: + default_options: + headers: {'foo': 'bar'} + clients: + foo: + default_options: + headers: {'bar': 'baz'} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php index 1bbd048319..6a98f7c184 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php @@ -51,6 +51,7 @@ use Symfony\Component\Serializer\Serializer; use Symfony\Component\Translation\DependencyInjection\TranslatorPass; use Symfony\Component\Validator\DependencyInjection\AddConstraintValidatorsPass; use Symfony\Component\Workflow; +use Symfony\Contracts\HttpClient\HttpClientInterface; abstract class FrameworkExtensionTest extends TestCase { @@ -1353,6 +1354,61 @@ abstract class FrameworkExtensionTest extends TestCase $this->assertFalse($container->has('disallow_search_engine_index_response_listener'), 'DisallowRobotsIndexingListener should NOT be registered'); } + public function testHttpClientDefaultOptions() + { + $container = $this->createContainerFromFile('http_client_default_options'); + $this->assertTrue($container->hasDefinition('http_client'), '->registerHttpClientConfiguration() loads http_client.xml'); + + $defaultOptions = [ + 'query' => [], + 'headers' => [], + 'resolve' => [], + ]; + $this->assertSame([$defaultOptions, 4], $container->getDefinition('http_client')->getArguments()); + + $this->assertTrue($container->hasDefinition('foo'), 'should have the "foo" service.'); + $this->assertSame(HttpClientInterface::class, $container->getDefinition('foo')->getClass()); + $this->assertSame([$defaultOptions, 4], $container->getDefinition('foo')->getArguments()); + } + + public function testHttpClientOverrideDefaultOptions() + { + $container = $this->createContainerFromFile('http_client_override_default_options'); + + $this->assertSame(['foo' => ['bar']], $container->getDefinition('http_client')->getArguments()[0]['headers']); + $this->assertSame(['bar' => ['baz'], 'foo' => ['bar']], $container->getDefinition('foo')->getArguments()[0]['headers']); + } + + public function testHttpClientFullDefaultOptions() + { + $container = $this->createContainerFromFile('http_client_full_default_options'); + + $defaultOptions = $container->getDefinition('http_client')->getArguments()[0]; + + $this->assertSame('foo:bar', $defaultOptions['auth_basic']); + $this->assertSame(['foo' => 'bar', 'bar' => 'baz'], $defaultOptions['query']); + $this->assertSame(['x-powered' => ['PHP']], $defaultOptions['headers']); + $this->assertSame(2, $defaultOptions['max_redirects']); + $this->assertSame(2.0, (float) $defaultOptions['http_version']); + $this->assertSame('http://example.com', $defaultOptions['base_uri']); + $this->assertSame(['localhost' => '127.0.0.1'], $defaultOptions['resolve']); + $this->assertSame('proxy.org', $defaultOptions['proxy']); + $this->assertSame(3.5, $defaultOptions['timeout']); + $this->assertSame('127.0.0.1', $defaultOptions['bindto']); + $this->assertTrue($defaultOptions['verify_peer']); + $this->assertTrue($defaultOptions['verify_host']); + $this->assertSame('/etc/ssl/cafile', $defaultOptions['cafile']); + $this->assertSame('/etc/ssl', $defaultOptions['capath']); + $this->assertSame('/etc/ssl/cert.pem', $defaultOptions['local_cert']); + $this->assertSame('/etc/ssl/private_key.pem', $defaultOptions['local_pk']); + $this->assertSame('password123456', $defaultOptions['passphrase']); + $this->assertSame('RC4-SHA:TLS13-AES-128-GCM-SHA256', $defaultOptions['ciphers']); + $this->assertSame([ + 'pin-sha256' => ['14s5erg62v1v8471g2revg48r7==', 'jsda84hjtyd4821bgfesd215bsfg5412='], + 'md5' => 'sdhtb481248721thbr=', + ], $defaultOptions['peer_fingerprint']); + } + protected function createContainer(array $data = []) { return new ContainerBuilder(new ParameterBag(array_merge([ diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json index cdf40d12a2..06d16c65d4 100644 --- a/src/Symfony/Bundle/FrameworkBundle/composer.json +++ b/src/Symfony/Bundle/FrameworkBundle/composer.json @@ -42,6 +42,7 @@ "symfony/security": "~3.4|~4.0", "symfony/form": "^4.3", "symfony/expression-language": "~3.4|~4.0", + "symfony/http-client": "^4.3", "symfony/messenger": "^4.2", "symfony/mime": "^4.3", "symfony/process": "~3.4|~4.0", diff --git a/src/Symfony/Component/HttpClient/CurlHttpClient.php b/src/Symfony/Component/HttpClient/CurlHttpClient.php index f5b8148529..5cbb839ead 100644 --- a/src/Symfony/Component/HttpClient/CurlHttpClient.php +++ b/src/Symfony/Component/HttpClient/CurlHttpClient.php @@ -53,7 +53,7 @@ final class CurlHttpClient implements HttpClientInterface if (\defined('CURLPIPE_MULTIPLEX')) { curl_multi_setopt($mh, CURLMOPT_PIPELINING, CURLPIPE_MULTIPLEX); } - curl_multi_setopt($mh, CURLMOPT_MAX_HOST_CONNECTIONS, $maxHostConnections); + curl_multi_setopt($mh, CURLMOPT_MAX_HOST_CONNECTIONS, 0 < $maxHostConnections ? $maxHostConnections : PHP_INT_MAX); // Use an internal stdClass object to share state between the client and its responses $this->multi = $multi = (object) [ diff --git a/src/Symfony/Component/HttpClient/HttpClientTrait.php b/src/Symfony/Component/HttpClient/HttpClientTrait.php index 223eba3e01..cbc08f40e2 100644 --- a/src/Symfony/Component/HttpClient/HttpClientTrait.php +++ b/src/Symfony/Component/HttpClient/HttpClientTrait.php @@ -141,7 +141,9 @@ trait HttpClientTrait // Option "query" is never inherited from defaults $options['query'] = $options['query'] ?? []; - $options += $defaultOptions; + foreach ($defaultOptions as $k => $v) { + $options[$k] = $options[$k] ?? $v; + } if ($defaultOptions['resolve'] ?? false) { $options['resolve'] += array_change_key_case($defaultOptions['resolve']); diff --git a/src/Symfony/Component/HttpClient/NativeHttpClient.php b/src/Symfony/Component/HttpClient/NativeHttpClient.php index afd8fbd089..bea3fe755b 100644 --- a/src/Symfony/Component/HttpClient/NativeHttpClient.php +++ b/src/Symfony/Component/HttpClient/NativeHttpClient.php @@ -52,7 +52,7 @@ final class NativeHttpClient implements HttpClientInterface 'openHandles' => [], 'handlesActivity' => [], 'pendingResponses' => [], - 'maxHostConnections' => $maxHostConnections, + 'maxHostConnections' => 0 < $maxHostConnections ? $maxHostConnections : PHP_INT_MAX, 'responseCount' => 0, 'dnsCache' => [], 'handles' => [],