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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/42/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-24 21:06:24  来源:igfitidea点击:

Best way to allow plugins for a PHP application

phppluginsarchitecturehook

提问by Wally Lawless

I am starting a new web application in PHP and this time around I want to create something that people can extend by using a plugin interface.

我正在用 PHP 启动一个新的 Web 应用程序,这一次我想创建一些人们可以使用插件界面扩展的东西。

How does one go about writing 'hooks' into their code so that plugins can attach to specific events?

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

采纳答案by Kevin

You could use an Observer pattern. A simple functional way to accomplish this:

您可以使用观察者模式。实现此目的的简单功能方法:

<?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:

笔记:

For this example source code, you must declare all your plugins before the actual source code that you want to be extendable. I've included an example of how to handle single or multiple values being passed to the plugin. The hardest part of this is writing the actual documentation which lists what arguments get passed to each hook.

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

This is just one method of accomplishing a plugin system in PHP. There are better alternatives, I suggest you check out the WordPress Documentation for more information.

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

回答by Volomike

So let's say you don't want the Observer pattern because it requires that you change your class methods to handle the task of listening, and want something generic. And let's say you don't want to use extendsinheritance because you may already be inheriting in your class from some other class. Wouldn't it be great to have a generic way to make any class pluggable without much effort? Here's how:

因此,假设您不想要观察者模式,因为它要求您更改类方法来处理侦听任务,并且想要一些通用的东西。假设您不想使用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;

In Part 1, that's what you might include with a require_once()call at the top of your PHP script. It loads the classes to make something pluggable.

在第 1 部分中,这就是您可能require_once()在 PHP 脚本顶部的调用中包含的内容。它加载类以制作可插入的东西。

In Part 2, that's where we load a class. Note I didn't have to do anything special to the class, which is significantly different than the Observer pattern.

在第 2 部分中,这就是我们加载类的地方。注意我不需要对类做任何特别的事情,这与观察者模式有很大的不同。

In Part 3, that's where we switch our class around into being "pluggable" (that is, supports plugins that let us override class methods and properties). So, for instance, if you have a web app, you might have a plugin registry, and you could activate plugins here. Notice also the Dog_bark_beforeEvent()function. If I set $mixed = 'BLOCK_EVENT'before the return statement, it will block the dog from barking and would also block the Dog_bark_afterEvent because there wouldn't be any event.

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

In Part 4, that's the normal operation code, but notice that what you might think would run does not run like that at all. For instance, the dog does not announce it's name as 'Fido', but 'Coco'. The dog does not say 'meow', but 'Woof'. And when you want to look at the dog's name afterwards, you find it is 'Different' instead of 'Coco'. All those overrides were provided in Part 3.

在第 4 部分中,这是正常的操作代码,但请注意,您可能认为会运行的代码根本不是这样运行的。例如,狗不会宣布它的名字是“Fido”,而是“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.)

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

回答by w-ll

The hookand listenermethod is the most commonly used, but there are other things you can do. Depending on the size of your app, and who your going to allow see the code (is this going to be a FOSS script, or something in house) will influence greatly how you want to allow plugins.

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

kdeloach has a nice example, but his implementation and hook function is a little unsafe. I would ask for you to give more information of the nature of php app your writing, And how you see plugins fitting in.

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

+1 to kdeloach from me.

+1 从我这里 kdeloach。

回答by andy.gurin

Here is an approach I've used, it's an attempt to copy from Qt signals/slots mechanism, a kind of Observer pattern. Objects can emit signals. Every signal has an ID in the system - it's composed by sender's id + object name Every signal can be binded to the receivers, which simply is a "callable" You use a bus class to pass the signals to anybody interested in receiving them When something happens, you "send" a signal. Below is and example implementation

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

?>

回答by helloandre

I believe the easiest way would be to follow Jeff's own advice and have a look around existing code. Try looking at Wordpress, Drupal, Joomla and other well known PHP-based CMS's to see how their API hooks look and feel. This way you can even get ideas you may have not thought of previously to make things a little more rubust.

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

A more direct answer would be to write general files that they would "include_once" into their file that would provide the usability they would need. This would be broken up into categories and NOT provided in one MASSIVE "hooks.php" file. Be careful though, because what ends up happening is that files that they include end up having more and more dependencies and functionality improves. Try to keep API dependencies low. I.E fewer files for them to include.

更直接的答案是将通用文件“include_once”写入他们的文件中,以提供他们需要的可用性。这将被分成几类,而不是在一个巨大的“hooks.php”文件中提供。不过要小心,因为最终会发生的是,它们包含的文件最终会具有越来越多的依赖项,并且功能会得到改进。尽量降低 API 依赖性。IE 包含更少的文件。

回答by julz

There's a neat project called Sticklebackby Matt Zandstra at Yahoo that handles much of the work for handling plugins in PHP.

有一个叫整洁的项目棘鱼马特Zandstra在雅虎那么多在PHP处理插件工作的手柄。

It enforces the interface of a plugin class, supports a command line interface and isn't too hard to get up and running - especially if you read the cover story about it in the PHP architect magazine.

它强制执行插件类的接口,支持命令行接口,并且启动和运行并不太难——尤其是如果您在PHP 架构师杂志上阅读了有关它的封面故事。

回答by THEMike

Good advice is to look how other projects have done it. Many call for having plugins installed and their "name" registered for services (like wordpress does) so you have "points" in your code where you call a function that identifies registered listeners and executes them. A standard OO design patter is the Observer Pattern, which would be a good option to implement in a truly object oriented PHP system.

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

The Zend Frameworkmakes use of many hooking methods, and is very nicely architected. That would be a good system to look at.

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

回答by Tim Groeneveld

I am surprised that most of the answers here seem to be geared about plugins that are local to the web application, ie, plugins that run on the local web server.

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

What about if you wanted the plugins to run on a different - remote - server? The best way to do this would be to provide a form that allows you to define different URLs that would be called when particular events occur in your application.

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

Different events would send different information based on the event that just occurred.

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

This way, you would just perform a cURL call to the URL that has been provided to your application (eg over https) where remote servers can perform tasks based on information that has been sent by your application.

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

This provides two benefits:

这提供了两个好处:

  1. You don't have to host any code on your local server (security)
  2. The code can be on remote servers (extensibility) in different languages other then PHP (portability)
  1. 您不必在本地服务器上托管任何代码(安全性)
  2. 代码可以在不同语言的远程服务器上(可扩展性),而不是 PHP(可移植性)