Skip to content

Instantly share code, notes, and snippets.

@kazuhei
Created September 2, 2014 13:48
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kazuhei/063960ff9d4e11183735 to your computer and use it in GitHub Desktop.
Save kazuhei/063960ff9d4e11183735 to your computer and use it in GitHub Desktop.
fuelphp Cache_Storage_Memcachier to use memcachier on heroku
<?php
/**
* Part of the Fuel framework.
*
* @package Fuel
* @version 1.7
* @author Fuel Development Team
* @license MIT License
* @copyright 2010 - 2014 Fuel Development Team
* @link http://fuelphp.com
*/
namespace Fuel\Core;
class Cache_Storage_Memcachier extends \Cache_Storage_Driver
{
/**
* @const string Tag used for opening & closing cache properties
*/
const PROPS_TAG = 'Fuel_Cache_Properties';
/**
* @var array driver specific configuration
*/
protected $config = array();
/*
* @var Memcached storage for the memcached object
*/
protected static $memcached = false;
// ---------------------------------------------------------------------
public function __construct($identifier, $config)
{
parent::__construct($identifier, $config);
$this->config = isset($config['memcachier']) ? $config['memcachier'] : array();
// make sure we have a memcache id
$this->config['cache_id'] = $this->_validate_config('cache_id', isset($this->config['cache_id'])
? $this->config['cache_id'] : 'fuel');
// check for an expiration override
$this->expiration = $this->_validate_config('expiration', isset($this->config['expiration'])
? $this->config['expiration'] : $this->expiration);
if (static::$memcached === false)
{
// make sure we have memcached servers configured
$this->config['servers'] = $this->_validate_config('servers', $this->config['servers']);
// do we have the PHP memcached extension available
if ( ! class_exists('Memcached') )
{
throw new \FuelException('Memcached cache are configured, but your PHP installation doesn\'t have the Memcached extension loaded.');
}
// instantiate the memcached object
static::$memcached = new \Memcached();
// add auth condition
static::$memcached->setSaslAuthData( getenv("MEMCACHIER_USERNAME"), getenv("MEMCACHIER_PASSWORD") );
// add the configured servers
static::$memcached->addServers($this->config['servers']);
// check if we can connect to the server(s)
if (static::$memcached->getVersion() === false)
{
throw new \FuelException('Memcached cache are configured, but there is no connection possible. Check your configuration.');
}
}
}
// ---------------------------------------------------------------------
/**
* Check if other caches or files have been changed since cache creation
*
* @param array
* @return bool
*/
public function check_dependencies(array $dependencies)
{
foreach($dependencies as $dep)
{
// get the section name and identifier
$sections = explode('.', $dep);
if (count($sections) > 1)
{
$identifier = array_pop($sections);
$sections = '.'.implode('.', $sections);
}
else
{
$identifier = $dep;
$sections = '';
}
// get the cache index
$index = static::$memcached->get($this->config['cache_id'].$sections);
// get the key from the index
$key = isset($index[$identifier][0]) ? $index[$identifier] : false;
// key found and newer?
if ($key === false or $key[1] > $this->created)
{
return false;
}
}
return true;
}
/**
* Delete Cache
*/
public function delete()
{
// get the memcached key for the cache identifier
$key = $this->_get_key(true);
// delete the key from the memcached server
if ($key and static::$memcached->delete($key) === false)
{
if (static::$memcached->getResultCode() !== \Memcached::RES_NOTFOUND)
{
throw new \FuelException('Memcached returned error code "'.static::$memcached->getResultCode().'" on delete. Check your configuration.');
}
}
$this->reset();
}
/**
* Purge all caches
*
* @param limit purge to subsection
* @return bool
*/
public function delete_all($section)
{
// determine the section index name
$section = $this->config['cache_id'].(empty($section)?'':'.'.$section);
// get the directory index
$index = static::$memcached->get($this->config['cache_id'].'__DIR__');
if (is_array($index))
{
// limit the delete if we have a valid section
if ( ! empty($section))
{
$dirs = in_array($section, $index) ? array($section) : array();
}
else
{
$dirs = $index;
}
// loop through the indexes, delete all stored keys, then delete the indexes
foreach ($dirs as $dir)
{
$list = static::$memcached->get($dir);
foreach ($list as $item)
{
static::$memcached->delete($item[0]);
}
static::$memcached->delete($dir);
}
// update the directory index
$index = array_diff($index, $dirs);
static::$memcached->set($this->config['cache_id'].'__DIR__', $index);
}
}
// ---------------------------------------------------------------------
/**
* Prepend the cache properties
*
* @return string
*/
protected function prep_contents()
{
$properties = array(
'created' => $this->created,
'expiration' => $this->expiration,
'dependencies' => $this->dependencies,
'content_handler' => $this->content_handler
);
$properties = '{{'.static::PROPS_TAG.'}}'.json_encode($properties).'{{/'.static::PROPS_TAG.'}}';
return $properties.$this->contents;
}
/**
* Remove the prepended cache properties and save them in class properties
*
* @param string
* @throws UnexpectedValueException
*/
protected function unprep_contents($payload)
{
$properties_end = strpos($payload, '{{/'.static::PROPS_TAG.'}}');
if ($properties_end === FALSE)
{
throw new \UnexpectedValueException('Cache has bad formatting');
}
$this->contents = substr($payload, $properties_end + strlen('{{/'.static::PROPS_TAG.'}}'));
$props = substr(substr($payload, 0, $properties_end), strlen('{{'.static::PROPS_TAG.'}}'));
$props = json_decode($props, true);
if ($props === NULL)
{
throw new \UnexpectedValueException('Cache properties retrieval failed');
}
$this->created = $props['created'];
$this->expiration = is_null($props['expiration']) ? null : (int) ($props['expiration'] - time());
$this->dependencies = $props['dependencies'];
$this->content_handler = $props['content_handler'];
}
/**
* Save a cache, this does the generic pre-processing
*
* @return bool success
*/
protected function _set()
{
// get the memcached key for the cache identifier
$key = $this->_get_key();
$payload = $this->prep_contents();
// calculate relative expiration time (eg. 60s)
$expiration = !is_null($this->expiration) ? $this->expiration - time() : 0;
// if expiration value is less than 30 days, use relative value, otherwise use unix timestamp:
$expiration = $expiration <= 2592000 ? (int) $expiration : (int) $this->expiration;
// write it to the memcached server
if (static::$memcached->set($key, $payload, $expiration) === false)
{
throw new \FuelException('Memcached returned error code "'.static::$memcached->getResultCode().'" on write. Check your configuration.');
}
// update the index
$this->_update_index($key);
return true;
}
/**
* Load a cache, this does the generic post-processing
*
* @return bool success
*/
protected function _get()
{
// get the memcached key for the cache identifier
$key = $this->_get_key();
// fetch the cached data from the Memcached server
$payload = static::$memcached->get($key);
try
{
$this->unprep_contents($payload);
}
catch (\UnexpectedValueException $e)
{
return false;
}
return true;
}
/**
* validate a driver config value
*
* @param string name of the config variable to validate
* @param mixed value
* @return mixed
*/
protected function _validate_config($name, $value)
{
switch ($name)
{
case 'cache_id':
if (empty($value) or ! is_string($value))
{
$value = 'fuel';
}
break;
case 'expiration':
if (empty($value) or ! is_numeric($value))
{
$value = null;
}
break;
case 'servers':
// do we have a servers config
if ( empty($value) OR ! is_array($value))
{
$value = array('default' => array('host' => '127.0.0.1', 'port' => '11211'));
}
// validate the servers
foreach ($value as $key => $server)
{
// do we have a host?
if ( ! isset($server['host']) OR ! is_string($server['host']))
{
throw new \FuelException('Invalid Memcached server definition in the cache configuration.');
}
// do we have a port number?
if ( ! isset($server['port']) OR ! is_numeric($server['port']) OR $server['port'] < 1025 OR $server['port'] > 65535)
{
throw new \FuelException('Invalid Memcached server definition in the cache configuration.');
}
// do we have a relative server weight?
if ( ! isset($server['weight']) OR ! is_numeric($server['weight']) OR $server['weight'] < 0)
{
// set a default
$value[$key]['weight'] = 0;
}
}
break;
default:
break;
}
return $value;
}
/**
* Get's the memcached key belonging to the cache identifier
*
* @param bool if true, remove the key retrieved from the index
* @return string
*/
protected function _get_key($remove = false)
{
// get the current index information
list($identifier, $sections, $index) = $this->_get_index();
// get the key from the index
$key = isset($index[$identifier][0]) ? $index[$identifier][0] : false;
if ($remove === true)
{
if ( $key !== false )
{
unset($index[$identifier]);
static::$memcached->set($this->config['cache_id'].$sections, $index);
}
}
else
{
// create a new key if needed
$key === false and $key = $this->_new_key();
}
return $key;
}
/**
* Generate a new unique key for the current identifier
*
* @return string
*/
protected function _new_key()
{
$key = '';
while (strlen($key) < 32)
{
$key .= mt_rand(0, mt_getrandmax());
}
return md5($this->config['cache_id'].'_'.uniqid($key, TRUE));
}
/**
* Get the section index
*
* @return array containing the identifier, the sections, and the section index
*/
protected function _get_index()
{
// get the section name and identifier
$sections = explode('.', $this->identifier);
if (count($sections) > 1)
{
$identifier = array_pop($sections);
$sections = '.'.implode('.', $sections);
}
else
{
$identifier = $this->identifier;
$sections = '';
}
// get the cache index and return it
return array($identifier, $sections, static::$memcached->get($this->config['cache_id'].$sections));
}
/**
* Update the section index
*
* @param string cache key
*/
protected function _update_index($key)
{
// get the current index information
list($identifier, $sections, $index) = $this->_get_index();
// create a new index and store the key
is_array($index) or $index = array();
// store the key in the index and write the index back
$index[$identifier] = array($key, $this->created);
static::$memcached->set($this->config['cache_id'].$sections, $index, 0);
// get the directory index
$index = static::$memcached->get($this->config['cache_id'].'__DIR__');
if (is_array($index))
{
if (!in_array($this->config['cache_id'].$sections, $index))
{
$index[] = $this->config['cache_id'].$sections;
}
}
else
{
$index = array($this->config['cache_id'].$sections);
}
// update the directory index
static::$memcached->set($this->config['cache_id'].'__DIR__', $index, 0);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment