Bawiąc się frameworkiem CakePHP (wersja 1.3) natrafiłem ostatnio na pewien problem: wyobraźmy sobie całkiem sporą aplikację opartą o Cake z dziesiątkami widoków. W każdym z tych widoków, co najmniej parę razy korzystamy z metody link() Cake-owego HtmlHelpera np.:
|
metoda domyślnie wygeneruje następujący mark-up:
|
tymczasem potrzebowałem, aby zwracany mark-up miał taką postać:
|
Aby tego dokonać, konieczne jest wywołanie metody link() w ten sposób:
|
Jak widać, aby osiągnąć pożądany mark-up, należy wyłączyć domyślny „escaping” tytułu linku w metodzie link(). Wszystko fajnie – tylko co jeśli chciałbym, aby linki w całej aplikacji miały element span w środku? Przecież nikt przy zdrowych zmysłach nie będzie przeczesywał kilkudziesięciu widoków i zmieniał parametrów wywołań metody link(). Jak ugryźć ten problem? Zanim przedstawię rozwiązanie, przytoczę parę istotnych informacji o helperach w CakePHP.
Helpery w CakePHP
Generalnie w Cake nie ma czegoś takiego jak automatyczne ładowanie helperów w momencie pierwszego użycia w widoku (przynajmniej na chwilę obecną). To, z jakich helperów chcemy korzystać w widoku, określamy za pomocą atrybutu $helpers w kontrolerze np.:
|
Dzięki powyższej konstrukcji, w widoku każdej akcji kontrolera Posts mamy dostęp do HtmlHelpera i RssHelpera. Atrybutu $helpers możemy użyć także w AppController, czyli „kontrolerze wszystkich kontrolerów” – w ten sposób dany helper zostanie załadowany globalnie dla wszystkich widoków w naszej aplikacji:
|
Teraz mała zagadka – jakie helpery zostaną załadowane w widoku akcji index() w PostsController? Prawidłowa odpowiedź: Session, Form, Html, Rss – dzieje się tak, ponieważ tablica $helpers z AppController jest łączona z tablicą $helpers danego kontrolera (w naszym przypadku PostsController).
W tym momencie rodzi się pytanie: co zrobić, jeżeli z danego helpera chcemy skorzystać tylko w jednej akcji danego kontrolera i nie chcemy ładować go dla innych akcji bo np. mamy na względzie oszczędność zasobów serwera? Nic bardziej prostszego:
|
Dzieki temu zabiegowi, trzymając się naszego przykładu, w widoku dla akcji index() mamy dostęp do 5 helperów: Session, Form, Html, Rss, Time. W taki sposób np. metoda paginate() kontrolera ładuje PaginatorHelper, jeśli nie został on jawnie określony w kontrolerze przez użytkownika. Czyli mamy coś na kształt ładowania helperów na żądanie. Dobrą praktyką jest napisanie prostej funkcji pomocniczej do ładowania helperów, którą umieszczamy w AppController, by mieć do niej dostęp z wnętrza każdej akcji każdego kontrolera:
|
Uzbrojeni w te informacje, prześledźmy rozwiązanie problemu z początku tego wpisu.
Złe rozwiązanie
Przedstawia się następująco:
- kopiujemy domyślny Cake-owy
HtmlHelper(plikhtml.php) z:
cake/libs/view/helpersdoapp/view/helpers - zaczynamy grzebać we wnętrzu metody
link()dostosowując ją do własnych potrzeb
Powyższe rozwiązanie ssie z prostego powodu: za każdym razem, gdy wyjdzie nowa wersja frameworka, należy sprawdzić, czy czasem deweloperzy nie zmieniali czegoś w tej metodzie. Jeśli tak – znowu musimy wykonać proces kopiowania pliku i wprowadzania naszych poprawek.
Dobre rozwiązanie
Najlepszym wyjściem jest napisanie własnego helpera, który będzie rozszerzał domyślny HtmlHelper – nasz helper umieszczamy w pliku app/view/helpers/custom_html.php:
|
Jak widać w wierszu 7 wywołujemy oryginalną metodę link() z Cake-owego HtmlHelpera, jednak zanim to nastąpi wykonujemy czynności, które rozwiązują nasz problem z elementem span wewnątrz linku. Oczywiście to tylko przykład, jednak daje nam obraz jak elastycznie możemy wchodzić w interakcję z Cake-owym helperem i zmieniać np. domyślne wartości parametrów. Przy tym musimy wiedzieć tylko i wyłącznie jak wygląda prototyp przeciążanej metody – nie interesuje nas jej wewnętrzna logika.
Aby skorzystać z naszego nowego helpera dodajemy go do tablicy $helpers kontrolera:
|
i już możemy wykorzystywać jego dobrodziejstwa w widoku:
|
Zaraz, zaraz – ale przecież nadal w całej aplikacji wywołania mają postać $this->Html->link() a nie $this->CustomHtml->link(). Okazuje się, że recepta na ten problem jest trywialna – wystarczy proste przypisanie w widoku:
|
To, że powyższy trick działa możemy wykorzystać do ładowania naszych helperów, które rozszerzają domyślne helpery CakePHP i co najważniejsze – odnosimy się do nich przez oryginalne „frameworkowe” nazwy. Zanim Cake załaduje helpery, możemy sprawdzić:
- czy nadpisujemy dany helper w naszej aplikacji (sprawdzamy po prostu, czy istnieje plik o określonej nazwie w
/app/view/helpers) - jeśli istnieje – ładujemy nasz helper w miejsce oryginalnego (czyli np.
CustomHtmlzamiastHtml) - dokonujemy odpowiedniego przypisania nazw helperów w widoku
Idealnym miejscem realizacji dwóch pierwszych punktów z powyższej listy jest callback beforeFilter() w AppController:
|
Oczywiście, jeśli np. w PostsController również korzystamy z callbacka beforeRender() musimy jawnie (najlepiej na końcu) wywołać callback z AppController: parent::beforeRender().
Mapowania naszych helperów na Cake-owe dokonujemy z kolei w callbacku beforeRender() w AppHelper:
|
Być może komuś powyższe rozwiązanie okaże się pomocne :) Więcej ciekawych artykułów na temat CakePHP znajdziecie na blogu Grzegorza Pawlika – webbricks.
Hej, dzięki za podlinkowanie do mojej strony :)
Dzięki też za podpowiedź
$this->Html = $this->CustomHtml.Wcześniej rozwiązywałem to w ten sposób, że na początku tworzyłemCustomHelper extends HtmlHelper {}z myślą, że może trzeba będzie kiedyś nadpisać jego działanie. Twoje rozwiązanie jest ciekawsze.Ale najlepiej byłoby, gdyby można było ładując helper podać pod jaką nazwą ma być dostępny, np:
$helpers = array("Html"); (masz helper Html w $html);$helpers = array("Html" => "CustomHtml"); (helper CustomHelper w $html)nie uważasz? Chociaż to mogłoby wprowadzić pewien bałagan…
Zatem może na przykład callbacki? Jak w przypadku AppController i AppModel (AppHtmlHelper?)
:)
hej,
tak jak napisales na poczatku, zdecydowanie ‚najbezpieczniejszy’ sposob to rozszerzenie wszystkich standardowych Cake-owych helperow juz na poczatku projektu i konsekwentne odnoszenie sie juz do naszych helperow. Przy czym pamietac nalezy, ze sam helper moze ladowac inne helpery – wiec tutaj takze nalezy dokonac odpowiednich zmian nazw w atrybucie
$helpers.To moje rozwiazanie traktuje jako szybki work-around, ktory kiedys moze przestac dzialac :)
Wszystko ładnie i pięknie, ale w cakephp 1.2 jest problem
funkcja _overloadCoreHelpers za bardzo nie ma sensu, ponieważ helper Form potrzebuje Html aby działać, a jak wyczyścimy zbędne form i html, pozostawiając tylko helpery Custom… to się wykrzacza.
Poza tym super artykuł :)
nie wiem jaka jest logika inicjalizacji helperow w 1.2 ale w 1.3 powyzszy kod dziala i nie powoduje zadnych problemow bo uzywam go w swoich projektach :)
w 2.x:
dokladnie, w 2.0 mozna latwo aliasowac helpery wiec ten post traci racje bytu :)