From 8a1f706be9df4c852d98b03b935dd204e3b866e8 Mon Sep 17 00:00:00 2001 From: Ryan Cramer Date: Fri, 12 Jan 2024 11:49:51 -0500 Subject: [PATCH] Add new $pages moveReady(), restoreReady(), and renameReady() hooks. Add option for callback hook on $pages->save(). Improvements to PagesTrash class. Update $pages class so restored() hook does not ever need to be called manually, and update ProcessPageEdit to reflect that. --- wire/core/Pages.php | 54 ++++++++++++++++++- wire/core/PagesEditor.php | 41 +++++++++++--- wire/core/PagesTrash.php | 44 ++++++++++----- .../ProcessPageEdit/ProcessPageEdit.module | 3 -- 4 files changed, 118 insertions(+), 24 deletions(-) diff --git a/wire/core/Pages.php b/wire/core/Pages.php index eeeb26de..d5e54764 100644 --- a/wire/core/Pages.php +++ b/wire/core/Pages.php @@ -8,7 +8,7 @@ * * This is the most used object in the ProcessWire API. * - * ProcessWire 3.x, Copyright 2022 by Ryan Cramer + * ProcessWire 3.x, Copyright 2024 by Ryan Cramer * https://processwire.com * * @link http://processwire.com/api/variables/pages/ Offical $pages Documentation @@ -62,10 +62,12 @@ * @method saveReady(Page $page) Hook called just before a page is saved. * @method saved(Page $page, array $changes = array(), $values = array()) Hook called after a page is successfully saved. * @method added(Page $page) Hook called when a new page has been added. + * @method moveReady(Page $page) Hook called when a page is about to be moved to another parent. * @method moved(Page $page) Hook called when a page has been moved from one parent to another. * @method templateChanged(Page $page) Hook called when a page template has been changed. * @method trashReady(Page $page) Hook called when a page is about to be moved to the trash. * @method trashed(Page $page) Hook called when a page has been moved to the trash. + * @method restoreReady(Page $page) Hook called when a page is about to be restored out of the trash. * @method restored(Page $page) Hook called when a page has been moved OUT of the trash. * @method deleteReady(Page $page, array $options) Hook called just before a page is deleted. * @method deleted(Page $page, array $options) Hook called after a page has been deleted. @@ -73,6 +75,7 @@ * @method deletedBranch(Page $page, array $options, $numDeleted) Hook called after branch of pages deleted, on initiating page only. * @method cloneReady(Page $page, Page $copy) Hook called just before a page is cloned. * @method cloned(Page $page, Page $copy) Hook called after a page has been successfully cloned. + * @method renameReady(Page $page) Hook called when a page is about to be renamed. * @method renamed(Page $page) Hook called after a page has been successfully renamed. * @method sorted(Page $page, $children = false, $total = 0) Hook called after $page has been sorted. * @method statusChangeReady(Page $page) Hook called when a page's status has changed and is about to be saved. @@ -2198,6 +2201,20 @@ class Pages extends Wire { $page->setQuietly('_added', true); } + /** + * Hook called when a page is about to be moved to another parent + * + * Note the previous parent is accessible in the `$page->parentPrevious` property. + * + * #pw-hooker + * + * @param Page $page Page that is about to be moved. + * @since 3.0.235 + * + */ + public function ___moveReady(Page $page) { + } + /** * Hook called when a page has been moved from one parent to another * @@ -2258,6 +2275,18 @@ class Pages extends Wire { public function ___trashed(Page $page) { $this->log("Trashed page", $page); } + + /** + * Hook called when a page is about to be moved OUT of the trash (restored) + * + * #pw-hooker + * + * @param Page $page Page that is about to be restored + * @since 3.0.235 + * + */ + public function ___restoreReady(Page $page) { + } /** * Hook called when a page has been moved OUT of the trash (restored) @@ -2387,6 +2416,29 @@ class Pages extends Wire { public function ___cloned(Page $page, Page $copy) { $this->log("Cloned page to $copy->path", $page); } + + /** + * Hook called when a page is about to be renamed i.e. had its name field change) + * + * The previous name can be accessed at `$page->namePrevious`. + * The new name can be accessed at `$page->name`. + * + * This hook is only called when a page's name changes. It is not called when + * a page is moved unless the name was changed at the same time. + * + * **Multi-language note:** + * Also note this hook may be called if a page's multi-language name changes. + * In those cases the language-specific name is stored in "name123" while the + * previous value is stored in "-name123" (where 123 is the language ID). + * + * #pw-hooker + * + * @param Page $page The $page that was renamed + * @since 3.0.235 + * + */ + public function ___renameReady(Page $page) { + } /** * Hook called when a page has been renamed (i.e. had its name field change) diff --git a/wire/core/PagesEditor.php b/wire/core/PagesEditor.php index c56c3a69..ca5ce589 100644 --- a/wire/core/PagesEditor.php +++ b/wire/core/PagesEditor.php @@ -5,7 +5,7 @@ * * Implements page manipulation methods of the $pages API variable * - * ProcessWire 3.x, Copyright 2021 by Ryan Cramer + * ProcessWire 3.x, Copyright 2024 by Ryan Cramer * https://processwire.com * */ @@ -424,6 +424,9 @@ class PagesEditor extends Wire { * - `ignoreFamily` (boolean): Bypass check of allowed family/parent settings when saving (default=false) * - `noHooks` (boolean): Prevent before/after save hooks from being called (default=false) * - `noFields` (boolean): Bypass saving of custom fields (default=false) + * - `caller` (string): Optional name of calling function (i.e. 'pages.trash'), for internal use (default='') 3.0.235+ + * - `callback` (string|callable): Hook method name from $pages or callable to trigger after save. + * It receives a single $page argument. For internal use. (default='') 3.0.235+ * @return bool True on success, false on failure * @throws WireException * @@ -438,6 +441,8 @@ class PagesEditor extends Wire { 'ignoreFamily' => false, 'noHooks' => false, 'noFields' => false, + 'caller' => '', + 'callback' => '', ); if(is_string($options)) $options = Selectors::keyValueStringToArray($options); @@ -445,6 +450,10 @@ class PagesEditor extends Wire { $user = $this->wire()->user; $languages = $this->wire()->languages; $language = null; + $parentPrevious = $page->parentPrevious; + $caller = $options['caller']; + $callback = $options['callback']; + $useHooks = empty($options['noHooks']); // if language support active, switch to default language so that saved fields and hooks don't need to be aware of language if($languages && $page->id != $user->id && "$user->language") { @@ -465,19 +474,35 @@ class PagesEditor extends Wire { $page->removeStatus(Page::statusUnpublished); } - if($page->parentPrevious && !$isNew) { - if($page->isTrash() && !$page->parentPrevious->isTrash()) { - $this->pages->trash($page, false); - } else if($page->parentPrevious->isTrash() && !$page->parent->isTrash()) { - $this->pages->restore($page, false); + if($parentPrevious && !$isNew) { + if($useHooks) $this->pages->moveReady($page); + if($caller !== 'pages.trash' && $caller !== 'pages.restore') { + if($page->isTrash() && !$parentPrevious->isTrash()) { + if($this->pages->trash($page, false)) $callback = 'trashed'; + } else if($parentPrevious->isTrash() && !$page->parent->isTrash()) { + if($this->pages->restore($page, false)) $callback = 'restored'; + } } } if($options['adjustName']) $this->pages->names()->checkNameConflicts($page); - if(!$this->savePageQuery($page, $options)) return false; - $result = $this->savePageFinish($page, $isNew, $options); + + if($page->namePrevious && !$isNew && $page->namePrevious != $page->name) { + if($useHooks) $this->pages->renameReady($page); + } + + $result = $this->savePageQuery($page, $options); + if($result) $result = $this->savePageFinish($page, $isNew, $options); if($language) $user->setLanguage($language); // restore language + if($result && !empty($callback) && $useHooks) { + if(is_string($callback) && ctype_alnum($callback)) { + $this->pages->$callback($page); // hook method name in $pages + } else if(is_callable($callback)) { + $callback($page); // user defined callback + } + } + return $result; } diff --git a/wire/core/PagesTrash.php b/wire/core/PagesTrash.php index a7859a4c..5df38be1 100644 --- a/wire/core/PagesTrash.php +++ b/wire/core/PagesTrash.php @@ -5,7 +5,7 @@ * * Implements page trash/restore/empty methods of the $pages API variable * - * ProcessWire 3.x, Copyright 2023 by Ryan Cramer + * ProcessWire 3.x, Copyright 2024 by Ryan Cramer * https://processwire.com * */ @@ -18,6 +18,14 @@ class PagesTrash extends Wire { */ protected $pages; + /** + * Last action, i.e. "restore:1234" + * + * @var int + * + */ + protected $lastAction = ''; + /** * Construct * @@ -41,17 +49,18 @@ class PagesTrash extends Wire { * */ public function trash(Page $page, $save = true) { - + if(!$this->pages->isDeleteable($page) || $page->template->noTrash) { throw new WireException("This page (id=$page->id) may not be placed in the trash"); } - $trash = $this->pages->get($this->config->trashPageID); + $trash = $this->pages->get($this->wire()->config->trashPageID); + if(!$trash->id) { throw new WireException("Unable to load trash page defined by config::trashPageID"); } - $this->pages->trashReady($page); + if($this->lastAction != "trash:$page") $this->pages->trashReady($page); $page->addStatus(Page::statusTrash); @@ -70,10 +79,10 @@ class PagesTrash extends Wire { // make the name unique when in trash, to avoid namespace collision and maintain parent restore info $name = $page->id; if($parentPrevious && $parentPrevious->id) { - $name .= "." . $parentPrevious->id; - $name .= "." . $page->sort; + $sort = $page->get('sortPrevious|sort'); + $name .= ".$parentPrevious->id.$sort"; } - $page->name = ($name . "_" . $page->name); + $page->name = ($name . '_' . $page->name); // do the same for other languages, if present $languages = $this->wire()->languages; @@ -87,9 +96,13 @@ class PagesTrash extends Wire { } } - if($save) $this->pages->save($page); + $this->lastAction = "trash:$page"; + + if($save) { + $this->pages->save($page, array('caller' => 'pages.trash', 'callback' => 'trashed')); + } + $this->pages->editor()->savePageStatus($page->id, Page::statusTrash, true, false); - if($save) $this->pages->trashed($page); $this->pages->debugLog('trash', $page, true); return true; @@ -107,24 +120,30 @@ class PagesTrash extends Wire { * */ public function restore(Page $page, $save = true) { + $info = $this->getRestoreInfo($page, true); if($info['restorable']) { // we detected original parent - if($save) $page->save(); + if($this->lastAction !== "restore:$page") $this->pages->restoreReady($page); } else if(!$page->parent->isTrash()) { // page has had new parent already set + if($this->lastAction !== "restore:$page") $this->pages->restoreReady($page); $page->removeStatus(Page::statusTrash); - if($save) $page->save(); $this->pages->editor()->savePageStatus($page->id, Page::statusTrash, true, true); - if($save) $this->pages->restored($page); $this->pages->debugLog('restore', $page, true); } else { // page is in trash and we cannot detect new parent return false; } + $this->lastAction = "restore:$page"; + + if($save) { + $this->pages->save($page, array('caller' => 'pages.restore', 'callback' => 'restored')); + } + return true; } @@ -221,6 +240,7 @@ class PagesTrash extends Wire { if($populateToPage) { $page->name = $name; + $page->removeStatus(Page::statusTrash); if($newParent) { $page->sort = $sort; $page->parent = $newParent; diff --git a/wire/modules/Process/ProcessPageEdit/ProcessPageEdit.module b/wire/modules/Process/ProcessPageEdit/ProcessPageEdit.module index 06604b63..cefc6b60 100644 --- a/wire/modules/Process/ProcessPageEdit/ProcessPageEdit.module +++ b/wire/modules/Process/ProcessPageEdit/ProcessPageEdit.module @@ -2157,20 +2157,17 @@ class ProcessPageEdit extends Process implements WirePageEditor, ConfigurableMod } } - $restored = false; if($input->post('restore_page') && $page->isTrash() && $page->restorable()) { if($formErrors) { $this->warning($this->_('Page cannot be restored while errors are present')); } else if($pages->restore($page, false)) { $message = sprintf($this->_('Restored Page: %s'), '{path}') . $numChanges; - $restored = true; } else { $this->warning($this->_('Error restoring page')); } } $pages->save($page, $options); - if($restored) $pages->restored($page); $message = str_replace('{path}', $page->path, $message); $this->message($message);