Jak w Cake próbujemy zrobić “prawdziwy” plugin

Oficjalnie CakePHP daje możliwość budowania pluginów, jednak sprowadza się to na razie jedynie do umożliwienia oddzielenia logiki “mini aplikacji” (którą jest plugin właśnie) od aplikacji głównej. Jednak, żeby plugin był pluginem, musi mieć możliwość łatwego plugIN oraz plugOUT naszej mini-aplikacji.

Pierwsze nasze (w firmie) podejście skutkowało “pluginem” newslettera, który był wręcz komicznie powiązany z aplikacją główną. Używał jej modeli, żeby pobrać Userów do wysyłki i nie tylko. Podłączenie i odłączenie pluginy od jakiekolwiek aplikacji oznaczało krach aplikacji w kilku miejscach i wymagało sporej zabawy z kodem. Nie tak powinno się to odbywać.

Z pluginami próbowałem bawić się przy okazji innych projektów stykałem się z tym problemem i powoli w głowie kiełkowały pomysły. Okazja aby je wypróbować nadarzyła się niedawno. Przy budowie nowego portalu potrzebowaliśmy forum. Wcześniej przy okazji dobrzemieszkaj.pl udało nam się “spiąć” phpBB z naszą aplikacją, ale okazało się to droga przez mękę. Do tego praktycznie nie było mowy o modyfikacji funkcjonalności phpBB.

Jako, że “nasze” forum ma być mniejsze (funkcjonalnie), jednak ma mieć możliwość łatwiejszego integrowania go z naszymi wszystkimi aplikacjami – rozsądnym wydało się napisanie go od zera. Jako plugin właśnie.

Jedyną zasadą jakiej wymagałem od pluginu była niezależność i maksymalna bezkolizyjność pluginy z aplikacją i innymi pluginami.

Teraz szczegóły:
1. po pierwsze plugin powinien mieć możliwość posiadania odseparowanej bazy danych. Czy to faktycznie innej, czy tylko poprzez zdefiniowanie innych prefixów. Dlatego został zdefiniowany dla niego osobna konfiguracja, oraz w ForumAppModel, po którym mają dziedziczyć modele tego pluginu wymusiliśmy korzystanie z tej konfiguracji

// /app/config/database.php
var $forum_plugin = array(
'driver' => 'mysql',
'persistent' => false,
'host' => 'localhost',
'login' => 'login',
'password' => '*******',
'database' => 'database',
'prefix' => 'forum_plugin_',
);
// /app/plugins/forum/forum_app_model.php
class ForumAppModel extends Model {
var $useDbConfig = 'forum_plugin';

var $actsAs = array("Containable");
}

W ten sposób jesteśmy bardziej zabezpieczeni przed kolizjami w bazie.

2. Wiadomo było, że nasz plugin będzie musiał korzystać z przynajmniej dwóch mechanizmów dostarczanych przez aplikację główną: Userów (logowanie, znajomi etc.) i Tagów (tematy miały być tagowane).

Postanowiłem wykorzystać koncepcję interfejsów z języka Java mimo, iż takowe w PHP nie istnieją. Od czego jednak wyobraźnia? Długo wahałem się gdzie taki interfejs miałby siedzieć – najlepszym miejscem wydał się komponent.


/**
* Interfejs User.
* Jeśli chcesz, aby forum plugin współdziałał z Twoimi klasami zarządzania użytkownikami - możesz
* tutaj ewentualnie oprogramować odpowiednie metody interfejsu. Jednak lepszym pomysłem jest podanie
* ścieżek do kontrolerów i akcji w bootstrap.php
*
*/
class IUserComponent extends Object {

function startup(&$controller){
;
}

/**
* @return int id zalogowanego użytkownika
*/
function getLoggedInUserId() {
return 1;
}

/**
* @param int $id id użytkownika
* @return string nazwa użytkownika
*/
function getUserName($id) {

}

/**
* Ma zwracać id użytkownika na podstawie jego nazwy
*
* @param string $name nazwa użytkownika
* @return mixed id użytkownika, lub false, jeśli nie istnieje żaden o takiej nazwie
*/
function getUserIdFromName($name) {
return 1;
}

/**
* Funckja ma zwracać listę z nazwami użytkowników ***podobnych*** do parametru $name
* Używana do listy autoComplete
* @param string $name
* @return array tablica użytkowników
*/
function listUsersByName($name) {
return array(1 => 'User1', 'User2', 'User3', 'User4', 'User5', 'User6', 'User7',
'User8', 'User9');
}

/**
* Funkcja pobiera listę użytkowników - przyjaciół zalogowanego użytkownika
* @return array
*/
function getUserFriends(){
return array(1=>'User 1', 2=>'User 2', 3=>'User 3', 4=>'User 4');
}

}
// oczywiście te metody muszą zostać zaimplementowane przy "spinaniu" pluginu z aplikacją główną

I od tej pory KAŻDA próba odczytu danych o użytkownikach musi przechodzić przez ten interfejs. Utrzymanie tej zasady było o tyle proste, że plugin kodowaliśmy w oderwaniu od aplikacji głównej (dopiero po jakimś czasie spróbowaliśmy ją spiąć z aplikacją, co się udało).

Jakie były plusy tego rozwiązania?

  • można pominąć brak implementacji elementów, które będą kluczowe dla działania pluginu (np User) gdyż jesteśmy schowani za interfejsem, który na razie udaje, że działa
  • przy “spinaniu” pluginu należy się co najwyżej zająć implementacją interfejsu. Najlepiej byłoby gdyby, wszystkie z metod można było wywołać przy pomocy requestAction w pluginie
  • plugin jest niezależny od aplikacji głownej – możemy go rozwijać niezaleźnie

O czym należy pamiętać? O tym, że nie tylko należy uważać, aby plugin nie “splątał” się z aplikacją główną, ale też żeby nie stało się na odwrót. Na przykład musieliśmy wyświetlić na stronie głównej najnowsze wątki. Zrobiliśmy to przez requestAction, jednak wcześniej należało wykonać test, czy plugin istnieje. Gdy zostanie usunięty – nie powinny pojawiać się błędy (a przynajmniej nie zbyt wiele ;))

Jakie są minusy rozwiązania?

  • Gdy pobiorę listę postów i chcę przy nich wyświetlić autora – dla każdego z osobna musze wywołać (przez requestAction na przykład) metodę w kontrolerze pluginu, który wywoła IUser->getUserName($id). Może to rzutować na wydajność.
  • powyższy problem można rozwiązać zapisując login usera w tablicy z postami (redundancja), ale powoduje to rozsynchronizowanie danych (przy zmianie loginu)

Jednak te problemy są mniejsze niż te, które są powodowane ścisłym powiązaniem pluginy z aplikację (wtedy po prostu przestaje był pluginem). Poza tym można by je rozwiązać przy pomocy bardziej wyszukanych mechanizmów obsługi pluginów, jednak o tym napiszę następnym razem.

Share Button

Leave a Reply

Your email address will not be published. Required fields are marked *