Elastyczne rozszerzanie domyślnych helperów w CakePHP Grzegorz Wójcik

CakePHP logo

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.:

  1. <?php echo $this->Html->link('Strona główna', '/'); ?>

metoda domyślnie wygeneruje następujący mark-up:

  1. <a href="/">Strona główna</a>

tymczasem potrzebowałem, aby zwracany mark-up miał taką postać:

  1. <a href="/"><span>Strona główna</span></a>

Aby tego dokonać, konieczne jest wywołanie metody link() w ten sposób:

  1. <?php echo $this->Html->link('<span>Strona głowna</span>', '/', array('escape' => 'false')); ?>

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.:

  1. class PostsController extends AppController {
  2.  
  3. public $name = 'Posts';
  4. public $helpers = array('Html', 'Rss');
  5.  
  6. public function index() {
  7. }
  8.  
  9. }

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:

  1. class AppController extends Controller {
  2.  
  3. public $helpers = array('Session', 'Form');
  4.  
  5. }

Teraz mała zagadka – jakie helpery zostaną załadowane w widoku akcji index() w PostsController? Prawidłowa odpowiedź: Session, Form, Html, Rssdzieje 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:

  1. class PostsController extends AppController {
  2.  
  3. public $name = 'Posts';
  4. public $helpers = array('Html', 'Rss');
  5.  
  6. public function index() {
  7. $this->helpers[] = 'Time';
  8. }
  9.  
  10. }

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:

  1. class AppController extends Controller {
  2.  
  3. public $helpers = array('Session', 'Form');
  4.  
  5. protected function _loadHelpers($helpers = array()) {
  6. foreach ($helpers as $helper) {
  7. if (
  8. !in_array($helper, $this->helpers) &&
  9. !array_key_exists($helper, $this->helpers)
  10. ) {
  11. $this->helpers[] = $helper;
  12. }
  13. }
  14. }
  15.  
  16. }

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 (plik html.php) z:
    cake/libs/view/helpers do app/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:

  1. App::import('Helper', 'Html');
  2. class CustomHtmlHelper extends HtmlHelper {
  3.  
  4. public function link($title, $url = null, $options = array(), $confirmMessage = false) {
  5. if (!isset($options['escape'])) $options['escape'] = false;
  6. $title = '<span>' . $title . '</span>';
  7. return parent::link($title, $url, $options, $confirmMessage);
  8. }
  9.  
  10. }

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:

  1. public $helpers = array('Session', 'Form', 'CustomHtml');

i już możemy wykorzystywać jego dobrodziejstwa w widoku:

  1. <?php echo $this->CustomHtml->link('Strona główna', '/'); ?>

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:

  1. <?php $this->Html = $this->CustomHtml; ?>

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. CustomHtml zamiast Html)
  • 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:

  1. <?php
  2. class AppController extends Controller {
  3.  
  4. public function beforeRender() {
  5. $this->_overloadCoreHelpers();
  6. }
  7.  
  8. protected function _overloadCoreHelpers() {
  9. foreach ($this->helpers as $helper) {
  10. $helperFilename = HELPERS . Inflector::underscore('Custom' . $helper) . '.php';
  11. if (file_exists($helperFilename)) {
  12. unset($this->helpers[array_search($helper, $this->helpers)]);
  13. $this->helpers[] = 'Custom' . $helper;
  14. }
  15. }
  16. $this->helpers = array_values($this->helpers);
  17. }
  18.  
  19. }
  20. ?>

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:

  1. <?php
  2. class AppHelper extends Helper {
  3.  
  4. public function beforeRender() {
  5.  
  6. $View = ClassRegistry::getObject('view');;
  7.  
  8. foreach ($View->helpers as $helper) {
  9. if (preg_match('/^Custom/', $helper)) {
  10. $View->{str_replace('Custom', '', $helper)} = $View->{$helper};
  11. }
  12. }
  13.  
  14. }
  15.  
  16. }
  17. ?>

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.

Czytaj więcej:
Artykuły » PHP
Tagi:
,

Grzegorz Wójcik

Grzegorz Wójcik jest założycielem internetowego magazynu kminek.pl. Pasjonat i twórca lekkich, dostępnych i użytecznych stron internetowych budowanych w oparciu o standardy sieciowe i najlepsze praktyki. Prywatnie wielki miłośnik ambitnego kina sci-fi oraz grunge-rocka z lat 90.

Zobacz wszystkie artykuły tego autora (15)

  1. Greg 1

    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łem CustomHelper 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 :)

  2. mike 3

    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ł :)

  3. michal 5

    w 2.x:

    1. class PostsController extends AppController {
    2. public $helpers = array(
    3. 'Html' => array(
    4. 'className' => 'MyHtml'
    5. )
    6. );
    7. }

Dodaj komentarz * pola obowiązkowe

 
 

Dozwolone tagi HTML: <strong> <em> <a href="" title=""> <code> <pre lang=""> <blockquote cite="">

Komentarze są moderowane. Mile widziane wpisy wnoszące nowe, ciekawe informacje do omawianego tematu
lub sygnalizujące ewentualne błędy merytoryczne. Wszelkie przejawy spamu lub nieetycznego zachowania bedą
karane blokadą adresu IP/domeny.