Last active
July 15, 2019 19:32
-
-
Save antydemant/c633903a94d3a5778eff1aefc326a14a to your computer and use it in GitHub Desktop.
Memcached(ElastiCache) + Custom session handler PHP boilerplate.
This file contains hidden or 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 | |
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 | |
{ | |
} |
This file contains hidden or 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 | |
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