Skip to content

Instantly share code, notes, and snippets.

@antydemant
Last active July 15, 2019 19:32
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save antydemant/c633903a94d3a5778eff1aefc326a14a to your computer and use it in GitHub Desktop.
Save antydemant/c633903a94d3a5778eff1aefc326a14a to your computer and use it in GitHub Desktop.
Memcached(ElastiCache) + Custom session handler PHP boilerplate.
<?php
class CacheClient
{
/**
* @var bool ElastiCache auto discovery flag
*/
public $elasticache_auto_discovery_active = false;
/**
* @var Memcached client
*/
private $cacheClient;
/**
* @var Memcached client type
*/
private $cacheClientType;
/**
* @var CacheClient instance
*/
private static $instance;
/**
* @var PHPErrorLogger elasticache logger
*/
public $cacheLogger;
/**
* Changed to public to support unit testing
*/
public function __construct()
{
if ($this->isMemcachedLoaded())
{
if (Config::getParam('elasticache_logger', "active", false))
{
$this->cacheLogger = $this->getLogger();
}
$this->elasticache_auto_discovery_active = Config::getParam("elasticache_auto_discovery", "active", false);
if (Config::getParam("elasticache_data", "active", false))
{
try
{
$this->setCacheClientType(self::CACHE_DATA);
$this->setCacheClient($this->get_elastic_data_client_connection());
} catch (Exception $e)
{
error_log("Can't start elasticache_data :" . $e->getMessage());
}
}
} else {
throw new CacheException('Memcached extension is not installed');
}
}
/**
* ElastiCache memcached data client
* @return Memcached memcached client with connection settings
* @throws CacheException
*/
private function get_elastic_data_client_connection()
{
if(Config::get("elasticache_data_cluster.host")
&& Config::get("elasticache_data_cluster.port"))
{
if (Config::get("elasticache_client_persistent_connection.active"))
{
$client = new Memcached('persistent_data_connection');
if (!$client->getServerList())
{
$this->get_elastic_data_client($client);
}
} else {
$client = new Memcached();
$this->get_elastic_data_client($client);
}
return $client;
} else {
throw new CacheException('Bad configuration.');
}
}
/**
* @param $client
* @return Memcached Memcached client
*/
private function get_elastic_data_client($client)
{
$client->setOption(Memcached::OPT_SERIALIZER, Memcached::SERIALIZER_PHP); // default option
$options = Config::getParam('elasticache_client', 'options', false);
if ($options = json_decode($options, true))
{
$client = $this->setOptions($client, $options);
}
if (defined('Memcached::DYNAMIC_CLIENT_MODE') && $this->elasticache_auto_discovery_active)
{
$client->setOption(Memcached::OPT_CLIENT_MODE, Memcached::DYNAMIC_CLIENT_MODE);
$result = $client->addServer(
Config::get("elasticache_data_cluster.host"),
Config::get("elasticache_data_cluster.port"),
Config::get("elasticache_data_cluster.weight")
);
if ($this->cacheLogger && !$result && $client->getResultCode())
{
$this->log('ElastiCache data|addServer FAILS|ResultMsg:'
. $client->getResultMessage() .'|ResultCode:'
. $client->getResultCode());
}
} else {
$numhosts = ((int)Config::get("elasticache_data_cluster.numhosts"))?:1;
for ($i = 1; $i <= $numhosts; $i++)
{
$result = $client->addServer($this->getNodeFromCluster(
Config::get("elasticache_data_cluster.host"), $i),
Config::get("elasticache_data_cluster.port"),
Config::get("elasticache_data_cluster.weight")
);
if ($this->cacheLogger && !$result && $client->getResultCode())
{
$this->log('ElastiCache data|addServer FAILS|ResultMsg:'
. $client->getResultMessage()
. '|ResultCode:' . $client->getResultCode());
}
}
}
return $client;
}
/**
* @param $cluster
* @param $node
* @return mixed
*/
public function getNodeFromCluster($cluster, $node){
return str_replace('.cfg.', '.'.(string)sprintf("%04s", $node).'.', $cluster);
}
/**
* Returns CacheImpl singletone.
*
* @return CacheImpl
*/
public static function getInstance()
{
if ( (!self::$instance) )
{
self::$instance = new CacheImpl();
}
return self::$instance;
}
/**
* @return bool
*/
public function flush()
{
$result = false;
if ($this->cacheClient)
{
try
{
$result = $this->cacheClient->flush();
}
catch(Exception $e)
{
error_log( __FILE__.':'.__LINE__.':|ElastiCache ' . $this->getCacheClientType() . ' flush|:' . $e->getMessage());
}
catch(Throwable $e)
{
error_log( __FILE__.':'.__LINE__.':|ElastiCache ' . $this->getCacheClientType() . ' flush|:' . $e->getMessage());
}
if ( !$result )
{
$this->log(__FUNCTION__);
}
}
return $result;
}
/**
* @param $key
* @param $data
* @param int $expires
* @return mixed
*/
public function set($key, $data, $expires = 300)
{
$result = false;
if ($this->cacheClient)
{
try
{
$result = $this->cacheClient->set($key, $data, $expires);
}
catch(Exception $e)
{
error_log( __FILE__.':'.__LINE__.':|ElastiCache ' . $this->getCacheClientType() . ' set|key:' . $key . '|:' . $e->getMessage());
}
catch(Throwable $e)
{
error_log( __FILE__.':'.__LINE__.':|ElastiCache ' . $this->getCacheClientType() . ' set|key:' . $key . '|:' . $e->getMessage());
}
if ( !$result )
{
$this->log(__FUNCTION__, [
'CACHE_KEY' => $key,
'DATA' => $data
]);
}
}
return $result;
}
/**
* @param $key
* @return bool|mixed
*/
public function get($key)
{
$result = false;
if ($this->cacheClient)
{
try
{
$result = $this->cacheClient->get($key);
}
catch(Exception $e)
{
error_log(__FILE__.':'.__LINE__.':|ElastiCache ' . $this->getCacheClientType() . ' get|key:' . $key . '|:' . $e->getMessage());
}
catch(Throwable $e)
{
error_log(__FILE__.':'.__LINE__.':|ElastiCache ' . $this->getCacheClientType() . ' get|key:' . $key . '|:' . $e->getMessage());
}
if ( !$result )
{
$this->log(__FUNCTION__, [
'CACHE_KEY' => $key
]);
}
}
return $result;
}
/**
* @param $keys
* @return bool|mixed
*/
public function getMulti($keys)
{
$result = false;
if (is_array($keys))
{
if ($this->cacheClient)
{
try
{
$result = $this->cacheClient->getMulti($keys);
}
catch (Exception $e)
{
error_log(__FILE__ . ':' . __LINE__ . ':|ElastiCache ' . $this->getCacheClientType() . ' getMulti|keys:' . implode(' ', $keys) . '|:' . $e->getMessage());
}
catch (Throwable $e)
{
error_log(__FILE__ . ':' . __LINE__ . ':|ElastiCache ' . $this->getCacheClientType() . ' getMulti|keys:' . implode(' ', $keys) . '|:' . $e->getMessage());
}
if ( !$result )
{
$this->log(__FUNCTION__, [
'CACHE_KEYS' => $keys
]);
}
}
}
return $result;
}
/*
* @param $key
* @return bool
*/
public function delete($key)
{
$result = false;
if ($this->cacheClient)
{
try
{
$result = $this->cacheClient->delete($key);
}
catch(Exception $e)
{
error_log(__FILE__.':'.__LINE__.':|ElastiCache ' . $this->getCacheClientType() . ' delete|key:' . $key . '|:' . $e->getMessage());
}
catch(Throwable $e)
{
error_log(__FILE__.':'.__LINE__.':|ElastiCache ' . $this->getCacheClientType() . ' delete|key:' . $key . '|:' . $e->getMessage());
}
if ( !$result )
{
$this->log(__FUNCTION__, [
'CACHE_KEYS' => $key
]);
}
}
return $result;
}
/**
* @param $key
* @param $data
* @param $expTime
* @return bool
*/
public function add($key, $data, $expTime)
{
$result = false;
if ($this->cacheClient)
{
try
{
$result = $this->cacheClient->add($key, $data, $expTime);
}
catch(Exception $e)
{
error_log(__FILE__.':'.__LINE__.':|ElastiCache ' . $this->getCacheClientType() . '|key:' . $key . '|:' . $e->getMessage());
}
catch(Throwable $e)
{
error_log(__FILE__.':'.__LINE__.':|ElastiCache ' . $this->getCacheClientType() . '|key:' . $key . '|:' . $e->getMessage());
}
if ( !$result )
{
$this->log(__FUNCTION__, [
'CACHE_KEYS' => $key,
'DATA' => $data,
]);
}
}
return $result;
}
/**
* @param $key
* @param int $byAmount
* @return bool|int
*/
public function increment($key, $byAmount=1)
{
$result = false;
if ($this->cacheClient)
{
try
{
$result = $this->cacheClient->increment($key, $byAmount);
}
catch(Exception $e)
{
error_log(__FILE__.':'.__LINE__.':|ElastiCache ' . $this->getCacheClientType() . ' increment|key:' . $key . '|:' . $e->getMessage());
}
catch(Throwable $e)
{
error_log(__FILE__.':'.__LINE__.':|ElastiCache ' . $this->getCacheClientType() . ' increment|key:' . $key . '|:' . $e->getMessage());
}
if ( !$result )
{
$this->log(__FUNCTION__, [
'CACHE_KEY' => $key,
'DATA' => $byAmount,
]);
}
}
return $result;
}
/**
* @param $option
* @param $value
* @return bool
*/
public function setOption($option, $value)
{
$result = false;
if ($this->cacheClient)
{
try
{
$result = $this->cacheClient->setOption($option, $value);
}
catch(Exception $e)
{
error_log(__FILE__.':'.__LINE__.':|ElastiCache ' . $this->getCacheClientType() . ' set option|:' . $e->getMessage());
}
catch(Throwable $e)
{
error_log(__FILE__.':'.__LINE__.':|ElastiCache ' . $this->getCacheClientType() . ' set option|:' . $e->getMessage());
}
if ( !$result )
{
$this->log(__FUNCTION__, [
'OPTION_KEY' => $option,
'OPTION_VALUE' => $value,
]);
}
}
return $result;
}
/**
* @param $action
* @param array $data
*/
private function log($action, array $data = array())
{
if ($this->cacheClient->getResultCode() && $this->cacheClient->getResultCode() != Memcached::RES_NOTFOUND)
{
error_log('ElastiCache ' . $this->getCacheClientType()
. '|' . $action . ' FAILED|ResultMsg:' . $this->cacheClient->getResultMessage()
. '|ResultCode:' . $this->cacheClient->getResultCode() . '|DATA:' . json_encode($data)
. '|');
}
}
/**
* Example of use:
* $options = array(
* 'libketama_compatible' => false,
* 'compression' => true,
* 'serializer' => 'php',
* );
*
* @param $client Memcached
* @param $options array
* @return $client Memcached
*/
public function setOptions(Memcached $client, $options)
{
if ($client && is_array($options))
{
$options = array_change_key_case($options, CASE_UPPER);
foreach ($options as $name => $value)
{
if (is_int($name))
{
continue;
}
if ('HASH' === $name || 'SERIALIZER' === $name || 'DISTRIBUTION' === $name)
{
$value = constant('Memcached::' . $name . '_' . strtoupper($value));
}
$opt = constant('Memcached::OPT_' . $name);
unset($options[$name]);
$options[$opt] = $value;
}
$client->setOptions($options);
}
return $client;
}
/**
* Method needed for testing
*/
public function setCacheClient($cacheClient)
{
$this->cacheClient = $cacheClient;
}
/**
* Method needed for testing
*/
public function setCacheClientType($cacheClientType)
{
$this->cacheClientType = $cacheClientType;
}
/**
* Method needed for testing
*/
public function getCacheClientType()
{
return $this->cacheClientType;
}
/**
* @return bool
*/
public function isMemcachedLoaded()
{
return extension_loaded('memcached');
}
}
class CacheException extends Exception
{
}
<?php
class MemcachedSessionHandler
{
const DEFAULT_LIFETIME = 86400;
static $overwrittenLifeTime = null;
public static function open($savePath, $sessionName)
{
return true;
}
public static function close()
{
return true;
}
public static function read($id)
{
// https://www.php.net/manual/ru/function.session-start.php#120589
// When you are using a custom session handler via session_set_save_handler()
// then calling session_start() in PHP 7.1 or higher you might see an error like this:
// session_start(): Failed to read session data: user (path: /var/lib/php/session) in ...
// The fix is simple, you just need to check for 'null' during your read method ¯\_(ツ)_/¯ :
if (empty($id)) return '';
$cache = CacheClient::getInstance();
$data = $cache->get($id);
$dataObj = Multitone::getInstance($id);
$dataObj->setData($data);
if (!$data) return '';
return $data;
}
public static function write($id, $data, $life = null)
{
if (empty($id)) {
error_log("Session id is empty!");
return true;
}
if (self::$overwrittenLifeTime) {
$life = self::$overwrittenLifeTime;
}
if (!$lifeTime) {
$life = self::DEFAULT_LIFETIME;
}
$cache = CacheClient::getInstance();
$dataObj = Multitone::getInstance($id);
if ($dataObj->compareData($data)) {
return $cache->set($id, $data, $life);
}
return true;
}
public static function destroy($id)
{
$dataObj = Multitone::getInstance($id);
$dataObj->setData(null);
$cache = CacheClient::getInstance();
$cache->delete($id);
return true;
}
public static function garbageCollect($maxInactivity)
{
return true;
}
/**
* @return MemcachedSessionHandler::$overwrittenLifeTime
*/
public static function getOverwrittenLifeTime()
{
return MemcachedSessionHandler::$overwrittenLifeTime;
}
/**
* If the value is set that's the one that is used to set the session expiration time.
* @param $overwrittenLifeTime
*/
public static function setOverwrittenLifeTime($overwrittenLifeTime)
{
MemcachedSessionHandler::$overwrittenLifeTime = $overwrittenLifeTime;
}
public static function setSessionHandlers()
{
session_set_save_handler(
array('MemcachedSessionHandler', 'open'),
array('MemcachedSessionHandler', 'close'),
array('MemcachedSessionHandler', 'read'),
array('MemcachedSessionHandler', 'write'),
array('MemcachedSessionHandler', 'destroy'),
array('MemcachedSessionHandler', 'garbageCollect')
);
register_shutdown_function('session_write_close');
}
}
class Multitone
{
private $md5;
private $id;
private static $data = [];
public static function getInstance($id)
{
if (!isset(self::$data[$id])) {
self::$data[$id] = new Multitone($id);
}
return self::$data[$id];
}
public function __construct($id)
{
$this->id = $id;
}
public function setData($data)
{
$this->md5 = md5($data);
}
public function compareData($data)
{
return $this->md5 != md5($data);
}
}
MemcachedSessionHandler::setSessionHandlers();
?>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment