PHP アプリケーションのプラグインを許可する最良の方法
-
08-06-2019 - |
質問
私は PHP で新しい Web アプリケーションを開始していますが、今回はプラグイン インターフェイスを使用して拡張できるものを作成したいと考えています。
プラグインを特定のイベントに接続できるようにコードに「フック」を記述するにはどうすればよいでしょうか?
解決
オブザーバー パターンを使用することもできます。これを実現する簡単な機能的な方法は次のとおりです。
<?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 ドキュメントをチェックすることをお勧めします。
申し訳ありませんが、マークダウンによってアンダースコア文字が HTML エンティティに置き換えられるようです。このバグが修正されたら、このコードを再投稿できます。
編集:気にしないでください、編集しているときにのみそのように表示されます
他のヒント
したがって、リッスンのタスクを処理するためにクラス メソッドを変更する必要があり、汎用的なものが必要なため、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 では、これは通常のオペレーション コードですが、実行されると思われるコードがまったくそのように実行されないことに注意してください。たとえば、犬は自分の名前を「フィド」ではなく「ココ」と名乗ります。犬は「ニャー」ではなく「ワン」と鳴きます。そして、後で犬の名前を見たいと思ったら、「ココ」ではなく「ディファレント」になっていることがわかります。これらのオーバーライドはすべてパート 3 で提供されました。
では、これはどのように機能するのでしょうか?まあ、除外しましょう eval()
(これは誰もが「悪」だと言います)そしてそれがオブザーバーパターンではないことを除外します。したがって、これが機能するのは、Pluggable と呼ばれる卑劣な空のクラスです。このクラスには、Dog クラスで使用されるメソッドとプロパティが含まれていません。したがって、それが起こると、魔法の方法が私たちに代わって作用します。そのため、パート 3 と 4 では、Dog クラス自体ではなく、Pluggable クラスから派生したオブジェクトをいじります。代わりに、Plugin クラスに Dog オブジェクトへの「タッチ」を行わせます。(それが私が知らない何らかのデザイン パターンである場合は、お知らせください。)
の 針 そして リスナー が最も一般的に使用される方法ですが、他にもできることはあります。アプリのサイズと、コードの閲覧を誰に許可するか (これが FOSS スクリプトになるのか、それとも社内のものになるのか) に応じて、プラグインをどのように許可するかが大きく影響します。
kdeloach には良い例がありますが、彼の実装とフック関数は少し安全ではありません。あなたの書いた php アプリの性質と、プラグインがどのように適合すると考えているかについて、さらに詳しい情報を提供していただきたいと思います。
私からkdeloachに+1。
これは私が使用したアプローチです。これは、オブザーバーパターンの一種である、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 つの大規模な「hooks.php」ファイルでは提供されません。ただし、最終的には、インクルードされるファイルの依存関係が増え、機能が向上するため、注意してください。API の依存関係を低く保つようにしてください。つまり、含めるファイルが少なくなります。
という素敵なプロジェクトがあります イトヨ Yahoo の Matt Zandstra によるもので、PHP でプラグインを処理するための作業の多くを担当しています。
これは、プラグイン クラスのインターフェイスを強制し、コマンド ライン インターフェイスをサポートしており、立ち上げて実行するのはそれほど難しくありません。特に、次の記事でカバー ストーリーを読んでいれば、 PHP建築家マガジン.
良いアドバイスは、他のプロジェクトがどのようにそれを行ったかを調べることです。多くの場合、プラグインをインストールし、その「名前」をサービスに登録する必要があります (WordPress の場合と同様)。そのため、登録されたリスナーを識別して実行する関数を呼び出すコード内に「ポイント」が必要になります。標準的な OO 設計パターンは次のとおりです。 オブザーバーパターン, これは、真のオブジェクト指向 PHP システムに実装するのに適したオプションです。
の Zend フレームワーク 多くのフックメソッドを利用しており、非常にうまく設計されています。それは検討してみると良いシステムでしょう。
ここでの回答のほとんどが、Web アプリケーションに対してローカルなプラグイン、つまりローカル Web サーバー上で実行されるプラグインを対象としているように見えることに驚きました。
プラグインを別のリモートサーバーで実行したい場合はどうすればよいでしょうか?これを行う最善の方法は、アプリケーションで特定のイベントが発生したときに呼び出されるさまざまな URL を定義できるフォームを提供することです。
異なるイベントは、発生したイベントに基づいて異なる情報を送信します。
この方法では、アプリケーションに提供された URL (https 経由など) に対して cURL 呼び出しを実行するだけで、リモート サーバーがアプリケーションから送信された情報に基づいてタスクを実行できます。
これにより、次の 2 つの利点が得られます。
- ローカルサーバーでコードをホストする必要はありません (セキュリティ)
- コードは PHP 以外のさまざまな言語でリモート サーバー上に置くことができます (拡張性) (移植性)