Skip to content

Commit

Permalink
Merge pull request #193 from FriendsOfSymfony/symfony-cache-client
Browse files Browse the repository at this point in the history
adding client for the symfony built-in reverse proxy HttpCache
  • Loading branch information
ddeboer committed May 30, 2015
2 parents 6d27bc0 + df54ddc commit 64c4acd
Show file tree
Hide file tree
Showing 14 changed files with 867 additions and 237 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
Changelog
=========

1.3.3
-----

* **2015-05-08** Added a client for the Symfony built-in HttpCache

1.3.0
-----

Expand Down
34 changes: 30 additions & 4 deletions doc/proxy-clients.rst
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Caching Proxy Clients
=====================

This library ships with clients for the Varnish and NGINX caching proxy. You
This library ships with clients for the Varnish, NGINX and Symfony built-in caching proxies. You
can use the clients either wrapped by the :doc:`cache invalidator <cache-invalidator>`
(recommended), or directly for low-level access to invalidation functionality.

Expand Down Expand Up @@ -35,16 +35,16 @@ include that port in the base URL::

.. note::

To use the client, you need to :doc:`configure Varnish <varnish-configuration>` accordingly.
To make invalidation work, you need to :doc:`configure Varnish <varnish-configuration>` accordingly.

NGINX Client
~~~~~~~~~~~~

At minimum, supply an array containing IPs or hostnames of the NGINX servers
that you want to send invalidation requests to. Make sure to include the port
NGINX runs on if it is not port 80::
NGINX runs on if it is not the default::

use FOS\HttpCache\Invalidation\Nginx;
use FOS\HttpCache\ProxyClient\Nginx;

$servers = array('10.0.0.1', '10.0.0.2:8088'); // Port 80 assumed for 10.0.0.1
$nginx = new Nginx($servers);
Expand All @@ -64,6 +64,29 @@ supply that location to the class as the third parameter::

To use the client, you need to :doc:`configure NGINX <nginx-configuration>` accordingly.

Symfony Client
~~~~~~~~~~~~~~

At minimum, supply an array containing IPs or hostnames of your web servers
running Symfony. Provide the direct access to the web server without any other
proxies that might block invalidation requests. Make sure to include the port
the web server runs on if it is not the default::

use FOS\HttpCache\ProxyClient\Symfony;

$servers = array('10.0.0.1', '10.0.0.2:8088'); // Port 80 assumed for 10.0.0.1
$client = new Symfony($servers);

This is sufficient for invalidating absolute URLs. If you also wish to
invalidate relative paths, supply the hostname (or base URL) where your website
is available as the second parameter::

$client = new Symfony($servers, 'my-cool-app.com');

.. note::

To make invalidation work, you need to :doc:`use the EventDispatchingHttpCache <symfony-cache-configuration>`.

Using the Clients
-----------------

Expand Down Expand Up @@ -200,4 +223,7 @@ send a basic authentication header, you can inject a custom Guzzle client::
$servers = array('10.0.0.1');
$varnish = new Varnish($servers, '/baseUrl', $client);

The Symfony client accepts a guzzle client as the 3rd parameter as well, NGINX
accepts it as 4th parameter.

.. _Guzzle client: http://guzzle3.readthedocs.org/
4 changes: 2 additions & 2 deletions doc/symfony-cache-configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,6 @@ not the recommended way. You would need to adjust every place you instantiate
the cache. Instead, overwrite the constructor of AppCache and register the
subscribers there. A simple cache will look like this::

require_once __DIR__.'/AppKernel.php';

use FOS\HttpCache\SymfonyCache\EventDispatchingHttpCache;
use FOS\HttpCache\SymfonyCache\UserContextSubscriber;

Expand All @@ -57,6 +55,8 @@ subscribers there. A simple cache will look like this::
parent::__construct($kernel, $cacheDir);

$this->addSubscriber(new UserContextSubscriber());
$this->addSubscriber(new PurgeSubscriber());
$this->addSubscriber(new RefreshSubscriber());
}
}

Expand Down
89 changes: 89 additions & 0 deletions src/ProxyClient/Symfony.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<?php

/*
* This file is part of the FOSHttpCache 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.
*/

namespace FOS\HttpCache\ProxyClient;

use FOS\HttpCache\Exception\InvalidArgumentException;
use FOS\HttpCache\Exception\MissingHostException;
use FOS\HttpCache\ProxyClient\Invalidation\BanInterface;
use FOS\HttpCache\ProxyClient\Invalidation\PurgeInterface;
use FOS\HttpCache\ProxyClient\Invalidation\RefreshInterface;
use FOS\HttpCache\SymfonyCache\PurgeSubscriber;
use Guzzle\Http\ClientInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

/**
* Symfony HttpCache invalidator.
*
* @author David de Boer <david@driebit.nl>
* @author David Buchmann <mail@davidbu.ch>
*/
class Symfony extends AbstractProxyClient implements PurgeInterface, RefreshInterface
{
const HTTP_METHOD_REFRESH = 'GET';

/**
* The options configured in the constructor argument or default values.
*
* @var array
*/
private $options;

/**
* {@inheritDoc}
*
* When creating the client, you can configure options:
*
* - purge_method: HTTP method that identifies purge requests.
*
* @param array $options The purge_method that should be used.
*/
public function __construct(array $servers, $baseUrl = null, ClientInterface $client = null, array $options = array())
{
parent::__construct($servers, $baseUrl, $client);

$resolver = new OptionsResolver();
$resolver->setDefaults(array(
'purge_method' => PurgeSubscriber::DEFAULT_PURGE_METHOD,
));

$this->options = $resolver->resolve($options);
}

/**
* {@inheritdoc}
*/
public function purge($url, array $headers = array())
{
$this->queueRequest($this->options['purge_method'], $url, $headers);

return $this;
}

/**
* {@inheritdoc}
*/
public function refresh($url, array $headers = array())
{
$headers = array_merge($headers, array('Cache-Control' => 'no-cache'));
$this->queueRequest(self::HTTP_METHOD_REFRESH, $url, $headers);

return $this;
}

/**
* {@inheritdoc}
*/
protected function getAllowedSchemes()
{
return array('http', 'https');
}
}
4 changes: 3 additions & 1 deletion src/SymfonyCache/PurgeSubscriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
*/
class PurgeSubscriber extends AccessControlledSubscriber
{
const DEFAULT_PURGE_METHOD = 'PURGE';

/**
* The options configured in the constructor argument or default values.
*
Expand Down Expand Up @@ -59,7 +61,7 @@ public function __construct(array $options = array())
$resolver->setDefaults(array(
'purge_client_matcher' => null,
'purge_client_ips' => null,
'purge_method' => 'PURGE',
'purge_method' => static::DEFAULT_PURGE_METHOD,
));

$this->options = $resolver->resolve($options);
Expand Down
65 changes: 65 additions & 0 deletions src/Test/Proxy/SymfonyProxy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php

/*
* This file is part of the FOSHttpCache 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.
*/

namespace FOS\HttpCache\Test\Proxy;

/**
* Controls the Symfony HttpCache proxy server.
*
* SYMFONY_CACHE_DIR directory to use for cache
* (default sys_get_temp_dir() + '/foshttpcache-symfony')
*/
class SymfonyProxy implements ProxyInterface
{
/**
* Get Symfony cache directory
*
* @return string
*/
public function getCacheDir()
{
return defined('SYMFONY_CACHE_DIR') ? SYMFONY_CACHE_DIR : sys_get_temp_dir() . '/foshttpcache-symfony';
}

/**
* Start the proxy server
*/
public function start()
{
$this->clear();
}

/**
* Stop the proxy server
*/
public function stop()
{
// nothing to do
}

/**
* Clear all cached content from the proxy server
*/
public function clear()
{
if (is_dir($this->getCacheDir())) {
$path = realpath($this->getCacheDir());
if (!$this->getCacheDir() || '/' == $path) {
throw new \Exception('Invalid test setup, the cache dir is ' . $path);
}
if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
system('DEL /S '.$path);
} else {
system('rm -r '.$path);
}
}
}
}
95 changes: 95 additions & 0 deletions src/Test/SymfonyTestCase.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?php

/*
* This file is part of the FOSHttpCache 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.
*/

namespace FOS\HttpCache\Test;

use FOS\HttpCache\ProxyClient\Symfony;
use FOS\HttpCache\Test\Proxy\SymfonyProxy;

/**
* A phpunit base class to write functional tests with the symfony HttpCache.
*
* The webserver with symfony is to be started with the WebServerListener.
*
* You can define constants in your phpunit to control how this test behaves.
*
* To define constants in the phpunit file, use this syntax:
* <php>
* <const name="WEB_SERVER_PORT" value="/tmp/foo" />
* </php>
*
* WEB_SERVER_PORT port the PHP webserver listens on (default 8080)
*
* Note that the SymfonyProxy also defines a SYMFONY_CACHE_DIR constant.
*/
abstract class SymfonyTestCase extends ProxyTestCase
{
/**
* @var Symfony
*/
protected $proxyClient;

/**
* @var SymfonyProxy
*/
protected $proxy;

/**
* Get server port
*
* @return int
*
* @throws \Exception
*/
protected function getCachingProxyPort()
{
if (!defined('WEB_SERVER_PORT')) {
throw new \Exception('Set WEB_SERVER_PORT in your phpunit.xml');
}

return WEB_SERVER_PORT;
}

/**
* {@inheritdoc}
*/
protected function getProxy()
{
if (null === $this->proxy) {
$this->proxy = new SymfonyProxy();
}

return $this->proxy;
}

/**
* Get Symfony proxy client
*
* We use a non-default method for PURGE because the built-in PHP webserver
* does not allow arbitrary HTTP methods.
* https://github.com/php/php-src/blob/PHP-5.4.1/sapi/cli/php_http_parser.c#L78-L102
*
* @return Symfony
*/
protected function getProxyClient()
{
if (null === $this->proxyClient) {
$this->proxyClient = new Symfony(
array('http://127.0.0.1:' . $this->getCachingProxyPort()),
$this->getHostName() . ':' . $this->getCachingProxyPort(),
null,
array('purge_method' => 'NOTIFY')
);
}

return $this->proxyClient;
}
}
28 changes: 28 additions & 0 deletions tests/Functional/Fixtures/Symfony/AppCache.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

namespace FOS\HttpCache\Tests\Functional\Fixtures\Symfony;

use FOS\HttpCache\SymfonyCache\EventDispatchingHttpCache;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\HttpKernelInterface;

class AppCache extends EventDispatchingHttpCache
{
public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQUEST, $catch = true)
{
$response = parent::handle($request, $type, $catch);

if ($response->headers->has('X-Symfony-Cache')) {
if (false !== strpos($response->headers->get('X-Symfony-Cache'), 'miss')) {
$state = 'MISS';
} elseif (false !== strpos($response->headers->get('X-Symfony-Cache'), 'fresh')) {
$state = 'HIT';
} else {
$state = 'UNDETERMINED';
}
$response->headers->set('X-Cache', $state);
}

return $response;
}
}
Loading

0 comments on commit 64c4acd

Please sign in to comment.