Meilleure façon d'autoriser les plugins pour une application PHP

276

Je démarre une nouvelle application web en PHP et cette fois je veux créer quelque chose que les gens peuvent étendre en utilisant une interface de plugin.

Comment peut-on écrire des "hooks" dans leur code afin que les plugins puissent se rattacher à des événements spécifiques?

Wally Lawless
la source

Réponses:

162

Vous pouvez utiliser un modèle Observer. Un moyen fonctionnel simple pour y parvenir:

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

Production:

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

Remarques:

Pour cet exemple de code source, vous devez déclarer tous vos plugins avant le code source réel que vous souhaitez étendre. J'ai inclus un exemple de gestion des valeurs uniques ou multiples transmises au plugin. La partie la plus difficile est d'écrire la documentation réelle qui répertorie les arguments transmis à chaque hook.

Ce n'est qu'une méthode pour réaliser un système de plugins en PHP. Il existe de meilleures alternatives, je vous suggère de consulter la documentation WordPress pour plus d'informations.

Kevin
la source
3
Notez que pour PHP> = 5.0, vous pouvez implémenter cela en utilisant les interfaces Observer / Subject définies dans le SPL: php.net/manual/en/class.splobserver.php
John Carter
20
Note pédante: ce n'est pas un exemple du modèle Observer. C'est un exemple du Mediator Pattern. Les vrais observateurs sont purement des notifications, il n'y a pas de passage de message ou de notification conditionnelle (ni de gestionnaire central pour contrôler les notifications). Cela ne rend pas la réponse fausse , mais il convient de noter pour empêcher les gens d'appeler des choses par le mauvais nom ...
ircmaxell
Notez que lorsque vous utilisez plusieurs hooks / écouteurs, vous ne devez renvoyer que des chaînes ou des tableaux, pas les deux. J'ai implémenté quelque chose de similaire pour Hound CMS - getbutterfly.com/hound .
Ciprian
59

Supposons donc que vous ne vouliez pas du modèle Observer car il nécessite que vous modifiiez vos méthodes de classe pour gérer la tâche d'écoute et que vous vouliez quelque chose de générique. Et disons que vous ne voulez pas utiliser l' extendshéritage parce que vous héritez peut-être déjà dans votre classe d'une autre classe. Ne serait-il pas formidable d'avoir un moyen générique de rendre n'importe quelle classe enfichable sans trop d'effort ? Voici comment:

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

Dans la partie 1, c'est ce que vous pourriez inclure avec un require_once()appel en haut de votre script PHP. Il charge les classes pour rendre quelque chose enfichable.

Dans la partie 2, c'est là que nous chargeons une classe. Notez que je n'ai rien eu à faire de spécial pour la classe, ce qui est très différent du modèle Observer.

Dans la partie 3, c'est là que nous basculons notre classe pour qu'elle soit "enfichable" (c'est-à-dire, supporte les plugins qui nous permettent de remplacer les méthodes et les propriétés de la classe). Ainsi, par exemple, si vous avez une application Web, vous pouvez avoir un registre de plugins et vous pouvez activer les plugins ici. Remarquez également la Dog_bark_beforeEvent()fonction. Si je mets $mixed = 'BLOCK_EVENT'avant la déclaration de retour, cela empêchera le chien d'aboyer et bloquerait également Dog_bark_afterEvent car il n'y aurait aucun événement.

Dans la partie 4, c'est le code de fonctionnement normal, mais notez que ce que vous pourriez penser ne fonctionnerait pas du tout comme ça. Par exemple, le chien n'annonce pas son nom comme «Fido», mais «Coco». Le chien ne dit pas «miaou», mais «woof». Et quand vous voulez regarder le nom du chien par la suite, vous trouvez qu'il est «différent» au lieu de «Coco». Tous ces remplacements ont été fournis dans la partie 3.

Donc comment ça fonctionne? Eh bien, excluons eval()(ce que tout le monde dit être "diabolique") et excluons que ce n'est pas un modèle Observateur. Ainsi, la façon dont cela fonctionne est la classe vide sournoise appelée Pluggable, qui ne contient pas les méthodes et les propriétés utilisées par la classe Dog. Ainsi, puisque cela se produit, les méthodes magiques vont s'engager pour nous. C'est pourquoi dans les parties 3 et 4, nous gâchons avec l'objet dérivé de la classe Pluggable, pas la classe Dog elle-même. Au lieu de cela, nous laissons la classe Plugin faire le "toucher" sur l'objet Dog pour nous. (Si c'est une sorte de modèle de conception que je ne connais pas - veuillez me le faire savoir.)

Volomike
la source
3
N'est-ce pas un décorateur?
MV.
1
J'ai lu sur Wikipédia à ce sujet et, whoa, vous avez raison! :)
Volomike
35

La méthode du hook et de l' écouteur est la plus utilisée, mais vous pouvez faire d'autres choses. Selon la taille de votre application et qui vous autoriserez à voir le code (est-ce que ce sera un script FOSS, ou quelque chose en interne) influencera grandement la façon dont vous voulez autoriser les plugins.

kdeloach a un bel exemple, mais sa mise en œuvre et sa fonction de hook sont un peu dangereuses. Je vous demanderais de donner plus d'informations sur la nature de l'application php dans votre écriture, et comment vous voyez les plugins s'intégrer.

+1 à kdeloach de ma part.

w-ll
la source
25

Voici une approche que j'ai utilisée, c'est une tentative de copier du mécanisme de signaux / slots Qt, une sorte de modèle Observer. Les objets peuvent émettre des signaux. Chaque signal a un ID dans le système - il est composé de l'identifiant de l'expéditeur + nom de l'objet Chaque signal peut être lié aux récepteurs, qui est simplement un "appelable" Vous utilisez une classe de bus pour transmettre les signaux à toute personne intéressée à les recevoir Quand quelque chose arrive, vous "envoyez" un signal. Ci-dessous et exemple d'implémentation

    <?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
la source
18

Je crois que le moyen le plus simple serait de suivre les propres conseils de Jeff et de jeter un œil au code existant. Essayez de regarder Wordpress, Drupal, Joomla et d'autres CMS bien connus basés sur PHP pour voir à quoi ressemblent leurs hooks API. De cette façon, vous pouvez même obtenir des idées auxquelles vous n'aviez pas pensé auparavant pour rendre les choses un peu plus rubustes.

Une réponse plus directe serait d'écrire des fichiers généraux qu'ils «incluraient» une fois dans leur fichier pour fournir l'utilisabilité dont ils auraient besoin. Cela serait divisé en catégories et NON fourni dans un fichier MASSIVE "hooks.php". Attention cependant, car ce qui finit par arriver, c'est que les fichiers qu'ils contiennent finissent par avoir de plus en plus de dépendances et de fonctionnalités. Essayez de garder les dépendances de l'API faibles. IE moins de fichiers à inclure.

bonjour
la source
J'ajouterais DokuWiki à la liste des systèmes que vous pourriez consulter. Il a un système d'événement agréable qui permet un riche écosystème de plugins.
chiborg
15

Il y a un projet soigné appelé Stickleback par Matt Zandstra chez Yahoo qui gère une grande partie du travail de gestion des plugins en PHP.

Il applique l'interface d'une classe de plug-in, prend en charge une interface de ligne de commande et n'est pas trop difficile à mettre en place - en particulier si vous lisez l'histoire de couverture à ce sujet dans le magazine PHP architect .

julz
la source
11

Un bon conseil est de voir comment d'autres projets l'ont fait. Beaucoup demandent que les plugins soient installés et que leur "nom" soit enregistré pour les services (comme wordpress le fait), vous avez donc des "points" dans votre code où vous appelez une fonction qui identifie les écouteurs enregistrés et les exécute. Un modèle de conception OO standard est le modèle Observer , qui serait une bonne option à implémenter dans un système PHP véritablement orienté objet.

Le Zend Framework utilise de nombreuses méthodes de hook et est très bien conçu. Ce serait un bon système à examiner.

THEMike
la source
8

Je suis surpris que la plupart des réponses ici semblent être axées sur les plugins locaux à l'application Web, c'est-à-dire les plugins qui s'exécutent sur le serveur Web local.

Et si vous vouliez que les plugins s'exécutent sur un autre serveur distant? La meilleure façon de le faire serait de fournir un formulaire qui vous permet de définir différentes URL qui seraient appelées lorsque des événements particuliers se produisent dans votre application.

Différents événements enverraient des informations différentes en fonction de l'événement qui vient de se produire.

De cette façon, vous effectueriez simplement un appel cURL à l'URL qui a été fournie à votre application (par exemple via https) où les serveurs distants peuvent effectuer des tâches en fonction des informations qui ont été envoyées par votre application.

Cela offre deux avantages:

  1. Vous n'avez pas à héberger de code sur votre serveur local (sécurité)
  2. Le code peut être sur des serveurs distants (extensibilité) dans différentes langues autres que PHP (portabilité)
Tim Groeneveld
la source
8
Il s'agit plus d'une «API push» que d'un système de «plugin» - vous fournissez un moyen pour d'autres services de recevoir des notifications d'événements sélectionnés. Ce que l'on entend généralement par "plug-ins", c'est que vous pouvez installer l'application, puis ajouter des fonctionnalités pour personnaliser son comportement à vos fins, ce qui nécessite que le plug-in s'exécute localement - ou au moins avoir une communication bidirectionnelle sécurisée et efficace à fournir informations à l'application non seulement prendre de lui. Les deux fonctionnalités sont quelque peu distinctes et, dans de nombreux cas, un "flux" (par exemple RSS, iCal) est une alternative simple à une API push.
IMSoP