Beste Möglichkeit, Plugins für eine PHP-Anwendung zuzulassen

276

Ich starte eine neue Webanwendung in PHP und dieses Mal möchte ich etwas erstellen, das die Leute mithilfe einer Plugin-Oberfläche erweitern können.

Wie schreibt man "Hooks" in ihren Code, damit Plugins an bestimmte Ereignisse angehängt werden können?

Wally Lawless
quelle

Antworten:

162

Sie können ein Observer-Muster verwenden. Ein einfacher funktionaler Weg, um dies zu erreichen:

<?php

/** Plugin system **/

$listeners = array();

/* Create an entry point for plugins */
function hook() {
    global $listeners;

    $num_args = func_num_args();
    $args = func_get_args();

    if($num_args < 2)
        trigger_error("Insufficient arguments", E_USER_ERROR);

    // Hook name should always be first argument
    $hook_name = array_shift($args);

    if(!isset($listeners[$hook_name]))
        return; // No plugins have registered this hook

    foreach($listeners[$hook_name] as $func) {
        $args = $func($args); 
    }
    return $args;
}

/* Attach a function to a hook */
function add_listener($hook, $function_name) {
    global $listeners;
    $listeners[$hook][] = $function_name;
}

/////////////////////////

/** Sample Plugin **/
add_listener('a_b', 'my_plugin_func1');
add_listener('str', 'my_plugin_func2');

function my_plugin_func1($args) {
    return array(4, 5);
}

function my_plugin_func2($args) {
    return str_replace('sample', 'CRAZY', $args[0]);
}

/////////////////////////

/** Sample Application **/

$a = 1;
$b = 2;

list($a, $b) = hook('a_b', $a, $b);

$str  = "This is my sample application\n";
$str .= "$a + $b = ".($a+$b)."\n";
$str .= "$a * $b = ".($a*$b)."\n";

$str = hook('str', $str);
echo $str;
?>

Ausgabe:

This is my CRAZY application
4 + 5 = 9
4 * 5 = 20

Anmerkungen:

Für diesen Beispielquellcode müssen Sie alle Plugins vor dem eigentlichen Quellcode deklarieren, der erweiterbar sein soll. Ich habe ein Beispiel beigefügt, wie einzelne oder mehrere Werte behandelt werden, die an das Plugin übergeben werden. Der schwierigste Teil davon ist das Schreiben der eigentlichen Dokumentation, in der aufgeführt ist, welche Argumente an jeden Hook übergeben werden.

Dies ist nur eine Methode, um ein Plugin-System in PHP zu erstellen. Es gibt bessere Alternativen. Ich empfehle Ihnen, die WordPress-Dokumentation zu lesen, um weitere Informationen zu erhalten.

Kevin
quelle
3
Beachten Sie, dass Sie dies für PHP> = 5.0 mithilfe der in der SPL definierten Observer / Subject-Schnittstellen implementieren können: php.net/manual/en/class.splobserver.php
John Carter
20
Pedantischer Hinweis: Dies ist kein Beispiel für das Observer-Muster. Es ist ein Beispiel für die Mediator Pattern. Echte Beobachter sind reine Benachrichtigungen, es gibt keine Nachrichtenübermittlung oder bedingte Benachrichtigung (noch gibt es einen zentralen Manager für die Steuerung von Benachrichtigungen). Es macht die Antwort nicht falsch , aber es sollte beachtet werden, um Leute davon abzuhalten, Dinge mit dem falschen Namen zu nennen ...
ircmaxell
Beachten Sie, dass Sie bei Verwendung mehrerer Hooks / Listener nur Zeichenfolgen oder Arrays zurückgeben sollten, nicht beide. Ich habe etwas Ähnliches für Hound CMS implementiert - getbutterfly.com/hound .
Ciprian
59

Nehmen wir also an, Sie möchten das Observer-Muster nicht, da Sie Ihre Klassenmethoden ändern müssen, um die Aufgabe des Abhörens zu bewältigen, und etwas Allgemeines wünschen. Angenommen, Sie möchten keine extendsVererbung verwenden, da Sie möglicherweise bereits in Ihrer Klasse von einer anderen Klasse erben. Wäre es nicht großartig, eine generische Möglichkeit zu haben, eine Klasse ohne großen Aufwand steckbar zu machen ? Hier ist wie:

<?php

////////////////////
// PART 1
////////////////////

class Plugin {

    private $_RefObject;
    private $_Class = '';

    public function __construct(&$RefObject) {
        $this->_Class = get_class(&$RefObject);
        $this->_RefObject = $RefObject;
    }

    public function __set($sProperty,$mixed) {
        $sPlugin = $this->_Class . '_' . $sProperty . '_setEvent';
        if (is_callable($sPlugin)) {
            $mixed = call_user_func_array($sPlugin, $mixed);
        }   
        $this->_RefObject->$sProperty = $mixed;
    }

    public function __get($sProperty) {
        $asItems = (array) $this->_RefObject;
        $mixed = $asItems[$sProperty];
        $sPlugin = $this->_Class . '_' . $sProperty . '_getEvent';
        if (is_callable($sPlugin)) {
            $mixed = call_user_func_array($sPlugin, $mixed);
        }   
        return $mixed;
    }

    public function __call($sMethod,$mixed) {
        $sPlugin = $this->_Class . '_' .  $sMethod . '_beforeEvent';
        if (is_callable($sPlugin)) {
            $mixed = call_user_func_array($sPlugin, $mixed);
        }
        if ($mixed != 'BLOCK_EVENT') {
            call_user_func_array(array(&$this->_RefObject, $sMethod), $mixed);
            $sPlugin = $this->_Class . '_' . $sMethod . '_afterEvent';
            if (is_callable($sPlugin)) {
                call_user_func_array($sPlugin, $mixed);
            }       
        } 
    }

} //end class Plugin

class Pluggable extends Plugin {
} //end class Pluggable

////////////////////
// PART 2
////////////////////

class Dog {

    public $Name = '';

    public function bark(&$sHow) {
        echo "$sHow<br />\n";
    }

    public function sayName() {
        echo "<br />\nMy Name is: " . $this->Name . "<br />\n";
    }


} //end class Dog

$Dog = new Dog();

////////////////////
// PART 3
////////////////////

$PDog = new Pluggable($Dog);

function Dog_bark_beforeEvent(&$mixed) {
    $mixed = 'Woof'; // Override saying 'meow' with 'Woof'
    //$mixed = 'BLOCK_EVENT'; // if you want to block the event
    return $mixed;
}

function Dog_bark_afterEvent(&$mixed) {
    echo $mixed; // show the override
}

function Dog_Name_setEvent(&$mixed) {
    $mixed = 'Coco'; // override 'Fido' with 'Coco'
    return $mixed;
}

function Dog_Name_getEvent(&$mixed) {
    $mixed = 'Different'; // override 'Coco' with 'Different'
    return $mixed;
}

////////////////////
// PART 4
////////////////////

$PDog->Name = 'Fido';
$PDog->Bark('meow');
$PDog->SayName();
echo 'My New Name is: ' . $PDog->Name;

In Teil 1 können Sie dies mit einem require_once()Aufruf oben in Ihrem PHP-Skript einschließen . Es lädt die Klassen, um etwas steckbar zu machen.

In Teil 2 laden wir dort eine Klasse. Hinweis: Ich musste nichts Besonderes für die Klasse tun, was sich erheblich vom Observer-Muster unterscheidet.

In Teil 3 schalten wir unsere Klasse in "steckbar" um (dh unterstützt Plugins, mit denen wir Klassenmethoden und -eigenschaften überschreiben können). Wenn Sie beispielsweise eine Web-App haben, verfügen Sie möglicherweise über eine Plugin-Registrierung und können hier Plugins aktivieren. Beachten Sie auch die Dog_bark_beforeEvent()Funktion. Wenn ich $mixed = 'BLOCK_EVENT'vor der return-Anweisung setze , blockiert dies das Bellen des Hundes und blockiert auch das Dog_bark_afterEvent, da es kein Ereignis geben würde.

In Teil 4 ist dies der normale Betriebscode. Beachten Sie jedoch, dass das, was Sie vielleicht für möglich halten, überhaupt nicht so ausgeführt wird. Zum Beispiel gibt der Hund seinen Namen nicht als "Fido" bekannt, sondern als "Coco". Der Hund sagt nicht "Miau", sondern "Woof". Und wenn Sie sich später den Namen des Hundes ansehen möchten, stellen Sie fest, dass er "anders" anstelle von "Coco" ist. Alle diese Überschreibungen wurden in Teil 3 bereitgestellt.

Wie funktioniert das? Nun, lassen Sie uns ausschließen eval()(was jeder als "böse" bezeichnet) und ausschließen, dass es sich nicht um ein Beobachtermuster handelt. Die Art und Weise, wie es funktioniert, ist die hinterhältige leere Klasse namens Pluggable, die nicht die von der Dog-Klasse verwendeten Methoden und Eigenschaften enthält. Da dies geschieht, werden sich die magischen Methoden für uns engagieren. Deshalb spielen wir in Teil 3 und 4 mit dem Objekt, das von der Pluggable-Klasse abgeleitet ist, und nicht mit der Dog-Klasse selbst. Stattdessen lassen wir die Plugin-Klasse das Dog-Objekt für uns "berühren". (Wenn das eine Art Designmuster ist, von dem ich nichts weiß - lassen Sie es mich bitte wissen.)

Volomike
quelle
3
Ist das nicht ein Dekorateur?
MV.
1
Ich habe auf Wikipedia darüber gelesen und, whoa, du hast recht! :)
Volomike
35

Die Hook- und Listener- Methode wird am häufigsten verwendet, aber Sie können auch andere Dinge tun. Abhängig von der Größe Ihrer App und davon, wem Sie das Anzeigen des Codes erlauben (wird dies ein FOSS-Skript oder etwas im Haus sein), wird dies einen großen Einfluss darauf haben, wie Sie Plugins zulassen möchten.

kdeloach hat ein schönes Beispiel, aber seine Implementierung und Hook-Funktion ist etwas unsicher. Ich würde Sie bitten, mehr Informationen über die Art der PHP-App zu geben, die Sie schreiben, und darüber, wie Plugins passen.

+1 bis kdeloach von mir.

w-ll
quelle
25

Hier ist ein Ansatz, den ich verwendet habe: Es ist ein Versuch, aus dem Qt-Signal- / Slot-Mechanismus eine Art Beobachtermuster zu kopieren. Objekte können Signale aussenden. Jedes Signal hat eine ID im System - es besteht aus der ID des Absenders + dem Objektnamen. Jedes Signal kann an die Empfänger gebunden werden, was einfach "aufrufbar" ist. Sie verwenden eine Busklasse, um die Signale an alle weiterzuleiten, die daran interessiert sind, sie zu empfangen, wenn etwas passiert, "senden" Sie ein Signal. Unten finden Sie eine Beispielimplementierung

    <?php

class SignalsHandler {


    /**
     * hash of senders/signals to slots
     *
     * @var array
     */
    private static $connections = array();


    /**
     * current sender
     *
     * @var class|object
     */
    private static $sender;


    /**
     * connects an object/signal with a slot
     *
     * @param class|object $sender
     * @param string $signal
     * @param callable $slot
     */
    public static function connect($sender, $signal, $slot) {
        if (is_object($sender)) {
            self::$connections[spl_object_hash($sender)][$signal][] = $slot;
        }
        else {
            self::$connections[md5($sender)][$signal][] = $slot;
        }
    }


    /**
     * sends a signal, so all connected slots are called
     *
     * @param class|object $sender
     * @param string $signal
     * @param array $params
     */
    public static function signal($sender, $signal, $params = array()) {
        self::$sender = $sender;
        if (is_object($sender)) {
            if ( ! isset(self::$connections[spl_object_hash($sender)][$signal])) {
                return;
            }
            foreach (self::$connections[spl_object_hash($sender)][$signal] as $slot) {
                call_user_func_array($slot, (array)$params);
            }

        }
        else {
            if ( ! isset(self::$connections[md5($sender)][$signal])) {
                return;
            }
            foreach (self::$connections[md5($sender)][$signal] as $slot) {
                call_user_func_array($slot, (array)$params);
            }
        }

        self::$sender = null;
    }


    /**
     * returns a current signal sender
     *
     * @return class|object
     */
    public static function sender() {
        return self::$sender;
    }

}   

class User {

    public function login() {
        /**
         * try to login
         */
        if ( ! $logged ) {
            SignalsHandler::signal(this, 'loginFailed', 'login failed - username not valid' );
        }
    }

}

class App {
    public static function onFailedLogin($message) {
        print $message;
    }
}


$user = new User();
SignalsHandler::connect($user, 'loginFailed', array($Log, 'writeLog'));
SignalsHandler::connect($user, 'loginFailed', array('App', 'onFailedLogin'));

$user->login();

?>
andy.gurin
quelle
18

Ich glaube, der einfachste Weg wäre, Jeffs eigenen Ratschlägen zu folgen und sich den vorhandenen Code anzusehen. Schauen Sie sich Wordpress, Drupal, Joomla und andere bekannte PHP-basierte CMS an, um zu sehen, wie ihre API-Hooks aussehen und sich anfühlen. Auf diese Weise können Sie sogar Ideen erhalten, an die Sie vorher vielleicht noch nicht gedacht haben, um die Dinge ein wenig rubustiger zu machen.

Eine direktere Antwort wäre, allgemeine Dateien zu schreiben, die sie "include_once" in ihre Datei aufnehmen würden, um die Benutzerfreundlichkeit zu bieten, die sie benötigen würden. Dies würde in Kategorien unterteilt und NICHT in einer MASSIVEN "hooks.php" -Datei bereitgestellt. Seien Sie jedoch vorsichtig, denn was am Ende passiert, ist, dass Dateien, die sie enthalten, immer mehr Abhängigkeiten und Funktionen aufweisen. Versuchen Sie, die API-Abhängigkeiten gering zu halten. IE weniger Dateien für sie enthalten.

halloandre
quelle
Ich würde DokuWiki zur Liste der Systeme hinzufügen, die Sie sich ansehen können. Es hat ein schönes Ereignissystem, das ein reichhaltiges Plugin-Ökosystem ermöglicht.
Chiborg
15

Es gibt ein nettes Projekt namens Stickleback von Matt Zandstra bei Yahoo, das einen Großteil der Arbeit für den Umgang mit Plugins in PHP erledigt.

Es erzwingt die Schnittstelle einer Plugin-Klasse, unterstützt eine Befehlszeilenschnittstelle und ist nicht allzu schwer in Betrieb zu nehmen - insbesondere, wenn Sie die Titelgeschichte im PHP Architect Magazine lesen .

julz
quelle
11

Ein guter Rat ist, zu sehen, wie andere Projekte dies getan haben. Viele fordern die Installation von Plugins und deren Registrierung für Dienste (wie bei WordPress), sodass Sie in Ihrem Code "Punkte" haben, an denen Sie eine Funktion aufrufen, die registrierte Listener identifiziert und ausführt. Ein Standard-OO-Designmuster ist das Observer Pattern , das eine gute Option für die Implementierung in einem wirklich objektorientierten PHP-System darstellt.

Das Zend Framework verwendet viele Hook-Methoden und ist sehr gut aufgebaut. Das wäre ein gutes System.

SIE
quelle
8

Ich bin überrascht, dass die meisten Antworten hier auf Plugins ausgerichtet zu sein scheinen, die lokal für die Webanwendung sind, dh Plugins, die auf dem lokalen Webserver ausgeführt werden.

Was ist, wenn Sie möchten, dass die Plugins auf einem anderen Remote-Server ausgeführt werden? Der beste Weg, dies zu tun, besteht darin, ein Formular bereitzustellen, mit dem Sie verschiedene URLs definieren können, die aufgerufen werden, wenn bestimmte Ereignisse in Ihrer Anwendung auftreten.

Unterschiedliche Ereignisse senden unterschiedliche Informationen basierend auf dem gerade aufgetretenen Ereignis.

Auf diese Weise führen Sie einfach einen cURL-Aufruf an die URL durch, die Ihrer Anwendung bereitgestellt wurde (z. B. über https), wo Remoteserver Aufgaben basierend auf Informationen ausführen können, die von Ihrer Anwendung gesendet wurden.

Dies bietet zwei Vorteile:

  1. Sie müssen keinen Code auf Ihrem lokalen Server hosten (Sicherheit)
  2. Der Code kann sich auf Remote-Servern (Erweiterbarkeit) in anderen Sprachen als PHP (Portabilität) befinden.
Tim Groeneveld
quelle
8
Dies ist eher eine "Push-API" als ein "Plugin" -System. Sie bieten anderen Diensten die Möglichkeit, Benachrichtigungen über ausgewählte Ereignisse zu erhalten. Mit "Plugins" ist im Allgemeinen gemeint, dass Sie die Anwendung installieren und dann Funktionen hinzufügen können, um ihr Verhalten an Ihre Zwecke anzupassen. Dazu muss das Plugin lokal ausgeführt werden - oder zumindest über eine sichere und effiziente bidirektionale Kommunikation verfügen Informationen zu der Anwendung nehmen sie es nicht nur von ihm. Die beiden Funktionen unterscheiden sich etwas voneinander, und in vielen Fällen ist ein "Feed" (z. B. RSS, iCal) eine einfache Alternative zu einer Push-API.
IMSoP