artabro/wire/core/PagesLoaderCache.php
2024-08-27 11:35:37 +02:00

323 lines
8.5 KiB
PHP

<?php namespace ProcessWire;
/**
* ProcessWire Pages Loader Cache
*
* Implements page caching of loaded pages and PageArrays for $pages API variable
*
* ProcessWire 3.x, Copyright 2022 by Ryan Cramer
* https://processwire.com
*
*/
class PagesLoaderCache extends Wire {
/**
* Pages that have been cached, indexed by ID
*
*/
protected $pageIdCache = array();
/**
* Cached selector strings and the PageArray that was found.
*
*/
protected $pageSelectorCache = array();
/**
* [ 'cache group name' => [ page IDs ] ]
*
* @var array
*
*/
protected $cacheGroups = array();
/**
* @var Pages
*
*/
protected $pages;
/**
* Construct
*
* @param Pages $pages
*
*/
public function __construct(Pages $pages) {
parent::__construct();
$this->pages = $pages;
}
/**
* Get cache status
*
* Returns count of each cache type, or contents of each cache type of verbose option is specified.
*
* @param bool|null $verbose Specify true to get contents of cache, false to get string counts, or omit for array of counts
* @return array|string
* @since 3.0.198
*
*/
public function getCacheStatus($verbose = null) {
$a = array(
'pages' => ($verbose ? $this->pageIdCache : count($this->pageIdCache)),
'selectors' => ($verbose ? $this->pageSelectorCache : count($this->pageSelectorCache)),
'groups' => ($verbose ? $this->cacheGroups : count($this->cacheGroups)),
);
return ($verbose === false ? "pages=$a[pages], selectors=$a[selectors], groups=$a[groups]" : $a);
}
/**
* Given a Page ID, return it if it's cached, or NULL of it's not.
*
* If no ID is provided, then this will return an array copy of the full cache.
*
* You may also pass in the string "id=123", where 123 is the page_id
*
* @param int|string|null $id
* @return Page|array|null
*
*/
public function getCache($id = null) {
if(!$id) return $this->pageIdCache;
if(!ctype_digit("$id")) $id = str_replace('id=', '', $id);
if(ctype_digit("$id")) $id = (int) $id;
if(!isset($this->pageIdCache[$id])) return null;
$page = $this->pageIdCache[$id]; /** @var Page $page */
$of = $this->pages->loader()->getOutputFormatting();
if(!$of && $page === $this->wire()->page) return $page; // skip of() adjustment
$page->of($of);
return $page;
}
/**
* Cache the given page.
*
* @param Page $page
* @return void
*
*/
public function cache(Page $page) {
if($page->id) $this->pageIdCache[$page->id] = $page;
}
/**
* Cache given page into a named group that it can be uncached with
*
* @param Page $page
* @param string $groupName
* @since 3.0.198
*
*/
public function cacheGroup(Page $page, $groupName) {
if(!$page->id) return;
if(!isset($this->cacheGroups[$groupName])) $this->cacheGroups[$groupName] = array();
$this->pageIdCache[$page->id] = $page;
$this->cacheGroups[$groupName][] = $page->id;
}
/**
* Remove the given page from the cache.
*
* Note: does not remove pages from selectorCache. Call uncacheAll to do that.
*
* @param Page|int $page Page to uncache or ID of page (prior to 3.0.153 only Page object was accepted)
* @param array $options Additional options to modify behavior:
* - `shallow` (bool): By default, this method also calls $page->uncache(). To prevent call to $page->uncache(), set 'shallow' => true.
* @return bool True if page was uncached, false if it didn't need to be
*
*/
public function uncache($page, array $options = array()) {
if($page instanceof Page) {
$pageId = $page->id;
} else {
$pageId = is_int($page) ? $page : (int) "$page";
$page = isset($this->pageIdCache[$pageId]) ? $this->pageIdCache[$pageId] : null;
}
if(empty($options['shallow']) && $page) {
$page->uncache();
}
if(isset($this->pageIdCache[$pageId])) {
unset($this->pageIdCache[$pageId]);
return true;
} else {
return false;
}
}
/**
* Remove all pages from the cache
*
* @param Page $page Optional Page that initiated the uncacheAll
* @param array $options Additional options to modify behavior:
* - `shallow` (bool): By default, this method also calls $page->uncache(). To prevent call to $page->uncache(), set 'shallow' => true.
* @return int Number of pages uncached
*
*/
public function uncacheAll(Page $page = null, array $options = array()) {
if($page) {} // to ignore unused parameter inspection
$user = $this->wire()->user;
$language = $this->wire()->languages ? $user->language : null;
$cnt = 0;
$this->pages->sortfields(true); // reset
if($this->wire()->config->debug) {
$this->pages->debugLog('uncacheAll', 'pageIdCache=' . count($this->pageIdCache) . ', pageSelectorCache=' .
count($this->pageSelectorCache));
}
foreach($this->pageIdCache as $id => $page) {
if($id == $user->id || ($language && $language->id == $id)) continue;
if($page->numChildren) continue;
if(empty($options['shallow'])) $page->uncache();
unset($this->pageIdCache[$page->id]);
$cnt++;
}
$this->pageIdCache = array();
$this->pageSelectorCache = array();
$this->cacheGroups = array();
Page::$loadingStack = array();
Page::$instanceIDs = array();
return $cnt;
}
/**
* Uncache pages that were cached with given group name
*
* @param string $groupName
* @param array $options
* @return int
* @since 3.0.198
*
*/
public function uncacheGroup($groupName, array $options = array()) {
$qty = 0;
if(!isset($this->cacheGroups[$groupName])) return 0;
foreach($this->cacheGroups[$groupName] as $pageId) {
if(!isset($this->pageIdCache[$pageId])) continue;
$page = $this->pageIdCache[$pageId];
if($page && empty($options['shallow'])) $page->uncache();
unset($this->pageIdCache[$pageId]);
$qty++;
}
unset($this->cacheGroups[$groupName]);
return $qty;
}
/**
* Cache the given selector string and options with the given PageArray
*
* @param string $selector
* @param array $options
* @param PageArray $pages
* @return bool True if pages were cached, false if not
*
*/
public function selectorCache($selector, array $options, PageArray $pages) {
// get the string that will be used for caching
$selector = $this->getSelectorCache($selector, $options, true);
// optimization: don't cache single pages that have an unpublished status or higher
if(count($pages) && !empty($options['findOne']) && $pages->first()->status >= Page::statusUnpublished) return false;
$this->pageSelectorCache[$selector] = clone $pages;
return true;
}
/**
* Convert an options array to a string
*
* @param array $options
* @return string
*
*/
protected function optionsArrayToString(array $options) {
$str = '';
ksort($options);
foreach($options as $key => $value) {
if(is_array($value)) {
$value = $this->optionsArrayToString($value);
} else if(is_object($value)) {
if(method_exists($value, '__toString')) {
$value = (string) $value;
} else {
$value = wireClassName($value);
}
}
$str .= "[$key:$value]";
}
return $str;
}
/**
* Retrieve any cached page IDs for the given selector and options OR false if none found.
*
* You may specify a third param as TRUE, which will cause this to just return the selector string (with hashed options)
*
* @param string $selector
* @param array $options
* @param bool $returnSelector default false
* @return array|null|string|PageArray
*
*/
public function getSelectorCache($selector, $options, $returnSelector = false) {
if(count($options)) {
$optionsHash = $this->optionsArrayToString($options);
$selector .= "," . $optionsHash;
} else {
$selector .= ",";
}
// optimization to use consistent conventions for commonly interchanged names
$selector = str_replace(
array(
'path=/,',
'parent=/,'
),
array(
'id=1,',
'parent_id=1,'
),
$selector
);
// optimization to filter out common status checks for pages that won't be cached anyway
if(!empty($options['findOne'])) {
$selector = str_replace(
array(
'status<' . Page::statusUnpublished,
'status<' . Page::statusMax,
'start=0',
'limit=1',
',',
' '
),
'',
$selector
);
$selector = trim($selector, ", ");
}
// cache non-default languages separately
if($this->wire()->languages) {
$language = $this->wire()->user->language;
if($language && !$language->isDefault && $language->name != 'default') {
$selector .= ", _lang=$language->id"; // for caching purposes only, not recognized by PageFinder
}
}
if($returnSelector) return $selector;
if(isset($this->pageSelectorCache[$selector])) return $this->pageSelectorCache[$selector];
return null;
}
}