Melhor maneira de permitir plugins para um aplicativo PHP

276

Estou iniciando um novo aplicativo Web em PHP e desta vez quero criar algo que as pessoas possam estender usando uma interface de plug-in.

Como alguém escreve 'hooks' em seu código para que plugins possam se conectar a eventos específicos?

Wally Lawless
fonte

Respostas:

162

Você pode usar um padrão Observer. Uma maneira funcional simples de fazer isso:

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

Resultado:

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

Notas:

Para este exemplo de código-fonte, você deve declarar todos os seus plug-ins antes do código-fonte real que deseja que seja extensível. Incluí um exemplo de como lidar com valores únicos ou múltiplos sendo passados ​​para o plug-in. A parte mais difícil disso é escrever a documentação real, que lista quais argumentos são passados ​​para cada gancho.

Este é apenas um método de realizar um sistema de plugins no PHP. Existem alternativas melhores, sugiro que você verifique a documentação do WordPress para obter mais informações.

Kevin
fonte
3
Observe que, para PHP> = 5.0, você pode implementá-lo usando as interfaces Observer / Subject definidas na SPL: php.net/manual/en/class.splobserver.php
John Carter
20
Nota pedante: este não é um exemplo do padrão Observador. É um exemplo do Mediator Pattern. Os verdadeiros observadores são puramente notificações, não há passagem de mensagens ou notificação condicional (nem um gerente central para controlar as notificações). Não faz a resposta errada , mas deve-se observar a parada pessoas chamando as coisas pelo nome errado ...
ircmaxell
Observe que, ao usar vários ganchos / ouvintes, você deve retornar apenas cadeias de caracteres ou matrizes, não as duas. Eu implementei algo semelhante para o Hound CMS - getbutterfly.com/hound .
Ciprian
59

Então, digamos que você não queira o padrão Observer, pois exige que você altere seus métodos de classe para lidar com a tarefa de escutar e queira algo genérico. E digamos que você não queira usar extendsherança, porque você já pode estar herdando sua classe de alguma outra classe. Não seria ótimo ter uma maneira genérica de tornar qualquer classe conectável sem muito esforço ? Aqui está como:

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

Na Parte 1, é isso que você pode incluir com uma require_once()chamada na parte superior do seu script PHP. Carrega as classes para tornar algo plugável.

Na parte 2, é aí que carregamos uma classe. Observe que não precisei fazer nada de especial na classe, o que é significativamente diferente do padrão Observador.

Na Parte 3, é aí que mudamos nossa classe para "plugável" (ou seja, suporta plugins que nos permitem substituir métodos e propriedades de classe). Portanto, por exemplo, se você possui um aplicativo da web, pode ter um registro de plug-in e ativar plug-ins aqui. Observe também a Dog_bark_beforeEvent()função. Se eu definir $mixed = 'BLOCK_EVENT'antes da declaração de retorno, ele impedirá o cão de latir e também bloqueará o Dog_bark_afterEvent porque não haveria nenhum evento.

Na Parte 4, esse é o código de operação normal, mas observe que o que você poderia pensar que seria executado não é assim. Por exemplo, o cão não anuncia seu nome como 'Fido', mas 'Coco'. O cão não diz 'miau', mas 'Woof'. E quando você quiser olhar o nome do cachorro depois, descobrirá que é 'Diferente' em vez de 'Coco'. Todas essas substituições foram fornecidas na Parte 3.

Então, como isso funciona? Bem, vamos descartar eval()(que todo mundo diz que é "mau") e descartar que não é um padrão de Observador. Portanto, a maneira como funciona é a classe vazia sorrateira chamada Pluggable, que não contém os métodos e propriedades usados ​​pela classe Dog. Assim, desde que isso ocorra, os métodos mágicos se envolverão para nós. É por isso que nas partes 3 e 4 mexemos no objeto derivado da classe Pluggable, não na classe Dog. Em vez disso, deixamos a classe Plugin fazer o "toque" no objeto Dog para nós. (Se esse é um tipo de padrão de design que eu não conheço - entre em contato.)

Volomike
fonte
3
Não é um decorador?
MV.
1
Eu li na Wikipedia sobre isso e, whoa, você está certo! :)
Volomike
35

O método hook e listener é o mais usado, mas há outras coisas que você pode fazer. Dependendo do tamanho do seu aplicativo e de quem você permitirá que veja o código (este será um script FOSS ou algo interno) influenciará bastante a maneira como você deseja permitir plug-ins.

O kdeloach tem um bom exemplo, mas sua implementação e função de gancho são um pouco inseguras. Eu pediria que você desse mais informações sobre a natureza do aplicativo php e como você vê os plugins adequados.

+1 para kdeloach de mim.

w-ll
fonte
25

Aqui está uma abordagem que eu usei, é uma tentativa de copiar do mecanismo de sinais / slots Qt, um tipo de padrão Observer. Objetos podem emitir sinais. Todo sinal tem um ID no sistema - é composto pelo id do remetente + nome do objeto. Todo sinal pode ser ligado aos receptores, o que é simplesmente "callable". Você usa uma classe de barramento para transmitir os sinais a qualquer pessoa interessada em recebê-los. acontece, você "envia" um sinal. Abaixo está um exemplo de implementação

    <?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
fonte
18

Acredito que a maneira mais fácil seria seguir os conselhos de Jeff e dar uma olhada no código existente. Tente olhar para Wordpress, Drupal, Joomla e outros CMSs baseados em PHP bem conhecidos para ver como suas APIs se parecem. Dessa forma, você pode até ter idéias que talvez não tenha pensado anteriormente para tornar as coisas um pouco mais irritadas.

Uma resposta mais direta seria escrever arquivos gerais que eles "incluiriam" em seu arquivo que forneceriam a usabilidade de que precisariam. Isso seria dividido em categorias e NÃO fornecido em um arquivo "hooks.php" MASSIVO. Porém, tenha cuidado, porque o que acaba acontecendo é que os arquivos que eles incluem acabam tendo mais e mais dependências e funcionalidades. Tente manter baixas as dependências da API. IE menos arquivos para incluir.

helloandre
fonte
Eu adicionaria o DokuWiki à lista de sistemas que você pode dar uma olhada. Ele possui um sistema de eventos agradável que permite um rico ecossistema de plugins.
chiborg
15

Existe um projeto interessante chamado Stickleback, de Matt Zandstra, no Yahoo, que lida com grande parte do trabalho de manipulação de plug-ins no PHP.

Ele reforça a interface de uma classe de plug-in, suporta uma interface de linha de comando e não é muito difícil de instalar - especialmente se você ler a matéria de capa sobre ela na revista de arquitetos PHP .

julz
fonte
11

Um bom conselho é verificar como outros projetos o fizeram. Muitos pedem a instalação de plug-ins e seu "nome" registrado para serviços (como o wordpress), para que você tenha "pontos" em seu código, onde você chama uma função que identifica os ouvintes registrados e os executa. Um padrão de design OO padrão é o Observer Pattern , que seria uma boa opção para implementar em um sistema PHP verdadeiramente orientado a objetos.

O Zend Framework faz uso de muitos métodos de conexão e é muito bem arquitetado. Esse seria um bom sistema para se olhar.

THEMike
fonte
8

Estou surpreso que a maioria das respostas aqui pareçam ser voltadas para plug-ins locais ao aplicativo da web, ou seja, plugins executados no servidor da web local.

E se você quiser que os plugins sejam executados em um servidor remoto diferente? A melhor maneira de fazer isso seria fornecer um formulário que permita definir diferentes URLs que seriam chamados quando eventos específicos ocorrerem no seu aplicativo.

Eventos diferentes enviariam informações diferentes com base no evento que acabou de ocorrer.

Dessa forma, você apenas executaria uma chamada cURL para a URL que foi fornecida ao seu aplicativo (por exemplo, através de https), na qual os servidores remotos podem executar tarefas com base nas informações enviadas pelo seu aplicativo.

Isso oferece dois benefícios:

  1. Você não precisa hospedar nenhum código no servidor local (segurança)
  2. O código pode estar em servidores remotos (extensibilidade) em diferentes idiomas, além do PHP (portabilidade)
Tim Groeneveld
fonte
8
Isso é mais uma "API de envio" do que um sistema de "plug-in" - você está fornecendo uma maneira para outros serviços receberem notificações de eventos selecionados. O que geralmente se entende por "plug-ins" é que você pode instalar o aplicativo e adicionar funcionalidades para personalizar seu comportamento para seus propósitos, o que exige que o plug-in seja executado localmente - ou pelo menos tenha uma comunicação bidirecional segura e eficiente para fornecer informações para o aplicativo não basta levá-la a partir dele. Os dois recursos são um pouco distintos e, em muitos casos, um "feed" (por exemplo, RSS, iCal) é uma alternativa simples a uma API push.
IMSOP