首页 文章

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

提问于
浏览
261

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

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

8 回答

  • 10

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

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

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

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

    这有两个好处:

    • 您不必在本地服务器上托管任何代码(安全性)

    • 除了PHP(可移植性)之外,代码可以在远程服务器(扩展性)上使用不同语言

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

    Output:

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

    Notes:

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

    这只是在PHP中完成插件系统的一种方法 . 有更好的选择,我建议您查看WordPress文档以获取更多信息 .

    抱歉,出现下划线字符被Markdown替换为HTML实体?当修复此错误时,我可以重新发布此代码 .

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

  • 22

    因此,让's say you don' t想要Observer模式,因为它要求您更改类方法以处理侦听任务,并且需要通用的东西 . 并且's say you don't想要使用 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部分中,我们将类切换为"pluggable"(即支持允许我们覆盖类方法和属性的插件) . 因此,举例来说,如果你有一个网络应用程序,你可能有一个插件注册表,你可以在这里激活插件 . 另请注意 Dog_bark_beforeEvent() 函数 . 如果我在return语句之前设置 $mixed = 'BLOCK_EVENT' ,它将阻止狗吠叫,并且还会阻止Dog_bark_afterEvent,因为不存在任何事件 .

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

    那么这是如何工作的呢?好吧,让我们排除 eval() (大家都说是"evil")并排除它为什么在第3和第4部分我们搞乱了Pluggable类派生的对象,而不是Dog类本身 . 相反,我们让Plugin类为我们做Dog对象的"touching" . (如果's some kind of design pattern I don'知道 - 请告诉我 . )

  • 33

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

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

    1来自我的kdeloach .

  • 7

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

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

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

  • 55

    雅虎的Matt Zandstra有一个名为Stickleback的简洁项目,负责处理PHP中处理插件的大部分工作 .

    它强制执行插件类的接口,支持命令行界面,并且不易启动和运行 - 特别是如果你在PHP architect magazine中阅读有关它的封面故事 .

  • 14

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

    Zend Framework使用了许多挂钩方法,并且构造非常精美 . 那将是一个很好的系统 .

相关问题