ProcessWire - Hooks: Unterschied zwischen den Versionen

Aus Wikizone
Wechseln zu: Navigation, Suche
Zeile 34: Zeile 34:
  
 
=== Wie nutze ich Hooks in der Praxis? ===
 
=== Wie nutze ich Hooks in der Praxis? ===
 +
 
==== Welchen soll ich nehmen? ====
 
==== Welchen soll ich nehmen? ====
 
Zunächst mal gibt es drei Funktionen mit denen seine eigenen Funktionen einklinkt:
 
Zunächst mal gibt es drei Funktionen mit denen seine eigenen Funktionen einklinkt:
Zeile 89: Zeile 90:
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 +
==== Das HookEvent Objekt ====
 +
In jedem Hook wird der Hookfunktion ein HookEvent Objekt mitgegeben. Das Objekt hilft einem bei folgenden Aufgaben
 +
myHookEvent->arguments(); //Eingabewerte der Funktion lesen und manipulieren (im addHookBefore)
 +
myHookEvent->return; Rückgabewert einer Funktion lesen und manipulieren (nur im addHookAfter)
 +
myHookEvent->object; //Enthält die Objektinstanz, über den der Hook aufgerufen wurde (z.B. das Page Objekt).
 +
myHookEvent->replace(); // Damit wird die Hookable Funktion komplett ersetzt. Vorsicht du mußt selbst für den richtigen Rückgabewert achten.
  
 
==== Wie kann ich mit einem Hook Argumente modifizieren? ====
 
==== Wie kann ich mit einem Hook Argumente modifizieren? ====
'''Das HookEvent Objekt'''
 
 
In jedem Hook wird der Hookfunktion ein HookEvent Objekt mitgegeben.
 
  
 
''' Argumente lesen... '''
 
''' Argumente lesen... '''
  
Lesen möchte man '''meistens im addHookBefore'''. Denn dann kann man die Werte nochmal verändern bevor sie durch die Funktion geschickt werden.   
+
Arugemente lesen möchte man '''meistens im addHookBefore'''. Denn dann kann man die Werte nochmal verändern bevor sie durch die Hookable Funktion geschickt werden.   
  
 
Das HookEvent Objekt hat eine arguments() Methode über die man die Argumente die in der original Methode übergeben werden (die mit den ___) abgreifen kann. Das funktioniert über den index oder den Namen des Arguments:
 
Das HookEvent Objekt hat eine arguments() Methode über die man die Argumente die in der original Methode übergeben werden (die mit den ___) abgreifen kann. Das funktioniert über den index oder den Namen des Arguments:
Zeile 125: Zeile 129:
 
</syntaxhighlight>
 
</syntaxhighlight>
  
==== Wie kann man den Rückgabewert einer Hookable Funktion manipulieren ====
+
==== Wie kann man den Rückgabewert einer Hookable Funktion manipulieren ?====
 +
 
 
Das macht man mit dem addHookAfter. Diesen setzt man in der Regel auf Ausgabewerte einer Funktion an.
 
Das macht man mit dem addHookAfter. Diesen setzt man in der Regel auf Ausgabewerte einer Funktion an.
  
Zeile 133: Zeile 138:
 
  $event->return = $derNeueRückgabewert.
 
  $event->return = $derNeueRückgabewert.
 
Beispiel siehe unten.
 
Beispiel siehe unten.
 +
 +
==== Welches Objekt hat meine Hook getriggert? ====
 +
Das HookEvent hat eine Eigenschaft Object. Diese enthält das Objekt (oder vielmehr die Instanz davon) die die Hookmethode aufruft.
 +
<syntaxhighlight lang="php">
 +
public function hookAfterPageRender($event) {
 +
  $page = $event->object;
 +
  if($page->id == 1) {
 +
    $event->return = str_replace("</body>", "<p>Homepage!</p></body>", $event->return);
 +
  }
 +
}
 +
</syntaxhighlight>
 +
 +
==== Wie kann ich eine hookable Function komplett ersetzen ====
 +
Das benötigt man eher selten und ist mit Vorsicht zu genießen. Kann aber manchmal nützlich sein. Dafür setzt man die replace Eigenschaft auf true:
 +
myEvent->replace = true;
 +
 +
Beispiel:
 +
<syntaxhighlight lang="php">
 +
public function hookBeforePageRender($event) {
 +
  $page = $event->object;
 +
  // we'll only apply this to the front-end of our site
 +
  if($page->template == 'admin') return;
 +
  // tell ProcessWire we are replacing the method we've hooked
 +
  $event->replace = true;
 +
  $event->return = "Sorry the site is undergoing maintenance";
 +
}
 +
</syntaxhighlight>
  
 
==== Wo kann man Hooks definieren ====
 
==== Wo kann man Hooks definieren ====
 +
 
Es bieten sich folgende Stellen an:
 
Es bieten sich folgende Stellen an:
 
* In einem eigenen Plugin
 
* In einem eigenen Plugin
Zeile 149: Zeile 182:
 
* Überall wo die Elternklasse verfügbar ist.
 
* Überall wo die Elternklasse verfügbar ist.
  
 +
==== Wie schreibt man eine eigene hookable Methode?====
 +
* Eine Funktion in die man hooken kann muss in einer Klasse sein, die von Wire oder WireData erbt.
 +
* Drei Underscores ___ macht sie automatisch zu einer Hookable Methode
 +
Da in ProcessWire fast alles von Wire oder WireData abstammt, kann man fast überall Hooks finden und einsetzen.
  
 
+
==== Was geht sonst so? ====
 
+
* Neue Funktionen injecten (mit addHook)
 
+
* Neue Eigenschaften injecten
 
+
* ... Siehe Link oben
 
 
<syntaxhighlight lang="php">
 
</syntaxhighlight>
 
 
 
<syntaxhighlight lang="php">
 
</syntaxhighlight>
 
 
 
 
<syntaxhighlight lang="php">
 
<syntaxhighlight lang="php">
 
</syntaxhighlight>
 
</syntaxhighlight>

Version vom 26. November 2020, 18:48 Uhr

Wie funktionieren Hooks

https://processwire.com/docs/modules/hooks/

Hier in Kürze die wichtigsten Infos.

Alle Klassen die von Wire abstammen können Hookable Methoden implementieren..

Beginnt eine Funktion in der Klasse mit 3 Underscores wird Sie automatisch Hookable. Das heißt also Hooks bestehen aus der hookable Funktion und den Hooks selbst die an dieser "einhaken".

public function ___hookableMethod($arg1, $arg2) {
  // this method is hookable
}

Aufgerufen wird die Funktion ohne die Underscores

'''
$this->hookableMethod('a', 'b');

Methoden die über Hooks erzeugt wurden sind selber Hookable Beispiele aus dem Core sind: Page::render(), Page::viewable() and Page::editable()

Es gibt in ProcessWire hookable Methoden die nichts tun sondern nur da sind um sich einzuhaken Z.B. Pages::saveReady (wird aufgerufen wenn eine Seite gleich gespeichert wird aber noch nicht ist, oder Page::saved (nach dem Speichern). Es gibt an dieser Stelle auch noch Page::save, hier muss man entscheiden ob man before oder after wählt. Bei den anderen beiden ist es egal, da sie immer vorher oder nach dem Speichern aufgerufen werden.


Das können Hooks

  • Argumente für die Funktion holen oder modifizieren bevor sie verarbeitet werden (beforeHook)
  • Den Rückgabewert der Funktion holen und/oder modifizieren nachdem die Argumente verarbeitet wurden (afterHook)
  • Die Methode komplett ersetzen (Variante des beforeHook)
  • Eine Methode in die Klasse einschleusen. Diese ist dann über $object->method() verfügbar.
  • Eine Eigenschaft in die Klasse einschleusen. Diese ist dann über $object->property verfügbar. Das funktioniert nicht bei allen Hooks.

Wie nutze ich Hooks in der Praxis?

Welchen soll ich nehmen?

Zunächst mal gibt es drei Funktionen mit denen seine eigenen Funktionen einklinkt:

addHookBefore(...) //do s.th. before hookable Method is running - i.e. modify arguments
addHookAfter(...) // do s.th. after hookable Method is running - i.e. modify return value
addHook(...) // inject a new method or property in to the class where the hookable Method is defined

Am Namen sieht man schon wo das ganze dann passiert. Bei addHook spielt es keine Rolle, denn diesen nutzt man um neue Methoden und Eigenschaften in die Klasse zu schleusen.

Wie wird er aufgerufen?

Zuerst mußt du schauen ob du in der selben (oder einer geerbten) Klasse bist. Dementsprechend greifst du mit $this-> oder wire()-> darauf zu.

$this->addHookBefore(…); // hook in the same class
wire()->addHookAfter(...); // hook from another class

Als Argumente des Hook übergibt man den Namen der Hook-Funktion (hookable Method)und eine Funktion die für dich etwas tun soll. Diese Funktion bekommt ein Objekt vom Typ Hookable Event. Dieses Objekt läßt einen auf die wichtigen Dinge in der Hookable Function zugreifen.

Fall-Beispiele

// INNERHALB einer Klasse (deshalb funktioniert $this), Funktion als ANONYME FUNKTION direkt im Aufruf
$this->addHookAfter('Class::method', function($event) {
  // function implementation
});

// INNERHALB einer Klasse mit FUNKTIONSNAME - 'myHookMethodName' ist eine public method dieser Klasse und kann direkt aufgerufen werden.
$this->addHookAfter('Class::method', $this, 'myHookMethodName');

// AUßERHALB einer Klasse nutzt man $wire oder wire()
wire()->addHookAfter('Class::method', function($event) {
  // function implementation
});

// Aufruf über FUNKTIONSNAME: Du gibst den Funktionsnamen an und die Funktion 'myHookFunctionName' wird separat definiert.
wire()->addHookAfter('Class::method', null, 'myHookFunctionName');

// Hooks gelten in der Regel für alle Instanzen eines Objekts (z.B. alle Seiten)
// Manchmal kann es sinnvoll sein nur EINE EINZELNE INSTANZ zu Hooken. Dann kann man direkt das Objekt hernehmen um eine Funktion einzuhaken.
// Einzelne Instanz innerhalb einer Klasse (z.B. Page Objekt). Kann kleine Performance Vorteile bringen.
$page = $this->wire('page'); // may be any Page object instance
$page->addHookAfter('render', function($event) { ... }); // Hook nach page::render dieses page Objekts

// Einzelne Instanz außerhalb der Klasse Hooken ($page may be any Page object instance)
$page->addHookAfter('render', function($event) { ... }); 


// mit addHook kann man auch BEFORE UND AFTER in einer Funktion nutzen. Dazu definiert man hinten noch das array welches man nutzen möchte.
$wire->addHook('Class::method', function(HookEvent $e) {
  if($e->when == 'before') {
    // this is the 'before' state
  } else if($e->when == 'after') {
    // this is the 'after' state'
  }
}, [ 'before' => true, 'after' => true ]);

Das HookEvent Objekt

In jedem Hook wird der Hookfunktion ein HookEvent Objekt mitgegeben. Das Objekt hilft einem bei folgenden Aufgaben

myHookEvent->arguments(); //Eingabewerte der Funktion lesen und manipulieren (im addHookBefore) 
myHookEvent->return; Rückgabewert einer Funktion lesen und manipulieren (nur im addHookAfter) 
myHookEvent->object; //Enthält die Objektinstanz, über den der Hook aufgerufen wurde (z.B. das Page Objekt).
myHookEvent->replace(); // Damit wird die Hookable Funktion komplett ersetzt. Vorsicht du mußt selbst für den richtigen Rückgabewert achten.

Wie kann ich mit einem Hook Argumente modifizieren?

Argumente lesen...

Arugemente lesen möchte man meistens im addHookBefore. Denn dann kann man die Werte nochmal verändern bevor sie durch die Hookable Funktion geschickt werden.

Das HookEvent Objekt hat eine arguments() Methode über die man die Argumente die in der original Methode übergeben werden (die mit den ___) abgreifen kann. Das funktioniert über den index oder den Namen des Arguments:

Schauen wir uns als Beispiel den Pages::saved Hook an. In der Definition (schau mal bei Captain Hook oder im File nach) finden wir, dass ein Page Objekt mit dem namen $page übergeben wird. Auf dieses übergebene $page Objekt können wir über den Index 0 oder den Namen 'page' zugreifen.

public function myHookFunction($event) {
  // retrieve first argument by index (0 based)
  $page = $event->arguments(0);
  // OR: retrieve argument by name 'page'
  $page = $event->arguments('page');
  $this->message("You saved page: $page->path");
}

Argumente schreiben

Wenn wir die Argumente gelesen haben können wir sie verändern und dann auch wieder zurückgeben. Dafür hat die arguments() Funktion einen zweiten Parameter. Setzt man diesen, überscheibt man den alten Wert. Macht man das nicht wird einfach der unveränderte Wert an die Funktion weitergereicht.

// assign by index (0=first argument)
$event->arguments(0, $myValue);

// assign by name
$event->arguments('argument_name', $myValue);

Wie kann man den Rückgabewert einer Hookable Funktion manipulieren ?

Das macht man mit dem addHookAfter. Diesen setzt man in der Regel auf Ausgabewerte einer Funktion an.

Der Hook kann über die Eigenschaft

$meinRückgabewert = $event->return 

auf den Rückgabewert zugreifen. Und Ihn auch wieder setzen:

$event->return = $derNeueRückgabewert.

Beispiel siehe unten.

Welches Objekt hat meine Hook getriggert?

Das HookEvent hat eine Eigenschaft Object. Diese enthält das Objekt (oder vielmehr die Instanz davon) die die Hookmethode aufruft.

public function hookAfterPageRender($event) {
  $page = $event->object;
  if($page->id == 1) {
    $event->return = str_replace("</body>", "<p>Homepage!</p></body>", $event->return);
  }
}

Wie kann ich eine hookable Function komplett ersetzen

Das benötigt man eher selten und ist mit Vorsicht zu genießen. Kann aber manchmal nützlich sein. Dafür setzt man die replace Eigenschaft auf true:

myEvent->replace = true;

Beispiel:

public function hookBeforePageRender($event) {
  $page = $event->object;
  // we'll only apply this to the front-end of our site
  if($page->template == 'admin') return;
  // tell ProcessWire we are replacing the method we've hooked
  $event->replace = true;
  $event->return = "Sorry the site is undergoing maintenance";
}

Wo kann man Hooks definieren

Es bieten sich folgende Stellen an:

  • In einem eigenen Plugin
    • in der init() Funktion wenn er vor dem Verarbeiten des Request vorhanden sein muss
    • in der ready() Funktion wenn das Page Objekt schon bekannt sein muss (dann benötigst du ein autoload Modul)
  • in den Dateien ready.php und init.php
    • Hier gilt das gleiche wie beim Plugin
  • in admin.php
    • dieses File wird nur bei Backend Zugriffen genutzt. Also cool um im Backen etwas zu erledigen.
  • In einem Template
    • z.B. in einem dass über config.php $config->prependTemplateFile = meinHauptTemplate definiert wurde. Hier ganz oben, dann hat man viele Möglichkeiten.

Wo kann man Hooks aufrufen

  • Überall wo die Elternklasse verfügbar ist.

Wie schreibt man eine eigene hookable Methode?

  • Eine Funktion in die man hooken kann muss in einer Klasse sein, die von Wire oder WireData erbt.
  • Drei Underscores ___ macht sie automatisch zu einer Hookable Methode

Da in ProcessWire fast alles von Wire oder WireData abstammt, kann man fast überall Hooks finden und einsetzen.

Was geht sonst so?

  • Neue Funktionen injecten (mit addHook)
  • Neue Eigenschaften injecten
  • ... Siehe Link oben

Wo setzt man hooks am Besten ein

Es gibt einige Standard Dateien die für solche Zwecke geeignet sind:

/site/init.php

PW Boot -> Module mit autoload -> init.php

This file is included during ProcessWire's boot initialization, immediately after autoload modules have been loaded and had their init() methods called. Anything you do in here will behave the same as an init() method on a module. When this file is called, the current $page has not yet been determined. This is an excellent place to attach hooks that don't need to know anything about the current page.

/site/ready.php

API geladen und ready -> $page bereit aber noch nicht gerendert

This file is included immediately after the API is fully ready. It behaves the same as a ready() method in an autoload module. The current $page has been determined, but not yet rendered. This is an excellent place to attach hooks that may need to know something about the current page. It's also an excellent place to include additional classes or libraries that will be used on all pages in your site.

/site/finished.php

Seite gerendert

This file is included when ProcessWire has finished rendering and delivering a page, and is in the process of shutting down. It is called immediately before the API is disengaged, so you can still access any API variable and update $session values as needed. Admittedly, this is probably not the place you would put hooks, but it is an ideal place to perform your own shutdown, should your application call for it.

Beispiele für Hooks

Basic Starter Hook

Erster Schritt zum ausprobieren

// i.e. in ready.php
$wire->addHookAfter('Pages::saved', function(HookEvent $event) {
  $page = $event->arguments(0);
  bd($page); // tracy debugger output
});

// more specific
$wire->addHookAfter('Pages::added(template=basic-page)', function(HookEvent $event) {
  $page = $event->arguments(0);
  bd($page);
});

Feld manipulieren

Beim Speichern einer Seite ein Feld anpassen

$pages->addHookAfter('saveReady', function(HookEvent $event) {
    $page = $event->arguments(0);
    // If the page has the relevant template...
    if($page->template == 'oe') {
        // make title uppercase
        $page->name = strtoupper($page->name);
    }
});

Seiten im Backend sortieren

/*sort clients of key account manager by title in backend*/
$pages->addHookAfter('saveReady', function(HookEvent $event) {
    $page = $event->arguments(0);
    // If the page has the relevant template...
    if($page->template == 'key_account_manager') {
        // Sort the Page Reference field by title
        $page->pr_kam_clients->sort('title') ;
    }
});

Externe Klasse verfügbar machen

/site/ready.php

<?php

$pages->addHookAfter('saved', function($event) {
  $page = $event->object;
  if($page->template == 'product') {
    // update other related pages when 'product' page is saved
  }
});

if($page->template != 'admin') {
  // include an external helper class...
  require('./classes/ProductCart.php');
  // ...and establish it as a new $cart API variable, and
  // populate it with products saved in the user's session:
  $wire->wire('cart', new ProductCart($session->productsInCart));
}

There is also a /site/finished.php that obtains the products from the $cart and saves them back to the session, ready for the next request:

/site/finished.php

<?php

if($page->template != 'admin') {
  $session->productsInCart = $cart->getArray();
}

Custom Hook Methode

Beispiel aus der ready.php

/** @var ProcessWire $wire */

/**
 * Example of a custom hook method
 * 
 * This hook adds a “numPosts” method to pages using template “category”.
 * The return value is the quantity of posts in category.
 *
 * Usage:
 * ~~~~~
 * $numPosts = $page->numPosts(); // returns integer
 * numPosts = $page->numPosts(true); // returns string like "5 posts"
 * ~~~~~
 *
 */
$wire->addHook('Page(template=category)::numPosts', function($event) {
	/** @var Page $page */
	$page = $event->object;

	// only category pages have numPosts
	if($page->template != 'category') return;

	// find number of posts
	$numPosts = $event->pages->count("template=blog-post, categories=$page");

	if($event->arguments(0) === true) {
		// if true argument was specified, format it as a "5 posts" type string
		$numPosts = sprintf(_n('%d post', '%d posts', $numPosts), $numPosts);
	}

	$event->return = $numPosts;
});

Rendering von Formularen anpassen

Manchmal rendern Module Formulare im Frontend, aber die Styles passen nicht ganz zum eigenen Style. Man kann aber vor dem Rendern des Formulars einhaken und noch ein paar Klassen nachschieben bevor es gerendert wird. Die Hookfunktion kann man in ready.php unterbringen. Also kurz bevor ProcessWire mit dem Rendern der Seite loslegt.

ready.php

/**
 * add uikit classes to checkout forms
 */
  $this->addHookBefore('InputfieldForm::render', function(HookEvent $event) {
    if(page()->template->name == 'checkout'){
        // Get the object the event occurred on
        $InputfieldForm = $event->object;
        foreach($InputfieldForm as $f){
            bd(get_class($f),'Inputfield Hook->field');
            switch (get_class($f)) {
                case 'ProcessWire\InputfieldEmail':
                case 'ProcessWire\InputfieldText':
                    $f->addClass('uk-input');
                    break;
                case 'ProcessWire\InputfieldTextarea':
                    $f->addClass('uk-textarea');
                    break;
                case 'ProcessWire\InputfieldRadios':
                    $f->addClass('uk-radio');
                    break;
                case 'ProcessWire\InputfieldCheckbox':
                    $f->addClass('uk-checkbox');
                    break;
                case 'ProcessWire\InputfieldSubmit':
                    $f->addClass('uk-button uk-button-primary');
                    break;
            }
            

        }
        return $InputfieldForm; // send back manipulated object
    }
  });