8000 [HttpClient] Add a ConditionalHttpClient · symfony/symfony@6bc396e · GitHub
[go: up one dir, main page]

Skip to content

Commit 6bc396e

Browse files
author
Anthony MARTIN
committed
[HttpClient] Add a ConditionalHttpClient
1 parent 2278d4c commit 6bc396e

File tree

3 files changed

+181
-0
lines changed

3 files changed

+181
-0
lines changed
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
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+
if (!parse_url($url, PHP_URL_SCHEME) || !parse_url($url, PHP_URL_HOST)) {
48+
throw new InvalidArgumentException(sprintf('Unsupported URL: host or scheme is missing in "%s".', $url));
49+
}
50+
51+
foreach ($this->options as $regexp => $defaultOptions) {
52+
$preparedOptions = self::mergeDefaultOptions($options, $defaultOptions, true);
53+
54+
if (preg_match($regexp, $url)) {
55+
return $this->client->request($method, $url, $preparedOptions);
56+
}
57+
}
58+
59+
return $this->client->request($method, $url, $options);
60+
}
61+
62+
/**
63+
* {@inheritdoc}
64+
*/
65+
public function stream($responses, float $timeout = null): ResponseStreamInterface
66+
{
67+
return $this->client->stream($responses, $timeout);
68+
}
69+
}

src/Symfony/Component/HttpClient/Response/MockResponse.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ class MockResponse implements ResponseInterface
2727
use ResponseTrait;
2828

2929
private $body;
30+
private $requestOptions;
3031

3132
private static $mainMulti;
3233
private static $idSequence = 0;
@@ -60,6 +61,11 @@ protected function close(): void
6061
$this->body = [];
6162
}
6263

64+
public function getRequestOptions(): array
65+
{
66+
return $this->requestOptions;
67+
}
68+
6369
/**
6470
* @internal
6571
*/
@@ -87,6 +93,8 @@ public static function fromRequest(string $method, string $url, array $options,
8793
$response->info['user_data'] = $options['user_data'] ?? null;
8894
$response->info['url'] = $url;
8995

96+
$response->requestOptions = $options;
97+
9098
self::writeRequest($response, $options, $mock);
9199
$response->body[] = [$options, $mock];
92100

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
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+
20+
class ConditionalHttpClientTest extends TestCase
21+
{
22+
public function testInvalidUrl()
23+
{
24+
$defaultOptions = [
25+
'#^/foo-bar$#' => [
26+
'headers' => ['Content-Type' => 'application/json'],
27+
],
28+
'#^.*$#' => [
29+
'headers' => ['Content-Type' => 'application/json'],
30+
],
31+
];
32+
33+
$mockClient = new MockHttpClient(null);
34+
$client = new ConditionalHttpClient($mockClient, $defaultOptions);
35+
36+
$this->expectException(InvalidArgumentException::class);
37+
$client->request('GET', '/foo');
38+
}
39+
40+
public function getUrlsAndDefaultOptions()
41+
{
42+
$defaultOptions = [
43+
'#^.*/foo-bar$#' => [
44+
'headers' => ['content-type' => 'application/json'],
45+
],
46+
'#^.*$#' => [
47+
'headers' => ['content-type' => 'text/html'],
48+
],
49+
];
50+
51+
yield ['regexp' => '#^.*/foo-bar$#', 'url' => 'http://example.com/foo-bar', 'default_options' => $defaultOptions];
52+
yield ['regexp' => '#^.*$#', 'url' => 'http://example.com/bar-foo', 'default_options' => $defaultOptions];
53+
yield ['regexp' => '#^.*$#', 'url' => 'http://example.com/foobar', 'default_options' => $defaultOptions];
54+
}
55+
56+
/**
57+
* @dataProvider getUrlsAndDefaultOptions
58+
*/
59+
public function testMatchingUrls(string $regexp, string $url, array $options)
60+
{
61+
$mockClient = new MockHttpClient(new MockResponse($url, $options[$regexp]), 'http://example.com');
62+
$client = new ConditionalHttpClient($mockClient, $options);
63+
64+
$response = $client->request('GET', $url);
65+
$responseInfo = $response->getInfo('headers');
66+
67+
$this->assertEquals([], array_diff_assoc($responseInfo, $options[$regexp]['headers']));
68+
}
69+
70+
public function testMatchingUrlsAndOptions()
71+
{
72+
$defaultOptions = [
73+
'#^/foo-bar$#' => ['headers' => ['x-app' => 'unit-test-foo-bar']],
74+
'#^.*$#' => ['headers' => ['content-type' => 'text/html']],
75+
];
76+
77+
$mockResponses = [
78+
new MockResponse('http://example.com/foo-bar', $defaultOptions['#^/foo-bar$#']),
79+
new MockResponse('http://example.com/bar-foo', $defaultOptions['#^.*$#']),
80+
new MockResponse('http://example.com/', $defaultOptions['#^.*$#']),
81+
];
82+
83+
$mockClient = new MockHttpClient($mockResponses, 'http://example.com');
84+
$client = new ConditionalHttpClient($mockClient, $defaultOptions);
85+
86+
$response = $client->request('GET', 'http://example.com/foo-bar', ['json' => ['url' => 'http://example.com']]);
87+
$requestOptions = $response->getRequestOptions();
88+
$responseInfos = $response->getInfo('headers');
89+
$this->assertEquals($requestOptions['json']['url'], 'http://example.com');
90+
$this->assertEquals($responseInfos['x-app'], $defaultOptions['#^/foo-bar$#']['headers']['x-app']);
91+
92+
$response = $client->request('GET', 'http://example.com/bar-foo', ['headers' => ['x-app' => 'unit-test']]);
93+
$requestOptions = $response->getRequestOptions();
94+
$responseInfos = $response->getInfo('headers');
95+
$this->assertEquals($requestOptions['headers']['x-app'][0], 'unit-test');
96+
$this->assertEquals($responseInfos['content-type'], 'text/html');
97+
98+
$response = $client->request('GET', 'http://example.com/bar-foo', ['headers' => ['x-app' => 'unit-test']]);
99+
$requestOptions = $response->getRequestOptions();
100+
$responseInfos = $response->getInfo('headers');
101+
$this->assertEquals($requestOptions['headers']['x-app'][0], 'unit-test');
102+
$this->assertEquals($responseInfos['content-type'], 'text/html');
103+
}
104+
}

0 commit comments

Comments
 (0)
0