diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php
index 2788cf019f5be..1e52ce9322486 100644
--- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php
+++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php
@@ -17,10 +17,12 @@
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 @@ public function getConfigTreeBuilder()
$this->addLockSection($rootNode);
$this->addMessengerSection($rootNode);
$this->addRobotsIndexSection($rootNode);
+ $this->addHttpClientSection($rootNode);
return $treeBuilder;
}
@@ -1170,4 +1173,151 @@ private function addRobotsIndexSection(ArrayNodeDefinition $rootNode)
->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 391408a348f4c..673c89f9fe16a 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\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\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\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 @@ public function load(array $configs, ContainerBuilder $container)
$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 @@ private function registerCacheConfiguration(array $config, ContainerBuilder $con
}
}
+ 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 0000000000000..c21d115828fa5
--- /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 4c48fe0a58819..c1242a1e08a92 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 589ddf50a63fe..56be70050ccf5 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\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_exists(SemaphoreStore::class) && SemaphoreStore::isSupported() ? 'semaphor
'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 0000000000000..bd36ab1f03d15
--- /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 0000000000000..59e7f85d03c23
--- /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 0000000000000..5482f2903e6c9
--- /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 0000000000000..5a16c54914c3a
--- /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 0000000000000..6f889ba6e8715
--- /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 0000000000000..33c201ef9f6e1
--- /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 0000000000000..4abf1b897380d
--- /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 0000000000000..3d18286820e05
--- /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 0000000000000..37516441720a3
--- /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 1bbd048319b5a..6a98f7c1841ee 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\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 @@ public function testRobotsTagListenerIsRegisteredInDebugMode()
$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 cdf40d12a21f1..06d16c65d46fd 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 f5b81485292ee..5cbb839ead3e4 100644
--- a/src/Symfony/Component/HttpClient/CurlHttpClient.php
+++ b/src/Symfony/Component/HttpClient/CurlHttpClient.php
@@ -53,7 +53,7 @@ public function __construct(array $defaultOptions = [], int $maxHostConnections
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 223eba3e010fc..cbc08f40e2788 100644
--- a/src/Symfony/Component/HttpClient/HttpClientTrait.php
+++ b/src/Symfony/Component/HttpClient/HttpClientTrait.php
@@ -141,7 +141,9 @@ private static function mergeDefaultOptions(array $options, array $defaultOption
// 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 afd8fbd0897b2..bea3fe755b068 100644
--- a/src/Symfony/Component/HttpClient/NativeHttpClient.php
+++ b/src/Symfony/Component/HttpClient/NativeHttpClient.php
@@ -52,7 +52,7 @@ public function __construct(array $defaultOptions = [], int $maxHostConnections
'openHandles' => [],
'handlesActivity' => [],
'pendingResponses' => [],
- 'maxHostConnections' => $maxHostConnections,
+ 'maxHostConnections' => 0 < $maxHostConnections ? $maxHostConnections : PHP_INT_MAX,
'responseCount' => 0,
'dnsCache' => [],
'handles' => [],