私はPHPで新しいWebアプリケーションを開始していますが、今回は、プラグインインターフェイスを使用して拡張できる何かを作成したいと考えています。
プラグインが特定のイベントにアタッチできるように、コードに「フック」をどのように書きますか?
私はPHPで新しいWebアプリケーションを開始していますが、今回は、プラグインインターフェイスを使用して拡張できる何かを作成したいと考えています。
プラグインが特定のイベントにアタッチできるように、コードに「フック」をどのように書きますか?
回答:
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のドキュメントを確認することをお勧めします。
Mediator Pattern
。真のオブザーバーは純粋に通知であり、メッセージパッシングや条件付き通知はありません(通知を制御するための中央マネージャーもありません)。それは答えを間違えませんが、人々が間違った名前で物事を呼ぶのを止めることに注意すべきです...
したがって、リスナのタスクを処理するようにクラスメソッドを変更し、汎用的なものを必要とするので、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オブジェクトの「タッチ」を行わせます。(それがわからないデザインパターンの場合は、お知らせください。)
これは私が使用したアプローチです。これは、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();
?>
最も簡単な方法は、Jeff自身のアドバイスに従い、既存のコードを確認することだと思います。Wordpress、Drupal、Joomla、その他のよく知られているPHPベースのCMSを調べて、APIフックの外観と感触を確認してください。このようにして、以前は考えていなかったかもしれないアイデアを得て、物事をもう少し堅牢にすることができます。
より直接的な答えは、「include_once」する一般的なファイルをファイルに書き込んで、必要なユーザビリティを提供することです。これはカテゴリに分割され、1つのMASSIVE "hooks.php"ファイルでは提供されません。ただし、最終的に発生するのは、ファイルに含まれるファイルの依存関係と機能が向上することです。APIの依存関係を低く保つようにしてください。それらが含めるためのIEより少ないファイル。
PHPでプラグインを処理する作業の多くを処理する、YahooのMatt ZandstraによるSticklebackと呼ばれるきちんとしたプロジェクトがあります。
プラグインクラスのインターフェイスを強制し、コマンドラインインターフェイスをサポートします。特に、PHPアーキテクトマガジンでカバーストーリーを読んだ場合は、起動して実行するのはそれほど難しくありません。
良いアドバイスは、他のプロジェクトがどのようにそれを行ったかを調べることです。多くの場合、プラグインをインストールし、その「名前」をサービスに登録する必要があります(ワードプレスの場合と同様)。登録されたリスナーを識別して実行する関数を呼び出すコードに「ポイント」があります。標準のOO設計パターンはObserverパターンです。これは、真にオブジェクト指向のPHPシステムに実装するのに適したオプションです。
Zend Frameworkには、多くのフックメソッドを使用すると、非常にうまく設計されています。それは調べるのに良いシステムでしょう。
ここでの答えのほとんどが、Webアプリケーションにローカルなプラグイン、つまりローカルWebサーバーで実行されるプラグインに的を絞っているように思われます。
プラグインを別のリモートサーバーで実行したい場合はどうでしょうか?これを行う最良の方法は、アプリケーションで特定のイベントが発生したときに呼び出されるさまざまなURLを定義できるフォームを提供することです。
異なるイベントは、発生したイベントに基づいて異なる情報を送信します。
このように、アプリケーションに提供されたURL(httpsなど)に対してcURL呼び出しを実行するだけで、リモートサーバーはアプリケーションから送信された情報に基づいてタスクを実行できます。
これには2つの利点があります。