Pergunta

Estou iniciando uma nova aplicação web em PHP e desta vez quero criar algo que as pessoas possam estender usando uma interface de plugin.

Como escrever 'ganchos' em seu código para que os plug-ins possam ser anexados a eventos específicos?

Foi útil?

Solução

Você poderia 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;
?>

Saída:

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 transmitidos ao 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 em PHP.Existem alternativas melhores, sugiro que você consulte a documentação do WordPress para obter mais informações.

Desculpe, parece que os caracteres de sublinhado foram substituídos por entidades HTML por Markdown?Posso postar novamente este código quando o bug for corrigido.

Editar:Deixa pra lá, só aparece assim quando você está editando

Outras dicas

Então, digamos que você não queira o padrão Observer porque ele exige que você altere seus métodos de classe para lidar com a tarefa de escuta e queira algo genérico.E digamos que você não queira usar extends herança porque você já pode estar herdando sua classe de alguma outra classe.Não seria ótimo ter uma maneira genérica de fazer qualquer classe conectável sem muito esforço?Veja 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 é o que você pode incluir em um require_once() chame no topo do seu script PHP.Ele carrega as classes para tornar algo conectável.

Na Parte 2, é onde carregamos uma classe.Observe que não precisei fazer nada de especial na classe, o que é significativamente diferente do padrão Observer.

Na Parte 3, é onde mudamos nossa classe para ser "conectável" (ou seja, suporta plug-ins que nos permitem substituir métodos e propriedades de classe).Então, por exemplo, se você tiver um aplicativo da web, poderá ter um registro de plug-ins e ativar plug-ins aqui.Observe também o Dog_bark_beforeEvent() função.Se eu definir $mixed = 'BLOCK_EVENT' antes da instrução return, ele impedirá o cachorro de latir e também bloquearia 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ê imagina que seria executado não funciona dessa maneira.Por exemplo, o cachorro não anuncia seu nome como ‘Fido’, mas sim como ‘Coco’.O cachorro não diz 'miau', mas sim 'Uau'.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 todos dizem ser "mau") e descartar que não seja um padrão Observer.Então, a forma como funciona é a classe vazia e 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 funcionarão para nós.É por isso que nas partes 3 e 4 mexemos com o objeto derivado da classe Pluggable, não com a classe Dog em si.Em vez disso, deixamos a classe Plugin fazer o "toque" no objeto Dog para nós.(Se isso for algum tipo de padrão de design que eu não conheço - por favor, me avise.)

O gancho e ouvinte método é o mais comumente usado, mas há outras coisas que você pode fazer.Dependendo do tamanho do seu aplicativo e de quem você permitirá ver o código (será um script FOSS ou algo interno) influenciará muito como você deseja permitir plug-ins.

kdeloach tem um bom exemplo, mas sua implementação e função de gancho são um pouco inseguras.Eu pediria que você fornecesse mais informações sobre a natureza do aplicativo php que você escreve e como você vê os plug-ins se encaixando.

+1 para kdeloach de mim.

Aqui está uma abordagem que usei, é uma tentativa de copiar do mecanismo de sinais/slots do Qt, uma espécie de padrão Observer.Objetos podem emitir sinais.Todo sinal tem um ID no sistema - ele é composto pelo nome de objeto ID + do remetente que todos os sinais podem ser bi -lined para os receptores, que simplesmente é um "chamável", você usa uma aula de ônibus para passar os sinais para qualquer pessoa interessada em recebê -los quando algo 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();

?>

Acredito que a maneira mais fácil seria seguir o conselho do próprio 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 seus ganchos de API se parecem.Dessa forma, você pode até ter ideias que talvez não tenha pensado anteriormente para tornar as coisas um pouco mais robustas.

Uma resposta mais direta seria escrever arquivos gerais que eles "incluiriam_once" em seu arquivo, proporcionando a usabilidade necessária.Isso seria dividido em categorias e NÃO fornecido em um arquivo ENORME "hooks.php".Porém, tenha cuidado, porque o que acaba acontecendo é que os arquivos que eles incluem acabam tendo cada vez mais dependências e melhorias de funcionalidade.Tente manter baixas as dependências da API.Ou seja, menos arquivos para eles incluirem.

Há um projeto legal chamado Stickleback por Matt Zandstra do Yahoo que cuida de grande parte do trabalho de manipulação de plug-ins em PHP.

Ele impõe a interface de uma classe de plugin, suporta uma interface de linha de comando e não é muito difícil de instalar e executar - especialmente se você ler a matéria de capa sobre isso no Revista do arquiteto PHP.

Um bom conselho é ver como outros projetos fizeram isso.Muitos pedem para ter plugins instalados e seu "nome" registrado para serviços (como o wordpress faz) 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 Padrão Observador, o que seria uma boa opção para implementar em um sistema PHP verdadeiramente orientado a objetos.

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

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

E se você quisesse que os plug-ins fossem executados em um servidor remoto diferente?A melhor maneira de fazer isso seria fornecer um formulário que permita definir diferentes URLs que seriam chamadas quando eventos específicos ocorrerem em seu aplicativo.

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

Dessa forma, você apenas realizaria uma chamada cURL para a URL que foi fornecida à sua aplicação (por exemplo, através de https), onde servidores remotos podem executar tarefas com base nas informações que foram enviadas pela sua aplicação.

Isso oferece dois benefícios:

  1. Você não precisa hospedar nenhum código em seu servidor local (segurança)
  2. O código pode estar em servidores remotos (extensibilidade) em diferentes linguagens além de PHP (portabilidade)
Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top