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.
a moze by tak sprawdzac czy zadanie jest przekazane za pomoca posta i jesli tak nie wywolywac tego helpera?
analogocznie mozna zalatwiac ajaxa
~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.
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:)
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.
a tak, masz racje – za szybko czytalem
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?
~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.
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.
~sapper
Dzięki za info. Będę musiał to sprawdzić.
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?
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.
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!
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 ?
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!
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ć.
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.
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 = 0755resources.cachemanager.page.backend.options.cache_file_umask = 0755
Dzięki raz jeszcze za pomoc, Batman!
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?
@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.
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?
Niestety nie ma na to prostego sposobu i trzeba nieco się namęczyć, by uzyskać wspomniany przez Ciebie efekt.
a mógłbyś opisać jak to wykonać?
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).