Skip to content

Instantly share code, notes, and snippets.

@talal424
Last active July 29, 2021 20:42
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 talal424/5fdaf1e4613e20c23069c095cf506168 to your computer and use it in GitHub Desktop.
Save talal424/5fdaf1e4613e20c23069c095cf506168 to your computer and use it in GitHub Desktop.
Phalcon auto cache model ( with many to many relationships ) for Phalcon 4.1.x
<?php
declare(strict_types=1);
use Phalcon\Di;
use Phalcon\Mvc\Model;
use Phalcon\Mvc\ModelInterface;
use Phalcon\Mvc\Model\ResultsetInterface;
use Phalcon\Mvc\Model\RelationInterface;
/**
* Phalcon auto cache model ( with many to many relationships ) for Phalcon 4.1.x
*
* @version 2.0.1
* @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\Cache service in your services file.
*
* >> You must make a seperate service for the adapter since you can't get cache keys from Phalcon\Cache
*
* <code>
* use Phalcon\Cache;
* use Phalcon\Cache\AdapterFactory;
* use Phalcon\Di\FactoryDefault;
* use Phalcon\Storage\SerializerFactory;
*
* $di->set(
* 'cacheAdapter',
* function () {
* $config = $this->getConfig();
* $serializerFactory = new SerializerFactory();
* $adapterFactory = new AdapterFactory($serializerFactory);
*
* $options = [
* 'defaultSerializer' => 'php',
* 'lifetime' => 7200,
* 'storageDir' => $config->application->cacheDir,
* 'prefix' => 'some_prefix'
* ];
*
* $adapter = $adapterFactory->newInstance('stream', $options);
*
* return $adapter;
* }
* );
*
* $di->set(
* 'modelsCache',
* function () {
* $adapter = $this->getCacheAdapter();
* return new Cache($adapter);
* }
* );
* </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 `$prefix` to your App name or call `Cache::setPrefix('SomeApp')`
*
* all queries goes throw Phalcon\Model should be cached automatically
*
* # PHQL queries:
*
* <code>
* // 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 = RobotsParts::getPHQL($phql, ['Parts'], ['robots_id' => '1']);
* </code>
*
* include used models in PHQL query (as the second paramater) to ensure the clearance of the cache keys when `Cache::clearCache(true)` is called
*
* if you call `Cache::clearCache()` it remove cache made using PHQL queries if the models' names used in key
*
* call `Cache::clearAllCache()` to clear all cache keys
*
* # Notice:
* if you override afterSave/afterDelete methods in models don't forget to call `parent::afterSave`/`parent::afterDelete`
*
* # Tested on:
* * Php 7.4.11
* * Phalcon 4.1.0
*/
abstract class Cache extends Model
{
/**
*the status of cache model.
*
* @var bool
*/
protected static $enableCache = true;
/**
* a prefix that will be prepended to every key created.
*
* @var string
*/
protected static $prefix = 'SomeApp';
/**
* 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(bool $toggle): void
{
self::$enableCache = $toggle;
}
/**
* Returns the status of the cache model.
*
* @return bool
*/
public static function cacheEnabled(): bool
{
return self::$enableCache;
}
/**
* Sets the master key.
*
* @param string $key
* @return void
*/
public static function setPrefix(string $prefix): void
{
self::$prefix = $prefix;
}
/**
* enables/disables using md5.
*
* @param bool $toggle
* @return void
*/
public static function enableMd5(bool $toggle): void
{
self::$useMd5 = $toggle;
}
/**
* Returns unique cache key for provided paramaters and key.
*
* @param array $parameters
* @param string $key
* @return string
*/
protected static function generateCacheKey(array $parameters, string $key): string
{
$model = get_called_class();
$prefix = self::$prefix;
if (isset($parameters['di'])) {
unset($parameters['di']);
}
$cacheKey = serialize($parameters);
if (self::$useMd5) {
$cacheKey = md5($cacheKey);
}
// replace namespace slashes if used
return str_replace('\\', '_', "{$prefix}_{$model}_{$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, string $key)
{
if (!self::$enableCache) {
return $parameters;
}
if (!is_array($parameters)) {
$parameters = [$parameters];
}
if (!isset($parameters['cache'])) {
$cacheKey = self::generateCacheKey($parameters,$key);
$parameters['cache'] = ['key' => $cacheKey];
}
return $parameters;
}
/**
* public alias for self::_createKey to be used in raw sql queries.
*
* @param array $parameters
* @param string $key
* @return string
*/
public static function getCacheKey(array $parameters, string $key): string
{
return self::generateCacheKey($parameters, $key);
}
/**
* Creates unique cache key for provided paramaters and checks if availabe.
*
* @param mixed $parameters
* @return mixed
*/
public static function find($parameters = null): ResultsetInterface
{
$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): ?ModelInterface
{
$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 relations
* Returns related records based on defined relations
*
* @param string $alias
* @param mixed $arguments
* @return \Phalcon\Mvc\Model\Resultset\Simple|Phalcon\Mvc\Model\Resultset\Simple|false
*/
public function getRelated(string $alias, $arguments = null)
{
$className = get_class($this);
$manager = $this->modelsManager;
$lowerAlias = strtolower($alias);
/**
* Query the relation by alias
*/
$relation = $manager->getRelationByAlias($className, $lowerAlias);
if (!is_object($relation)) {
throw new Exception("There is no defined relations for the model '" . $className . "' using alias '" . $alias . "'");
}
/**
* If there are any arguments, Manager with handle the caching of the records
*/
if (is_null($arguments)) {
/**
* If the related records are already in cache and the relation is reusable,
* we return the cached records.
*/
if ($relation->isReusable() && $this->isRelationshipLoaded($lowerAlias)) {
$result = $this->related[$lowerAlias];
} else {
/**
* get cached records or Call the 'getRelationRecords' in the models manager.
*/
$result = $this->_getRelationRecords($alias, $relation, $arguments);
/**
* We store relationship objects in the related cache if there were no arguments.
*/
$this->related[$lowerAlias] = $result;
}
} else {
/**
* get cached records or
* Individually queried related records are handled by Manager.
* The Manager also checks and stores reusable records.
*/
$result = $this->_getRelationRecords($alias, $relation, $arguments);
}
return $result;
}
/**
* Helper method to query records based on a relation definition
*
* @return \Phalcon\Mvc\Model\Resultset\Simple|Phalcon\Mvc\Model\Resultset\Simple|int|false
*/
final protected function _getRelationRecords(string $alias, RelationInterface $relation, $parameters = null)
{
$manager = $this->modelsManager;
if (self::$enableCache && $relation->getType() > 2) {
$extraParameters = $relation->getParams();
// merge provided paramaters and ones setup in relation
$keyCaption = $this->_mergeFindParameters($extraParameters, $parameters);
// 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);
}
$cacheKey = self::generateCacheKey($keyCaption, strtolower($alias));
$cacheService = $this->di->get('modelsCache');
if ($cacheService->has($cacheKey)) {
return $cacheService->get($cacheKey);
} else {
$result = $manager->getRelationRecords($relation, $this, $parameters);
$cacheService->set($cacheKey, $result);
return $result;
}
}
return $manager->getRelationRecords($relation, $this, $parameters);
}
/**
* Merge two arrays of find parameters
*/
final protected function _mergeFindParameters($findParamsOne, $findParamsTwo): array
{
$findParams = [];
if (is_string($findParamsOne)) {
$findParamsOne = ['conditions' => $findParamsOne];
}
if (is_string($findParamsTwo)) {
$findParamsTwo = ['conditions' => $findParamsTwo];
}
if (is_array($findParamsOne)) {
foreach ($findParamsOne as $key => $value) {
if ($key === 0 || $key === 'conditions') {
if (!isset($findParams[0])) {
$findParams[0] = $value;
} else {
$findParams[0] = "(" . $findParams[0] . ") AND (" . $value . ")";
}
} else {
$findParams[$key] = $value;
}
}
}
if (is_array($findParamsTwo)) {
foreach ($findParamsTwo as $key => $value) {
if ($key === 0 || $key === 'conditions') {
if (!isset($findParams[0])) {
$findParams[0] = $value;
} else {
$findParams[0] = "(" . $findParams[0] . ") AND (" . $value . ")";
}
} else if ($key === "bind" || $key === "bindTypes") {
if (is_array($value)) {
if (!isset($findParams[$key])) {
$findParams[$key] = $value;
} else {
$findParams[$key] = array_merge($findParams[$key], $value);
}
}
} else {
$findParams[$key] = $value;
}
}
}
return $findParams;
}
/**
* Queries the PHQL string with provided params
* include all model class names to ensure clearance when Cache::clearCache(true) is called
*
* @param string $phql PHQL query
* @param array $models models included in query
* @return Phalcon\Mvc\Model\ResultsetInterface|Phalcon\Mvc\Model\Query\StatusInterface
*/
public static function getPHQL(string $phql, array $models = [], array $params = [])
{
$di = Di::getDefault();
if (static::$enableCache) {
$keys = implode('_', $models);
$modelsCache = $di->get('modelsCache');
$cacheKey = static::getCacheKey($params, $keys);
if ($modelsCache->has($cacheKey)) {
return $modelsCache->get($cacheKey);
}
}
$modelsManager = $di->getModelsManager();
$results = $modelsManager->executeQuery($phql,$params);
if (static::$enableCache) {
$modelsCache->set($cacheKey, $results);
}
return $results;
}
/**
* Flushes/clears the cache
*
* @return void
*/
public static function clearAllCache(): void
{
if (!self::$enableCache) {
return;
}
$di = Di::getDefault();
$cacheAdapter = $di->get('cacheAdapter');
$prefix = self::$prefix;
$keys = $cacheAdapter->getKeys($prefix);
foreach ($keys as $key) {
$cacheAdapter->delete($key);
}
}
/**
* Deletes all cache for model with the option to delete other related models
*
* @param bool $resetRelated delete all related models
* @return void
*/
public function clearCache(bool $resetRelated = false): void
{
if (!self::$enableCache) {
return;
}
$modelName = get_called_class();
$prefix = self::$prefix;
$models = [$modelName];
// get all related models when its set to true
if ($resetRelated) {
$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);
$cacheAdapter = $this->getDi()->get('cacheAdapter');
$keys = $cacheAdapter->getKeys($prefix);
foreach ($keys as $key) {
foreach ($models as $model) {
if (strpos($key, $model) !== false) {
$cacheAdapter->delete($key);
break;
}
}
}
}
/**
* Deletes all cache for model after Insert/Update queries
*
* @return void
*/
public function afterSave(): void
{
$this->clearCache();
}
/**
* Deletes all cache for model after Delete queries
*
* @return void
*/
public function afterDelete(): void
{
$this->clearCache();
}
}
@talal424
Copy link
Author

Usage:

  • Adjust the Phalcon\Cache service in your services file.

You must make a separate service for the adapter since you can't get cache keys from Phalcon\Cache

use Phalcon\Cache;
use Phalcon\Cache\AdapterFactory;
use Phalcon\Di\FactoryDefault;
use Phalcon\Storage\SerializerFactory;

$di->set(
    'cacheAdapter',
    function () {
        $config = $this->getConfig();
        $serializerFactory = new SerializerFactory();
        $adapterFactory    = new AdapterFactory($serializerFactory);

        $options = [
            'defaultSerializer' => 'php',
            'lifetime'          => 7200,
            'storageDir'        => $config->application->cacheDir,
            'prefix'            => 'some_prefix'
        ];

        $adapter = $adapterFactory->newInstance('stream', $options);

        return $adapter;
    }
);

$di->set(
    'modelsCache',
    function () {
        $adapter = $this->getCacheAdapter();
        return new Cache($adapter);
    }
);
  • Extend this class in all your models.
class Parts extends Cache
{
}
  • Set $enableCache to true to enable cache or call Cache::enableCache(true)

  • Change $prefix to your App name or call Cache::setPrefix('SomeApp')

all queries goes throw Phalcon\Model should be cached automatically

PHQL queries:

// 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 = RobotsParts::getPHQL($phql, ['Parts'], ['robots_id' => '1']);

include used models in PHQL query (as the second paramater) to ensure the clearance of the cache keys when Cache::clearCache(true) is called

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

call Cache::clearAllCache() to clear all cache keys

Notice:

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

Tested on:

  • Php 7.4.11
  • Phalcon 4.1.0

@netstu
Copy link

netstu commented Feb 15, 2021

redis storage not work

@talal424
Copy link
Author

redis storage not work

what is the issue?

i only tested on Phalcon\Cache\Adapter\Stream

if you have a solution can you post it here

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