允许插入PHP应用程序的最佳方法

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

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

0
额外 编辑
意见: 14

8 答案

你可以使用观察者模式。一个简单的功能方法来实现这一点:

<?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实体?当这个错误得到解决时,我可以重新发布此代码。

编辑:无所谓,编辑时只会出现这种方式

0
额外
迂腐的笔记:这不是观察者模式的例子。这是 调解员模式 的示例。真正的观察者纯粹是通知,没有消息传递或有条件的通知(也没有控制通知的中央管理器)。它不会让答案错误,但应该指出,要阻止人们用错误的名称来称呼事情......
额外 作者 ircmaxell,
请注意,对于PHP> = 5.0,您可以使用SPL中定义的Observer / Subject接口来实现此功能: php.net/manual/en/class.splobserver.php
额外 作者 John Carter,
请注意,在使用多个钩子/侦听器时,您应该只返回字符串或数组,而不是两者。我为Hound CMS实现了类似的功能 - getbutterfly.com/hound
额外 作者 Ciprian,

钩子侦听器方法是最常用的方法,但还有其他一些您可以执行的操作。根据您的应用程序的大小以及您将允许查看代码的人(这是否为FOSS脚本或内部的某些内容)将极大地影响您希望允许插件的方式。

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

从我的kdeloach +1。

0
额外

我相信最简单的方法是遵循Jeff的建议并浏览现有的代码。试着看看Wordpress,Drupal,Joomla和其他着名的基于PHP的CMS,看看它们的API钩子的外观和感觉如何。通过这种方式,你甚至可以得到你以前可能没想过的想法,以使事情变得更加鲁棒。

更直接的答案是编写一般文件,他们将“include_once”放入他们的文件中,以提供他们需要的可用性。这将被分解成类别,而不是在一个MASSIVE“hooks.php”文件中提供。但要小心,因为最终发生的事情是,它们包含的文件最终会导致越来越多的依赖性和功能得到改进。尽量保持API依赖性低。 I.E为他们包括更少的文件。

0
额外
我会将DokuWiki添加到您可能会看到的系统列表中。它有一个很好的事件系统,可以实现丰富的插件生态系统。
额外 作者 chiborg,

所以我们假设你不需要Observer模式,因为它需要你改变你的类方法来处理监听任务,并且需要一些通用的东西。假设您不想使用 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
\n"; } public function sayName() { echo "
\nMy Name is: " . $this->Name . "
\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部分中,您可能会在PHP脚本的顶部使用 require_once()调用包含这些内容。它加载类来创建可插入的类。

在第2部分中,这是我们加载类的地方。注意我不需要为这个类做任何特殊的事情,这与Observer模式有很大不同。

在第3部分中,我们将类切换为“可插入”(即,支持让我们覆盖类方法和属性的插件)。所以,举例来说,如果你有一个网络应用程序,你可能有一个插件注册表,你可以在这里激活插件。还请注意 Dog_bark_beforeEvent()函数。如果我在返回语句之前设置了 $ mixed ='BLOCK_EVENT',它将阻止狗咆哮并阻止Dog_bark_afterEvent,因为不会有任何事件发生。

在第4部分中,这是正常的操作代码,但请注意,您可能会想到的运行方式不会像这样运行。例如,狗没有宣布它的名字为'Fido',而是'Coco'。狗不会说'喵',而是'Woof'。之后当你想看看狗的名字时,你会发现它是'不同'而不是'可可'。所有这些重写都在第3部分中提供。

那么这是如何工作的?那么,让我们排除 eval()(每个人都说“邪恶”),并排除它不是Observer模式。所以,它的工作方式是称为Pluggable的空洞类,它不包含Dog类使用的方法和属性。因此,自从发生这种情况以后,这些神奇的方法将为我们所用。这就是为什么在第3部分和第4部分中,我们会混淆来自Pluggable类的对象,而不是Dog类本身。相反,我们让Plugin类为我们做了Dog对象的“触摸”。 (如果这是我不知道的某种设计模式 - 请让我知道。)

0
额外
阅读维基百科关于这个,哇,你是对的! :)
额外 作者 Volomike,
这不是装饰者吗?
额外 作者 MV.,

这是我用过的一种方法,它试图从Qt信号/插槽机制(一种Observer模式)中复制。 物体可以发射信号。 每个信号在系统中都有一个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();

?>
0
额外

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

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

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

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

这提供了两个好处:

  1. 您不必在本地服务器(安全)上托管任何代码
  2. 除了PHP(可移植性)
  3. 以外,代码可以在不同语言的远程服务器(可扩展性)上
0
额外
与“插件”系统相比,这更像是一个“推送API” - 您正在为其他服务提供接收所选事件通知的方法。 “插件”通常意味着你可以安装应用程序,然后添加功能来自定义其行为以达到你的目的,这需要插件在本地运行 - 或者至少要有安全有效的双向通信来提供信息应用程序不只是把它从中加入。这两个特性有些不同,在很多情况下,“feed”(例如RSS,iCal)是推送API的简单替代方案。
额外 作者 IMSoP,

雅虎的Matt Zandstra提供了一个名为 Stickleback 的整洁项目,该项目处理很多在PHP中处理插件的工作。

它强制实现了插件类的接口,支持命令行界面,并且不会太难以启动和运行 - 尤其是当您在 PHP架构杂志

0
额外

好的建议是看看其他项目如何完成它。许多人呼吁安装插件和他们的“名称”注册服务(如wordpress),所以你在你的代码中有“点”,你可以调用一个函数来标识注册的监听器并执行它们。标准的OO设计模式是观察者模式,这将是一个很好的选择一个真正面向对象的PHP系统。

Zend Framework 利用了许多挂钩方法,并且构建得非常好。这将是一个很好的系统。

0
额外