Skip to content

Commit

Permalink
Add Fastly support (#597)
Browse files Browse the repository at this point in the history
  • Loading branch information
DemigodCode committed Oct 10, 2023
1 parent 521e8ef commit 7bcc34f
Show file tree
Hide file tree
Showing 12 changed files with 240 additions and 7 deletions.
1 change: 1 addition & 0 deletions Resources/doc/features/invalidation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Invalidation
* :ref:`Varnish <foshttpcache:varnish configuration>`
* :ref:`Nginx <foshttpcache:nginx configuration>` (except regular expressions)
* :doc:`symfony-http-cache` (except regular expressions)
* Fastly (except regular expressions)

**Preparation**:

Expand Down
1 change: 1 addition & 0 deletions Resources/doc/features/tagging.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Tagging

* :ref:`Varnish <foshttpcache:varnish_tagging>`
* :ref:`Symfony <foshttpcache:symfony_httpcache_tagging>`
* Fastly

If your application has many intricate relationships between cached items,
which makes it complex to invalidate them by route, cache tagging will be
Expand Down
37 changes: 36 additions & 1 deletion Resources/doc/reference/configuration/proxy-client.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ 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``, ``fos_http_cache.proxy_client.symfony``
or ``fos_http_cache.proxy_client.cloudflare``).
, ``fos_http_cache.proxy_client.cloudflare`` or ``fos_http_cache.proxy_client.fastly``).

If you need to adjust the proxy client, you can also configure the ``CacheManager``
with a :ref:`custom proxy client <custom_proxy_client>` that you defined as a
Expand Down Expand Up @@ -325,6 +325,41 @@ the `AWS Async documentation_`. It can not be used with the ``client`` option.
Service identifier of a `AsyncAws\CloudFront\CloudFrontClient` client. More information is available on the
`AWS Async documentation_`. It can not be used with the ``configuration`` option.

.. _configuration_fastly_proxy_client:

Fastly
----------
.. code-block:: yaml
# config/packages/fos_http_cache.yaml
fos_http_cache:
proxy_client:
fastly:
service_identifier: '<my-service-identifier>'
authentication_token: '<my-authentication-token>'
soft_purge: true
``service_identifier``
"""""""""""""""""""""

**type**: ``string``

Identifier for the Fastly service you want to purge the cache for.

``authentication_token``
"""""""""""""""""""""

**type**: ``string``

Authentication token (API Token) which can be created in the profile section of your account

``soft_purge``
"""""""""""""""""""""

**type**: ``boolean`` **default**: ``true``

Boolean for doing soft purges or not on tag & URL purging. Soft purges expires the cache unlike hard purge (removal), and allow grace/stale handling within Fastly VCL.

.. _configuration_noop_proxy_client:

noop
Expand Down
1 change: 1 addition & 0 deletions Resources/doc/spelling_word_list.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ cacheable
cloudflare
cloudfront
ETag
Fastly
friendsofsymfony
github
hardcoded
Expand Down
78 changes: 74 additions & 4 deletions src/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,17 @@ function ($v) {
}
)
->then(function ($v) {
$v['tags']['response_header'] = $this->isVarnishXkey($v) ? 'xkey' : TagHeaderFormatter::DEFAULT_HEADER_NAME;
switch (true) {
case $this->isVarnishXkey($v):
$v['tags']['response_header'] = 'xkey';
break;
case $this->isFastly($v):
$v['tags']['response_header'] = 'Surrogate-Key';
break;
default:
$v['tags']['response_header'] = TagHeaderFormatter::DEFAULT_HEADER_NAME;
break;
}

return $v;
})
Expand All @@ -188,7 +198,15 @@ function ($v) {
}
)
->then(function ($v) {
$v['tags']['separator'] = $this->isVarnishXkey($v) ? ' ' : ',';
switch (true) {
case $this->isVarnishXkey($v):
case $this->isFastly($v):
$v['tags']['separator'] = ' ';
break;
default:
$v['tags']['separator'] = ',';
break;
}

return $v;
})
Expand Down Expand Up @@ -216,6 +234,12 @@ private function isVarnishXkey(array $v): bool
;
}

private function isFastly(array $v): bool
{
return array_key_exists('proxy_client', $v)
&& array_key_exists('fastly', $v['proxy_client']);
}

private function addCacheableResponseSection(ArrayNodeDefinition $rootNode)
{
$rootNode
Expand Down Expand Up @@ -413,7 +437,7 @@ private function addProxyClientSection(ArrayNodeDefinition $rootNode)
->arrayNode('proxy_client')
->children()
->enumNode('default')
->values(['varnish', 'nginx', 'symfony', 'cloudflare', 'cloudfront', 'noop'])
->values(['varnish', 'nginx', 'symfony', 'cloudflare', 'cloudfront', 'fastly', 'noop'])
->info('If you configure more than one proxy client, you need to specify which client is the default.')
->end()
->arrayNode('varnish')
Expand Down Expand Up @@ -518,6 +542,23 @@ private function addProxyClientSection(ArrayNodeDefinition $rootNode)
->end()
->end()

->arrayNode('fastly')
->info('Configure a client to interact with Fastly.')
->children()
->scalarNode('service_identifier')
->info('Identifier for your Fastly service account.')
->end()
->scalarNode('authentication_token')
->info('User token for authentication against Fastly APIs.')
->end()
->scalarNode('soft_purge')
->info('Boolean for doing soft purges or not on tag & URL purging. Soft purges expires the cache unlike hard purge (removal), and allow grace/stale handling within Fastly VCL.')
->defaultValue(true)
->end()
->append($this->getFastlyHttpDispatcherNode())
->end()
->end()

->booleanNode('noop')->end()
->end()
->validate()
Expand All @@ -536,7 +577,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', 'cloudflare', 'cloudfront'])) {
if (!\in_array($proxyName, ['noop', 'default', 'symfony', 'cloudflare', 'cloudfront', 'fastly'])) {
if (!$arrayServersConfigured && !$jsonServersConfigured) {
throw new InvalidConfigurationException(sprintf('The "http.servers" or "http.servers_from_jsonenv" section must be defined for the proxy "%s"', $proxyName));
}
Expand Down Expand Up @@ -636,6 +677,35 @@ private function getCloudflareHttpDispatcherNode()
return $node;
}

private function getFastlyHttpDispatcherNode()
{
$treeBuilder = new TreeBuilder('http');

$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. For fastly, you normally do not need to change the default value.')
->useAttributeAsKey('name')
->requiresAtLeastOneElement()
->defaultValue(['https://api.fastly.com'])
->prototype('scalar')->end()
->end()
->scalarNode('base_url')
->defaultValue('service')
->info('Default host name and optional path for path based invalidation.')
->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
Expand Down
26 changes: 24 additions & 2 deletions src/DependencyInjection/FOSHttpCacheExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,9 @@ private function loadProxyClient(ContainerBuilder $container, XmlFileLoader $loa
if (isset($config['cloudfront'])) {
$this->loadCloudfront($container, $loader, $config['cloudfront']);
}
if (isset($config['fastly'])) {
$this->loadFastly($container, $loader, $config['fastly']);
}
if (isset($config['noop'])) {
$loader->load('noop.xml');
}
Expand Down Expand Up @@ -498,19 +501,34 @@ private function loadCloudfront(ContainerBuilder $container, XmlFileLoader $load
$loader->load('cloudfront.xml');
}

private function loadFastly(ContainerBuilder $container, XmlFileLoader $loader, array $config)
{
$this->createHttpDispatcherDefinition($container, $config['http'], 'fos_http_cache.proxy_client.fastly.http_dispatcher');

$options = [
'service_identifier' => $config['service_identifier'],
'authentication_token' => $config['authentication_token'],
'soft_purge' => $config['soft_purge'],
];

$container->setParameter('fos_http_cache.proxy_client.fastly.options', $options);

$loader->load('fastly.xml');
}

/**
* @param array $config Configuration section for the tags node
* @param string $client Name of the client used with the cache manager,
* "custom" when a custom client is used
*/
private function loadCacheTagging(ContainerBuilder $container, XmlFileLoader $loader, array $config, $client)
{
if ('auto' === $config['enabled'] && !in_array($client, ['varnish', 'symfony', 'cloudflare'])) {
if ('auto' === $config['enabled'] && !in_array($client, ['varnish', 'symfony', 'cloudflare', 'fastly'])) {
$container->setParameter('fos_http_cache.compiler_pass.tag_annotations', false);

return;
}
if (!in_array($client, ['varnish', 'symfony', 'cloudflare', 'custom', 'noop'])) {
if (!in_array($client, ['varnish', 'symfony', 'cloudflare', 'custom', 'fastly', 'noop'])) {
throw new InvalidConfigurationException(sprintf('You can not enable cache tagging with the %s client', $client));
}

Expand Down Expand Up @@ -660,6 +678,10 @@ private function getDefaultProxyClient(array $config)
return 'cloudfront';
}

if (isset($config['fastly'])) {
return 'fastly';
}

if (isset($config['noop'])) {
return 'noop';
}
Expand Down
16 changes: 16 additions & 0 deletions src/Resources/config/fastly.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?xml version="1.0" ?>

<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

<services>
<service id="fos_http_cache.proxy_client.fastly"
class="FOS\HttpCache\ProxyClient\Fastly"
public="false">
<argument type="service" id="fos_http_cache.proxy_client.fastly.http_dispatcher"/>
<argument>%fos_http_cache.proxy_client.fastly.options%</argument>
</service>
</services>

</container>
19 changes: 19 additions & 0 deletions tests/Resources/Fixtures/config/fastly.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

/*
* This file is part of the FOSHttpCacheBundle package.
*
* (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
*
* 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' => [
'fastly' => [
'service_identifier' => 'myServiceIdentifier',
'authentication_token' => 'mytoken',
],
],
]);
11 changes: 11 additions & 0 deletions tests/Resources/Fixtures/config/fastly.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services">

<config xmlns="http://example.org/schema/dic/fos_http_cache">
<proxy-client>
<fastly authentication-token="mytoken" service-identifier="myServiceIdentifier">
</fastly>
</proxy-client>

</config>
</container>
6 changes: 6 additions & 0 deletions tests/Resources/Fixtures/config/fastly.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
fos_http_cache:

proxy_client:
fastly:
service_identifier: myServiceIdentifier
authentication_token: mytoken
31 changes: 31 additions & 0 deletions tests/Unit/DependencyInjection/ConfigurationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,37 @@ public function testCloudfrontConfigurationWithClientIsNotAllowed()
(new Processor())->processConfiguration($configuration, ['fos_http_cache' => $params]);
}

public function testSupportsFastly()
{
$expectedConfiguration = $this->getEmptyConfig();
$expectedConfiguration['proxy_client'] = [
'fastly' => [
'service_identifier' => 'myServiceIdentifier',
'authentication_token' => 'mytoken',
'soft_purge' => true,
'http' => ['servers' => ['https://api.fastly.com'], 'http_client' => null, 'base_url' => 'service'],
],
];
$expectedConfiguration['cache_manager']['enabled'] = 'auto';
$expectedConfiguration['cache_manager']['generate_url_type'] = 'auto';
$expectedConfiguration['tags']['enabled'] = 'auto';
$expectedConfiguration['tags']['response_header'] = 'Surrogate-Key';
$expectedConfiguration['tags']['separator'] = ' ';
$expectedConfiguration['invalidation']['enabled'] = 'auto';

$formats = array_map(function ($path) {
return __DIR__.'/../../Resources/Fixtures/'.$path;
}, [
'config/fastly.yml',
'config/fastly.xml',
'config/fastly.php',
]);

foreach ($formats as $format) {
$this->assertProcessedConfigurationEquals($expectedConfiguration, [$format]);
}
}

public function testEmptyServerConfigurationIsNotAllowed()
{
$this->expectException(InvalidConfigurationException::class);
Expand Down
20 changes: 20 additions & 0 deletions tests/Unit/DependencyInjection/FOSHttpCacheExtensionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,26 @@ public function testConfigLoadCloudfrontWithClient()
$this->assertTrue($container->hasDefinition('fos_http_cache.event_listener.invalidation'));
}

public function testConfigLoadFastly()
{
$container = $this->createContainer();
$this->extension->load([
[
'proxy_client' => [
'fastly' => [
'service_identifier' => 'test',
'authentication_token' => 'test',
],
],
],
], $container);

$this->assertFalse($container->hasDefinition('fos_http_cache.proxy_client.varnish'));
$this->assertTrue($container->hasDefinition('fos_http_cache.proxy_client.fastly'));
$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();
Expand Down

0 comments on commit 7bcc34f

Please sign in to comment.