我正在用 PHP 启动一个新的 Web 应用程序,这一次我想创建一些人们可以通过使用插件接口来扩展的东西。

如何将“钩子”写入代码中,以便插件可以附加到特定事件?

有帮助吗?

解决方案

您可以使用观察者模式。实现此目的的简单功能方法:

<?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 文档以获取更多信息。

抱歉,下划线字符似乎被 Markdown 替换为 HTML 实体?当这个错误得到修复时,我可以重新发布这个代码。

编辑:没关系,只有在编辑时才会这样

其他提示

假设您不想要观察者模式,因为它要求您更改类方法来处理侦听任务,并且想要一些通用的东西。假设你不想使用 extends 继承,因为您的类中可能已经从其他类继承了。如果有一种通用的方法来制作不是很好吗 任何类都可以轻松插入?就是这样:

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

在第 1 部分中,您可能会在 require_once() 在 PHP 脚本的顶部调用。它加载类以使某些东西可插入。

在第 2 部分中,我们将在此处加载类。注意我不需要对类做任何特殊的事情,这与观察者模式有很大不同。

在第 3 部分中,我们将类转变为“可插入”(即支持允许我们重写类方法和属性的插件)。因此,举例来说,如果您有一个网络应用程序,您可能有一个插件注册表,并且您可以在此处激活插件。另请注意 Dog_bark_beforeEvent() 功能。如果我设置 $mixed = 'BLOCK_EVENT' 在 return 语句之前,它将阻止狗吠叫,并且还会阻止 Dog_bark_afterEvent,因为不会有任何事件。

在第 4 部分中,这是正常的操作代码,但请注意,您可能认为会运行的代码根本不会像那样运行。例如,狗不会宣布自己的名字为“Fido”,而是“Coco”。狗不会说“喵”,而是“汪”。当你事后想查看狗的名字时,你会发现它是“不同”而不是“可可”。所有这些覆盖均在第 3 部分中提供。

那么这是如何运作的呢?好吧,我们排除一下 eval() (每个人都说这是“邪恶的”)并排除它不是观察者模式。因此,它的工作方式是名为 Pluggable 的偷偷摸摸的空类,它不包含 Dog 类使用的方法和属性。那么,既然如此,魔法手段就会对我们起作用。这就是为什么在第 3 部分和第 4 部分中,我们使用从 Pluggable 类派生的对象,而不是 Dog 类本身。相反,我们让 Plugin 类为我们“触摸”Dog 对象。(如果这是我不知道的某种设计模式——请告诉我。)

听众 方法是最常用的,但您还可以执行其他操作。根据您的应用程序的大小以及您将允许谁查看代码(这将是 FOSS 脚本还是内部的东西)将极大地影响您允许插件的方式。

kdeloach 有一个很好的例子,但是他的实现和钩子函数有点不安全。我会要求你提供更多有关你所写的 php 应用程序性质的信息,以及你如何看待插件的融入。

+1 给我的 kdeloach。

这是我使用过的一种方法,它试图复制 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 依赖性。即他们要包含的文件更少。

有一个很棒的项目叫做 刺鱼 由 Yahoo 的 Matt Zandstra 负责处理 PHP 插件的大部分工作。

它强制执行插件类的接口,支持命令行接口,并且启动和运行并不难 - 特别是如果您在 PHP 架构师杂志.

好的建议是看看其他项目是如何做到的。许多人要求安装插件并为服务注册它们的“名称”(就像 WordPress 所做的那样),这样您的代码中就有了“点”,您可以在其中调用识别已注册侦听器并执行它们的函数。标准的 OO 设计模式是 观察者模式, ,这将是在真正面向对象的 PHP 系统中实现的一个不错的选择。

Zend框架 使用了许多挂钩方法,并且架构非常漂亮。这将是一个值得关注的好系统。

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

如果您希望插件在不同的远程服务器上运行怎么办?做到这一点的最佳方法是提供一个表单,允许您定义不同的 URL,当应用程序中发生特定事件时将调用这些 URL。

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

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

这有两个好处:

  1. 您不必在本地服务器上托管任何代码(安全)
  2. 代码可以以 PHP 以外的不同语言存储在远程服务器上(可扩展性)(可移植性)
许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top