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

php plugins architecture hook


我正在用PHP开始一个新的网络应用,这次我想创建一个人们可以通过使用插件接口来扩展的东西。

如何在自己的代码中编写 "钩子",使插件可以附加到特定的事件?




Answer 1 Kevin


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

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




Answer 2 Volomike


假设您不希望使用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部分中,这就是您可能需要在PHP脚本顶部的 require_once() 调用中包含的内容。它加载类以使其可插入。

在第2部分,就是我们加载一个类。请注意,我没有对类做任何特殊的处理,这与Observer模式有很大的不同。

在第3部分中,我们将类转换为“可插入”的(即,支持让我们覆盖类方法和属性的插件)。因此,例如,如果您有一个Web应用程序,则可能有一个插件注册表,您可以在此处激活插件。还要注意 Dog_bark_beforeEvent() 函数。如果我在return语句之前设置 $mixed = 'BLOCK_EVENT' ,它将阻止狗吠叫,并且也将阻止Dog_bark_afterEvent,因为不会发生任何事件。

在第4部分,这就是正常的操作代码,但是注意,你可能认为会运行的东西根本不是这样运行的。比如,狗没有报出它的名字是'Fido',而是'Coco'。狗不会说'喵',而是说'汪'。而当你事后想看狗的名字时,你会发现它是'不同',而不是'可可'。这些重写的内容在第三部分都有提供。

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




Answer 3 w-ll


监听的方法是常用的大多数,但也有其他事情可以做。根据您的应用程序大小,以及允许谁查看代码(这将是FOSS脚本还是内部代码),将极大地影响您希望允许插件的方式。

kdeloach有一个很好的例子,但是他的实现和钩子功能有点不安全。我想请你提供更多关于你所写的php应用性质的信息,以及你是如何看待插件的。

+1来自我的kdeloach。




Answer 4 andy.gurin


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

?>



Answer 5 helloandre


我相信最简单的方法就是听从Jeff的建议,看看现有的代码。试着看看Wordpress、Drupal、Joomla和其他知名的基于PHP的CMS,看看他们的API钩子的外观和感觉。这样你甚至可以得到一些你之前可能没有想到的想法,让事情变得更有摩擦力。

一个更直接的答案是写一般的文件,他们将 "include_once "到他们的文件,将提供他们需要的可用性。这些文件会被分成不同的类别,而不是在一个巨大的 "hooks.php "文件中提供。但是要小心,因为最终发生的情况是,他们包含的文件最终会有越来越多的依赖性和功能改进。尽量保持API的低依赖性。即减少他们需要包含的文件。