Jump to content
Gadgetto

Hooks inside hooks possible?

Recommended Posts

Posted (edited)

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?

Edited by Gadgetto
Changed post title

Share this post


Link to post
Share on other sites

Why just not implement a module? 

Share this post


Link to post
Share on other sites
On 3/11/2020 at 8:24 AM, Zeka said:

Why just not implement a module? 

What would be the benefit of making a custom helper class a module?

Share this post


Link to post
Share on other sites

Hey @Gadgetto did you find a solution for your problem?

On 3/10/2020 at 6:21 PM, Gadgetto said:

$this->wire('webhooks')->process();

What does that do?

I'm still not sure I understand the problem completely and the best solution in more complex setup always depends on the situation, but maybe you could make a proxy method in your main module that is hookable instead of making all methods in the helper module hookable? Some untested pseudo-code:

// main module SnipWire
public function webhooks(...$args) {
	$helper = $this->get('webhooks_endpoint')
	if($args['foo']) return $helper->foo(...$args); // returns "foo"
	if($args['bar']) return $helper->bar(...$args); // returns "bar"
	...
}


// site/ready.php
$wire->addHookAfter("SnipWire::webhooks", function($event) {
	$name = $event->arguments('name');
	if($name != "foo") return;
	$event->return .= " hooked";
}

// then the foo webhook should return "foo hooked" instead of "foo"

 

Share this post


Link to post
Share on other sites

Hi @bernhard, thanks for jumping in!

14 hours ago, bernhard said:
On 3/10/2020 at 6:21 PM, Gadgetto said:

$this->wire('webhooks')->process();

What does that do?

The Webhooks class handles Snipcart's web hook events and provides hookable methods to attach custom code to those events. eg. orderCompleted, customerUpdated, ad so on ... A Snipcart webhook event is a POST request which is sent to your site by Snipcart. Each POST request holds json encoded data corresponding to the specific situation. e.g. A orderCompleted event POSTs the full order array to your site.

The Webhooks class is available as custom API variable $webhooks.

This code in SnipWire main module takes over POST request from Snipcart:

    /**
     * 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) {
                $this->wire('webhooks')->process();
                $event->replace = true;
                // @note: Tracy Debug won't work from here on as normal page rendering is omitted!
            }
        }
    }

This line...

$this->wire('webhooks')->process();

... starts the process which handles a Snipcart POST request:

https://github.com/gadgetto/SnipWire/blob/cc9922fbe059b3961d3ee63be253e34d4cf96bc1/services/Webhooks.php#L108

Within this processing the specific hookable method within the Webhooks class is called. I thought it should be easy to provide those methods a hookable. But it seems this isn't so easy with custom classes.

For example - this doesn't work:

$webhooks->addHook('handleOrderCompleted', function($event) {
	wire('log')->save('webhookstest', 'hook works');
}); 

and also not this:

$webhooks->addHook('Webhooks::handleOrderCompleted', function($event) {
	wire('log')->save('webhookstest', 'hook works');
}); 

or this:

$wire->addHook('Webhooks::handleOrderCompleted', function($event) {
	wire('log')->save('webhookstest', 'hook works');
}); 

 

Share this post


Link to post
Share on other sites

Just tried and it works just as you expected it to work:

4DH1qQh.png

That's why I was asking what the process() does exactly (code!).

<?php namespace RockSearch;
use ProcessWire\Wire;
class Matcher extends Wire {
  public function ___foo() {
    return 'foo';
  }

 

Share this post


Link to post
Share on other sites
12 minutes ago, bernhard said:

Just tried and it works just as you expected it to work:

This is strange! Why doesn't it work in my environment? None of the hooks I posted above is triggered.

Share this post


Link to post
Share on other sites
class Webhooks extends WireData {

I guess WireData does not support hooks whereas Wire does.

Share this post


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

class Webhooks extends WireData {

I guess WireData does not support hooks whereas Wire does.

From PW docs:

Quote

To make a class capable of having hookable methods, simply extend one of ProcessWire's classes such as Wire or WireData as your base

So it should work. 😕

Share this post


Link to post
Share on other sites

Of course it does, as WireData extends Wire, sorry for that 🤦‍♂️

Does it work if you call $this->handleOrderCompleted() directly? Maybe the dynamic method call is the problem (wouldn't know why, though). Maybe it is working, but you just think it is not. How do you check if it is working? 😉 

Share this post


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

Of course it does, as WireData extends Wire, sorry for that 🤦‍♂️

Does it work if you call $this->handleOrderCompleted() directly? Maybe the dynamic method call is the problem (wouldn't know why, though). Maybe it is working, but you just think it is not. How do you check if it is working? 😉 

I have this part in my ready.php

$webhooks->addHook('handleOrderCompleted', function($event) {
	wire('log')->save('webhookstest', 'hook works');
}); 

But the log entry isn't written.

Share this post


Link to post
Share on other sites

OK, If I call this manually in Tracy Console:

$webhooks->handleOrderCompleted();

The hook is triggered!

Shouldn't a dynamic method call trigger a hook event?

$this->{$methodName}();

 

Share this post


Link to post
Share on other sites
34 minutes ago, Gadgetto said:

I have this part in my ready.php


$webhooks->addHook('handleOrderCompleted', function($event) {
	wire('log')->save('webhookstest', 'hook works');
}); 
13 minutes ago, Gadgetto said:

Could be a bug in PW...

No, it is a bug in your hook 😉  You are adding a new method with your hook, not hooking into that method! You are simply missing the addHookAFTER

$webhooks->addHookAfter('handleOrderCompleted', function($event) {
	wire('log')->save('webhookstest', 'hook works');
}); 

 

Share this post


Link to post
Share on other sites
44 minutes ago, bernhard said:

No, it is a bug in your hook 😉  You are adding a new method with your hook, not hooking into that method! You are simply missing the addHookAFTER


$webhooks->addHookAfter('handleOrderCompleted', function($event) {
	wire('log')->save('webhookstest', 'hook works');
}); 

 

I changed to addHookAfter but still doesn't work. (had this tried before without success) 🤔

Share this post


Link to post
Share on other sites

Are you sure? Maybe I don't understand this correctly... It seems a new method is only added if the specified method name doesn't exist.

From the docs:

---

You can add new methods to any ProcessWire class in the same way that you would hook an existing method. The only difference is that the method doesn't already exist, so you are adding it. Also, because there is no distinction of "before" or "after" for something that doesn't already exist, you can attach the method using addHook(); rather than addHookBefore(); or addHookAfter() … though technically it doesn't really matter what you use here. Lets say that you wanted to add a new method to all Page instances that outputs a relative time string of when the page was last modified (for example, "45 minutes ago" or "3 days ago", etc.):

public function init() {
  $this->addHook('Page::lastModified', $this, 'lastModified');
}

public function lastModified($event) {
  $page = $event->object;
  $event->return = wireRelativeTimeStr($page->modified);
}

Hooking both before and after
If you want your hook method to run both before and after a particular event occurs, you can specify that in an $options array to the addHook() call, like this:

$wire->addHook('Class::method', function(HookEvent $e) {
  // ...
}, [ 'before' => true, 'after' => true ]); 

Share this post


Link to post
Share on other sites

You are right, I was wrong, sorry again 🙂 

gdLvDsv.png

Well, so I'd try changing the dynamic method call to a static one and see if the hook get's triggered. If yes, you know where the problem lies...

  • Like 1

Share this post


Link to post
Share on other sites

Changed to a static method call - still not working... 🤯

I'm just thinking it could be a problem that the process() method (which calls the hook method dynamically) itself is also triggered by a hook?

public function init() {
    ...
    $this->addHookBefore('ProcessPageView::execute', $this, 'checkWebhookRequest');
    ...
}

/**
 * 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) {
            $this->wire('webhooks')->process();
            $event->replace = true;
            // @note: Tracy Debug won't work from here on as normal page rendering is omitted!
        }
    }
}

 

Share this post


Link to post
Share on other sites

Yes, that sounds reasonable!

Share this post


Link to post
Share on other sites
18 hours ago, bernhard said:

Yes, that sounds reasonable!

If this is the real reason, I don't know how to solve this, as process() needs to be called by a hook. 😵

Share this post


Link to post
Share on other sites
Quote

it could be a problem that the process() method (which calls the hook method dynamically) itself is also triggered by a hook

Could this be a bug in PW or is it intentional? Should I file an issue?

Any ideas from the other pros here? I'm really stuck with this. 😞 

Share this post


Link to post
Share on other sites
Just now, Gadgetto said:

Could this be a bug in PW or is it intentional? Should I file an issue?

I'd first confirm that this is really the problem by putting together a test-setup that makes the issue reproducable. It is definitely possible to add hooks inside hooks, but timing is very important! https://processwire.com/blog/posts/new-ajax-driven-inputs-conditional-hooks-template-family-settings-and-more/#new-conditional-hooks

So I think it's more likely a problem in your code than a bug in PW... 

Share this post


Link to post
Share on other sites
59 minutes ago, bernhard said:

I'd first confirm that this is really the problem by putting together a test-setup that makes the issue reproducable. It is definitely possible to add hooks inside hooks, but timing is very important! https://processwire.com/blog/posts/new-ajax-driven-inputs-conditional-hooks-template-family-settings-and-more/#new-conditional-hooks

So I think it's more likely a problem in your code than a bug in PW... 

I'll try to write a small test case. Thanks again @bernhard!

Share this post


Link to post
Share on other sites

I have written a small test module which simulates the required situation. Unfortunately the hook mentioned above does not work here either.

I think I'll file an issue on GitHub.

Here is a ZIP with the module files:

HookTest.zip

Share this post


Link to post
Share on other sites

That's strange. I've just realized that I did exactly the same in one of my modules and prepared methods to be hookable (just in case I'd need it one day) and those hooks do also not get triggered. The strange thing is that this little module works just as expected:

<?php namespace ProcessWire;
class RockHook extends WireData implements Module {

  public static function getModuleInfo() {
    return [
      'title' => 'RockHook',
      'version' => '0.0.1',
      'summary' => 'RockHook',
      'autoload' => true,
      'icon' => 'bolt',
      'requires' => [],
      'installs' => [],
    ];
  }

  public function init() {
    $this->addHookAfter("ProcessPageView::execute", function(HookEvent $event) {
      $this->hello("trigger hello() in execute() hook");
    });
    $this->addHookAfter('hello', function(HookEvent $event) {
      bd('hello hooked in RockHook::init()');
    });
  }

  public function ___hello($where) {
    bd('hello!', $where);
  }
}
// in site/ready.php
$this->addHookAfter('RockHook::hello', function(HookEvent $event) {
  bd('hello hooked in /site/ready.php');
});

7ZOmZDg.png

Edit: Attaching the hook like this does also work:

<?php namespace ProcessWire;
class RockHook extends WireData implements Module {

  public static function getModuleInfo() {
    return [
      'title' => 'RockHook',
      'version' => '0.0.1',
      'summary' => 'RockHook',
      'autoload' => true,
      'singular' => false,
      'icon' => 'bolt',
      'requires' => [],
      'installs' => [],
    ];
  }

  public function init() {
    $this->addHookAfter("ProcessPageView::execute", $this, "handleWebhook");
    $this->addHookAfter('hello', function(HookEvent $event) {
      bd('hello hooked in RockHook::init()');
    });
  }

  public function handleWebhook($event) {
    $this->hello("trigger hello() in execute() hook");
  }

  public function ___hello($where) {
    bd('hello!', $where);
  }
}

 

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 Andi
      Continuing my journey into PW hooks, I'm trying to find a way to retrieve all images from a page that explicitly *do not* have a certain tag (or tags) attached to them.
      Found this post from 2015
      But I'm wondering if there's a more elegant way to go about this.
      Let's say I have a multi-image field called "images_header" and instead of
      $page->images_header->findTag('mytag'); I would like to do this:
      $page->images_header->excludeTag('mytag'); So I'd be able to do
      // find images that don't have the tag "mytag" $images = $page->images_header->excludeTag('mytag'); // check if there's any images if (count($images)>0) { // do something.. } Would this be possible by hooking into Pagefiles somehow?
      There's this bit in /wire/core/Pagefiles.php Line 626 that I'd basically just need to reverse (or at least in my mind 😄 )
      public function findTag($tag) { $items = $this->makeNew(); foreach($this as $pagefile) { if($pagefile->hasTag($tag)) $items->add($pagefile); } return $items; } Any ideas on how this could be done in a graceful manner?
      Thanks in advance!
    • By Andi
      Getting a little deeper into the ProcessWire state-of-mind here. I seriously think I wouldn't have come back to webdev if it wasn't for this wonderful little gem of a CMS.
      I have an "Options" field added to all users on a site. If the user has anything other then "default" selected, I would like to show a permanent message in the admin like the one in the screenshot, only so that the user can't close it. As a friendly reminder that he changed that option from default to something crazy 🙂
      I've read up on how to send messages to users, but where would I hook into to make this show up all the time in the backend?
      https://processwire.com/api/ref/wire/message/
      Thanks in advance!

    • By celfred
      Hello,
       
      Here's what I'm trying to achieve : I have a textarea field that is frontend editable on any page I want to. If a user with a specific role updates it, I want to tick a checkbox field on the parent page where the textarea resides. So I tried to hook as follows in my ready.php file :

        $this->addHookBefore('Fieldtype::savePageField', function(HookEvent $event) {       $page = $event->argument[0];       $field = $event->argument[1];       if($this->user->hasRole('teacher')) {      $page->alertBox = 1;       } // DEBUG HERE (bd($page),  l($field)... ????   }); Of course (!) it doesn't work (yet !) but the thing is I have no idea how to debug this since my bd() never triggers. I've tried the 'Event interceptor' of Tracy debugger which helped me setting up my hook. I guess my $page and $field are correct... but how could I go any further ?
      The road is long to become a dev (when you're not one 🙂 )...
      For further information in case someone wonders : I'd like to set up a textarea that is modified by a user (having a 'player' role, ie 'student'). When front-end modified, I wish it would automatically alert me (the teacher) by ticking a box. So when I log in with my teacher role, I get a list of all textareas I have to read over. When I read/correct/update them (front-end), I would like my hook to automatically untick the box to remove it from my list. In other words, the 'textarea' status should go back and forth according to who modified it last. For the time being, I have managed to make it work with a checkbox that the user has to manually tick/untick, but I've noticed many kids forget to tick the box so they edit their text and I don't get a notice 😞
      Thanks if anyone takes time to try and help me on that one !
    • By celfred
      Hello,
      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! } } } }  
×
×
  • Create New...