Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 9b66a31

Browse filesBrowse files
author
Anthony MARTIN
committed
[HttpClient] Add a ConditionalHttpClient
1 parent 6fdc1b4 commit 9b66a31
Copy full SHA for 9b66a31

File tree

9 files changed

+368
-14
lines changed
Filter options

9 files changed

+368
-14
lines changed

‎src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php
+9-7Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1193,9 +1193,9 @@ private function addHttpClientSection(ArrayNodeDefinition $rootNode)
11931193
->arrayPrototype()
11941194
->children();
11951195

1196-
$this->addHttpClientOptionsSection($subNode);
1196+
$this->addHttpClientOptionsSection($subNode, true);
11971197

1198-
$subNode = $subNode
1198+
$subNode
11991199
->end()
12001200
->end()
12011201
->end()
@@ -1205,13 +1205,15 @@ private function addHttpClientSection(ArrayNodeDefinition $rootNode)
12051205
;
12061206
}
12071207

1208-
private function addHttpClientOptionsSection(NodeBuilder $rootNode)
1208+
private function addHttpClientOptionsSection(NodeBuilder $rootNode, bool $isSubClient = false)
12091209
{
1210-
$rootNode
1211-
->integerNode('max_host_connections')
1210+
if (!$isSubClient) {
1211+
$rootNode = $rootNode->integerNode('max_host_connections')
12121212
->info('The maximum number of connections to a single host.')
1213-
->end()
1214-
->arrayNode('default_options')
1213+
->end();
1214+
}
1215+
1216+
$rootNode = $rootNode->arrayNode('default_options')
12151217
->fixXmlConfig('header')
12161218
->children()
12171219
->scalarNode('auth_basic')

‎src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
+11-4Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,11 @@
5858
use Symfony\Component\Form\FormTypeExtensionInterface;
5959
use Symfony\Component\Form\FormTypeGuesserInterface;
6060
use Symfony\Component\Form\FormTypeInterface;
61+
use Symfony\Component\HttpClient\ConditionalHttpClient;
6162
use Symfony\Component\HttpClient\HttpClient;
6263
use Symfony\Component\HttpClient\HttpClientTrait;
6364
use Symfony\Component\HttpClient\Psr18Client;
65+
use Symfony\Component\HttpClient\ScopedHttpClient;
6466
use Symfony\Component\HttpKernel\CacheClearer\CacheClearerInterface;
6567
use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface;
6668
use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface;
@@ -1791,19 +1793,24 @@ public function merge(array $options, array $defaultOptions)
17911793

17921794
$defaultOptions = $merger->merge($config['default_options'] ?? [], []);
17931795
$container->getDefinition('http_client')->setArguments([$defaultOptions, $config['max_host_connections'] ?? 6]);
1796+
$httpClient = $container->get('http_client');
17941797

17951798
if (!$hasPsr18 = interface_exists(ClientInterface::class)) {
17961799
$container->removeDefinition('psr18.http_client');
17971800
$container->removeAlias(ClientInterface::class);
17981801
}
17991802

18001803
foreach ($config['clients'] as $name => $clientConfig) {
1801-
$options = $merger->merge($clientConfig['default_options'] ?? [], $defaultOptions);
1804+
$container->register('conditionnal_client_'.$name, ConditionalHttpClient::class)
1805+
->setArguments([new Reference('http_client'), $clientConfig['default_options'] ?? []]);
18021806

1803-
$container->register($name, HttpClientInterface::class)
1804-
->setFactory([HttpClient::class, 'create'])
1805-
->setArguments([$options, $clientConfig['max_host_connections'] ?? $config['max_host_connections'] ?? 6]);
1807+
$container->registerAliasForArgument('conditionnal_client_'.$name, ConditionalHttpClient::class);
1808+
$container->registerAliasForArgument('conditionnal_client_'.$name, HttpClientInterface::class);
18061809

1810+
$container->register($name, ScopedHttpClient::class)
1811+
->setArguments([new Reference('conditionnal_client_'.$name), $clientConfig['default_options'] ? $clientConfig['default_options']['base_uri'] ?? '' : '']);
1812+
1813+
$container->registerAliasForArgument($name, ScopedHttpClient::class);
18071814
$container->registerAliasForArgument($name, HttpClientInterface::class);
18081815

18091816
if ($hasPsr18) {

‎src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/http_client_override_default_options.php

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/http_client_override_default_options.php
-1Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
],
99
'clients' => [
1010
'foo' => [
11-
'max_host_connections' => 5,
1211
'default_options' => [
1312
'headers' => ['bar' => 'baz'],
1413
],

‎src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/http_client_override_default_options.xml

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/http_client_override_default_options.xml
+1-1Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
<framework:default-options>
1111
<framework:header name="foo">bar</framework:header>
1212
</framework:default-options>
13-
<framework:client name="foo" max-host-connections="5">
13+
<framework:client name="foo">
1414
<framework:default-options>
1515
<framework:header name="bar">baz</framework:header>
1616
</framework:default-options>

‎src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/http_client_override_default_options.yml

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/http_client_override_default_options.yml
-1Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,5 @@ framework:
55
headers: {'foo': 'bar'}
66
clients:
77
foo:
8-
max_host_connections: 5
98
default_options:
109
headers: {'bar': 'baz'}
+111Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpClient;
13+
14+
use Symfony\Component\HttpClient\Exception\InvalidArgumentException;
15+
use Symfony\Contracts\HttpClient\HttpClientInterface;
16+
use Symfony\Contracts\HttpClient\ResponseInterface;
17+
use Symfony\Contracts\HttpClient\ResponseStreamInterface;
18+
19+
/**
20+
* Auto-configure the default options based on the requested absolute URL.
21+
*
22+
* @author Anthony Martin <anthony.martin@sensiolabs.com>
23+
*
24+
* @experimental in 4.3
25+
*/
26+
class ConditionalHttpClient implements HttpClientInterface
27+
{
28+
use HttpClientTrait;
29+
30+
private $client;
31+
private $options;
32+
33+
/**
34+
* @param array[] $options the default options to use when the regexp provided as key matches the requested URL
35+
*/
36+
public function __construct(HttpClientInterface $client, array $options)
37+
{
38+
$this->client = $client;
39+
$this->options = $options;
40+
}
41+
42+
/**
43+
* {@inheritdoc}
44+
*/
45+
public function request(string $method, string $url, array $options = []): ResponseInterface
46+
{
47+
$url = $this->checkAndFormatUrl($url, $options['base_uri'] ?? null);
48+
49+
foreach ($this->options as $regexp => $defaultOptions) {
50+
if (preg_match(sprintf('{%s}A', $regexp), $url)) {
51+
$options = self::mergeDefaultOptions($options, $defaultOptions, true);
52+
53+
return $this->client->request($method, $url, $options);
54+
}
55+
}
56+
57+
return $this->client->request($method, $url, $options);
58+
}
59+
60+
/**
61+
* {@inheritdoc}
62+
*/
63+
public function stream($responses, float $timeout = null): ResponseStreamInterface
64+
{
65+
return $this->client->stream($responses, $timeout);
66+
}
67+
68+
private function checkAndFormatUrl(string $url, string $baseUri = null): string
69+
{
70+
// Normalize $url
71+
$url = self::parseUrl($url);
72+
73+
if (null === $url['scheme'] || null === $url['authority']) {
74+
$url = implode('', $url);
75+
76+
if (null === $baseUri) {
77+
foreach ($this->options as $regexp => $defaultOptions) {
78+
if (!isset($defaultOptions['base_uri'])) {
79+
continue;
80+
}
81+
82+
$baseUri = self::parseUrl($defaultOptions['base_uri']);
83+
if (null === $baseUri['scheme'] || null === $baseUri['authority']) {
84+
throw new InvalidArgumentException(sprintf('Unvalid URL: the base_uri configured "%s" is not a valid base URI ', implode('', $baseUri)));
85+
}
86+
87+
$baseUri['path'] = rtrim($baseUri['path']);
88+
89+
if (preg_match(sprintf('{%s}A', $regexp), $baseUri['scheme'].$baseUri['authority'].$baseUri['path'].'/'.ltrim($url, '/'))) {
90+
return $baseUri['scheme'].$baseUri['authority'].$baseUri['path'].'/'.ltrim($url, '/');
91+
}
92+
}
93+
94+
throw new InvalidArgumentException(sprintf('Unsupported URL: host or scheme is missing in "%s" and no option "base_uri" set/given.', $url));
95+
}
96+
97+
$baseUri = self::parseUrl($baseUri);
98+
if (null === $baseUri['scheme'] || null === $baseUri['authority']) {
99+
throw new InvalidArgumentException(sprintf('Unvalid URL: the base_uri configured "%s" is not a valid base URI ', implode('', $baseUri)));
100+
}
101+
102+
return $baseUri['scheme'].$baseUri['authority'].rtrim($baseUri['path'], '/').'/'.ltrim($url, '/');
103+
}
104+
105+
if (null !== $baseUri && false !== strpos($url, 'http://') && false === strpos($url, $baseUri)) {
106+
throw new InvalidArgumentException(sprintf('Unvalid URL: the base_uri configured "%s" is not the same then the one given in the url "%s"', $baseUri, $url));
107+
}
108+
109+
return implode('', $url);
110+
}
111+
}
+70Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpClient;
13+
14+
use Symfony\Component\HttpClient\Exception\InvalidArgumentException;
15+
use Symfony\Contracts\HttpClient\HttpClientInterface;
16+
use Symfony\Contracts\HttpClient\ResponseInterface;
17+
use Symfony\Contracts\HttpClient\ResponseStreamInterface;
18+
19+
/**
20+
* Transforms relative urls given to an.
21+
*
22+
* @author Anthony Martin <anthony.martin@sensiolabs.com>
23+
*
24+
* @experimental in 4.3
25+
*/
26+
class ScopedHttpClient implements HttpClientInterface
27+
{
28+
use HttpClientTrait;
29+
30+
private $client;
31+
private $baseUri;
32+
33+
public function __construct(HttpClientInterface $client, $baseUri = null)
34+
{
35+
$this->client = $client;
36+
37+
if (null === $baseUri || '' === $baseUri) {
38+
throw new InvalidArgumentException(sprintf('Unvalid URL: the base_uri configured "%s" is not a valid base URI ', $baseUri));
39+
}
40+
41+
$baseUri = self::parseUrl($baseUri);
42+
if (null === $baseUri['scheme'] || null === $baseUri['authority']) {
43+
throw new InvalidArgumentException(sprintf('Unvalid URL: the base_uri configured "%s" is not a valid base URI ', implode('', $baseUri)));
44+
}
45+
46+
$this->baseUri = $baseUri['scheme'].$baseUri['authority'].rtrim($baseUri['path'], '/');
47+
}
48+
49+
/**
50+
* {@inheritdoc}
51+
*/
52+
public function request(string $method, string $url, array $options = []): ResponseInterface
53+
{
54+
if (!parse_url($url, PHP_URL_SCHEME) || !parse_url($url, PHP_URL_HOST)) {
55+
$url = $this->baseUri.'/'.ltrim($url, '/');
56+
} elseif (false !== strpos($url, 'http://') && false === strpos($url, $this->baseUri)) {
57+
throw new InvalidArgumentException(sprintf('Unvalid URL: the base_uri configured "%s" is not the same then the one given in the url "%s"', $this->baseUri, $url));
58+
}
59+
60+
return $this->client->request($method, $url, $options);
61+
}
62+
63+
/**
64+
* {@inheritdoc}
65+
*/
66+
public function stream($responses, float $timeout = null): ResponseStreamInterface
67+
{
68+
return $this->client->stream($responses, $timeout);
69+
}
70+
}
+97Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpClient\Tests;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\HttpClient\ConditionalHttpClient;
16+
use Symfony\Component\HttpClient\Exception\InvalidArgumentException;
17+
use Symfony\Component\HttpClient\MockHttpClient;
18+
use Symfony\Component\HttpClient\Response\MockResponse;
19+
use Symfony\Contracts\HttpClient\ResponseInterface;
20+
21+
class ConditionalHttpClientTest extends TestCase
22+
{
23+
public function testInvalidUrl()
24+
{
25+
$mockClient = new MockHttpClient([]);
26+
$client = new ConditionalHttpClient($mockClient, []);
27+
28+
$this->expectException(InvalidArgumentException::class);
29+
$client->request('GET', '/foo');
30+
}
31+
32+
public function testInvalidUrlWithBaseUri()
33+
{
34+
$mockClient = new MockHttpClient([new MockResponse(), new MockResponse()]);
35+
$client = new ConditionalHttpClient($mockClient, ['.*/foo' => [], '.*/bar' => ['base_uri' => 'http://example.com']]);
36+
$this->assertInstanceOf(ResponseInterface::class, $client->request('GET', '/foo', ['base_uri' => 'http://example.com']));
37+
$this->assertInstanceOf(ResponseInterface::class, $client->request('GET', '/bar'));
38+
}
39+
40+
/**
41+
* @dataProvider provideMatchingUrls
42+
*/
43+
public function testMatchingUrls(string $regexp, string $url, array $options)
44+
{
45+
$mockClient = new MockHttpClient(new MockResponse());
46+
$client = new ConditionalHttpClient($mockClient, $options);
47+
48+
$response = $client->request('GET', $url);
49+
$reuestedOptions = $response->getRequestOptions();
50+
51+
$this->assertEquals($reuestedOptions['case'], $options[$regexp]['case']);
52+
}
53+
54+
public function provideMatchingUrls()
55+
{
56+
$defaultOptions = [
57+
'.*/foo-bar' => ['case' => 1],
58+
'.*' => ['case' => 2],
59+
];
60+
61+
yield ['regexp' => '.*/foo-bar', 'url' => 'http://example.com/foo-bar', 'default_options' => $defaultOptions];
62+
yield ['regexp' => '.*', 'url' => 'http://example.com/bar-foo', 'default_options' => $defaultOptions];
63+
yield ['regexp' => '.*', 'url' => 'http://example.com/foobar', 'default_options' => $defaultOptions];
64+
}
65+
66+
public function testMatchingUrlsAndOptions()
67+
{
68+
$defaultOptions = [
69+
'.*/foo-bar' => ['headers' => ['x-app' => 'unit-test-foo-bar']],
70+
'.*' => ['headers' => ['content-type' => 'text/html']],
71+
];
72+
73+
$mockResponses = [
74+
new MockResponse(),
75+
new MockResponse(),
76+
new MockResponse(),
77+
];
78+
79+
$mockClient = new MockHttpClient($mockResponses);
80+
$client = new ConditionalHttpClient($mockClient, $defaultOptions);
81+
82+
$response = $client->request('GET', 'http://example.com/foo-bar', ['json' => ['url' => 'http://example.com']]);
83+
$requestOptions = $response->getRequestOptions();
84+
$this->assertEquals($requestOptions['json']['url'], 'http://example.com');
85+
$this->assertEquals($requestOptions['headers']['x-app'][0], $defaultOptions['.*/foo-bar']['headers']['x-app']);
86+
87+
$response = $client->request('GET', 'http://example.com/bar-foo', ['headers' => ['x-app' => 'unit-test']]);
88+
$requestOptions = $response->getRequestOptions();
89+
$this->assertEquals($requestOptions['headers']['x-app'][0], 'unit-test');
90+
$this->assertEquals($requestOptions['headers']['content-type'][0], 'text/html');
91+
92+
$response = $client->request('GET', 'http://example.com/foobar-foo', ['headers' => ['x-app' => 'unit-test']]);
93+
$requestOptions = $response->getRequestOptions();
94+
$this->assertEquals($requestOptions['headers']['x-app'][0], 'unit-test');
95+
$this->assertEquals($requestOptions['headers']['content-type'][0], 'text/html');
96+
}
97+
}

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.