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 @@
+
+
+
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: