Najlepszy sposób na zezwolenie na wtyczki dla aplikacji PHP


276

Zaczynam nową aplikację internetową w PHP i tym razem chcę stworzyć coś, co ludzie mogą rozszerzyć za pomocą interfejsu wtyczki.

W jaki sposób można napisać „haki” w kodzie, aby wtyczki mogły dołączać się do określonych zdarzeń?

Odpowiedzi:


162

Możesz użyć wzorca Observer. Prosty funkcjonalny sposób na osiągnięcie tego:

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

Wynik:

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

Uwagi:

W tym przykładowym kodzie źródłowym musisz zadeklarować wszystkie swoje wtyczki przed rzeczywistym kodem źródłowym, który chcesz rozszerzyć. Podałem przykład obsługi pojedynczej lub wielu wartości przekazywanych do wtyczki. Najtrudniejszą częścią tego jest napisanie faktycznej dokumentacji, która zawiera listę argumentów przekazywanych do każdego haka.

To tylko jedna metoda na stworzenie systemu wtyczek w PHP. Są lepsze alternatywy, sugeruję zapoznać się z Dokumentacją WordPress, aby uzyskać więcej informacji.


3
Zauważ, że dla PHP> = 5.0 możesz to zaimplementować za pomocą interfejsów Observer / Subject zdefiniowanych w SPL: php.net/manual/en/class.splobserver.php
John Carter

20
Uwaga pedantyczna: nie jest to przykład wzorca Observer. To przykład Mediator Pattern. Prawdziwi obserwatorzy są wyłącznie powiadomieniami, nie ma przekazywania wiadomości ani powiadomień warunkowych (nie ma też centralnego menedżera do kontrolowania powiadomień). Nie powoduje to, że odpowiedź jest zła , ale należy zauważyć, aby ludzie nie nazywali rzeczy złym imieniem ...
ircmaxell,

Zauważ, że jeśli używasz wielu zaczepów / detektorów, powinieneś zwracać tylko łańcuchy lub tablice, a nie oba. Zaimplementowałem coś podobnego dla Hound CMS - getbutterfly.com/hound .
Ciprian

59

Powiedzmy, że nie chcesz wzorca Observer, ponieważ wymaga to zmiany metod klas, aby obsłużyć zadanie nasłuchiwania, i chcesz czegoś ogólnego. Powiedzmy, że nie chcesz używać extendsdziedziczenia, ponieważ możesz już dziedziczyć w swojej klasie od innej klasy. Czy nie byłoby wspaniale mieć ogólny sposób na podłączenie dowolnej klasy bez większego wysiłku ? Oto jak:

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

W części pierwszej możesz zawrzeć require_once()wywołanie u góry skryptu PHP. Ładuje klasy, aby coś można podłączyć.

W części 2 właśnie tam ładujemy klasę. Uwaga: Nie musiałem robić nic specjalnego w klasie, co znacznie różni się od wzorca Obserwatora.

W części 3, gdzie zamieniamy naszą klasę na „wtykową” (to znaczy obsługuje wtyczki, które pozwalają nam przesłonić metody klasowe i właściwości). Na przykład, jeśli masz aplikację internetową, możesz mieć rejestr wtyczek i możesz tutaj aktywować wtyczki. Zwróć także uwagę na Dog_bark_beforeEvent()funkcję. Jeśli $mixed = 'BLOCK_EVENT'ustawię przed instrukcją return, zablokuje to szczekanie psa, a także zablokuje Dog_bark_afterEvent, ponieważ nie będzie żadnego zdarzenia.

W części 4 jest to normalny kod działania, ale zauważ, że to, co według ciebie mogłoby się uruchomić, wcale tak nie działa. Na przykład pies nie ogłasza swojej nazwy „Fido”, ale „Coco”. Pies nie mówi „miau”, ale „hau”. A kiedy później chcesz zobaczyć imię psa, okazuje się, że jest to „Różne” zamiast „Coco”. Wszystkie te zastąpienia podano w części 3.

Jak to działa? Cóż, wykluczmy eval()(który wszyscy mówią, że jest „zły”) i wykluczymy, że nie jest to wzorzec Obserwatora. Tak więc działa to podstępna pusta klasa o nazwie Pluggable, która nie zawiera metod i właściwości używanych przez klasę Dog. Ponieważ tak się dzieje, magiczne metody zaangażują się dla nas. Dlatego w częściach 3 i 4 rozmawiamy z obiektem pochodzącym z klasy wtykowej, a nie z samą klasą Dog. Zamiast tego pozwalamy klasie Plugin wykonać dla nas „dotykanie” obiektu Dog. (Jeśli to jakiś wzorzec projektowy, o którym nie wiem - daj mi znać.)


3
Czy to nie dekorator?
MV.

1
I przeczytać na Wikipedii na ten temat , a zaraz, masz rację! :)
Volomike

35

Hak i słuchacz metoda jest najbardziej powszechnie używane, ale są jeszcze inne rzeczy, które możesz zrobić. W zależności od wielkości aplikacji i tego, komu pozwolisz zobaczyć kod (czy to będzie skrypt FOSS, czy coś w domu), będzie to miało duży wpływ na to, jak chcesz zezwolić na wtyczki.

kdeloach ma dobry przykład, ale jego implementacja i funkcja przechwytywania jest trochę niebezpieczna. Poprosiłbym cię o podanie więcej informacji na temat natury aplikacji php podczas pisania oraz o tym, jak widzisz odpowiednie wtyczki.

+1 do kdeloach ode mnie.


25

Oto podejście, którego użyłem, to próba skopiowania z mechanizmu sygnałów / szczelin Qt, rodzaj wzorca obserwatora. Obiekty mogą emitować sygnały. Każdy sygnał ma identyfikator w systemie - składa się z identyfikatora nadawcy + nazwy obiektu Każdy sygnał można powiązać z odbiornikami, co jest po prostu „wywoływalne”. Korzystasz z klasy magistrali, aby przekazywać sygnały wszystkim zainteresowanym ich otrzymaniem. zdarza się, że „wysyłasz” sygnał. Poniżej znajduje się przykładowa implementacja

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

Uważam, że najłatwiejszym sposobem byłoby postępowanie według własnych rad Jeffa i rozejrzenie się po istniejącym kodzie. Spróbuj spojrzeć na Wordpress, Drupal, Joomla i inne dobrze znane CMS-y oparte na PHP, aby zobaczyć, jak wyglądają i działają ich zaczepy API. W ten sposób możesz nawet uzyskać pomysły, o których wcześniej nawet nie myślałeś, aby uczynić z nich coś więcej.

Bardziej bezpośrednią odpowiedzią byłoby zapisanie ogólnych plików, które „włączają_once” do swojego pliku, co zapewni użyteczność, której będą potrzebować. Zostałoby to podzielone na kategorie i NIE zapewnione w jednym MASYWNYM pliku „hooks.php”. Bądź jednak ostrożny, ponieważ w rezultacie dołączane do nich pliki mają coraz więcej zależności i poprawia funkcjonalność. Staraj się utrzymywać niskie zależności API. IE mniej plików, aby je uwzględnić.


Dodałbym DokuWiki do listy systemów, które możesz zobaczyć. Ma ładny system wydarzeń, który pozwala na bogaty ekosystem wtyczek.
chiborg,

15

Jest fajny projekt o nazwie Stickleback autorstwa Matta Zandstry w Yahoo, który zajmuje się znaczną częścią pracy z wtyczkami w PHP.

Wymusza interfejs klasy wtyczek, obsługuje interfejs wiersza poleceń i nie jest zbyt trudny do uruchomienia i uruchomienia - szczególnie jeśli przeczytasz o tym okładkę w magazynie dla architektów PHP .


11

Dobra rada to przyjrzeć się, jak to zrobiły inne projekty. Wielu wzywa do zainstalowania wtyczek i zarejestrowania ich „nazwy” dla usług (tak jak robi to Wordpress), więc masz „punkty” w kodzie, w których wywołujesz funkcję, która identyfikuje zarejestrowanych słuchaczy i wykonuje je. Standardowym wzorcem projektowym OO jest wzorzec obserwatora , który byłby dobrą opcją do wdrożenia w prawdziwie obiektowym systemie PHP.

Zend Framework umożliwia korzystanie z wielu metod zahaczających i jest bardzo ładnie architekturę. To byłby dobry system do obejrzenia.


8

Dziwię się, że większość odpowiedzi tutaj wydaje się dotyczyć wtyczek lokalnych dla aplikacji WWW, tj. Wtyczek działających na lokalnym serwerze WWW.

A jeśli chcesz, aby wtyczki działały na innym - zdalnym - serwerze? Najlepszym sposobem na to byłoby udostępnienie formularza, który pozwala zdefiniować różne adresy URL, które będą wywoływane, gdy wystąpią określone zdarzenia w aplikacji.

Różne zdarzenia wysyłałyby różne informacje na podstawie zdarzenia, które właśnie miało miejsce.

W ten sposób po prostu wykonasz wywołanie cURL na adres URL, który został podany twojej aplikacji (np. Przez https), gdzie zdalne serwery mogą wykonywać zadania w oparciu o informacje wysłane przez twoją aplikację.

Zapewnia to dwie korzyści:

  1. Nie musisz hostować żadnego kodu na swoim lokalnym serwerze (bezpieczeństwo)
  2. Kod może znajdować się na zdalnych serwerach (rozszerzalność) w różnych językach innych niż PHP (przenośność)

8
To bardziej „push API” niż system „pluginów” - zapewniasz innym usługom możliwość otrzymywania powiadomień o wybranych zdarzeniach. Przez „wtyczki” rozumie się ogólnie to, że można zainstalować aplikację, a następnie dodać funkcjonalność, aby dostosować jej zachowanie do swoich celów, co wymaga uruchomienia wtyczki lokalnie - lub przynajmniej zapewnienia bezpiecznej i wydajnej dwukierunkowej komunikacji w celu zapewnienia informacje do aplikacji, nie tylko ją z niej biorą . Te dwie funkcje są nieco odmienne i w wielu przypadkach „kanał” (np. RSS, iCal) jest prostą alternatywą dla push API.
IMSoP,
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.