2008-08-01 28 views
244

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 một cái gì đó mà mọi người có thể mở rộng bằng cách sử dụng giao diện plugin.Cách tốt nhất để cho phép các plugin cho ứng dụng PHP

Làm cách nào để viết "móc" vào mã của họ để các plugin có thể đính kèm vào các sự kiện cụ thể?

Trả lời

149

Bạn có thể sử dụng mẫu Observer. Một cách chức năng đơ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; 
?> 

Output:

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 bạn trước khi nguồn thực tế mã mà bạn muốn được 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 chuyển tới 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ê những đối số nào được truyền cho mỗi móc.

Đây chỉ là một phương pháp hoàn thành 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 xem Tài liệu WordPress để biết thêm thông tin.

Rất tiếc, các ký tự gạch dưới xuất hiện bằng các thực thể HTML bằng Markdown? Tôi có thể đăng lại mã này khi lỗi này được sửa.

Edit: Nevermind, nó chỉ xuất hiện theo cách đó khi bạn đang chỉnh sửa

+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: http://www.php.net/manual/en/class.splobserver.php –

+19

Lưu ý về lưu ý: đây không phải là ví dụ về mẫu Observer. Đó là một ví dụ về [Mô hình hòa giải '] (http://sourcemaking.com/design_patterns/mediator). 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 đi qua hoặc thông báo có điều kiện (cũng không có một người quản lý trung tâm để kiểm soát các 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 bằng tên sai ... – ircmaxell

+0

Lưu ý rằng khi sử dụng nhiều móc/trình nghe, bạn chỉ nên trả về chuỗi hoặc mảng, không phải cả hai. Tôi đã thực hiện một cái gì đó tương tự cho Hound CMS - https://getbutterfly.com/hound/. – Ciprian

13

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

Câu trả lời trực tiếp hơn là viết các tệp chung mà họ sẽ "include_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 "hooks.php" MASSIVE. Tuy nhiên, hãy cẩn thận, bởi vì điều cuối cùng 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ải thiện. Cố gắng giữ cho các phụ thuộc API thấp. I.E ít tệp hơn để chúng bao gồm.

+0

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

31

Các mócnghe phương pháp là phổ biến nhất được sử dụng, nhưng có những điều khác mà 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 bạn xem mã (đây có phải là tập lệnh FOSS hay thứ gì đó trong nhà) sẽ ảnh hưởng rất lớn đến cách bạn muốn cho phép plugin.

kdeloach có một ví dụ hay, nhưng chức năng triển khai và móc của anh ta không an toàn chút nào. 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 xem các plugin phù hợp.

+1 to kdeloach from me.

12

Có một dự án gọn gàng được gọi là Stickleback bởi Matt Zandstra tại Yahoo xử lý nhiều công việc để xử lý 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ó để thiết lập và chạy - đặc biệt nếu bạn đọc câu chuyện về nó trong PHP architect magazine.

9

Lời khuyên tốt là xem các dự án khác đã thực hiện nó như thế nào. Nhiều người kêu gọi cài đặt plugin và "tên" của họ được đăng ký cho các dịch vụ (như wordpress) vì vậy bạn có "điểm" trong mã của mình, nơi bạn gọi một hàm xác định người nghe đã đăng ký và thực hiện chúng. Một thiết kế OO tiêu chuẩn là Observer Pattern, đâ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ự.

Zend Framework sử dụng nhiều phương pháp gắn kết và được thiết kế rất đẹp mắt. Đó sẽ là một hệ thống tốt để xem xét.

18

Đây là một cách tiếp cận mà tôi đã sử dụng, đó là một nỗ lực để sao chép từ cơ chế tín hiệu/khe Qt, một loại mẫu Observer. Đối tượng có thể phát ra tín hiệu. Mỗi tín hiệu có một 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 người nhận, đơn giản là "có thể gọi" Bạn sử dụng lớp xe buýt để chuyển tín hiệu cho bất kỳ ai quan tâm đến việc nhận chúng Khi có điều gì đó xảy ra, bạn "gửi" một tín hiệu. Dưới đây là ví dụ và 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(); 

?> 
49

Vì vậy, hãy nói rằng bạn không muốn mẫu Observer bởi vì nó đòi hỏi bạn phải thay đổi phương pháp lớp học của bạn để xử lý các nhiệm vụ 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 thừa kế extends vì bạn có thể đã kế thừa trong lớp của mình từ một số lớp khác. Nó sẽ không tuyệt vời khi có một cách chung chung để làm cho bất kỳ lớp học nào có thể cắm bất kỳ nỗ lực nào mà không cần nỗ lực nhiều? Dưới đâ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 lời gọi require_once() ở đầu tập lệnh PHP của bạn. Nó tải các lớp để làm một cái gì đó có thể cắm được.

Trong phần 2, đó là nơi chúng tôi tải lớp học. Lưu ý Tôi không phải làm bất cứ điều gì đặc biệt cho lớp, điều này khác biệt đáng kể so với mẫu Observer.

Trong phần 3, đó là nơi chúng tôi chuyển lớp của chúng tôi thành "có thể cắm" (nghĩa là, hỗ trợ các plugin cho phép chúng tôi ghi đè lên các phương thức và thuộc tính 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 đăng ký plugin, và bạn có thể kích hoạt các plugin ở đây. Cũng thông báo chức năng Dog_bark_beforeEvent(). Nếu tôi đặt $mixed = 'BLOCK_EVENT' trước câu lệnh trả về, nó sẽ chặn con chó khỏi sủa và cũng sẽ chặn Dog_bark_afterEvent vì sẽ không có bất kỳ 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 không chạy như thế chút nào. Ví dụ, con chó không công bố tên của nó là 'Fido', nhưng 'Coco'. Con chó không nói 'meow', nhưng 'Woof'. Và khi bạn muốn nhìn vào tên của con chó sau đó, bạn thấy nó là 'Khác biệt' thay vì 'Coco'. Tất cả những ghi đè đó được cung cấp trong Phần 3.

Vậy cách thức hoạt động của tính năng này? Vâng, hãy loại trừ eval() (mà mọi người nói là "ác") và loại trừ rằng đó không phải là mẫu Observer. 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. Vì vậy, vì điều đó xảy ra, các phương pháp ma thuật sẽ thu hút chúng tôi. Đó là lý do tại sao trong phần 3 và 4 chúng ta gây rối với đối tượng bắt nguồn từ lớp Pluggable, không phải chính lớp Dog.Thay vào đó, chúng ta để cho lớp Plugin làm "chạm" trên đối tượng Dog cho chúng ta. (Nếu đó là một số kiểu thiết kế mà tôi không biết - hãy cho tôi biết.)

+3

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

+1

Tôi [đọc lên trên Wikipedia về điều này] (http://en.wikipedia.org/wiki/Decorator_pattern) và, whoa, bạn nói đúng! :) – Volomike

6

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

Còn nếu bạn muốn plugin chạy trên máy chủ từ xa khác thì sao? Cách tốt nhất để làm điều này là cung cấp 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 chỉ cần 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ụ: trên 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.

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

  1. Bạn không cần phải lưu trữ bất kỳ mã trên máy chủ nội bộ của bạn (an ninh)
  2. Mã này có thể được trên các máy chủ từ xa (mở rộng) trong ngôn ngữ khác nhau khác thì chương trình PHP (tính di động)
+8

Đây là một "API đẩy" nhiều hơn 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 thông báo về các sự kiện đã chọn. Nói chung, "plugin" có nghĩa 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 ứng dụng, yêu cầu plugin phải chạy cục bộ - hoặc ít nhất 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

Các vấn đề liên quan