Skip to content

Instantly share code, notes, and snippets.

@talal424 talal424/Cache.php
Created Mar 8, 2019

Embed
What would you like to do?
Phalcon auto cache model ( with many to many relationships )
<?php
/**
* Phalcon auto cache model ( with many to many relationships )
*
* @version 1.0.0
* @author Talal Alenizi <talal.alenizi@gmail.com> <@talal_alenizi>
* @license BSD License (3-clause)
* @link https://gist.github.com/talal424/
* @link https://github.com/talal424/
*
*
* Usage:
* Adjust the Phalcon service in your services file
* <code>
* $di->setShared('modelsCache', function () {
* $config = $this->getConfig();
* $frontCache = new FrontData(
* [
* "lifetime" => 172800,
* ]
* );
* return new BackFile(
* $frontCache,
* [
* "cacheDir" => $config->application->cacheDir,
* ]
* );
* });
* </code>
*
* Extend this class in all your models;
* <code>
* class Parts extends Cache
* {}
* </code>
*
*
* set $enableCache to true to enable cache or call Cache::enableCache(true)
*
* Change $cacheServiceName to your Phalcon Cache Service
* or call Cache::setCacheServiceName('modelsCache')
*
* Change $masterKey to your App name or call Cache::setMasterKey('SomeApp')
*
* all queries goes throw Phalcon\Model should be cached automatically
*
* PHQL queries:
*
* <code>
* // get model cache service
* $modelsCache = $this->modelsCache;
*
* // create unique cache key based on paramaters for this query with key name (RobotsParts_with_Parts)
* // this would help to this remove cache later on when looking for model name in keys
* $cacheKey = RobotsParts::getCacheKey(['robots_id'=>'1'],'RobotsParts_Parts');
*
* // check if this paramaters and key name used before with this query if cache is enabled
* if (Cache::cacheEnabled() && $modelsCache->exists($cacheKey)) {
* return $modelsCache->get($cacheKey);
* }
*
* // get Phalcon\Di
* $di = \Phalcon\Di::getDefault();
*
* // get model manager
* $modelsManager = $di->getModelsManager();
*
* // set phql query
* $phql = "SELECT RobotsParts.*, Parts.* FROM RobotsParts
* LEFT JOIN Parts ON RobotsParts.parts_id = Parts.id
* WHERE RobotsParts.robots_id = :robots_id:";
*
* // excute query and bind paramaters
* $results = $modelsManager->executeQuery($phql,['robots_id'=>'1']);
*
* // cache results if enabled
* if (Cache::cacheEnabled()) {
* $modelsCache->save($cacheKey,$results,172800);
* }
* </code>
*
* if you call `Cache::resetCache()` it remove cache made using PHQL queries if the models' names used in key
*
* if you override afterSave/afterDelete don't forget to call parent::afterSave/afterDelete
* or call $this->resetCache() to reset cache
*/
use \Phalcon\Text;
abstract class Cache extends \Phalcon\Mvc\Model
{
/**
* the status of cache model.
*
* @var bool
*/
protected static $enableCache = true;
/**
* a string that will be prepended to every key created.
*
* @var string
*/
protected static $masterKey = 'SomeApp';
/**
* your Phalcon Cache service name.
*
* @var string
*/
protected static $cacheServiceName = 'modelsCache';
/**
* whether encrypting paramaters or not ( which included in cache key )
* recommended when under windows because of the file name restrictions
* if you use file cache.
*
* @var bool
*/
protected static $useMd5 = true;
/**
* enables/disables cache model.
*
* @param bool $toggle
* @return void
*/
public static function enableCache($toggle = null)
{
if (is_bool($toggle)) {
self::$enableCache = $toggle;
}
}
/**
* Returns the status of the cache model.
*
* @return bool
*/
public static function cacheEnabled()
{
return self::$enableCache;
}
/**
* Sets the master key.
*
* @param string $key
* @return void
*/
public static function setMasterKey($key = null)
{
self::$masterKey = $key;
}
/**
* Sets Phalcon Cache service name.
*
* @param string $serviceName
* @return void
*/
public static function setCacheServiceName($serviceName = null)
{
self::$cacheServiceName = $serviceName;
}
/**
* enables/disables using md5.
*
* @param bool $toggle
* @return void
*/
public static function enableMd5($toggle = null)
{
if (is_bool($toggle)) {
self::$useMd5 = $toggle;
}
}
/**
* Returns unique cache key for provided paramaters and key.
*
* @param mixed $parameters
* @param string $key
* @return string
*/
protected static function _createKey($parameters,$key)
{
$modelName = get_called_class();
$masterKey = self::$masterKey;
if (isset($parameters['di'])) {
unset($parameters['di']);
}
$cacheKey = serialize($parameters);
if (self::$useMd5) {
$cacheKey = md5($cacheKey);
}
// replace namespace slashes if used
return str_replace('\\', '_', "{$masterKey}_{$modelName}_{$key}({$cacheKey})");
}
/**
* Creates unique cache key and inserts it into the paramaters.
*
* @param mixed $parameters
* @param string $key
* @return array
*/
protected static function createKey($parameters,$key)
{
if (!self::$enableCache) {
return $parameters;
}
if (!is_array($parameters)) {
$parameters = [$parameters];
}
if (!isset($parameters["cache"])) {
$cacheKey = self::_createKey($parameters,$key);
$parameters["cache"] = ["key" => $cacheKey];
}
return $parameters;
}
/**
* public alias for self::_createKey to be used in raw sql queries.
*
* @param mixed $parameters
* @param string $key
* @return void
*/
public static function getCacheKey($parameters,$key)
{
return self::_createKey($parameters,$key);
}
/**
* Creates unique cache key for provided paramaters and checks if availabe.
*
* @param mixed $parameters
* @return mixed
*/
public static function find($parameters = null)
{
$parameters = self::createKey($parameters,'find');
return parent::find($parameters);
}
/**
* Creates unique cache key for provided paramaters and checks if availabe.
*
* @param mixed $parameters
* @return mixed
*/
public static function findFirst($parameters = null)
{
$parameters = self::createKey($parameters,'findFirst');
return parent::findFirst($parameters);
}
/**
* Creates unique cache key for provided paramaters and checks if availabe.
*
* @param mixed $parameters
* @return mixed
*/
public static function count($parameters = null)
{
$parameters = self::createKey($parameters,'count');
return parent::count($parameters);
}
/**
* Creates unique cache key for provided paramaters and checks if availabe.
*
* @param mixed $parameters
* @return mixed
*/
public static function sum($parameters = null)
{
$parameters = self::createKey($parameters,'sum');
return parent::sum($parameters);
}
/**
* Creates unique cache key for provided paramaters and checks if availabe.
*
* @param mixed $parameters
* @return mixed
*/
public static function maximum($parameters = null)
{
$parameters = self::createKey($parameters,'maximum');
return parent::maximum($parameters);
}
/**
* Creates unique cache key for provided paramaters and checks if availabe.
*
* @param mixed $parameters
* @return mixed
*/
public static function minimum($parameters = null)
{
$parameters = self::createKey($parameters,'minimum');
return parent::minimum($parameters);
}
/**
* Creates unique cache key for provided paramaters and checks if availabe.
*
* @param mixed $parameters
* @return mixed
*/
public static function average($parameters = null)
{
$parameters = self::createKey($parameters,'average');
return parent::average($parameters);
}
/**
* Overrides parent method to cache when getter is used ($robot->getRobotsParts()).
* Returns related records defined relations depending on the method name
*
* @param string modelName
* @param string method
* @param array arguments
* @return mixed
*/
protected function _getRelatedRecords($modelName, $method, $arguments)
{
$manager = $this->_modelsManager;
$relation = false;
$queryMethod = null;
// Calling find/findFirst if the method starts with "get"
if (Text::startsWith($method, "get")) {
$alias = substr($method, 3);
$relation = $manager->getRelationByAlias($modelName, $alias);
}
// Calling count if the method starts with "count"
elseif (Text::startsWith($method, "count")) {
$alias = substr($method, 5);
$queryMethod = "count";
$relation = $manager->getRelationByAlias($modelName, $alias);
}
// If the relation was found perform the query via the models manager
if (!is_object($relation)) {
return null;
}
$extraArgs = isset($arguments[0]) ? $arguments[0] : null;
// check if cache is enabled and the relation type is many to many
// if its belongs to or has many it will invoke findFirst/find so no need for caching it twice
if (self::$enableCache && $relation->getType() > 2 && $this->di->has(self::$cacheServiceName)) {
if (!is_array($arguments)) {
$arguments = [$arguments];
}
$extraParameters = $relation->getParams();
if (!is_array($extraParameters)) {
$extraParameters = [$extraParameters];
}
// making sure that if the conditions are the same
// the key would match everytime using getters or properties
if (empty($arguments)) {
$arguments = [null];
}
$extraParameters['method'] = $queryMethod ?: 'get';
$keyCaption = array_merge($arguments, $extraParameters);
//getting fields used in relationship
$fields = $relation->getFields();
if (!is_array($fields)) {
$fields = [$fields];
}
//making a unique key based on field's value
foreach ($fields as $key => $value) {
$keyCaption['APR' . $key] = $this->readAttribute($value);
}
//create key
$cacheKey = self::_createKey($keyCaption,strtolower($alias));
$cacheService = $this->di->get(self::$cacheServiceName);
if ($cacheService->exists($cacheKey)) {
return $cacheService->get($cacheKey);
} else {
$result = $manager->getRelationRecords($relation,$queryMethod,$this,$extraArgs);
$cacheService->save($cacheKey, $result);
return $result;
}
}
return $manager->getRelationRecords($relation,$queryMethod,$this,$extraArgs);
}
/**
* Overrides parent method to cache when getter is used ($robot->getRelated('robotsParts')).
* Returns related records based on defined relations
*
* @param string alias
* @param array arguments
* @return \Phalcon\Mvc\Model\ResultsetInterface
*/
public function getRelated($alias, $arguments = null)
{
// Query the relation by alias
$className = get_class($this);
$manager = $this->_modelsManager;
$relation = $manager->getRelationByAlias($className, $alias);
if (!is_object($relation)) {
throw new \Exception("There is no defined relations for the model '" . $className . "' using alias '" . $alias . "'");
}
// check if cache is enabled and the relation type is many to many
// if its belongs to or has many it will invoke findFirst/find so no need for caching it twice
if (self::$enableCache && $relation->getType() > 2 && $this->di->has(self::$cacheServiceName)) {
if (!is_array($arguments)) {
$arguments = [$arguments];
}
$extraParameters = $relation->getParams();
if (!is_array($extraParameters)) {
$extraParameters = [$extraParameters];
}
// making sure that if the conditions are the same
// the key would match everytime using getters or properties
$extraParameters['method'] = 'get';
$keyCaption = array_merge($arguments, $extraParameters);
// getting fields used in relationship
$fields = $relation->getFields();
if (!is_array($fields)) {
$fields = [$fields];
}
// making a unique key based on field's value
foreach ($fields as $key => $value) {
$keyCaption['APR' . $key] = $this->readAttribute($value);
}
// create key
$cacheKey = self::_createKey($keyCaption,strtolower($alias));
$cacheService = $this->di->get(self::$cacheServiceName);
if ($cacheService->exists($cacheKey)) {
return $cacheService->get($cacheKey);
} else {
$result = $manager->getRelationRecords($relation, null, $this, $arguments);
$cacheService->save($cacheKey, $result);
return $result;
}
}
// Call the 'getRelationRecords' in the models manager
return $manager->getRelationRecords($relation, null, $this, $arguments);
}
/**
* Overrides parent method to cache when magic property is used ($robot->robotsParts).
* Magic method to get related records using the relation alias as a property
*
* @param string property
* @return \Phalcon\Mvc\Model\Resultset|Phalcon\Mvc\Model
*/
public function __get($property)
{
$modelName = get_class($this);
$manager = $this->getModelsManager();
$lowerProperty = strtolower($property);
// Check if the property is a relationship
$relation = $manager->getRelationByAlias($modelName, $lowerProperty);
if (is_object($relation)) {
// Not fetch a relation if it is on CamelCase
if (property_exists($this,$lowerProperty) && is_object($this->{$lowerProperty})) {
return $this->{$lowerProperty};
}
// check if cache is enabled and the relation type is many to many
// if its belongs to or has many it will invoke findFirst/find so no need for caching it twice
if (self::$enableCache && $relation->getType() > 2 && $this->di->has(self::$cacheServiceName)) {
// keep in mind if conditions set in relationship may change
// and check if cache has been set
$extraParameters = $relation->getParams();
if (!is_array($extraParameters)) {
$extraParameters = [$extraParameters];
}
// making sure that if the conditions are the same
// the key would match everytime using getters or properties
$extraParameters['method'] = 'get';
$extraParameters = array_merge([0 => null],$extraParameters);
// getting fields used in relationship
$fields = $relation->getFields();
if (!is_array($fields)) {
$fields = [$fields];
}
// making a unique key based on field's value
foreach ($fields as $key => $value) {
$extraParameters['APR' . $key] = $this->readAttribute($value);
}
//create key
$cacheKey = self::_createKey($extraParameters,$lowerProperty);
// Get the related records using cache
$cacheService = $this->di->get(self::$cacheServiceName);
if ($cacheService->exists($cacheKey)) {
$result = $cacheService->get($cacheKey);
} else {
$result = $manager->getRelationRecords($relation,null,$this,null);
$cacheService->save($cacheKey, $result);
}
} else {
// Get the related records without cahe
$result = $manager->getRelationRecords($relation,null,$this,null);
}
// Assign the result to the object
if (is_object($result)) {
// We assign the result to the instance avoiding future queries
$this->{$lowerProperty} = $result;
// For belongs-to relations we store the object in the related bag
if ($result instanceof \Phalcon\Mvc\ModelInterface) {
$this->_related[$lowerProperty] = $result;
}
}
return $result;
}
// in source code (method_exists) has been used
// so i decided to escape the autoload crap
return parent::__get($property);
}
/**
* Deletes all cache for model with the option to delete other related models
*
* @param bool $resetRelated delete all related models
* @param null|string|array $otherModels other optional models to delete
* @return void
*/
public function resetCache($resetRelated = false, $otherModels = null)
{
if (!self::$enableCache) {
return;
}
$modelName = get_called_class();
$masterKey = self::$masterKey;
$models = [$modelName];
// check for provided other models to remove cache for
if (!is_null($otherModels)) {
if (!is_array($otherModels)) {
$otherModels = [$otherModels];
}
$models = array_merge($models,$otherModels);
}
// get all related models when its set to true
if ($resetRelated === true) {
$manager = $this->_modelsManager;
$relations = array_merge(
$manager->getHasMany($this),
$manager->getHasOne($this),
$manager->getHasManyToMany($this),
$manager->getHasOneAndHasMany($this)
);
foreach ($relations as $relation) {
$models[] = $relation->getReferencedModel();
$intermediateModel = $relation->getIntermediateModel();
if ($intermediateModel) {
$models[] = $intermediateModel;
}
}
}
// remove duplicate models
$models = array_unique($models);
// replace namespace slashes if used
$models = array_map(function($model) {
return str_replace('\\', '_', $model);
},$models);
$modelsCache = $this->getDi()->get(self::$cacheServiceName);
$keys = $modelsCache->queryKeys($masterKey);
foreach ($keys as $key) {
if ($resetRelated === 'ALLCACHE') {
$modelsCache->delete($key);
continue;
}
foreach ($models as $model) {
if (strpos($key,$model) !== false) {
$modelsCache->delete($key);
break;
}
}
}
}
/**
* Deletes all cache for model after Insert/Update queries
*
* @return void
*/
public function afterSave()
{
$this->resetCache();
}
/**
* Deletes all cache for model after Delete queries
*
* @return void
*/
public function afterDelete()
{
$this->resetCache();
}
}
@talal424

This comment has been minimized.

Copy link
Owner Author

talal424 commented Mar 8, 2019

Usage:

  • Adjust the Phalcon service in your services file.
$di->setShared('modelsCache', function () {
    $config = $this->getConfig();
    $frontCache = new FrontData(
        [
            "lifetime" => 172800,
        ]
    );
    return new BackFile(
        $frontCache,
        [
            "cacheDir" => $config->application->cacheDir,
        ]
    );
});
  • Extend this class in all your models.
class Parts extends Cache
{
}
  • Set $enableCache to true to enable cache or call Cache::enableCache(true)
  • Change $cacheServiceName to your Phalcon Cache Service or call Cache::setCacheServiceName('modelsCache')
  • Change $masterKey to your App name or call Cache::setMasterKey('SomeApp')

all queries goes throw Phalcon\Model should be cached automatically

PHQL queries:

// get model cache service
$modelsCache = $this->modelsCache;

// create unique cache key based on paramaters for this query with key name (RobotsParts_with_Parts)
// this would help to this remove cache later on when looking for model name in keys
$cacheKey = RobotsParts::getCacheKey(['robots_id'=>'1'],'RobotsParts_Parts');

// check if this paramaters and key name used before with this query if cache is enabled
if (Cache::cacheEnabled() && $modelsCache->exists($cacheKey)) {
	return $modelsCache->get($cacheKey);
}

// get Phalcon\Di
$di = \Phalcon\Di::getDefault();

// get model manager
$modelsManager = $di->getModelsManager();

// set phql query
$phql = "SELECT RobotsParts.*, Parts.* FROM RobotsParts 
LEFT JOIN Parts ON RobotsParts.parts_id = Parts.id 
WHERE RobotsParts.robots_id = :robots_id:";

// excute query and bind paramaters
$results = $modelsManager->executeQuery($phql,['robots_id'=>'1']);

// cache results if enabled
if (Cache::cacheEnabled()) {
	$modelsCache->save($cacheKey,$results,172800);
}

if you call Cache::resetCache() it remove cache made using PHQL queries if the models' names used in key

Notice:

if you override afterSave/afterDelete methods in models don't forget to call parent::afterSave/parent::afterDelete

Tested on:

  • Php 5.6.18
  • Phalcon 3.4.2
@emiliodeg

This comment has been minimized.

Copy link

emiliodeg commented May 2, 2019

Great work!!!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.