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

276

Estoy iniciando una nueva aplicación web en PHP y esta vez quiero crear algo que la gente pueda ampliar utilizando una interfaz de complemento.

¿Cómo se escribe 'ganchos' en su código para que los complementos se puedan adjuntar a eventos específicos?

Wally Lawless
fuente

Respuestas:

162

Podrías usar un patrón de Observador. Una forma funcional simple de 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;
?>

Salida:

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

Notas:

Para este código fuente de ejemplo, debe declarar todos sus complementos antes del código fuente real que desea que se pueda ampliar. He incluido un ejemplo de cómo manejar valores únicos o múltiples que se pasan al complemento. 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 solo un método para lograr un sistema de complementos en PHP. Hay mejores alternativas, le sugiero que consulte la documentación de WordPress para obtener más información.

Kevin
fuente
3
Tenga en cuenta que para PHP> = 5.0 puede implementar esto utilizando las interfaces Observer / Subject definidas en el SPL: php.net/manual/en/class.splobserver.php
John Carter
20
Nota pedante: este no es un ejemplo del patrón Observador. Es un ejemplo de la Mediator Pattern. Los verdaderos observadores son meramente notificaciones, no hay mensajes que pasan o notificaciones condicionales (ni hay un administrador central para controlar las notificaciones). No tiene la respuesta equivocada , pero debe tenerse en cuenta a las personas dejar de llamar a las cosas por el nombre equivocado ...
ircmaxell
Tenga en cuenta que cuando use múltiples ganchos / escuchas, solo debe devolver cadenas o matrices, no ambas. He implementado algo similar para Hound CMS: getbutterfly.com/hound .
Ciprian
59

Entonces, digamos que no desea el patrón Observador porque requiere que cambie los métodos de su clase para manejar la tarea de escuchar y desee algo genérico. Y supongamos que no desea usar la extendsherencia porque ya puede estar heredando en su clase de 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 puede incluir con una require_once()llamada en la parte superior de su script PHP. Carga las clases para hacer algo conectable.

En la Parte 2, ahí es donde cargamos una clase. Tenga en cuenta que no tuve que hacer nada especial para la clase, que es significativamente diferente al 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, es posible que tenga un registro de complementos, y podría activar los complementos aquí. Observe también la Dog_bark_beforeEvent()función. Si configuro $mixed = 'BLOCK_EVENT'antes de la declaración de devolución, bloqueará al perro para que no 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 tenga en cuenta que lo que podría pensar que se ejecutarí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 desee ver el nombre del perro después, encontrará que es 'Diferente' en lugar de 'Coco'. Todas esas anulaciones se proporcionaron en la Parte 3.

Entonces, ¿cómo funciona esto? Bueno, descartemos eval()(lo que todos dicen que es "malvado") y descartemos que no sea un patrón de Observador. Entonces, la forma en que funciona es la clase vacía disimulada llamada Pluggable, que no contiene los métodos y propiedades utilizados por la clase Dog. Por lo tanto, ya que eso ocurre, los métodos mágicos nos comprometerá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 cambio, dejamos que la clase Plugin haga el "toque" en el objeto Dog por nosotros. (Si ese es algún tipo de patrón de diseño que no conozco, hágamelo saber).

Volomike
fuente
3
¿No es esto un decorador?
MV.
1
Yo leo en la Wikipedia sobre esto y, espera, tienes razón! :)
Volomike
35

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

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

+1 a kdeloach de mi parte.

w-ll
fuente
25

Aquí hay un enfoque que he usado, es un intento de copiar del mecanismo de señales / ranuras Qt, una especie de patrón de Observador. Los objetos pueden emitir señales. Cada señal tiene una ID en el sistema: está compuesta por la identificación del remitente + el nombre del objeto. Cada señal se puede vincular a los receptores, que simplemente es "invocable". Utiliza una clase de bus para transmitir las señales a cualquier persona interesada en recibirlas. sucede, usted "envía" 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();

?>
andy.gurin
fuente
18

Creo que la forma más fácil sería seguir los consejos de Jeff y echar un vistazo al código existente. Intente mirar Wordpress, Drupal, Joomla y otros conocidos CMS basados ​​en PHP para ver cómo se ven y se sienten sus enganches API. De esta manera, incluso puede obtener ideas que quizás no haya pensado anteriormente para hacer que las cosas sean un poco más ricas.

Una respuesta más directa sería escribir archivos generales que "incluirían una vez" en su archivo que proporcionarían la usabilidad que necesitarían. Esto se dividiría en categorías y NO se proporcionaría en un archivo MASIVO "hooks.php". Sin embargo, tenga cuidado, porque lo que termina sucediendo es que los archivos que incluyen terminan teniendo más y más dependencias y funcionalidades mejoradas. Intenta mantener bajas las dependencias de la API. Es decir, menos archivos para incluir.

holaandre
fuente
Añadiría DokuWiki a la lista de sistemas a los que puede echar un vistazo. Tiene un buen sistema de eventos que permite un rico ecosistema de complementos.
chiborg
15

Hay un bonito proyecto llamado Stickleback de Matt Zandstra en Yahoo que maneja gran parte del trabajo para manejar complementos en PHP.

Aplica la interfaz de una clase de complemento, admite una interfaz de línea de comandos y no es demasiado difícil de poner en funcionamiento, especialmente si lees la historia de portada al respecto en la revista de arquitectos PHP .

julz
fuente
11

Un buen consejo es ver cómo lo han hecho otros proyectos. Muchos piden que se instalen complementos y que se registre su "nombre" para los servicios (como Wordpress) para que tenga "puntos" en su código donde llame a una función que identifica a los oyentes registrados y los ejecuta. Un patrón de diseño OO estándar es el Patrón de Observador , que sería una buena opción para implementar en un sistema PHP verdaderamente orientado a objetos.

El Zend Framework hace uso de muchos métodos de enganche, y está muy bien con arquitectura. Ese sería un buen sistema para mirar.

Como
fuente
8

Me sorprende que la mayoría de las respuestas aquí parecen estar orientadas a complementos que son locales para la aplicación web, es decir, complementos que se ejecutan en el servidor web local.

¿Qué pasa si desea que los complementos se ejecuten en un servidor remoto diferente? La mejor manera de hacerlo sería proporcionar un formulario que le permita definir diferentes URL a las que se llamaría cuando ocurrieran eventos particulares en su aplicación.

Diferentes eventos enviarían información diferente según el evento que acaba de ocurrir.

De esta manera, simplemente realizaría una llamada cURL a la URL que se le ha proporcionado a su aplicación (por ejemplo, a través de https) donde los servidores remotos pueden realizar tareas basadas en la información que ha enviado su aplicación.

Esto proporciona dos beneficios:

  1. No tiene que alojar ningún código en su servidor local (seguridad)
  2. El código puede estar en servidores remotos (extensibilidad) en diferentes idiomas además de PHP (portabilidad)
Tim Groeneveld
fuente
8
Esto es más una "API de inserción" que un sistema de "complemento": está proporcionando una forma para que otros servicios reciban notificaciones de eventos seleccionados. Lo que generalmente se entiende por "complementos" es que puede instalar la aplicación y luego agregar funcionalidad para personalizar su comportamiento a sus propósitos, lo que requiere que el complemento se ejecute localmente, o al menos tenga una comunicación bidireccional segura y eficiente para proporcionar la información a la aplicación no solo la toma de ella. Las dos características son algo distintas y, en muchos casos, un "feed" (por ejemplo, RSS, iCal) es una alternativa simple a una API push.
IMSoP