Statyczny cache w Zend Framework

Zend Framework oferuje wiele sposobów cache’owania danych. Jednym z najlepszych jest cache statyczny. Przy jego pomocy można zapisać wszystko to, co mamy na wyjściu (np. to co zostało wyświetlone w przeglądarce), do pliku HTML. W takim przypadku, każdy kolejny request będzie od razu otrzymywał w odpowiedzi wygenerowany plik HTML, bez użycia PHP. Brzmi nieprawdopodobnie? A jednak. Poza kilkoma niedociągnięciami, mechanizm ten spisuje się całkiem dobrze.

Mechanizmem, o którym przed chwilą pisałem, jest Zend_Cache_Backend_Static. Jego zastosowanie jest niezwykle proste, chociaż wymaga kilku niestandardowych modyfikacji.

Konfiguracja

Na samym początku musimy poinformować framework, że będziemy korzystać ze statycznego cache’u. Najszybciej zrobimy to korzystając z Zend_Cache_Manager. W tym celu w pliku application.ini musimy dodać następujący kod:

resources.cachemanager.page.backend.name = Static
resources.cachemanager.page.backend.options.public_dir = APPLICATION_PATH "/../public/static"
resources.cachemanager.pagetag.backend.options.cache_dir = APPLICATION_PATH "/../data/cache/static"

Informuje on naszą aplikację, że będziemy korzystać ze statycznego cache’u, który jest przechowywany w lokalizacji APPLICATION_PATH "/../public/static", a tagi opisujące cache znajdują się w katalogu APPLICATION_PATH "/../data/cache/static". Tagami na razie się nie przejmujcie. Opiszę je w dalszej części wpisu.

Powyższe opcje nie są jedynymi. Pełną ich listę znajdziecie w dokumentacji.

Skoro już przy pliku konfiguracyjnym jesteśmy, nie można zapomnieć o jednej bardzo ważnej modyfikacji. Musimy wyłączyć buforowanie wyjścia, które domyślnie jest włączone.

resources.frontController.params.disableOutputBuffering = true

Nie zapomnijcie o stworzeniu odpowiednich katalogów, ponieważ bez nich aplikacja przestanie działać.

.htaccess i punkt wejścia do aplikacji

Spore zmiany należy wprowadzić w pliku .htaccess. Powinien on wyglądać w następujący sposób (podziękowania dla Arka za pomoc z DirectoryIndex).

DirectoryIndex main.php

RewriteEngine On

RewriteCond %{DOCUMENT_ROOT}/static/index.html -f
RewriteRule ^/*$ static/index.html [L]

RewriteCond %{DOCUMENT_ROOT}/static/%{REQUEST_URI}.html -f
RewriteRule .* static/%{REQUEST_URI}.html [L]

RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_URI} ^(.*)/+$
RewriteRule ^.*$ %1 [R=301,L]

RewriteCond %{REQUEST_FILENAME} -s [OR]
RewriteCond %{REQUEST_FILENAME} -l [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^.*$ - [NC,L]
RewriteRule ^.*$ main.php [NC,L]

Na samym początku musimy określić DirectoryIndex, czyli nazwę pliku, który zostanie uruchomiony, gdy użytkownik podając adres, zakończy go slashem (/). Dlaczego main.php, a nie index.php? O tym za chwilę.

Pierwsze dwa warunki, sprawdzają, czy żądany adres nie znajduje się w cache’u. Jeśli cache istnieje, użytkownik dostaje w odpowiedzi jego zawartość, a PHP nie jest niepokojony.

Kolejnym etapem jest sprawdzenie, czy użytkownik wpisał po nazwie kontrolera lub akcji slash (/). Jeśli tak, musimy go usunąć, ponieważ jego obecność spowoduje ominięcie cache’u.

Na koniec pozostaje standardowa część pliku .htaccess dostarczana razem z Zend Frameworkiem.

Jak zapewne zauważyliście, plik index.php został zastąpiony main.php. Jeśli nie dokonamy tego zabiegu, wówczas żadna akcja znajdująca się w kontrolerze index, nie będzie cache’owana.

Jak cache’ować?

Ostatnim etapem statycznego cache’owania jest uruchomienie mechanizmu odpowiedzialnego za generowanie cache’u. Aby to zrobić w metodzie init kontrolera musimy wywołać helper akcji cache z co najmniej jednym parametrem.

$this->_helper->cache(array('akcja1', 'akcja2', 'akcja3'));

Obowiązkowym parametrem jest tablica akcji, dla których chcemy wygenerować cache. Do wywołania helpera możemy dodać drugi parametr, również tablicę, zawierającą listę tagów przypisanych do cache’u. W ten sposób zyskamy możliwość programowego usuwania cache’u.

$this->_helper->cache(array('akcja1', 'akcja2', 'akcja3'), array('tagA'));

Niestety nie mam najlepszych wiadomości. Od samego początku swojego istnienia, Zend_Cache_Backend_Static sprawia wrażenie wydanego w pośpiechu i bez jakichkolwiek testów. Najlepszym dowodem na to jest niezdefiniowana zmienna $path w jednej z kluczowych metod usuwających cache. Niemniej, usuwanie cache’u jest możliwe (w ograniczonym zakresie) i sprowadza się do wywołania jednej z dwóch metod.

// usuwanie cache'u według tagów
$this->_helper->getHelper('cache')->removePagesTagged(array('tagA', 'tagC'));

// usuwanie cache'u według adresu. niestety w chwili obecnej działa tylko dla pojedynczej akcji
$this->_helper->getHelper('cache')->removePage('akcja');

Wady

Nie ma róży bez kolców. Pomijając wspomniane błędy, Zend_Cache_Backend_Static ma jedną poważną wadę. Jeśli już coś raz zapisze do cache’u, nie można tego w żaden sposób ominąć. Przez to obsługa formularzy, AJAXa, kanałów RSS i innych elementów, które w jakiś sposób powinny być dynamiczne, staje się trudna (ale nie niemożliwa). Problemem jest również sposób w jaki uruchamiany jest cache, czyli dodawanie do każdego kontrolera, który ma być cache’owany, helpera akcji. Pracując z Zend_Cache_Backend_Static cały czas odnoszę wrażenie, iż mam do czynienia z wersją beta, która nie powinna znaleźć się w oficjalnym wydaniu frameworka.

Podsumowanie

Zend_Cache_Backend_Static idealnie nadaje się do niewielkich zastosowań. Dzięki niemu będzie można tworzyć małe stronki (w schemacie home – o nas – kontakt) bez obawy o ich wydajność. Nic nie stoi na przeszkodzie, aby korzystać z niego w bardziej specyficznych rozwiązaniach, jednak należy mieć na uwadze, iż nie jest to produkt w pełni sprawny. Dopóki ekipa odpowiedzialna za ZF nie naprawi w nim tak oczywistych błędów jak niezdefiniowana zmienna, powinniście ostrożnie do niego podchodzić w produkcyjnych środowiskach.

Przykładową aplikację znajdziecie w repozytorium SVN.

Leave a comment ?

23 Comments.

  1. a moze by tak sprawdzac czy zadanie jest przekazane za pomoca posta i jesli tak nie wywolywac tego helpera?
    analogocznie mozna zalatwiac ajaxa

  2. ~murwazy
    W sumie niezły pomysł. .htaccess ma możliwość wykrycia, czy request to POST. Nie wiem jak z AJAXem. Siądę za kilka dni do zmiany htaccessa i jak okaże się, że wszystko działa, zaktualizuję wpis.

  3. hm, ale po co w htaccesie?
    if ($this->_request->isPost()) {}
    lub
    if ($this->_request->isXmlHttpRequest()) {}
    i tak odpalasz caly fw i dopiero na poziomie akcji wskazujesz czy keszowac czy nie.

    ja u siebie uzywam Zend_Cache_Frontend_Page i tez sie swietnie sprawdza. bedzie nawet szybsze niz static bo po prostu laduje z dysku od razu:)

  4. Dlatego w htaccesie, że jak już cache zostanie wygenerowany, PHP nie jest nawet uruchamiany. htaccess sprawdza, czy dla danego requestu istnieje plik z cache i jeśli tak, to go zwraca i kończy działanie. Do PHP nawet ten request nie dolatuje. Dlatego cache ten idealnie nadaje się do statycznych treści, np statyczne strony w jakimś systemie CMS.

  5. a tak, masz racje – za szybko czytalem :)

  6. Jeśli dobrze rozumiem, to htaccess uruchomi cache tylko wtedy gdy on istnieje.
    Jeśli w kontrolerze sprawdzisz, że to POST lub AJAX (albo inne warunki), to nie utworzysz cache i problem rozwiązany?

  7. ~pc3t
    Nie do końca. Jeśli jedna akcja obsługuje POST i GET, wówczas cache wygeneruje się dla requestu GET i będzie go zwracał nawet dla POSTa.

  8. Rozwiązanie identyczne jak w plugin-ie do WordPress-a WP Super Cache tylko tam dodatkowo strony są pakowane gz i jeżeli przeglądarka obsługuje gzip-a wraca spakowany plik.
    tam jest problem z POST rozwiązany.

  9. ~sapper
    Dzięki za info. Będę musiał to sprawdzić.

  10. Wszystko fajnie, ale plik .html generuje mi się co odświeżenie strony, przez co strona nie generuje się z cache.
    Testowałem na stronie, która nie jest w roocie, tylko w podkatalogu – czy to ma znaczenie?

  11. Strona może być w dowolnym katalogu, nie ma to żadnego znaczenia. Zastosowałeś wszystkie uwagi z posta, czy tylko niektóre? Możesz gdzieś wrzucić spakowany projekt? Z kodem będzie łatwiej znaleźć problem.

  12. Dzięki za odpowiedź.
    Zastosowałem wszystkie uwagi.
    – application.ini dostał 4 nowe linijki,
    – zrobiłem kopię public/index.php i zamieniłem na main.php
    – zmieniłem zawartość public/.htaccess na podaną tutaj
    – zmieniłem (a wcześniej o tym zapomniałem, także 2 warianty testowałem) zawartość .htaccess w katalogu głównym aplikacji z:

    RewriteEngine on
    RewriteRule .* public/index.php

    na:

    RewriteEngine on
    RewriteRule .* public/main.php

    – dodałem w IndexController.php w metodzie init() kod:

    $this->_helper->cache(array('index'));

    I cache się tworzy, ale za każdym razem nowy, nadal.

    Katalog strony nosi nazwę (dla przykładu) ‘strona’. Zatem mam:
    strona/application
    strona/application/controllers (itd)
    strona/data/cache/static (i tutaj też tworzą się poprawnie pliki i też za każdym razem nowe)
    strona/public/static

    I w strona/public/static dla IndexController’a tworzy się plik strona.html

    Postaram się zaraz szybko zrobić ‘czysty’ projekt i zobaczę czy na nim zadziała poprawnie.

    Aha, pracuję na Windowsie… ale to chyba nie powinno mieć znaczenia.

    Pozdrawiam!

  13. Spróbuj tworzyć cache na kontrolerze i akcji innych od index. Z tym czasami są problemy.
    Windows tutaj nie ma nic do rzeczy.
    Dziwnie się tworzy ten cache. Powinien powstać plik index.html a nie strona.html. Domena wskazuje na katalog public, czy korzystasz z adresu http://localhost/strona/index/index ?

  14. http://localhost/strona/index/index (ale wpisuję localhost/strona).
    Testowałem na strona/aktualnosci, strona/aktualnosci/pokaz/aktualnosc/1-tytul-aktualnosci, strona/kontakt

    Tworzy mi następującą strukturę (w public/static):
    strona.html
    strona
    strona/aktualnosci.html
    strona/aktualnosci
    strona/aktualnosci/pokaz
    strona/aktualnosci/pokaz/aktualnosc/1-tytul-aktualnosci.html
    strona/kontakt.html

    I nadal tak samo, tworzy plik za każdym odświeżeniem strony. Zmiana pliku .html

    Zobaczę zaraz na serwerze nie-lokalnym i sprawdzę czy tam też są takie cyrki.

    Pozdrawiam!

  15. Aha, nie wiem, czy zauważyłeś, mam dodatkowy plik .htaccess (w katalogu strony):

    application
    data
    library
    public
    .htaccess

    A w nim wspomniany wcześniej:

    RewriteEngine on
    RewriteRule .* public/main.php

    Domena nie wskazuje na public, tylko na katalog ze strukturą jak wyżej. Localhost wskazuje na katalogi stron, dlatego w application.ini dodatkowa linijka:

    resources.frontController.baseUrl = "/strona"

    Nie wiem co jeszcze podać, żeby to sprawdzić.

  16. Wygląda na to, że problemem jest struktura katalogów. Powyższy przykład stworzony był dla projektu, w którym domena wskazuje bezpośrednio na katalog public.
    Statyczny cache wykorzystuje metodę getRequestUri obiektu request, która pobiera całą ścieżkę z adresu (razem z nazwą katalogu strona).
    Cache odświeża się za każdym razem, ponieważ mimo, iż tworzony jest poprawnie, to nie ma jak go sprawdzić. Musiałbyś zmienić .htaccess, aby cache zadziałał.
    Dla strony głównej powinno zdziałać dodanie

    RewriteCond %{DOCUMENT_ROOT}/static/strona.html -f
    RewriteRule ^/*$ static/strona.html [L]

    Dla pozostałych stron musiałbyś pokombinować z pozostałymi regułami.

  17. Działa.

    Batman pomógł (generalnie rozmowa toczyła się mailowo poza blogiem). Pomogło ustawienie chmod na pliki cache na 0755.

    Kod (też od Batmana):
    resources.cachemanager.page.backend.options.hashed_directory_umask = 0755
    resources.cachemanager.page.backend.options.cache_file_umask = 0755

    Dzięki raz jeszcze za pomoc, Batman! :razz:

  18. Hej! Mam ten sam problem co Jacek :) Doprowadziłem do stanu, w którym statyczne sa faktycznie wszystkie strony oprócz index.html, który wciąż i wciąż się przeladowuje :) Rozumiem, że rozwiązania nie znaleziono wciąż na ten mankament? :)

  19. @Uirapuru
    Rozwiązaniem tego problemu jest zmiana nazwy pliku index.php na inną, nieużywaną w aplikacji oraz ewentualne sprawdzenie praw dostępu do plików.

  20. jest mozliwe zrobienie cache samego layoutu bez $this->content()? tak aby caly layout byl w jakims pliku html (albo jakos inaczej zrobione cache), a byla tylko dogrywana do niego zawartosc z akcji?

  21. Niestety nie ma na to prostego sposobu i trzeba nieco się namęczyć, by uzyskać wspomniany przez Ciebie efekt.

  22. a mógłbyś opisać jak to wykonać?

  23. Na chwilę obecną przychodzi mi do głowy takie coś:
    1. Parsujesz plik z layoutem i zamieniasz wywołanie $this->layout()->content na jakiś unikatowy ciąg znaków.
    2. Wykonujesz layout (wykonują się helpery widoku i dołączają partiale).
    3. Zamieniasz z powrotem unikatowy ciąg znaków na $this->layout()->content.
    4. Zapisujesz do pliku phtml i wskazujesz go jako layout.
    Proces można zautomatyzować poprzez stworzenie własnej klasy do cache’owania i/lub nadpisując Zend_Layout (z tym będzie nieco zabawy).

Leave a Comment


NOTE - You can use these HTML tags and attributes:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

Subscribe without commenting