diff --git a/CHANGELOG.md b/CHANGELOG.md index d99f448b..73e8cf82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ Changelog ========= +2.15.0 +------ + +* Added Cloudflare proxy client. + 2.14.0 ------ diff --git a/Resources/doc/reference/configuration/proxy-client.rst b/Resources/doc/reference/configuration/proxy-client.rst index efb1fb68..392bd634 100644 --- a/Resources/doc/reference/configuration/proxy-client.rst +++ b/Resources/doc/reference/configuration/proxy-client.rst @@ -12,7 +12,8 @@ The proxy client is also directly available as a service. The default client can be autowired with the ``FOS\HttpCache\ProxyClient\ProxyClient`` type declaration or the service ``fos_http_cache.default_proxy_client``. Specific clients, if configured, are available as ``fos_http_cache.proxy_client.varnish`` -, ``fos_http_cache.proxy_client.nginx`` or ``fos_http_cache.proxy_client.symfony``). +, ``fos_http_cache.proxy_client.nginx``, ``fos_http_cache.proxy_client.symfony`` +or ``fos_http_cache.proxy_client.cloudflare``). If you need to adjust the proxy client, you can also configure the ``CacheManager`` with a :ref:`custom proxy client ` that you defined as a @@ -236,6 +237,43 @@ HTTP method for sending purge requests to the Symfony HttpCache. Make sure to configure the purge plugin for your HttpCache with the matching header if you change this. +cloudflare +------- + +.. code-block:: yaml + + # config/packages/fos_http_cache.yaml + fos_http_cache: + proxy_client: + cloudflare: + zone_identifier: '' + authentication_token: '' + http: + servers: + - 'https://api.cloudflare.com' + +``authentication_token`` +""""""""""""""""""""""" + +**type**: ``string`` + +User API token for authentication against Cloudflare APIs, requires ``Zone.Cache`` Purge permissions. + +``zone_identifier`` +""""""""""""""""" + +**type**: ``string`` + +Identifier for the Cloudflare zone you want to purge the cache for. + +``http.servers`` +"""""""""""""""" + +**type**: ``array`` **default**: ``['https://api.cloudflare.com']`` + +List of Cloudflare API endpoints to use for purging the cache. You can use this to specify a different +endpoint for testing purposes. + .. _configuration_noop_proxy_client: noop diff --git a/Resources/doc/spelling_word_list.txt b/Resources/doc/spelling_word_list.txt index 649fb170..9403674f 100644 --- a/Resources/doc/spelling_word_list.txt +++ b/Resources/doc/spelling_word_list.txt @@ -5,6 +5,7 @@ autoconfigure autoconfigured backend cacheable +cloudflare ETag friendsofsymfony github diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index d2cc67e3..9afd2e1a 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -412,7 +412,7 @@ private function addProxyClientSection(ArrayNodeDefinition $rootNode) ->arrayNode('proxy_client') ->children() ->enumNode('default') - ->values(['varnish', 'nginx', 'symfony', 'noop']) + ->values(['varnish', 'nginx', 'symfony', 'cloudflare', 'noop']) ->info('If you configure more than one proxy client, you need to specify which client is the default.') ->end() ->arrayNode('varnish') @@ -482,6 +482,18 @@ private function addProxyClientSection(ArrayNodeDefinition $rootNode) ->end() ->end() + ->arrayNode('cloudflare') + ->children() + ->scalarNode('authentication_token') + ->info('API authorization token, requires Zone.Cache Purge permissions') + ->end() + ->scalarNode('zone_identifier') + ->info('Identifier for your Cloudflare zone you want to purge the cache for') + ->end() + ->append($this->getCloudflareHttpDispatcherNode()) + ->end() + ->end() + ->booleanNode('noop')->end() ->end() ->validate() @@ -500,7 +512,7 @@ private function addProxyClientSection(ArrayNodeDefinition $rootNode) throw new InvalidConfigurationException(sprintf('You can only set one of "http.servers" or "http.servers_from_jsonenv" but not both to avoid ambiguity for the proxy "%s"', $proxyName)); } - if (!\in_array($proxyName, ['noop', 'default', 'symfony'])) { + if (!\in_array($proxyName, ['noop', 'default', 'symfony', 'cloudflare'])) { if (!$arrayServersConfigured && !$jsonServersConfigured) { throw new InvalidConfigurationException(sprintf('The "http.servers" or "http.servers_from_jsonenv" section must be defined for the proxy "%s"', $proxyName)); } @@ -564,6 +576,36 @@ private function getHttpDispatcherNode() return $node; } + private function getCloudflareHttpDispatcherNode() + { + $treeBuilder = new TreeBuilder('http'); + + // Keep compatibility with symfony/config < 4.2 + if (!method_exists($treeBuilder, 'getRootNode')) { + $node = $treeBuilder->root('http'); + } else { + $node = $treeBuilder->getRootNode(); + } + + $node + ->addDefaultsIfNotSet() + ->children() + ->arrayNode('servers') + ->info('Addresses of the hosts the caching proxy is running on. The values may be hostnames or ips, and with :port if not the default port 80.') + ->useAttributeAsKey('name') + ->requiresAtLeastOneElement() + ->defaultValue(['https://api.cloudflare.com']) + ->prototype('scalar')->end() + ->end() + ->scalarNode('http_client') + ->defaultNull() + ->info('Httplug async client service name to use for sending the requests.') + ->end() + ->end(); + + return $node; + } + private function addTestSection(ArrayNodeDefinition $rootNode) { $rootNode diff --git a/src/DependencyInjection/FOSHttpCacheExtension.php b/src/DependencyInjection/FOSHttpCacheExtension.php index 36a7fa1e..c3a1857b 100644 --- a/src/DependencyInjection/FOSHttpCacheExtension.php +++ b/src/DependencyInjection/FOSHttpCacheExtension.php @@ -337,6 +337,9 @@ private function loadProxyClient(ContainerBuilder $container, XmlFileLoader $loa if (isset($config['symfony'])) { $this->loadSymfony($container, $loader, $config['symfony']); } + if (isset($config['cloudflare'])) { + $this->loadCloudflare($container, $loader, $config['cloudflare']); + } if (isset($config['noop'])) { $loader->load('noop.xml'); } @@ -455,6 +458,19 @@ private function loadSymfony(ContainerBuilder $container, XmlFileLoader $loader, $loader->load('symfony.xml'); } + private function loadCloudflare(ContainerBuilder $container, XmlFileLoader $loader, array $config) + { + $this->createHttpDispatcherDefinition($container, $config['http'], 'fos_http_cache.proxy_client.cloudflare.http_dispatcher'); + $options = [ + 'authentication_token' => $config['authentication_token'], + 'zone_identifier' => $config['zone_identifier'], + ]; + + $container->setParameter('fos_http_cache.proxy_client.cloudflare.options', $options); + + $loader->load('cloudflare.xml'); + } + /** * @param array $config Configuration section for the tags node * @param string $client Name of the client used with the cache manager, @@ -462,12 +478,12 @@ private function loadSymfony(ContainerBuilder $container, XmlFileLoader $loader, */ private function loadCacheTagging(ContainerBuilder $container, XmlFileLoader $loader, array $config, $client) { - if ('auto' === $config['enabled'] && !in_array($client, ['varnish', 'symfony'])) { + if ('auto' === $config['enabled'] && !in_array($client, ['varnish', 'symfony', 'cloudflare'])) { $container->setParameter('fos_http_cache.compiler_pass.tag_annotations', false); return; } - if (!in_array($client, ['varnish', 'symfony', 'custom', 'noop'])) { + if (!in_array($client, ['varnish', 'symfony', 'cloudflare', 'custom', 'noop'])) { throw new InvalidConfigurationException(sprintf('You can not enable cache tagging with the %s client', $client)); } @@ -609,6 +625,10 @@ private function getDefaultProxyClient(array $config) return 'symfony'; } + if (isset($config['cloudflare'])) { + return 'cloudflare'; + } + if (isset($config['noop'])) { return 'noop'; } diff --git a/src/Resources/config/cloudflare.xml b/src/Resources/config/cloudflare.xml new file mode 100644 index 00000000..3e38a0db --- /dev/null +++ b/src/Resources/config/cloudflare.xml @@ -0,0 +1,16 @@ + + + + + + + + %fos_http_cache.proxy_client.cloudflare.options% + + + + diff --git a/tests/Resources/Fixtures/config/cloudflare.php b/tests/Resources/Fixtures/config/cloudflare.php new file mode 100644 index 00000000..5a695ae5 --- /dev/null +++ b/tests/Resources/Fixtures/config/cloudflare.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +$container->loadFromExtension('fos_http_cache', [ + 'proxy_client' => [ + 'cloudflare' => [ + 'authentication_token' => 'mytoken', + 'zone_identifier' => 'myzone', + ], + ], +]); diff --git a/tests/Resources/Fixtures/config/cloudflare.xml b/tests/Resources/Fixtures/config/cloudflare.xml new file mode 100644 index 00000000..11614046 --- /dev/null +++ b/tests/Resources/Fixtures/config/cloudflare.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/tests/Resources/Fixtures/config/cloudflare.yml b/tests/Resources/Fixtures/config/cloudflare.yml new file mode 100644 index 00000000..4a6c35b0 --- /dev/null +++ b/tests/Resources/Fixtures/config/cloudflare.yml @@ -0,0 +1,6 @@ +fos_http_cache: + + proxy_client: + cloudflare: + authentication_token: mytoken + zone_identifier: myzone diff --git a/tests/Unit/DependencyInjection/ConfigurationTest.php b/tests/Unit/DependencyInjection/ConfigurationTest.php index fab96e55..f03642a9 100644 --- a/tests/Unit/DependencyInjection/ConfigurationTest.php +++ b/tests/Unit/DependencyInjection/ConfigurationTest.php @@ -298,6 +298,35 @@ public function testSupportsSymfony() } } + public function testSupportsCloudflare() + { + $expectedConfiguration = $this->getEmptyConfig(); + $expectedConfiguration['proxy_client'] = [ + 'cloudflare' => [ + 'authentication_token' => 'mytoken', + 'zone_identifier' => 'myzone', + 'http' => ['servers' => ['https://api.cloudflare.com'], 'http_client' => null], + ], + ]; + $expectedConfiguration['cache_manager']['enabled'] = 'auto'; + $expectedConfiguration['cache_manager']['generate_url_type'] = 'auto'; + $expectedConfiguration['tags']['enabled'] = 'auto'; + $expectedConfiguration['invalidation']['enabled'] = 'auto'; + $expectedConfiguration['user_context']['logout_handler']['enabled'] = false; + + $formats = array_map(function ($path) { + return __DIR__.'/../../Resources/Fixtures/'.$path; + }, [ + 'config/cloudflare.yml', + 'config/cloudflare.xml', + 'config/cloudflare.php', + ]); + + foreach ($formats as $format) { + $this->assertProcessedConfigurationEquals($expectedConfiguration, [$format]); + } + } + public function testEmptyServerConfigurationIsNotAllowed() { $this->expectException(InvalidConfigurationException::class); diff --git a/tests/Unit/DependencyInjection/FOSHttpCacheExtensionTest.php b/tests/Unit/DependencyInjection/FOSHttpCacheExtensionTest.php index d8a42d50..969b99c5 100644 --- a/tests/Unit/DependencyInjection/FOSHttpCacheExtensionTest.php +++ b/tests/Unit/DependencyInjection/FOSHttpCacheExtensionTest.php @@ -146,6 +146,26 @@ public function testConfigLoadSymfonyWithKernelDispatcher() $this->assertSame(KernelDispatcher::class, $container->getDefinition('fos_http_cache.proxy_client.symfony.http_dispatcher')->getClass()); } + public function testConfigLoadCloudflare() + { + $container = $this->createContainer(); + $this->extension->load([ + [ + 'proxy_client' => [ + 'cloudflare' => [ + 'authentication_token' => 'test', + 'zone_identifier' => 'test', + ], + ], + ], + ], $container); + + $this->assertFalse($container->hasDefinition('fos_http_cache.proxy_client.varnish')); + $this->assertTrue($container->hasDefinition('fos_http_cache.proxy_client.cloudflare')); + $this->assertTrue($container->hasAlias('fos_http_cache.default_proxy_client')); + $this->assertTrue($container->hasDefinition('fos_http_cache.event_listener.invalidation')); + } + public function testConfigLoadNoop() { $container = $this->createContainer();