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

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


当前回答

因此,假设您不想要Observer模式,因为它要求您更改类方法来处理侦听任务,并且希望使用一些通用的模式。假设你不想使用扩展继承因为你可能已经在你的类中从其他类继承了。如果有一种通用的方法使任何类都可以不费多大力气就可插入,这不是很棒吗?方法如下:

<?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部分中,我们将加载一个类。注意,我不需要对类做任何特殊的操作,这与观察者模式有很大不同。

在第3部分中,我们将类转换为“可插入的”(也就是说,支持让我们重写类方法和属性的插件)。例如,如果你有一个web应用,你可能有一个插件注册表,你可以在这里激活插件。还要注意Dog_bark_beforeEvent()函数。如果我在return语句之前设置$mixed = 'BLOCK_EVENT',它会阻止狗叫,也会阻止Dog_bark_afterEvent,因为不会有任何事件。

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

So how does this work? Well, let's rule out eval() (which everyone says is "evil") and rule out that it's not an Observer pattern. So, the way it works is the sneaky empty class called Pluggable, which does not contain the methods and properties used by the Dog class. Thus, since that occurs, the magic methods will engage for us. That's why in parts 3 and 4 we mess with the object derived from the Pluggable class, not the Dog class itself. Instead, we let the Plugin class do the "touching" on the Dog object for us. (If that's some kind of design pattern I don't know about -- please let me know.)

其他回答

我相信最简单的方法是遵循Jeff自己的建议,查看一下现有的代码。试着看看WordPress、Drupal、Joomla和其他知名的基于php的CMS,看看它们的API挂钩的外观和感觉如何。通过这种方式,您甚至可以获得您以前可能没有想到的想法,从而使事情变得更加健壮。

一个更直接的答案是将通用文件“include_once”写入文件,以提供所需的可用性。这将被分解成类别,而不是在一个巨大的“hooks.php”文件中提供。但是要小心,因为最终发生的情况是,它们包含的文件最终具有越来越多的依赖关系,功能也得到了改进。尽量保持较低的API依赖性。也就是要包含更少的文件。

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

?>

您可以使用观察者模式。一个简单的功能方法来完成这个:

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

因此,假设您不想要Observer模式,因为它要求您更改类方法来处理侦听任务,并且希望使用一些通用的模式。假设你不想使用扩展继承因为你可能已经在你的类中从其他类继承了。如果有一种通用的方法使任何类都可以不费多大力气就可插入,这不是很棒吗?方法如下:

<?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部分中,我们将加载一个类。注意,我不需要对类做任何特殊的操作,这与观察者模式有很大不同。

在第3部分中,我们将类转换为“可插入的”(也就是说,支持让我们重写类方法和属性的插件)。例如,如果你有一个web应用,你可能有一个插件注册表,你可以在这里激活插件。还要注意Dog_bark_beforeEvent()函数。如果我在return语句之前设置$mixed = 'BLOCK_EVENT',它会阻止狗叫,也会阻止Dog_bark_afterEvent,因为不会有任何事件。

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

So how does this work? Well, let's rule out eval() (which everyone says is "evil") and rule out that it's not an Observer pattern. So, the way it works is the sneaky empty class called Pluggable, which does not contain the methods and properties used by the Dog class. Thus, since that occurs, the magic methods will engage for us. That's why in parts 3 and 4 we mess with the object derived from the Pluggable class, not the Dog class itself. Instead, we let the Plugin class do the "touching" on the Dog object for us. (If that's some kind of design pattern I don't know about -- please let me know.)

雅虎的Matt Zandstra有一个叫做Stickleback的项目,它处理了很多PHP插件的处理工作。

它加强了插件类的接口,支持命令行接口,并且安装和运行起来并不太难——特别是如果您阅读了PHP架构师杂志上关于它的封面故事。