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

Je commence 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 va-t-on écrire des «hooks» dans son code pour que les plugins puissent s'attacher à des événements spécifiques?

0
ajouté édité
Vues: 18

8 Réponses

Vous pouvez utiliser un pattern Observer. Un moyen fonctionnel simple pour accomplir ceci:

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

Sortie:

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

Notes:

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 la façon de gérer les valeurs uniques ou multiples qui sont passées au plugin. La partie la plus difficile est d'écrire la documentation qui répertorie les arguments transmis à chaque hook.

C'est juste une méthode pour accomplir un système de plugin en PHP. Il existe de meilleures alternatives, je vous suggère de consulter la documentation WordPress pour plus d'informations.

Désolé, il semble que les caractères de soulignement soient remplacés par des entités HTML par Markdown? Je peux re-publier ce code lorsque ce bug est corrigé.

Edit: Nevermind, il ne s'affiche que lorsque vous modifiez

0
ajouté
Note pédante: ce n'est pas un exemple du pattern Observer. C'est un exemple de Motif de médiateur . Les vrais observateurs sont purement une notification, il n'y a pas de transmission de message ou de notification conditionnelle (il n'y a pas de gestionnaire central pour contrôler les notifications). Cela ne rend pas la réponse fausse , mais il faut noter que cela empêche les gens d'appeler les choses par un mauvais nom ...
ajouté l'auteur ircmaxell, source
Notez que pour PHP> = 5.0, vous pouvez implémenter ceci en utilisant les interfaces Observer / Subject définies dans le SPL: php.net/manual/fr/class.splobserver.php
ajouté l'auteur John Carter, source
Notez que lorsque vous utilisez plusieurs hooks / listeners, 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 .
ajouté l'auteur Ciprian, source

La méthode hook et listener est la plus couramment utilisée, mais il y a d'autres choses que vous pouvez faire. Selon la taille de votre application, et qui va permettre de voir le code (est-ce que ce sera un script FOSS, ou quelque chose dans la maison) influencera grandement la façon dont vous souhaitez autoriser les plugins.

kdeloach a un bon exemple, mais sa fonction d'implémentation et de hook est un peu dangereuse. Je voudrais vous demander de donner plus d'informations sur la nature de l'application PHP votre écriture, et comment vous voyez les plugins s'intégrer.

+1 à kdeloach de moi.

0
ajouté

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

Une réponse plus directe serait d'écrire des fichiers généraux qu'ils incluraient "include_once" dans leur fichier, ce qui leur fournirait l'utilisabilité dont ils auraient besoin. Cela serait décomposé en catégories et non fourni dans un fichier MASSIVE "hooks.php". Soyez prudent 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 API faibles. I.E moins de fichiers à inclure.

0
ajouté
J'ajouterais DokuWiki à la liste des systèmes que vous pouvez consulter. Il a un système d'événement agréable qui permet un riche écosystème de plugin.
ajouté l'auteur chiborg, source

Donc, disons que vous ne voulez pas le pattern Observer car cela nécessite de changer vos méthodes de classe pour gérer la tâche d'écoute et de vouloir quelque chose de générique. Et disons que vous ne voulez pas utiliser l'héritage extends , car il se peut que vous héritiez déjà dans votre classe d'une autre classe. Ne serait-ce pas génial d'avoir un moyen générique de rendre n'importe quelle classe connectable sans trop d'efforts ? 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
\n"; } public function sayName() { echo "
\nMy Name is: " . $this->Name . "
\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 appel à require_once() en haut de votre script PHP. Il charge les classes pour rendre quelque chose connectable.

Dans la partie 2, c'est là que nous chargeons une classe. Remarque: Je n'ai pas eu à faire quelque chose de spécial à la classe, ce qui est sensiblement différent du modèle Observer.

Dans la troisième partie, c'est là que nous transformons notre classe en «pluggable» (c'est-à-dire, supporte les plugins qui nous permettent de surcharger les méthodes et propriétés de classe). Ainsi, par exemple, si vous avez une application web, vous pouvez avoir un registre de plugin, et vous pouvez activer des plugins ici. Notez également la fonction Dog_bark_beforeEvent() . Si je mets $ mixed = 'BLOCK_EVENT' avant l'instruction return, cela empêchera le chien d'aboyer et bloquerait aussi Dog_bark_afterEvent car il n'y aurait pas d'évènement.

Dans la partie 4, c'est le code d'opération normal, mais remarquez que ce que vous pourriez penser ne fonctionnerait pas du tout. 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 après, vous trouvez qu'il est «différent» au lieu de «Coco». Toutes ces dérogations ont été fournies dans la partie 3.

Alors, comment ça marche? Eh bien, excluons eval() (que tout le monde dit être "mauvais") et excluons que ce ne soit pas un pattern Observer. 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 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 sais pas - s'il vous plaît faites le moi savoir.)

0
ajouté
Je lire sur Wikipedia à ce sujet et, whoa, vous avez raison! :)
ajouté l'auteur Volomike, source
N'est-ce pas un décorateur?
ajouté l'auteur MV., source

Voici une approche que j'ai utilisée, c'est une tentative de copier à partir du mécanisme des signaux / slots Qt, une sorte de pattern Observer. Les objets peuvent émettre des signaux. Chaque signal a un ID dans le système - il est composé par l'identifiant de l'expéditeur + le nom de l'objet Chaque signal peut être lié aux récepteurs, ce 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 est et exemple de mise en œuvre

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

?>
0
ajouté

Je suis surpris que la plupart des réponses semblent être axées sur les plugins qui sont 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 fonctionnent 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 effectuerez simplement un appel cURL à l'URL qui a été fournie à votre application (par exemple sur https) où les serveurs distants peuvent effectuer des tâches en fonction des informations qui ont été envoyées par votre application.

Cela fournit deux avantages:

  1. Vous n'avez pas besoin d'héberger de code sur votre serveur local (sécurité)
  2. Le code peut être sur des serveurs distants (extensibilité) dans d'autres langages que PHP (portabilité)
0
ajouté
Il s'agit plus d'une «API push» que d'un système «plugin» - vous fournissez un moyen pour les autres services de recevoir une notification des événements sélectionnés. Ce que l'on entend généralement par "plugins", c'est que vous pouvez installer l'application, puis ajouter des fonctionnalités pour personnaliser son comportement, ce qui nécessite que le plugin fonctionne localement - ou au moins dispose d'une communication bidirectionnelle sécurisée et efficace informations à l'application ne pas simplement le prendre de . Les deux caractéristiques sont quelque peu distincte
ajouté l'auteur IMSoP, source

Il y a un projet intéressant appelé Stickleback par Matt Zandstra à Yahoo qui gère une grande partie du travail pour la gestion des plugins en PHP.

Il renforce l'interface d'une classe de plugin, supporte une interface de ligne de commande et n'est pas trop difficile à mettre en place - surtout si vous lisez l'histoire de la couverture dans le magazine d'architecture PHP .

0
ajouté

Un bon conseil est de regarder 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), donc vous avez 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 d'observateur , qui serait une bonne option à implémenter dans un système PHP orienté objet.

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

0
ajouté