Created
July 29, 2013 21:57
-
-
Save cryptiklemur/6108241 to your computer and use it in GitHub Desktop.
MemcachedStore class for the Symfony2 AppCache Class
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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