Skip to content

Instantly share code, notes, and snippets.

@cryptiklemur
Created July 29, 2013 21:57
Show Gist options
  • Save cryptiklemur/6108241 to your computer and use it in GitHub Desktop.
Save cryptiklemur/6108241 to your computer and use it in GitHub Desktop.
MemcachedStore class for the Symfony2 AppCache Class
<?php
use Symfony\Component\HttpKernel\HttpCache\StoreInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Aequasi\Bundle\MemcachedBundle\Cache\AntiStampedeMemcached as Memcached;
/**
* {@inheritDoc}
*
* This specific Store caches data to a memcache instance
*
* @author Fabien Potencier <[email protected]>
*/
class MemcachedStore implements StoreInterface
{
/**
* @var array
*/
protected $options;
/**
* @var Memcached|\Memcached
*/
private $keyCache;
/**
* @var string
*/
private static $prefix = 'SectionsReverseProxy';
/**
* @var string
*/
private static $lockKey = 'Locks';
/**
* Constructor.
*
* @param array $options Options for the memcache instance
* @param array $servers Array of server info
*/
public function __construct( array $options, array $servers )
{
$this->options = $this->getDefaultOptions( $options );
$this->keyCache = new Memcached( $options[ 'enabled' ], $options[ 'debug' ], $options[ 'persistentId' ] );
$this->keyCache->addServers( $servers );
}
/**
* Cleanups storage.
*
* Unlocking everything
*/
public function cleanup()
{
$keys = $this->keyCache->get( $this->getLockKey() );
if( empty( $keys ) ) {
return true;
}
foreach( $keys as $key => $val ) {
$this->keyCache->delete( $key );
}
$this->keyCache->set( $this->getLockKey(), [ ], null );
}
/**
* Locks the cache for a given Request.
*
* @param Request $request A Request instance
*
* @return Boolean true if the lock is acquired, false otherwise
*/
public function lock( Request $request )
{
$requestKey = $this->getCacheKey( $request );
$lockData = $this->keyCache->get( $this->getLockKey() );
if( in_array( $requestKey, $lockData ) ) {
return false;
}
$lockData[ ] = $requestKey;
$this->keyCache->set( $this->getLockKey(), $lockData, null );
return true;
}
/**
* Releases the lock for the given Request.
*
* @param Request $request A Request instance
*
* @return Boolean False if the lock entry does not exist or cannot be unlocked, true otherwise
*/
public function unlock( Request $request )
{
$requestKey = $this->getCacheKey( $request );
$lockData = $this->keyCache->get( $this->getLockKey() );
if( $lockData === false ) {
$lockData = [ ];
}
if( ( $key = array_search( $requestKey, $lockData ) ) !== false ) {
unset( $lockData[ $key ] );
}
return $this->keyCache->set( $this->getLockKey(), $lockData, null );
}
/**
* @param Request $request
*
* @return bool
*/
public function isLocked( Request $request )
{
return in_array( $this->getCacheKey( $request ), $this->keyCache->get( $this->getLockKey() ) );
}
/**
* Locates a cached Response for the Request provided.
*
* @param Request $request A Request instance
*
* @return Response|null A Response instance, or null if no cache entry was found
*/
public function lookup( Request $request )
{
$key = $this->getCacheKey( $request );
if( !$entries = $this->getMetadata( $key ) ) {
return null;
}
// find a cached entry that matches the request.
$match = null;
foreach( $entries as $entry ) {
if( $this->requestsMatch(
isset( $entry[ 1 ][ 'vary' ][ 0 ] ) ? $entry[ 1 ][ 'vary' ][ 0 ] : '',
$request->headers->all(),
$entry[ 0 ]
)
) {
$match = $entry;
break;
}
}
if( null === $match ) {
return null;
}
list( $req, $headers ) = $match;
$body = $this->keyCache->get( $headers[ 'x-content-digest' ][ 0 ] );
if( !empty( $body ) ) {
return $this->restoreResponse( $headers, $body );
}
// TODO the metaStore referenced an entity that doesn't exist in
// the entityStore. We definitely want to return nil but we should
// also purge the entry from the meta-store when this is detected.
return null;
}
/**
* Writes a cache entry to the store for the given Request and Response.
*
* Existing entries are read and any that match the response are removed. This
* method calls write with the new list of cache entries.
*
* @param Request $request A Request instance
* @param Response $response A Response instance
*
* @return string The key under which the response is stored
*
* @throws \RuntimeException
*/
public function write( Request $request, Response $response )
{
$key = $this->getCacheKey( $request );
$storedEnv = $this->persistRequest( $request );
// write the response body to the entity store if this is the original response
if( !$response->headers->has( 'X-Content-Digest' ) ) {
$digest = $this->generateContentDigest( $response );
if( false === $this->save( $digest, $response->getContent() ) ) {
throw new \RuntimeException( 'Unable to store the entity.' );
}
$response->headers->set( 'X-Content-Digest', $digest );
if( !$response->headers->has( 'Transfer-Encoding' ) ) {
$response->headers->set( 'Content-Length', strlen( $response->getContent() ) );
}
}
// read existing cache entries, remove non-varying, and add this one to the list
$entries = array();
$vary = $response->headers->get( 'vary' );
foreach( $this->getMetadata( $key ) as $entry ) {
if( !isset( $entry[ 1 ][ 'vary' ][ 0 ] ) ) {
$entry[ 1 ][ 'vary' ] = array( '' );
}
if( $vary != $entry[ 1 ][ 'vary' ][ 0 ] || !$this->requestsMatch( $vary, $entry[ 0 ], $storedEnv ) ) {
$entries[ ] = $entry;
}
}
$headers = $this->persistResponse( $response );
unset( $headers[ 'age' ] );
array_unshift( $entries, array( $storedEnv, $headers ) );
if( false === $this->save( $key, serialize( $entries ) ) ) {
throw new \RuntimeException( 'Unable to store the metadata.' );
}
return $key;
}
/**
* Returns content digest for $response.
*
* @param Response $response
*
* @return string
*/
protected function generateContentDigest( Response $response )
{
return self::$prefix . '_' . 'en' . sha1( $response->getContent() );
}
/**
* Invalidates all cache entries that match the request.
*
* @param Request $request A Request instance
*
* @throws \RuntimeException
*/
public function invalidate( Request $request )
{
$modified = false;
$key = $this->getCacheKey( $request );
$entries = array();
foreach( $this->getMetadata( $key ) as $entry ) {
$response = $this->restoreResponse( $entry[ 1 ] );
if( $response->isFresh() ) {
$response->expire();
$modified = true;
$entries[ ] = array( $entry[ 0 ], $this->persistResponse( $response ) );
} else {
$entries[ ] = $entry;
}
}
if( $modified ) {
if( false === $this->save( $key, serialize( $entries ) ) ) {
throw new \RuntimeException( 'Unable to store the metadata.' );
}
}
}
/**
* Determines whether two Request HTTP header sets are non-varying based on
* the vary response header value provided.
*
* @param string $vary A Response vary header
* @param array $env1 A Request HTTP header array
* @param array $env2 A Request HTTP header array
*
* @return Boolean true if the two environments match, false otherwise
*/
private function requestsMatch( $vary, $env1, $env2 )
{
if( empty( $vary ) ) {
return true;
}
foreach( preg_split( '/[\s,]+/', $vary ) as $header ) {
$key = strtr( strtolower( $header ), '_', '-' );
$v1 = isset( $env1[ $key ] ) ? $env1[ $key ] : null;
$v2 = isset( $env2[ $key ] ) ? $env2[ $key ] : null;
if( $v1 !== $v2 ) {
return false;
}
}
return true;
}
/**
* Gets all data associated with the given key.
*
* Use this method only if you know what you are doing.
*
* @param string $key The store key
*
* @return array An array of data associated with the key
*/
private function getMetadata( $key )
{
if( false === $entries = $this->load( $key ) ) {
return array();
}
return unserialize( $entries );
}
/**
* Purges data for the given URL.
*
* @param string $url A URL
*
* @return Boolean true if the URL exists and has been purged, false otherwise
*/
public function purge( $url )
{
return $this->keyCache->delete( $this->getCacheKey( Request::create( $url ) ) );
}
/**
* Loads data for the given key.
*
* @param string $key The store key
*
* @return string The data associated with the key
*/
private function load( $key )
{
return $this->keyCache->get( $key );
}
/**
* Save data for the given key.
*
* @param string $key The store key
* @param string $data The data to store
*
* @return Boolean
*/
private function save( $key, $data )
{
$this->keyCache->set( $key, $data, null );
}
/**
* Returns a cache key for the given Request.
*
* @param Request $request A Request instance
*
* @return string A key for the given Request
*/
private function getCacheKey( Request $request )
{
$keyString = sprintf(
"%s %s",
$request->getMethod(),
$request->getRequestUri()
);
$data = $this->keyCache->get( self::$prefix . '_' . $keyString );
if( empty( $data ) ) {
$data = self::$prefix . '_' . 'md' . sha1( $keyString );
$this->keyCache->set( self::$prefix . '_' . $request, $data, null );
}
return $data;
}
/**
* Persists the Request HTTP headers.
*
* @param Request $request A Request instance
*
* @return array An array of HTTP headers
*/
private function persistRequest( Request $request )
{
return $request->headers->all();
}
/**
* Persists the Response HTTP headers.
*
* @param Response $response A Response instance
*
* @return array An array of HTTP headers
*/
private function persistResponse( Response $response )
{
$headers = $response->headers->all();
$headers[ 'X-Status' ] = array( $response->getStatusCode() );
return $headers;
}
/**
* Restores a Response from the HTTP headers and body.
*
* @param array $headers An array of HTTP headers for the Response
* @param string $body The Response body
*
* @return Response
*/
private function restoreResponse( $headers, $body = null )
{
$status = $headers[ 'X-Status' ][ 0 ];
unset( $headers[ 'X-Status' ] );
if( null !== $body ) {
$headers[ 'X-Body-Eval' ] = $body;
}
return new Response( $body, $status, $headers );
}
/**
* Gets the lock key with a prefix
*/
private function getLockKey()
{
return self::$prefix . '_' . self::$lockKey;
}
/**
* @param array $options
*
* @return array
*/
private function getDefaultOptions( array $options )
{
return array_merge(
[
'enabled' => true,
'debug' => false,
'persistentId' => null
],
$options
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment