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

php plugins architecture hook


私はPHPで新しいウェブアプリケーションを始めようとしていますが、今回はプラグインのインターフェイスを使って人々が拡張できるものを作りたいと思っています。

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




Answer 1 Kevin


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のドキュメントをチェックしてみることをお勧めします。




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' 前に$ mixed = 'BLOCK_EVENT'を設定すると、犬の吠えがブロックされ、イベントが発生しないため、Dog_bark_afterEventもブロックされます。

パート4では、それは通常の操作コードですが、あなたが実行すると思うかもしれないものが全くそのように実行されていないことに気付いてください。例えば、犬は'Fido'としてそれの名前を発表しませんが、'Coco'。犬は 'ニャー' が、'Woof' とは言いません。そして、その後に犬の名前を見たいと思ったとき、それは'Coco'の代わりに'Different'であることがわかります。それらのオーバーライドはすべてPart3で用意されていました。

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




Answer 3 w-ll


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

kdeloachさんは良い例を持っていますが、彼の実装とフック関数は少し安全ではありません。私は、あなたが書いているphpアプリの性質と、プラグインがどのようにフィットするかについて、より多くの情報を提供するようにお願いしたいと思います。

私からkdeloachに+1。




Answer 4 andy.gurin


これは、Qtのsignals/slotsのメカニズム、一種のオブザーバーパターンをコピーしようとしたものです。オブジェクトはシグナルを出すことができます。すべてのシグナルはシステム内で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 "する一般的なファイルを書くことでしょう。これはカテゴリに分割され、1つの巨大な "hooks.php "ファイルでは提供されません。しかし、注意してください、何が起こって終わるかは、彼らが含むファイルは、より多くの依存性と機能性が向上してしまうということです。APIの依存性を低く保つようにしてください。つまり、彼らがインクルードするファイルの数を少なくします。