Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions UPGRADE-7.4.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ HttpClient
----------

* Deprecate using amphp/http-client < 5
* Deprecate passing an instance of `StoreInterface` as `$cache` argument to `CachingHttpClient` constructor

HttpFoundation
--------------
Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ CHANGELOG
* Add `framework.type_info.aliases` option
* Add `KernelBrowser::getSession()`
* Add support for configuring workflow places with glob patterns matching consts/backed enums
* Add support for configuring the `CachingHttpClient`

7.3
---
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1993,6 +1993,7 @@ private function addHttpClientSection(ArrayNodeDefinition $rootNode, callable $e
->defaultNull()
->info('Rate limiter name to use for throttling requests.')
->end()
->append($this->createHttpClientCachingSection())
->append($this->createHttpClientRetrySection())
->end()
->end()
Expand Down Expand Up @@ -2138,6 +2139,7 @@ private function addHttpClientSection(ArrayNodeDefinition $rootNode, callable $e
->defaultNull()
->info('Rate limiter name to use for throttling requests.')
->end()
->append($this->createHttpClientCachingSection())
->append($this->createHttpClientRetrySection())
->end()
->end()
Expand All @@ -2148,6 +2150,33 @@ private function addHttpClientSection(ArrayNodeDefinition $rootNode, callable $e
;
}

private function createHttpClientCachingSection(): ArrayNodeDefinition
{
$root = new NodeBuilder();

return $root
->arrayNode('caching')
->info('Caching configuration.')
->canBeEnabled()
->addDefaultsIfNotSet()
->children()
->stringNode('cache_pool')
->info('The taggable cache pool to use for storing the responses.')
->defaultValue('cache.http_client')
->cannotBeEmpty()
->end()
->booleanNode('shared')
->info('Indicates whether the cache is shared (public) or private.')
->defaultTrue()
->end()
->integerNode('max_ttl')
->info('The maximum TTL (in seconds) allowed for cached responses. Null means no cap.')
->defaultNull()
->min(0)
->end()
->end();
}

private function createHttpClientRetrySection(): ArrayNodeDefinition
{
$root = new NodeBuilder();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@
use Symfony\Component\HtmlSanitizer\HtmlSanitizer;
use Symfony\Component\HtmlSanitizer\HtmlSanitizerConfig;
use Symfony\Component\HtmlSanitizer\HtmlSanitizerInterface;
use Symfony\Component\HttpClient\CachingHttpClient;
use Symfony\Component\HttpClient\Exception\ChunkCacheItemNotFoundException;
use Symfony\Component\HttpClient\Messenger\PingWebhookMessageHandler;
use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\HttpClient\Retry\GenericRetryStrategy;
Expand Down Expand Up @@ -2770,6 +2772,8 @@ private function registerHttpClientConfiguration(array $config, ContainerBuilder
$loader->load('http_client.php');

$options = $config['default_options'] ?? [];
$cachingOptions = $options['caching'] ?? ['enabled' => false];
unset($options['caching']);
$rateLimiter = $options['rate_limiter'] ?? null;
unset($options['rate_limiter']);
$retryOptions = $options['retry_failed'] ?? ['enabled' => false];
Expand All @@ -2793,6 +2797,10 @@ private function registerHttpClientConfiguration(array $config, ContainerBuilder
$container->removeAlias(HttpClient::class);
}

if ($this->readConfigEnabled('http_client.caching', $container, $cachingOptions)) {
$this->registerCachingHttpClient($cachingOptions, $options, 'http_client', $container);
}

if (null !== $rateLimiter) {
$this->registerThrottlingHttpClient($rateLimiter, 'http_client', $container);
}
Expand All @@ -2818,6 +2826,8 @@ private function registerHttpClientConfiguration(array $config, ContainerBuilder

$scope = $scopeConfig['scope'] ?? null;
unset($scopeConfig['scope']);
$cachingOptions = $scopeConfig['caching'] ?? ['enabled' => false];
unset($scopeConfig['caching']);
$rateLimiter = $scopeConfig['rate_limiter'] ?? null;
unset($scopeConfig['rate_limiter']);
$retryOptions = $scopeConfig['retry_failed'] ?? ['enabled' => false];
Expand All @@ -2841,6 +2851,10 @@ private function registerHttpClientConfiguration(array $config, ContainerBuilder
;
}

if ($this->readConfigEnabled('http_client.scoped_clients.'.$name.'.caching', $container, $cachingOptions)) {
$this->registerCachingHttpClient($cachingOptions, $scopeConfig, $name, $container);
}

if (null !== $rateLimiter) {
$this->registerThrottlingHttpClient($rateLimiter, $name, $container);
}
Expand Down Expand Up @@ -2882,6 +2896,24 @@ private function registerHttpClientConfiguration(array $config, ContainerBuilder
}
}

private function registerCachingHttpClient(array $options, array $defaultOptions, string $name, ContainerBuilder $container): void
{
if (!class_exists(ChunkCacheItemNotFoundException::class)) {
throw new LogicException('Caching cannot be enabled as version 7.3+ of the HttpClient component is required.');
}

$container
->register($name.'.caching', CachingHttpClient::class)
->setDecoratedService($name, null, 13) // between RetryableHttpClient (10) and ThrottlingHttpClient (15)
->setArguments([
new Reference($name.'.caching.inner'),
new Reference($options['cache_pool']),
$defaultOptions,
$options['shared'],
$options['max_ttl'],
]);
}

private function registerThrottlingHttpClient(string $rateLimiter, string $name, ContainerBuilder $container): void
{
if (!class_exists(ThrottlingHttpClient::class)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\StreamFactoryInterface;
use Symfony\Component\Cache\Adapter\TagAwareAdapter;
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\HttpClient\HttplugClient;
use Symfony\Component\HttpClient\Messenger\PingWebhookMessageHandler;
Expand All @@ -25,6 +26,14 @@

return static function (ContainerConfigurator $container) {
$container->services()
->set('cache.http_client.pool')
->parent('cache.app')
->tag('cache.pool')

->set('cache.http_client', TagAwareAdapter::class)
->args([service('cache.http_client.pool')])
->tag('cache.taggable', ['pool' => 'cache.http_client.pool'])

->set('http_client.transport', HttpClientInterface::class)
->factory([HttpClient::class, 'create'])
->args([
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -730,6 +730,7 @@
<xsd:element name="header" type="http_header" minOccurs="0" maxOccurs="unbounded" />
<xsd:element name="var" type="http_var" minOccurs="0" maxOccurs="unbounded" />
<xsd:element name="peer-fingerprint" type="fingerprint" minOccurs="0" maxOccurs="unbounded" />
<xsd:element name="caching" type="http_client_caching" minOccurs="0" maxOccurs="1" />
<xsd:element name="retry-failed" type="http_client_retry_failed" minOccurs="0" maxOccurs="1" />
<xsd:element name="extra" type="xsd:anyType" minOccurs="0" maxOccurs="unbounded" />
</xsd:choice>
Expand Down Expand Up @@ -757,6 +758,7 @@
<xsd:element name="resolve" type="http_resolve" minOccurs="0" maxOccurs="unbounded" />
<xsd:element name="header" type="http_header" minOccurs="0" maxOccurs="unbounded" />
<xsd:element name="peer-fingerprint" type="fingerprint" minOccurs="0" maxOccurs="unbounded" />
<xsd:element name="caching" type="http_client_caching" minOccurs="0" maxOccurs="1" />
<xsd:element name="retry-failed" type="http_client_retry_failed" minOccurs="0" maxOccurs="1" />
<xsd:element name="extra" type="xsd:anyType" minOccurs="0" maxOccurs="unbounded" />
</xsd:choice>
Expand Down Expand Up @@ -790,6 +792,13 @@
</xsd:choice>
</xsd:complexType>

<xsd:complexType name="http_client_caching">
<xsd:attribute name="enabled" type="xsd:boolean" />
<xsd:attribute name="cache-pool" type="xsd:string" />
<xsd:attribute name="shared" type="xsd:boolean" />
<xsd:attribute name="max-ttl" type="xsd:unsignedInt" />
</xsd:complexType>

<xsd:complexType name="http_client_retry_failed">
<xsd:sequence>
<xsd:element name="http-code" type="http_client_retry_code" minOccurs="0" maxOccurs="unbounded" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

$container->loadFromExtension('framework', [
'annotations' => false,
'http_method_override' => false,
'handle_all_throwables' => true,
'php_errors' => ['log' => true],
'http_client' => [
'default_options' => [
'headers' => ['X-powered' => 'PHP'],
'caching' => [
'cache_pool' => 'foo',
'shared' => false,
'max_ttl' => 2,
],
],
'scoped_clients' => [
'bar' => [
'base_uri' => 'http://example.com',
'caching' => ['cache_pool' => 'baz'],
],
],
],
]);
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:framework="http://symfony.com/schema/dic/symfony"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd
http://symfony.com/schema/dic/symfony http://symfony.com/schema/dic/symfony/symfony-1.0.xsd">

<framework:config http-method-override="false" handle-all-throwables="true">
<framework:annotations enabled="false" />
<framework:php-errors log="true" />
<framework:http-client>
<framework:default-options>
<framework:header name="X-powered">PHP</framework:header>
<framework:caching enabled="true" cache-pool="foo" shared="false" max-ttl="2"/>
</framework:default-options>
<framework:scoped-client name="bar" base-uri="http://example.com">
<framework:caching cache-pool="baz"/>
</framework:scoped-client>
</framework:http-client>
</framework:config>
</container>
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
framework:
annotations: false
http_method_override: false
handle_all_throwables: true
php_errors:
log: true
http_client:
default_options:
headers:
X-powered: PHP
caching:
cache_pool: foo
shared: false
max_ttl: 2
scoped_clients:
bar:
base_uri: http://example.com
caching:
cache_pool: baz
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@
use Symfony\Component\HtmlSanitizer\HtmlSanitizer;
use Symfony\Component\HtmlSanitizer\HtmlSanitizerConfig;
use Symfony\Component\HtmlSanitizer\HtmlSanitizerInterface;
use Symfony\Component\HttpClient\CachingHttpClient;
use Symfony\Component\HttpClient\Exception\ChunkCacheItemNotFoundException;
use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\HttpClient\RetryableHttpClient;
use Symfony\Component\HttpClient\ScopingHttpClient;
Expand Down Expand Up @@ -2185,6 +2187,39 @@ public function testHttpClientOverrideDefaultOptions()
$this->assertEquals($expected, $container->getDefinition('foo')->getArgument(2));
}

public function testCachingHttpClient()
{
if (!class_exists(ChunkCacheItemNotFoundException::class)) {
$this->expectException(LogicException::class);
}

$container = $this->createContainerFromFile('http_client_caching');

$this->assertTrue($container->hasDefinition('http_client.caching'));
$definition = $container->getDefinition('http_client.caching');
$this->assertSame(CachingHttpClient::class, $definition->getClass());
$this->assertSame('http_client', $definition->getDecoratedService()[0]);
$this->assertCount(5, $arguments = $definition->getArguments());
$this->assertInstanceOf(Reference::class, $arguments[0]);
$this->assertSame('http_client.caching.inner', (string) $arguments[0]);
$this->assertInstanceOf(Reference::class, $arguments[1]);
$this->assertSame('foo', (string) $arguments[1]);
$this->assertArrayHasKey('headers', $arguments[2]);
$this->assertSame(['X-powered' => 'PHP'], $arguments[2]['headers']);
$this->assertFalse($arguments[3]);
$this->assertSame(2, $arguments[4]);

$this->assertTrue($container->hasDefinition('bar.caching'));
$definition = $container->getDefinition('bar.caching');
$this->assertSame(CachingHttpClient::class, $definition->getClass());
$this->assertSame('bar', $definition->getDecoratedService()[0]);
$arguments = $definition->getArguments();
$this->assertInstanceOf(Reference::class, $arguments[0]);
$this->assertSame('bar.caching.inner', (string) $arguments[0]);
$this->assertInstanceOf(Reference::class, $arguments[1]);
$this->assertSame('baz', (string) $arguments[1]);
}

public function testHttpClientRetry()
{
$container = $this->createContainerFromFile('http_client_retry');
Expand Down
2 changes: 2 additions & 0 deletions src/Symfony/Component/HttpClient/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ CHANGELOG
---

* Deprecate using amphp/http-client < 5
* Add RFC 9111–based caching support to `CachingHttpClient`
* Deprecate passing an instance of `StoreInterface` as `$cache` argument to `CachingHttpClient` constructor

7.3
---
Expand Down
35 changes: 35 additions & 0 deletions src/Symfony/Component/HttpClient/Caching/Freshness.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\HttpClient\Caching;

/**
* @internal
*/
enum Freshness
{
/**
* The cached response is fresh and can be used without revalidation.
*/
case Fresh;
/**
* The cached response is stale and must be revalidated before use.
*/
case MustRevalidate;
/**
* The cached response is stale and should not be used.
*/
case Stale;
/**
* The cached response is stale but may be used as a fallback in case of errors.
*/
case StaleButUsable;
}
Loading
Loading