我开始一个新的web应用程序在PHP和这一次,我想创建一些东西,人们可以通过使用插件接口扩展。

如何在他们的代码中编写“钩子”,以便插件可以附加到特定的事件?


当前回答

这是我使用的一种方法,它试图复制Qt信号/槽机制,一种观察者模式。 物体可以发射信号。 每个信号在系统中都有一个ID——它由发送者的ID +对象名称组成 每个信号都可以绑定到接收器,这就是一个“可调用的”。 您使用总线类将信号传递给任何有兴趣接收它们的人 当某事发生时,你“发送”一个信号。 下面是一个示例实现

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

?>

其他回答

我相信最简单的方法是遵循Jeff自己的建议,查看一下现有的代码。试着看看WordPress、Drupal、Joomla和其他知名的基于php的CMS,看看它们的API挂钩的外观和感觉如何。通过这种方式,您甚至可以获得您以前可能没有想到的想法,从而使事情变得更加健壮。

一个更直接的答案是将通用文件“include_once”写入文件,以提供所需的可用性。这将被分解成类别,而不是在一个巨大的“hooks.php”文件中提供。但是要小心,因为最终发生的情况是,它们包含的文件最终具有越来越多的依赖关系,功能也得到了改进。尽量保持较低的API依赖性。也就是要包含更少的文件。

这是我使用的一种方法,它试图复制Qt信号/槽机制,一种观察者模式。 物体可以发射信号。 每个信号在系统中都有一个ID——它由发送者的ID +对象名称组成 每个信号都可以绑定到接收器,这就是一个“可调用的”。 您使用总线类将信号传递给任何有兴趣接收它们的人 当某事发生时,你“发送”一个信号。 下面是一个示例实现

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

?>

我很惊讶,这里的大多数答案似乎都是关于web应用程序的本地插件,即运行在本地web服务器上的插件。

如果你想让插件在另一个远程服务器上运行呢?最好的方法是提供一个表单,允许您定义不同的url,当应用程序中发生特定事件时将调用这些url。

不同的事件会根据刚刚发生的事件发送不同的信息。

这样,您只需对提供给应用程序的URL(例如通过https)执行cURL调用,远程服务器可以根据应用程序发送的信息执行任务。

这提供了两个好处:

您不必在本地服务器上托管任何代码(安全性) 代码可以在远程服务器上(可扩展性),使用PHP以外的不同语言(可移植性)

您可以使用观察者模式。一个简单的功能方法来完成这个:

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

输出:

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

注:

对于这个示例源代码,您必须在您想要扩展的实际源代码之前声明所有的插件。我已经包含了一个如何处理传递给插件的单个或多个值的示例。其中最困难的部分是编写实际的文档,列出传递给每个钩子的参数。

这只是在PHP中实现插件系统的一种方法。有更好的选择,我建议你查看WordPress文档以获得更多信息。

钩子和监听器方法是最常用的方法,但您还可以做其他事情。取决于你的应用程序的大小,以及你允许谁看到代码(这将是一个自由/开源软件脚本,还是内部的东西)将极大地影响你想要如何允许插件。

Kdeloach有一个很好的例子,但是他的实现和钩子函数有点不安全。我想请你给更多的信息,php应用程序的性质,你写的,以及你如何看待插件适合。

+1给我。