PHPアプリケーションのプラグインを許可する最良の方法


276

私はPHPで新しいWebアプリケーションを開始していますが、今回は、プラグインインターフェイスを使用して拡張できる何かを作成したいと考えています。

プラグインが特定のイベントにアタッチできるように、コードに「フック」をどのように書きますか?

回答:


162

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でプラグインシステムを実現する方法の1つにすぎません。より良い代替案があります。詳細については、WordPressのドキュメントを確認することをお勧めします。


3
:> = 5.0 PHPのためにあなたはSPLで定義されたオブザーバー/件名のインターフェースを使用してこれを実装することもできますphp.net/manual/en/class.splobserver.php
ジョン・カーター

20
歩み寄りの注意:これはObserverパターンの例ではありません。これはの例ですMediator Pattern。真のオブザーバーは純粋に通知であり、メッセージパッシングや条件付き通知はありません(通知を制御するための中央マネージャーもありません)。それは答えを間違えませんが、人々が間違った名前で物事を呼ぶのを止めることに注意すべきです...
ircmaxell

複数のフック/リスナーを使用する場合は、文字列または配列のいずれかのみを返す必要があります。両方を返すことはできません。ハウンドCMSに似たものを実装しました-getbutterfly.com/hound
Ciprian '19

59

したがって、リスナのタスクを処理するようにクラスメソッドを変更し、汎用的なものを必要とするので、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では、ここでクラスを「プラグイン可能」に切り替えます(つまり、クラスのメソッドとプロパティをオーバーライドできるプラグインをサポートします)。したがって、たとえば、Webアプリがある場合、プラグインレジストリがあり、ここでプラグインをアクティブ化できます。Dog_bark_beforeEvent()関数にも注目してください。$mixed = 'BLOCK_EVENT'returnステートメントの前に設定すると、犬が吠えるのをブロックし、イベントがないため、Dog_bark_afterEventもブロックします。

パート4では、これが通常の操作コードですが、実行すると思われるかもしれないが、まったくそのように実行されないことに注意してください。たとえば、犬はその名前を「Fido」ではなく「Coco」と発表します。犬は「ニャー」ではなく「ウーフ」と言います。そして、後で犬の名前を確認したい場合、「ココ」ではなく「異なる」とわかります。これらのオーバーライドはすべてパート3で提供されました。

これはどのように機能するのでしょうか?さて、eval()(誰もが「悪だ」と言う)を除外し、それがObserverパターンではないことを除外しましょう。したがって、動作する方法は、Pluggableと呼ばれる卑劣な空のクラスであり、Dogクラスで使用されるメソッドとプロパティが含まれていません。したがって、それが起こるので、魔法の方法が私たちのために従事します。そのため、パート3と4では、Dogクラス自体ではなく、Pluggableクラスから派生したオブジェクトをいじっています。代わりに、PluginクラスにDogオブジェクトの「タッチ」を行わせます。(それがわからないデザインパターンの場合は、お知らせください。)


3
これはデコレータではありませんか?
MV。

1
はウィキペディアでこれについて読みました、そして、おっと、あなたは正しいです!:)
Volomike 2013年

35

フックリスナー方法が一般的に使用されるほとんどのですが、あなたがすることができる他のものがあります。アプリのサイズ、および誰がコードを参照できるようにするか(これはFOSSスクリプトになるか、社内にあるか)によって、プラグインを許可する方法に大きく影響します。

kdeloachには良い例がありますが、彼の実装とフック機能は少し安全ではありません。私はあなたにあなたの文章のphpアプリの性質のより多くの情報を与え、そしてあなたがプラグインがどのように適合するかをあなたに尋ねるでしょう。

私からkdeloachに+1。


25

これは私が使用したアプローチです。これは、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();

?>

18

最も簡単な方法は、Jeff自身のアドバイスに従い、既存のコードを確認することだと思います。Wordpress、Drupal、Joomla、その他のよく知られているPHPベースのCMSを調べて、APIフックの外観と感触を確認してください。このようにして、以前は考えていなかったかもしれないアイデアを得て、物事をもう少し堅牢にすることができます。

より直接的な答えは、「include_once」する一般的なファイルをファイルに書き込んで、必要なユーザビリティを提供することです。これはカテゴリに分割され、1つのMASSIVE "hooks.php"ファイルでは提供されません。ただし、最終的に発生するのは、ファイルに含まれるファイルの依存関係と機能が向上することです。APIの依存関係を低く保つようにしてください。それらが含めるためのIEより少ないファイル。


私がDokuWikiをあなたが見ているかもしれないシステムのリストに追加したいと思います。豊富なプラグインエコシステムを可能にする素晴らしいイベントシステムがあります。
サイボーグ

15

PHPでプラグインを処理する作業の多くを処理する、YahooのMatt ZandstraによるSticklebackと呼ばれるきちんとしたプロジェクトがあります。

プラグインクラスのインターフェイスを強制し、コマンドラインインターフェイスをサポートします。特に、PHPアーキテクトマガジンでカバーストーリーを読んだ場合は、起動して実行するのはそれほど難しくありません。


11

良いアドバイスは、他のプロジェクトがどのようにそれを行ったかを調べることです。多くの場合、プラグインをインストールし、その「名前」をサービスに登録する必要があります(ワードプレスの場合と同様)。登録されたリスナーを識別して実行する関数を呼び出すコードに「ポイント」があります。標準のOO設計パターンはObserverパターンです。これは、真にオブジェクト指向のPHPシステムに実装するのに適したオプションです。

Zend Frameworkには、多くのフックメソッドを使用すると、非常にうまく設計されています。それは調べるのに良いシステムでしょう。


8

ここでの答えのほとんどが、Webアプリケーションにローカルなプラグイン、つまりローカルWebサーバーで実行されるプラグインに的を絞っているように思われます。

プラグインを別のリモートサーバーで実行したい場合はどうでしょうか?これを行う最良の方法は、アプリケーションで特定のイベントが発生したときに呼び出されるさまざまなURLを定義できるフォームを提供することです。

異なるイベントは、発生したイベントに基づいて異なる情報を送信します。

このように、アプリケーションに提供されたURL(httpsなど)に対してcURL呼び出しを実行するだけで、リモートサーバーはアプリケーションから送信された情報に基づいてタスクを実行できます。

これには2つの利点があります。

  1. ローカルサーバーでコードをホストする必要はありません(セキュリティ)
  2. コードは、PHP(移植性)以外のさまざまな言語のリモートサーバー(拡張性)に置くことができます。

8
これは、「プラグイン」システムよりも「プッシュAPI」に近いものです。他のサービスが選択したイベントの通知を受け取る方法を提供しています。「プラグイン」が一般的に意味することは、アプリケーションをインストールして、その動作を目的に合わせてカスタマイズする機能を追加できることです。これには、プラグインをローカルで実行する必要があります-または少なくとも安全で効率的な双方向通信を提供する必要があります情報への応用はそれを取らないから、それ。2つの機能は多少異なり、多くの場合、「フィード」(RSS、iCalなど)はプッシュAPIの単純な代替手段です。
IMSoP 2013年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.