วิธีที่ดีที่สุดในการอนุญาตปลั๊กอินสำหรับแอปพลิเคชัน PHP


276

ฉันเริ่มต้นเว็บแอปพลิเคชันใหม่ใน PHP และคราวนี้ฉันต้องการสร้างสิ่งที่ผู้คนสามารถขยายได้โดยใช้ส่วนต่อประสานปลั๊กอิน

วิธีการหนึ่งที่เกี่ยวกับการเขียน 'hooks' ลงในรหัสของพวกเขาเพื่อให้ปลั๊กอินสามารถแนบกับเหตุการณ์ที่เฉพาะเจาะจงได้อย่างไร

คำตอบ:


162

คุณสามารถใช้รูปแบบการสังเกตการณ์ วิธีการทำงานที่ง่ายเพื่อให้บรรลุสิ่งนี้:

<?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 ประสบความสำเร็จ มีทางเลือกที่ดีกว่าฉันขอแนะนำให้คุณตรวจสอบเอกสาร WordPress สำหรับข้อมูลเพิ่มเติม


3
โปรดทราบว่าสำหรับ PHP> = 5.0 คุณสามารถใช้สิ่งนี้ได้โดยใช้อินเตอร์เฟสผู้สังเกตการณ์ / หัวเรื่องที่กำหนดใน SPL: php.net/manual/en/class.splobserver.php
John Carter

20
หมายเหตุคนเดินเท้า: นี่ไม่ใช่ตัวอย่างของรูปแบบการสังเกตการณ์ Mediator Patternมันเป็นตัวอย่างของการที่ ผู้สังเกตการณ์ที่แท้จริงคือการแจ้งเตือนอย่างแท้จริงไม่มีการส่งข้อความหรือการแจ้งเตือนแบบมีเงื่อนไข (และไม่มีผู้จัดการกลางสำหรับการควบคุมการแจ้งเตือน) มันไม่ได้ทำให้คำตอบผิดแต่ควรสังเกตเพื่อหยุดคนเรียกสิ่งต่าง ๆ โดยใช้ชื่อที่ไม่ถูกต้อง ...
ircmaxell

โปรดทราบว่าเมื่อใช้ hooks / Listeners หลายรายการคุณควรส่งคืนสตริงหรืออาร์เรย์อย่างใดอย่างหนึ่งเท่านั้นไม่ใช่ทั้งสองอย่าง ฉันได้ดำเนินการบางสิ่งบางอย่างที่คล้ายกันสำหรับ Hound CMS - getbutterfly.com/hound
Ciprian

59

ดังนั้นสมมติว่าคุณไม่ต้องการรูปแบบการสังเกตเนื่องจากมันต้องการให้คุณเปลี่ยนวิธีการเรียนเพื่อจัดการงานการฟังและต้องการบางสิ่งบางอย่างทั่วไป และสมมติว่าคุณไม่ต้องการใช้การextendsสืบทอดเนื่องจากคุณอาจสืบทอดในคลาสของคุณจากคลาสอื่น มันจะไม่ดีที่จะมีวิธีการทั่วไปที่จะทำให้pluggable ระดับใด ๆ โดยไม่ต้องใช้ความพยายามมาก ? นี่คือวิธี:

<?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 นั่นคือที่ที่เราโหลดคลาส หมายเหตุฉันไม่ต้องทำอะไรเป็นพิเศษในชั้นเรียนซึ่งแตกต่างจากรูปแบบการสังเกตการณ์อย่างมีนัยสำคัญ

ในส่วนที่ 3 นั่นคือที่ที่เราสลับคลาสของเราให้กลายเป็น "เสียบได้" (นั่นคือรองรับปลั๊กอินที่ให้เราแทนที่เมธอดและคุณสมบัติของคลาส) ตัวอย่างเช่นหากคุณมีเว็บแอพคุณอาจมีปลั๊กอินรีจิสตรีและคุณสามารถเปิดใช้งานปลั๊กอินได้ที่นี่ แจ้งให้ทราบด้วยDog_bark_beforeEvent()ฟังก์ชั่น หากฉันตั้งค่าไว้$mixed = 'BLOCK_EVENT'ก่อนหน้าคำสั่งส่งคืนมันจะบล็อกสุนัขไม่ให้เห่าและจะบล็อก Dog_bark_afterEvent เพราะจะไม่มีเหตุการณ์ใด ๆ

ในส่วนที่ 4 นั่นคือรหัสการดำเนินการปกติ แต่สังเกตว่าสิ่งที่คุณอาจคิดว่าจะทำงานไม่ทำงานอย่างนั้น ตัวอย่างเช่นสุนัขไม่ได้ประกาศชื่อเป็น 'Fido' แต่เป็น 'Coco' สุนัขไม่ได้พูดว่า 'meow' แต่ 'Woof' และเมื่อคุณต้องการดูชื่อสุนัขหลังจากนั้นคุณจะพบว่า 'แตกต่าง' แทน 'Coco' การแทนที่ทั้งหมดนั้นมีให้ในส่วนที่ 3

แล้วมันทำงานอย่างไร ทีนี้มาแยกกันeval()(ซึ่งทุกคนบอกว่าเป็น "ความชั่วร้าย") และแยกแยะว่ามันไม่ใช่รูปแบบผู้สังเกต ดังนั้นวิธีการทำงานก็คือคลาสที่ว่างเปล่าซึ่งเรียกว่า Pluggable ซึ่งไม่มีวิธีการและคุณสมบัติที่ใช้โดยคลาส Dog ดังนั้นตั้งแต่ที่เกิดขึ้นวิธีการที่วิเศษจะมีส่วนร่วมสำหรับเรา นั่นเป็นเหตุผลที่ในส่วนที่ 3 และ 4 เรายุ่งกับวัตถุที่ได้มาจากคลาส Pluggable ไม่ใช่คลาส Dog แต่เราปล่อยให้คลาสปลั๊กอินทำการ "แตะ" บนวัตถุ Dog แทนเรา (ถ้าเป็นรูปแบบการออกแบบที่ฉันไม่รู้ - กรุณาแจ้งให้เราทราบ)


3
นี่ไม่ใช่มัณฑนากรหรือ
MV


35

เบ็ดและฟังเป็นวิธีการที่ใช้กันมากที่สุด แต่มีสิ่งอื่น ๆ ที่คุณสามารถทำได้ ขึ้นอยู่กับขนาดของแอพของคุณและใครที่คุณจะอนุญาตให้ดูรหัส (นี่จะเป็นสคริปต์ FOSS หรืออะไรบางอย่างในบ้าน) จะมีอิทธิพลอย่างมากต่อวิธีที่คุณต้องการอนุญาตปลั๊กอิน

kdeloach มีตัวอย่างที่ดี แต่การใช้งานและฟังก์ชั่น hook ของเขานั้นไม่ปลอดภัย ฉันจะขอให้คุณให้ข้อมูลเพิ่มเติมเกี่ยวกับลักษณะของ php app ที่คุณเขียนและวิธีที่คุณเห็นปลั๊กอินที่เหมาะสม

+1 ถึง kdeloach จากฉัน


25

นี่เป็นวิธีที่ฉันใช้มันเป็นความพยายามที่จะคัดลอกจากกลไกสัญญาณ / ช่องเสียบ Qt ซึ่งเป็นรูปแบบ Observer ชนิดหนึ่ง วัตถุสามารถส่งสัญญาณ สัญญาณทั้งหมดมี ID ในระบบ - มันประกอบด้วย id ของผู้ส่ง + ชื่อวัตถุทุกสัญญาณสามารถผูกกับตัวรับซึ่งก็คือ "callable" คุณใช้คลาสบัสเพื่อส่งสัญญาณไปยังใครก็ตามที่สนใจจะรับเมื่อมีบางอย่าง เกิดขึ้นคุณ "ส่ง" สัญญาณ ด้านล่างนี้คือตัวอย่างการใช้งาน

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

ฉันเชื่อว่าวิธีที่ง่ายที่สุดคือการทำตามคำแนะนำของ Jeff และดูรหัสที่มีอยู่ ลองดูที่ Wordpress, Drupal, Joomla และ CMS อื่น ๆ ที่เป็นที่รู้จักกันดีของ PHP เพื่อดูว่า API ของพวกเขามีลักษณะอย่างไร วิธีนี้คุณสามารถรับแนวคิดที่คุณอาจไม่เคยคิดมาก่อนเพื่อทำให้สิ่งต่าง ๆ เป็นสีแดงเข้มยิ่งขึ้น

คำตอบที่ตรงกว่าคือการเขียนไฟล์ทั่วไปที่พวกเขาจะ "รวมเข้าด้วยกัน" ลงในไฟล์ของพวกเขาที่จะให้การใช้งานที่พวกเขาต้องการ สิ่งนี้จะถูกแบ่งออกเป็นหมวดหมู่และไม่มีให้ในไฟล์ "hooks.php" ขนาดใหญ่หนึ่งไฟล์ ระวังเพราะสิ่งที่เกิดขึ้นคือไฟล์ที่พวกเขารวมถึงการมีการพึ่งพาและการทำงานที่ดีขึ้น พยายามทำให้ API อ้างอิงต่ำ IE ไฟล์ที่น้อยลงสำหรับพวกเขาที่จะรวม


ฉันจะเพิ่ม DokuWiki ในรายการระบบที่คุณอาจจะได้ดู มีระบบเหตุการณ์ที่ดีที่ช่วยให้ระบบนิเวศปลั๊กอินที่อุดมไปด้วย
chiborg

15

มีโครงการที่ประณีตชื่อว่าSticklebackโดย Matt Zandstra ที่ Yahoo ซึ่งจัดการงานส่วนใหญ่สำหรับจัดการปลั๊กอินใน PHP

มันบังคับใช้อินเตอร์เฟซของชั้นปลั๊กอินสนับสนุนอินเตอร์เฟซบรรทัดคำสั่งและไม่ยากเกินไปที่จะได้รับและทำงาน - โดยเฉพาะอย่างยิ่งถ้าคุณอ่านหนังสือเรื่องเกี่ยวกับเรื่องนี้ในนิตยสารสถาปนิก PHP


11

คำแนะนำที่ดีคือดูว่าโครงการอื่น ๆ ได้ทำไปแล้ว หลายคนเรียกร้องให้ติดตั้งปลั๊กอินและลงทะเบียนชื่อ "บริการ" (เช่นเดียวกับ wordpress) ดังนั้นคุณจึงมี "คะแนน" ในรหัสของคุณที่คุณเรียกใช้ฟังก์ชันที่ระบุผู้ฟังที่ลงทะเบียนและดำเนินการ ลวดลายการออกแบบมาตรฐาน OO คือรูปแบบการสังเกตการณ์ซึ่งจะเป็นตัวเลือกที่ดีในการนำไปใช้ในระบบ PHP เชิงวัตถุอย่างแท้จริง

Zend Frameworkทำให้การใช้วิธีการ hooking จำนวนมากและมีการออกแบบอย่างมาก นั่นจะเป็นระบบที่ดีในการดู


8

ฉันประหลาดใจที่คำตอบส่วนใหญ่ที่นี่ดูเหมือนจะมุ่งเน้นเกี่ยวกับปลั๊กอินที่อยู่ในท้องถิ่นของแอปพลิเคชันเว็บเช่นปลั๊กอินที่ทำงานบนเว็บเซิร์ฟเวอร์ท้องถิ่น

ถ้าคุณต้องการให้ปลั๊กอินทำงานบนเซิร์ฟเวอร์ระยะไกลตัวอื่น วิธีที่ดีที่สุดในการทำเช่นนี้คือจัดเตรียมแบบฟอร์มที่ให้คุณกำหนด URL ที่แตกต่างกันซึ่งจะถูกเรียกเมื่อมีเหตุการณ์เฉพาะเกิดขึ้นในแอปพลิเคชันของคุณ

เหตุการณ์ต่าง ๆ จะส่งข้อมูลที่แตกต่างกันไปตามเหตุการณ์ที่เพิ่งเกิดขึ้น

ด้วยวิธีนี้คุณจะทำการเรียก cURL ไปยัง URL ที่ให้ไว้ในแอปพลิเคชันของคุณ (เช่นผ่าน https) ซึ่งเซิร์ฟเวอร์ระยะไกลสามารถทำงานตามข้อมูลที่แอปพลิเคชันของคุณส่งมา

สิ่งนี้มีสองประโยชน์:

  1. คุณไม่จำเป็นต้องโฮสต์รหัสใด ๆ บนเซิร์ฟเวอร์ภายในเครื่องของคุณ (ความปลอดภัย)
  2. รหัสสามารถอยู่บนเซิร์ฟเวอร์ระยะไกล (ความสามารถในการขยาย) ในภาษาที่แตกต่างอื่น ๆ แล้ว PHP (การพกพา)

8
นี่เป็น "push API" มากกว่าระบบ "ปลั๊กอิน" - คุณกำลังให้บริการสำหรับบริการอื่น ๆ เพื่อรับการแจ้งเตือนของกิจกรรมที่เลือก โดยทั่วไปสิ่งที่มีความหมายโดย "ปลั๊กอิน" คือคุณสามารถติดตั้งแอปพลิเคชันแล้วเพิ่มฟังก์ชันการทำงานเพื่อปรับแต่งพฤติกรรมตามวัตถุประสงค์ของคุณซึ่งต้องการให้ปลั๊กอินทำงานภายในเครื่องหรืออย่างน้อยก็มีการสื่อสารสองทางที่ปลอดภัยและมีประสิทธิภาพ ข้อมูลไปยังแอปพลิเคชันไม่เพียง แต่นำมาจากมัน คุณสมบัติสองอย่างนั้นค่อนข้างแตกต่างกันไปและสำหรับหลาย ๆ กรณี "ฟีด" (เช่น RSS, iCal) เป็นทางเลือกที่ง่ายในการพุช API
IMSoP
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.