From 0023a712604decb5ce3de9fe17128c8e2b6f55cc Mon Sep 17 00:00:00 2001 From: Thomas Talbot Date: Fri, 22 Feb 2019 16:10:59 +0100 Subject: [PATCH 1/2] [FrameworkBundle] Add integration of http-client component --- .../DependencyInjection/Configuration.php | 122 ++++++++++++++++++ .../FrameworkExtension.php | 52 ++++++++ .../Resources/config/http_client.xml | 20 +++ .../Resources/config/schema/symfony-1.0.xsd | 61 +++++++++ .../DependencyInjection/ConfigurationTest.php | 5 + .../php/http_client_default_options.php | 13 ++ .../php/http_client_full_default_options.php | 31 +++++ .../http_client_override_default_options.php | 16 +++ .../xml/http_client_default_options.xml | 17 +++ .../xml/http_client_full_default_options.xml | 45 +++++++ .../http_client_override_default_options.xml | 24 ++++ .../yml/http_client_default_options.yml | 7 + .../yml/http_client_full_default_options.yml | 26 ++++ .../http_client_override_default_options.yml | 8 ++ .../FrameworkExtensionTest.php | 59 +++++++++ .../Bundle/FrameworkBundle/composer.json | 1 + .../Component/HttpClient/CurlHttpClient.php | 2 +- .../Component/HttpClient/HttpClientTrait.php | 4 +- .../Component/HttpClient/NativeHttpClient.php | 2 +- 19 files changed, 512 insertions(+), 3 deletions(-) create mode 100644 src/Symfony/Bundle/FrameworkBundle/Resources/config/http_client.xml create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/http_client_default_options.php create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/http_client_full_default_options.php create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/http_client_override_default_options.php create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/http_client_default_options.xml create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/http_client_full_default_options.xml create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/http_client_override_default_options.xml create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/http_client_default_options.yml create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/http_client_full_default_options.yml create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/http_client_override_default_options.yml diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php index 2788cf019f..1d882101d5 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -17,6 +17,7 @@ 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; @@ -109,6 +110,7 @@ class Configuration implements ConfigurationInterface $this->addLockSection($rootNode); $this->addMessengerSection($rootNode); $this->addRobotsIndexSection($rootNode); + $this->addHttpClientSection($rootNode); return $treeBuilder; } @@ -1170,4 +1172,124 @@ class Configuration implements ConfigurationInterface ->end() ; } + + private function addHttpClientSection(ArrayNodeDefinition $rootNode) + { + $subNode = $rootNode + ->children() + ->arrayNode('http_client') + ->info('HTTP Client configuration') + ->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') + ->info('An HTTP Basic authentication "username:password".') + ->end() + ->arrayNode('query') + ->info('Associative array of query string values merged with URL parameters.') + ->useAttributeAsKey('key') + ->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() + ->booleanNode('buffer') + ->info('Indicates if the response should be buffered or not.') + ->end() + ->arrayNode('resolve') + ->info('Associative array: domain => IP.') + ->useAttributeAsKey('host') + ->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).') + ->useAttributeAsKey('algo') + ->normalizeKeys(false) + ->variablePrototype()->end() + ->end() + ->end() + ->end() + ; + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 391408a348..99970c3d54 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,48 @@ 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 { + [, $options] = $this->prepareRequest(null, null, $options, $defaultOptions); + + return $options; + } 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]); + + 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->register('psr18.'.$name, Psr18Client::class) + ->setAutowired(true) + ->setArguments([new Reference($name)]); + + $container->registerAliasForArgument($name, HttpClientInterface::class); + $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..add9b19771 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,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php index 589ddf50a6..3aee994484 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php @@ -331,6 +331,11 @@ class ConfigurationTest extends TestCase 'buses' => ['messenger.bus.default' => ['default_middleware' => true, 'middleware' => []]], ], 'disallow_search_engine_index' => true, + 'http_client' => [ + 'enabled' => false, + 'max_host_connections' => 6, + '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..2fd7822c4e --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/http_client_full_default_options.php @@ -0,0 +1,31 @@ +loadFromExtension('framework', [ + 'http_client' => [ + 'default_options' => [ + 'auth' => 'foo:bar', + 'query' => ['foo' => 'bar', 'bar' => 'baz'], + 'headers' => ['X-powered' => 'PHP'], + 'max_redirects' => 2, + 'http_version' => '2.0', + 'base_uri' => 'http://example.com', + 'buffer' => true, + '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..92d35bf9d0 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/http_client_default_options.xml @@ -0,0 +1,17 @@ + + + + + + 4 + + + + + + + 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..e1353fb6c0 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/http_client_full_default_options.xml @@ -0,0 +1,45 @@ + + + + + + + foo:bar + + bar + baz + + + PHP + + 2 + 2.0 + http://example.com + true + + 127.0.0.1 + + proxy.org + 3.5 + 127.0.0.1 + true + true + /etc/ssl/cafile + /etc/ssl + /etc/ssl/cert.pem + /etc/ssl/private_key.pem + password123456 + RC4-SHA:TLS13-AES-128-GCM-SHA256 + + 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..af8760a7fd --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/http_client_override_default_options.xml @@ -0,0 +1,24 @@ + + + + + + + + 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..946472b894 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/http_client_full_default_options.yml @@ -0,0 +1,26 @@ +framework: + http_client: + default_options: + auth: foo:bar + query: {'foo': 'bar', 'bar': 'baz'} + headers: + X-powered: PHP + max_redirects: 2 + http_version: 2.0 + base_uri: 'http://example.com' + buffer: true + 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..a851d89304 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,64 @@ 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' => [], + 'peer_fingerprint' => [], + 'http_version' => '', + ]; + $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']); + $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->assertTrue($defaultOptions['buffer']); + $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' => [], From f2d2cf3021f9a86b8603d3c6696fd428feea606f Mon Sep 17 00:00:00 2001 From: nicoweb Date: Wed, 13 Mar 2019 18:23:40 +0100 Subject: [PATCH 2/2] work with attributes for xml config --- .../DependencyInjection/Configuration.php | 42 ++++++++-- .../FrameworkExtension.php | 29 +++++-- .../Resources/config/schema/symfony-1.0.xsd | 80 ++++++++++--------- .../DependencyInjection/ConfigurationTest.php | 4 +- .../php/http_client_full_default_options.php | 3 +- .../xml/http_client_default_options.xml | 9 +-- .../xml/http_client_full_default_options.xml | 58 ++++++-------- .../http_client_override_default_options.xml | 20 ++--- .../yml/http_client_full_default_options.yml | 3 +- .../FrameworkExtensionTest.php | 5 +- 10 files changed, 141 insertions(+), 112 deletions(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php index 1d882101d5..1e52ce9322 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -22,6 +22,7 @@ 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; @@ -1179,7 +1180,7 @@ class Configuration implements ConfigurationInterface ->children() ->arrayNode('http_client') ->info('HTTP Client configuration') - ->canBeEnabled() + ->{!class_exists(FullStack::class) && class_exists(HttpClient::class) ? 'canBeDisabled' : 'canBeEnabled'}() ->fixXmlConfig('client') ->children(); @@ -1213,12 +1214,27 @@ class Configuration implements ConfigurationInterface ->arrayNode('default_options') ->fixXmlConfig('header') ->children() - ->scalarNode('auth') + ->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() @@ -1237,12 +1253,21 @@ class Configuration implements ConfigurationInterface ->scalarNode('base_uri') ->info('The URI to resolve relative URLs, following rules in RFC 3986, section 2.') ->end() - ->booleanNode('buffer') - ->info('Indicates if the response should be buffered or not.') - ->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() @@ -1284,9 +1309,12 @@ class Configuration implements ConfigurationInterface ->end() ->arrayNode('peer_fingerprint') ->info('Associative array: hashing algorithm => hash(es).') - ->useAttributeAsKey('algo') ->normalizeKeys(false) - ->variablePrototype()->end() + ->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 99970c3d54..673c89f9fe 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -1771,9 +1771,16 @@ class FrameworkExtension extends Extension public function merge(array $options, array $defaultOptions) { try { - [, $options] = $this->prepareRequest(null, null, $options, $defaultOptions); + [, $mergedOptions] = $this->prepareRequest(null, null, $options, $defaultOptions); - return $options; + 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); } @@ -1783,6 +1790,11 @@ class FrameworkExtension extends Extension $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); @@ -1790,12 +1802,15 @@ class FrameworkExtension extends Extension ->setFactory([HttpClient::class, 'create']) ->setArguments([$options, $clientConfig['max_host_connections'] ?? $config['max_host_connections'] ?? 6]); - $container->register('psr18.'.$name, Psr18Client::class) - ->setAutowired(true) - ->setArguments([new Reference($name)]); - $container->registerAliasForArgument($name, HttpClientInterface::class); - $container->registerAliasForArgument('psr18.'.$name, ClientInterface::class, $name); + + if ($hasPsr18) { + $container->register('psr18.'.$name, Psr18Client::class) + ->setAutowired(true) + ->setArguments([new Reference($name)]); + + $container->registerAliasForArgument('psr18.'.$name, ClientInterface::class, $name); + } } } 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 add9b19771..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,7 +32,7 @@ - + @@ -448,61 +448,63 @@ - - + + - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + - + - - - + + + + + - - - - + + - - - - + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php index 3aee994484..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; @@ -332,8 +333,7 @@ class ConfigurationTest extends TestCase ], 'disallow_search_engine_index' => true, 'http_client' => [ - 'enabled' => false, - 'max_host_connections' => 6, + 'enabled' => !class_exists(FullStack::class) && class_exists(HttpClient::class), 'clients' => [], ], ]; 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 index 2fd7822c4e..59e7f85d03 100644 --- 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 @@ -3,13 +3,12 @@ $container->loadFromExtension('framework', [ 'http_client' => [ 'default_options' => [ - 'auth' => 'foo:bar', + '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', - 'buffer' => true, 'resolve' => ['localhost' => '127.0.0.1'], 'proxy' => 'proxy.org', 'timeout' => 3.5, 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 index 92d35bf9d0..5a16c54914 100644 --- 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 @@ -6,12 +6,11 @@ http://symfony.com/schema/dic/symfony http://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> - - 4 - + + - + - + 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 index e1353fb6c0..6f889ba6e8 100644 --- 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 @@ -6,40 +6,34 @@ http://symfony.com/schema/dic/symfony http://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> - - - foo:bar - - bar - baz - - - PHP - - 2 - 2.0 - http://example.com - true - - 127.0.0.1 - - proxy.org - 3.5 - 127.0.0.1 - true - true - /etc/ssl/cafile - /etc/ssl - /etc/ssl/cert.pem - /etc/ssl/private_key.pem - password123456 - RC4-SHA:TLS13-AES-128-GCM-SHA256 - + + + 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 index af8760a7fd..33c201ef9f 100644 --- 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 @@ -6,19 +6,15 @@ http://symfony.com/schema/dic/symfony http://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> - - - - bar - - + + + bar + - - - baz - - + + baz + - + 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 index 946472b894..3d18286820 100644 --- 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 @@ -1,14 +1,13 @@ framework: http_client: default_options: - auth: foo:bar + 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' - buffer: true resolve: {'localhost': '127.0.0.1'} proxy: proxy.org timeout: 3.5 diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php index a851d89304..6a98f7c184 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php @@ -1363,8 +1363,6 @@ abstract class FrameworkExtensionTest extends TestCase 'query' => [], 'headers' => [], 'resolve' => [], - 'peer_fingerprint' => [], - 'http_version' => '', ]; $this->assertSame([$defaultOptions, 4], $container->getDefinition('http_client')->getArguments()); @@ -1387,13 +1385,12 @@ abstract class FrameworkExtensionTest extends TestCase $defaultOptions = $container->getDefinition('http_client')->getArguments()[0]; - $this->assertSame('foo:bar', $defaultOptions['auth']); + $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->assertTrue($defaultOptions['buffer']); $this->assertSame(['localhost' => '127.0.0.1'], $defaultOptions['resolve']); $this->assertSame('proxy.org', $defaultOptions['proxy']); $this->assertSame(3.5, $defaultOptions['timeout']);