Cara terbaik untuk mengizinkan plugin untuk aplikasi PHP

276

Saya memulai aplikasi web baru dalam PHP dan kali ini saya ingin membuat sesuatu yang dapat diperluas orang dengan menggunakan antarmuka plugin.

Bagaimana cara menulis 'kait' ke dalam kode mereka sehingga plugin dapat melampirkan ke acara tertentu?

Wally Lawless
sumber

Jawaban:

162

Anda bisa menggunakan pola Observer. Cara fungsional sederhana untuk mencapai ini:

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

Keluaran:

This is my CRAZY application
4 + 5 = 9
4 * 5 = 20

Catatan:

Untuk contoh kode sumber ini, Anda harus mendeklarasikan semua plugin Anda sebelum kode sumber aktual yang Anda inginkan dapat diperpanjang. Saya telah menyertakan contoh bagaimana menangani nilai tunggal atau ganda yang diteruskan ke plugin. Bagian tersulit dari ini adalah menulis dokumentasi aktual yang mencantumkan argumen apa yang diteruskan ke setiap kait.

Ini hanyalah salah satu metode untuk menyelesaikan sistem plugin dalam PHP. Ada alternatif yang lebih baik, saya sarankan Anda memeriksa Dokumentasi WordPress untuk informasi lebih lanjut.

Kevin
sumber
3
Perhatikan bahwa untuk PHP> = 5.0 Anda dapat mengimplementasikan ini menggunakan antarmuka Observer / Subject yang didefinisikan dalam SPL: php.net/manual/en/class.splobserver.php
John Carter
20
Catatan Pedantic: ini bukan contoh pola Observer. Ini adalah contoh dari Mediator Pattern. Pengamat sejati adalah pemberitahuan murni, tidak ada pesan yang lewat atau pemberitahuan bersyarat (juga tidak ada manajer pusat untuk mengendalikan pemberitahuan). Itu tidak membuat jawaban salah , tetapi harus dicatat untuk menghentikan orang memanggil sesuatu dengan nama yang salah ...
ircmaxell
Perhatikan bahwa saat menggunakan beberapa kait / pendengar, Anda hanya boleh mengembalikan string atau array, bukan keduanya. Saya telah mengimplementasikan sesuatu yang serupa untuk Hound CMS - getbutterfly.com/hound .
Ciprian
59

Jadi katakanlah Anda tidak menginginkan pola Observer karena mengharuskan Anda mengubah metode kelas Anda untuk menangani tugas mendengarkan, dan menginginkan sesuatu yang generik. Dan katakanlah Anda tidak ingin menggunakannyaextends warisan karena Anda mungkin sudah mewarisi di kelas Anda dari beberapa kelas lain. Bukankah lebih baik memiliki cara generik untuk membuat kelas mana pun dapat dicolokkan tanpa banyak usaha ? Begini caranya:

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

Di Bagian 1, itulah yang mungkin Anda sertakan dengan a require_once() panggilan di bagian atas skrip PHP Anda. Ini memuat kelas-kelas untuk membuat sesuatu yang dapat dicolokkan

Di Bagian 2, di situlah kita memuat kelas. Catatan saya tidak perlu melakukan sesuatu yang khusus untuk kelas, yang secara signifikan berbeda dari pola Pengamat.

Di Bagian 3, di situlah kita mengubah kelas kita menjadi "pluggable" (artinya, mendukung plugin yang memungkinkan kita mengganti metode dan properti kelas). Jadi, misalnya, jika Anda memiliki aplikasi web, Anda mungkin memiliki registry plugin, dan Anda dapat mengaktifkan plugin di sini. Perhatikan jugaDog_bark_beforeEvent() fungsinya. Jika saya mengatur $mixed = 'BLOCK_EVENT'sebelum pernyataan kembali, itu akan memblokir anjing dari menggonggong dan juga akan memblokir Dog_bark_afterEvent karena tidak akan ada acara.

Di Bagian 4, itulah kode operasi normal, tetapi perhatikan bahwa apa yang Anda pikir akan berjalan tidak berjalan sama sekali. Misalnya, anjing tidak mengumumkan nama itu sebagai 'Fido', tetapi 'Coco'. Anjing itu tidak mengatakan 'meow', tetapi 'Woof'. Dan ketika Anda ingin melihat nama anjing setelahnya, Anda menemukan itu 'Berbeda' dan bukan 'Coco'. Semua penggantian tersebut disediakan di Bagian 3.

Jadi bagaimana cara kerjanya? Baiklah, mari kita singkirkan eval()(yang semua orang katakan adalah "jahat") dan singkirkan bahwa itu bukan pola Pengamat. Jadi, cara kerjanya adalah kelas kosong licik yang disebut Pluggable, yang tidak mengandung metode dan properti yang digunakan oleh kelas Dog. Jadi, sejak itu terjadi, metode sihir akan melibatkan kita. Itu sebabnya di bagian 3 dan 4 kita mengacaukan objek yang berasal dari kelas Pluggable, bukan kelas Dog itu sendiri. Sebagai gantinya, kami membiarkan kelas Plugin melakukan "sentuhan" pada objek Dog untuk kami. (Jika itu semacam pola desain yang tidak saya ketahui - tolong beri tahu saya.)

Volomike
sumber
3
Bukankah ini dekorator?
MV.
1
Saya membaca di Wikipedia tentang ini dan, wah, Anda benar! :)
Volomike
35

The kait dan pendengar adalah yang paling umum digunakan, tetapi ada hal-hal lain yang dapat Anda lakukan. Bergantung pada ukuran aplikasi Anda, dan siapa yang akan Anda izinkan melihat kode (apakah ini akan menjadi skrip FOSS, atau sesuatu di dalam rumah) akan sangat memengaruhi cara Anda ingin mengizinkan plugin.

kdeloach memiliki contoh yang bagus, tetapi fungsi implement dan hook-nya sedikit tidak aman. Saya akan meminta Anda untuk memberikan informasi lebih lanjut tentang sifat dari aplikasi php tulisan Anda, Dan bagaimana Anda melihat plugin masuk

+1 ke kdeloach dari saya.

b-ll
sumber
25

Ini adalah pendekatan yang saya gunakan, ini merupakan upaya untuk menyalin dari mekanisme sinyal / slot Qt, semacam pola Observer. Objek dapat memancarkan sinyal. Setiap sinyal memiliki ID dalam sistem - itu disusun oleh id + nama objek pengirim. Setiap sinyal dapat diikatkan ke penerima, yang hanya merupakan "panggilan". Anda menggunakan kelas bus untuk menyampaikan sinyal kepada siapa saja yang tertarik menerimanya. Ketika sesuatu terjadi, Anda "mengirim" sinyal. Di bawah ini adalah dan contoh implementasi

    <?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();

?>
andy.gurin
sumber
18

Saya percaya cara termudah adalah mengikuti saran Jeff sendiri dan melihat-lihat kode yang ada. Coba lihat Wordpress, Drupal, Joomla dan CMS berbasis PHP terkenal lainnya untuk melihat bagaimana kait dan tampilan API mereka. Dengan cara ini Anda bahkan bisa mendapatkan ide-ide yang mungkin belum terpikirkan sebelumnya untuk membuat segalanya menjadi lebih banyak sampah.

Jawaban yang lebih langsung adalah menulis file-file umum yang akan mereka "sertakan" pada file mereka yang akan memberikan kegunaan yang mereka butuhkan. Ini akan dipecah menjadi beberapa kategori dan TIDAK disediakan dalam satu file "hooks.php" BESAR. Berhati-hatilah, karena apa yang akhirnya terjadi adalah bahwa file yang mereka masukkan akhirnya memiliki lebih banyak ketergantungan dan fungsionalitas yang meningkat. Usahakan agar ketergantungan API tetap rendah. IE lebih sedikit file untuk mereka sertakan.

helloandre
sumber
Saya akan menambahkan DokuWiki ke daftar sistem yang mungkin Anda lihat. Ini memiliki sistem acara yang bagus yang memungkinkan ekosistem plugin kaya.
chiborg
15

Ada proyek rapi bernama Stickleback oleh Matt Zandstra di Yahoo yang menangani banyak pekerjaan untuk menangani plugin dalam PHP.

Ini menegakkan antarmuka kelas plugin, mendukung antarmuka baris perintah dan tidak terlalu sulit untuk bangun dan berjalan - terutama jika Anda membaca cerita sampul tentang hal itu di majalah arsitek PHP .

julz
sumber
11

Saran yang baik adalah untuk melihat bagaimana proyek lain melakukannya. Banyak panggilan untuk memasang plugin dan "nama" mereka terdaftar untuk layanan (seperti halnya wordpress) sehingga Anda memiliki "poin" dalam kode Anda tempat Anda memanggil fungsi yang mengidentifikasi pendengar terdaftar dan mengeksekusinya. Pola desain OO standar adalah Pola Pengamat , yang akan menjadi pilihan yang baik untuk diterapkan dalam sistem PHP yang benar-benar berorientasi objek.

The Zend Framework memanfaatkan banyak metode hooking, dan sangat baik architected. Itu akan menjadi sistem yang bagus untuk dilihat.

Seperti mereka
sumber
8

Saya terkejut bahwa sebagian besar jawaban di sini tampaknya diarahkan tentang plugin yang bersifat lokal untuk aplikasi web, yaitu, plugin yang berjalan di server web lokal.

Bagaimana jika Anda ingin plugin dijalankan di server yang berbeda? Cara terbaik untuk melakukan ini adalah dengan menyediakan formulir yang memungkinkan Anda untuk menentukan berbagai URL yang akan dipanggil ketika peristiwa tertentu terjadi dalam aplikasi Anda.

Peristiwa yang berbeda akan mengirimkan informasi yang berbeda berdasarkan peristiwa yang baru saja terjadi.

Dengan cara ini, Anda hanya akan melakukan panggilan CURL ke URL yang telah disediakan untuk aplikasi Anda (misalnya lebih dari https) di mana server jauh dapat melakukan tugas berdasarkan informasi yang telah dikirim oleh aplikasi Anda.

Ini memberikan dua manfaat:

  1. Anda tidak perlu meng-host kode apa pun di server lokal Anda (keamanan)
  2. Kode dapat berada di server jarak jauh (ekstensibilitas) dalam berbagai bahasa selain PHP (portabilitas)
Tim Groeneveld
sumber
8
Ini lebih merupakan "push API" daripada sistem "plugin" - Anda menyediakan cara bagi layanan lain untuk menerima pemberitahuan acara yang dipilih. Apa yang secara umum dimaksud dengan "plugins" adalah bahwa Anda dapat menginstal aplikasi dan kemudian menambahkan fungsionalitas untuk menyesuaikan perilakunya untuk tujuan Anda, yang mengharuskan plugin dijalankan secara lokal - atau setidaknya memiliki komunikasi 2 arah yang aman dan efisien untuk menyediakan informasi untuk aplikasi tidak hanya mengambil dari itu. Kedua fitur ini agak berbeda, dan untuk banyak kasus "umpan" (mis. RSS, iCal) adalah alternatif sederhana untuk push API.
IMSoP