La mejor manera de permitir plugins para una aplicación PHP

php plugins architecture hook


Estoy empezando una nueva aplicación web en PHP y esta vez quiero crear algo que la gente pueda extender usando una interfaz de plugin.

¿Cómo se puede escribir "ganchos" en su código para que los plugins se puedan adjuntar a eventos específicos?




Answer 1 Kevin


Podrías usar un patrón de Observador.Una forma funcional simple para lograr esto:

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

Output:

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

Notes:

Para este código fuente de ejemplo,debes declarar todos tus plugins antes del código fuente real que quieres que sea extensible.He incluido un ejemplo de cómo manejar valores simples o múltiples que se pasan al plugin.La parte más difícil de esto es escribir la documentación real que enumera qué argumentos se pasan a cada gancho.

Este es sólo un método para lograr un sistema de plugins en PHP.Hay mejores alternativas,te sugiero que revises la documentación de WordPress para más información.




Answer 2 Volomike


Entonces, digamos que no desea el patrón Observer porque requiere que cambie los métodos de su clase para manejar la tarea de escuchar y desea algo genérico. Y digamos que no desea utilizar la herencia extends porque es posible que ya esté heredando en su clase de alguna otra clase. ¿No sería genial tener una forma genérica de hacer que cualquier clase se pueda conectar sin mucho esfuerzo ? Así es cómo:

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

En la Parte 1, eso es lo que podría incluir con una llamada require_once() en la parte superior de su script PHP. Carga las clases para hacer algo conectable.

En la segunda parte,ahí es donde cargamos una clase.Note que no tuve que hacer nada especial a la clase,que es significativamente diferente del patrón del Observador.

En la Parte 3, ahí es donde cambiamos nuestra clase para que sea "conectable" (es decir, admite complementos que nos permiten anular los métodos y propiedades de la clase). Entonces, por ejemplo, si tiene una aplicación web, puede tener un registro de complementos y puede activarlos aquí. Observe también la función Dog_bark_beforeEvent() . Si configuro $mixed = 'BLOCK_EVENT' antes de la declaración de devolución, evitará que el perro ladre y también bloqueará el Dog_bark_afterEvent porque no habría ningún evento.

En la parte 4,ese es el código de operación normal,pero noten que lo que podrían pensar que funcionaría no funciona así en absoluto.Por ejemplo,el perro no anuncia su nombre como "Fido",sino "Coco".El perro no dice "miau",sino "guau".Y cuando quieres mirar el nombre del perro después,te das cuenta de que es "Diferente" en lugar de "Coco".Todas esas anulaciones fueron proporcionadas en la parte 3.

Entonces, ¿cómo funciona esto? Bueno, eval() (que todos dicen que es "malvado") y descartemos que no sea un patrón de Observer. Entonces, la forma en que funciona es la clase vacía furtiva llamada Pluggable, que no contiene los métodos y propiedades utilizados por la clase Dog. Por lo tanto, dado que eso ocurre, los métodos mágicos nos ayudarán. Es por eso que en las partes 3 y 4 nos metemos con el objeto derivado de la clase Pluggable, no con la clase Dog en sí. En su lugar, dejamos que la clase Plugin "toque" el objeto Dog por nosotros. (Si se trata de algún tipo de patrón de diseño que no conozca, hágamelo saber).




Answer 3 w-ll


El método de gancho y escucha es el más utilizado, pero hay otras cosas que puede hacer. Dependiendo del tamaño de su aplicación y de a quién va a permitir que vea el código (será un script de FOSS o algo interno) influirá en gran medida en cómo desea permitir los complementos.

kdeloach tiene un buen ejemplo,pero su implementación y función de gancho es un poco insegura.Le pediría que diera más información de la naturaleza de la aplicación php que escribe,y cómo ve que los plugins encajan.

+1 para kdeloach de mí.




Answer 4 andy.gurin


Este es un enfoque que he utilizado,es un intento de copiar de las señales Qt/mecanismo de ranuras,una especie de patrón de Observación.Los objetos pueden emitir señales.Cada señal tiene un ID en el sistema-está compuesto por el ID del emisor+el nombre del objeto Cada señal puede ser ligada a los receptores,lo que simplemente es un "llamable" Utilizas una clase de bus para pasar las señales a cualquiera interesado en recibirlas Cuando algo sucede,"envías" una señal.A continuación se muestra un ejemplo de implementación

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

?>



Answer 5 helloandre


Creo que la forma más fácil sería seguir el consejo de Jeff y echar un vistazo al código existente.Intenta mirar Wordpress,Drupal,Joomla y otros conocidos CMS basados en PHP para ver cómo se ven y se sienten sus ganchos API.De esta manera puedes incluso obtener ideas que no habías pensado antes para hacer las cosas un poco más borrosas.

Una respuesta más directa sería escribir archivos generales que "incluyan_una vez" en su archivo y que proporcionen la usabilidad que necesitan.Esto se dividiría en categorías y NO se proporcionaría en un archivo MASIVO "hooks.php".Pero ten cuidado,porque lo que acaba pasando es que los archivos que incluyen acaban teniendo cada vez más dependencias y la funcionalidad mejora.Intente mantener bajas las dependencias de la API.Es decir,menos archivos para que se incluyan.