Il modo migliore per consentire i plug-in per un'applicazione PHP


276

Sto avviando una nuova applicazione Web in PHP e questa volta voglio creare qualcosa che le persone possano estendere utilizzando un'interfaccia plug-in.

Come si fa a scrivere "hook" nel loro codice in modo che i plugin possano collegarsi a eventi specifici?

Risposte:


162

È possibile utilizzare un modello Observer. Un modo funzionale semplice per ottenere questo risultato:

<?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;
?>

Produzione:

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

Appunti:

Per questo codice sorgente di esempio, è necessario dichiarare tutti i plug-in prima del codice sorgente effettivo che si desidera estendere. Ho incluso un esempio di come gestire i valori singoli o multipli passati al plugin. La parte più difficile di questo è scrivere la documentazione effettiva che elenca quali argomenti vengono passati a ciascun hook.

Questo è solo un metodo per realizzare un sistema di plugin in PHP. Ci sono alternative migliori, ti suggerisco di consultare la documentazione di WordPress per ulteriori informazioni.


3
Si noti che per PHP> = 5.0 è possibile implementarlo usando le interfacce Observer / Subject definite nell'SPL: php.net/manual/en/class.splobserver.php
John Carter,

20
Nota pedante: questo non è un esempio del modello Observer. È un esempio di Mediator Pattern. I veri osservatori sono puramente notifica, non vi è alcun passaggio di messaggi o notifica condizionale (né esiste un gestore centrale per il controllo delle notifiche). Non rende la risposta sbagliata , ma va notato per impedire alle persone di chiamare le cose con il nome sbagliato ...
ircmaxell,

Si noti che quando si utilizzano più hook / listener, è necessario restituire solo stringhe o array, non entrambi. Ho implementato qualcosa di simile per Hound CMS: getbutterfly.com/hound .
Ciprian,

59

Quindi diciamo che non vuoi il modello Observer perché richiede che tu modifichi i metodi della tua classe per gestire l'attività di ascolto e desideri qualcosa di generico. E diciamo che non vuoi usare l' extendsereditarietà perché potresti già ereditare nella tua classe da un'altra classe. Non sarebbe bello avere un modo generico per rendere collegabile qualsiasi classe senza troppi sforzi ? Ecco come:

<?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;

Nella parte 1, ecco cosa potresti includere con una require_once()chiamata nella parte superiore dello script PHP. Carica le classi per rendere collegabile qualcosa.

Nella parte 2, è qui che si carica una classe. Nota non ho dovuto fare nulla di speciale per la classe, che è significativamente diversa dal modello Observer.

Nella parte 3, è qui che trasformiamo la nostra classe in "innestabile" (ovvero, supporta i plug-in che ci consentono di ignorare i metodi e le proprietà della classe). Ad esempio, se disponi di un'app Web, potresti avere un registro dei plug-in e qui puoi attivare i plug-in. Notare anche la Dog_bark_beforeEvent()funzione. Se avessi impostato $mixed = 'BLOCK_EVENT'prima dell'istruzione return, questo avrebbe impedito al cane di abbaiare e avrebbe anche bloccato Dog_bark_afterEvent perché non ci sarebbero stati eventi.

Nella parte 4, questo è il normale codice operativo, ma nota che ciò che potresti pensare non funzionerebbe affatto. Ad esempio, il cane non annuncia il suo nome come "Fido", ma "Coco". Il cane non dice "miagolio", ma "Woof". E quando vuoi guardare il nome del cane in seguito, scopri che è "Diverso" anziché "Coco". Tutte queste sostituzioni sono state fornite nella Parte 3.

Quindi come funziona? Bene, escludiamo eval()(che tutti dicono sia "malvagio") e escludiamo che non sia un modello di osservatore. Quindi, il modo in cui funziona è la subdola classe vuota chiamata Pluggable, che non contiene i metodi e le proprietà utilizzate dalla classe Dog. Quindi, dato che ciò accade, i metodi magici si impegneranno per noi. Ecco perché nelle parti 3 e 4 pasticciamo con l'oggetto derivato dalla classe Pluggable, non dalla classe Dog stessa. Invece, lasciamo che la classe Plugin faccia il "toccante" sull'oggetto Dog per noi. (Se questo è un tipo di modello di progettazione che non conosco - per favore fatemi sapere.)


3
Non è un decoratore?
MV.

1
Ho letto su Wikipedia su questo e, whoa, hai ragione! :)
Volomike

35

Il metodo hook e listener è il metodo più comunemente usato, ma ci sono altre cose che puoi fare. A seconda delle dimensioni della tua app e di chi consenti di vedere il codice (sarà uno script FOSS o qualcosa in casa) influenzerà notevolmente il modo in cui vuoi consentire i plug-in.

kdeloach ha un buon esempio, ma la sua implementazione e la sua funzione hook sono un po 'pericolose. Ti chiederei di fornire maggiori informazioni sulla natura dell'app php per la tua scrittura e su come vedi che si inseriscono i plugin.

+1 a kdeloach da parte mia.


25

Ecco un approccio che ho usato, è un tentativo di copiare dal meccanismo di segnali / slot Qt, una sorta di modello di Observer. Gli oggetti possono emettere segnali. Ogni segnale ha un ID nel sistema - è composto dall'id del mittente + nome oggetto Ogni segnale può essere associato ai ricevitori, che è semplicemente un "callable" Usi una classe bus per passare i segnali a chiunque sia interessato a riceverli Quando qualcosa succede, "invii" un segnale. Di seguito è riportato un esempio di implementazione

    <?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();

?>

18

Credo che il modo più semplice sarebbe seguire i consigli di Jeff e dare un'occhiata al codice esistente. Prova a guardare Wordpress, Drupal, Joomla e altri noti CMS basati su PHP per vedere come appaiono e si sentono i loro hook API. In questo modo puoi persino ottenere idee a cui potresti non aver pensato in precedenza per rendere le cose un po 'più piene.

Una risposta più diretta sarebbe quella di scrivere file generali che "includere_once" nel loro file fornirebbero l'usabilità di cui avrebbero bisogno. Questo sarebbe suddiviso in categorie e NON fornito in un file MASSIVE "hooks.php". Fai attenzione, perché ciò che finisce per accadere è che i file che includono finiscono per avere sempre più dipendenze e funzionalità migliorate. Cerca di mantenere basse le dipendenze API. Vale a dire meno file da includere.


Aggiungerei DokuWiki all'elenco dei sistemi che potresti dare un'occhiata. Ha un bel sistema di eventi che consente un ricco ecosistema di plugin.
chiborg,

15

C'è un bel progetto chiamato Stickleback di Matt Zandstra presso Yahoo che gestisce gran parte del lavoro per la gestione dei plugin in PHP.

Rafforza l'interfaccia di una classe di plugin, supporta un'interfaccia a riga di comando e non è troppo difficile da avviare e funzionare, specialmente se leggi la copertina su di essa nella rivista dell'architetto PHP .


11

Un buon consiglio è quello di guardare come altri progetti lo hanno fatto. Molti richiedono l'installazione di plug-in e il loro "nome" registrato per i servizi (come Wordpress), in modo da avere "punti" nel codice in cui si chiama una funzione che identifica i listener registrati e li esegue. Un modello di progettazione OO standard è il modello di osservatore , che sarebbe una buona opzione da implementare in un sistema PHP veramente orientato agli oggetti.

Il quadro Zend fa uso di molti metodi di aggancio, ed è molto ben congegnata. Sarebbe un buon sistema da guardare.


8

Sono sorpreso che la maggior parte delle risposte qui sembrano orientate ai plug-in locali per l'applicazione Web, ad esempio plug-in eseguiti sul server Web locale.

E se volessi che i plugin funzionassero su un altro server remoto? Il modo migliore per farlo sarebbe fornire un modulo che ti consenta di definire URL diversi che verrebbero chiamati quando si verificano determinati eventi nella tua applicazione.

Eventi diversi invierebbero informazioni diverse in base all'evento che si è appena verificato.

In questo modo, eseguiresti semplicemente una chiamata cURL all'URL che è stato fornito alla tua applicazione (ad esempio su https) in cui i server remoti possono eseguire attività in base alle informazioni che sono state inviate dalla tua applicazione.

Ciò offre due vantaggi:

  1. Non è necessario ospitare alcun codice sul server locale (sicurezza)
  2. Il codice può essere su server remoti (estensibilità) in lingue diverse da PHP (portabilità)

8
Si tratta più di una "API push" che di un sistema "plug-in": stai fornendo un modo per altri servizi di ricevere notifiche di eventi selezionati. Ciò che generalmente si intende con "plug-in" è che è possibile installare l'applicazione e quindi aggiungere funzionalità per personalizzare il suo comportamento in base ai propri scopi, il che richiede che il plug-in sia eseguito localmente - o almeno abbia una comunicazione bidirezionale sicura ed efficiente per fornire informazioni per l'applicazione non basta prendere da esso. Le due funzionalità sono in qualche modo distinte e in molti casi un "feed" (ad esempio RSS, iCal) è una semplice alternativa a un'API push.
IMSoP
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.