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


Estoy comenzando 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 escribe 'hooks' en su código para que los plugins puedan adjuntarse a eventos específicos?

Author: Abdulla Nilam, 2008-08-01

8 answers

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 sea extensible. He incluido un ejemplo de cómo manejar valores únicos 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 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.

Lo sentimos, parece que los caracteres de subrayado se reemplazan por entidades HTML por Markdown? Puedo volver a publicar este código cuando este error se arregla.

Editar: No importa, solo aparece de esa manera cuando está editando

 151
Author: Kevin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-02-20 11:18:33

Así que digamos que no quieres el patrón de Observador porque requiere que cambies tus métodos de clase para manejar la tarea de escuchar, y quieres algo genérico. Y digamos que no desea usar extends herencia porque ya puede estar heredando en su clase de alguna otra clase. ¿No sería genial tener una forma genérica de hacer cualquier clase conectable sin mucho esfuerzo? He aquí 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 el 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). Así, por ejemplo, si usted tiene una aplicación web, es posible que tenga un registro plugin, y usted podría activar plugins aquí. Observe también la función Dog_bark_beforeEvent(). Si establezco $mixed = 'BLOCK_EVENT' antes de la instrucción return, bloqueará el ladrido del perro 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 observe que lo que podría pensar que se ejecutaría no se ejecuta 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 el nombre del perro después, usted encuentra que es 'Diferente' en lugar de 'Coco'. Todas esas anulaciones se proporcionaron en la parte 3.

Entonces, ¿cómo funciona esto? Bueno, descartemos eval() (que todo el mundo dice que es "malo") y descartemos que no es un patrón de Observador. Por lo tanto, 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, ya que eso ocurre, los métodos mágicos se comprometerán para nosotros. Es por eso que en las partes 3 y 4 nos metemos con el objeto deriva de la clase Conectable, no de la clase Perro en sí. En su lugar, dejamos que la clase Plugin haga el "tocar" en el objeto Perro por nosotros. (Si eso es algún tipo de patrón de diseño que no conozco let por favor hágamelo saber.)

 52
Author: Volomike,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-05-15 16:03:48

El método hook y listener es el más utilizado, pero hay otras cosas que puedes hacer. Dependiendo del tamaño de su aplicación, y que su va a permitir ver el código (esto va a ser un script de software libre, o algo en la casa) influirá en gran medida cómo desea permitir plugins.

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 su escritura, Y cómo ves que los complementos encajan.

+1 a kdeloach de mí.

 33
Author: w-ll,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-08-01 17:23:43

Aquí es un enfoque que he utilizado, es un intento de copiar de Qt señales/mecanismo de ranuras, una especie de patrón de Observador. Los objetos pueden emitir señales. Cada señal tiene un ID en el sistema-está compuesta por el id del remitente + nombre del objeto Cada señal se puede vincular a los receptores, que simplemente es un " callable" Utiliza una clase de autobús para pasar las señales a cualquier persona interesada 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();

?>
 20
Author: andy.gurin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-01-03 13:57:14

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

Una respuesta más directa sería escribir archivos generales que "include_once" en su archivo que proporcionaría la usabilidad que necesitarían. Este sería dividido en categorías y NO en un gigantesco "ganchos.archivo 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 mejora la funcionalidad. Intente mantener bajas las dependencias de la API. Es decir, menos archivos para que incluyan.

 15
Author: helloandre,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-08-01 13:44:35

Hay un buen proyecto llamado Stickleback por Matt Zandstra en Yahoo que maneja gran parte del trabajo para manejar plugins en PHP.

Hace cumplir la interfaz de una clase de plugin, soporta una interfaz de línea de comandos y no es demasiado difícil de poner en marcha - especialmente si lees la historia de portada al respecto en el PHP architect magazine.

 13
Author: julz,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-05-25 21:01:19

Un buen consejo es mirar cómo lo han hecho otros proyectos. Muchos piden tener plugins instalados y su" nombre "registrado para servicios (como lo hace Wordpress) por lo que tiene" puntos " en su código donde llama 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 muchas conexiones métodos, y está muy bien diseñado. Ese sería un buen sistema para mirar.

 10
Author: THEMike,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-09-17 19:38:52

Me sorprende que la mayoría de las respuestas aquí parecen estar orientadas a plugins que son locales a la aplicación web, es decir, plugins 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 hacer esto sería proporcionar un formulario que le permita definir diferentes URL que se llamarían cuando ocurran eventos particulares en su aplicación.

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

De esta manera, solo realizaría una llamada cURL a la URL que se 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 sido enviada por 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 lenguajes distintos a PHP (portabilidad)
 7
Author: Tim Groeneveld,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-04-22 07:41:24