Jump to content

Hook runs twice

Recommended Posts


The only hook into 'trash' in a module runs twice, by itself, as it seems.

The following code outputs:

  • Session: Hooked 1 times on abcd
  • Session: Hooked 2 times on abcd

on the admin side when one page is deleted from it's delete tab.

And it's no matter 'addHookBefore', 'addHookaAfter' or 'addHook' method is used.


private $i = 0;

public function init() {

	$this->pages->addHookBefore('trash', $this, 'test');


public function test($event) {

	$page = $event->arguments('page');

	$this->message("Hooked {$this->i} times on {$page->title}");


Share this post

Link to post
Share on other sites


I could reproduce this in current dev 3.0.16. .   Pages::trash seems to be executed twice.
Is this still up to date for you? Maybe anyone else has a hint on what is wrong with it?


Share this post

Link to post
Share on other sites


I could reproduce this in current dev 3.0.16. .   Pages::trash seems to be executed twice.

Is this still up to date for you? Maybe anyone else has a hint on what is wrong with it?


I had found nothing about this.

Just updated my installation to dev 3.0.17 and still have two messages instead of one.

It is critical for me, because  I have write current amount of 'Pages' within the structure like 'Parent(1) -> Parent(1.1) + Parent(1.2) +Parent(1.3) , ... -> Pages' to the 'Parent(1)' each time it is changed, so I don't know any other way, than hook into 'save', 'trash', 'delete', 'move' methods.

Particularly, concerning the 'trash' or 'delete' methods, I have to hook into them before execution to find '$page->parent->parent', to which data will be written (as you see after that it is impossible), but with 'trash' method my 'beforeHook' saves '$page->parent->parent' to a module class property correctly, and then it is get overwritten immediately, so it looks like 'addHookBefore' adds hook before and after.

Share this post

Link to post
Share on other sites

Hi thanks for that hint. I can only try it out later.

For feniks, this might not help as I understood, as he needs the page before it is trashed, but maybe it could work if you change your hook method (test above) with a check if the page is trashed? If there are really two different calls like Soma suggested, it should be one before actually trashed and in the second call of the modified page before save(?).
Maybe you get to try it out before me:

$page = $event->arguments('page');
if($page->isTrash()) return; //should only happen on the second call(?)

However I did not fully understand your use-case fully. Depending on what you want, it might be much easier to just count the children where you need them in Parent(1) or if you know the parent(s) you want the count for, you could also do it easier I guess:

// e.g. just:
// in the template, or outside e.g:

Hope this helps.

Share this post

Link to post
Share on other sites

I ran into this same issue with triggers 'trash' and 'restore'. I was trying to do stuff before page is trashed/restored. I saw weird behavior but it took a good while until I understood triggers were being triggered multiple times per request.

My use case: I need to communicate with REST API when page changes. Running triggers multiple times is an issue as it leads to unexpected results. I had to go around the issue with class properties, but I don't think that should be the case. I assumed addHookBefore('Pages::restore'...) would be run once.

Share this post

Link to post
Share on other sites

I know it's a long time since the last post, but this still seems to be an issue in 3.0.123. It's easily avoided by setting a flag to prevent repeats (see below), but is it a bug or a side-effect of some intended behaviour?

wire()->addHookBefore('Pages::trash', function ($event) {
    $p = $event->arguments(0);
    if ($this->skip != $p->id) {
        $this->skip = $p->id;


Share this post

Link to post
Share on other sites

For anyone wanting to trace how it is that Pages::trash is called twice...

Pages::trash (first call) calls PagesTrash::trash, and when the "save" argument is true (as it is when trashing via the admin) then Pages:save is called, which calls PagesEditor::save.

And if that saved page is in the trash then Pages::trash is called (second call) with the "save" argument false.

As to whether this second Pages::trash call is necessary and correct, I don't know.

Best thing is to hook Pages::trashed as suggested above - this method only fires if the page is successfully trashed, which is probably what is wanted in most cases.

  • Like 3

Share this post

Link to post
Share on other sites
1 hour ago, Robin S said:

(second call) with the "save" argument false.

Aha! So a better way to avoid the hook being called twice would be to test for the 'save' argument.

1 hour ago, Robin S said:

Best thing is to hook Pages::trashed as suggested above

In my case I can't use the trashed hook as I need to access the parent of the page being trashed, which is too late if it has been trashed.

Share this post

Link to post
Share on other sites
26 minutes ago, MarkE said:

test for the 'save' argument.

Except that, in the 'before' hook, the test is is_null() because the default 'true' value has not been set. The second time, the argument will be 'false', which is not null 😉 

Share this post

Link to post
Share on other sites
48 minutes ago, MarkE said:

In my case I can't use the trashed hook as I need to access the parent of the page being trashed, which is too late if it has been trashed.

Yeah, makes me wonder if it would have been better if the Pages::trashed method was called immediately before saving the trashed page rather than after. Maybe Ryan has a good reason for doing it that way.

I still think it's better to hook after Pages::trashed if you want to know for sure which pages are trashed because when hooking Pages::trash there are still instances where the trashing can fail. For example, there might be another Pages::trash hook in a module that deliberately prevents trashing of particular pages.

If you hook Pages::trashed you can parse the parent page ID (and some other info) from the name of the page in the trash:

$pages->addHookAfter('trashed', function(HookEvent $event) {
	$page = $event->arguments(0);
	$pages = $event->object;
	/* @var PagesTrash $trasher */
	$trasher = $pages->trasher();
	$name_info = $trasher->parseTrashPageName($page->name);
	if(!empty($name_info['parent_id'])) {
		$parent = $pages($name_info['parent_id']);
		// ...


  • Like 1

Share this post

Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

  • Recently Browsing   0 members

    No registered users viewing this page.

  • Similar Content

    • By celfred
      I'm struggling with hooks to try and do this : adding user page to a page field when visiting a particular page on my website. In fact, I want to have a list of connected users on a particular page (called meeting-hall).
      I've tried all kinds of things (which I don't really understand, sorry) but the latest is this :

        $wire->addHookBefore('Page::render', function($e) {     $page = $e->arguments(0);     if ($page->is("name=meeting-hall")) {       $page->connected->add($user); // connected is a Page field       $page->save();     } else {       $meeting = wire("pages")->get("name=meeting-hall");       $meeting->connected->remove($user);       $meeting->save();     }   }); I put this in my _init.php file but nothing works...
      The best I managed was updating my 'connected' field when the user loads the 'meeting hall' page by having this on the page template :

          if ($user->isLoggedin()) {       $page->connected->add($user);       $page->of(false);       $page->save();       $page->of(true);     } But I wanted to remove the user when he or she leaves the page... hence my thought about using hooks...
      If you can give me a hint, I'd greatly appreciate !
    • By Gadgetto
      Hi there,
      I'd like to prevent duplicate values in a specific page field (FieldtypeText) across all pages. The hook should also prevent saving of the page if a duplicate value is detected. Therefore I created two hook methods but I can't get it to work properly.
      Here are both hooks placed in init() method of an autoloader module:
      The first hook is to initially preset the field with the page id. The field value can be customized by the admin before saving.
      The second hook should check if the value is unique.
      If I create a new page and try to save, the WireException is already triggered in first step of page creation, where you enter the page title: SKU [] is already in use
      If I check the value of $sku (with Tracy) it's always empty!
      Any idea what could be wrong?
      EDIT: if I remove the second hook, the field value is correctly preset with the page id!
      $this->addHookAfter('Pages::added', $this, 'presetProductFields', ['priority' => 99]); $this->addHookAfter('Pages::saveReady', $this, 'checkSKUUnique', ['priority' => 101]); And this are the hook methods:
      public function presetProductFields(HookEvent $event) { $snipwire = $this->wire('snipwire'); if (!$snipwire) return; $page = $event->arguments(0); if ($snipwire->isProductTemplate($page->template)) { if ($page->hasfield('snipcart_item_id')) $page->setAndSave('snipcart_item_id', $page->id); } } /** * Check if the SKU value is unique across all product pages. * (Method triggered after Pages saveReady -> just before page is saved) * * @throws WireException * */ public function checkSKUUnique(HookEvent $event) { $snipwire = $this->wire('snipwire'); if (!$snipwire) return; $page = $event->arguments(0); if ($snipwire->isProductTemplate($page->template)) { $field = $page->getField('snipcart_item_id'); $sku = $page->snipcart_item_id; // SKU field value bd($sku); if ($page->isChanged('snipcart_item_id')) { $exists = $this->wire('pages')->get("snipcart_item_id=$sku"); if ($exists->id) { // value is not unique! $error = $this->_('SKU must be unique'); $exception = sprintf( $this->_('SKU [%s] is already in use'), $sku ); $inputfield = $page->getInputfield($field); $inputfield->error($error); throw new WireException($exception); // Prevent saving of non-unique value! } } } }  
    • By t0b1
      Hello there, and thanks for ProcessWire!
      I'm getting to know ProcessWire while doing my first project using it and I really like it so far.
      The challenge I'm facing right now is the following:
      I have a One-Pager using fullpage.js, realized as a single PW-Page containing a Repeater Field where each Repeater Item is one Section.
      Some Sections are supposed to have a little menu at the Top which references/links to different Sections of the Website, so I wanted to use a Checkbox Field "Top Menu" to decide if a Section gets a Menu and a Page Reference Field to choose the different Sections (Repeater Items) it should contain.
      I've already accomplished this by pasting the following code into /site/ready.php:
      $wire->addHookAfter('InputfieldPage::getSelectablePages', function($event) { if($event->object->hasField == 'top_menu_entries') { $page = $event->arguments('page'); if($page instanceof RepeaterPage) $page = $page->getForPage(); $event->return = $page->; } });  
      The only problem that still remains is that when I select the Entry of the Repeater Item itself it doesnt save the selection, meaning after I saved it's unselected again.
      On some Sections I do want a Menu-Entry for the Section itself though (which would be styled differently and not link anywhere) for Continuity-Reasons, any ideas on how to achieve that?
    • By Gadgetto
      Hi guys (didn't see any gals here),
      I have a module which has some custom helper classes. One of these classes has methods which are hookable. The class is derived from WireData. An additional complication is that this class has its own namespace. What would be the correct way to provide the hookable methods via API?
      Here is a very simplified code sample of the class:
      <?php namespace SnipWire\Services; use ProcessWire\WireData; use ProcessWire\WireException; class Webhooks extends WireData { ... public function ___handleOrderCompleted() { if ($this->debug) $this->wire('log')->save( self::snipWireWebhooksLogName, '[DEBUG] Webhooks request: handleOrderCompleted' ); $this->responseStatus = 202; // Accepted } public function ___handleOrderStatusChanged() { if ($this->debug) $this->wire('log')->save( self::snipWireWebhooksLogName, '[DEBUG] Webhooks request: handleOrderStatusChanged' ); $this->responseStatus = 202; // Accepted } ... } The class itself is invoked/used by a hook from within the main module:
      Depending of the event, one of the methods of the Webhooks class above is triggered. A developer should now be able to use one of the ___handle* methods to do further things.
      /** * Check for webohook request and process them. * (Method triggered before ProcessPageView::execute) * */ public function checkWebhookRequest(HookEvent $event) { if ($webhooksEndpoint = $this->get('webhooks_endpoint')) { if ($this->sanitizer->url($this->input->url) == $webhooksEndpoint) { /** @var Webhooks $webhooks Custom ProcessWire API variable */ $this->wire('webhooks', new Webhooks()); $this->wire('webhooks')->process(); $event->replace = true; // @note: Tracy Debug won't work from here on as normal page rendering is omitted! } } } Should I provide a custom API variable e.g. $webhooks? Or how is the best way to do this?
    • By Flashmaster82
      Hi, can you guys help a beginner with a problem..
      On my template (profile_page) i have a dropdown (page reference) where i can choose a sports team (team_page) that is related to that profile which is also its parent. Then on my Competition1 page (competition_page) I have page reference field (profiles) a dropdown that i want to display only profiles that has choose a specific sports team (template=team_page) the page parent to be specific.
      Sports_team1 (team_page)
           Profile1 (profile_page)
           Profile2 (profile_page)
           Profile3 (profile_page)
           Competition1 (competition_page)
      <?php $wire->addHookAfter('InputfieldPage::getSelectablePages', function($event) { if($event->object->hasField == 'profiles') { $relative = $page->parent->name; $event->return = $event->pages->find("template=profile_page, sports_team=$relative"); } }); ?> This returns with no results in the dropdown. If i remove sports_team=$relative then it displays all profiles that have profile_page as template, so it works almost. But i will have more sports teams so this is just an example. I only want to display the profiles that has choosen the parent team on there profile page in admin not front end.
      I hope i was able to explain it so you guys can understand a little bit. Need some help please! /Thanks
  • Create New...