Skip to content

Instantly share code, notes, and snippets.

@talal424
Last active March 2, 2021 11:13
Show Gist options
  • Save talal424/4c0e661f9c41359b354c54a0b5957103 to your computer and use it in GitHub Desktop.
Save talal424/4c0e661f9c41359b354c54a0b5957103 to your computer and use it in GitHub Desktop.
Phalcon auto cache model ( with many to many relationships ) for Phalcon 3.4.x
<?php
/**
* Phalcon auto cache model ( with many to many relationships ) for Phalcon 3.4.x
*
* @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();
}
}
@emiliodeg
Copy link

Great work!!!

@paramov
Copy link

paramov commented Feb 10, 2021

Great work!!! Can you help me for phalcon 4?

@talal424
Copy link
Author

https://gist.github.com/talal424/5fdaf1e4613e20c23069c095cf506168

i just wrote this and did some tests and it should work just fine

it could be tricky if you use namespaces ( when clearing related models cache )

@paramov
Copy link

paramov commented Mar 2, 2021

https://gist.github.com/talal424/5fdaf1e4613e20c23069c095cf506168

i just wrote this and did some tests and it should work just fine

it could be tricky if you use namespaces ( when clearing related models cache )

Perfect!!! Thank you for interest.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment