Skip to content

Commit 89ad097

Browse files
feature #49302 [HttpClient] Add UriTemplateHttpClient (fancyweb)
This PR was merged into the 6.3 branch. Discussion ---------- [HttpClient] Add `UriTemplateHttpClient` | Q | A | ------------- | --- | Branch? | 6.3 | Bug fix? | no | New feature? | yes | Deprecations? | no | Tickets | - | License | MIT | Doc PR | - This PR adds `UriTemplateHttpClient` to ease using URI templates (see https://www.rfc-editor.org/rfc/rfc6570) with `symfony/http-client`. The goal is not to reimplement the RFC 6570 but to provide a better DX. A vendor has to be installed to expand the urls and we do not impose which one. The simple usage is: ```php (new UriTemplateHttpClient()) ->request('GET', 'https//ccc.tld/{resource}{?page}', [ 'vars' => [ 'resource' => 'users', 'page' => 3, ], ]); // the requested url is https//ccc.tld/users?page=3 ``` In a full framework context, all HTTP clients are decorated by `UriTemplateHttpClient`. The support is transparent and enabled globally. It's possible to configure a custom expander by redefining the `http_client.uri_template_expander` alias. Commits ------- 803a54e51e [HttpClient] Add UriTemplateHttpClient
2 parents 179ae03 + b4d1dac commit 89ad097

File tree

4 files changed

+65
-4
lines changed

4 files changed

+65
-4
lines changed

DependencyInjection/Configuration.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1655,6 +1655,11 @@ private function addHttpClientSection(ArrayNodeDefinition $rootNode, callable $e
16551655
->normalizeKeys(false)
16561656
->variablePrototype()->end()
16571657
->end()
1658+
->arrayNode('vars')
1659+
->info('Associative array: the default vars used to expand the templated URI.')
1660+
->normalizeKeys(false)
1661+
->variablePrototype()->end()
1662+
->end()
16581663
->integerNode('max_redirects')
16591664
->info('The maximum number of redirects to follow.')
16601665
->end()

DependencyInjection/FrameworkExtension.php

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@
8080
use Symfony\Component\HttpClient\Retry\GenericRetryStrategy;
8181
use Symfony\Component\HttpClient\RetryableHttpClient;
8282
use Symfony\Component\HttpClient\ScopingHttpClient;
83+
use Symfony\Component\HttpClient\UriTemplateHttpClient;
8384
use Symfony\Component\HttpFoundation\Request;
8485
use Symfony\Component\HttpKernel\Attribute\AsController;
8586
use Symfony\Component\HttpKernel\CacheClearer\CacheClearerInterface;
@@ -2339,6 +2340,8 @@ private function registerHttpClientConfiguration(array $config, ContainerBuilder
23392340
$options = $config['default_options'] ?? [];
23402341
$retryOptions = $options['retry_failed'] ?? ['enabled' => false];
23412342
unset($options['retry_failed']);
2343+
$defaultUriTemplateVars = $options['vars'] ?? [];
2344+
unset($options['vars']);
23422345
$container->getDefinition('http_client')->setArguments([$options, $config['max_host_connections'] ?? 6]);
23432346

23442347
if (!$hasPsr18 = ContainerBuilder::willBeAvailable('psr/http-client', ClientInterface::class, ['symfony/framework-bundle', 'symfony/http-client'])) {
@@ -2350,11 +2353,31 @@ private function registerHttpClientConfiguration(array $config, ContainerBuilder
23502353
$container->removeDefinition(HttpClient::class);
23512354
}
23522355

2353-
if ($this->readConfigEnabled('http_client.retry_failed', $container, $retryOptions)) {
2356+
if ($hasRetryFailed = $this->readConfigEnabled('http_client.retry_failed', $container, $retryOptions)) {
23542357
$this->registerRetryableHttpClient($retryOptions, 'http_client', $container);
23552358
}
23562359

2357-
$httpClientId = ($retryOptions['enabled'] ?? false) ? 'http_client.retryable.inner' : ($this->isInitializedConfigEnabled('profiler') ? '.debug.http_client.inner' : 'http_client');
2360+
if ($hasUriTemplate = class_exists(UriTemplateHttpClient::class)) {
2361+
if (ContainerBuilder::willBeAvailable('guzzlehttp/uri-template', \GuzzleHttp\UriTemplate\UriTemplate::class, [])) {
2362+
$container->setAlias('http_client.uri_template_expander', 'http_client.uri_template_expander.guzzle');
2363+
} elseif (ContainerBuilder::willBeAvailable('rize/uri-template', \Rize\UriTemplate::class, [])) {
2364+
$container->setAlias('http_client.uri_template_expander', 'http_client.uri_template_expander.rize');
2365+
}
2366+
2367+
$container
2368+
->getDefinition('http_client.uri_template')
2369+
->setArgument(2, $defaultUriTemplateVars);
2370+
} elseif ($defaultUriTemplateVars) {
2371+
throw new LogicException('Support for URI template requires symfony/http-client 6.3 or higher, try upgrading.');
2372+
}
2373+
2374+
$httpClientId = match (true) {
2375+
$hasUriTemplate => 'http_client.uri_template.inner',
2376+
$hasRetryFailed => 'http_client.retryable.inner',
2377+
$this->isInitializedConfigEnabled('profiler') => '.debug.http_client.inner',
2378+
default => 'http_client',
2379+
};
2380+
23582381
foreach ($config['scoped_clients'] as $name => $scopeConfig) {
23592382
if ('http_client' === $name) {
23602383
throw new InvalidArgumentException(sprintf('Invalid scope name: "%s" is reserved.', $name));
@@ -2385,6 +2408,17 @@ private function registerHttpClientConfiguration(array $config, ContainerBuilder
23852408
$this->registerRetryableHttpClient($retryOptions, $name, $container);
23862409
}
23872410

2411+
if ($hasUriTemplate) {
2412+
$container
2413+
->register($name.'.uri_template', UriTemplateHttpClient::class)
2414+
->setDecoratedService($name, null, 7) // Between TraceableHttpClient (5) and RetryableHttpClient (10)
2415+
->setArguments([
2416+
new Reference('.inner'),
2417+
new Reference('http_client.uri_template_expander', ContainerInterface::NULL_ON_INVALID_REFERENCE),
2418+
$defaultUriTemplateVars,
2419+
]);
2420+
}
2421+
23882422
$container->registerAliasForArgument($name, HttpClientInterface::class);
23892423

23902424
if ($hasPsr18) {

Resources/config/http_client.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use Symfony\Component\HttpClient\HttplugClient;
1919
use Symfony\Component\HttpClient\Psr18Client;
2020
use Symfony\Component\HttpClient\Retry\GenericRetryStrategy;
21+
use Symfony\Component\HttpClient\UriTemplateHttpClient;
2122
use Symfony\Contracts\HttpClient\HttpClientInterface;
2223

2324
return static function (ContainerConfigurator $container) {
@@ -60,5 +61,25 @@
6061
abstract_arg('max delay ms'),
6162
abstract_arg('jitter'),
6263
])
64+
65+
->set('http_client.uri_template', UriTemplateHttpClient::class)
66+
->decorate('http_client', null, 7) // Between TraceableHttpClient (5) and RetryableHttpClient (10)
67+
->args([
68+
service('.inner'),
69+
service('http_client.uri_template_expander')->nullOnInvalid(),
70+
abstract_arg('default vars'),
71+
])
72+
73+
->set('http_client.uri_template_expander.guzzle', \Closure::class)
74+
->factory([\Closure::class, 'fromCallable'])
75+
->args([
76+
[\GuzzleHttp\UriTemplate\UriTemplate::class, 'expand'],
77+
])
78+
79+
->set('http_client.uri_template_expander.rize', \Closure::class)
80+
->factory([\Closure::class, 'fromCallable'])
81+
->args([
82+
[inline_service(\Rize\UriTemplate::class), 'expand'],
83+
])
6384
;
6485
};

Tests/DependencyInjection/FrameworkExtensionTestCase.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
use Symfony\Component\HttpClient\MockHttpClient;
5656
use Symfony\Component\HttpClient\RetryableHttpClient;
5757
use Symfony\Component\HttpClient\ScopingHttpClient;
58+
use Symfony\Component\HttpClient\UriTemplateHttpClient;
5859
use Symfony\Component\HttpKernel\DependencyInjection\LoggerPass;
5960
use Symfony\Component\HttpKernel\Fragment\FragmentUriGeneratorInterface;
6061
use Symfony\Component\Messenger\Transport\TransportFactory;
@@ -2003,15 +2004,15 @@ public function testHttpClientMockResponseFactory()
20032004
{
20042005
$container = $this->createContainerFromFile('http_client_mock_response_factory');
20052006

2006-
$definition = $container->getDefinition('http_client.mock_client');
2007+
$definition = $container->getDefinition(($uriTemplateHttpClientExists = class_exists(UriTemplateHttpClient::class)) ? 'http_client.uri_template.inner.mock_client' : 'http_client.mock_client');
20072008

20082009
$this->assertSame(MockHttpClient::class, $definition->getClass());
20092010
$this->assertCount(1, $definition->getArguments());
20102011

20112012
$argument = $definition->getArgument(0);
20122013

20132014
$this->assertInstanceOf(Reference::class, $argument);
2014-
$this->assertSame('http_client', current($definition->getDecoratedService()));
2015+
$this->assertSame($uriTemplateHttpClientExists ? 'http_client.uri_template.inner' : 'http_client', current($definition->getDecoratedService()));
20152016
$this->assertSame('my_response_factory', (string) $argument);
20162017
}
20172018

0 commit comments

Comments
 (0)