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

Wally Lawless 259 php architecture hook plugins

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

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

Kevin.. 156

您可以使用Observer模式.一种简单的功能方法来完成此任务:

<?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实体?当修复此错误时,我可以重新发布此代码.

编辑:没关系,它只在你编辑时出现

  • 迂腐:这不是观察者模式的一个例子.这是[`Mediator Pattern`]的一个例子(http://sourcemaking.com/design_patterns/mediator).真正的观察者纯粹是通知,没有消息传递或有条件通知(也没有用于控制通知的中央管理器).它没有使答案*错误*,但应该注意阻止人们用错误的名字呼叫事物...... (20认同)
  • 请注意,对于PHP> = 5.0,您可以使用SPL中定义的Observer/Subject接口实现此目的:http://www.php.net/manual/en/class.splobserver.php (3认同)

Volomike.. 55

因此,假设您不需要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<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部分中,这是我们加载类的地方.注意我没有必要对该类做任何特殊操作,这与Observer模式有很大不同.

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

在第4部分中,这是正常的操作代码,但请注意,您可能认为运行的代码根本不会运行.例如,狗不会将它的名字称为'Fido',而是'Coco'.狗不会说'喵',而是'Woof'.当你想要看看狗的名字之后,你发现它是'不同'而不是'可可'.所有这些覆盖都在第3部分中提供.

那么这是如何工作的呢?好吧,让我们排除eval()(每个人都说是"邪恶的"),并排除它不是观察者模式.因此,它的工作方式是名为Pluggable的偷偷摸摸的空类,它不包含Dog类使用的方法和属性.因此,既然发生了这种情况,神奇的方法就会为我们所吸引.这就是为什么在第3和第4部分中我们混淆了从Pluggable类派生的对象,而不是Dog类本身.相反,我们让Plugin类为我们做Dog对象的"触摸".(如果这是我不知道的某种设计模式 - 请告诉我.)

  • 这不是装饰师吗? (3认同)

w-ll.. 34

监听的方法是最常用的,但也有其他事情可以做.根据应用程序的大小,以及允许查看代码的人(这将是一个FOSS脚本,或者内部的某些内容)将极大地影响您希望如何允许插件.

kdeloach有一个很好的例子,但他的实现和钩子函数有点不安全.我想请你提供更多关于php app性质的信息,以及你如何看待插件.

+1来自我的kdeloach.


用户甲.. 22

这是我使用的一种方法,它试图从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();

?>


helloandre.. 17

我认为最简单的方法是遵循Jeff自己的建议并查看现有代码.尝试查看Wordpress,Drupal,Joomla和其他众所周知的基于PHP的CMS,看看它们的API挂钩的外观和感觉.通过这种方式,您甚至可以获得以前未曾想过的想法,以使事情变得更加糟糕.

更直接的答案是将一般文件写入他们将"include_once"到他们的文件中,这将提供他们需要的可用性.这将被分解为类别,而不是在一个MASSIVE"hooks.php"文件中提供.但要小心,因为最终发生的事情是它们包含的文件最终会有越来越多的依赖关系和功能.尽量保持API依赖性较低.IE包含的文件较少.


julz.. 14

有一个叫整洁的项目棘鱼马特Zandstra在雅虎,处理多在PHP处理插件的工作.

它强制执行插件类的接口,支持命令行界面,并且不难启动和运行 - 特别是如果您在PHP架构师杂志中阅读有关它的封面故事.


THEMike.. 10

好的建议是看看其他项目是如何做到的.许多人要求安装插件并为服务注册他们的"名称"(如wordpress),因此您在代码中有"点",您可以在其中调用标识已注册侦听器并执行它们的函数.标准的OO设计模式是Observer模式,它是在真正的面向对象的PHP系统中实现的一个很好的选择.

Zend框架使用了诸多挂钩的方法,并且非常漂亮架构.那将是一个很好的系统.


Tim Groeneve.. 7

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

如果你想让插件在另一台 - 远程服务器上运行怎么办?执行此操作的最佳方法是提供一个表单,该表单允许您定义在应用程序中发生特定事件时将调用的不同URL.

不同的事件将根据刚刚发生的事件发送不同的信息.

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

这有两个好处:

  1. 您不必在本地服务器上托管任何代码(安全性)
  2. 代码可以在远程服务器(可扩展性)上以不同的语言而不是PHP(可移植性)

  • 这更像是一个"推送API"而不是"插件"系统 - 您正在为其他服务提供接收所选事件通知的方法."插件"通常意味着您可以安装应用程序,然后添加功能以根据您的目的自定义其行为,这需要插件在本地运行 - 或者至少要提供安全有效的双向通信信息*到*应用程序不只是从它*它.这两个特征有些不同,对于许多情况,"feed"(例如RSS,iCal)是推送API的简单替代方案. (8认同)