Cách tốt nhất để cho phép bổ trợ cho ứng dụng PHP


276

Tôi đang bắt đầu một ứng dụng web mới trong PHP và lần này tôi muốn tạo ra thứ gì đó mà mọi người có thể mở rộng bằng cách sử dụng giao diện plugin.

Làm thế nào một người đi về việc viết 'hook' vào mã của họ để các plugin có thể đính kèm vào các sự kiện cụ thể?

Câu trả lời:


162

Bạn có thể sử dụng một mẫu Observer. Một cách đơn giản để thực hiện điều này:

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

Đầu ra:

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

Ghi chú:

Đối với mã nguồn ví dụ này, bạn phải khai báo tất cả các plugin của mình trước mã nguồn thực tế mà bạn muốn có thể mở rộng. Tôi đã bao gồm một ví dụ về cách xử lý một hoặc nhiều giá trị được truyền cho plugin. Phần khó nhất của việc này là viết tài liệu thực tế liệt kê các đối số được truyền cho mỗi hook.

Đây chỉ là một phương pháp để hoàn thành một hệ thống plugin trong PHP. Có những lựa chọn thay thế tốt hơn, tôi khuyên bạn nên kiểm tra Tài liệu WordPress để biết thêm thông tin.


3
Lưu ý rằng đối với PHP> = 5.0, bạn có thể thực hiện điều này bằng cách sử dụng giao diện Quan sát / Chủ đề được xác định trong SPL: php.net/manual/en/ class.splobserver.php
John Carter

20
Lưu ý Pedantic: đây không phải là một ví dụ về mẫu Observer. Đó là một ví dụ về Mediator Pattern. Các nhà quan sát thực sự hoàn toàn là thông báo, không có thông báo chuyển hoặc thông báo có điều kiện (cũng không có người quản lý trung tâm để kiểm soát thông báo). Nó không làm cho câu trả lời sai , nhưng cần lưu ý để ngăn mọi người gọi nhầm tên ...
ircmaxell

Lưu ý rằng khi sử dụng nhiều hook / listener, bạn chỉ nên trả về chuỗi hoặc mảng, không phải cả hai. Tôi đã triển khai một cái gì đó tương tự cho Hound CMS - getbutoston.com/hound .
Ciprian

59

Vì vậy, giả sử bạn không muốn mẫu Observer bởi vì nó yêu cầu bạn thay đổi các phương thức lớp để xử lý công việc lắng nghe và muốn một cái gì đó chung chung. Và giả sử bạn không muốn sử dụng extendsquyền thừa kế vì bạn có thể đã được thừa kế trong lớp của mình từ một số lớp khác. Sẽ không tuyệt vời khi có một cách chung để làm cho bất kỳ lớp nào có thể cắm được mà không cần nhiều nỗ lực ? Đây là cách thực hiện:

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

Trong Phần 1, đó là những gì bạn có thể bao gồm với một require_once()cuộc gọi ở đầu tập lệnh PHP của bạn. Nó tải các lớp để làm cho một cái gì đó có thể cắm được.

Trong Phần 2, đó là nơi chúng ta tải một lớp. Lưu ý tôi đã không phải làm bất cứ điều gì đặc biệt cho lớp, khác biệt đáng kể so với mẫu Người quan sát.

Trong Phần 3, đó là nơi chúng ta chuyển lớp của mình thành "có thể cắm được" (nghĩa là, hỗ trợ các plugin cho phép chúng ta ghi đè các phương thức và thuộc tính của lớp). Vì vậy, ví dụ, nếu bạn có một ứng dụng web, bạn có thể có một sổ đăng ký plugin và bạn có thể kích hoạt các plugin ở đây. Cũng lưu ý Dog_bark_beforeEvent()chức năng. Nếu tôi đặt $mixed = 'BLOCK_EVENT'trước câu lệnh return, nó sẽ chặn con chó sủa và cũng sẽ chặn Dog_bark_afterEvent vì sẽ không có sự kiện nào.

Trong Phần 4, đó là mã hoạt động bình thường, nhưng lưu ý rằng những gì bạn có thể nghĩ sẽ chạy hoàn toàn không chạy như vậy. Chẳng hạn, con chó không thông báo tên của nó là 'Fido', mà là 'Coco'. Con chó không nói 'meo', mà là 'Woo'. Và khi bạn muốn nhìn vào tên của con chó sau đó, bạn sẽ thấy nó là 'Khác biệt' thay vì 'Coco'. Tất cả các phần ghi đè được cung cấp trong Phần 3.

Vậy làm thế nào để làm việc này? Chà, hãy loại trừ eval()(mà mọi người nói là "xấu xa") và loại trừ rằng đó không phải là mẫu Người quan sát. Vì vậy, cách nó hoạt động là lớp trống lén lút được gọi là Pluggable, không chứa các phương thức và thuộc tính được sử dụng bởi lớp Dog. Do đó, kể từ khi điều đó xảy ra, các phương pháp ma thuật sẽ tham gia vào chúng ta. Đó là lý do tại sao trong phần 3 và 4, chúng ta gặp rắc rối với đối tượng xuất phát từ lớp Pluggable chứ không phải lớp Dog. Thay vào đó, chúng tôi để lớp Plugin thực hiện "chạm" vào đối tượng Dog cho chúng tôi. (Nếu đó là một kiểu mẫu thiết kế mà tôi không biết - vui lòng cho tôi biết.)


3
Đây không phải là một trang trí?
MV.

1
Tôi đã đọc trên Wikipedia về điều này và, whoa, bạn nói đúng! :)
Volomike

35

Phương thức hooklistener được sử dụng phổ biến nhất, nhưng có những thứ khác bạn có thể làm. Tùy thuộc vào kích thước ứng dụng của bạn và ai sẽ cho phép xem mã (đây có phải là tập lệnh FOSS hoặc nội dung nào đó không) sẽ ảnh hưởng rất lớn đến cách bạn muốn cho phép bổ trợ.

kdeloach có một ví dụ hay, nhưng chức năng hook và hook của anh ta hơi không an toàn. Tôi sẽ yêu cầu bạn cung cấp thêm thông tin về bản chất của ứng dụng php bằng văn bản của bạn và cách bạn thấy các plugin phù hợp.

+1 cho kdeloach từ tôi.


25

Đây là một cách tiếp cận tôi đã sử dụng, đó là một nỗ lực sao chép từ cơ chế tín hiệu / khe cắm Qt, một kiểu mẫu của Người quan sát. Các đối tượng có thể phát ra tín hiệu. Mọi tín hiệu đều có ID trong hệ thống - nó được tạo bởi id + tên đối tượng của người gửi Mọi tín hiệu có thể được liên kết với các máy thu, đơn giản là "có thể gọi được" Bạn sử dụng một lớp xe buýt để truyền tín hiệu cho bất kỳ ai quan tâm đến chúng khi có gì đó xảy ra, bạn "gửi" một tín hiệu. Dưới đây là và ví dụ thực hiện

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

Tôi tin rằng cách dễ nhất sẽ là làm theo lời khuyên của Jeff và xem xét mã hiện có. Hãy thử nhìn vào Wordpress, Drupal, Joomla và các CMS dựa trên PHP nổi tiếng khác để xem các API của chúng trông và cảm nhận như thế nào. Bằng cách này, bạn thậm chí có thể lấy ý tưởng mà trước đây bạn có thể chưa từng nghĩ đến để khiến mọi thứ trở nên mạnh mẽ hơn một chút.

Một câu trả lời trực tiếp hơn sẽ là viết các tệp chung mà họ sẽ "gộp_once" vào tệp của họ để cung cấp khả năng sử dụng mà họ cần. Điều này sẽ được chia thành các loại và KHÔNG được cung cấp trong một tệp "hook.php" MASSIVE. Mặc dù vậy, hãy cẩn thận, bởi vì những gì kết thúc xảy ra là các tệp mà chúng bao gồm cuối cùng ngày càng có nhiều phụ thuộc và chức năng được cải thiện. Cố gắng giữ cho các phụ thuộc API thấp. IE ít tập tin hơn để bao gồm chúng.


Tôi sẽ thêm DokuWiki vào danh sách các hệ thống mà bạn có thể xem qua. Nó có một hệ thống sự kiện đẹp cho phép hệ sinh thái plugin phong phú.
chiborg

15

Có một dự án gọn gàng được gọi là Cá Gai Matt Zandstra tại Yahoo mà xử lý nhiều công việc để xử lý các plugin trong PHP.

Nó thực thi giao diện của lớp plugin, hỗ trợ giao diện dòng lệnh và không quá khó để khởi động và chạy - đặc biệt nếu bạn đọc câu chuyện trang bìa về nó trên tạp chí kiến ​​trúc sư PHP .


11

Lời khuyên tốt là hãy xem các dự án khác đã làm như thế nào. Nhiều người gọi đã cài đặt các plugin và "tên" của họ đã được đăng ký cho các dịch vụ (như wordpress hiện) để bạn có "điểm" trong mã của mình, nơi bạn gọi một chức năng xác định người nghe đã đăng ký và thực thi chúng. Một mẫu thiết kế OO tiêu chuẩn là Mẫu quan sát , đây sẽ là một lựa chọn tốt để thực hiện trong một hệ thống PHP hướng đối tượng thực sự.

Các Zend Framework tận dụng nhiều phương pháp hooking, và rất độc đáo kiến trúc. Đó sẽ là một hệ thống tốt để xem xét.


8

Tôi ngạc nhiên khi hầu hết các câu trả lời ở đây dường như đều hướng đến các plugin là cục bộ của ứng dụng web, tức là các plugin chạy trên máy chủ web cục bộ.

Còn nếu bạn muốn các plugin chạy trên một máy chủ khác - từ xa thì sao? Cách tốt nhất để làm điều này là cung cấp một biểu mẫu cho phép bạn xác định các URL khác nhau sẽ được gọi khi các sự kiện cụ thể xảy ra trong ứng dụng của bạn.

Các sự kiện khác nhau sẽ gửi thông tin khác nhau dựa trên sự kiện vừa xảy ra.

Bằng cách này, bạn sẽ thực hiện cuộc gọi cURL tới URL đã được cung cấp cho ứng dụng của bạn (ví dụ: qua https) nơi máy chủ từ xa có thể thực hiện các tác vụ dựa trên thông tin được gửi bởi ứng dụng của bạn.

Điều này cung cấp hai lợi ích:

  1. Bạn không phải lưu trữ bất kỳ mã nào trên máy chủ cục bộ (bảo mật)
  2. Mã có thể trên các máy chủ từ xa (khả năng mở rộng) bằng các ngôn ngữ khác nhau ngoài PHP (tính di động)

8
Đây là một "API đẩy" hơn là một hệ thống "plugin" - bạn đang cung cấp một cách để các dịch vụ khác nhận được thông báo về các sự kiện đã chọn. Điều thường có nghĩa là "plugin" là bạn có thể cài đặt ứng dụng và sau đó thêm chức năng để tùy chỉnh hành vi của nó theo mục đích của bạn, yêu cầu plugin phải chạy cục bộ - hoặc ít nhất là có giao tiếp 2 chiều an toàn và hiệu quả để cung cấp thông tin cho ứng dụng không chỉ lấy nó từ nó. Hai tính năng này hơi khác biệt và trong nhiều trường hợp, "nguồn cấp dữ liệu" (ví dụ RSS, iCal) là một thay thế đơn giản cho API đẩy.
IMSoP
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.