feature #30419 [FrameworkBundle] Add integration of http-client component (Ioni14, nicoweb)
This PR was merged into the 4.3-dev branch. Discussion ---------- [FrameworkBundle] Add integration of http-client component | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | - | License | MIT | Doc PR | - This PR adds the integration of the HttpClient component on FrameworkBundle. By default, two services are provided, one implementing SFC-HttpClient, and another PSR18: * `http_client` + its autowiring alias for `Symfony\Contracts\HttpClient\HttpClientInterface`) This service is automatically set to the best HTTP client available with the configuration given under the `framework.http_client` key. * `psr18.http_client` + its autowiring alias for `Psr\Http\Client\ClientInterface`). To make it work, one needs to provide autowiring aliases for `ResponseFactoryInterface` and `StreamFactoryInterface`, which are provided by [the recipe](https://github.com/symfony/recipes-contrib/blob/master/nyholm/psr7/1.0/config/packages/nyholm_psr7.yaml) for `nyholm/psr7` (but could be overriden by apps when using something else). * one can also configure the default options, and "scoped" clients. For example: ```yaml http_client: default_options: capath: '...' clients: github_client: default_options: base_uri: 'https://api.github.com' ``` This definition create a `github_client` service implementing SFC-HttpClient and a `psr18.github_client` one implementing PSR18, +2 corresponding named autowiring aliases: `HttpClientInterface $githubClient`, and `ClientInterface $githubClient`. Commits -------f2d2cf3021
work with attributes for xml config0023a71260
[FrameworkBundle] Add integration of http-client component
This commit is contained in:
commit
401c1d3d75
@ -17,10 +17,12 @@ use Doctrine\DBAL\Connection;
|
|||||||
use Symfony\Bundle\FullStack;
|
use Symfony\Bundle\FullStack;
|
||||||
use Symfony\Component\Asset\Package;
|
use Symfony\Component\Asset\Package;
|
||||||
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
|
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\Builder\TreeBuilder;
|
||||||
use Symfony\Component\Config\Definition\ConfigurationInterface;
|
use Symfony\Component\Config\Definition\ConfigurationInterface;
|
||||||
use Symfony\Component\DependencyInjection\Exception\LogicException;
|
use Symfony\Component\DependencyInjection\Exception\LogicException;
|
||||||
use Symfony\Component\Form\Form;
|
use Symfony\Component\Form\Form;
|
||||||
|
use Symfony\Component\HttpClient\HttpClient;
|
||||||
use Symfony\Component\HttpFoundation\Cookie;
|
use Symfony\Component\HttpFoundation\Cookie;
|
||||||
use Symfony\Component\Lock\Lock;
|
use Symfony\Component\Lock\Lock;
|
||||||
use Symfony\Component\Lock\Store\SemaphoreStore;
|
use Symfony\Component\Lock\Store\SemaphoreStore;
|
||||||
@ -109,6 +111,7 @@ class Configuration implements ConfigurationInterface
|
|||||||
$this->addLockSection($rootNode);
|
$this->addLockSection($rootNode);
|
||||||
$this->addMessengerSection($rootNode);
|
$this->addMessengerSection($rootNode);
|
||||||
$this->addRobotsIndexSection($rootNode);
|
$this->addRobotsIndexSection($rootNode);
|
||||||
|
$this->addHttpClientSection($rootNode);
|
||||||
|
|
||||||
return $treeBuilder;
|
return $treeBuilder;
|
||||||
}
|
}
|
||||||
@ -1170,4 +1173,151 @@ class Configuration implements ConfigurationInterface
|
|||||||
->end()
|
->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()
|
||||||
|
;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,7 @@ use Doctrine\Common\Annotations\AnnotationRegistry;
|
|||||||
use Doctrine\Common\Annotations\Reader;
|
use Doctrine\Common\Annotations\Reader;
|
||||||
use Psr\Cache\CacheItemPoolInterface;
|
use Psr\Cache\CacheItemPoolInterface;
|
||||||
use Psr\Container\ContainerInterface as PsrContainerInterface;
|
use Psr\Container\ContainerInterface as PsrContainerInterface;
|
||||||
|
use Psr\Http\Client\ClientInterface;
|
||||||
use Psr\Log\LoggerAwareInterface;
|
use Psr\Log\LoggerAwareInterface;
|
||||||
use Symfony\Bridge\Monolog\Processor\DebugProcessor;
|
use Symfony\Bridge\Monolog\Processor\DebugProcessor;
|
||||||
use Symfony\Bridge\Twig\Extension\CsrfExtension;
|
use Symfony\Bridge\Twig\Extension\CsrfExtension;
|
||||||
@ -57,6 +58,9 @@ use Symfony\Component\Finder\Finder;
|
|||||||
use Symfony\Component\Form\FormTypeExtensionInterface;
|
use Symfony\Component\Form\FormTypeExtensionInterface;
|
||||||
use Symfony\Component\Form\FormTypeGuesserInterface;
|
use Symfony\Component\Form\FormTypeGuesserInterface;
|
||||||
use Symfony\Component\Form\FormTypeInterface;
|
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\CacheClearer\CacheClearerInterface;
|
||||||
use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface;
|
use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface;
|
||||||
use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface;
|
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\Command\LintCommand as BaseYamlLintCommand;
|
||||||
use Symfony\Component\Yaml\Yaml;
|
use Symfony\Component\Yaml\Yaml;
|
||||||
use Symfony\Contracts\Cache\CacheInterface;
|
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\ResetInterface;
|
||||||
use Symfony\Contracts\Service\ServiceSubscriberInterface;
|
use Symfony\Contracts\Service\ServiceSubscriberInterface;
|
||||||
|
|
||||||
@ -301,6 +307,10 @@ class FrameworkExtension extends Extension
|
|||||||
$this->registerLockConfiguration($config['lock'], $container, $loader);
|
$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 ($this->isConfigEnabled($container, $config['web_link'])) {
|
||||||
if (!class_exists(HttpHeaderSerializer::class)) {
|
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".');
|
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.
|
* Returns the base path for the XSD files.
|
||||||
*
|
*
|
||||||
|
@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0" ?>
|
||||||
|
|
||||||
|
<container xmlns="http://symfony.com/schema/dic/services"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
|
||||||
|
|
||||||
|
<services>
|
||||||
|
<service id="http_client" class="Symfony\Contracts\HttpClient\HttpClientInterface">
|
||||||
|
<factory class="Symfony\Component\HttpClient\HttpClient" method="create" />
|
||||||
|
<argument type="collection" /> <!-- default options -->
|
||||||
|
<argument /> <!-- max host connections -->
|
||||||
|
</service>
|
||||||
|
<service id="Symfony\Contracts\HttpClient\HttpClientInterface" alias="http_client" />
|
||||||
|
|
||||||
|
<service id="psr18.http_client" class="Symfony\Component\HttpClient\Psr18Client" autowire="true">
|
||||||
|
<argument type="service" id="http_client" />
|
||||||
|
</service>
|
||||||
|
<service id="Psr\Http\Client\ClientInterface" alias="psr18.http_client" />
|
||||||
|
</services>
|
||||||
|
</container>
|
@ -32,6 +32,7 @@
|
|||||||
<xsd:element name="php-errors" type="php-errors" minOccurs="0" maxOccurs="1" />
|
<xsd:element name="php-errors" type="php-errors" minOccurs="0" maxOccurs="1" />
|
||||||
<xsd:element name="lock" type="lock" minOccurs="0" maxOccurs="1" />
|
<xsd:element name="lock" type="lock" minOccurs="0" maxOccurs="1" />
|
||||||
<xsd:element name="messenger" type="messenger" minOccurs="0" maxOccurs="1" />
|
<xsd:element name="messenger" type="messenger" minOccurs="0" maxOccurs="1" />
|
||||||
|
<xsd:element name="http-client" type="http_client" minOccurs="0" maxOccurs="1" />
|
||||||
</xsd:choice>
|
</xsd:choice>
|
||||||
|
|
||||||
<xsd:attribute name="http-method-override" type="xsd:boolean" />
|
<xsd:attribute name="http-method-override" type="xsd:boolean" />
|
||||||
@ -444,4 +445,66 @@
|
|||||||
</xsd:sequence>
|
</xsd:sequence>
|
||||||
<xsd:attribute name="id" type="xsd:string" use="required"/>
|
<xsd:attribute name="id" type="xsd:string" use="required"/>
|
||||||
</xsd:complexType>
|
</xsd:complexType>
|
||||||
|
|
||||||
|
<xsd:complexType name="http_client">
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="default-options" type="http_client_options" minOccurs="0" />
|
||||||
|
<xsd:element name="client" type="http_client_client" minOccurs="0" maxOccurs="unbounded" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="enabled" type="xsd:boolean" />
|
||||||
|
<xsd:attribute name="max-host-connections" type="xsd:integer" />
|
||||||
|
</xsd:complexType>
|
||||||
|
|
||||||
|
<xsd:complexType name="http_client_options" mixed="true">
|
||||||
|
<xsd:choice maxOccurs="unbounded">
|
||||||
|
<xsd:element name="query" type="http_query" minOccurs="0" maxOccurs="unbounded" />
|
||||||
|
<xsd:element name="resolve" type="http_resolve" minOccurs="0" maxOccurs="unbounded" />
|
||||||
|
<xsd:element name="header" type="http_header" minOccurs="0" maxOccurs="unbounded" />
|
||||||
|
<xsd:element name="peer-fingerprint" type="fingerprint" minOccurs="0" maxOccurs="unbounded" />
|
||||||
|
</xsd:choice>
|
||||||
|
<xsd:attribute name="proxy" type="xsd:string" />
|
||||||
|
<xsd:attribute name="timeout" type="xsd:float" />
|
||||||
|
<xsd:attribute name="bindto" type="xsd:string" />
|
||||||
|
<xsd:attribute name="verify-peer" type="xsd:boolean" />
|
||||||
|
<xsd:attribute name="auth-basic" type="xsd:string" />
|
||||||
|
<xsd:attribute name="auth-bearer" type="xsd:string" />
|
||||||
|
<xsd:attribute name="max-redirects" type="xsd:integer" />
|
||||||
|
<xsd:attribute name="http-version" type="xsd:string" />
|
||||||
|
<xsd:attribute name="base-uri" type="xsd:string" />
|
||||||
|
<xsd:attribute name="no-proxy" type="xsd:string" />
|
||||||
|
<xsd:attribute name="verify-host" type="xsd:boolean" />
|
||||||
|
<xsd:attribute name="cafile" type="xsd:string" />
|
||||||
|
<xsd:attribute name="capath" type="xsd:string" />
|
||||||
|
<xsd:attribute name="local-cert" type="xsd:string" />
|
||||||
|
<xsd:attribute name="local-pk" type="xsd:string" />
|
||||||
|
<xsd:attribute name="passphrase" type="xsd:string" />
|
||||||
|
<xsd:attribute name="ciphers" type="xsd:string" />
|
||||||
|
</xsd:complexType>
|
||||||
|
|
||||||
|
<xsd:complexType name="http_client_client">
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="default-options" type="http_client_options" minOccurs="0" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" type="xsd:string" />
|
||||||
|
</xsd:complexType>
|
||||||
|
|
||||||
|
<xsd:complexType name="fingerprint">
|
||||||
|
<xsd:choice maxOccurs="unbounded">
|
||||||
|
<xsd:element name="pin-sha256" type="xsd:string" minOccurs="0" />
|
||||||
|
<xsd:element name="sha1" type="xsd:string" minOccurs="0" />
|
||||||
|
<xsd:element name="md5" type="xsd:string" minOccurs="0" />
|
||||||
|
</xsd:choice>
|
||||||
|
</xsd:complexType>
|
||||||
|
|
||||||
|
<xsd:complexType name="http_query" mixed="true">
|
||||||
|
<xsd:attribute name="key" type="xsd:string" />
|
||||||
|
</xsd:complexType>
|
||||||
|
|
||||||
|
<xsd:complexType name="http_resolve" mixed="true">
|
||||||
|
<xsd:attribute name="host" type="xsd:string" />
|
||||||
|
</xsd:complexType>
|
||||||
|
|
||||||
|
<xsd:complexType name="http_header" mixed="true">
|
||||||
|
<xsd:attribute name="name" type="xsd:string" />
|
||||||
|
</xsd:complexType>
|
||||||
</xsd:schema>
|
</xsd:schema>
|
||||||
|
@ -17,6 +17,7 @@ use Symfony\Bundle\FrameworkBundle\DependencyInjection\Configuration;
|
|||||||
use Symfony\Bundle\FullStack;
|
use Symfony\Bundle\FullStack;
|
||||||
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
|
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
|
||||||
use Symfony\Component\Config\Definition\Processor;
|
use Symfony\Component\Config\Definition\Processor;
|
||||||
|
use Symfony\Component\HttpClient\HttpClient;
|
||||||
use Symfony\Component\Lock\Store\SemaphoreStore;
|
use Symfony\Component\Lock\Store\SemaphoreStore;
|
||||||
use Symfony\Component\Messenger\MessageBusInterface;
|
use Symfony\Component\Messenger\MessageBusInterface;
|
||||||
|
|
||||||
@ -331,6 +332,10 @@ class ConfigurationTest extends TestCase
|
|||||||
'buses' => ['messenger.bus.default' => ['default_middleware' => true, 'middleware' => []]],
|
'buses' => ['messenger.bus.default' => ['default_middleware' => true, 'middleware' => []]],
|
||||||
],
|
],
|
||||||
'disallow_search_engine_index' => true,
|
'disallow_search_engine_index' => true,
|
||||||
|
'http_client' => [
|
||||||
|
'enabled' => !class_exists(FullStack::class) && class_exists(HttpClient::class),
|
||||||
|
'clients' => [],
|
||||||
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
$container->loadFromExtension('framework', [
|
||||||
|
'http_client' => [
|
||||||
|
'max_host_connections' => 4,
|
||||||
|
'default_options' => null,
|
||||||
|
'clients' => [
|
||||||
|
'foo' => [
|
||||||
|
'default_options' => null,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
$container->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=',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
$container->loadFromExtension('framework', [
|
||||||
|
'http_client' => [
|
||||||
|
'default_options' => [
|
||||||
|
'headers' => ['foo' => 'bar'],
|
||||||
|
],
|
||||||
|
'clients' => [
|
||||||
|
'foo' => [
|
||||||
|
'default_options' => [
|
||||||
|
'headers' => ['bar' => 'baz'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<container xmlns="http://symfony.com/schema/dic/services"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xmlns:framework="http://symfony.com/schema/dic/symfony"
|
||||||
|
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd
|
||||||
|
http://symfony.com/schema/dic/symfony http://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
|
||||||
|
|
||||||
|
<framework:config>
|
||||||
|
<framework:http-client max-host-connections="4">
|
||||||
|
<framework:default-options />
|
||||||
|
<framework:client name="foo">
|
||||||
|
<framework:default-options />
|
||||||
|
</framework:client>
|
||||||
|
</framework:http-client>
|
||||||
|
</framework:config>
|
||||||
|
</container>
|
@ -0,0 +1,39 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<container xmlns="http://symfony.com/schema/dic/services"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xmlns:framework="http://symfony.com/schema/dic/symfony"
|
||||||
|
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd
|
||||||
|
http://symfony.com/schema/dic/symfony http://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
|
||||||
|
|
||||||
|
<framework:config>
|
||||||
|
<framework:http-client>
|
||||||
|
<framework:default-options
|
||||||
|
proxy="proxy.org"
|
||||||
|
bindto="127.0.0.1"
|
||||||
|
timeout="3.5"
|
||||||
|
verify-peer="true"
|
||||||
|
auth-basic="foo:bar"
|
||||||
|
max-redirects="2"
|
||||||
|
http-version="2.0"
|
||||||
|
base-uri="http://example.com"
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<framework:query key="foo">bar</framework:query>
|
||||||
|
<framework:query key="bar">baz</framework:query>
|
||||||
|
<framework:header name="X-powered">PHP</framework:header>
|
||||||
|
<framework:resolve host="localhost">127.0.0.1</framework:resolve>
|
||||||
|
<framework:peer-fingerprint>
|
||||||
|
<framework:pin-sha256>14s5erg62v1v8471g2revg48r7==</framework:pin-sha256>
|
||||||
|
<framework:pin-sha256>jsda84hjtyd4821bgfesd215bsfg5412=</framework:pin-sha256>
|
||||||
|
<framework:md5>sdhtb481248721thbr=</framework:md5>
|
||||||
|
</framework:peer-fingerprint>
|
||||||
|
</framework:default-options>
|
||||||
|
</framework:http-client>
|
||||||
|
</framework:config>
|
||||||
|
</container>
|
@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<container xmlns="http://symfony.com/schema/dic/services"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xmlns:framework="http://symfony.com/schema/dic/symfony"
|
||||||
|
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd
|
||||||
|
http://symfony.com/schema/dic/symfony http://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
|
||||||
|
|
||||||
|
<framework:config>
|
||||||
|
<framework:http-client>
|
||||||
|
<framework:default-options>
|
||||||
|
<framework:header name="foo">bar</framework:header>
|
||||||
|
</framework:default-options>
|
||||||
|
<framework:client name="foo">
|
||||||
|
<framework:default-options>
|
||||||
|
<framework:header name="bar">baz</framework:header>
|
||||||
|
</framework:default-options>
|
||||||
|
</framework:client>
|
||||||
|
</framework:http-client>
|
||||||
|
</framework:config>
|
||||||
|
</container>
|
@ -0,0 +1,7 @@
|
|||||||
|
framework:
|
||||||
|
http_client:
|
||||||
|
max_host_connections: 4
|
||||||
|
default_options: ~
|
||||||
|
clients:
|
||||||
|
foo:
|
||||||
|
default_options: ~
|
@ -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='
|
@ -0,0 +1,8 @@
|
|||||||
|
framework:
|
||||||
|
http_client:
|
||||||
|
default_options:
|
||||||
|
headers: {'foo': 'bar'}
|
||||||
|
clients:
|
||||||
|
foo:
|
||||||
|
default_options:
|
||||||
|
headers: {'bar': 'baz'}
|
@ -51,6 +51,7 @@ use Symfony\Component\Serializer\Serializer;
|
|||||||
use Symfony\Component\Translation\DependencyInjection\TranslatorPass;
|
use Symfony\Component\Translation\DependencyInjection\TranslatorPass;
|
||||||
use Symfony\Component\Validator\DependencyInjection\AddConstraintValidatorsPass;
|
use Symfony\Component\Validator\DependencyInjection\AddConstraintValidatorsPass;
|
||||||
use Symfony\Component\Workflow;
|
use Symfony\Component\Workflow;
|
||||||
|
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||||
|
|
||||||
abstract class FrameworkExtensionTest extends TestCase
|
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');
|
$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 = [])
|
protected function createContainer(array $data = [])
|
||||||
{
|
{
|
||||||
return new ContainerBuilder(new ParameterBag(array_merge([
|
return new ContainerBuilder(new ParameterBag(array_merge([
|
||||||
|
@ -42,6 +42,7 @@
|
|||||||
"symfony/security": "~3.4|~4.0",
|
"symfony/security": "~3.4|~4.0",
|
||||||
"symfony/form": "^4.3",
|
"symfony/form": "^4.3",
|
||||||
"symfony/expression-language": "~3.4|~4.0",
|
"symfony/expression-language": "~3.4|~4.0",
|
||||||
|
"symfony/http-client": "^4.3",
|
||||||
"symfony/messenger": "^4.2",
|
"symfony/messenger": "^4.2",
|
||||||
"symfony/mime": "^4.3",
|
"symfony/mime": "^4.3",
|
||||||
"symfony/process": "~3.4|~4.0",
|
"symfony/process": "~3.4|~4.0",
|
||||||
|
@ -53,7 +53,7 @@ final class CurlHttpClient implements HttpClientInterface
|
|||||||
if (\defined('CURLPIPE_MULTIPLEX')) {
|
if (\defined('CURLPIPE_MULTIPLEX')) {
|
||||||
curl_multi_setopt($mh, CURLMOPT_PIPELINING, 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
|
// Use an internal stdClass object to share state between the client and its responses
|
||||||
$this->multi = $multi = (object) [
|
$this->multi = $multi = (object) [
|
||||||
|
@ -141,7 +141,9 @@ trait HttpClientTrait
|
|||||||
// Option "query" is never inherited from defaults
|
// Option "query" is never inherited from defaults
|
||||||
$options['query'] = $options['query'] ?? [];
|
$options['query'] = $options['query'] ?? [];
|
||||||
|
|
||||||
$options += $defaultOptions;
|
foreach ($defaultOptions as $k => $v) {
|
||||||
|
$options[$k] = $options[$k] ?? $v;
|
||||||
|
}
|
||||||
|
|
||||||
if ($defaultOptions['resolve'] ?? false) {
|
if ($defaultOptions['resolve'] ?? false) {
|
||||||
$options['resolve'] += array_change_key_case($defaultOptions['resolve']);
|
$options['resolve'] += array_change_key_case($defaultOptions['resolve']);
|
||||||
|
@ -52,7 +52,7 @@ final class NativeHttpClient implements HttpClientInterface
|
|||||||
'openHandles' => [],
|
'openHandles' => [],
|
||||||
'handlesActivity' => [],
|
'handlesActivity' => [],
|
||||||
'pendingResponses' => [],
|
'pendingResponses' => [],
|
||||||
'maxHostConnections' => $maxHostConnections,
|
'maxHostConnections' => 0 < $maxHostConnections ? $maxHostConnections : PHP_INT_MAX,
|
||||||
'responseCount' => 0,
|
'responseCount' => 0,
|
||||||
'dnsCache' => [],
|
'dnsCache' => [],
|
||||||
'handles' => [],
|
'handles' => [],
|
||||||
|
Reference in New Issue
Block a user