mirror of
https://github.com/processwire/processwire.git
synced 2025-08-09 08:17:12 +02:00
Major refactor of WireCache which now isolates the cache getting/saving/deleting to a separate module/class implementing the WireCacheInterface interface. Eventually this will enable one to modify/replace where and how PW's cache data is stored. For instance, file system, Redis, Memcache, etc. The default class is WireCacheDatabase which stores cache data in the database, as WireCache did prior to this update.
This commit is contained in:
@@ -533,8 +533,12 @@ class Debug {
|
||||
$suffix = $options['ellipsis'];
|
||||
}
|
||||
foreach($value as $k => $v) {
|
||||
if(is_string($k) && strlen($k)) {
|
||||
$value[$k] = "$$k => " . self::traceStr($v, $options);
|
||||
} else {
|
||||
$value[$k] = self::traceStr($v, $options);
|
||||
}
|
||||
}
|
||||
$str = '[ ' . implode(', ', $value) . $suffix . ' ]';
|
||||
}
|
||||
} else if(is_string($value)) {
|
||||
@@ -632,6 +636,7 @@ class Debug {
|
||||
case 'json_encode':
|
||||
$value = json_encode($value, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
$value = str_replace(' ', ' ', $value);
|
||||
if(strpos($value, '\\"') !== false) $value = str_replace('\\"', "'", $value);
|
||||
break;
|
||||
case 'var_export':
|
||||
$value = var_export($value, true);
|
||||
|
@@ -720,4 +720,90 @@ interface InputfieldHasSelectableOptions {
|
||||
public function addOptionLabel($value, $label, $language = null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for WireCache handler classes
|
||||
*
|
||||
* @since 3.0.218
|
||||
*
|
||||
*/
|
||||
interface WireCacheInterface {
|
||||
/**
|
||||
* Get single cache
|
||||
*
|
||||
* @param string $name
|
||||
* @param string|array|null|false $expire
|
||||
* @return string|false
|
||||
*
|
||||
*/
|
||||
public function get($name, $expire);
|
||||
|
||||
/**
|
||||
* Get multiple caches
|
||||
*
|
||||
* @param array $names
|
||||
* @param string|array|null|false $expire
|
||||
* @return array
|
||||
*
|
||||
*/
|
||||
public function getMultiple(array $names, $expire);
|
||||
|
||||
/**
|
||||
* Save a cache
|
||||
*
|
||||
* @param string $name
|
||||
* @param string $data
|
||||
* @param string $expire
|
||||
* @return bool
|
||||
*
|
||||
*/
|
||||
public function save($name, $data, $expire);
|
||||
|
||||
/**
|
||||
* Delete cache
|
||||
*
|
||||
* @param string $name
|
||||
* @return bool
|
||||
*
|
||||
*/
|
||||
public function delete($name);
|
||||
|
||||
/**
|
||||
* Delete all caches
|
||||
*
|
||||
* @return int
|
||||
*
|
||||
*/
|
||||
public function deleteAll();
|
||||
|
||||
/**
|
||||
* Expire all caches
|
||||
*
|
||||
* @return int
|
||||
*
|
||||
*/
|
||||
public function expireAll();
|
||||
|
||||
/**
|
||||
* Cache maintenance / remove expired caches
|
||||
*
|
||||
* Called as part of a regular maintenance routine and after page/template save/deletion.
|
||||
*
|
||||
* @param Template|Page|null|bool Item to run maintenance for or, if not specified, general maintenance is performed.
|
||||
* General maintenance only runs once per request. Specify boolean true to force general maintenance to run.
|
||||
* @return bool
|
||||
*
|
||||
*/
|
||||
public function maintenance($obj = null);
|
||||
|
||||
/**
|
||||
* Get info about caches
|
||||
*
|
||||
* @param array $options
|
||||
* - `verbose` (bool): Return verbose details? (default=true)
|
||||
* - `names` (array): Names of caches to return info for, or omit for all (default=[])
|
||||
* - `exclude` (array): Name prefixes of caches to exclude from return value (default=[])
|
||||
* @return array
|
||||
*
|
||||
*/
|
||||
public function getInfo(array $options = array());
|
||||
}
|
||||
|
@@ -550,7 +550,7 @@ class ProcessWire extends Wire {
|
||||
$cache = $this->wire('cache', new WireCache(), true);
|
||||
$cacheNames = $config->preloadCacheNames;
|
||||
if($database->getEngine() === 'innodb') $cacheNames[] = 'InnoDB.stopwords';
|
||||
$cache->preload($cacheNames);
|
||||
$cache->preload($cacheNames, WireCache::expireIgnore);
|
||||
|
||||
$modules = null;
|
||||
try {
|
||||
|
@@ -378,9 +378,12 @@ class Sanitizer extends Wire {
|
||||
$value = mb_strtolower($value);
|
||||
|
||||
if(empty($replacements)) {
|
||||
$modules = $this->wire()->modules;
|
||||
if($modules) {
|
||||
$configData = $this->wire()->modules->getModuleConfigData('InputfieldPageName');
|
||||
$replacements = empty($configData['replacements']) ? InputfieldPageName::$defaultReplacements : $configData['replacements'];
|
||||
}
|
||||
}
|
||||
|
||||
foreach($replacements as $from => $to) {
|
||||
if(mb_strpos($value, $from) !== false) {
|
||||
@@ -5788,4 +5791,3 @@ class Sanitizer extends Wire {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@@ -5,7 +5,7 @@
|
||||
*
|
||||
* Simple cache for storing strings (encoded or otherwise) and serves as $cache API var
|
||||
*
|
||||
* ProcessWire 3.x, Copyright 2019 by Ryan Cramer
|
||||
* ProcessWire 3.x, Copyright 2023 by Ryan Cramer
|
||||
* https://processwire.com
|
||||
*
|
||||
* #pw-summary Provides easy, persistent caching of markup, strings, arrays or PageArray objects.
|
||||
@@ -86,6 +86,12 @@ class WireCache extends Wire {
|
||||
*/
|
||||
const expireMonthly = 2419200;
|
||||
|
||||
/**
|
||||
* Ignore expiration (skips expiration check) 3.0.218+
|
||||
*
|
||||
*/
|
||||
const expireIgnore = false;
|
||||
|
||||
/**
|
||||
* Date format used by our database queries
|
||||
* #pw-internal
|
||||
@@ -109,6 +115,7 @@ class WireCache extends Wire {
|
||||
'weekly' => self::expireWeekly,
|
||||
'month' => self::expireMonthly,
|
||||
'monthly' => self::expireMonthly,
|
||||
'ignore' => self::expireIgnore
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -120,20 +127,34 @@ class WireCache extends Wire {
|
||||
protected $preloads = array();
|
||||
|
||||
/**
|
||||
* Memory cache used by the maintenancePage method
|
||||
* Are we currently preloading?
|
||||
*
|
||||
* @var array|null Once determined becomes array of cache names => Selectors objects
|
||||
* @var bool
|
||||
*
|
||||
*/
|
||||
protected $cacheNameSelectors = null;
|
||||
protected $preloading = false;
|
||||
|
||||
/**
|
||||
* Whether or not it's worthwhile to attempt Page or Template maintenance after saves
|
||||
*
|
||||
* @var null|bool
|
||||
* @var WireCacheInterface
|
||||
*
|
||||
*/
|
||||
protected $usePageTemplateMaintenance = null;
|
||||
protected $cacher = null;
|
||||
|
||||
/**
|
||||
* Get the current WireClassInterface instance
|
||||
*
|
||||
* @return WireCacheInterface
|
||||
*
|
||||
*/
|
||||
protected function cacher() {
|
||||
$class = __NAMESPACE__ . "\\WireCacheDatabase";
|
||||
// $class = __NAMESPACE__ . "\\WireCacheFilesystem";
|
||||
if($this->cacher === null) {
|
||||
$this->cacher = new $class();
|
||||
$this->wire($this->cacher);
|
||||
}
|
||||
return $this->cacher;
|
||||
}
|
||||
|
||||
/**
|
||||
* Preload the given caches, so that they will be returned without query on the next get() call
|
||||
@@ -142,13 +163,14 @@ class WireCache extends Wire {
|
||||
*
|
||||
* #pw-group-advanced
|
||||
*
|
||||
* @param string|array $names
|
||||
* @param array $names
|
||||
* @param int|string|null $expire
|
||||
*
|
||||
*/
|
||||
public function preload(array $names, $expire = null) {
|
||||
if(!is_array($names)) $names = array($names);
|
||||
$this->preloading = true;
|
||||
$this->preloads = array_merge($this->preloads, $this->get($names, $expire));
|
||||
$this->preloading = false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -163,7 +185,9 @@ class WireCache extends Wire {
|
||||
public function preloadFor($ns, $expire = null) {
|
||||
if(is_object($ns)) $ns = wireClassName($ns, false);
|
||||
$ns .= '__*';
|
||||
$this->preloading = true;
|
||||
$this->preloads = array_merge($this->preloads, $this->get($ns, $expire));
|
||||
$this->preloading = false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -196,7 +220,7 @@ class WireCache extends Wire {
|
||||
* - If given a single cache name (string) just the contents of that cache will be returned.
|
||||
* - If given an array of names, multiple caches will be returned, indexed by cache name.
|
||||
* - If given a cache name with an asterisk in it, it will return an array of all matching caches.
|
||||
* @param int|string|null $expire Optionally specify max age (in seconds) OR oldest date string.
|
||||
* @param int|string|null|false $expire Optionally specify max age (in seconds) OR oldest date string, or false to ignore.
|
||||
* - If cache exists and is older, then blank returned. You may omit this to divert to whatever expiration
|
||||
* was specified at save() time. Note: The $expire and $func arguments may optionally be reversed.
|
||||
* - If using a $func, the behavior of $expire becomes the same as that of save().
|
||||
@@ -210,20 +234,24 @@ class WireCache extends Wire {
|
||||
*/
|
||||
public function get($name, $expire = null, $func = null) {
|
||||
|
||||
$_expire = $expire;
|
||||
if(!is_null($expire)) {
|
||||
if(!is_int($expire) && !is_string($expire) && !$expire instanceof Wire && is_callable($expire)) {
|
||||
$values = array();
|
||||
$expireNow = $expire === self::expireNow;
|
||||
$getMultiple = is_array($name); // retrieving multiple caches at once?
|
||||
|
||||
if($expire !== null && $expire !== self::expireIgnore) {
|
||||
if(!is_int($expire) && !is_string($expire) && is_callable($expire)) {
|
||||
$_func = $func;
|
||||
$func = $expire;
|
||||
$expire = is_null($_func) ? null : $this->getExpires($_func);
|
||||
$expire = $_func === null ? null : $this->getExpires($_func);
|
||||
unset($_func);
|
||||
} else {
|
||||
$expire = $this->getExpires($expire);
|
||||
}
|
||||
}
|
||||
|
||||
$multi = is_array($name); // retrieving multiple caches at once?
|
||||
if($multi) {
|
||||
if($expire === WireCache::expireNow) return ($getMultiple ? false : $values);
|
||||
|
||||
if($getMultiple) {
|
||||
$names = $name;
|
||||
} else {
|
||||
if(isset($this->preloads[$name])) {
|
||||
@@ -234,80 +262,46 @@ class WireCache extends Wire {
|
||||
$names = array($name);
|
||||
}
|
||||
|
||||
$where = array();
|
||||
$binds = array();
|
||||
$wildcards = array();
|
||||
$n = 0;
|
||||
|
||||
foreach($names as $name) {
|
||||
$n++;
|
||||
if(strpos($name, '*') !== false || strpos($name, '%') !== false) {
|
||||
foreach($names as $s) {
|
||||
if(strpos($s, '%') !== false) $s = str_replace('%', '*', $s);
|
||||
if(strpos($s, '*') === false) continue;
|
||||
// retrieve all caches matching wildcard
|
||||
$wildcards[$name] = $name;
|
||||
$name = str_replace('*', '%', $name);
|
||||
$multi = true;
|
||||
$where[$n] = "name LIKE :name$n";
|
||||
} else {
|
||||
$where[$n] = "name=:name$n";
|
||||
}
|
||||
$binds[":name$n"] = $name;
|
||||
$getMultiple = true;
|
||||
$wildcards[$s] = $s;
|
||||
}
|
||||
|
||||
if($multi && !is_null($func)) {
|
||||
if($getMultiple && $func !== null) {
|
||||
throw new WireException("Function (\$func) may not be specified to \$cache->get() when requesting multiple caches.");
|
||||
}
|
||||
|
||||
$sql = "SELECT name, data FROM caches WHERE (" . implode(' OR ', $where) . ") ";
|
||||
$cacher = $this->cacher();
|
||||
|
||||
if(is_null($expire)) { // || $func) {
|
||||
$sql .= "AND (expires>=:now OR expires<=:never) ";
|
||||
$binds[':now'] = date(self::dateFormat, time());
|
||||
$binds[':never'] = self::expireNever;
|
||||
} else if(is_array($expire)) {
|
||||
// expire is specified by a page selector, so we just let it through
|
||||
// since anything present is assumed to be valid
|
||||
} else {
|
||||
$sql .= "AND expires<=:expire ";
|
||||
$binds[':expire'] = $expire;
|
||||
// $sql .= "AND (expires>=:expire OR expires<=:never) ";
|
||||
//$binds[':never'] = self::expireNever;
|
||||
if($getMultiple) {
|
||||
$values = $expireNow ? array() : $cacher->getMultiple($names, $expire);
|
||||
foreach($values as $key => $value) {
|
||||
if($this->looksLikeJSON($value)) {
|
||||
$value = $this->decodeJSON($value);
|
||||
$values[$key] = $value;
|
||||
}
|
||||
|
||||
$query = $this->wire('database')->prepare($sql, "cache.get(" .
|
||||
implode('|', $names) . ", " . ($expire ? print_r($expire, true) : "null") . ")");
|
||||
|
||||
foreach($binds as $key => $value) $query->bindValue($key, $value);
|
||||
|
||||
$value = ''; // return value for non-multi mode
|
||||
$values = array(); // return value for multi-mode
|
||||
|
||||
if($_expire !== self::expireNow) try {
|
||||
$query->execute();
|
||||
if($query->rowCount() == 0) {
|
||||
$value = null; // cache does not exist
|
||||
} else while($row = $query->fetch(\PDO::FETCH_NUM)) {
|
||||
list($name, $value) = $row;
|
||||
if($this->looksLikeJSON($value)) $value = $this->decodeJSON($value);
|
||||
if($multi) $values[$name] = $value;
|
||||
}
|
||||
$query->closeCursor();
|
||||
|
||||
} catch(\Exception $e) {
|
||||
$this->trackException($e, false);
|
||||
$value = null;
|
||||
}
|
||||
|
||||
if($multi) {
|
||||
foreach($names as $name) {
|
||||
foreach($names as $s) {
|
||||
// ensure there is at least a placeholder for all requested caches
|
||||
if(!isset($values[$name]) && !isset($wildcards[$name])) $values[$name] = '';
|
||||
if(!isset($values[$s]) && !isset($wildcards[$s])) $values[$s] = '';
|
||||
}
|
||||
} else if(empty($value) && !is_null($func) && is_callable($func)) {
|
||||
} else {
|
||||
$value = $expireNow ? false : $cacher->get($name, $expire);
|
||||
if($value !== false && $this->looksLikeJSON($value)) {
|
||||
$value = $this->decodeJSON($value);
|
||||
}
|
||||
if(empty($value) && $func !== null && is_callable($func)) {
|
||||
// generate the cache now from the given callable function
|
||||
$value = $this->renderCacheValue($name, $expire, $func);
|
||||
}
|
||||
}
|
||||
|
||||
return $multi ? $values : $value;
|
||||
return $getMultiple ? $values : $value;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -382,6 +376,7 @@ class WireCache extends Wire {
|
||||
* @param string $name Cache name
|
||||
* @param null|int|string $expire Optional expiration
|
||||
* @param callable|null $func Optional cache generation function
|
||||
*
|
||||
* @return string|array|PageArray|mixed|null Returns null if cache doesn’t exist and no generation function provided.
|
||||
* @see WireCache::get()
|
||||
*
|
||||
@@ -418,6 +413,7 @@ class WireCache extends Wire {
|
||||
*
|
||||
*/
|
||||
public function save($name, $data, $expire = self::expireDaily) {
|
||||
$options = array(); // additional data to pass along to cacher save() method
|
||||
|
||||
if(is_array($data)) {
|
||||
if(array_key_exists('WireCache', $data)) {
|
||||
@@ -443,8 +439,9 @@ class WireCache extends Wire {
|
||||
'selector' => $expire['selector'],
|
||||
'WireCache' => $data
|
||||
);
|
||||
$options['expireArray'] = $expire;
|
||||
$expire = self::expireSelector;
|
||||
$this->cacheNameSelectors = null; // clear memory cache for maintenancePage method
|
||||
// $this->cacheNameSelectors = null; // clear memory cache for maintenancePage method
|
||||
}
|
||||
|
||||
if(is_array($data)) {
|
||||
@@ -457,18 +454,10 @@ class WireCache extends Wire {
|
||||
|
||||
if(is_null($data)) $data = '';
|
||||
|
||||
$sql =
|
||||
'INSERT INTO caches (`name`, `data`, `expires`) VALUES(:name, :data, :expires) ' .
|
||||
'ON DUPLICATE KEY UPDATE `data`=VALUES(`data`), `expires`=VALUES(`expires`)';
|
||||
|
||||
$query = $this->wire('database')->prepare($sql, "cache.save($name)");
|
||||
$query->bindValue(':name', $name);
|
||||
$query->bindValue(':data', $data);
|
||||
$query->bindValue(':expires', $expire);
|
||||
|
||||
try {
|
||||
$result = $query->execute();
|
||||
$result = $this->cacher()->save($name, $data, $expire);
|
||||
$this->log($this->_('Saved cache ') . ' - ' . $name);
|
||||
|
||||
} catch(\Exception $e) {
|
||||
$this->trackException($e, false);
|
||||
$result = false;
|
||||
@@ -517,11 +506,13 @@ class WireCache extends Wire {
|
||||
* Returns an array if expires info requires multiple parts, like with self::expireSelector.
|
||||
* In this case it returns array with array('expires' => date, 'selector' => selector);
|
||||
*
|
||||
* #pw-internal
|
||||
*
|
||||
* @param $expire
|
||||
* @return string|array
|
||||
*
|
||||
*/
|
||||
protected function getExpires($expire) {
|
||||
public function getExpires($expire) {
|
||||
|
||||
if(is_object($expire) && $expire->id) {
|
||||
|
||||
@@ -608,20 +599,8 @@ class WireCache extends Wire {
|
||||
*/
|
||||
public function delete($name) {
|
||||
try {
|
||||
if(strpos($name, '*') !== false || strpos($name, '%') !== false) {
|
||||
// delete all caches matching wildcard
|
||||
$name = str_replace('*', '%', $name);
|
||||
if($name === '%') return $this->deleteAll() ? true : false;
|
||||
$sql = 'DELETE FROM caches WHERE name LIKE :name';
|
||||
} else {
|
||||
$sql = 'DELETE FROM caches WHERE name=:name';
|
||||
}
|
||||
$query = $this->wire('database')->prepare($sql, "cache.delete($name)");
|
||||
$query->bindValue(':name', $name);
|
||||
$query->execute();
|
||||
$query->closeCursor();
|
||||
$success = true;
|
||||
$this->log($this->_('Cleared cache') . ' - ' . $name);
|
||||
$success = $this->cacher()->delete($name);
|
||||
$this->log("Cleared cache: $name");
|
||||
} catch(\Exception $e) {
|
||||
$this->trackException($e, true);
|
||||
$this->error($e->getMessage());
|
||||
@@ -641,12 +620,7 @@ class WireCache extends Wire {
|
||||
*/
|
||||
public function deleteAll() {
|
||||
try {
|
||||
$sql = "DELETE FROM caches WHERE expires!=:reserved";
|
||||
$query = $this->wire('database')->prepare($sql, "cache.deleteAll()");
|
||||
$query->bindValue(':reserved', self::expireReserved);
|
||||
$query->execute();
|
||||
$qty = $query->rowCount();
|
||||
$query->closeCursor();
|
||||
$qty = $this->cacher()->deleteAll();
|
||||
} catch(\Exception $e) {
|
||||
$this->trackException($e, true);
|
||||
$this->error($e->getMessage());
|
||||
@@ -666,12 +640,7 @@ class WireCache extends Wire {
|
||||
*/
|
||||
public function expireAll() {
|
||||
try {
|
||||
$sql = "DELETE FROM caches WHERE expires>:never";
|
||||
$query = $this->wire('database')->prepare($sql, "cache.expireAll()");
|
||||
$query->bindValue(':never', self::expireNever);
|
||||
$query->execute();
|
||||
$qty = $query->rowCount();
|
||||
$query->closeCursor();
|
||||
$qty = $this->cacher()->expireAll();
|
||||
} catch(\Exception $e) {
|
||||
$this->trackException($e, true);
|
||||
$this->error($e->getMessage());
|
||||
@@ -716,194 +685,16 @@ class WireCache extends Wire {
|
||||
*
|
||||
*/
|
||||
public function maintenance($obj = null) {
|
||||
|
||||
static $done = false;
|
||||
|
||||
$forceRun = false;
|
||||
$database = $this->wire()->database;
|
||||
$config = $this->wire()->config;
|
||||
|
||||
if(!$database || !$config) return false;
|
||||
|
||||
if(is_object($obj)) {
|
||||
|
||||
// check to see if it is worthwhile to perform this kind of maintenance at all
|
||||
if(is_null($this->usePageTemplateMaintenance)) {
|
||||
$templates = $this->wire()->templates;
|
||||
if(!$templates) $templates = array();
|
||||
$minID = 999999;
|
||||
$maxID = 0;
|
||||
foreach($templates as $template) {
|
||||
if($template->id > $maxID) $maxID = $template->id;
|
||||
if($template->id < $minID) $minID = $template->id;
|
||||
}
|
||||
$sql =
|
||||
"SELECT COUNT(*) FROM caches " .
|
||||
"WHERE (expires=:expireSave OR expires=:expireSelector) " .
|
||||
"OR (expires>=:minID AND expires<=:maxID)";
|
||||
|
||||
$query = $database->prepare($sql);
|
||||
$query->bindValue(':expireSave', self::expireSave);
|
||||
$query->bindValue(':expireSelector', self::expireSelector);
|
||||
$query->bindValue(':minID', date(self::dateFormat, $minID));
|
||||
$query->bindValue(':maxID', date(self::dateFormat, $maxID));
|
||||
$query->execute();
|
||||
$this->usePageTemplateMaintenance = (int) $query->fetchColumn();
|
||||
$query->closeCursor();
|
||||
}
|
||||
|
||||
if($this->usePageTemplateMaintenance) {
|
||||
if($obj instanceof Page) return $this->maintenancePage($obj);
|
||||
if($obj instanceof Template) return $this->maintenanceTemplate($obj);
|
||||
return true;
|
||||
} else {
|
||||
// skip it: no possible caches to maintain
|
||||
return true;
|
||||
}
|
||||
|
||||
} else if($obj === true) {
|
||||
// force run general maintenance, even if run earlier
|
||||
$forceRun = true;
|
||||
$done = true;
|
||||
|
||||
} else {
|
||||
// general maintenance: only perform maintenance once per request
|
||||
if($done) return true;
|
||||
$done = true;
|
||||
}
|
||||
|
||||
// don't perform general maintenance during ajax requests
|
||||
if($config->ajax && !$forceRun) return false;
|
||||
|
||||
// perform general maintenance now
|
||||
return $this->maintenanceGeneral();
|
||||
}
|
||||
|
||||
/**
|
||||
* General maintenance removes expired caches
|
||||
*
|
||||
* @return bool
|
||||
*
|
||||
*/
|
||||
protected function maintenanceGeneral() {
|
||||
|
||||
$database = $this->wire()->database;
|
||||
|
||||
$sql = 'DELETE FROM caches WHERE (expires<=:now AND expires>:never) ';
|
||||
$query = $database->prepare($sql, "cache.maintenance()");
|
||||
$query->bindValue(':now', date(self::dateFormat, time()));
|
||||
$query->bindValue(':never', self::expireNever);
|
||||
|
||||
try {
|
||||
$result = $query->execute();
|
||||
$qty = $result ? $query->rowCount() : 0;
|
||||
if($qty) $this->log(sprintf($this->_('General maintenance expired %d cache(s)'), $qty));
|
||||
$query->closeCursor();
|
||||
|
||||
$result = $this->cacher()->maintenance($obj);
|
||||
} catch(\Exception $e) {
|
||||
$this->trackException($e, false);
|
||||
$this->error($e->getMessage(), Notice::debug | Notice::log);
|
||||
$result = false;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run maintenance for a page that was just saved or deleted
|
||||
*
|
||||
* @param Page $page
|
||||
* @return bool
|
||||
*
|
||||
*/
|
||||
protected function maintenancePage(Page $page) {
|
||||
|
||||
$database = $this->wire()->database;
|
||||
|
||||
if(is_null($this->cacheNameSelectors)) {
|
||||
// locate all caches that specify selector strings and cache them so that
|
||||
// we don't have to re-load them on every page save
|
||||
try {
|
||||
$query = $database->prepare("SELECT * FROM caches WHERE expires=:expire");
|
||||
$query->bindValue(':expire', self::expireSelector);
|
||||
$query->execute();
|
||||
$this->cacheNameSelectors = array();
|
||||
} catch(\Exception $e) {
|
||||
$this->trackException($e, false);
|
||||
$this->error($e->getMessage(), Notice::log);
|
||||
return false;
|
||||
}
|
||||
if($query->rowCount()) {
|
||||
while($row = $query->fetch(\PDO::FETCH_ASSOC)) {
|
||||
$data = json_decode($row['data'], true);
|
||||
if($data !== false && isset($data['selector'])) {
|
||||
$name = $row['name'];
|
||||
$selectors = $this->wire(new Selectors($data['selector']));
|
||||
$this->cacheNameSelectors[$name] = $selectors;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// cacheNameSelectors already loaded once and is in cache
|
||||
}
|
||||
|
||||
// determine which selectors match the page: the $clearNames array
|
||||
// will hold the selectors that match this $page
|
||||
$n = 0;
|
||||
$clearNames = array();
|
||||
foreach($this->cacheNameSelectors as $name => $selectors) {
|
||||
if($page->matches($selectors)) {
|
||||
$clearNames["name" . (++$n)] = $name;
|
||||
}
|
||||
}
|
||||
|
||||
// clear any caches that expire on expireSave or specific page template
|
||||
$sql = "expires=:expireSave OR expires=:expireTemplateID ";
|
||||
|
||||
// expire any caches that match names found in cacheNameSelectors
|
||||
foreach($clearNames as $key => $name) {
|
||||
$sql .= "OR name=:$key ";
|
||||
}
|
||||
|
||||
$query = $database->prepare("DELETE FROM caches WHERE $sql");
|
||||
|
||||
// bind values
|
||||
$query->bindValue(':expireSave', self::expireSave);
|
||||
$query->bindValue(':expireTemplateID', date(self::dateFormat, $page->template->id));
|
||||
|
||||
foreach($clearNames as $key => $name) {
|
||||
$query->bindValue(":$key", $name);
|
||||
}
|
||||
|
||||
$result = $query->execute();
|
||||
$qty = $result ? $query->rowCount() : 0;
|
||||
if($qty) $this->log(sprintf($this->_('Maintenance expired %d cache(s) for saved page'), $qty));
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run maintenance for a template that was just saved or deleted
|
||||
*
|
||||
* @param Template $template
|
||||
* @return bool Returns true if any caches were deleted, false if not
|
||||
*
|
||||
*/
|
||||
protected function maintenanceTemplate(Template $template) {
|
||||
|
||||
$sql = 'DELETE FROM caches WHERE expires=:expireTemplateID OR expires=:expireSave';
|
||||
$query = $this->wire()->database->prepare($sql);
|
||||
|
||||
$query->bindValue(':expireSave', self::expireSave);
|
||||
$query->bindValue(':expireTemplateID', date(self::dateFormat, $template->id));
|
||||
|
||||
$result = $query->execute();
|
||||
$qty = $result ? $query->rowCount() : 0;
|
||||
if($qty) $this->log(sprintf($this->_('Maintenance expired %d cache(s) for saved template'), $qty));
|
||||
|
||||
return $qty > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a cacheable array to a PageArray
|
||||
*
|
||||
@@ -922,12 +713,12 @@ class WireCache extends Wire {
|
||||
}
|
||||
|
||||
$options = array();
|
||||
$template = empty($data['template']) ? null : $this->wire('templates')->get((int) $data['template']);
|
||||
$template = empty($data['template']) ? null : $this->wire()->templates->get((int) $data['template']);
|
||||
if($template) $options['template'] = $template;
|
||||
if($pageArrayClass != 'PageArray') $options['pageArrayClass'] = $pageArrayClass;
|
||||
if(!empty($data['pageClass']) && $data['pageClass'] != 'Page') $options['pageClass'] = $data['pageClass'];
|
||||
|
||||
return $this->wire('pages')->getById($data['PageArray'], $options);
|
||||
return $this->wire()->pages->getById($data['PageArray'], $options);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -983,106 +774,14 @@ class WireCache extends Wire {
|
||||
*/
|
||||
public function getInfo($verbose = true, $names = array(), $exclude = array()) {
|
||||
|
||||
$templates = $this->wire()->templates;
|
||||
$database = $this->wire()->database;
|
||||
|
||||
if(is_string($names)) $names = empty($names) ? array() : array($names);
|
||||
if(is_string($exclude)) $exclude = empty($exclude) ? array() : array($exclude);
|
||||
|
||||
$all = array();
|
||||
$binds = array();
|
||||
$wheres = array();
|
||||
$sql = "SELECT name, data, expires FROM caches ";
|
||||
|
||||
if(count($names)) {
|
||||
$a = array();
|
||||
foreach($names as $n => $s) {
|
||||
$a[] = "name=:name$n";
|
||||
$binds[":name$n"] = $s;
|
||||
}
|
||||
$wheres[] = '(' . implode(' OR ', $a) . ')';
|
||||
}
|
||||
|
||||
if(count($exclude)) {
|
||||
foreach($exclude as $n => $s) {
|
||||
$wheres[] = "name NOT LIKE :ex$n";
|
||||
$binds[":ex$n"] = $s . '%';
|
||||
}
|
||||
}
|
||||
|
||||
if(count($wheres)) {
|
||||
$sql .= "WHERE " . implode(' AND ', $wheres);
|
||||
}
|
||||
|
||||
$query = $database->prepare($sql);
|
||||
|
||||
foreach($binds as $key => $val) {
|
||||
$query->bindValue($key, $val);
|
||||
}
|
||||
|
||||
$query->execute();
|
||||
|
||||
while($row = $query->fetch(\PDO::FETCH_ASSOC)) {
|
||||
|
||||
$info = array(
|
||||
'name' => $row['name'],
|
||||
'type' => 'string',
|
||||
'expires' => '',
|
||||
);
|
||||
|
||||
if($this->looksLikeJSON($row['data'])) {
|
||||
// json encoded
|
||||
$data = json_decode($row['data'], true);
|
||||
if(is_array($data)) {
|
||||
if(array_key_exists('WireCache', $data)) {
|
||||
if(isset($data['selector'])) {
|
||||
$selector = $data['selector'];
|
||||
$info['expires'] = $verbose ? 'when selector matches modified page' : 'selector';
|
||||
$info['selector'] = $selector;
|
||||
}
|
||||
$data = $data['WireCache'];
|
||||
}
|
||||
if(is_array($data) && array_key_exists('PageArray', $data) && array_key_exists('template', $data)) {
|
||||
$info['type'] = 'PageArray';
|
||||
if($verbose) $info['type'] .= ' (' . count($data['PageArray']) . ' pages)';
|
||||
} else if(is_array($data)) {
|
||||
$info['type'] = 'array';
|
||||
if($verbose) $info['type'] .= ' (' . count($data) . ' items)';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(empty($info['expires'])) {
|
||||
if($row['expires'] === self::expireNever) {
|
||||
$info['expires'] = $verbose ? 'never' : '';
|
||||
} else if($row['expires'] === self::expireReserved) {
|
||||
$info['expires'] = $verbose ? 'reserved' : '';
|
||||
} else if($row['expires'] === self::expireSave) {
|
||||
$info['expires'] = $verbose ? 'when any page or template is modified' : 'save';
|
||||
} else if($row['expires'] < self::expireSave) {
|
||||
// potential template ID encoded as date string
|
||||
$templateId = strtotime($row['expires']);
|
||||
$template = $templates->get($templateId);
|
||||
if($template) {
|
||||
$info['expires'] = $verbose ? "when '$template->name' page or template is modified" : 'save';
|
||||
$info['template'] = $template->id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(empty($info['expires'])) {
|
||||
$info['expires'] = $row['expires'];
|
||||
if($verbose) $info['expires'] .= " (" . wireRelativeTimeStr($row['expires']) . ")";
|
||||
}
|
||||
}
|
||||
|
||||
if($verbose) $info['size'] = strlen($row['data']);
|
||||
|
||||
$all[] = $info;
|
||||
}
|
||||
|
||||
$query->closeCursor();
|
||||
|
||||
return $all;
|
||||
return $this->cacher()->getInfo(array(
|
||||
'verbose' => $verbose,
|
||||
'names' => $names,
|
||||
'exclude' => $exclude
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1134,9 +833,8 @@ class WireCache extends Wire {
|
||||
'throwExceptions' => true,
|
||||
);
|
||||
|
||||
$out = null;
|
||||
$paths = $this->wire('config')->paths;
|
||||
$files = $this->wire('files');
|
||||
$paths = $this->wire()->config->paths;
|
||||
$files = $this->wire()->files;
|
||||
$filename = $files->unixFileName($filename);
|
||||
|
||||
if(strpos($filename, '/') !== 0 && strpos($filename, ':') === false && strpos($filename, '//') === false) {
|
||||
@@ -1164,7 +862,7 @@ class WireCache extends Wire {
|
||||
// cache value is array where [ 0=created, 1='value' ]
|
||||
if(!is_array($data) || $data[0] < $mtime) {
|
||||
// cache does not exist or is older source file mtime
|
||||
$out = $this->wire('files')->render($filename, $options['vars'], $options);
|
||||
$out = $files->render($filename, $options['vars'], $options);
|
||||
if($out === false) return false;
|
||||
$data = array(time(), $out);
|
||||
if($expire === null) $expire = self::expireDaily;
|
||||
@@ -1179,15 +877,17 @@ class WireCache extends Wire {
|
||||
/**
|
||||
* Make sure a cache name is of the right length and format for a cache name
|
||||
*
|
||||
* #pw-internal
|
||||
*
|
||||
* @param string $name Name including namespace (if applicable)
|
||||
* @param bool|string $ns True to allow namespace present, false to prevent, or specify namespace to add to name if not already present.
|
||||
* @param bool|string $ns True to allow namespace present, false to prevent, or specify namespace to add to name if not already present. (default=true)
|
||||
* @return string
|
||||
* @since 3.0.130
|
||||
* @todo update other methods in this class to use this method
|
||||
*
|
||||
*
|
||||
*/
|
||||
protected function cacheName($name, $ns = true) {
|
||||
public function cacheName($name, $ns = true) {
|
||||
|
||||
$maxLength = 190;
|
||||
$name = trim($name);
|
||||
@@ -1233,15 +933,16 @@ class WireCache extends Wire {
|
||||
return $name;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Does the given string look like it might be JSON?
|
||||
*
|
||||
* #pw-internal
|
||||
*
|
||||
* @param string $str
|
||||
* @return bool
|
||||
*
|
||||
*/
|
||||
protected function looksLikeJSON(&$str) {
|
||||
public function looksLikeJSON(&$str) {
|
||||
if(empty($str)) return false;
|
||||
$c = substr($str, 0, 1);
|
||||
if($c === '{' && substr(trim($str), -1) === '}') return true;
|
||||
@@ -1254,12 +955,14 @@ class WireCache extends Wire {
|
||||
*
|
||||
* Returns the given $value if it cannot be decoded.
|
||||
*
|
||||
* #pw-internal
|
||||
*
|
||||
* @param string $value JSON encoded text value
|
||||
* @param bool $toArray Decode to associative array? Specify false to decode to object. (default=true)
|
||||
* @return array|mixed|PageArray
|
||||
*
|
||||
*/
|
||||
protected function decodeJSON($value, $toArray = true) {
|
||||
public function decodeJSON($value, $toArray = true) {
|
||||
|
||||
$a = json_decode($value, $toArray);
|
||||
|
||||
@@ -1298,9 +1001,9 @@ class WireCache extends Wire {
|
||||
*
|
||||
*/
|
||||
public function ___log($str = '', array $options = array()) {
|
||||
//parent::___log($str, array('name' => 'modules'));
|
||||
return null;
|
||||
//parent::___log($str, array('name' => 'cache'));
|
||||
$str = ''; // disable log
|
||||
return parent::___log($str, $options);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
533
wire/core/WireCacheDatabase.php
Normal file
533
wire/core/WireCacheDatabase.php
Normal file
@@ -0,0 +1,533 @@
|
||||
<?php namespace ProcessWire;
|
||||
|
||||
/**
|
||||
* Database cache handler for WireCache
|
||||
*
|
||||
* ProcessWire 3.x, Copyright 2023 by Ryan Cramer
|
||||
* https://processwire.com
|
||||
*
|
||||
* @since 2.0.218
|
||||
*
|
||||
*/
|
||||
class WireCacheDatabase extends Wire implements WireCacheInterface {
|
||||
|
||||
const useLog = false;
|
||||
|
||||
/**
|
||||
* Memory cache used by the maintenancePage method
|
||||
*
|
||||
* @var array|null Once determined becomes array of cache names => Selectors objects
|
||||
*
|
||||
*/
|
||||
protected $cacheNameSelectors = null;
|
||||
|
||||
/**
|
||||
* Whether or not it's worthwhile to attempt Page or Template maintenance after saves
|
||||
*
|
||||
* @var null|bool
|
||||
*
|
||||
*/
|
||||
protected $usePageTemplateMaintenance = null;
|
||||
|
||||
/**
|
||||
* Get cache by name
|
||||
*
|
||||
* @param string $name Cache name to get
|
||||
* @param string|array|null $expire Datetime in 'YYYY-MM-DD HH:MM:SS' format or array of them, or null for any
|
||||
* @return string|false
|
||||
*
|
||||
*/
|
||||
public function get($name, $expire) {
|
||||
$values = $this->getMultiple(array($name), $expire);
|
||||
return count($values) ? reset($values) : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find multiple caches by name and return them
|
||||
*
|
||||
* @param array $names Cache names to get
|
||||
* @param string|array|null|false $expire Datetime in 'YYYY-MM-DD HH:MM:SS' format or array of them, or null for any, false to ignore
|
||||
* @return array
|
||||
*
|
||||
*/
|
||||
public function getMultiple(array $names, $expire) {
|
||||
|
||||
$where = array();
|
||||
$binds = array();
|
||||
$n = 0;
|
||||
|
||||
foreach($names as $s) {
|
||||
$n++;
|
||||
if(strpos($s, '*') !== false) {
|
||||
// retrieve all caches matching wildcard
|
||||
$s = str_replace('*', '%', $s);
|
||||
$where[$n] = "name LIKE :name$n";
|
||||
} else {
|
||||
$where[$n] = "name=:name$n";
|
||||
}
|
||||
$binds[":name$n"] = $s;
|
||||
}
|
||||
|
||||
$sql = "SELECT name, data FROM caches WHERE (" . implode(' OR ', $where) . ") ";
|
||||
|
||||
if($expire === null) {
|
||||
$sql .= "AND (expires>=:now OR expires<=:never) ";
|
||||
$binds[':now'] = date(WireCache::dateFormat, time());
|
||||
$binds[':never'] = WireCache::expireNever;
|
||||
} else if($expire === WireCache::expireIgnore) {
|
||||
// ignore expiration
|
||||
} else if(is_array($expire)) {
|
||||
// expire is specified by a page selector, so we just let it through
|
||||
// since anything present is assumed to be valid
|
||||
} else {
|
||||
$sql .= "AND expires<=:expire ";
|
||||
$binds[':expire'] = $expire;
|
||||
}
|
||||
|
||||
$query = $this->wire()->database->prepare($sql, "cache.get(" .
|
||||
implode('|', $names) . ", " . ($expire ? print_r($expire, true) : "null") . ")");
|
||||
|
||||
foreach($binds as $key => $value) {
|
||||
$query->bindValue($key, $value);
|
||||
}
|
||||
|
||||
$values = array(); // return value for multi-mode
|
||||
|
||||
$query->execute();
|
||||
|
||||
if(!$query->rowCount()) return $values;
|
||||
|
||||
while($row = $query->fetch(\PDO::FETCH_NUM)) {
|
||||
list($name, $value) = $row;
|
||||
$values[$name] = $value;
|
||||
}
|
||||
|
||||
$query->closeCursor();
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a cache
|
||||
*
|
||||
* @param string $name
|
||||
* @param string $data
|
||||
* @param string $expire
|
||||
* @return bool
|
||||
*
|
||||
*/
|
||||
public function save($name, $data, $expire) {
|
||||
|
||||
if($expire === WireCache::expireSelector) {
|
||||
$this->cacheNameSelectors = null;
|
||||
}
|
||||
|
||||
$sql =
|
||||
'INSERT INTO caches (`name`, `data`, `expires`) VALUES(:name, :data, :expires) ' .
|
||||
'ON DUPLICATE KEY UPDATE `data`=VALUES(`data`), `expires`=VALUES(`expires`)';
|
||||
|
||||
$query = $this->wire()->database->prepare($sql, "cache.save($name)");
|
||||
$query->bindValue(':name', $name);
|
||||
$query->bindValue(':data', $data);
|
||||
$query->bindValue(':expires', $expire);
|
||||
|
||||
$result = $query->execute();
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a cache by name
|
||||
*
|
||||
* @param string $name
|
||||
* @return bool
|
||||
*
|
||||
*/
|
||||
public function delete($name) {
|
||||
if(strpos($name, '*') !== false) {
|
||||
// delete all caches matching wildcard
|
||||
$name = str_replace('*', '%', $name);
|
||||
if($name === '%') return $this->deleteAll() ? true : false;
|
||||
$sql = 'DELETE FROM caches WHERE name LIKE :name';
|
||||
} else {
|
||||
$sql = 'DELETE FROM caches WHERE name=:name';
|
||||
}
|
||||
$query = $this->wire()->database->prepare($sql, "cache.delete($name)");
|
||||
$query->bindValue(':name', $name);
|
||||
$result = $query->execute();
|
||||
$query->closeCursor();
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all caches
|
||||
*
|
||||
* @return int
|
||||
*
|
||||
*/
|
||||
public function deleteAll() {
|
||||
$sql = "DELETE FROM caches WHERE expires!=:reserved";
|
||||
$query = $this->wire()->database->prepare($sql, "cache.deleteAll()");
|
||||
$query->bindValue(':reserved', WireCache::expireReserved);
|
||||
$query->execute();
|
||||
$qty = $query->rowCount();
|
||||
$query->closeCursor();
|
||||
return $qty;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expire all caches
|
||||
*
|
||||
* @return int
|
||||
*
|
||||
*/
|
||||
public function expireAll() {
|
||||
$sql = "DELETE FROM caches WHERE expires>:never";
|
||||
$query = $this->wire()->database->prepare($sql, "cache.expireAll()");
|
||||
$query->bindValue(':never', WireCache::expireNever);
|
||||
$query->execute();
|
||||
$qty = $query->rowCount();
|
||||
$query->closeCursor();
|
||||
return $qty;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache maintenance removes expired caches
|
||||
*
|
||||
* Should be called as part of a regular maintenance routine and after page/template save/deletion.
|
||||
* ProcessWire already calls this automatically, so you don’t typically need to call this method on your own.
|
||||
*
|
||||
* #pw-group-advanced
|
||||
*
|
||||
* @param Template|Page|null|bool Item to run maintenance for or, if not specified, general maintenance is performed.
|
||||
* General maintenance only runs once per request. Specify boolean true to force general maintenance to run.
|
||||
* @return bool
|
||||
*
|
||||
*/
|
||||
public function maintenance($obj = null) {
|
||||
|
||||
static $done = false;
|
||||
|
||||
$forceRun = false;
|
||||
$database = $this->wire()->database;
|
||||
$config = $this->wire()->config;
|
||||
|
||||
if(!$database || !$config) return false;
|
||||
|
||||
if(is_object($obj)) {
|
||||
|
||||
// check to see if it is worthwhile to perform this kind of maintenance at all
|
||||
if($this->usePageTemplateMaintenance === null) {
|
||||
$templates = $this->wire()->templates;
|
||||
if(!$templates) $templates = array();
|
||||
$minID = 999999;
|
||||
$maxID = 0;
|
||||
foreach($templates as $template) {
|
||||
if($template->id > $maxID) $maxID = $template->id;
|
||||
if($template->id < $minID) $minID = $template->id;
|
||||
}
|
||||
$sql =
|
||||
"SELECT COUNT(*) FROM caches " .
|
||||
"WHERE (expires=:expireSave OR expires=:expireSelector) " .
|
||||
"OR (expires>=:minID AND expires<=:maxID)";
|
||||
|
||||
$query = $database->prepare($sql);
|
||||
$query->bindValue(':expireSave', WireCache::expireSave);
|
||||
$query->bindValue(':expireSelector', WireCache::expireSelector);
|
||||
$query->bindValue(':minID', date(WireCache::dateFormat, $minID));
|
||||
$query->bindValue(':maxID', date(WireCache::dateFormat, $maxID));
|
||||
$query->execute();
|
||||
$this->usePageTemplateMaintenance = (int) $query->fetchColumn();
|
||||
$query->closeCursor();
|
||||
}
|
||||
|
||||
if($this->usePageTemplateMaintenance) {
|
||||
if($obj instanceof Page) return $this->maintenancePage($obj);
|
||||
if($obj instanceof Template) return $this->maintenanceTemplate($obj);
|
||||
} else {
|
||||
// skip it: no possible caches to maintain
|
||||
}
|
||||
return true;
|
||||
|
||||
} else if($obj === true) {
|
||||
// force run general maintenance, even if run earlier
|
||||
$forceRun = true;
|
||||
$done = true;
|
||||
|
||||
} else {
|
||||
// general maintenance: only perform maintenance once per request
|
||||
if($done) return true;
|
||||
$done = true;
|
||||
}
|
||||
|
||||
// don't perform general maintenance during ajax requests
|
||||
if($config->ajax && !$forceRun) return false;
|
||||
|
||||
// perform general maintenance now
|
||||
return $this->maintenanceGeneral();
|
||||
}
|
||||
|
||||
/**
|
||||
* General maintenance removes expired caches
|
||||
*
|
||||
* @return bool
|
||||
*
|
||||
*/
|
||||
protected function maintenanceGeneral() {
|
||||
|
||||
$database = $this->wire()->database;
|
||||
|
||||
$sql = 'DELETE FROM caches WHERE (expires<=:now AND expires>:never) ';
|
||||
$query = $database->prepare($sql, "cache.maintenance()");
|
||||
$query->bindValue(':now', date(WireCache::dateFormat, time()));
|
||||
$query->bindValue(':never', WireCache::expireNever);
|
||||
|
||||
$result = $query->execute();
|
||||
$qty = $result ? $query->rowCount() : 0;
|
||||
if(self::useLog && $qty) $this->wire()->cache->log(sprintf('General maintenance expired %d cache(s)', $qty));
|
||||
$query->closeCursor();
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run maintenance for a page that was just saved or deleted
|
||||
*
|
||||
* @param Page $page
|
||||
* @return bool
|
||||
*
|
||||
*/
|
||||
protected function maintenancePage(Page $page) {
|
||||
|
||||
$database = $this->wire()->database;
|
||||
|
||||
if($this->cacheNameSelectors === null) {
|
||||
// locate all caches that specify selector strings and cache them so that
|
||||
// we don't have to re-load them on every page save
|
||||
$this->cacheNameSelectors = array();
|
||||
try {
|
||||
$query = $database->prepare("SELECT * FROM caches WHERE expires=:expire");
|
||||
$query->bindValue(':expire', WireCache::expireSelector);
|
||||
$query->execute();
|
||||
} catch(\Exception $e) {
|
||||
$this->trackException($e, false);
|
||||
$this->error($e->getMessage(), Notice::log);
|
||||
return false;
|
||||
}
|
||||
if($query->rowCount()) {
|
||||
while($row = $query->fetch(\PDO::FETCH_ASSOC)) {
|
||||
$data = json_decode($row['data'], true);
|
||||
if($data !== false && isset($data['selector'])) {
|
||||
$name = $row['name'];
|
||||
$selectors = $this->wire(new Selectors($data['selector']));
|
||||
$this->cacheNameSelectors[$name] = $selectors;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// cacheNameSelectors already loaded once and is in cache
|
||||
}
|
||||
|
||||
// determine which selectors match the page: the $clearNames array
|
||||
// will hold the selectors that match this $page
|
||||
$n = 0;
|
||||
$clearNames = array();
|
||||
foreach($this->cacheNameSelectors as $name => $selectors) {
|
||||
if($page->matches($selectors)) {
|
||||
$clearNames["name" . (++$n)] = $name;
|
||||
}
|
||||
}
|
||||
|
||||
// clear any caches that expire on expireSave or specific page template
|
||||
$sql = "expires=:expireSave OR expires=:expireTemplateID ";
|
||||
|
||||
// expire any caches that match names found in cacheNameSelectors
|
||||
foreach($clearNames as $key => $name) {
|
||||
$sql .= "OR name=:$key ";
|
||||
}
|
||||
|
||||
$query = $database->prepare("DELETE FROM caches WHERE $sql");
|
||||
|
||||
// bind values
|
||||
$query->bindValue(':expireSave', WireCache::expireSave);
|
||||
$query->bindValue(':expireTemplateID', date(WireCache::dateFormat, $page->template->id));
|
||||
|
||||
foreach($clearNames as $key => $name) {
|
||||
$query->bindValue(":$key", $name);
|
||||
}
|
||||
|
||||
$result = $query->execute();
|
||||
$qty = $result ? $query->rowCount() : 0;
|
||||
if(self::useLog && $qty) {
|
||||
$this->wire()->cache->log(sprintf('Maintenance expired %d cache(s) for saved page', $qty));
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run maintenance for a template that was just saved or deleted
|
||||
*
|
||||
* @param Template $template
|
||||
* @return bool Returns true if any caches were deleted, false if not
|
||||
*
|
||||
*/
|
||||
protected function maintenanceTemplate(Template $template) {
|
||||
|
||||
$sql = 'DELETE FROM caches WHERE expires=:expireTemplateID OR expires=:expireSave';
|
||||
$query = $this->wire()->database->prepare($sql);
|
||||
|
||||
$query->bindValue(':expireSave', WireCache::expireSave);
|
||||
$query->bindValue(':expireTemplateID', date(WireCache::dateFormat, $template->id));
|
||||
|
||||
$result = $query->execute();
|
||||
$qty = $result ? $query->rowCount() : 0;
|
||||
if(self::useLog && $qty) $this->wire()->cache->log(sprintf('Maintenance expired %d cache(s) for saved template', $qty));
|
||||
|
||||
return $qty > 0;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get info about caches
|
||||
*
|
||||
* @param array $options
|
||||
* - `verbose` (bool): Return verbose details? (default=true)
|
||||
* - `names` (array): Names of caches to return info for, or omit for all (default=[])
|
||||
* - `exclude` (array): Name prefixes of caches to exclude from return value (default=[])
|
||||
* @return array
|
||||
*
|
||||
*/
|
||||
public function getInfo(array $options = array()) {
|
||||
|
||||
$templates = $this->wire()->templates;
|
||||
$database = $this->wire()->database;
|
||||
|
||||
$defaults = array(
|
||||
'verbose' => true,
|
||||
'names' => array(),
|
||||
'exclude' => array()
|
||||
);
|
||||
|
||||
$options = array_merge($defaults, $options);
|
||||
$verbose = (bool) $options['verbose'];
|
||||
$names = $options['names'];
|
||||
$exclude = $options['exclude'];
|
||||
|
||||
$all = array();
|
||||
$binds = array();
|
||||
$wheres = array();
|
||||
$sql = "SELECT name, data, expires FROM caches ";
|
||||
|
||||
if(count($names)) {
|
||||
$a = array();
|
||||
foreach($names as $n => $s) {
|
||||
$a[] = "name=:name$n";
|
||||
$binds[":name$n"] = $s;
|
||||
}
|
||||
$wheres[] = '(' . implode(' OR ', $a) . ')';
|
||||
}
|
||||
|
||||
if(count($exclude)) {
|
||||
foreach($exclude as $n => $s) {
|
||||
$wheres[] = "name NOT LIKE :ex$n";
|
||||
$binds[":ex$n"] = $s . '%';
|
||||
}
|
||||
}
|
||||
|
||||
if(count($wheres)) {
|
||||
$sql .= "WHERE " . implode(' AND ', $wheres);
|
||||
}
|
||||
|
||||
$query = $database->prepare($sql);
|
||||
|
||||
foreach($binds as $key => $val) {
|
||||
$query->bindValue($key, $val);
|
||||
}
|
||||
|
||||
$query->execute();
|
||||
|
||||
while($row = $query->fetch(\PDO::FETCH_ASSOC)) {
|
||||
|
||||
$info = array(
|
||||
'name' => $row['name'],
|
||||
'type' => 'string',
|
||||
'expires' => '',
|
||||
);
|
||||
|
||||
if($this->wire()->cache->looksLikeJSON($row['data'])) {
|
||||
// json encoded
|
||||
$data = json_decode($row['data'], true);
|
||||
if(is_array($data)) {
|
||||
if(array_key_exists('WireCache', $data)) {
|
||||
if(isset($data['selector'])) {
|
||||
$selector = $data['selector'];
|
||||
$info['expires'] = $verbose ? 'when selector matches modified page' : 'selector';
|
||||
$info['selector'] = $selector;
|
||||
}
|
||||
$data = $data['WireCache'];
|
||||
}
|
||||
if(is_array($data) && array_key_exists('PageArray', $data) && array_key_exists('template', $data)) {
|
||||
$info['type'] = 'PageArray';
|
||||
if($verbose) $info['type'] .= ' (' . count($data['PageArray']) . ' pages)';
|
||||
} else if(is_array($data)) {
|
||||
$info['type'] = 'array';
|
||||
if($verbose) $info['type'] .= ' (' . count($data) . ' items)';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(empty($info['expires'])) {
|
||||
if($row['expires'] === WireCache::expireNever) {
|
||||
$info['expires'] = $verbose ? 'never' : '';
|
||||
} else if($row['expires'] === WireCache::expireReserved) {
|
||||
$info['expires'] = $verbose ? 'reserved' : '';
|
||||
} else if($row['expires'] === WireCache::expireSave) {
|
||||
$info['expires'] = $verbose ? 'when any page or template is modified' : 'save';
|
||||
} else if($row['expires'] < WireCache::expireSave) {
|
||||
// potential template ID encoded as date string
|
||||
$templateId = strtotime($row['expires']);
|
||||
$template = $templates->get($templateId);
|
||||
if($template) {
|
||||
$info['expires'] = $verbose ? "when '$template->name' page or template is modified" : 'save';
|
||||
$info['template'] = $template->id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(empty($info['expires'])) {
|
||||
$info['expires'] = $row['expires'];
|
||||
if($verbose) $info['expires'] .= " (" . wireRelativeTimeStr($row['expires']) . ")";
|
||||
}
|
||||
}
|
||||
|
||||
if($verbose) $info['size'] = strlen($row['data']);
|
||||
|
||||
$all[] = $info;
|
||||
}
|
||||
|
||||
$query->closeCursor();
|
||||
|
||||
return $all;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save to the cache log
|
||||
*
|
||||
* #pw-internal
|
||||
*
|
||||
* @param string $str Message to log
|
||||
* @param array $options
|
||||
* @return WireLog
|
||||
*
|
||||
*/
|
||||
public function ___log($str = '', array $options = array()) {
|
||||
//parent::___log($str, array('name' => 'cache'));
|
||||
if(self::useLog) {
|
||||
return $this->wire()->cache->log($str, $options);
|
||||
} else {
|
||||
$str = ''; // disable log
|
||||
}
|
||||
return parent::___log($str, $options);
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user