From 047ffb1c204deebf7f8e7646433515bf1b98a0d7 Mon Sep 17 00:00:00 2001 From: Ryan Cramer Date: Thu, 14 Apr 2022 08:11:19 -0400 Subject: [PATCH] Add support for runtime page cache groups. This enables pages to be cached as a group, or more importantly, uncached as a group. It was added primarily to add efficiency to $pages->findMany(), so that it can cache supporting pages (like parents of pages returned by findMany). Previously, it would have to load a fresh copy of each supporting page used by findMany(), for every returned page, since findMany() used no in-memory caching, otherwise you could run out of memory on large results. So if you iterated a $pages->findMany() result and output the URL of each page (which loads parents), then it would have to reload all those parents for each iteration. Now it can cache them for each chunk of 250 pages, offering a significant potential performance improvement in many cases. --- wire/core/Page.php | 8 ++-- wire/core/PageArrayIterator.php | 22 ++++++++--- wire/core/PagesLoader.php | 33 ++++++++++------ wire/core/PagesLoaderCache.php | 68 ++++++++++++++++++++++++++++++++- 4 files changed, 108 insertions(+), 23 deletions(-) diff --git a/wire/core/Page.php b/wire/core/Page.php index 072008cf..b334b73c 100644 --- a/wire/core/Page.php +++ b/wire/core/Page.php @@ -81,7 +81,7 @@ * @property string $editUrl URL that this page can be edited at. #pw-group-urls * @property string $editURL Alias of $editUrl. #pw-internal * @property PageRender $render May be used for field markup rendering like $page->render->title. #pw-advanced - * @property bool $loaderCache Whether or not pages loaded as a result of this one may be cached by PagesLoaderCache. #pw-internal + * @property bool|string $loaderCache Whether or not pages loaded as a result of this one may be cached by PagesLoaderCache. #pw-internal * @property PageArray $references Return pages that are referencing the given one by way of Page references. #pw-group-traversal * @property int $numReferences Total number of pages referencing this page with Page reference fields. #pw-group-traversal * @property int $hasReferences Number of visible pages (to current user) referencing this page with page reference fields. #pw-group-traversal @@ -462,7 +462,7 @@ class Page extends WireData implements \Countable, WireMatchable { /** * Whether or not pages loaded by this one are allowed to be cached by PagesLoaderCache class * - * @var bool + * @var bool|string Bool for yes/no or string for yes w/group name where page cached/cleared with others having same group name. * */ protected $loaderCache = true; @@ -764,7 +764,7 @@ class Page extends WireData implements \Countable, WireMatchable { self::$instanceIDs[$value] = $this->settings['id']; break; case 'loaderCache': - $this->loaderCache = (bool) $value; + $this->loaderCache = is_bool($value) || ctype_digit("$value") ? (bool) $value : (string) $value; break; default: if(isset(PageProperties::$languageProperties[$key])) { @@ -4451,7 +4451,7 @@ class Page extends WireData implements \Countable, WireMatchable { if(!is_int($this->lazyLoad) || $this->lazyLoad < 1) return false; $this->lazyLoad = true; $page = $this->wire()->pages->getById($this->id, array( - 'cache' => false, + 'cache' => (is_string($this->loaderCache) ? $this->loaderCache : false), 'getOne' => true, 'page' => $this // This. Just This. )); diff --git a/wire/core/PageArrayIterator.php b/wire/core/PageArrayIterator.php index c20f3e56..52a09a19 100644 --- a/wire/core/PageArrayIterator.php +++ b/wire/core/PageArrayIterator.php @@ -74,6 +74,12 @@ class PageArrayIterator extends Wire implements \Iterator { * */ protected $chunkSize = 250; + + /** + * @var string + * + */ + protected $cacheGroup = ''; /** * Construct @@ -92,24 +98,26 @@ class PageArrayIterator extends Wire implements \Iterator { * */ protected function loadChunk() { - $this->chunkSize = (int) $this->wire('config')->lazyPageChunkSize; + $this->chunkSize = (int) $this->wire()->config->lazyPageChunkSize; $this->pagesPosition = 0; $start = $this->currentChunk++ * $this->chunkSize; + $pages = $this->wire()->pages; + + if($this->cacheGroup) { + $this->wire()->pages->cacher()->uncacheGroup($this->cacheGroup); + } // If the starting position exceeds the amount of placeholder objects, we just issue an empty // PageArray, which causes the loop to stop (because valid() will return false) if(!isset($this->lazypages[$start])) { - $this->pages = $this->wire('pages')->newPageArray(); + $this->pages = $pages->newPageArray(); } else { // Check if the user gave options for the loading $options = isset($this->options['loadOptions']) ? $this->options['loadOptions'] : array(); - // Always disable the cache - $options['cache'] = false; - // Here we retrieve a chunk of Page objects and loop over them to retrieve the IDs of the Pages. $lazypages = array_slice($this->lazypages, $start, $this->chunkSize); $ids = array(); @@ -120,8 +128,10 @@ class PageArrayIterator extends Wire implements \Iterator { // of real Page-objects from Pages::getById() $ids[] = $page->id; } + + $this->cacheGroup = 'lazy' . md5(implode(',', $ids)); + $options['cache'] = $this->cacheGroup; - $pages = $this->wire('pages'); $debug = $pages->debug(); if($debug) $pages->debug(false); $this->pages = $pages->getById($ids, $options); diff --git a/wire/core/PagesLoader.php b/wire/core/PagesLoader.php index b4cd2b98..bd40e7b8 100644 --- a/wire/core/PagesLoader.php +++ b/wire/core/PagesLoader.php @@ -5,7 +5,7 @@ * * Implements page finding/loading methods of the $pages API variable * - * ProcessWire 3.x, Copyright 2016 by Ryan Cramer + * ProcessWire 3.x, Copyright 2022 by Ryan Cramer * https://processwire.com * */ @@ -319,8 +319,14 @@ class PagesLoader extends Wire { $debug = $this->debug && !$lazy; $allowShortcuts = $loadPages && !$lazy && (!$findIDs || $findIDs === 4); $joinFields = isset($options['joinFields']) ? $options['joinFields'] : array(); - $cachePages = isset($options['cache']) ? (bool) $options['cache'] : true; - if(!$cachePages && !isset($loadOptions['cache'])) $loadOptions['cache'] = false; + $cachePages = isset($options['cache']) ? $options['cache'] : true; + + if($cachePages) { + $options['cache'] = $cachePages; + $loadOptions['cache'] = $cachePages; + } else if(!isset($loadOptions['cache'])) { + $loadOptions['cache'] = false; + } if($allowShortcuts) { $pages = $this->findShortcut($selector, $options, $loadOptions); @@ -940,10 +946,8 @@ class PagesLoader extends Wire { 'caller' => '', ); - /** @var Templates $templates */ - $templates = $this->wire('templates'); - /** @var WireDatabasePDO $database */ - $database = $this->wire('database'); + $templates = $this->wire()->templates; + $database = $this->wire()->database; $idsByTemplate = array(); $loading = $this->loading; @@ -952,6 +956,7 @@ class PagesLoader extends Wire { $options = array_merge($options, $template); $template = $options['template']; $parent_id = $options['parent_id']; + if("$options[cache]" === "1") $options['cache'] = true; } else if(!is_null($template) && !$template instanceof Template) { throw new WireException('getById argument 2 must be Template or $options array'); } @@ -1107,7 +1112,7 @@ class PagesLoader extends Wire { if($template) { $fields = $template->fieldgroup; } else { - $fields = $this->wire('fields'); + $fields = $this->wire()->fields; } /** @var DatabaseQuerySelect $query */ @@ -1196,22 +1201,26 @@ class PagesLoader extends Wire { )); } unset($row['templates_id'], $row['parent_id']); + $page->loaderCache = $options['cache']; foreach($row as $key => $value) $page->set($key, $value); - if($options['cache'] === false) $page->loaderCache = false; $page->instanceID = ++self::$pageInstanceID; $page->setIsLoaded(true); $page->setIsNew(false); $page->resetTrackChanges(true); $page->setOutputFormatting($this->outputFormatting); $loaded[$page->id] = $page; - if($options['cache']) $this->pages->cache($page); + if($options['cache'] === true) { + $this->pages->cache($page); + } else if($options['cache']) { + $this->pages->cacher()->cacheGroup($page, $options['cache']); + } $this->totalPagesLoaded++; } } catch(\Exception $e) { $error = $e->getMessage() . " [pageClass=$class, template=$template]"; $user = $this->wire('user'); if($user && $user->isSuperuser()) $this->error($error); - $this->wire('log')->error($error); + $this->wire()->log->error($error); $this->trackException($e, false); } @@ -1232,7 +1241,7 @@ class PagesLoader extends Wire { // debug mode only if($this->debug) { - $page = $this->wire('page'); + $page = $this->wire()->page; if($page && $page->template == 'admin') { if(empty($options['caller'])) { $_template = is_null($template) ? '' : ", $template"; diff --git a/wire/core/PagesLoaderCache.php b/wire/core/PagesLoaderCache.php index b08f4de0..299eb926 100644 --- a/wire/core/PagesLoaderCache.php +++ b/wire/core/PagesLoaderCache.php @@ -5,7 +5,7 @@ * * Implements page caching of loaded pages and PageArrays for $pages API variable * - * ProcessWire 3.x, Copyright 2016 by Ryan Cramer + * ProcessWire 3.x, Copyright 2022 by Ryan Cramer * https://processwire.com * */ @@ -24,6 +24,14 @@ class PagesLoaderCache extends Wire { */ protected $pageSelectorCache = array(); + /** + * [ 'cache group name' => [ page IDs ] ] + * + * @var array + * + */ + protected $cacheGroups = array(); + /** * @var Pages * @@ -39,6 +47,25 @@ class PagesLoaderCache extends Wire { public function __construct(Pages $pages) { $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. @@ -73,6 +100,21 @@ class PagesLoaderCache extends Wire { 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. * @@ -135,6 +177,7 @@ class PagesLoaderCache extends Wire { $this->pageIdCache = array(); $this->pageSelectorCache = array(); + $this->cacheGroups = array(); Page::$loadingStack = array(); Page::$instanceIDs = array(); @@ -142,6 +185,29 @@ class PagesLoaderCache extends Wire { 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 *