Skip to content

Commit e46dc68

Browse files
fancywebnicolas-grekas
authored andcommitted
[HttpClient] Add UriTemplateHttpClient
1 parent ea30144 commit e46dc68

File tree

5 files changed

+232
-0
lines changed

5 files changed

+232
-0
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
6.3
5+
---
6+
7+
* Add `UriTemplateHttpClient` to use URI templates as specified in the RFC 6570
8+
49
6.2
510
---
611

HttpClientTrait.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,10 @@ private static function mergeDefaultOptions(array $options, array $defaultOption
245245
throw new InvalidArgumentException(sprintf('Option "auth_ntlm" is not supported by "%s", '.$msg, __CLASS__, CurlHttpClient::class));
246246
}
247247

248+
if ('vars' === $name) {
249+
throw new InvalidArgumentException(sprintf('Option "vars" is not supported by "%s", try using "%s" instead.', __CLASS__, UriTemplateHttpClient::class));
250+
}
251+
248252
$alternatives = [];
249253

250254
foreach ($defaultOptions as $k => $v) {

HttpOptions.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,16 @@ public function setBaseUri(string $uri): static
135135
return $this;
136136
}
137137

138+
/**
139+
* @return $this
140+
*/
141+
public function setVars(array $vars): static
142+
{
143+
$this->options['vars'] = $vars;
144+
145+
return $this;
146+
}
147+
138148
/**
139149
* @return $this
140150
*/

Tests/UriTemplateHttpClientTest.php

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
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\MockHttpClient;
16+
use Symfony\Component\HttpClient\Response\MockResponse;
17+
use Symfony\Component\HttpClient\UriTemplateHttpClient;
18+
19+
final class UriTemplateHttpClientTest extends TestCase
20+
{
21+
public function testExpanderIsCalled()
22+
{
23+
$client = new UriTemplateHttpClient(
24+
new MockHttpClient(),
25+
function (string $url, array $vars): string {
26+
$this->assertSame('https://foo.tld/{version}/{resource}{?page}', $url);
27+
$this->assertSame([
28+
'version' => 'v2',
29+
'resource' => 'users',
30+
'page' => 33,
31+
], $vars);
32+
33+
return 'https://foo.tld/v2/users?page=33';
34+
},
35+
[
36+
'version' => 'v2',
37+
],
38+
);
39+
$this->assertSame('https://foo.tld/v2/users?page=33', $client->request('GET', 'https://foo.tld/{version}/{resource}{?page}', [
40+
'vars' => [
41+
'resource' => 'users',
42+
'page' => 33,
43+
],
44+
])->getInfo('url'));
45+
}
46+
47+
public function testWithOptionsAppendsVarsToDefaultVars()
48+
{
49+
$client = new UriTemplateHttpClient(
50+
new MockHttpClient(),
51+
function (string $url, array $vars): string {
52+
$this->assertSame('https://foo.tld/{bar}', $url);
53+
$this->assertSame([
54+
'bar' => 'ccc',
55+
], $vars);
56+
57+
return 'https://foo.tld/ccc';
58+
},
59+
);
60+
$this->assertSame('https://foo.tld/{bar}', $client->request('GET', 'https://foo.tld/{bar}')->getInfo('url'));
61+
62+
$client = $client->withOptions([
63+
'vars' => [
64+
'bar' => 'ccc',
65+
],
66+
]);
67+
$this->assertSame('https://foo.tld/ccc', $client->request('GET', 'https://foo.tld/{bar}')->getInfo('url'));
68+
}
69+
70+
public function testExpanderIsNotCalledWithEmptyVars()
71+
{
72+
$this->expectNotToPerformAssertions();
73+
74+
$client = new UriTemplateHttpClient(new MockHttpClient(), $this->fail(...));
75+
$client->request('GET', 'https://foo.tld/bar', [
76+
'vars' => [],
77+
]);
78+
}
79+
80+
public function testExpanderIsNotCalledWithNoVarsAtAll()
81+
{
82+
$this->expectNotToPerformAssertions();
83+
84+
$client = new UriTemplateHttpClient(new MockHttpClient(), $this->fail(...));
85+
$client->request('GET', 'https://foo.tld/bar');
86+
}
87+
88+
public function testRequestWithNonArrayVarsOption()
89+
{
90+
$this->expectException(\InvalidArgumentException::class);
91+
$this->expectExceptionMessage('The "vars" option must be an array.');
92+
93+
(new UriTemplateHttpClient(new MockHttpClient()))->request('GET', 'https://foo.tld', [
94+
'vars' => 'should be an array',
95+
]);
96+
}
97+
98+
public function testWithOptionsWithNonArrayVarsOption()
99+
{
100+
$this->expectException(\InvalidArgumentException::class);
101+
$this->expectExceptionMessage('The "vars" option must be an array.');
102+
103+
(new UriTemplateHttpClient(new MockHttpClient()))->withOptions([
104+
'vars' => new \stdClass(),
105+
]);
106+
}
107+
108+
public function testVarsOptionIsNotPropagated()
109+
{
110+
$client = new UriTemplateHttpClient(
111+
new MockHttpClient(function (string $method, string $url, array $options): MockResponse {
112+
$this->assertArrayNotHasKey('vars', $options);
113+
114+
return new MockResponse();
115+
}),
116+
static fn (): string => 'ccc',
117+
);
118+
119+
$client->withOptions([
120+
'vars' => [
121+
'foo' => 'bar',
122+
],
123+
])->request('GET', 'https://foo.tld', [
124+
'vars' => [
125+
'foo2' => 'bar2',
126+
],
127+
]);
128+
}
129+
}

UriTemplateHttpClient.php

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
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\Contracts\HttpClient\HttpClientInterface;
15+
use Symfony\Contracts\HttpClient\ResponseInterface;
16+
use Symfony\Contracts\Service\ResetInterface;
17+
18+
class UriTemplateHttpClient implements HttpClientInterface, ResetInterface
19+
{
20+
use DecoratorTrait;
21+
22+
/**
23+
* @param (\Closure(string $url, array $vars): string)|null $expander
24+
*/
25+
public function __construct(HttpClientInterface $client = null, private ?\Closure $expander = null, private array $defaultVars = [])
26+
{
27+
$this->client = $client ?? HttpClient::create();
28+
}
29+
30+
public function request(string $method, string $url, array $options = []): ResponseInterface
31+
{
32+
$vars = $this->defaultVars;
33+
34+
if (\array_key_exists('vars', $options)) {
35+
if (!\is_array($options['vars'])) {
36+
throw new \InvalidArgumentException('The "vars" option must be an array.');
37+
}
38+
39+
$vars = [...$vars, ...$options['vars']];
40+
unset($options['vars']);
41+
}
42+
43+
if ($vars) {
44+
$url = ($this->expander ??= $this->createExpanderFromPopularVendors())($url, $vars);
45+
}
46+
47+
return $this->client->request($method, $url, $options);
48+
}
49+
50+
public function withOptions(array $options): static
51+
{
52+
if (!\is_array($options['vars'] ?? [])) {
53+
throw new \InvalidArgumentException('The "vars" option must be an array.');
54+
}
55+
56+
$clone = clone $this;
57+
$clone->defaultVars = [...$clone->defaultVars, ...$options['vars'] ?? []];
58+
unset($options['vars']);
59+
60+
$clone->client = $this->client->withOptions($options);
61+
62+
return $clone;
63+
}
64+
65+
/**
66+
* @return \Closure(string $url, array $vars): string
67+
*/
68+
private function createExpanderFromPopularVendors(): \Closure
69+
{
70+
if (class_exists(\GuzzleHttp\UriTemplate\UriTemplate::class)) {
71+
return \GuzzleHttp\UriTemplate\UriTemplate::expand(...);
72+
}
73+
74+
if (class_exists(\League\Uri\UriTemplate::class)) {
75+
return static fn (string $url, array $vars): string => (new \League\Uri\UriTemplate($url))->expand($vars);
76+
}
77+
78+
if (class_exists(\Rize\UriTemplate::class)) {
79+
return (new \Rize\UriTemplate())->expand(...);
80+
}
81+
82+
throw new \LogicException('Support for URI template requires a vendor to expand the URI. Run "composer require guzzlehttp/uri-template" or pass your own expander \Closure implementation.');
83+
}
84+
}

0 commit comments

Comments
 (0)