processwire / processwire-requests

ProcessWire feature requests.
40 stars 0 forks source link

automatically call the init method on custom page classes #456

Open jmartsch opened 2 years ago

jmartsch commented 2 years ago

Custom Page classes are great, but they would be even greater if we could use hooks in their init method which would be called automatically.

One thing I and others like Bernhard are doing when developing custom modules (or modifying ProcessPageEdit pages), is to add hooks to a custom page class like so:

<?php namespace ProcessWire;
// site/classes/OrderPage.php
class OrderPage extends Page{
  public function init(){
    bd("init OrderPage");
    bd($this->template->name);
    $this->addHookBefore('ProcessPageEdit::buildFormContent', $this, "buildFormContent");
  }

  public function buildFormContent($event){
    bd("buildFormContent");
    // modify the form before output
  }
}

However the init method isn't called when you go to a page with the order template (in my case in the admin).

Bernhard has a workaround where he triggers the init method of the custom page class in /site/init.php via $pages->get('template=order')->init();;

Problems with this approach are:

If I want to use the workaround Bernhard provided, I would have to return early manually if the template name doesn't match, like so:

public function buildFormContent(HookEvent $event)
  {
    // Change the title of the page and the browser tab
    // Get page being edited
    $page = $event->object->getPage();
    // Only use on pages with the order template
     if ($page->template != 'order') return;

   // else do stuff
  }

Else the hook would be executed on every page.

What you think about this, or are there other ways, to modify things like the form content in the admin via custom page classes?

BernhardBaumrock commented 2 years ago

You can do this:

<?php namespace ProcessWire;
use RockMigrations\MagicPage;
class OrderPage extends Page {
  use MagicPage;

  public function editForm($form) {
    $form->get('title')->label = 'hooked label';
  }
}

;)

See https://github.com/baumrock/RockMigrations/blob/1312656a459a723510019f9a3e1d95b550b3625a/RockMigrations.module.php#L506

jmartsch commented 2 years ago

@BernhardBaumrock This requires installing your RockMigrations module. I would prefer to have this natively and not via an additional module. But thank you for showing me this solution, which I think I will use until we have something better.

ryancramerdesign commented 2 years ago

@jmartsch How about using the wired() method? I think it should do the same thing as an init() method in this case. It is called for any Wire-derived class when the current ProcessWire instance dependency injection is complete. Of course, you could use the construct() method too, but at that point the current instance dependencies have not been injected, so construct() is not an ideal method for adding hooks.

jmartsch commented 2 years ago

Thanks for your answer @ryancramerdesign. I didn't know about the wired() method. Using it works absolutely fine.

BernhardBaumrock commented 2 years ago

@ryancramerdesign and @jmartsch using wired() might have unexpected side effects! For example if you create a "BasicPagePage" pageclass and add the wired() method, than this wired() method is called for every loaded BasicPage. So if you had 3 BasicPages in your pagetree, then that triggers wired() three times!

Depending on your implementation of wire() this might add your hook more often than you want...

RockMigrations makes sure that for MagicPages the init() method is triggered on "init" of the boot process and "ready" at ready. And both are triggered only ONCE no matter how many pages you have created (with the latest version also if no page is created!).

It does also offer other helper methods like "onSaveReady", which is only triggered on saveReady of this specific pageclass! So you get extremely simple and clean code, see https://www.youtube.com/watch?v=eBOB8dZvRN4&t=517s

jmartsch commented 2 years ago

I experienced something similar to what @BernhardBaumrock mentioned. I use AdminOnSteroids, which adds a prev and next Button to a ProcessPageEdit page. As the prev and next Button query the PW API, it executes the hook for all pages which have the type OrderPage.
In my hook I add a field (or more) to the ProcessPageEdit form. So I had three fields added, instead of one.

It would be nice, if there is a native way of doing what Bernhad mentioned. So that the hook only gets added once.

teppokoivula commented 2 years ago

It would be nice, if there is a native way of doing what Bernhad mentioned. So that the hook only gets added once.

I get that you're perhaps looking for a "ProcessWire native" way to do this, but just for the record: this should be quite easy to avoid by using static class property in your OrderPage class. Store the hook ID returned by addHook there, and only add the hook if that property is empty.

Sure, that adds a bit of boilerplate, but we're talking about a couple of lines of code here. I guess it's a question of point of view if we need something in the core for this :)

szabeszg commented 2 years ago

Bernhad and I recently discussed the same topic, see: https://processwire.com/talk/topic/27419-processwirerocks-my-youtube-channel-%F0%9F%98%8E/?do=findComment&comment=226313

I also used to ask Ryan about a related issue, namely how to handle a frontend page request via a Custom Page method (LoginRegisterPro module topic) module: https://processwire.com/talk/topic/23310-register-link-not-correct/?do=findComment&comment=223150

What I would really like to see is core support for both needs, because initialising a page object at the right time in the page request process is a must. We can agree on that, I think.

It's great that we have the basics and it is documented: https://processwire.com/blog/posts/pw-3.0.152/ but the next step should be official support to make the initialisation process simple and documented too. I wish... :)

jmartsch commented 1 year ago

@ryancramerdesign Any further thoughts on this? I think a native way of triggering the init or ready method at the right time would be a huge improvement to what i doable with ProcessWire as a software framework.

jlahijani commented 6 months ago

@ryancramerdesign πŸ™

szabeszg commented 6 months ago

@ryancramerdesign πŸ™

As far as I know, Ryan checks the number of "thumbs up" of the initial post of the request. Anyone want to add more?

BernhardBaumrock commented 6 months ago

Hey @ryancramerdesign I still think this would be a great addition! Others try to find a solution for this as well, see See https://processwire.com/talk/topic/29996-a-simple-way-to-have-your-hook-methods-inside-your-custom-page-classes/

I think it would already be enough to trigger init() and ready() properly. No need (impossible) to reflect all hookable methods like buildform or saveready, we could do that on our own, but we'd need at least the ready() and init() method to attach the needed hooks and have everything related to the custom page class inside the custom page class.

This is how RockMigrations is doing it, which works great for years now: https://github.com/baumrock/RockMigrations/blob/5d7bb4b427f953bf79257c332bd9dcfbd8bba261/MagicPages.module.php#L90-L91

szabeszg commented 4 months ago

I just want to share my follow-up example code I posted originally over here: https://processwire.com/talk/topic/30138-page-classes-diving-into-one-of-processwires-best-features/?do=findComment&comment=242747

/*
 * Called when the page is requested on the frontend.
 * Called from _init.php via include_once(config()->paths->templates . 'path/to/this/_init_once.php');
 * so that it only runs once per request:
 * page()->requested();
 */
public function requested() {
    $this->request_ws = "\ProcessWire\RqtProduct";
    parent::requested();
}

/*
 * When initialization of object properties is required right from the beginning.
 */
function __construct(Template $tpl = null) {
    parent::__construct($tpl);
    $this->factsAy = new Arrayy([]);
}

public function ___loaded() {
    parent::___loaded();

    // Either building a structured array of all product data and caching it in a variable or reading that from memory.
    if (empty($this->facts)) {
        $this->encodeFacts();
    } else {
        $this->factsAy = Arrayy::createFromJson($this->facts);
    }
}

/**
 * Product page specific hooks. This method is called from site/init.php from my custom loop 
 * that runs for each frontend template based page.
 * if (in_array($templateName, config()->noneFrontendTemplates)) {
    * if ($templateName === "user") call_user_func_array("{$namespace}{$className}::initiate", []); // UserPage needs special treatment.
    * continue;
 * }
 * if (class_exists($namespace . $className)) call_user_func_array("{$namespace}{$className}::initiate", []); // Page classes register hooks in their initiate() method this way.
 */
public static function initiate() {
    parent::initiate();

    wire()->addHookBefore("Pages::saveReady(template=product)", function ($event) {

... more hooks go here ....

So all in all, I am seeking an officially recommended, supported and documented way of handling:

jlahijani commented 3 months ago

Link to Ryan's additional thoughts about this on the forum: https://processwire.com/talk/topic/30194-weekly-update-%E2%80%93%C2%A011-july-2024/?do=findComment&comment=243010