Development

Documentation/pl_PL/book/1.0/06-Inside-the-Controller-Layer

You must first sign up to be able to contribute.

Version 38 (modified by Pawel.Ledwon, 10 years ago)
Odnośniki…

Oryginalny tekst: http://www.symfony-project.com/book/trunk/06-Inside-the-Controller-Layer [EN]

WERSJA ROBOCZA

Rozdział 6 - Wewnątrz Warstwy Kontrolera

W symfony warstwa kontrolera, która zawiera kod odpowiedzialny za łączenie logiki biznesowej i prezentacji, jest podzielona na kilka komponentów służących do różnych celów:

  • Front controller jest punktem wejścia aplikacji. Zajmuje się ładowaniem konfiguracji i wyborem akcji do wykonania.
  • Akcje zawierają logikę aplikacji. Sprawdzają spójność żądania i przygotowują dane potrzebne przez warstwę prezentacji.
  • Obiekty żądania, odpowiedzi oraz sesji udostępniają parametry żądania, nagłówki odpowiedzi i trwałe dane użytkownika. Są intensywnie wykorzystywane w warstwie kontrolera.
  • Filtry są fragmentami kodu wykonywanymi przy każdym żądaniu, przed, jak i po wykonaniu akcji. Przykładem są filtry bezpieczeństwa oraz walidacji, powszechnie używane w aplikacjach web. Możesz rozszerzyć funkcjonalność frameworka poprzez stworzenie własnych filtrów.

Rozdział ten opisuje wszystkie te elementy, lecz nie przestrasz się ich liczbą. Na potrzeby prostej strony, najprawdopodobniej, będziesz zmuszony do napisania symbolicznej ilości linii kodu klas akcji, i to wszystko. Pozostałe składniki kontrolera będą używane tylko w specyficznych sytuacjach.

Front Controller

Wszystkie żądania są obsługiwane przez pojedynczy front controller, który jest punktem wejścia do całej aplikacji w danym środowisku.

Kiedy front controller otrzymuje żądanie, korzysta z systemu routingu, aby dopasować nazwę akcji i modułu do URL wysłanego przez użytkownika. Na przykład następujący URL wywołuje skrypt index.php (który jest front controllerem) i zostanie przetłumaczony na wywołanie akcji myAction modułu mymodule:

http://localhost/index.php/mymodule/myAction

Jeżeli nie jesteś zainteresowany szczegółami symfony, to posiadasz całą niezbędną Ci wiedzę na temat front controllera. Jest on nieodzownym komponentem architektury MVC symfony, lecz rzadko kiedy będziesz zmuszony do jego modyfikacji. Możesz więc przeskoczyć do kolejnego podrozdziału, chyba że na prawdę chcesz zagłębić się we wnętrze front controllera.

Szczegółowo o zadaniach front controllera

Front controller dokonuje dyspozycji (ang. dispatching) żądania, co znaczy trochę więcej niż wybieranie akcji do wywołania. W rzeczywistości, wykonuje on kod wspólny dla wszystkich akcji, wliczając w to:

  1. Definiowanie stałych jądra.
  2. Odnajdywanie bibliotek symfony.
  3. Ładowanie i inicjację klas jądra.
  4. Ładowanie konfiguracji.
  5. Dekodowanie URL żądania, w celu odczytania nazwy akcji do wykonania i parametrów żądania.
  6. Jeżeli akcja nie istnieje, to wykonanie przekierowania do akcji błędu 404.
  7. Aktywacja filtrów (na przykład jeżeli żądanie wymaga sprawdzenia tożsamości).
  8. Wywołanie filtrów, pierwsze przejście.
  9. Wywołanie akcji i wygenerowanie widoku.
  10. Wywołanie filtrów, drugie przejście.
  11. Zwrócenie odpowiedzi.

Domyślny front controller

Domyślny front controller nazwany index.php, znajdujący się w katalogu web/ projektu, jest prostym plikiem PHP, jak pokazano na Listingu 6-1.

Listing 6-1 - Domyślny produkcyjny Front Controller

[php]
<?php

define('SF_ROOT_DIR',    realpath(dirname(__FILE__).'/..'));
define('SF_APP',         'myapp');
define('SF_ENVIRONMENT', 'prod');
define('SF_DEBUG',       false);

require_once(SF_ROOT_DIR.DIRECTORY_SEPARATOR.'apps'.DIRECTORY_SEPARATOR.SF_APP.DIRECTORY_SEPARATOR.'config'.DIRECTORY_SEPARATOR.'config.php');

sfContext::getInstance()->getController()->dispatch();

Definicja stałych odnosi się do pierwszego kroku opisanego w poprzednim fragmencie. Następnie front controller dołącza plik config.php, który zajmuje się wykonaniem kroków 2-4. Wywołanie metody dispatch() obiektu sfController (który jest jądrem kontrolera w architekturze MVC symfony) dokonuje dyspozycji żądania, o czym mówią kroki od 5 do 7. Ostatnie czynności obsługiwane są przez łańcuch filtrów, co opisano w dalszej części tego rozdziału.

Wywoływanie różnych front controllerów w celu zmiany środowiska

W danym środowisku funkcjonuje jeden front controller; to właśnie jego obecność wyznacza środowisko. Jest ono definiowane przez stałą SF_ENVIRONMENT.

Aby zmienić środowisko, w którym chcesz przeglądać swoją aplikację, musisz, po prostu, wybrać inny front controller. Domyślne kontrolery dostępne po utworzeniu aplikacji przy pomocy polecenia symfony init-app to: index.php dla środowiska produkcyjnego oraz myapp_dev.php dla środowiska rozwojowego (przy założeniu, że Twoja aplikacja nosi nazwę myapp). Początkowa konfiguracja mod_rewrite wykorzystuje index.php, jeżeli URL nie zawiera nazwy skryptu front controllera. Tak więc, następujące adresy wyświetlają tę samą stronę (mymodule/index) w środowisku produkcyjnym:

http://localhost/index.php/mymodule/index
http://localhost/mymodule/index

a ten URL wyświetla tę stronę w środowisku rozwojowym:

http://localhost/myapp_dev.php/mymodule/index

Tworzenie nowego środowiska jest równie proste jak stworzenie front controllera. Na przykład możesz potrzebować środowiska testowego (ang. staging), aby pozwolić swoim klientom na sprawdzenie działania aplikacji przed wdrożeniem do środowiska produkcyjnego. Aby stworzyć to środowisko, po prostu, skopiuj zawartość pliku web/myapp_dev.php do web/myapp_staging.php i zmień wartość stałej SF_ENVIRONMENT na staging. Teraz możesz dodać we wszystkich plikach konfiguracyjnych sekcję staging:, aby zmienić ustawienia dla danego środowiska, co pokazano na Listingu 6-2.

Listing 6-2 - Przykładowy app.yml ze specyficznymi ustawieniami dla środowiska staging

staging:
  mail:
    webmaster:    dummy@mysite.com
    contact:      dummy@mysite.com
all:
  mail:
    webmaster:    webmaster@mysite.com
    contact:      contact@mysite.com

Jeżeli chcesz zobaczyć, jak aplikacja działa w nowym środowisku, wywołaj wspomniany kontroler:

http://localhost/myapp_staging.php/mymodule/index

Pliki wsadowe

Czasami istnieje potrzeba wywołania skryptu z linii poleceń (lub z tabeli crona), posiadając dostęp do wszystkich klas i możliwości symfony, na przykład, aby wysłać e-maile lub uaktualnić model poprzez wykonanie kosztownych obliczeń. Na potrzeby takiego skryptu musisz dołączyć na początku te same linie, co we front controllerze. Listing 6-3 pokazuje przykład początku pliku wsadowego.

Listing 6-3 - Przykładowy plik wsadowy

[php]
<?php

define('SF_ROOT_DIR',    realpath(dirname(__FILE__).'/..'));
define('SF_APP',         'myapp');
define('SF_ENVIRONMENT', 'prod');
define('SF_DEBUG',       false);

require_once(SF_ROOT_DIR.DIRECTORY_SEPARATOR.'apps'.DIRECTORY_SEPARATOR.SF_APP.DIRECTORY_SEPARATOR.'config'.DIRECTORY_SEPARATOR.'config.php');

// dopisz tutaj swój kod

Jak widać, jedyną brakującą linią jest wywołanie metody dispatch() obiektu sfController, który może być używany wyłącznie w aplikacjach web, nie w pliku wsadowym. Definiowanie aplikacji i środowiska, w którym zostanie one wykonana, daje Ci dostęp do odpowiedniej konfiguracji. Dołączenie pliku config.php powoduje inicjację kontekstu i wykonanie automatycznego ładowania klas (ang. autoloading).

TIP CLI symfony udostępnia komendę init-batch, która automatycznie tworzy szkielet, podobny do tego z Listignu 6-3, w katalogu batch/. Wystarczy podać jako argumenty nazwę aplikacji, środowiska oraz pliku wsadowego.

Akcje

Akcje stanowią serce aplikacji, gdyż zawierają całą logikę jej działania. Korzystają z modelu w celu definicji zmiennych dla widoku. Kiedy wysyłasz żądanie do aplikacji web symfony, jego URL definiuje akcję oraz jej parametry.

Klasa akcji

Akcje są metodami nazwanymi executeActionName klasy o nazwie moduleNameActions, dziedziczącej z klasy fActions, i grupowanymi przez moduły. Klasa akcji danego modułu jest zapisana w pliku actions.class.php w folderze actions/ tego modułu. Listing 6-4 jest przykładem pliku actions.class.php posiadającego jedynie akcję index modułu mymodule.

Listing 6-4 - Przykładowa klasa akcji, w apps/myapp/modules/mymodule/actions/actions.class.php

class mymoduleActions extends sfActions
{
  public function executeIndex()
  {

  }
}

CAUTION Pomimo tego, że PHP nie rozróżnia w nazwach metod wielkości liter, symfony postępuje przeciwnie. Nie zapomnij więc, że nazwy metod akcji muszą zaczynać się prefiksem execute pisanym małymi literami, po którym następuje dokładna nazwa akcji zaczynająca się dużą literą.

W celu wysłania żądania wykonania akcji, musisz wywołać skrypt front controllera wraz z nazwą modułu i akcji podanymi jako parametry. Domyślną implementacją jest dodanie pary module_name/action_name do skryptu. Znaczy to, że akcja zdefiniowana w listingu 6-4 może zostać wywołana z następującym URL:

http://localhost/index.php/mymodule/index

Dodawanie kolejnych akcji jest jednoznaczne z dopisywaniem kolejnych metod execute do klasy dziedziczącej po sfActions, co obrazuje listing 6-5.

Listing 6-5 - Klasa z dwiema akcjami, w myapp/modules/mymodule/actions/actions.class.php

class mymoduleActions extends sfActions
{
  public function executeIndex()
  {
    ...
  }

  public function executeList()
  {
    ...
  }
}

Jeżeli kod klasy rozrasta się za bardzo, prawdopodobnie, musisz dokonać refaktoryzacji (ang. refactoring) i przenieść część kodu do warstwy modelu. Akcje powinny być przeważnie krótkie (nie dłuższe niż kilka linii kodu), a cała logika biznesowa powinna znaleźć się w modelu.

Liczba akcji w module może stanowić powód do podziału go na dwa mniejsze.

SIDEBAR Standardy kodowania symfony

W przykładach znajdujących się w tej książce zauważyłeś, prawdopodobnie, że zarówno klamry otwierające, jak i zamykające znajdują się w osobnych liniach. W ten sposób kod staje się czytelniejszy.

Pośród standardów kodowania projektu, wcięcie jest reprezentowane poprzez dwie spacje; tabulatory nie są używane. Jest to spowodowane faktem, iż tabulatory mają różne wartości odstępu w edytorach, a także kod z wymieszanymi tabulatorami i spacjami jest niemożliwy do odczytania.

Pliki PHP jądra oraz wygenerowane przez symfony nie kończą się zamykającym znacznikiem ?>. Nie jest to tak na prawdę potrzebne i może powodować problemy z wyjściem, jeżeli występują spacje po zamykającym znaczniku.

Jeżeli zwróciłeś dostatecznie uwagę, zauważyłeś pewnie, że żadna linia w symfony nie kończy się spacją. Powód jest, tym razem, bardziej prozaiczny: linie kończące się spacjami wyglądają okropnie w edytorze Fabiena.

Alternatywna składnia klas akcji

Alternatywna składnia klas akcji została udostępniona w celu podziału akcji na osobne pliki - jeden na akcję. W tym wypadku każda akcja musi rozszerzać klasę sfAction (zamiast sfActions) i nazywać się actionNameAction. Metoda akcji nazywa się po prostu execute. Nazwa pliku jest taka sama jak nazwa akcji. To znaczy, że Listing 6-5 może zostać zapisany w dwóch plikach, co pokazano na Listingach 6-6 i 6-7.

Listing 6-6 - Pojedynczy plik akcji, w myapp/modules/mymodule/actions/indexAction.class.php

[php]
class indexAction extends sfAction
{
  public function execute()
  {
    ...
  }
}

Listing 6-7 - Pojedynczy plik akcji, w myapp/modules/mymodule/actions/listAction.class.php

[php]
class listAction extends sfAction
{
  public function execute()
  {
    ...
  }
}

Pobieranie informacji wewnątrz akcji

Klasa akcji posiada metody umożliwiające pobranie informacji związanych z kontrolerem, oraz dostęp do obiektów jądra symfony. Listing 6-8 pokazuje wykorzystanie tych możliwości.

Listing 6-8 - Ogólne metody klasy sfActions

class mymoduleActions extends sfActions
{
  public function executeIndex()
  {
    // Pobieranie parametrów żądania
    $password    = $this->getRequestParameter('password');

    // Pobieranie informacji o kontrolerze
    $moduleName  = $this->getModuleName();
    $actionName  = $this->getActionName();

    // Pobieranie obiektów jądra frameworka
    $request     = $this->getRequest();
    $userSession = $this->getUser();
    $response    = $this->getResponse();
    $controller  = $this->getController();
    $context     = $this->getContext();

    // Nadawanie wartości zmiennym akcji w celu przesłania ich do szablonu
    $this->setVar('foo', 'bar');
    $this->foo = 'bar';            // Wersja skrócona
  }
}

SIDEBAR Singleton Context

Zapewne zauważyłeś już wywołanie funkcji sfContext::getInstance() we front controllerze. W akcji metoda getContext() zwraca ten sam singleton. Jest on bardzo użytecznym obiektem, który przechowuje referencje do wszystkich obiektów jądra symfony, odwołujących się do obecnego żądania, a także akcesory do każdego z nich:

sfController: Obiekt kontrolera (->getController()) sfRequest: Obiekt żądania (->getRequest()) sfResponse: Obiekt odpowiedzi (->getResponse()) sfUser: Obiekt sesji użytkownika (->getUser()) sfDatabaseConnection: Połączenie z bazą danych (->getDatabaseConnection()) sfLogger: Obiekt loggera (->getLogger()) sfI18N: Obiekt internacjonalizacji (->getI18N())

Metodę singletonu sfContext::getInstance() możesz wywołać z dowolnego fragmentu kodu.

Kończenie akcji

Działania akcji mogą kończyć się na różne sposoby. Wartość zwracana przez metodę akcji określa, w jaki sposób zostanie wyświetlony widok. Stałe klasy sfView są wykorzystywane do wyznaczenia, który szablon zostanie wykorzystany do wyświetlenia wyniku akcji.

Jeżeli chcemy wyświetlić domyślny widok (przeważnie właśnie to chcemy zrobić), akcja powinna kończyć się w następujący sposób:

[php]
return sfView::SUCCESS;

Wtedy symfony poszuka szablonu o nazwie actionNameSuccess.php. Jest to domyślne zachowanie akcji, więc jeżeli pominiesz wyrażenie return, to symfony będzie również poszukiwało szablonu actionNameSuccess.php. Puste akcje również zachowują się w ten sposób. Listing 6-9 pokazuje przykłady pomyślnego zakończenia akcji.

Listing 6-9 - Akcje, które wywołają szablony indexSuccess.php i listSuccess.php

[php]
public function executeIndex()
{
  return sfView::SUCCESS;
}

public function executeList()
{
}

Jeżeli musimy wywołać widok błędu, to akcja powinna kończyć się następująco:

[php]
return sfView::ERROR;

Symfony wtedy odszuka plik szablonu o nazwie actionNameError.php.

Aby wywołać własny widok, użyj następującego zakończenia:

[php]
return 'MyResult';

Wtedy symfony poszuka szablonu actionNameMyResult.php.

Jeżeli nie chcesz wywołać żadnego widoku -- na przykład gdy wykonywana akcja jest procesem wsadowym -- akcja powinna kończyć się:

[php]
return sfView::NONE;

W tym przypadku żaden szablon nie zostanie wyświetlony. Znaczy to, że możesz całkowicie ominąć warstwę widoku i zwrócić kod HTML bezpośrednio z akcji. W Listingu 6-10 przedstawiono specjalnie stworzoną do tego metodę symfony renderText(). Ten sposób może stać się użyteczny, jeżeli potrzebujesz nadzwyczajnej reaktywności akcji, jak w przypadku interakcji AJAX, które zostaną omówione w rozdziale 11.

Listing 6-10 - Omijanie widoku poprzez wypisywanie odpowiedzi i zwrócenie sfView::NONE

[php]
public function executeIndex()
{
  echo "<html><body>Hello, World!</body></html>";

  return sfView::NONE;
}

// Działa identycznie
public function executeIndex()
{
  return $this->renderText("<html><body>Hello, World!</body></html>");
}

W niektórych przypadkach potrzebujesz wysłać wyłącznie zdefiniowane nagłówki (szczególnie nagłówek X-JSON) bez ciała odpowiedzi. Określ wtedy nagłówki poprzez obiekt sfResponse, omawiany w kolejnym rozdziale, i zwróć stałą sfView::HEADER_ONLY, tak jak pokazano na listingu 6-11.

Listing 6-11 - Pomijanie wyświetlania widoku i wysłanie wyłącznie nagłówków

[php]
public function executeRefresh()
{
  $output = '<"title","My basic letter"],["name","Mr Brown">';
  $this->getResponse()->setHttpHeader("X-JSON", '('.$output.')');

  return sfView::HEADER_ONLY;
}

Jeżeli akcja ma zostać wyświetlona przez specyficzny szablon, zignoruj wyrażenie return i wykorzystaj zamiast tego metodę setTemplate().

[php]
$this->setTemplate('myCustomTemplate');

Przeskakiwanie do innej akcji

W pewnych przypadkach wykonanie akcji kończy się wywołaniem kolejnej. Na przykład akcja obsługująca wysyłanie formularzy metodą POST zazwyczaj przekierowuje do kolejnej akcji po zaktualizowaniu bazy danych. Kolejnym przykładem jest alias akcji: akcja index służy często do wyświetlania list i forwarduje do akcji list.

Klasa akcji udostępnia dwie metody do wywołania innej akcji:

  • Forwardowanie wywołania do innej akcji [php] $this->forward('otherModule', 'index');
  • Przekierowanie poprzez żądanie [php] $this->redirect('otherModule/index'); $this->redirect('http://www.google.com/');

NOTE Kod występujący po forwardzie albo przekierowaniu nie jest nigdy wykonywany. Możesz utożsamiać te wywołania z wyrażeniem return. Wyrzucają one wyjątek sfStopException w celu zatrzymania akcji; później wyjątek jest przechwytywany przez symfony i, po prostu, ignorowany.

Wybór pomiędzy forwardowaniem a przekierowaniem jest czasem podchwytliwy. Aby wybrać najlepsze rozwiązanie pamiętaj, że forward jest działaniem wewnętrznym, niewidocznym dla użytkownika. Od strony użytkownika obowiązuje wciąż ten sam adres URL. Przeciwnie, przekierowanie jest wiadomością wysyłaną do przeglądarki użytkownika, wykorzystującą nowe żądanie i kończącą się zmianą URL.

Jeżeli akcja jest wywoływana z wysłanego formularza z method="post", powinieneś zawsze przesyłać przekierowanie. Główną zaletą tego rozwiązania jest fakt, po odświeżeniu strony wynikowej przez użytkownika formularz nie zostanie wysłany powtórnie. W dodatku, przycisk wstecz działa tak, jak powinien, czyli wyświetla formularz bez pytania o chęć ponownego przesłania formularza metodą POST.

Istnieje specjalny rodzaj przekierowań, który jest wyjątkowo często wykorzystywany. Metoda forward404() forwarduje do akcji wyświetlającej wiadomość "strona nie została znaleziona". Przeważnie wykonujemy tę metodę, kiedy parametr wymagany do wykonania akcji nie jest obecny w żądaniu (w ten sposób wykrywając niepoprawny URL). Listing 6-12 pokazuje przykład akcji show oczekującej parametru id.

Listing 6-12 - Wykorzystanie metody forward404()

[php]
public function executeShow()
{
  $article = ArticlePeer::retrieveByPK($this->getRequestParameter('id'));
  if (!$article)
  {
    $this->forward404();
  }
}

TIP Jeżeli szukasz akcji i szablonu 404, znajdziesz je w katalogu $sf_symfony_ data_dir/modules/default/. Możesz zmodyfikować tę stronę poprzez dodanie nowego modułu default do Twojej aplikacji, nadpisując tę znajdującą się we frameworku, oraz poprzez zdefiniowanie akcji error404 i szablonu error404Success wewnątrz niego. Alternatywnie możesz nadać odpowiednią wartość stałym error_404_module i error_404_ action w pliku settings.yml, aby użyć istniejącej akcji.

Doświadczenie pokazuje, że przeważnie akcja wykonuje przekierowanie albo forward po wykonaniu pewnych testów, takich jak na przykład w Listingu 6-12. To wyjaśnia, dlaczego klasa sfActions posiada kilka dodatkowych metod, nazwanych forwardIf(), forwardUnless(), forward404If(), forward404Unless(), redirectIf() i redirectUnless(). Metody te pobierają dodatkowy argument, reprezentujący warunek, który uruchamia wywołanie, jeżeli jest prawdą (dla metod xxxIf()), bądź fałszem (dla metod xxxUnless()), co obrazuje Listing 6-13.

Listing 6-13 - Wykorzystanie metody forward404If()

[php]
// Działa identycznie jak akcja z Listingu 6-12
public function executeShow()
{
  $article = ArticlePeer::retrieveByPK($this->getRequestParameter('id'));
  $this->forward404If(!$article);
}

// Ta również
public function executeShow()
{
  $article = ArticlePeer::retrieveByPK($this->getRequestParameter('id'));
  $this->forward404Unless($article);
}

Używanie tych metod sprawi, że Twój kod nie tylko będzie krótszy, lecz również bardziej czytelny.

TIP Kiedy akcja wywołuje forward404() albo jej bliźniacze metody, symfony wyrzuca wyjątek sfError404Exception, który obsługuje odpowiedź 404. To znaczy, że jeżeli zechcesz wyświetlić komunikat 404 z miejsca, w którym nie masz dostępu do kontrolera, możesz wyrzucić podobny wyjątek.

Powtarzanie kodu w kilku akcjach

Konwencja nazewnictwa metod akcji executeActionName() (w przypadku klasy sfActions) lub execute() (w przypadku klasy sfAction) gwarantuje, że symfony zawsze znajdzie odpowiednią metodę akcji. Umożliwia to dodanie Twoich własnych metod, które nie będą brane pod uwagę, jeżeli nie zaczynają się od execute.

Istnieje inna konwencja, szczególnie przydatna, gdy zachodzi potrzeba wykonania kilku poleceń przed wywołaniem każdej akcji. Możesz przenieść ten fragment kodu do metody o nazwie preExecute() znajdującej się w Twojej klasie akcji. Pewnie domyślasz się już, jak wykonywać wspólny kod po wywołaniu akcji: zapisz je w metodzie postExecute(). Składnia tych metod została pokazana w Listingu 6-14.

Listing 6-14 - Użycie preExecute, postExecute oraz własnych metod w klasie akcji

[php]
class mymoduleActions extends sfActions
{
  public function preExecute()
  {
    // Kod umieszczony tutaj jest wykonywany przed każdą akcją
    ...
  }

  public function executeIndex()
  {
    ...
  }

  public function executeList()
  {
    ...
    $this->myCustomMethod();  // Dostępne są metody klasy akcji
  }

  public function postExecute()
  {
    // Kod umieszczony tutaj jest wykonywany po każdej akcji
    ...
  }

  protected function myCustomMethod()
  {
    // Możesz dodać swoje własne metody, jeżeli nie zaczynają się od "execute"
    // W tym przypadku, najlepszym wyjściem jest zadeklarowanie ich jako chronione bądź prywatne
    ...
  }
}

Dostęp do żądania

Jesteś już zaznajomiony z metodą getRequestParameter('myparam'), służącą do pobierania wartości parametru żądania o podanej nazwie. W rzeczywistości stanowi ona metodę proxy dla łańcucha wywołań do kontenera parametrów żądania getRequest()->getParameter('myparam'). Klasa akcji ma dostęp do obiektu żądania, nazwanego w symfony sfWebRequest, a także do jego wszystkich metod, poprzez metodę getRequest(). Tabela 6-1 zawiera spis najbardziej użytecznych metod klasy sfWebRequest.

Tabela 6-1 - Metody obiektu sfWebRequest

Nazwa | Funkcja | Przykładowy rezultat działania ----------------------------------------| -------------------------------------- | ------------------------------------------------------------------ Informacje o żądaniu | | getMethod() | Metoda żądania | Zwraca stałe sfRequest::GET lub sfRequest::POST getMethodName() | Nazwa metody żądania | 'POST' getHttpHeader('Server') | Wartość danego nagłówka HTTP | 'Apache/2.0.59 (Unix) DAV/2 PHP/5.1.6' getCookie('foo') | Wartość danego ciastka | 'bar' isXmlHttpRequest()* | Czy jest żądaniem AJAX? | true isSecure() | Czy jest żądaniem przez SSL? | true Parametry żądania | | hasParameter('foo') | Czy parametr został wysłany w żądaniu? | true getParameter('foo') | Wartość danego parametru | 'bar' getParameterHolder()->getAll() | Tablica wszystkich parametrów żądania | Informacje związane z URI | | getUri() | Pełny URI | 'http://localhost/myapp_dev.php/mymodule/myaction' getPathInfo() | Path info | '/mymodule/myaction' getReferer() | Referrer | 'http://localhost/myapp_dev.php/' getHost() | Nazwa hosta | 'localhost' getScriptName() | Nazwa i ścieżka front controllera | 'myapp_dev.php' Informacje o przeglądarce klienta | | getLanguages() | Tablica akceptowanych języków | Array( [0] => fr [1] => fr_FR [2] => en_US [3] => en ) getCharsets() | Tablica akceptowanych kodowań | Array( [0] => ISO-8859-1 [1] => UTF-8 [2] => * ) getAcceptableContentTypes() | Tablica akceptowanych typów dokumentu | Array( [0] => text/xml [1] => text/html

  • Działa tylko z prototypem Jest blokowane przez niektóre serwery proxy

Klasa sfActions udostępnia kilka metod proxy, dzięki którym można szybciej uzyskać dostęp do metod żądania, co pokazano na Listingu 6-15.

Listing 6-15 - Wywoływanie metod klasy sfRequest z akcji

[php]
class mymoduleActions extends sfActions
{
  public function executeIndex()
  {
    $hasFoo = $this->getRequest()->hasParameter('foo');
    $hasFoo = $this->hasRequestParameter('foo');  // Wersja skrócona
    $foo    = $this->getRequest()->getParameter('foo');
    $foo    = $this->getRequestParameter('foo');  // SWersja skrócona
  }
}

Do żądań wieloczęściowych (ang. multipart requests), do których użytkownicy dołączają pliki, klasa sfWebRequest udostępnia środki do uzyskania dostępu i przenoszenia tych plików, jak widać w Listingu 6-16.

Listing 6-16 - Obiekt sfWebRequest wie jak obsługiwać dołączone pliki

[php]
class mymoduleActions extends sfActions
{
  public function executeUpload()
  {
    if ($this->getRequest()->hasFiles())
    {
      foreach ($this->getRequest()->getFileNames() as $fileName)
      {
        $fileSize  = $this->getRequest()->getFileSize($fileName);
        $fileType  = $this->getRequest()->getFileType($fileName);
        $fileError = $this->getRequest()->hasFileError($fileName);
        $uploadDir = sfConfig::get('sf_upload_dir');
        $this->getRequest()->moveFile('file', $uploadDir.'/'.$fileName);
      }
    }
  }
}

Nie musisz martwić się, czy Twój serwer obsługuje zmienną $_SERVER czy $_ENV, czy o inne problemy z kompatybilnością -- metody sfWebRequest wykonują całą tę pracę za Ciebie. Poza tym, ich nazwy są tak oczywiste, że nie będziesz musiał już przeglądać dokumentacji PHP, aby sprawdzić, jak pobiera się informacje o żądaniu.

Sesje użytkowników

Symfony automatycznie zarządza sesjami i jest w stanie przechować trwałe dane pomiędzy żądaniami użytkownika. Używa do tego wbudowanego w PHP mechanizmu obsługi sesji oraz usprawnia je, aby uczynić je łatwiejszymi w konfiguracji i użytkowaniu.

Dostęp do sesji użytkownika

Obiekt sesji użytkownika jest dostępny w akcji poprzez wywołanie metody getUser() i jest instancją klasy sfUser. Klasa ta zawiera kontener (ang. holder) parametrów, który umożliwia Ci przechowywanie dowolnego atrybutu. Dane te będą tostępne w pozostałych żądaniach, dopóki nie zakończy się sesja użytkownika, jak na Listingu 6-17. Atrybuty użytkownika mogą przechowywać dane dowolnego typu (łańcuchy znaków, tablice, w tym asocjacyjne). Mogą być zapisane dla dowolnego użytkownika, nawet jeżeli nie został on wcześniej zidentyfikowany.

Listing 6-17 - Obiekt klasy sfUser może zawierać atrybuty użytkownika przenoszone pomiędzy żądaniami

[php]
class mymoduleActions extends sfActions
{
  public function executeFirstPage()
  {
    $nickname = $this->getRequestParameter('nickname');

    // Zapisz dane w sesji użytkownika
    $this->getUser()->setAttribute('nickname', $nickname);
  }

  public function executeSecondPage()
  {
    // Pobierz dane z sesji użytkownika, używając domyślnej wartości
    $nickname = $this->getUser()->getAttribute('nickname', 'Anonymous Coward');
  }
}

CAUTION Możesz przechowywać obiekty w sesji użytkownika, lecz jest to stanowczo odradzane. Obiekt sesji jest serializowany pomiędzy żądaniami i zapisywany do pliku. Kiedy sesja jest deserializowana, klasa przechowywanego obiektu musi być wcześniej załadowana, co nie zawsze ma miejsce. W dodatku, mogą się tam znajdować "przestarzałe" obiekty, gdy przechowujesz instancje klas Propela.

Jak wiele akcesorów w symfony, metoda getAttribute() przyjmuje drugi argument określający domyślną wartość atrybutu, która zostanie zwrócona, jeżeli atrybut nie został zdefiniowany. Aby sprawdzić, czy atrybut został zdefiniowany dla danego uzytkownika, użyj metody hasAttribute(). Atrybuty są składowane w kontenerze, który jest dostępny po wywołaniu metody getAttributeHolder(). Pozwala on na łatwe porządkowanie atrybutów użytkownika przy pomocy zwyczajnych metod kontenera parametrów, co pokazano na Listingu 6-18.

Listing 6-18 - Usuwanie danych z sesji użytkownika

[php]
class mymoduleActions extends sfActions
{
  public function executeRemoveNickname()
  {
    $this->getUser()->getAttributeHolder()->remove('nickname');
  }

  public function executeCleanup()
  {
    $this->getUser()->getAttributeHolder()->clear();
  }
}

Atrybuty sesji użytkownika są domyślnie dostępne w szablonach poprzez zmienną $sf_user, która przechowuja obecny obiekt klasy sfUser, jak na Listingu 6-19.

Listing 6-19 - Szablony również mają dostęp do atrybutów sesji użytkownika

[php]
<p>
  Hello, <?php echo $sf_user->getAttribute('nickname') ?>
</p>

NOTE Jeżeli chcesz przechować dane tylko podczas trwania obecnego żądania -- na przykład aby przesłać informacje przez łańcuch akcji -- powinieneś użyć klasy sfRequest, która również posiada metody getAttribute() i setAttribute(). Tylko atrybuty obiektu sfUser są przechowywane między żądaniami.

Atrybuty jednorazowe

Odwiecznym problemem z atrybutami użytkownika jest czyszczenie sesji po tym, jak atrybut staje się już niepotrzebny. Na przykład możesz zechcieć wyświetlić potwierdzenie po aktualizacji danych poprzez formularz. Jako że akcja obsługi formularza tworzy przekierowanie, jedynym sposobem na przesłanie informacji z tej akcji do akcji docelowej jest przechowanie ich w sesji użytkownika. Ale po wyświetleniu potwierdzenia musisz usunąć ten atrybut; w przeciwnym wypadku będzie on przechowywany aż do przedawnienia sesji.

Atrybut jednorazowy (ang. flash attribute) jest atrybutem krótkotrwałym, który możesz zdefiniować i zapomnieć o nim, wiedząc, że zniknie on po następnym żądaniu i na przyszłość pozostawi sesję użytkownika w należytym porządku. Atrybuty jednorazowe możesz zdefiniować w swojej akcji w poniższy sposób:

[php]
$this->setFlash('attrib', $value);

Szablon zostanie wygenerowany i przesłany do użytkownika, który następnie wyśle żądanie kolejnej akcji. W niej możesz pobrać wartość atrybutu jednorazowego w następujący sposób:

[php]
$value = $this->getFlash('attrib');

Możesz już o nim zapomnieć. Po przesłaniu drugiej strony atrybut zostanie oczyszczony. Nawet jeżeli nie wykorzystasz go w drugiej akcji, zniknie on z sesji użytkownika.

Jeżeli potrzebujesz dostępu do atrybutu jednorazowego z szablonu, użyj obiektu $sf_flash.

[php]
<?php if ($sf_flash->has('attrib')): ?>
  <?php echo $sf_flash->get('attrib') ?>
<?php endif; ?>

lub po prostu:

[php]
<?php echo $sf_flash->get('attrib') ?>

Atrybuty jednorazowe są dobrym sposobem na przekazywanie danych do kolejnego żądania.

Zarządzanie sesją

System obsługi sesji symfony kompletnie ukrywa przed programistą mechanizm przechowywania identyfikatora sesji, zarówno po stronie serwera, jak i klienta. Jeżeli jednak chciałbyś zmienić domyślne zachowanie mechanizmu zarządzania sesją, jest to zawsze możliwe. Ta funkcja jest przeznaczona głównie dla zaawansowanych użytkowników.

Po stronie klienta sesje są obsługiwane poprzez ciastka. Ciastko sesyjne symfony nosi nazwę symfony, ale możesz ją zmienić edytując plik konfiguracji factories.yml, jak pokazano na Listingu 6-20.

Listing 6-20 - Zmiana nazwy ciastka sesyjnego, w apps/myapp/config/factories.yml

all:
  storage:
    class: sfSessionStorage
    param:
      session_name: my_cookie_name

TIP Sesja jest startowana (przy użyciu wbudowanej funkcji PHP session_start) tylko wtedy, gdy parametr auto_start ma wartość true w factories.yml (domyślnie). Jeżeli chcesz ręcznie rozpocząć sesję, wyłącz tę opcję fabryki magazynów (ang. storage factory).

Obsługa sesji symfony bazuje na sesjach PHP. Znaczy to, że jeżeli chcesz, aby zarządzanie sesję po stronie klienta było obsługiwane przez parametry URL, a nie ciastka, musisz tylko zmienić opcję use_trans_sid w Twoim pliku php.ini. Zachowaj jednak uwagę, gdyż nie jest to zalecaną praktyką.

session.use_trans_sid = 1

Po stronie serwera symfony domyślnie przechowuje sesje w plikach. Możesz trzymać je w bazie danych, zmieniając wartość parametru class w pliku factories.yml, jak w Listingu 6-21.

Listing 6-21 - Zmiana systemu przechowywania danych sesyjnych serwera, w apps/myapp/config/factories.yml

all:
  storage:
    class: sfMySQLSessionStorage
    param:
      db_table: SESSION_TABLE_NAME      # Nazwa tabeli, w której mają być zapisane dane sesyjne
      database: DATABASE_CONNECTION     # Nazwa używanej bazy danych

Dostępne klasy przechowywania danych sesyjnych to sfMySQLSessionStorage, sfPostgreSQLSessionStorage i sfPDOSessionStorage; zaleca się używania ostatniej. Opcjonalna opcja database definiuje połączenie z bazą danych, które ma zostać użyte; wtedy symfony użyje pliku databases.yml (patrz rozdział 8), aby pobrać ustawienia danego połączenia (hosta, nazwę bazy danych, użytkownika i hasło).

Przedawnienie sesji następuje automatycznie po liczbie sekund określonej przez sf_timeout. Stała ta ma domyślną wartość 30 minut, lecz może być zmieniana dla dowolnego środowiska w pliku konfiguracji settings.yml, co pokazano na Listingu 6-22.

Listing 6-22 - Zmiana czasu życia sesji, w apps/myapp/config/settings.yml

default:
  .settings:
    timeout:     1800           # Czas życia sesji w sekundach

Bezpieczeństwo akcji

Możliwość wykonania akcji może zostać zawężona do grona użytkowników posiadających określone uprawnienia. Narzędzia dostarczane przez symfony pozwalają na tworzenie bezpiecznych aplikacji, w których użytkownicy muszą przejść proces uwierzytelnienia, zanim uzyskają dostęp do części funkcjonalności. Zabezpieczenie aplikacji wymaga wykonania dwóch kroków: zadeklarowania wymagań dla każdej akcji i zalogowania użytkowników z przywilejami, aby Ci mogli uzyskać dostęp do bezpiecznych akcji.

Ograniczenie dostępu

Przed wykonaniem każda akcja przechodzi przez specjalny filtr, który sprawdza, czy obecny użytkownik ma prawo do dostępu do żądanej akcji. W symfony przywileje składają się z dwóch części:

  • Bezpieczne akcje wymagają, aby użytkownicy zostali uwierzytelnieni.
  • Przywileje (ang. credentials, roles) są nazwanymi uprawnieniami, które pomagają w organizacji zabezpieczeń w grupy.

Ograniczenie dostępu do akcji jest wykonywane, po prostu, poprzez utworzenie i edycję pliku konfiguracji YAML nazwanego security.yml w katalogu config/ modułu. W tym pliku możesz ustalić wymagania, które użytkownicy muszą spełnić do konkretnych akcji lub do dowolnej z nich. Listing 6-23 zawiera treść przykładowego pliku security.yml.

Listing 6-23 - Ustawianie ograniczeń dostępu, w apps/myapp/modules/mymodule/config/security.yml

read:
  is_secure:   off       # Wszyscy użytkownicy mają dostęp do akcji read

update:
  is_secure:   on        # Akcja update jest dostępna tylko dla uwierzytelnionych

delete:
  is_secure:   on        # Tylko dla uwierzytelnionych
  credentials: admin     # Z rolą admin

all:
  is_secure:  off        # Tak czy inaczej, jest to domyślna wartość

Akcje nie są domyślnie zabezpieczone, więc jeżeli nie istnieje plik security.yml, bądź nie ma w nim mowy o danej akcji, to jest ona dostępna dla każdego. Jeżeli istnieje plik security.yml, symfony poszukuje nazwy wywołanej akcji i, jeżeli ta istnieje, sprawdza spełnienione wymagania bezpieczeństwa. Co się stanie, gdy użytkownik próbuje uzyskać dostęp do zastrzeżonej części akcji, zależy od jego uprawnień.

  • Jeżeli użytkownik jest uwierzytelniony i ma odpowiednie uprawnienia, to akcja jest wykonywana.
  • Jeżeli użytkownik nie jest zidentyfikowany, zostanie przekierowany do domyślnej akcji logowania
  • Jeżeli użytkownik jest zidentyfikowany, ale nie ma odpowiednich uprawnień, zostanie przekierowany do domyślnej strony zabezpieczeń, co pokazano na Ryc. 6-1.

Domyślne strony logowania i zabezpieczeń są stosunkowo proste, więc prawdopodobnie będziesz chciał je zmodyfikować. Możesz skonfigurować akcje, które mają zostać wywołane w przypadku niewystarczających przywilejów w pliku konfiguracji aplikacji settings.yml, poprzez zmianę wartości właściwości zamieszczonych w Listingu 6-24.

Ryc. 6-1 - Domyślna strona zabezpieczeń

Domyślna strona zabezpieczeń

Listing 6-24 - Domyślne akcje zabezpieczeń są zdefiniowane w apps/myapp/config/settings.yml

all:
  .actions:
    login_module:           default
    login_action:           login

    secure_module:          default
    secure_action:          secure

Przyznawanie dostępu

Aby uzyskać dostęp do zabezpieczonych akcji, użytkownicy muszą być uwierzytelnieni i/lub posiadać określone przywileje. Możesz poszerzyć uprawnienia użytkownika, wywołując metody obiektu klasy sfUser. Status użytkownika uwierzytelnionego jest przyznawany poprzez metodę setAuthenticated(). Listing 6-25 pokazuje prosty przykład uwierzytelniania użytkownika.

Listing 6-25 - Nadawanie statusu użytkownika uwierzytelnionego

[php]
class myAccountActions extends sfActions
{
  public function executeLogin()
  {
    if ($this->getRequestParameter('login') == 'foobar')
    {
      $this->getUser()->setAuthenticated(true);
    }
  }

  public function executeLogout()
  {
    $this->getUser()->setAuthenticated(false);
  }
}

Uprawnienia są nieco trudniejsze w opanowaniu, gdyż możesz je sprawdzać, przyznawać, odbierać i czyścić. Listing 6-26 opisuje metody uprawnień klasy sfUsers.

Listing 6-26 - Obsługa uprawnień użytkownika w akcji

[php]
class myAccountActions extends sfActions
{
  public function executeDoThingsWithCredentials()
  {
    $user = $this->getUser();

    // Dodawanie jednego lub większej liczby przywilejów
    $user->addCredential('foo');
    $user->addCredentials('foo', 'bar');

    // Sprawdzanie, czy użytkownik ma przywilej
    echo $user->hasCredential('foo');                      =>   prawda

    // Sprawdzanie, czy użytkownik ma oba przywileje
    echo $user->hasCredential(array('foo', 'bar'));        =>   prawda

    // Sprawdzanie, czy użytkownik ma jeden z przywilejów
    echo $user->hasCredential(array('foo', 'bar'), false); =>   prawda

    // Odbieranie przywileju
    $user->removeCredential('foo');
    echo $user->hasCredential('foo');                      =>   fałsz

    // Odbieranie wszystkich przywilejów (użyteczne w procesie wylogowania)
    $user->clearCredentials();
    echo $user->hasCredential('bar');                      =>   fałsz
  }
}

Jeżeli użytkownik ma przywilej 'foo', będzie mógł uzyskać dostęp do akcji, dla których wymagany jest ten przywilej w pliku security.yml. Przywileje mogą być również wykorzystywane do wyświetlenia treści tylko dla autoryzowanych użytkowników, co pokazano na Listingu 6-27.

Listing 6-27 - Obsługa przywilejów w szablonie

[php]
<ul>
  <li><?php echo link_to('section1', 'content/section1') ?></li>
  <li><?php echo link_to('section2', 'content/section2') ?></li>
  <?php if ($sf_user->hasCredential('section3')): ?>
  <li><?php echo link_to('section3', 'content/section3') ?></li>
  <?php endif; ?>
</ul>

Zupełnie jak w przypadku uwierzytelnienia, przywileje są często przyznawane użytkownikom w procesie logowania. Jest to główny powód do rozszerzenia klasy sfUser o metody logowania i wylogowania, w celu ustanawiania uprawnień użytkowników w jednym miejscu.

TIP Wśród pluginów symfony, sfGuardPlugin rozszerza klasę sesji, aby uczynić logowanie i wylogowanie prostszymi. Rozdział 17 zawiera więcej informacji na ten temat.

Przywileje złożone

Składnia YAML, wykorzystywana w pliku security.yml, umożliwia Ci ograniczenie dostępu do użytkowników posiadających kombinację przywilejów, używając powiązań typu I albo LUB. Z takimi możliwościami możesz zbudować kompleksowy system przepływu informacji i zarządzania uprawnieniami użytkowników -- na przykład zaplecze systemu zarządzania treścią (ang. Content Management System, CMS), dostępne wyłącznie dla użytkowników posiadających przywilej administratora, gdzie artykuły mogłyby być edytowane wyłącznie przez użytkowników z przywijelem editor i publikowane przez posiadających przywilej publisher. Listing 6-28 jest tego przykładem.

Listing 6-28 - Składnia kombinacji przywilejów

editArticle:
  credentials: [ admin, editor ]              # admin I editor

publishArticle:
  credentials: [ admin, publisher ]           # admin I publisher

userManagement:
  credentials: [[ admin, superuser ]]         # admin LUB superuser

Za każdym razem, gdy dodajesz nową parę nawiasów kwadratowych, logika zmienia się z I na LUB. Tak więc, możesz stworzyć bardzo skomplikowane kombinacje wymagań, takie jak ta:

credentials: [[root, [supplier, [owner, quasiowner]], accounts]]
             # root LUB (supplier I (owner LUB quasiowner)) LUB accounts

Metody walidacji i obsługi błędów

Walidacja danych wejścia akcji - głównie parametrów żądania - jest zadaniem monotonnym i nudnym. Symfony posiada wbudowany system sprawdzania poprawności żądania, który wykorzystuje metody akcji.

Zacznijmy od przykładu. Kiedy użytkownik wysyła żądanie akcji myAction, symfony zawsze poszukuje wpierw metody nazwanej validateMyAction. Jeżeli zostanie znaleziona, to następuje jej wywołanie. Wartość zwrócona przez tę funkcję określa następną metodę do wykonania: jeżeli jest to prawda, to executeMyAction() jest wykonywana; w przeciwnym wypadku, staje się nią handleErrorMyAction(). Jeżeli w drugim przypadku nie zostanie odnaleziona metoda handleErrorMyAction(), symfony próbuje znaleźć wbudowaną metodę handleError(). Jeżeli ta próba również nie powiedzie się, to zwracana jest wartość sfView::ERROR w celu wygenerowania szablonu myActionError. Ryc. 6-2 przedstawia ten proces.

Ryc. 6-2 - Proces walidacji

Proces walidacji

Tak więc, kluczem do walidacji jest podporządkowanie się nazewnictwu metod akcji:

  • validateActionName jest metodą walidującą, która zwraca wartość logiczną. Jest pierwszą, poszukiwaną metodą po zażądaniu wykonania akcji ActionName. Jeżeli nie istnieje, to akcja jest bezpośrednio wywoływana.
  • handleErrorActionName jest metodą wywoływaną w przypadku niepowodzenia metody walidacji. Jeżeli nie istnieje, to wyświetlany jest szablon Error.
  • executeActionName jest metodą akcji. Musi istnieć dla każdej akcji.

Listing 6-29 pokazuje przykład klasy akcji z metodami walidującymi. Bez względu na wynik walidacji, szablon myActionSuccess.php zostanie wykonany, lecz z różnymi parametrami.

Listing 6-29 - Przykładowe metody walidujące

[php]
class mymoduleActions extends sfActions
{
  public function validateMyAction()
  {
    return ($this->getRequestParameter('id') > 0);
  }

  public function handleErrorMyAction()
  {
    $this->message = "Błędne parametry";

    return sfView::SUCCESS;
  }

  public function executeMyAction()
  {
    $this->message = "Parametry są poprawne";
  }
}

W metodach validate() możesz umieścić dowolny kod. Upewnij się tylko, czy zwracają one albo prawdę, albo fałsz. Jako, że jest to metoda klasy sfActions, ma ona również dostęp do obiektów sfRequest i sfUser, które mogą być przydatne w walidacji wejścia i kontekstu żądania.

Możesz użyć tego mechanizmu, aby zaimplementować walidację formularzy (tj. kontrolować wartości wprowadzane przez użytkownika w formularzu, zanim zostaną przetworzone), lecz jest to typ powtarzalnego zadania, dla których symfony posiada zautomatyzowane narzędzia, które opisano w rozdziale 10.

Filtry

Proces zabezpieczania może być rozumiany jako filtr, przez który muszą przejść wszelkie żądania, zanim zostanie wykonana akcja. Powołując się na testy wykonane przez filtry, przetwarzanie żądania może zostać zmienione -- na przykład poprzez zmianę akcji (domyślna/zabezpieczeń zamiast żądanej, w przypadku filtra zabezpieczeń). Symfony rozszerza tę koncepcję do klas filtrów. Możesz ustalić liczbę klas filtrów do wykonania, przed wykonaniem akcji lub przed wygenerowaniem odpowiedzi, dla każdego żądania. Możesz traktować filtry jak sposób na opakowanie kodu, podobny do preExecute() i postExecute(), lecz na wyższym poziomie (aplikacji, a nie modułu).

Łańcuch filtrów

Symfony traktuje przetwarzanie żądania jak łańcuch filtrów. Kiedy żądanie jest odbierane przez framework, pierwszy filtr (którym zawsze jest sfRenderingFilter) jest wykonywany. W pewnym momencie wywołuje on następny filtr, ten kolejny itd. Kiedy ostatni filtr (którym zawsze jest sfExecutionFilter) zostaje wywołany, poprzedni filtr może ukończyć swe działanie, a po nim wszystkie poprzednie do filtra renderującego. Ryc. 6-3 ilustruje tę koncepcję diagramem przebiegu, używając sztucznie skróconego łańcucha (prawdziwe posiadają więcej klas).

Ryc. 6-3 - Przykładowy łańcuch filtrów

Przykładowy łańcuch filtrów

Proces ten wyjaśnia strukturę klas filtrów. Wszystkie z nich rozszerzają klasę sfFilter oraz zawierają jedną metodę execute(), przyjmującej za jedyny parametr obiekt $filterChain. W pewnym miejscu tej metody filtr przekazuje sterowanie kolejnemu w łańcuchu, wywołując $filterChain->execute(). Przeglądnij Listing 6-30 jako przykład. Filtry są więc, po prostu, podzielone na dwie części:

  • Kod przed wywołaniem $filterChain->execute() jest wykonywany przed przejściem do akcji.
  • Kod po wywołaniu $filterChain->execute()jest wykonywany po wyjściu z akcji, przed wygenerowaniem odpowiedzi.

Listing 6-30 - Struktura klasy filtra

[php]
class myFilter extends sfFilter
{
  public function execute ($filterChain)
  {
    // Kod przed wykonaniem akcji
    ...

    // Wywołanie kolejnego filtra w łańcuchu
    $filterChain->execute();

    // Kod po wykonaniu akcji
    ...
  }
}

Domyślny łańcuch filtrów jest definiowany w pliku konfiguracji aplikacji nazwanym filters.yml, co pokazano na Listingu 6-31. Plik ten zawiera listę filtrów do wykonania przy każdym żądaniu.

Listing 6-31 - Domyślny łańcuch filtrów, w myapp/config/filters.yml

rendering: ~
web_debug: ~
security:  ~

# Przeważnie tutaj będziesz chciał umieścić swoje filtry

cache:     ~
common:    ~
flash:     ~
execution: ~

Deklaracje te nie przyjmują żadnych parametrów (tylda znaczy w YAML-u "null"), ponieważ dziedziczą one je po konfiguracji jądra symfony. W jądrze symfony definiuje ustawienia class i param dla każdego z tych filtrów. Na przykład Listing 6-32 przedstawia domyślne parametry filtra rendering.

Listing 6-32 - Domyślne ustawienia filtra rendering, w $sf_symfony_data_dir/config/filters.yml

rendering:
  class: sfRenderingFilter   # Klasa filtra
  param:                     # Parametry filtra
    type: rendering

Poprzez pozostawienie pustej wartości (~) w pliku aplikacji filters.yml, dajesz znać symfony, aby utworzyła filtr z domyślnymi ustawieniami zdefiniowanymi w jądrze.

Możesz dostosować łańcuch filtrów na wiele różnych sposobów:

  • Możesz wyłączyć wybrane filtry poprzez dodanie parametru enabled: off. Na przykład aby wyłączyć filtr web debug, dopisz: web_debug: enabled: off
  • Nie usuwaj wpisów z pliku filters.yml, aby wyłączyć filtr. W tym przypadku zostanie wyrzucony przez symfony wyjątek.
  • Dodaj swoje nowe deklaracje w dowolnym miejscu łańcucha (zwykle za filtrem security), aby dodać własny filtr (co zostanie przedyskutowane w następnym podrozdziale). Zwróć uwagę na to, że filtr rendering musi być pierwszym wpisem łańcucha, a execution ostatnim.
  • Nadpisz klasy i parametry filtrów domyślnych (szczególnie, aby dostosować zabezpieczenia do Twoich własnych filtrów).

TIP Parametr enabled: off działa poprawnie przy wyłączaniu filtrów, lecz możesz deaktywować domyślne filtry poprzez plik settings.yml, modyfikując wartości ustawień web_debug, use_security, cache i use_flash. Spowodowane jest to faktem, że każdy z domyślnych filtrów posiada parametr condition, który sprawdza wartość tych ustawień.

Tworzenie własnego filtra

Tworzenie własnych filtrów jest zadaniem dość prostym. Stwórz definicję klasy, podobną do tej z Listingu 6-30, i umieść ją w pliku w jednym z katalogów lib/ Twojej aplikacji, aby wykorzystać korzyści automatycznego ładowania klas.

Akcje mogą spowodować forward lub przekierowanie do innej akcji, a co za tym idzie powtórne wykonanie łańcucha akcji, możesz więc chcieć ograniczyć wykonanie Twoich filtrów wyłącznie do pierwszego ich wywołania. Stworzona do tego metoda isFirstCall() klasy sfFilter zwraca wartość logiczną. Jej wywołanie ma sens tylko przed wykonaniem akcji.

Koncepcje te są prostsze do zrozumienia dzięki przykładom. Listing 6-33 przedstawia filtr, służący do automatycznego logowania użytkowników posiadających ciastko MyCookie, które jest hipotetycznie tworzone przez akcję logowania. Jest to szczątkowy, ale działający, system implementacji opcji "zapamiętaj mnie" w formularzu logowania.

Listing 6-33 - Przykładowa klasa filtru, w apps/myapp/lib/rememberFilter.class.php

[php]
class rememberFilter extends sfFilter
{
  public function execute($filterChain)
  {
    // Jednorazowe wykonanie filtru
    if ($this->isFirstCall())
    {
      // Filtry nie mają bezpośredniego dostępu do obiektów żądania i użytkownika.
      // Musisz wykorzystać obiekt kontekstu do pobrania ich
      $request = $this->getContext()->getRequest();
      $user    = $this->getContext()->getUser();

      if ($request->getCookie('MyWebSite'))
      {
        // Zaloguj
        $user->setAuthenticated(true);
      }
    }

    // Wykonaj kolejny filtr
    $filterChain->execute();
  }
}

W pewnych przypadkach, zamiast kontynuować działanie łańcucha filtrów, będziesz musiał wykonać na końcu filtra forward do innej akcji. sfFilter nie posiada metody forward(), ale sfController ją ma, więc możesz to zrobić wywołując:

[php]
return $this->getContext()->getController()->forward('mymodule', 'myAction');

NOTE Klasa sfFilter posiada metodę initialize(), wykonywaną w momencie tworzenia obiektu filtra. Możesz nadpisać ją w Twoim filtrze, jeżeli musisz obsługiwać parametry (definiowane w filters.yml, co opisano poniżej) na swój własny sposób.

Aktywacja filtrów i parametry

Utworzenie samego filtra nie wystarcza do jego aktywacji. Musisz jeszcze dodać go do łańcucha, a więc zadeklarować klasę filtra w pliku filters.yml, znajdującym się w katalogu config/ aplikacji bądź modułu, co pokazano na Listingu 6-34.

Listing 6-34 - Przykładowy plik aktywacji filtra, w apps/myapp/config/filters.yml

rendering: ~
web_debug: ~
security:  ~

remember:                 # Filtry muszą posiadać unikalną nazwę
  class: rememberFilter
  param:
    cookie_name: MyWebSite
    condition:   %APP_ENABLE_REMEMBER_ME%

cache:     ~
common:    ~
flash:     ~
execution: ~

Po aktywacji filtr jest wykonywany przy każdym żądaniu. Plik konfiguracji filtra może zawierać jedną albo więcej definicji parametrów pod kluczem param. Klasa filtra ma możliwość pobrania wartoścy tych parametrów przy pomocy metody getParameter(). Listing 6-35 demonstruje, w jaki sposób pobrać wartość parametru.

Listing 6-35 - Pobieranie wartości parametru, w apps/myapp/lib/rememberFilter.class.php

[php]
class rememberFilter extends sfFilter
{
  public function execute ($filterChain)
  {
      ...
      if ($request->getCookie($this->getParameter('cookie_name')))
      ...
  }
}

Parametr condition jest sprawdzany przez łańcuch filtrów, aby dowiedzieć się, czy filtr ma być wykonywany. Tak więc, deklaracje Twoich filtrów mogą polegać na konfiguracji aplikacji, zupełnie jak ta z Listingu 6-34. Filtr remember zostanie wykonany tylko wtedy, gdy plik app.yml aplikacji zawiera:

all:
  enable_remember_me: on

Przykładowe filtry

Filtry są bardzo przydatne przy wykonywaniu kodu powtarzającego się przy każdym żądaniu akcji. Na przykład, jeżeli używasz zdalnego skryptu statystyk (ang. analytics system), prawdopodobnie będziesz musiał dopisać fragment kodu wywołujący skrypt trackera na każdej stronie. Możesz umieścić ten kod w globalnym szablonie, ale wtedy będzie on aktywny w całej aplikacji. Alternatywnie możesz umieścić ten kod w filtrze takim, jak ten z Listingu 6-36 i aktywować go dla wybranych modułów.

Listing 6-36 - Filtr Google Analytics

[php]
class sfGoogleAnalyticsFilter extends sfFilter
{
  public function execute($filterChain)
  {
    // Przed wykonaniem akcji nie robimy nic
    $filterChain->execute();

    // Dopisz do odpowiedzi kod trackera
    $googleCode = '
<script src="http://www.google-analytics.com/urchin.js"  type="text/javascript">
</script>
<script type="text/javascript">
  _uacct="UA-'.$this->getParameter('google_id').'";urchinTracker();
</script>';
    $response = $this->getContext()->getResponse();
    $response->setContent(str_ireplace('</body>', $googleCode.'</body>',$response->getContent()));
   }
}

Zwróć uwagę na to, że filtr ten nie jest doskonały, gdyż nie powinien dodawać trackera do odpowiedzi, które nie są kodem HTML.

Kolejnym przykładem może być filtr, który zmienia żądanie na wykorzystujące SSL, jeżeli nie zostało zabezpieczone wcześniej, co przedstawiono na Listingu 6-37.

Listing 6-37 - Filtr bezpiecznej komunikacji

[php]
class sfSecureFilter extends sfFilter
{
  public function execute($filterChain)
  {
    $context = $this->getContext();
    $request = $context->getRequest();

    if (!$request->isSecure())
    {
      $secure_url = str_replace('http', 'https', $request->getUri());

      return $context->getController()->redirect($secure_url);
      // Nie kontynuujemy wykonywania łańcucha filtrów
    }
    else
    {
      // Żądanie jest szyfrowane, więc kontynuujemy
      $filterChain->execute();
    }
  }
}

Filtry są intensywnie wykorzystywane jako plug-iny, jako że umożliwają rozszerzenie całej aplikacji. Przeczytaj rozdział 17, aby dowiedzieć się więcej o plug-inach, i zaglądnij do wiki (http://www.symfony-project.com/trac/wiki) po więcej przykładów.

Konfiguracja modułów

Pewne zachowania modułów zależą od ich konfiguracji. Aby je zmodyfikować, musisz utworzyć plik module.yml w katalogu config/ tegoż modułu i zdefiniować ustawienia dla każdego ze środowisk (lub pod nagłówkiem all: dla wszystkich). Listing 6-38 jest przykładem takiego pliku modułu mymodule.

Listing 6-38 - Konfiguracja modułu, w apps/myapp/modules/mymodule/config/module.yml

all:                 # Dla wszystkich środowisk
  enabled:     true
  is_internal: false
  view_name:   sfPHP

Parametr enabled pozwala Ci na wyłączenie wszystkich akcji modułu. Są one wtedy przekierowywane do akcji module_disabled_module/module_disabled_action (zdefiniowanej w settings.yml).

Parametr is_internal umożliwia ograniczenie wykonania wszystkich akcji, wyłącznie do wywołań wewnętrznych. Na przykład jest to użytecznie do tworzenia akcji poczty e-mail, które musisz mieć możliwość wywołania z innych akcji, żeby wysłać list, lecz z zewnątrz nie może być dostępna.

Parametr view_name definiuje nazwę klasy widoku. Musi ona być pochodną sfView. Nadpisanie tej wartości umożliwia skorzystanie z innego systemu widoku, z innym systemem szablonów, takiego jak na przykład Smarty.

Podsumowanie

Warstwa kontrolera symfony jest podzielona na dwie części: front controller, który jest punktem wejścia aplikacji w danym środowisku, oraz akcje, zawierające logikę aplikacji. Akcja ma możliwość określenia jak zostanie wyświetlony jej widok, poprzez zwrócenie jednej ze stałych sfView. Wewnątrz akcji możesz manipulować różnymi elementami kontekstu, wliczając w to obiekt żądania (sfRequest) i obecnej sesji użytkownika (sfUser).

Wykorzystując obiekt sesji, akcji i konfigurację zabezpieczeń, otrzymujemy kompletny system bezpieczeństwa, łącznie z ograniczaniem dostępu i przywilejami. Specjalne metody akcji validate() oraz handleError() umożliwiają obsługę walidacji żądania. I jeśli metody preExecute() i postExecute() służą wtórnemu wykorzystaniu kodu w obrębie modułu, to filtry pełnią tę samą funkcję, lecz w obrębie całej aplikacji, za sprawą kontrolera, który wywołuje ich kod przy każdym żądaniu.