Académique Documents
Professionnel Documents
Culture Documents
Wydawnictwo HELION
ul. Kościuszki 1c, 44-100 GLIWICE
tel. 32 231 22 19, 32 230 98 63
e-mail: helion@helion.pl
WWW: http://helion.pl (księgarnia internetowa, katalog książek)
Drogi Czytelniku!
Jeżeli chcesz ocenić tę książkę, zajrzyj pod adres
http://helion.pl/user/opinie?symfo2_ebook
Możesz tam wpisać swoje uwagi, spostrzeżenia, recenzję.
ISBN: 978-83-246-6241-8
Printed in Poland.
• Oceń książkę
Spis treści
Podziękowania .............................................................................. 13
Włodzimierz Gajda
Lublin, 20 maja 2012 r.
14 Symfony 2 od podstaw
Część I
Tworzenie prostych
stron WWW
16 Część I ♦ Tworzenie prostych stron WWW
Rozdział 1. ♦ Uruchomienie przykładowego projektu 17
Rozdział 1.
Uruchomienie
przykładowego projektu
Oprogramowanie Symfony 2 jest dostępne w postaci dwóch różnych dystrybucji:
Symfony Standard with vendors, nazwa pliku: Symfony_Standard_Vendors_2.0.x.zip;
Symfony Standard without vendors, nazwa pliku: Symfony_Standard_2.0.x.zip.
ROZWIĄZANIE
Krok 1. Utwórz nowy projekt Symfony 2
W folderze przeznaczonym na aplikacje WWW1 utwórz folder Symfony/. Do folderu tego
wypakuj zawartość archiwum Symfony_Standard_Vendors_2.0.X.zip2. Po wykonaniu tej
operacji zawartość folderu Symfony/ powinna być taka jak na rysunku 1.1.
Rysunek 1.1.
Katalogi i pliki utworzone
po wypakowaniu archiwum
Symfony_Standard_
Vendors_2.0.X.zip
1
Procedura instalacji oprogramowania jest przedstawiona w dodatku A. Jeśli przygotowałeś stanowisko
pracy zgodnie z podanym opisem, to tym folderem jest C:\xampp\htdocs\.
2
W chwili pisania książki najnowszą dostępną wersją była wersja Symfony_Standard_Vendors_2.0.10.zip.
Wszystkie podane przykłady zostały wykonane w wersji 2.0.10. Symfony 2 jest obecnie rozwijane i w chwili
wydania książki dostępne będą z pewnością nowe wersje.
Rozdział 1. ♦ Uruchomienie przykładowego projektu 19
Statyczne pliki zawarte w folderze web/ (m.in style .css, skrypty .js oraz pliki graficzne
.jpg i .gif) są w oryginalnej dokumentacji określane wspólnym terminem assets.
3
Więcej o środowiskach w kolejnym rozdziale.
20 Część I ♦ Tworzenie prostych stron WWW
ujrzysz pustą stronę WWW, może to świadczyć o tym, że zainstalowana jest zbyt stara
wersja PHP. W celu upewnienia się, że zainstalowane oprogramowanie jest odpowiednie,
odwiedź adres:
http://localhost/Symfony/web/config.php
4
Błąd ten występuje w systemie Windows 7, gdy w PHP zainstalowany jest akcelerator APC.
Rozdział 1. ♦ Uruchomienie przykładowego projektu 21
Oczywiście należy usunąć wszelkie błędy zgłaszane przez skrypt widoczny na rysunku 1.3.
Jeśli w adresie:
http://localhost/hello-world/web/app_dev.php
spróbujesz pominąć nazwę pliku app_dev.php:
http://localhost/hello-world/web/
ujrzysz wówczas stronę z tekstem:
Oops! An Error Occurred
The server returned a "404 Not Found".
Podsumowanie
Uruchamiając przykładową aplikację, poznaliśmy rolę, jaką odgrywają pliki i foldery
widoczne na rysunku 1.4.
Rozdział 1. ♦ Uruchomienie przykładowego projektu 23
Rysunek 1.4.
Pliki i foldery poznane
podczas uruchamiania
przykładowej aplikacji
Dystrybucja without vendors nie zawiera folderu vendor/, w którym znajdują się rozmaite
pakiety konieczne do uruchomienia aplikacji. Dlatego naukę rozpoczynamy od dystrybucji
with vendors.
Przestrzenie nazw
Przestrzenie nazw umożliwiają stosowanie wieloczłonowych nazw klas. Dzięki temu
nazwy klas zawartych w aplikacji tworzą strukturę drzewa. Na przykład w Symfony 2
występują klasy o nazwach:
Symfony\Component\Finder\Finder
Symfony\Component\DomCrawler\Crawler
Symfony\Component\ClassLoader\UniversalClassLoader
Przestrzenie nazw rozwiązują dwa istotne problemy dotyczące nazewnictwa klas w du-
żych projektach:
Gwarantują niezależność nazw klas tworzonych przez grupy programistów.
Umożliwiają stosowanie skróconych nazw.
Dzięki temu wewnątrz własnej aplikacji możemy utworzyć klasę o nazwie Lorem, nie
przejmując się tym, czy w innym miejscu aplikacji istnieje klasa o identycznej nazwie.
Przestrzenie nazw tworzymy, umieszczając klasy w osobnych folderach i dołączając
deklarację namespace.
26 Część I ♦ Tworzenie prostych stron WWW
należy utworzyć foldery Lorem, Ipsum i Dolor oraz umieścić w nich plik Sit.php:
Lorem\Ipsum\Dolor\Sit.php
W celu skrócenia powyższego zapisu w dowolnym pliku aplikacji możesz dodać in-
strukcję:
use Lorem\Ipsum\Dolor\Sit;
Pakiet
Pakiety są niezależnymi fragmentami aplikacji. Każdy z pakietów może być wyko-
rzystywany w wielu różnych aplikacjach. Cały pakiet jest umieszczony wewnątrz jed-
nego folderu i może zawierać kontrolery, akcje, widoki, pliki konfiguracyjne, klasy
pomocnicze oraz zasoby takie jak style CSS, obrazy czy skrypty JavaScript.
Wspólną cechą wszystkich pakietów jest ich niezależność od konkretnej aplikacji. Celem
tworzenia pakietów jest ułatwienie ponownego wykorzystania fragmentu projektu.
Kontroler i akcja
W Symfony 2 kontroler odpowiada za przetworzenie żądania HTTP i wygenerowanie
odpowiedzi. Wprawdzie kontrolerem może być zarówno metoda klasy, jak i zwykła
funkcja, jednak w przykładach omawianych w dalszej części podręcznika w roli kontro-
lera będziemy stosowali wyłącznie metody klasy.
Dla ułatwienia będę stosował konwencję nazewniczą z frameworków Symfony 1.4 oraz
Zend Framework. Kontrolerem będę nazywał klasę (np. DefaultController), zaś akcją
— metodę (np. indexAction()). Unikniemy wówczas dwuznaczności.
Widok
Widoki to pliki, które zawierają kod HTML oraz specjalne instrukcje umieszczające
w kodzie HTML wartości zmiennych. W Symfony 2 domyślnym językiem przetwarza-
nia widoków jest Twig. Pliki widoków mają podwójne rozszerzenie .html.twig.
ROZWIĄZANIE
Krok 1. Utwórz nowy projekt Symfony 2
W folderze przeznaczonym na aplikacje WWW1 utwórz folder hello-world/. Do folderu
tego wypakuj zawartość archiwum Symfony_Standard_Vendors_2.0.X.zip. Po wykona-
niu tej operacji zawartość folderu hello-world/ powinna być taka jak na rysunku 2.1.
1
Procedura instalacji oprogramowania jest przedstawiona w dodatku A. Jeśli przygotowałeś stanowisko
pracy zgodnie z podanym opisem, to tym folderem jest C:\xampp\htdocs\.
Rozdział 2. ♦ Hello, world! 29
Rysunek 2.1.
Foldery i pliki projektu
Hello, World!
_demo_secured:
resource: "@AcmeDemoBundle/Controller/SecuredController.php"
type: annotation
_demo:
resource: "@AcmeDemoBundle/Controller/DemoController.php"
type: annotation
prefix: /demo
Powyższa komenda będzie działała poprawnie wyłącznie wtedy, gdy program php.exe
jest dostępny w ścieżkach poszukiwań. Należy także pamiętać, że php.exe uruchamiane
wsadowo może mieć inną konfigurację niż PHP uruchamiane protokołem HTTP. Do
sprawdzenia poprawności instalacji PHP w wierszu poleceń służy skrypt app/check.php,
który uruchamiamy poleceniem:
php app/check.php
Procedura instalacji PHP w wierszu poleceń jest szczegółowo opisana w dodatku A.
która wygeneruje nowy pakiet (ang. bundle) wewnątrz projektu. Po wydaniu komendy
w odpowiedzi na monit:
Bundle namespace:
30 Część I ♦ Tworzenie prostych stron WWW
Nazwa pakietu jest dwuczłonowa. Pierwszy człon może być traktowany jako oznaczenie
autora pakietu. Nazwa:
My/HelloworldBundle
Nazwa pakietu musi się kończyć przyrostkiem Bundle. Poprawnymi nazwami pa-
kietów są:
Gajdaw/HelloworldBundle
GW/HelloBundle
Gajda/HelloWorldBundle
2
Adnotacje są specjalnymi komentarzami umieszczanymi w kodzie PHP.
Rozdział 2. ♦ Hello, world! 31
W celu wygenerowania nowego pakietu w sposób wsadowy (tj. bez trybu interak-
tywnego) wydaj komendę app/console generate:bundle w sposób następujący:
Rysunek 2.2.
Foldery i pliki
utworzone po wydaniu
komendy app/console
generate:bundle
3
Innymi słowy do folderu hello-world/src/ nie da się zajrzeć za pomocą przeglądarki.
32 Część I ♦ Tworzenie prostych stron WWW
Jeśli na pytanie:
Do you want to generate the whole directory structure [no]?
odpowiesz:
yes
Rysunek 2.3.
Kompletna struktura
zasobów z folderu
Resources/
use Symfony\Component\HttpKernel\Kernel;
use Symfony\Component\Config\Loader\LoaderInterface;
return $bundles;
}
Opcja:
resource: "@MyHelloworldBundle/Controller/"
namespace My\HelloworldBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
Komentarz widoczny na listingu 2.4 zawiera dwie adnotacje — @Route oraz @Template:
/**
* @Route("/hello/{name}")
* @Template()
*/
Adnotacja @Route ustala adres URL akcji, zaś adnotacja @Template włącza przetwa-
rzanie widoku.
Adnotacja:
@Route("/hello/{name}")
4
Adnotacja @Route("/hello/abc") występuje przed metodą indexAction(). Wynika stąd, że po odwiedzeniu
strony /hello/abc wykonana zostanie akcja index.
36 Część I ♦ Tworzenie prostych stron WWW
Zapis:
{{ name }}
Po odwiedzeniu adresu:
http://localhost/hello-world/web/hello/abc
ujrzysz stronę widoczną na rysunku 2.4.
Rysunek 2.4.
Strona
wyświetlana
po odwiedzeniu
adresu
http://localhost/
hello-world/web/
app.php/hello/abc
Adres:
http://localhost/hello-world/web/app_dev.php/hello/abc
jest domyślnie zajęty przez przykładową aplikację z rysunku 1.2. Adres ten będzie się
odnosił do naszej aplikacji dopiero wtedy, gdy z projektu usuniesz przykład demo.
Wykonaliśmy to w kroku 2.
Rozdział 2. ♦ Hello, world! 37
Zwróć uwagę, że na rysunku 2.4 nie pojawia się tzw. Web Debug Toolbar — pasek
narzędzi developerskich, który jest widoczny w dolnej części rysunku 1.2. Dzieje się tak
z dwóch powodów. Po pierwsze, pasek Web Debug Toolbar jest wyświetlany wyłącznie
w trybie deweloperskim, a po drugie, generowana strona WWW nie zawiera znaczników
<html> oraz <body>.
namespace My\HelloworldBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
Adnotacja:
* @Route("/hello-world.html")
Z nagłówka metody indexAction() usuwamy parametr $name. Metoda akcji jest bezpa-
rametrowa:
public function indexAction()
<h1>Hello, world!</h1>
</body>
</html>
Rysunek 2.5. Strona otrzymana po wprowadzeniu modyfikacji z listingów 2.6 oraz 2.7
Rozdział 2. ♦ Hello, world! 39
Adresy:
http://localhost/hello-world/web/app.php/hello-world.html
http://localhost/hello-world/web/hello-world.html
są równoważne i mogą być używane zamiennie. Powodują one uruchomienie aplikacji
w środowisku produkcyjnym. W takim przypadku informacje o błędach w aplikacji nie
będą wyświetlane w przeglądarce.
Adres:
http://localhost/hello-world/web/app_dev.php/hello-world.html
uruchamia aplikację w środowisku deweloperskim, w którym wszystkie błędy są wy-
świetlane w oknie przeglądarki. Dodatkowo na stronach zawierających kod HTML po-
jawia się widoczny w dolnej części rysunku 2.5 pasek narzędzi nazywany Web Debug
Toolbar.
Web Debug Toolbar pojawia się wówczas, gdy dokument zawiera znaczniki HTML
<html> oraz <body> i jest wyświetlany w środowisku deweloperskim.
Zmodyfikowane pliki
Wykonując przykład Hello, world!, ręcznie zmodyfikowaliśmy tylko cztery pliki przed-
stawione na rysunku 2.6.
Rysunek 2.6.
Pliki, które ręcznie
modyfikowaliśmy,
wykonując projekt
Hello, world!
40 Część I ♦ Tworzenie prostych stron WWW
Środowiska pracy
Aplikacje tworzone w Symfony 2 umożliwiają zdefiniowanie ogromnej liczby opcji kon-
figuracyjnych. W celu ułatwienia wprowadzania zmian w konfiguracji wprowadzono
pojęcie środowiska. Środowisko jest zestawem opcji konfiguracyjnych ustalających
parametry pracy aplikacji. Domyślnie projekt tworzony w Symfony 2 zawiera dwa śro-
dowiska: produkcyjne (prod) oraz deweloperskie (dev).
Pierwszy parametr konstruktora AppKernel() jest nazwą środowiska, a drugi wyłącza wy-
świetlanie komunikatów diagnostycznych. Jeśli zatem użyjemy adresów:
http://localhost/hello-world/web/app.php/hello-world.html
http://localhost/hello-world/web/hello-world.html
uruchomimy wówczas aplikację w środowisku prod w taki sposób, by komunikaty o błę-
dach nie były wyświetlane.
Pierwszy parametr przekazany do konstruktora AppKernel(), czyli prod lub dev, decy-
duje o tym, które pliki konfiguracyjne zostaną użyte. Jeśli parametrem tym jest prod, to użyte
zostaną pliki:
app/config/config.yml
app/config/config_prod.yml
app/config/routing.yml
...
Pliki:
app/config/config.yml
app/config/routing.yml
wyłącznie dla środowiska dev. Wynika stąd między innymi różnica w adresach URL.
W przykładzie z poprzedniego rozdziału adres:
http://localhost/hello-world/web/app_dev.php/
powoduje wyświetlenie strony z rysunku 1.2. Po podaniu adresu:
http://localhost/hello-world/web/
ujrzymy zaś komunikat:
Oops! An Error Occurred
The server returned a "404 Not Found".
Polecenie generate:bundle:
Tworzy nowy pakiet w folderze src/, np. src/My/HelloworldBundle/.
W pliku app/AppKernel.php dodaje instrukcję włączającą pakiet, np.:
new My\HelloworldBundle\MyHelloworldBundle(),
<?php
namespace My\HelloworldBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
W celu skrócenia powyższego zapisu stosujemy instrukcję use. Jeśli umieścimy w skryp-
cie instrukcję use, tworzenie obiektu klasy DefaultController możemy zapisać jako:
use My\HelloworldBundle\Controller\DefaultController;
...
new DefaultController();
Skrócona nazwa klasy Controller pojawia się m.in. w deklaracji klasy DefaultController:
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
...
class DefaultController extends Controller
{
...
}
Bez użycia instrukcji use po słowie kluczowym extends musielibyśmy użyć pełnej
nazwy klasy, czyli:
class DefaultController extends
Symfony\Bundle\FrameworkBundle\Controller\Controller
{
...
}
44 Część I ♦ Tworzenie prostych stron WWW
Cechy Symfony 2
Symfony 2 zostało zaprojektowane w taki sposób, by pozwalało na maksymalne do-
stosowanie tworzonej aplikacji do konkretnych warunków. Nie jesteśmy związani ani
z żadnym formatem konfiguracyjnym, ani z językiem przetwarzania szablonów, ani
z systemem ORM.
Co ciekawe, kod całego frameworka jest także zawarty w pakietach. Przekonasz się o tym,
analizując kod dystrybucji without vendors. Rozmiar tej dystrybucji jest tak mały dlatego,
że po usunięciu pakietów cały projekt składa się z kilku plików! Lista plików tworzących
projekt, które nie są zawarte w pakietach, jest przedstawiona na rysunku 2.7.
Rysunek 2.7.
Pliki tworzące projekt
Symfony 2, które nie są
zawarte w pakietach
Formaty konfiguracji
Po wydaniu polecenia:
php app/console generate:bundle
ujrzysz monit, który umożliwia wybranie formatu konfiguracji dla tworzonego pakietu:
Configuration format (yml, xml, php, or annotation) [annotation]:
Rozdział 2. ♦ Hello, world! 45
Format YAML
Jeśli tworząc pakiet, wybierzesz format konfiguracji YAML, wówczas w klasie Default
´Controller nie pojawią się adnotacje @Route ani @Template. Adnotacja @Template
zostanie zastąpiona5 instrukcją:
return $this->render('MyHelloworldBundle:Default:index.html.twig', array('name' =>
$name));
w formacie:
MyHelloworldBundle_homepage:
pattern: /hello/{name}
defaults: { _controller: MyHelloworldBundle:Default:index }
Format XML
Jeśli wybierzesz format konfiguracji XML, kontroler DefaultController będzie identyczny
jak w przypadku konfiguracji YAML, zaś konfiguracja routingu zostanie zapisana w pliku:
src\My\HelloworldBundle\Resources\config\routing.xml
w formacie:
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/routing
´http://symfony.com/schema/routing/routing-1.0.xsd">
5
Adnotacje @Template() omówimy szczegółowo w części poświęconej szablonom Twig.
46 Część I ♦ Tworzenie prostych stron WWW
Format PHP
Po wybraniu formatu konfiguracji PHP konfiguracja routingu zostanie zapisana w pliku:
src\My\HelloworldBundle\Resources\config\routing.php
w formacie:
<?php
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;
return $collection;
Pakiety Symfony 2 mogą być bardzo złożone i wymagać własnych zasobów (np. stylów
czy obrazów). W takim przypadku warto zasoby umieścić wewnątrz folderu pakietu.
W ten sposób pakiet będzie stanowił kompletną całość. Instalacja pakietu w aplikacji nie
będzie wymagała kopiowania żadnych dodatkowych plików.
1
Wielokropek występujący na początku ścieżki zostanie zastąpiony nazwami folderów, w których
umieszczono projekt.
48 Część I ♦ Tworzenie prostych stron WWW
na pytanie:
Do you want to generate the whole directory structure [no]?
odpowiesz:
yes
Rysunek 3.1.
Foldery przeznaczone
na pliki graficzne, style
CSS oraz skrypty
JavaScript
nie są dostępne za pomocą protokołu HTTP. W celu udostępnienia zasobów .css, .js i .jpg
należy je przekopiować do folderu web/. Służy do tego komenda:
php app/console assets:install web
Po wydaniu powyższej komendy w folderze web/ pojawi się folder bundles/, a w nim
folder pakietu, foldery css/, images/ i js/ oraz przekopiowane zasoby:
[projekt]/web/bundles/[pakiet]/css/style.css
[projekt]/web/bundles/[pakiet]/js/skrypt.js
[projekt]/web/bundles/[pakiet]/images/zdjecie.jpg
2
Oczywiście foldery te możesz także utworzyć ręcznie.
Rozdział 3. ♦ Dołączanie zewnętrznych zasobów 49
Polecenie:
php app/console assets:install web
kopiuje zasoby z folderów wszystkich zainstalowanych w projekcie pakietów:
[projekt]/src/[pakiet]/Resources/public/
do folderów:
[projekt]/web/bundles/[pakiet]/
Parametr web jest nazwą folderu docelowego. Jeśli zechcesz zasoby .css i .js przeko-
piować do folderu o innej nazwie niż web (może tego wymagać konfiguracja Twojego
serwera), wówczas w poleceniu podaj nazwę folderu, np. public_html:
php app/console assets:install public_html
Zasoby zostaną wtedy umieszczone w folderach:
[projekt]/public_html/bundles/[pakiet]/
Podany folder (np. public_html) musi istnieć.
ROZWIĄZANIE
Krok 1. Utwórz nowy projekt Symfony 2
W folderze przeznaczonym na aplikacje WWW utwórz folder mountains/ i wypakuj do
niego zawartość archiwum Symfony_Standard_Vendors_2.0.X.zip.
_demo_secured:
resource: "@AcmeDemoBundle/Controller/SecuredController.php"
type: annotation
_demo:
resource: "@AcmeDemoBundle/Controller/DemoController.php"
type: annotation
prefix: /demo
W odpowiedzi na monit:
Bundle namespace:
* @Route("/valley.html")
* @Template()
*/
public function indexAction()
{
return array();
}
}
<h1>Tatry</h1>
<h2>Pusta Dolinka</h2>
<p>
<img src="{{ asset('images/pusta-dolinka.jpg') }}" alt="Pusta Dolinka" />
</p>
...odsyłacze do Wikipedii...
</body>
</html>
52 Część I ♦ Tworzenie prostych stron WWW
Wartością parametru pattern jest adres URL, a parametru _controller — akcja index
w kontrolerze DefaultController pakietu My/ValleyBundle.
3
Nie usuwaj zawartości, która tam była. Przesuń ją poniżej reguły _homepage.
Rozdział 3. ♦ Dołączanie zewnętrznych zasobów 53
Przykład 3.2.
Dolina Pięciu Stawów Polskich
Wykorzystując oprogramowanie Symfony 2, wykonaj aplikację, która będzie prezento-
wała stronę WWW ze zdjęciem przedstawiającym tatrzańską Dolinę Pięciu Stawów Pol-
skich. Wykorzystaj style CSS oraz skrypty JavaScript zawarte w pliku 03-02-start.zip.
Zadanie wykonaj w taki sposób, by strona ze zdjęciem Doliny Pięciu Stawów była wy-
świetlana od razu po odwiedzeniu w przeglądarce folderu web/. Zasoby (tj. pliki .css, .js
oraz .jpg) umieść wewnątrz folderu pakietu. Do opublikowania zasobów w folderze web/
wykorzystaj polecenie:
php app/console assets:install web
ROZWIĄZANIE
Krok 1. Utwórz nowy projekt Symfony 2
W folderze przeznaczonym na aplikacje WWW utwórz folder tatras/ i wypakuj do niego
zawartość archiwum Symfony_Standard_Vendors_2.0.X.zip.
_demo_secured:
resource: "@AcmeDemoBundle/Controller/SecuredController.php"
54 Część I ♦ Tworzenie prostych stron WWW
type: annotation
_demo:
resource: "@AcmeDemoBundle/Controller/DemoController.php"
type: annotation
prefix: /demo
W odpowiedzi na monit:
Bundle namespace:
W odpowiedzi na pytanie:
Do you want to generate the whole directory structure [no]?
odpowiedz:
yes
{
return array();
}
}
<h1>Tatry</h1>
<h2>Dolina Pięciu Stawów Polskich</h2>
<h3>Wielki Staw Polski</h3>
<p>
<img src="{{ asset('bundles/mylake/images/dolina-pieciu-stawow-polskich.jpg')
´}}" alt="Dolina Pięciu Stawów Polskich" />
</p>
...odsyłacze do Wikipedii...
</body>
</html>
W folderze tatras/web/ pojawi się folder bundles/mylake/, zawierający zasoby .css, .js
oraz .jpg.
Przeanalizujmy kod strony WWW z listingu 4.1. Strona ta prezentuje tekst jednego
wiersza.
<div id="pojemnik">
<h1 id="naglowek">Wiersze i wierszyki...</h1>
<div id="tekst">
<h2>Włodzimierz Gajda</h2>
<h3>Dwa kabele</h3>
<p>
Czesem tak sie dziwnie składa,<br />
Że gdy nic nie zapowiada<br />
Żadnych nieszczęść czy frustracji,<br />
Jakiś smyk wkroczy do akcji<br />
I, być może bez złych chęci,<br />
Sielankę ojcu zamąci.<br />
...
</p>
</div>
<div id="dol"></div>
</div>
</body>
</html>
58 Część I ♦ Tworzenie prostych stron WWW
Jeśli kod HTML strony z listingu 4.1 zechcemy wykorzystać do prezentacji wielu wier-
szy w obrębie jednej witryny, należy przygotować dwa osobne pliki: layout.html oraz
tekst.html. Pierwszy z plików będzie zawierał znaczniki html, body, div oraz h1, ustalające
wygląd strony WWW. W drugim dokumencie, tekst.html, należy umieścić treść wiersza.
Pliki layout.html oraz tekst.html są przedstawione na listingach 4.2 oraz 4.3.
Listing 4.2. Plik layout.html otrzymany po podzieleniu strony z listingu 4.1 na układ i treść
<!DOCTYPE html>
<html>
<head>
<title>Wierszyki...</title>
</head>
<body>
<div id="pojemnik">
<h1 id="naglowek">Wiersze i wierszyki...</h1>
<div id="tekst">
</div>
<div id="dol"></div>
</div>
</body>
</html>
Listing 4.3. Plik tekst.html otrzymany po podzieleniu strony z listingu 4.1 na układ i treść
<h2>Włodzimierz Gajda</h2>
<h3>Dwa kabele</h3>
<p>
Czesem tak sie dziwnie składa,<br />
Że gdy nic nie zapowiada<br />
Żadnych nieszczęść czy frustracji,<br />
Jakiś smyk wkroczy do akcji<br />
I, być może bez złych chęci,<br />
Sielankę ojcu zamąci.<br />
...
</p>
W celu wykonania strony z treścią nowego wierszyka, np. wyliczanki Ene, due, wystarczy
teraz przygotować kod z listingu 4.4 i ozdobić go szablonem z listingu 4.2.
Na tym polega idea dekoracji widoków akcji przy użyciu wspólnego szablonu stron.
W pliku layout.html.twig umieszczamy kod HTML z listingu 4.2, dodając w nim specjalne
znaczniki {% block %}. Zarys pliku layout.html.twig jest przedstawiony na listingu 4.5.
<body>
<div id="pojemnik">
<h1 id="naglowek">Wiersze i wierszyki...</h1>
<div id="tekst">
{% block content %}
{% endblock %}
</div>
<div id="dol"></div>
</div>
</body>
Znaczniki:
{% block content %}
{% endblock %}
definiują w szablonie blok o nazwie content. Blok ten może być wypełniony w widokach
akcji dowolną zawartością. W celu wypełnienia bloku content w widoku akcji index treścią
wiersza pt. Dwa kabele należy w pliku index.html.twig wprowadzić zawartość widoczną
na listingu 4.6.
60 Część I ♦ Tworzenie prostych stron WWW
{% block content %}
<h2>Włodzimierz Gajda</h2>
<h3>Dwa kabele</h3>
<p>
Czesem tak sie dziwnie składa,<br />
Że gdy nic nie zapowiada<br />
Żadnych nieszczęść czy frustracji,<br />
Jakiś smyk wkroczy do akcji<br />
I, być może bez złych chęci,<br />
Sielankę ojcu zamąci.<br />
...
</p>
{% endblock %}
Pierwsza z instrukcji z listingu 4.6 włącza dekorację widoku akcji szablonem layout.html.
twig z folderu My/LoremBundle/Resources/views/. Parametrem funkcji extends jest nazwa:
MyLoremBundle::layout.html.twig
Dzięki temu, że nazwa kontrolera jest pusta (tj. w parametrze funkcji extends wystę-
pują obok siebie dwa dwukropki ::), szablon będzie pobrany z folderu views/ pakietu
LoremBundle.
ROZWIĄZANIE
Krok 1. Utwórz nowy projekt Symfony 2
W folderze przeznaczonym na aplikacje WWW utwórz folder poems/ i wypakuj do niego
zawartość archiwum Symfony_Standard_Vendors_2.0.X.zip. Z projektu usuń pakiet
demo, który jest zawarty w folderze src/Acme/.
Zwróć uwagę, że adnotacja @Route() zawiera adres /. Dzięki temu strona akcji index
będzie wyświetlana po odwiedzeniu w przeglądarce folderu:
.../web/
<div id="pojemnik">
<h1 id="naglowek">Wiersze i wierszyki...</h1>
<div id="tekst">
{% block content %}
{% endblock %}
</div>
<div id="dol"></div>
</div>
</body>
</html>
{% block content %}
<h2>Włodzimierz Gajda</h2>
<h3>Dwa kabele</h3>
<p>
Czesem tak sie dziwnie składa,<br />
Że gdy nic nie zapowiada<br />
Żadnych nieszczęść czy frustracji,<br />
Jakiś smyk wkroczy do akcji<br />
I, być może bez złych chęci,<br />
Sielankę ojcu zamąci.<br />
...
</p>
{% endblock %}
poems/web/images/gora.png
poems/web/images/naglowek.png
poems/web/images/pojemnik.png
poems/web/images/stopka.png
Listing 5.1. Trzy akcje: lorem, ipsum i dolor, o adresach /lorem.html, /ipsum.html oraz /dolor.html
class DefaultController extends Controller
{
/**
* @Route("/lorem.html")
* @Template()
*/
public function loremAction()
{
66 Część I ♦ Tworzenie prostych stron WWW
return array();
}
/**
* @Route("/ipsum.html")
* @Template()
*/
public function ipsumAction()
{
return array();
}
/**
* @Route("/dolor.html")
* @Template()
*/
public function dolorAction()
{
return array();
}
Oczywiście w celu usunięcia z aplikacji akcji lorem wystarczy usunąć metodę lorem
´Action() oraz plik lorem.html.twig.
W Symfony 1 po zdefiniowaniu modułu lorem oraz akcji ipsum strona akcji była dostępna
pod adresem:
.../web/lorem/ipsum
W Symfony 2 akcja nie ma żadnego domyślnego adresu. Jeśli definiując akcję amet,
pominiesz regułę konfiguracyjną @Route():
/**
* @Template()
*/
public function ametAction()
{
return array();
}
to akcja taka nie będzie dostępna pod żadnym adresem URL. Będzie ona wów-
czas przypominała metodę definiowaną w Symfony 1 w komponentach.
Rozdział 5. ♦ Hiperłącza i struktura aplikacji 67
W celu usunięcia kontrolera należy usunąć plik LoremController.php oraz folder [pakiet]/
Resources/views/Lorem/.
Adnotacja taka definiuje dwukierunkową translację adresów URL. Żądanie HTTP doty-
czące zasobu /lorem.html spowoduje wykonanie w aplikacji akcji loremAction() ozna-
czonej adnotacją:
@Route("/lorem.html)
Nazwa reguły translacji, czyli wartość parametru name, może być dowolnym unikalnym
ciągiem znaków.
W najprostszym przypadku parametrem funkcji path() jest nazwa reguły podana w ad-
notacji @Route().
Rozdział 5. ♦ Hiperłącza i struktura aplikacji 69
nazwą jest ciąg _homepage. W roli nazwy możemy użyć dowolnego innego napisu, np.:
strona_domowa:
pattern: /
defaults: { _controller: MyValleyBundle:Default:index }
W aplikacji wykonaj jeden pakiet, jeden kontroler oraz trzy akcje o nazwach dogoscia,
naswojeksiegi i ozywocieludzkim. Dane potrzebne do wykonania aplikacji znajdziesz
w pliku 05-01-start.zip.
ROZWIĄZANIE
Krok 1. Utwórz nowy projekt
W folderze przeznaczonym na aplikacje WWW utwórz folder fraszki/ i wypakuj do niego
zawartość archiwum Symfony_Standard_Vendors_2.0.X.zip. Z projektu usuń pakiet demo,
który jest zawarty w folderze src/Acme/.
/**
* @Route("/na-swoje-ksiegi.html", name="url_naswojeksiegi")
* @Template()
*/
public function naswojeksiegiAction()
{
return array();
}
/**
* @Route("/o-zywocie-ludzkim.html", name="url_ozywocieludzkim")
* @Template()
*/
public function ozywocieludzkimAction()
{
return array();
}
W ten sposób w aplikacji występuje jeden pakiet, My/FraszkaBundle, a w nim jeden kon-
troler, DefaultController, który zawiera trzy akcje: dogosciaAction(), naswojeksiegi
´Action() oraz ozywocieludzkimAction().
<div id="pojemnik">
<div id="naglowek">
<h1>Jan Kochanowski</h1>
<h2>Fraszki</h2>
<ol id="menuglowne">
<li><a href="{{ path('url_dogoscia') }}">Do gościa</a></li>
<li><a href="{{ path('url_naswojeksiegi') }}">Na swoje księgi</a></li>
<li><a href="{{ path('url_ozywocieludzkim') }}">O żywocie...</a></li>
</ol>
</div>
<div id="tresc">
{% block content %}
{% endblock %}
</div>
<div id="stopka">
©2011 Włodzimierz Gajda
</div>
</div>
</body>
</html>
{% block content %}
<h3>Do gościa</h3>
<p>
Jesli darmo masz te książki,<br />
A spełna w wacku pieniążki,<br />
Chwalę twą rzecz, gościu-bracie,<br />
Bo nie przydziesz ku utracie;<br />
Ale jesliś dał co z taszki,<br />
Nie kupiłeś, jedno fraszki.<br />
</p>
{% endblock %}
Adres zdefiniowany regułą o nazwie _welcome nie jest użyty w żadnym widoku. Jeśli ze-
chcesz dodać w witrynie odwołanie do strony głównej, użyj instrukcji:
<li><a href="{{ path('_welcome') }}">Strona główna</a></li>
Lista folderów oraz plików, które należy utworzyć bądź zmodyfikować podczas pracy
nad przykładem 5.1, jest przedstawiona na rysunku 5.2.
W aplikacji wykonaj jeden pakiet i trzy kontrolery. W każdym kontrolerze powinna wy-
stąpić jedna akcja o nazwie index. Dane potrzebne do wykonania aplikacji znajdziesz
w pliku 05-02-start.zip.
Rozdział 5. ♦ Hiperłącza i struktura aplikacji 73
Rysunek 5.2.
Pliki i foldery,
które należy utworzyć
lub zmodyfikować
podczas wykonywania
przykładu 5.1
74 Część I ♦ Tworzenie prostych stron WWW
ROZWIĄZANIE
Krok 1. Utwórz nowy projekt
W folderze przeznaczonym na aplikacje WWW utwórz folder zabytki/ i wypakuj do niego
zawartość archiwum Symfony_Standard_Vendors_2.0.X.zip. Z projektu usuń pakiet demo,
który znajduje się w folderze src/Acme/.
namespace My\ZabytekBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
namespace My\ZabytekBundle\Controller;
Rozdział 5. ♦ Hiperłącza i struktura aplikacji 75
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
<div id="pojemnik">
<h1>Zabytki Lublina</h1>
<div id="menu">
<a href="{{ path('url_zamek') }}">Zamek Królewski</a>
<a href="{{ path('url_brama') }}">Brama Krakowska</a>
<a href="{{ path('url_wieza') }}">Wieża Trynitarska</a>
</div>
{% block content %}
{% endblock %}
</div>
</body>
</html>
W tym przykładzie także występuje jeden plik layout.html.twig. Plik ten będzie użyty do
dekoracji wszystkich trzech akcji:
{% extends "MyZabytekBundle::layout.html.twig" %}
76 Część I ♦ Tworzenie prostych stron WWW
{% extends "MyZabytekBundle::layout.html.twig" %}
{% block content %}
<div class="element">
<div class="foto">
<img src="{{ asset('images/brama-krakowska.jpg') }}" alt="Brama Krakowska" />
</div>
<div class="podpis">
<h2>Brama Krakowska</h2>
<p>
Przy placu Władysława Łokietka wznosi się Brama Krakowska.
W średniowieczu zaczynała się stąd droga do Krakowa.
Brama Krakowska została wybudowana na polecenie Kazimierza Wielkiego.
Dolna część bramy jest najstarsza, pochodzi z czasów średniowiecza.
W XV wieku brama otrzymała nadbudowę.
</p>
</div>
<br />
</div>
{% endblock %}
_welcome:
pattern: /
defaults: { _controller: MyZabytekBundle:Brama:index }
Lista folderów oraz plików, które należy utworzyć bądź zmodyfikować podczas pracy
nad przykładem 5.2, jest przedstawiona na rysunku 5.4.
Rysunek 5.4.
Pliki i foldery,
które należy utworzyć
lub zmodyfikować
podczas wykonywania
przykładu 5.2
ROZWIĄZANIE
Krok 1. Utwórz nowy projekt
W folderze przeznaczonym na aplikacje WWW utwórz folder piosenki/ i wypakuj do niego
zawartość archiwum Symfony_Standard_Vendors_2.0.X.zip. Z projektu usuń pakiet demo,
który jest zawarty w folderze src/Acme/.
1
Oczywiście komendę generate:bundle należy wydać trzykrotnie.
Rozdział 5. ♦ Hiperłącza i struktura aplikacji 79
namespace My\LoremBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
<div id="pojemnik">
<h1 id="naglowek">Piosenki dla dzieci<span> </span></h1>
<ul id="menuglowne">
<li><a href="{{ path('_misie') }}">Jadą, jadą misie</a></li>
<li><a href="{{ path('_kolko') }}">Kółko graniaste</a></li>
<li><a href="{{ path('_ojciec') }}">Ojciec i syn</a></li>
80 Część I ♦ Tworzenie prostych stron WWW
</ul>
<div id="tresc">
{% block content %}
{% endblock %}
</div>
</div>
</body>
</html>
Także i w tym przykładzie, pomimo tego, że utworzyliśmy trzy pakiety, wykorzystamy je-
den plik layout.html.twig. Dekorację widoków we wszystkich trzech pakietach uru-
chamiamy instrukcją:
{% extends "MyLoremBundle::layout.html.twig" %}
Jeśli szablon współdzielony przez kilka pakietów zechcesz umieścić poza pakietami,
użyj folderu app/Resources/views/. Widoki zawarte w tym folderze możesz wykorzystać
do dekoracji, podając pustą nazwę pakietu oraz pustą nazwę kontrolera:
{% extends "::layout.html.twig" %}
{% block content %}
<h2>Jadą, jadą misie</h2>
<p>
Jadą, jadą misie,<br />
Tra la, tra la la,<br />
...
</p>
{% endblock %}
Lista folderów oraz plików, które należy utworzyć bądź zmodyfikować podczas pracy nad
przykładem 5.3, jest przedstawiona na rysunku 5.6.
82 Część I ♦ Tworzenie prostych stron WWW
Rysunek 5.6.
Pliki i foldery,
które należy utworzyć
lub zmodyfikować
podczas wykonywania
przykładu 5.3
Rozdział 6.
Błędy 404
Kiedy wędrujemy po Internecie, niekiedy zdarzy nam się odwiedzić nieistniejący adres
URL. W takim przypadku aplikacja obsługująca daną domenę powinna zareagować,
wyświetlając stronę WWW zawierającą skrótową informację o błędzie. Ponieważ
odpowiedź serwera WWW jest oznaczona kodem 404, błędy takie są powszechnie
nazywane błędami 404.
Analiza odpowiedzi HTTP przy użyciu wtyczki Live HTTP Headers jest przedsta-
wiona na rysunku 6.2.
Rysunek 6.2. Analiza odpowiedzi HTTP przy użyciu wtyczki Live HTTP Headers
Znajdziemy tam między innymi plik error.html.twig, który jest wykorzystywany podczas
wyświetlania domyślnej strony błędu. Jeśli w aplikacji wykonanej przy użyciu Symfony
2 spróbujemy odwiedzić nieistniejący adres:
.../web/lorem/ipsum.html
1
Wtyczka Live HTTP Headers dla przeglądarki Firefox jest dostępna na stronie
http://livehttpheaders.mozdev.org.
Rozdział 6. ♦ Błędy 404 85
Możemy nadpisywać domyślne szablony błędów. W tym celu należy utworzyć folder
app/Resources/TwigBundle/views/Exception/ i umieścić w nim nadpisywane widoki,
np. error.html.twig. W takim przypadku po odwiedzeniu błędnego adresu ujrzymy stronę
wygenerowaną na podstawie widoku app/Resources/TwigBundle/views/Exception/error.
html.twig.
ROZWIĄZANIE
Krok 1. Utwórz nowy projekt
W folderze przeznaczonym na aplikacje WWW utwórz folder gady/ i wypakuj do niego
zawartość archiwum Symfony_Standard_Vendors_2.0.X.zip. Z projektu usuń pakiet demo,
który jest zawarty w folderze src/Acme/.
/**
* @Route("/zaskroniec.html", name="_animals_zaskroniec")
* @Template()
*/
public function zaskroniecAction()
{
return array();
}
/**
Rozdział 6. ♦ Błędy 404 87
* @Route("/zmija.html", name="_animals_zmija")
* @Template()
*/
public function zmijaAction()
{
return array();
}
/**
* @Route("/zolw.html", name="_animals_zolw")
* @Template()
*/
public function zolwAction()
{
return array();
}
}
<div id="pojemnik">
<h1 id="logo">Gady w Polsce</h1>
<ul id="menu">
<li id="o1"><a href="{{ path('_animals_jaszczurka') }}">jaszczurka</a></li>
<li id="o2"><a href="{{ path('_animals_zaskroniec') }}">zaskroniec</a></li>
<li id="o3"><a href="{{ path('_animals_zmija') }}">żmija </a></li>
<li id="o4"><a href="{{ path('_animals_zolw') }}">żółw</a></li>
</ul>
<div id="tresc">
{% block content %}
{% endblock %}
</div>
88 Część I ♦ Tworzenie prostych stron WWW
</div>
</body>
</html>
{% block content %}
<h2>Jaszczurka zwinka</h2>
<h3>Lacerta agilis</h3>
<p>
<img src="{{ asset('images/jaszczurka.jpg') }}" alt="Jaszczurka zwinka" />
</p>
{% endblock %}
Ujrzysz stronę przedstawioną na rysunku 6.4. Zawiera ona komunikat widoczny na li-
stingu 6.5.
2
Aby wyczyścić pamięć podręczną, możesz użyć polecenia php app/console cache:clear lub usunąć
zawartość folderu app/cache/.
90 Część I ♦ Tworzenie prostych stron WWW
Plik z listingu 6.6 jest wykorzystywany wówczas, gdy w aplikacji wystąpi błąd unie-
możliwiający poprawne działanie. Dlatego kod widoku error500.html.twig nie może wy-
korzystywać dekoracji. Jest to kompletny, statyczny dokument HTML.
W celu sprawdzenia wyglądu strony błędu 500 najpierw wyczyść pamięć podręczną pro-
jektu (np. usuwając zawartość folderu app/cache/), a następnie w pliku AppKernel.php
zakomentuj wiersz, w którym dołączany jest pakiet AnimalsBundle:
//new My\AnimalsBundle\MyAnimalsBundle(),
Jeśli teraz spróbujesz odwiedzić dowolny adres w aplikacji, ujrzysz stronę generowaną
na podstawie szablonu z listingu 6.6.
Nadpisywanie widoków
dowolnych pakietów
Jeśli w aplikacji utworzysz pakiet My/LoremBundle, a w nim kontroler IpsumController
oraz akcję dolorAction(), wówczas widok akcji dolor będzie zapisany w pliku:
[projekt]/src/My/LoremBundle/Resources/views/Ipsum/dolor.html.twig
W ten sposób możesz nadpisać domyślne widoki akcji dowolnego pakietu zawartego
w aplikacji.
92 Część I ♦ Tworzenie prostych stron WWW
Programowe generowanie
błędów 404 oraz 500
Błędy 404 oraz 500 możemy generować programowo. W celu wygenerowania błędu 404
umieść w kodzie akcji wywołanie:
throw $this->createNotFoundException('Komunikat...');
Omawiany przykład przygotujemy w trzech wersjach, które będą różniły się drobnymi
szczegółami:
http://gady.domena.pl.localhost wersja lokalna z własną domeną
http://gady.domena.pl publikacja na serwerze NetArt przy użyciu FTP
http://gady.domena.pl publikacja na serwerze Light Hosting przy użyciu
SSH oraz rsync
Wiele osób posługuje się adresami, w których wyraz localhost jest zapisywany w skrócie
jako lh, np.:
http://gady.domena.pl.lh
ROZWIĄZANIE
Krok 1. Skonfiguruj wirtualną domenę
Przyjmijmy, że przykład 6.1. został wykonany w folderze C:\xampp\htdocs\gady. W pliku
konfiguracyjnym serwera Apache, np. C:\xampp\apache\conf\httpd.conf, wprowadź
reguły przedstawione na listingu 7.1.
127.0.0.1 gady.domena.pl.localhost
Oczywiście domenę:
gady.domena.pl.localhost
ROZWIĄZANIE
Krok 1. Zablokuj dostęp do plików
Na serwerach firmy NetArt cały folder dostępny za pomocą usługi FTP jest dostępny
także przy użyciu protokołu HTTP. Oznacza to, że wszystkie pliki tworzące projekt mo-
żesz teraz odwiedzić za pomocą przeglądarki. Należy koniecznie uniemożliwić przeglądanie
wszystkich folderów i plików projektu z wyjątkiem folderu gady/web/. Utwórz plik
.htaccess o zawartości takiej jak na listingu 7.3 i umieść go w folderze gady/. W ten
sposób zablokujesz dostęp do wszystkich plików aplikacji.
/home/domena/ftp/gady/app/AppKernel.php
/home/domena/ftp/gady/web/app.php
...
ROZWIĄZANIE
Krok 1. Zainstaluj oprogramowanie rsync
Narzędzie rsync służy do synchronizacji plików i folderów pomiędzy dwoma kom-
puterami1. Możemy je wykorzystać m.in. do przekopiowania całej aplikacji z komputera,
1
Por. http://pl.wikipedia.org/wiki/Rsync.
98 Część I ♦ Tworzenie prostych stron WWW
[Date]
date.timezone = Europe/Berlin
zatem:
Właściciel będzie miał dostęp w trybie rwx (tj. r — read, w — write,
x — execute).
Grupa będzie miała dostęp w trybie r-x (tj. r — read, x — execute).
Pozostali użytkownicy będą mieli dostęp w trybie r-x (tj. r — read,
x — execute).
Parametr:
--exclude-from=rsync_exclude.txt
ustala, że plik rsync_exclude.txt będzie potraktowany jako lista wyjątków, tj. plików i fol-
derów, które należy pominąć. W pliku tym wprowadź zawartość taką jak na listingu 7.8.
Podczas przesyłania plików na serwer pomijany będzie folder app/cache/ oraz pliki
app_dev.php i config.php.
W poleceniu z listingu 7.7 parametr –p40022 ustala numer portu 40022, zaś twój
´serwer@twojserwer.lh.pl zawiera dane do zalogowania przy użyciu SSH. Folder, do
którego kopiowane będą pliki, podajemy po dwukropku kończącym adres serwera. Jeśli
masz konto lorem@ipsum.example.net w systemie, w którym komunikacja za pomocą
protokołu SSH odbywa się na porcie 1234, i chcesz kopiowane pliki umieścić w folderze
dolor/sit/amet (względem folderu domowego), to w poleceniu z listingu 7.7 wprowadź
następujące modyfikacje:
rsync
--chmod=a+rwx,g-w,o-w -azC –force –delete
–progress --exclude-from=rsync_exclude.txt
-e "ssh –p1234" ./ lorem@ipsum.example.net:dolor/sit/amet
2
Komendę z listingu 7.7 należy zapisać w postaci jednego długiego wiersza. Znaki złamania wiersza zostały
dodane na listingu w celu zwiększenia czytelności. Pomiędzy systemami Windows, Mac oraz Linux
występują drobne różnice w parametrach. Podana wersja działa poprawnie w systemie Windows.
100 Część I ♦ Tworzenie prostych stron WWW
Dystrybucje Symfony 2
Naukę Symfony 2 rozpoczęliśmy od poznania dwóch dystrybucji:
with vendors,
without vendors.
Dystrybucji without vendors będziemy używali w części trzeciej, gdy zechcemy doinsta-
lować dodatkowe pakiety.
Drugi skrypt testuje poprawność instalacji PHP w wierszu poleceń. W celu uruchomienia
skryptu należy w wierszu poleceń wydać komendę:
php -f app/check.php
Gdy konfiguracja komputera jest poprawna, sprawdzamy wygląd strony aplikacji demo:
http://localhost/Symfony/web/app_dev.php
Pierwszy samodzielnie
wykonany projekt
Wykonanie pierwszego projektu rozpoczynamy od usunięcia z dystrybucji Symfony 2
aplikacji demo. Jest to konieczne, gdyż aplikacja demo przysłania adresy w środowi-
sku dev.
Zewnętrzne zasoby
W rozdziale trzecim zajęliśmy się dołączaniem do projektu zasobów CSS, JS oraz plików
graficznych. Zasoby te należy umieszczać w folderze [projekt]/web/. W kodzie HTML/
Twig adresy do zasobów z folderu web/ generujemy funkcją pomocniczą asset(), np.:
<link rel="stylesheet" href="{{ asset('css/style.css') }}" />
Pakiety Symfony 2 mogą wymagać własnych stylów CSS, plików graficznych lub skryptów
JS. Przykładem takiego pakietu jest panel administracyjny, który zawiera różne ikony
i własne style CSS. Zasoby pakietu umieszczamy w folderach widocznych na rysunku 3.1.
Foldery te możemy automatycznie utworzyć, jeśli po wydaniu komendy generate:
´bundle na pytanie:
Do you want to generate the whole directory structure [no]?
odpowiemy twierdząco:
yes
Szablon witryny
W rozdziale czwartym omówiliśmy dekorację widoku akcji szablonem layout.html.twig.
Dzięki temu wszystkie strony zawarte w witrynie będą miały wspólną szatę graficzną.
Aby włączyć dekorację widoku akcji index, w pliku index.html.twig dodajemy instrukcję
extends, która ustala nazwę szablonu użytego do dekoracji, oraz blok content:
104 Część I ♦ Tworzenie prostych stron WWW
{% block content %}
<h2>Włodzimierz Gajda</h2>
<h3>Dwa kabele</h3>
...
{% endblock %}
Podstawy routingu
Rozdział piąty wykorzystaliśmy do nauki tworzenia i usuwania:
pakietów,
kontrolerów,
akcji
oraz widoków.
Każda adnotacja @Route() definiuje jedną regułę routingu adresów URL. W celu wy-
generowania w kodzie HTML podanego adresu wywołujemy funkcję pomocniczą,
przekazując do niej nazwę reguły routingu:
<a href="{{ path('adres_lorem') }}">...</a>
Błędy 404
Szósty rozdział opisuje obsługę błędów 404. Po lekturze rozdziału dowiedzieliśmy się
bardzo ważnej rzeczy: wszystkie widoki zawarte w pakietach z folderu vendor/ możemy
nadpisywać. W celu dostosowania wyglądu stron błędów 404 oraz 500 nadpisaliśmy
widoki zawarte w folderze pakietu TwigBundle:
Rozdział 8. ♦ Podsumowanie części I 105
vendor/symfony/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/
Dowiedzieliśmy się także, w jaki sposób programowo generować błędy. Służą do tego
instrukcje:
throw $this->createNotFoundException('Komunikat...');
throw new \Exception('Komunikat...');
Publikowanie projektu
Projekt wykonany w Symfony 2 jest kompletny. W celu uruchomienia go na serwerze
wystarczy przekopiować pliki. Możesz do tego użyć programu FTP.
W tym celu:
Modyfikujemy plik zawierający nazwy hostów: hosts.
Modyfikujemy plik konfiguracyjny serwera Apache: http.conf.
1
Powyższy przykład pochodzi z wtyczki LightBox biblioteki jQuery.
106 Część I ♦ Tworzenie prostych stron WWW
ROZWIĄZANIE
Krok 1. Pobierz najnowszą wersję Symfony 2
Odwiedź stronę:
http://symfony.com/download
i pobierz najnowszą wersję pliku Symfony Standard, np. Symfony_Standard_Vendors_
2.0.X.zip. Pobrane archiwum rozpakuj. Wypakowana zawartość jest domyślnie umieszcza-
na w folderze Symfony/.
_demo_secured:
resource: "@AcmeDemoBundle/Controller/SecuredController.php"
type: annotation
_demo:
resource: "@AcmeDemoBundle/Controller/DemoController.php"
type: annotation
prefix: /demo
Rozdział 9.
Twig
Domyślnym językiem przetwarzania widoków w Symfony 2 jest Twig. Pełna doku-
mentacja biblioteki Twig jest dostępna w witrynie http://twig.sensiolabs.org.
Jeśli w projekcie wystąpi pakiet My/LoremBundle oraz kontroler Ipsum, plik kontrolera
otrzyma wówczas nazwę:
src/My/LoremBundle/Controller/IpsumController.php
Dla kontrolera Ipsum z pakietu My/LoremBundle folder ten będzie się nazywał1:
src/My/LoremBundle/Resources/views/Ipsum/
1
Zwróć uwagę na brak przyrostka Controller w poniższej ścieżce.
112 Część II ♦ Widoki
Ponieważ domyślne nazwy widoków akcji powstają przez usunięcie przyrostka Action
z nazwy metody i dodanie podwójnego rozszerzenia .html.twig ., widok akcji dolorAction()
z kontrolera Ipsum będzie zatem zapisany w pliku o nazwie:
src/My/LoremBundle/Resources/views/Ipsum/dolor.html.twig
Odwołując się do szablonów Twig, będziemy posługiwali się nazwami logicznymi, a nie
nazwami plików. Nazwa logiczna widoku powstaje z nazwy pakietu, nazwy kontrolera
oraz nazwy pliku oddzielonych od siebie dwukropkami. Nazwą logiczną widoku:
src/My/LoremBundle/Resources/views/Ipsum/dolor.html.twig
jest:
MyLoremBundle:Ipsum:dolor.html.twig
Dwa pierwsze człony nazwy logicznej widoku mogą być puste. Jeśli pustym członem
jest nazwa kontrolera, widok pochodzi wówczas z folderu Resources/ danego pakietu.
Na przykład nazwa logiczna:
MyLoremBundle::sit.html.twig
wskazuje widok:
src/My/LoremBundle/Resources/views/sit.html.twig
W przypadku gdy pominiemy nazwę pakietu, nazwa logiczna będzie wskazywała widoki
z folderu app/Resources/views/. Nazwa logiczna:
::amet.html.twig
wskazuje widok
app/Resources/views/amet.html.twig
Rozdział 9. ♦ Twig 113
wskazuje widok:
app/Resources/views/Nunc/pede.html.twig
Rysunek 9.2.
Logiczne nazwy
widoków z folderu
app/Resources/views/
Nadpisywanie widoków
z folderu vendor
W rozdziale 6. opracowaliśmy własne strony błędów 404 oraz 500. W tym celu wystar-
czyło przygotować własne widoki:
app/Resources/TwigBundle/views/Exception/error404.html.twig
app/Resources/TwigBundle/views/Exception/error500.html.twig
app/Resources/TwigBundle/views/Exception/error.html.twig
zawartego w pakiecie:
https://github.com/KnpLabs/KnpPaginatorBundle
114 Część II ♦ Widoki
np.
@Template("MyLoremBundle:Ipsum:dolor.html.twig")
@Template("::base.html.twig")
Nazwa widoku akcji powstaje wówczas na podstawie nazwy metody akcji. Oba poniższe
zapisy są równoważne:
@Template()
@Template
Jeśli w projekcie występuje pakiet My/LoremBundle, a w nim kontroler Ipsum, który zawiera
metodę dolorAction(), to adnotacja:
/**
* @Template()
*/
Rozdział 9. ♦ Twig 115
Użycie reguły konfiguracyjnej @Template() nie tylko ustala nazwę widoku, który zo-
stanie użyty do wygenerowania strony, ale także uruchamia procedurę przetwarzania
widoku. Jeśli dla metody dolorAction() pominiemy regułę @Template(), to w celu
wygenerowania strony WWW musimy ręcznie wywołać metodę render(), która odpo-
wiada za przetworzenie szablonu. Kod z listingów 9.1 oraz 9.2 zapisany z pominięciem
reguły @Template() przyjmie postać taką jak na listingu 9.3.
116 Część II ♦ Widoki
ROZWIĄZANIE
Krok 1. Utwórz nowy projekt i pakiet
W folderze przeznaczonym na aplikacje WWW utwórz folder zad-09-01/ i wypakuj do
niego zawartość archiwum symfony2-customized-v1.zip. Następnie komendą:
Rozdział 9. ♦ Twig 117
/**
* @Route("/lorem.html")
* @Template("::lorem.html.twig")
*/
public function loremAction()
{
return array();
}
/**
* @Route("/ipsum.html")
* @Template(":Abc:ipsum.html.twig")
*/
public function ipsumAction()
{
return array();
}
/**
* @Route("/dolor.html")
*/
public function dolorAction()
{
return $this->render("MySprBundle::dolor.html.twig");
}
/**
* @Route("/sit.html")
*/
public function sitAction()
{
return $this->render("MySprBundle:Def:sit.html.twig");
}
}
118 Część II ♦ Widoki
odpowiada plikowi:
src/My/SprBundle/Resources/views/dolor.html.twig
Logiczna nazwa:
MySprBundle:Def:sit.html.twig
odpowiada plikowi:
src/My/SprBundle/Resources/views/Def/sit.html.twig
W każdym z nich umieść stronę WWW zawierającą jeden z wyrazów: lorem, ipsum,
dolor lub sit. Treść pliku:
app/Resources/views/lorem.html.twig
<h1>Lorem</h1>
</body>
</html>
Zwróć uwagę, że plik HTML, który nie zawiera żadnych znaczników Twig, jest poprawnym
widokiem.
Jeśli ujrzysz białe strony WWW, kod prawdopodobnie zawiera błędy. Użyj wówczas
kontrolera app_dev.php, który wyświetli informacje o błędach:
.../web/app_dev.php/lorem.html
.../web/app_dev.php/ipsum.html
.../web/app_dev.php/dolor.html
.../web/app_dev.php/sit.html
{# #}
komentarze Twig;
{{ }}
wydruk zmiennych, wyrażeń oraz wartości zwracanych przez funkcje Twig;
{% %}
instrukcje sterujące Twig.
{% block content %}
...
{% endblock %}
zostaną wydrukowane w sposób dosłowny, bez interpretacji przez Twig. Zapis tego
typu możemy wykorzystać, m.in. przygotowując stronę WWW zawierającą samouczek
opisujący szablony Twig.
ROZWIĄZANIE
Krok 1. Utwórz nowy projekt i pakiet
W folderze przeznaczonym na aplikacje WWW utwórz folder zad-09-02/ i wypakuj
do niego zawartość archiwum symfony2-customized-v1.zip. Następnie komendą:
php app/console generate:bundle
--namespace=My/SprBundle --dir=src --no-interaction
<h1>Twig - tutorial</h1>
<p>
Komentarze Twig oznaczamy znacznikami:
</p>
<pre>
{% raw %}
{# ...lorem ipsum... #}
{% endraw %}
</pre>
...
Rozdział 9. ♦ Twig 123
Jeśli zechcemy, by wynikiem akcji były dane tekstowe, nie wystarczy jednak zmiana
rozszerzenia widoku z .html.twig na .txt.twig. Należy także dostosować kod akcji tak,
by generowała ona odpowiedni nagłówek HTTP oraz by przetwarzała widok o zmie-
nionej nazwie.
Wartość:
_format=html
ustala, że:
Widokiem akcji jest plik o rozszerzeniu .html.twig.
Odpowiedź HTTP zawiera nagłówek: Content-Type: text/html.
2
Parametr ten możemy umieszczać w regułach routingu, czyli np. w plikach routing.yml oraz w adnotacjach
@Route().
124 Część II ♦ Widoki
wówczas:
Widokiem akcji będzie plik o rozszerzeniu .txt.twig.
Odpowiedź HTTP będzie zawierać nagłówek: Content-Type: text/plain.
W ten sposób możemy dowolnie modyfikować nagłówki odpowiedzi HTTP, bez względu
na rozszerzenie nazwy widoku.
3
Kod klasy znajdziesz w pliku vendor\symfony\src\Symfony\Component\HttpFoundation\Response.php.
4
Właściwość headers jest obiektem klasy ResponseHeaderBag, której kod źródłowy znajdziesz w pliku
vendor\symfony\src\Symfony\Component\HttpFoundation\ResponseHeaderBag.php.
Rozdział 9. ♦ Twig 125
ROZWIĄZANIE
Krok 1. Utwórz nowy projekt i pakiet
W folderze przeznaczonym na aplikacje WWW utwórz folder zad-09-03/ i wypakuj do
niego zawartość archiwum symfony2-customized-v1.zip. Następnie komendą:
php app/console generate:bundle
--namespace=My/SprBundle --dir=src --no-interaction
/**
* @Route("/", defaults={"_format"="txt"})
* @Template()
*/
public function indexAction()
{
return array();
}
126 Część II ♦ Widoki
/**
* @Route("/style.css", defaults={"_format"="css"})
* @Template()
*/
public function sAction()
{
return array();
}
/**
* @Route("/win.txt")
*/
public function winAction()
{
$response = $this->render('MySprBundle:Default:win.txt.twig');
$response->headers->set('Content-Type', 'text/plain;charset=windows-1250');
return $response;
}
Adnotacje akcji winAction() ustalają adres dokumentu win.txt. Nazwa widoku jest
przekazywana jako parametr do metody render():
$response = $this->render('MySprBundle:Default:win.txt.twig');
W pierwszym z nich umieść jeden akapit tekstu lorem ipsum. W drugim — dowolne
style CSS, a w trzecim — dowolny tekst w języku polskim. Plik win.txt.twig zapisz, sto-
sując kodowanie windows-1250.
Rysunek 9.3.
Analiza odpowiedzi
HTTP dla adresu
win.txt
W przypadku użycia metody render() dane przekazujemy do widoku jako jej drugi
parametr. Struktura parametru jest identyczna jak w poprzednim przypadku: jest to
tablica asocjacyjna, której klucze ustalają nazwy zmiennych. Listing 10.2 ilustruje
przekazanie do widoku akcji ipsumAction() zmiennej o nazwie name i wartości Janek.
130 Część II ♦ Widoki
W obu przypadkach skutek będzie identyczny: w widoku dostępna będzie zmienna o nazwie
name i wartości Janek. Wartość zmiennej możemy wydrukować, stosując znacznik {{ }}:
{{ name }}
Należy pamiętać, że w przypadku gdy zmienna o podanej nazwie nie istnieje, wydru-
kowany zostanie napis pusty. Zakładając, że w widoku nie występuje zmienna ipsum,
instrukcja:
xxx{{ ipsum }}yyy
Na pierwszej stronie wyświetl bieżącą datę, a na drugiej — bieżącą godzinę. Niech strona
data.html będzie wynikiem przetwarzania akcji dataAction(). Renderowanie widoku
zaimplementuj, wykorzystując adnotację @Template(). Strona godzina.html powinna
być natomiast generowana jako wynik przetwarzania akcji godzinaAction(). Rende-
rowanie widoku zaimplementuj, wywołując metodę render().
Rozdział 10. ♦ Zmienne, wyrażenia i operatory Twig 131
ROZWIĄZANIE
Krok 1. Utwórz nowy projekt i pakiet
W folderze przeznaczonym na aplikacje WWW utwórz folder zad-10-01/ i wypakuj do
niego zawartość archiwum symfony2-customized-v1.zip. Następnie komendą:
php app/console generate:bundle
--namespace=My/DateTimeBundle --dir=src --no-interaction
}
132 Część II ♦ Widoki
Zabezpieczanie zmiennych
Wydruk zmiennych przekazywanych z kontrolera do widoku stwarza niebezpieczeństwo
ataków typu Cross Site Scripting1 oraz Cross Site Request Forgery2. Dlatego drukowanie
zmiennych w widoku należy zabezpieczyć, konwertując znaki:
< > & " '
na odpowiadające im encje:
< > & " '
wówczas instrukcja:
{{ tekst }}
1
Por. http://pl.wikipedia.org/wiki/Cross-site_scripting.
2
Por. http://pl.wikipedia.org/wiki/Cross-site_request_forgery.
Rozdział 10. ♦ Zmienne, wyrażenia i operatory Twig 133
Co ciekawe, wydruk wyrażeń tekstowych, które nie zawierają zmiennych, nie podlega
automatycznym zabezpieczeniom. Instrukcja:
{{ "<h3>Uwaga! Znaczniki niezabezpieczone!</h3>" }}
Po dodaniu reguły konfiguracyjnej autoescape o wartości false zmienne nie będą automa-
tycznie zabezpieczane. W takiej sytuacji poszczególne zmienne możemy zabezpieczyć,
wykorzystując filtr escape. Jeśli zmienna tekst ma wartość:
<h2>Pożegnanie</h2>
wówczas instrukcja:
{{ tekst | escape }}
wydrukuje tekst:
<h2>Pożegnanie</h2>
Ustawienia opcji konfiguracyjnej autoescape nie będą miały w tym przypadku żadnego
wpływu na postać generowanego napisu. Filtr escape możemy zapisywać w skróconej
postaci. Obie poniższe instrukcje są równoważne:
{{ tekst | escape }}
{{ tekst | e }}
134 Część II ♦ Widoki
Wynikiem przetwarzania akcji mogą być dane w dowolnym formacie (m.in. text/html
oraz text/javascript), dlatego filtr escape przyjmuje parametr, który ustala sposób
zabezpieczania zmiennych. Domyślnie zmienne są zabezpieczane dla formatu HTML.
Jeśli widok generuje kod JavaScript, zmienną należy wówczas zabezpieczyć w następu-
jący sposób:
{{ zmienna | escape('js') }}
Zmienne zawarte wewnątrz powyższego bloku, tj. tytul oraz opis, będą podlegały
automatycznym zabezpieczeniom.
Filtr raw:
{{ zm | raw }}
Przykład 10.2.
Zabezpieczanie zmiennych
Wykorzystując Symfony 2, wykonaj aplikację, która będzie ilustrowała wszystkie możli-
wości dotyczące zabezpieczania zmiennych drukowanych w widoku.
Rozdział 10. ♦ Zmienne, wyrażenia i operatory Twig 135
ROZWIĄZANIE
Krok 1. Utwórz nowy projekt i pakiet
W folderze przeznaczonym na aplikacje WWW utwórz folder zad-10-02/ i wypakuj do
niego zawartość archiwum symfony2-customized-v1.zip. Następnie komendą:
php app/console generate:bundle
--namespace=My/HtmlBundle --dir=src --no-interaction
Jeśli kodowanie pliku DefaultController.php będzie inne niż UTF-8, umieszczenie w wi-
doku akcji powitanie instrukcji:
{{ komunikat }}
spowoduje wygenerowanie wyjątku:
The string to escape is not a valid UTF-8 string
src/My/HtmlBundle/Resources/views/Default/powitanie.html.twig
{{ komunikat }}
<hr />
{{ komunikat|raw }}
<hr />
{{ komunikat | e }}
<hr />
{{ komunikat | e('js') }}
<hr />
{% autoescape true %}
{{ komunikat }}
{% endautoescape %}
<hr />
{% autoescape true js %}
{{ komunikat }}
{% endautoescape %}
<hr />
{% autoescape false %}
{{ komunikat }}
{% endautoescape %}
<hr />
<hr />
</body>
</html>
Rozdział 10. ♦ Zmienne, wyrażenia i operatory Twig 137
Sposób działania wszystkich instrukcji z listingu 10.6 jest sumarycznie zebrany w tabeli
10.1. We wszystkich przypadkach zmienna komunikat ma wartość:
<h1>Cześć!</h1>
Napisy osadzone jako wyrażenia wewnątrz instrukcji {{ }} nie podlegają zatem au-
tomatycznym zabezpieczeniom.
wówczas najpierw tworzona jest zmienna, która zawiera połączone napisy. Następnie
zmienna ta poddana zostaje automatycznym zabezpieczeniom. W ten sposób w druko-
wanym napisie zabezpieczone zostają wszystkie wystąpienia znaków < oraz >:
---> zabezpieczone <h1>Cześć!</h1> <===
3
W szablonach Twig operator ~ służy do konkatenacji napisów.
138 Część II ♦ Widoki
lub
{{ tablica.indeks }}
oraz:
{{ dane.0 }}
{{ dane.1 }}
{{ dane.ipsum }}
lub:
{{ dane.student1.imie }}
{{ dane.student1.nazwisko }}
Wyrażenia Twig
Podobnie jak w języku PHP, w szablonach Twig w miejscach, w których możemy użyć
zmiennej, może wystąpić wyrażenie. Znacznik {{ }}, którego używamy do wydruku
zmiennych, może posłużyć do wydruku wyrażenia. Instrukcja:
{{ 3 * 8 - 4 }}
wydrukuje wartość wyrażenia 3 · 8 – 4, czyli 20. Jeśli przekażemy z akcji do widoku trzy
zmienne — a, b i c:
public function ipsumAction()
{
return array('a' => 2, 'b' => 3, 'c' => 4);
}
Liczby w wyrażeniach Twig zapisujemy, wykorzystując cyfry dziesiętne oraz znak kropki:
1234
3.1415
4
30 = 2 ⋅ 3 ⋅ 4 + 6
Rozdział 10. ♦ Zmienne, wyrażenia i operatory Twig 141
Operatory Twig
Operatory Twig możemy podzielić na następujące grupy:
operatory arytmetyczne,
operatory porównania,
operatory logiczne,
operatory dotyczące napisów,
operatory specjalne.
5
Opis instrukcji if znajdziesz w rozdziale 11.
Rozdział 10. ♦ Zmienne, wyrażenia i operatory Twig 143
Dostępne testy, które mogą być wykonywane operatorem is, są zawarte w tabeli 10.6.
Dzięki temu, że operatory or oraz and mają najniższy priorytet, wyrażenia logiczne może-
my zapisywać bez nawiasów otaczających poszczególne człony. Oba poniższe wyra-
żenia są równoważne:
{{ (a < b) and (c > d) }}
{{ a < b and c > d }}
144 Część II ♦ Widoki
Definiowanie zmiennych
wewnątrz widoku
Zmienne widoku możemy przekazywać z akcji lub definiować znacznikiem {% set %}.
Instrukcja:
{% set ile = 2 %}
tworzy nową zmienną o nazwie ile i o wartości 2. Zmienna ta może być wydrukowana
w sposób bezpośredni:
{{ ile }}
Instrukcją {% set %} możemy definiować zmienne skalarne typu integer, float, string
i boolean oraz tablice. Do definiowania tablic wykorzystujemy nawiasy kwadratowe
i klamrowe. Instrukcja:
{% set t = [11, 22] %}
Instrukcja:
{% set q = {'lorem': 'ipsum', 'dolor': 'sit'} %}
6
15 = 2 · 10 – 5
Rozdział 10. ♦ Zmienne, wyrażenia i operatory Twig 145
Instrukcja:
{% set t[0] = 999 %}
generuje wyjątek:
Unexpected token "punctuation" of value "[" ("end of statement block" expected) in
´{} at line X
utworzy zmienną o nazwie fragment, która będzie ciągiem znaków zawierającym wyge-
nerowany kod HTML:
<ul>
...
</ul>
Zmienne globalne
Oprócz zmiennych przekazanych z kontrolera oraz zmiennych zdefiniowanych znaczni-
kami {% set %} w każdym widoku występują trzy zmienne globalne:
_self
_context
_charset
Instrukcja for
Znacznik for służy do iteracyjnego przetwarzania tablic. Ogólna składnia znacznika for
jest przedstawiona na listingu 11.1.
Inaczej niż w języku PHP, instrukcja for może zawierać blok else, który zostanie
wykonany w przypadku, gdy tablica będzie pusta. Składnia znacznika for zawierającego
blok else jest przedstawiona na listingu 11.2.
Kod z listingu 11.2 możemy zapisać w bardziej tradycyjnej postaci, wykorzystując opi-
sany w kolejnym punkcie znacznik if. Przykład taki jest przedstawiony na listingu 11.3.
Listing 11.3. Tradycyjny zapis kodu z listingu 11.2 przy użyciu znaczników for oraz if
{% if tablica|length > 0 %}
{% for element in tablica %}
148 Część II ♦ Widoki
{{ element }}
{% endfor %}
{% else %}
brak elementów
{% endif %}
Kod z listingu 11.4 możemy równoważnie zapisać w sposób tradycyjny, tak jak na listin-
gu 11.5.
Listing 11.5. Kod z listingu 11.4 zapisany przy użyciu znaczników for oraz if
{% for element in tablica %}
{% if warunek %}
{{ element }}
{% endif %}
{% endfor %}
Wewnątrz instrukcji for dostępna jest specjalna zmienna loop, która ułatwia dostęp do
indeksów iteracji. Zestawienie składowych zmiennej loop jest zawarte w tabeli 11.1.
drukuje imiona oddzielone przecinkami, przy czym po ostatnim wyrazie nie występuje
nigdy przecinek.
Znacznik for umożliwia także przetwarzanie tablic asocjacyjnych, zapewniając dostęp za-
równo do kluczy, jak i do wartości. Ogólna postać rozszerzonego znacznika for jest przed-
stawiona na listingu 11.6.
Listing 11.6. Znacznik for zapewniający dostęp zarówno do wartości, jak i do kluczy tablicy asocjacyjnej
{% for k, v in tablica %}
{{ k }}
{{ v }}
{% endfor %}
Warto pamiętać, że znacznik for może być wykorzystany nie tylko do przetwarzania
tablic przekazanych z akcji do widoku, ale także do przetwarzania tablic utworzonych
w widoku operatorami specjalnymi .., [] oraz {}. Użycie znacznika for w połączeniu
z operatorami specjalnymi .., [] oraz {} ilustruje listing 11.7.
Listing 11.7. Użycie znacznika for w połączeniu z operatorami specjalnymi .., [] oraz {}
{% for numer in 1..15 %}
{{ numer }}
{% endfor %}
150 Część II ♦ Widoki
Pętla {% for %} nie może być użyta do iteracyjnego przetwarzania napisów litera
po literze.
Instrukcja if
Znacznik if przyjmuje jedną z postaci przedstawionych na listingu 11.8.
{% if warunek %}
...
{% else %}
...
{% endif %}
{% if warunek %}
...
{% elseif warunek %}
...
{% elseif warunek %}
...
{% else %}
...
{% endif %}
Na przykład w celu sprawdzenia, czy zmienna jest nieparzysta, należy użyć operatora is
oraz testu odd:
{% if zmienna is odd %}
...
{% endif %}
Do pokolorowania co piątego wiersza tabeli HTML możemy użyć operatora is oraz testu
divisibleby w odniesieniu do indeksu pętli zawartego w zmiennej loop.index:
{% if loop.index is divisibleby(5) %}
...
{% endif %}
Znaczniki:
{% if ... %}
...
{% endif %}
możemy także zapisać w skróconej postaci, wykorzystując operator specjalny ?:. Zapis
skrótowy instrukcji if przyjmuje w Twig postać:
{{ warunek ? wyrazenie_wykonywane_gdy_prawda : wyrazenie_wykonywane_gdy_falsz }}
na przykład:
{{ zmienna is even ? 'zmienna jest parzysta' : 'zmienna jest nieparzysta' }}
Wykonaj aplikację, która będzie prezentowała na stronie głównej tabelę HTML za-
wierającą dane z pliku korona-ziemi.txt.
ROZWIĄZANIE
Krok 1. Utwórz nowy projekt i pakiet
W folderze przeznaczonym na aplikacje WWW utwórz folder zad-11-01/ i wypakuj do
niego zawartość archiwum symfony2-customized-v1.zip. Następnie komendą:
php app/console generate:bundle
--namespace=My/MountainsBundle --dir=src --no-interaction
Następnie w folderze projektu utwórz folder data/ i umieść w nim plik korona-ziemi.txt.
<table>
<tr>
<th>lp.</th>
<th>Kontynent</th>
<th>Szczyt</th>
154 Część II ♦ Widoki
</body>
</html>
Rysunek 11.1.
Korona Ziemi — witryna
z przykładu 11.1
Rozdział 11. ♦ Instrukcje sterujące for oraz if 155
Przykład 11.2.
Dzieła literatury światowej
Dany jest plik tekstowy dziela_literatury_swiatowej.txt, którego fragmenty przedstawio-
no na listingu 11.12.
Wykonaj aplikację, która będzie prezentowała na stronie głównej tabelę HTML za-
wierającą dane z pliku tekstowego. W tabeli HTML co dziesiąty wiersz wyróżnij kolo-
rem czarnym, a co drugi — kolorem szarym.
ROZWIĄZANIE
Krok 1. Utwórz nowy projekt i pakiet
W folderze przeznaczonym na aplikacje WWW utwórz folder zad-11-02/ i wypakuj do
niego zawartość archiwum symfony2-customized-v1.zip. Następnie komendą:
php app/console generate:bundle
--namespace=My/LiteratureBundle --dir=src --no-interaction
Następnie w folderze projektu utwórz folder data/ i umieść w nim plik dziela_literatury_
swiatowej.txt.
Pętla foreach przetwarza plik tekstowy w dwuwymiarową tablicę $t, która na zakończe-
nie jest przekazywana do widoku akcji jako zmienna o nazwie literatura.
Znacznik oznaczony klasą w10 jest generowany, jeśli numer obrotu pętli jest podzielny
przez 10. W przeciwnym razie, jeśli numer obrotu pętli jest podzielny przez 2, generuje-
my znacznik oznaczony klasą w2. Pozostałe wiersze (tj. wiersze o numerach niepodziel-
nych przez 10 ani przez 2) są oznaczone znacznikiem tr pozbawionym klasy. Zarys
pliku index.html.twig jest przedstawiony na listingu 11.14.
</tr>
{% for utwor in literatura %}
{% if loop.index is divisibleby(10) %}
<tr class="w10">
{% elseif loop.index is divisibleby(2) %}
<tr class="w2">
{% else %}
<tr>
{% endif %}
</body>
</html>
ROZWIĄZANIE
Krok 1. Utwórz nowy projekt i pakiet
W folderze przeznaczonym na aplikacje WWW utwórz folder zad-11-03/ i wypakuj
do niego zawartość archiwum symfony2-customized-v1.zip. Następnie komendą:
php app/console generate:bundle
--namespace=My/MultiplicationTableBundle --dir=src --no-interaction
Następnie generujemy pierwszy wiersz tabeli HTML, który zawiera numery kolumn:
<tr>
<th></th>
{% for i in 1..liczba_kolumn %}
<th>{{ i }}</th>
{% endfor %}
</tr>
<h1>Tabliczka mnożenia</h1>
{% set liczba_wierszy = 12 %}
{% set liczba_kolumn = 8 %}
<table>
<tr>
<th></th>
{% for i in 1..liczba_kolumn %}
<th>{{ i }}</th>
160 Część II ♦ Widoki
{% endfor %}
</tr>
{% for i in 1..liczba_wierszy %}
<tr>
<th>{{ i }}</th>
{% for j in 1..liczba_kolumn %}
<td>{{ i * j }}</td>
{% endfor %}
</tr>
{% endfor %}
</table>
</body>
</html>
Rysunek 11.3.
Tabliczka mnożenia
— witryna
z przykładu 11.3
Rozdział 11. ♦ Instrukcje sterujące for oraz if 161
ROZWIĄZANIE
Krok 1. Utwórz nowy projekt i pakiet
W folderze przeznaczonym na aplikacje WWW utwórz folder zad-11-04/ i wypakuj
do niego zawartość archiwum symfony2-customized-v1.zip. Następnie komendą:
php app/console generate:bundle
--namespace=My/PowersBundle --dir=src --no-interaction
Następnie generujemy pierwszy wiersz tabeli HTML, który zawiera symbole a1, a2, a3 itd.:
<tr>
<th>a</th>
{% for i in 1..wykladnik %}
<th>a<sup>{{ i }}</sup></th>
{% endfor %}
</tr>
{% endfor %}
</tr>
{% endfor %}
<h1>Potęgi</h1>
<h1>Potęgi</h1>
{% set podstawa = 10 %}
{% set wykladnik = 5 %}
<table>
<tr>
<th>a</th>
{% for i in 1..wykladnik %}
<th>a<sup>{{ i }}</sup></th>
{% endfor %}
</tr>
{% for i in 2..podstawa %}
<tr>
<th>{{ i }}</th>
{% for j in 1..wykladnik %}
<td>{{ i ** j }}</td>
{% endfor %}
</tr>
{% endfor %}
</table>
</body>
</html>
Rozdział 11. ♦ Instrukcje sterujące for oraz if 163
Rysunek 11.4.
Tabela potęg
— witryna
z przykładu 11.4
Przykład 11.5.
Bezpieczna paleta kolorów
Wykonaj aplikację, która będzie prezentowała na stronie głównej tabelę z bezpieczną
paletą kolorów1. Kody szesnastkowe kolorów bezpiecznych są kombinacjami wartości:
00
33
66
99
CC
FF
1
Por. http://pl.wikipedia.org/wiki/Kolory_w_Internecie.
164 Część II ♦ Widoki
ROZWIĄZANIE
Krok 1. Utwórz nowy projekt i pakiet
W folderze przeznaczonym na aplikacje WWW utwórz folder zad-11-05/ i wypakuj do
niego zawartość archiwum symfony2-customized-v1.zip. Następnie komendą:
php app/console generate:bundle
--namespace=My/ColorsBundle --dir=src --no-interaction
Zmienna kody zawiera wszystkie dopuszczone kody bajtów, zmienna kolumny ustala
liczbę kolumn tabeli HTML, zaś zmienna numer zawiera numer kolejnego drukowanego
koloru. Wykorzystując zmienne numer oraz kolumny, wygenerujemy tabelę o zadanej
liczbie kolumn.
Bezpieczne kolory RGB powstają jako kombinacje trzech wartości z tablicy kody. Dlatego
całe przetwarzanie jest zawarte w potrójnej pętli for. Ponieważ indeksami w tablicy kody
są liczby od 0 do 5, pętle for przyjmują postać:
<table>
{% for i in 0..5 %}
{% for j in 0..5 %}
{% for k in 0..5 %}
...
{% endfor %}
{% endfor %}
{% endfor %}
</table>
...
FFCC00 FFCC33 FFCC66 FFCC99 FFCCCC FFCCFF
FFFF00 FFFF33 FFFF66 FFFF99 FFFFCC FFFFFF
<table>
{% for i in 0..5 %}
{% for j in 0..5 %}
{% for k in 0..5 %}
{% set kolorek = kody[i] ~ kody[j] ~ kody[k] %}
{% if (numer % kolumny) == 0 %}
<tr>
{% endif %}
</body>
</html>
Rysunek 11.5.
Bezpieczna paleta
kolorów — witryna
z przykładu 11.5
168 Część II ♦ Widoki
Rozdział 12.
Znaczniki, filtry i funkcje
Dokumentacja szablonów Twig dzieli dostępne instrukcje na:
znaczniki (ang. tags),
filtry (ang. filters),
funkcje (ang. functions).
Znaczniki Twig
Pełna lista znaczników Twig jest zawarta w tabeli 12.1. Zwróć uwagę, że niektóre z nich,
np. for, zawierają ogranicznik końcowy i otaczają większy fragment kodu:
{% for ... %}
...
{% endfor %}
170 Część II ♦ Widoki
inne zaś, np. extends, składają się z jednego wiersza kodu i nie mają ogranicznika
końcowego end:
{% extends ... %}
Rozdział 12. ♦ Znaczniki, filtry i funkcje 171
{% block ... %}
...
{% endblock %}
Makrodefinicja z listingu 12.1 może być wywołana wewnątrz widoku, w którym została
zdefiniowana jako:
<p>{{ _self.autolink('http://google.com') }}</p>
użyta zostanie wartość domyślna ustalona na listingu 12.1 filtrem default. Wygenero-
wany kod przyjmie postać:
<a href="http://example.net">
http://example.net
</a>
172 Część II ♦ Widoki
wygeneruje wówczas znacznik div zawierający jej wartość. Makrodefinicja str jest
przedstawiona na listingu 12.2.
W celu uniknięcia konfliktu z istniejącymi makrami możemy nadać nową nazwę do-
łączanej makrodefinicji. Instrukcja:
{% from 'm.html.twig' import spr as s %}
Znacznik filter
Znacznik {% filter %} przekształca wybranym filtrem blok kodu HTML. Instrukcja:
{% filter lower %}
LOREM IPSUM
{% endfilter %}
Rozdział 12. ♦ Znaczniki, filtry i funkcje 173
wygeneruje napis:
lorem ipsum
Znacznik set
Znacznik {% set ... %} został szczegółowo opisany w rozdziale 9.
Znacznik extends
Znacznik {% extends %} służy do definiowania szablonu bazowego. Parametrem znacznika
{%extends %} jest logiczna nazwa widoku, np.:
{% extends 'MyLoremBundle:Ipsum:dolor.html.twig' %}
{% extends '::base.html.twig' %}
W Symfony 2 parametrem znacznika {% extends %} nie może być tablica nazw logicz-
nych widoków1:
PRZYKŁAD BŁĘDNY
{% extends ['MyLoremBundle:Ipsum:dolor.html.twig', '::base.html.twig'] %}
1
Por. http://twig.sensiolabs.org/doc/tags/extends.html.
174 Część II ♦ Widoki
Listing 12.4. Przykładowa zawartość widoku index.html.twig dla pliku base.html.twig z listingu 12.3
{% extends '::base.html.twig' %}
{% block body %}
<h1>Powitanie</h1>
{% endblock %}
Widok z listingu 12.4 nie może zawierać kodu HTML poza blokami:
PRZYKŁAD BŁĘDNY
{% extends '::base.html.twig' %}
<h1>Powitanie</h1>
Rozdział 12. ♦ Znaczniki, filtry i funkcje 175
Znacznik block
Znacznik {% block %} definiuje pojemnik przeznaczony na treść. Jest on odpowiednikiem
slotów występujących w Symfony 1.4. Parametrem znacznika jest nazwa bloku:
{% block nazwabloku %}
...
{% endblock %}
Znacznik zamykający także może zawierać nazwę bloku. Taki zapis ma na celu zwięk-
szenie czytelności widoków:
{% block nazwabloku %}
...
{% endblock nazwabloku %}
Bloki generujące niewielkie ilości kodu możemy zapisywać w postaci skróconej, pozba-
wionej znacznika zamykającego:
{% block nazwabloku wyrazenie %}
na przykład:
{% block info '<div>' ~ zmienna ~ '</div>' %}
{% block b %}
...
{% endblock %}
{% block c %}
...
{% endblock %}
{% block body %}
<h1>Powitanie</h1>
{% endblock %}
Funkcja parent() zwraca zawartość bloku o nazwie identycznej jak blok, w którym zo-
stała wywołana, ale pochodzącą z szablonu nadrzędnego. Powyższe wywołanie parent()
jest zawarte w bloku o nazwie lorem w widoku, który zawiera instrukcję:
{% extends '::base.html.twig' %}
Rozdział 12. ♦ Znaczniki, filtry i funkcje 177
Wynikiem funkcji parent() jest zatem zawartość bloku lorem z dokumentu base.
html.twig.
W pliku dziedziczącym index.html.twig może również wystąpić blok dolor, który nie
występuje w pliku bazowym:
//plik index.html.twig
{% extends '::base.html.twig' %}
{% block dolor %}
<h2>Dolor</h2>
{% endblock %}
W takim przypadku zawartość bloku dolor zostanie pominięta i nie pojawi się w gene-
rowanym kodzie HTML.
Zwróć uwagę, że jedynym widokiem, który zawiera kod HTML umieszczony poza blo-
kami, jest widok base.html.twig. Widoki zawierające znaczniki {% extends %} mogą
zawierać wyłącznie bloki. Nie możemy w nich umieszczać treści poza blokami.
Znacznik use
Znacznik {% use %} umożliwia dołączenie bloków z innego widoku. W ten sposób wi-
dok, który dziedziczy po jednym widoku, może jednocześnie zawierać nadpisane bloki
z innego widoku.
widoku bloki.html.twig:
//bloki.html.twig
{% block body %}
ipsum
{% endblock %}
Znacznik include
Znacznik {% include %} dołącza we wskazanym miejscu zewnętrzny widok:
{% include '::reklama.html.twig' %}
W Symfony 2 parametrem znacznika {% include %} nie może być tablica nazw lo-
2
gicznych widoków :
PRZYKŁAD BŁĘDNY
{% include ['::reklama.html.twig', '::box.html.twig'] %}
Znacznik spaceless
Znacznik {% spaceless %} służy do usunięcia białych znaków otaczających znaczniki
HTML. Instrukcje:
2
Por. http://twig.sensiolabs.org/doc/tags/include.html.
180 Część II ♦ Widoki
{% spaceless %}
<header>
<h1>Witaj</h1>
</header>
{% endspaceless %}
wygenerują kod:
<header><h1>Witaj</h1></header>
Znacznik autoescape
Znacznik {% autoescape %}, opcja konfiguracyjna autoescape (listing 10.4) oraz filtry:
|escape
|e
|raw
Znacznik raw
Znacznik {% raw %} został szczegółowo omówiony w rozdziale 9.
Znacznik flush
Kod HTML generowany przez widoki Twig podlega buforowaniu. Znacznik {% flush %}
opróżnia bufor wyjściowy.
Znacznik do
Znacznik {% do %} ewaluuje wyrażenie bez generowania wydruku. Instrukcja:
{{ 2 * 3 * 4 }}
Instrukcja:
{{ do 2 * 3 * 4 }}
Znacznik render
Parametrem znacznika {% render %} jest logiczna nazwa akcji, która powstaje w analogicz-
ny sposób jak logiczna nazwa widoku. Jeśli w aplikacji występuje pakiet My/LoremBundle,
w nim — kontroler Ipsum oraz akcja dolorAction(), to logiczną nazwą akcji dolor jest:
MyLoremBundle:Ipsum:dolor
Instrukcja:
{% render 'MyLoremBundle:Ipsum:dolor' %}
Filtry
Tabela 12.3 zawiera wszystkie filtry domyślnie dostępne w Twig.
{{ [1, 2, 3] | join('*') }}
wydrukuje
1*2*3
json_encode Konwertuje dane do postaci JSON. —
Stosuje funkcję PHP json_encode().
keys Dla zadanej tablicy asocjacyjnej {% for k in {'x': 'y', 'a': 'b'} |
zwraca tablicę kluczy. ´keys %}
{{ k }}
{% endfor %}
wydrukuje:
x
a
length Zwraca długość ciągu znaków lub {{ 'ala' | length }}
liczbę elementów tablicy. wydrukuje 3
lower Konwertuje wszystkie litery na {{ 'ĄĆĘŁŃÓŚŹŻ' | lower }}
małe. Uwzględnia polskie znaki wydrukuje:
diakrytyczne.
ąćęłńóśźż
merge Łączy tablice. Jeśli tablica imiona zawiera elementy
'Anna' i 'Maria' to po wywołaniu:
imiona|merge(['Piotr', 'Paweł'])
będzie zawierała:
'Anna', 'Maria', 'Piotr', 'Paweł'.
nl2br Konwertuje znaki złamania wiersza, {{ "a \n b \n c" | nl2br }}
dodając przed nimi znaczniki <br/>. wydrukuje:
Uwaga: domyślnie ten filtr jest
w Symfony 2 wyłączony. a <br />
b <br />
c
Raw Wyłącza zabezpieczenia włączone —
opcją konfiguracyjną autoescape.
Rozdział 12. ♦ Znaczniki, filtry i funkcje 183
Funkcje
Wszystkie funkcje Twig są zawarte w tabeli 12.4.
wówczas adresy generowane przez funkcję asset() będą zawierały na końcu oznaczenie
wersji. Instrukcja:
{{ asset('css/style.css') }}
Rozdział 12. ♦ Znaczniki, filtry i funkcje 185
path() Generuje adres URL dla podanej Jeśli w kontrolerze występuje adnotacja:
reguły routingu. @Route("/a.html", name="strona")
wówczas instrukcja:
{{ path('strona') }}
wydrukuje:
/pelna/sciezka/do/projektu/a.html
url() Generuje bezwzględny adres URL Jeśli w kontrolerze występuje adnotacja:
zawierający także nazwę hosta. @Route("/a.html", name="strona")
wówczas instrukcja:
{{ url('strona') }}
wydrukuje:
http://nazwa.hosta.pl/pelna/sciezka/do/projektu/a.html
wydrukuje adres:
/pelna/sciezka/do/projektu/css/style.css?v1
pliki CSS, JavaScript oraz pliki graficzne zostaną na pewno przeładowane. Żaden z nich nie
będzie pochodził z pamięci podręcznej przeglądarki.
Napisz aplikację, która będzie prezentowała teksty piosenek. Zadanie rozwiąż w taki
sposób, by na stronie głównej znajdowało się menu zawierające tytuły wszystkich piose-
nek i umożliwiające przejście do strony prezentującej dowolną piosenkę. Na stronie
każdej z piosenek umieść hiperłącze pozwalające na pobranie tekstu piosenki w for-
macie TXT.
Aplikacja powinna działać w taki sposób, by dodanie nowych plików z tekstami piosenek
automatycznie3 powodowało pojawienie się nowych tytułów w menu witryny. Ponadto do
identyfikacji piosenek użyj nazw plików tekstowych. Jeśli w folderze danych znajdują
się pliki:
lorem-ipsum.txt
dolor-sit-amet.txt
oraz
/web/lorem-ipsum.txt
/web/dolor-sit-amet.txt
Wykonując zadanie, wykorzystaj szablon HTML/CSS oraz pliki tekstowe zawarte w pliku
12-start.zip.
ROZWIĄZANIE
Krok 1. Utwórz nowy projekt i pakiet
W folderze przeznaczonym na aplikacje WWW utwórz folder zad-12/ i wypakuj do niego
zawartość archiwum symfony2-customized-v1.zip. Następnie komendą:
php app/console generate:bundle
--namespace=My/SongBundle --dir=src --no-interaction
3
Jedyną operacją, jaką należy wykonać po dodaniu nowych plików, powinno być odświeżenie strony.
Rozdział 12. ♦ Znaczniki, filtry i funkcje 187
/**
* @Route("/{slug}.{_format}", name="_show_")
* @Template()
*/
public function showAction($slug, $_format)
{
$slugs = array();
$data = array();
$data[] = array(
'title' => $t,
'slug' => $s
);
$slugs[] = $s;
}
if (!in_array($slug, $slugs)) {
throw $this->createNotFoundException('Podana strona nie istnieje!');
}
Parametr slug będzie zawierał nazwę pliku tekstowego pozbawioną rozszerzenia, a para-
metr _format — typ wyniku: txt lub html. W adresie:
jada-jada-misie.txt
a w adresie:
kolko-graniaste.html
188 Część II ♦ Widoki
natomiast w tablicy $data należy umieścić dwie tablice zawierające tytuły piosenek i na-
zwy plików:
$data = array(
array('title' => 'Lorem ipsum', 'slug' => 'lorem-ipsum');
array('title' => 'Dolor sit amet', 'slug' => 'dolor-sit-amet');
);
W ten sposób utworzyliśmy zmienne $slugs oraz $data. Następnie sprawdzamy, czy
podana wartość parametru $slug jest poprawna. Poprawne są tylko te wartości, które wy-
stępują w tablicy $slugs. Jeśli więc podana wartość nie występuje w tablicy $slugs
(tj. funkcja in_array() zwraca logiczny fałsz), to metodą createNotFoundException()
generujemy wyjątek:
if (!in_array($slug, $slugs)) {
throw $this->createNotFoundException('Podana strona nie istnieje!');
}
Jeśli wyjątek nie został wygenerowany, oznacza to, że wartość $slug jest poprawną
nazwą pliku tekstowego z folderu [projekt]/data/. Plik ten odczytujemy:
$p = file('../data/' . $slug . '.txt');
Dzięki temu w zmiennej $contents wystąpi kompletny tekst piosenki pozbawiony tytułu.
<div id="pojemnik">
<h1 id="naglowek">Piosenki dla dzieci<span></span></h1>
<ul id="menuglowne">
{% for song in data %}
<li><a href="{{ path('_show_', {'slug': song.slug, '_format': 'html'})
´}}">{{ song.title }}</a></li>
{% endfor %}
</ul>
<div id="tresc">
{% block body %}{% endblock %}
</div>
</div>
</body>
</html>
Tytuł piosenki, który przekazaliśmy do widoku jako zmienną title, drukujemy wewnątrz
znacznika <title></title>:
<title>Piosenki / {{ title }}</title>
Do funkcji path() przekazujemy nazwę reguły routingu _show_ oraz tablicę asocjacyjną
zawierającą klucze slug oraz _format.
{% block body %}
<h3>{{ title }}</h3>
<p>
{{ contents|nl2br }}
</p>
{% endblock %}
{{ contents }}
skopiuj do folderu:
[project]/web/
który zawierał cały generowany kod HTML. Przykłady te w ogóle nie wykorzystywały
pliku app/Resources/views/base.html.twig.
oraz szablon:
[projekt]/src/My/AnimalsBundle/Resources/views/layout.html.twig
oraz szablon:
[projekt]/app/Resources/views/base.html.twig
W większych projektach, które będą zawierały wiele kontrolerów oraz akcji, najkorzyst-
niejszym rozwiązaniem będzie trójstopniowy podział widoków. Szkielet strony WWW
będziemy umieszczali w widoku:
[projekt]/app/Resources/views/base.html.twig
196 Część II ♦ Widoki
Znajdą się w nim m.in. znaczniki head, meta i link oraz bloki przeznaczone do wy-
pełnienia w widokach potomnych. Domyślny plik base.html.twig jest przedstawiony na
listingu 13.1.
Umieścimy w nim blok body definiujący szablon strony WWW i zawierający bloki
przeznaczone na treść oraz menu. Przykładowy plik layout.html.twig jest przedstawiony
na listingu 13.2.
{% block body %}
<header>
<h1>Logo</h1>
</header>
<nav>
<ul>
<li><a href="#">lorem</a></li>
<li><a href="#">ipsum</a></li>
</ul>
</nav>
<section>
{% block contents %}{% endblock %}
</section>
<footer>
©2012 by gajdaw
</footer>
{% endblock %}
Rozdział 13. ♦ Trójstopniowy podział widoków 197
Następnie wystąpią w nim bloki title oraz contents. Blok title będzie nadpisywał
zawartość bloku z widoku base.html.twig, a blok contents — zawartość bloku contents
z widoku layout.html.twig. Zarys widoku index.html.twig jest przedstawiony na li-
stingu 13.3.
{% block title %}
Witaj!
{% endblock %}
{% block contents %}
<h1>Lorem ipsum</h1>
<p>
Dolor sit amet...
</p>
{% endblock %}
Przykład 13.1.
Opowiadania Edgara Allana Poe
Dana są pliki tekstowe zawierające opowiadania Edgara Allana Poe. Każde opowiadanie
jest zawarte w osobnym pliku tekstowym. Struktura plików jest następująca:
Pierwszy wiersz pliku zawiera tytuł utworu.
Kolejne wiersze zawierają tekst utworu.
Każdy wiersz pliku zawiera kompletny akapit tekstu.
Ponadto dany jest plik tekstowy 00index.log zawierający listę wszystkich utworów.
Fragment pliku 00index.log jest przedstawiony na listingu 13.4.
198 Część II ♦ Widoki
Napisz aplikację, która będzie prezentowała utwory Edgara Allana Poe w postaci witryny
internetowej. Witryna powinna zawierać menu główne pozwalające na wybranie
utworu.
Wykonując zadanie, wykorzystaj szablon HTML/CSS oraz pliki tekstowe zawarte w pliku
13-start.zip.
ROZWIĄZANIE
Krok 1. Utwórz nowy projekt i pakiet
W folderze przeznaczonym na aplikacje WWW utwórz folder zad-13/ i wypakuj do
niego zawartość archiwum symfony_customized_v1.zip. Z projektu usuń pakiet demo,
który jest zawarty w folderze src/Acme/.
Następnie komendą:
php app/console generate:bundle
ustala postać adresów URL. W treści metody przygotowujemy tablicę $slugs, która
jest konieczna do walidacji zmiennej $slug. Tablica $slugs powstaje na podstawie ostat-
niej kolumny pliku 00index.log.
Jeśli wartość zmiennej $slug jest poprawna, odczytujemy plik z treścią odpowiednie-
go utworu:
$contents = file('../data/' . $slug . '.txt');
Na podstawie zmiennych $contents oraz $title tworzymy zmienną $novel, która zawiera
tytuł opowiadania (składowa title) oraz treść podzieloną na akapity (składowa contents):
$novel = array(
'title' => trim($title),
'contents' => $contents
);
Akcja menuAction() będzie służyła do wygenerowania menu witryny. W kodzie akcji należy
przygotować tablicę menuData , która będzie zawierała zawartość pliku 00index.log. Po
utworzeniu pustej tablicy i odczytaniu pliku:
$menuData = array();
$indexFile = file('../data/00index.log');
po czym na podstawie otrzymanych elementów $e[0] oraz $e[1] tworzymy tablicę aso-
cjacyjną o indeksach title oraz slug i dołączamy ją na końcu tablicy $menuData:
$menuData[] = array(
'title' => $e[0],
'slug' => $e[1]
);
Widok menu.html.twig zawiera pętlę {% for %}, która przetwarza tablicę menuData wygen-
erowaną w akcji menuAction(). Na podstawie tablicy generujemy elementy hiperłącza,
których adres powstaje przy użyciu funkcji path().
W celu wygenerowania kodu HTML menu należy wykonać funkcję menuAction() i prze-
tworzyć szablon menu.html.twig. Osiągniemy to, umieszczając w dowolnym widoku
instrukcję:
{% render 'MyNovelBundle:Default:menu' %}
Parametrem znacznika {% render %} jest logiczna nazwa akcji menuAction(). Nazwa wi-
doku przetwarzanego po wywołaniu funkcji jest ustalona adnotacją:
/**
* @Template()
*/
public function menuAction()
...
</head>
<body>
{% block body %}{% endblock %}
</body>
</html>
{% block body %}
<section>
{% render 'MyNovelBundle:Default:menu' %}
<article>
{% block contents %}
{% endblock %}
</article>
</section>
{% endblock %}
{% block title %}
{{ novel.title}} / {{ parent() }}
{% endblock %}
{% block contents %}
<h1>{{ novel.title}}</h1>
<p>
{{ paragraph }}
</p>
{% endfor %}
{% endblock %}
Jak to się dzieje, że każdy akapit tekstu jest osobnym elementem tablicy? Taki
efekt osiągniemy dzięki odpowiedniemu opracowaniu plików tekstowych. W pliku
the-pit-and-the-pendulum.txt znajdziemy tekst:
THE PIT AND THE PENDULUM
I WAS sick ... night were the universe.
I had swooned ... before arrested his attention.
...
Pierwszy akapit utworu rozpoczyna się od słów „I WAS sick” i kończy słowami „night
were the universe”. Drugi akapit utworu rozpoczyna się od słów „I had swooned”,
a kończy słowami „before arrested his attention”. Dzięki temu dołączenie znaczników
<p></p> sprowadza się do prostej iteracji tablicy.
skopiuj do folderu:
[project]/web/
Domyślną wartością zmiennej slug będzie index. Zwróć uwagę, że w folderze danych
znajduje się plik index.txt, który zawiera treść utworu pt. „A Predicament”. Potwierdzi to
także pierwszy wiersz pliku 00index.log:
A PREDICAMENT|index
204 Część II ♦ Widoki
Przy takich ustawieniach routingu utwór „A Predicament” jest dostępny pod dwoma
różnymi adresami:
.../web/
.../web/index.html
Adres strony:
.../web/
możesz także ustalić, podając w pliku routing.yml regułę pozbawioną zmiennej slug:
_homepage:
pattern: /
defaults: { _controller: MyNovelBundle:Default:show }
Przetwarzanie widoku:
return $this->render('Logiczna:Nazwa:widoku.html.twig');
Rozdział 15.
Dodawanie
nowych pakietów
Symfony 2 składa się z pewnej liczby pakietów. Standardowa dystrybucja Symfony 2
nie zawiera jednak wielu przydatnych pakietów, które wykorzystamy w kolejnych
częściach podręcznika. Zanim przejdziemy do omawiania zagadnień dotyczących baz
danych, najpierw nauczymy się więc instalować rozszerzenia. Procedurę instalacji
rozszerzeń omówimy na przykładzie pakietu DoctrineFixturesBundle, który ułatwi
nam wypełnianie bazy danych na podstawie plików.
Plik [projekt]/deps ma składnię plików INI. Jego początkowy fragment jest przedstawio-
ny na listingu 15.1.
[twig]
git=http://github.com/fabpot/Twig.git
version=v1.6.0
[monolog]
git=http://github.com/Seldaek/monolog.git
version=1.0.2
[doctrine-common]
git=http://github.com/doctrine/common.git
version=2.1.4
...
O tym, że plik [projekt]/deps ma składnię plików INI, przekonasz się, analizując skrypt
[projekt]/bin/vendors. Jest to skrypt PHP, który zawiera następującą instrukcję odczytu-
jącą zawartość pliku [projekt]/deps:
$deps = parse_ini_file($rootDir.'/deps', true, INI_SCANNER_RAW);
Obie dystrybucje zawierają identyczne pliki deps oraz deps.lock. Różnią się tylko tym,
że w dystrybucji Symfony Standard pakiety zostały pobrane do folderu vendor/,
a w dystrybucji Symfony Standard without vendors — nie.
Rozdział 15. ♦ Dodawanie nowych pakietów 211
Jeśli chcesz użyć dystrybucji Symfony Standard with vendors, do instalacji pakie-
tów użyj instrukcji:
php bin/vendors install --reinstall
która wymusza ponowne pobranie wszystkich pakietów.
Po wydaniu komendy:
php bin/vendors install
folder zawierający projekt będzie zajmował ponad 100 MB! Przyczyną tego jest fakt, że
w pakietach będą zawarte foldery .git/ zawierające kompletną historię każdego z pakie-
tów. Zawartość folderów .git/, które znajdują się wewnątrz folderu [projekt]/vendor/,
jest zbędna1 i mogą one zostać usunięte. Po usunięciu wszystkich folderów .git rozmiar
projektu zmniejszy się do ok. 20 MB, co po skompresowaniu da ok. 8 MB.
Polecenie z listingu 15.2 wyszukuje w folderze vendor (parametr vendor) wszystkie fol-
dery (parametr -type d) o nazwie .git (parametr -name .git) i wykonuje na nich (para-
metr -exec) polecenie rm fr. Powoduje ono zatem usunięcie wszystkich folderów o na-
zwie .git/ zawartych w katalogu bieżącym i wszystkich jego podkatalogach.
Komendę find usuwającą foldery .git należy koniecznie wydać w folderze zawierającym
projekt! Jeśli wydasz ją w folderze głównym, to usuniesz wszystkie foldery .git na
dysku!
1
Pod warunkiem, że nie zamierzasz modyfikować kodu pakietów i umieszczać ich na serwerze Git.
212 Część III ♦ Dostosowywanie Symfony 2
ROZWIĄZANIE
Krok 1. Pobierz najnowszą wersję Symfony 2
Odwiedź stronę:
http://symfony.com/download
i pobierz najnowszą wersję dystrybucji Symfony Standard without vendors, np. plik
Symfony_Standard_2.0.10.zip. Pobrane archiwum rozpakuj. Wypakowana zawartość
jest domyślnie umieszczana w folderze Symfony/. Zmień nazwę folderu Symfony/ na
symfony2-customized-v2/.
_demo_secured:
resource: "@AcmeDemoBundle/Controller/SecuredController.php"
type: annotation
_demo:
resource: "@AcmeDemoBundle/Controller/DemoController.php"
type: annotation
prefix: /demo
[DoctrineFixturesBundle]
git=http://github.com/doctrine/DoctrineFixturesBundle.git
target=/bundles/Symfony/Bundle/DoctrineFixturesBundle
version=origin/2.0
pobierze najnowszą wersję pakietu. Nie zawsze jest to dobre rozwiązanie, gdyż nowe
wersje pakietów mogą być niekompatybilne z poprzednimi. Dlatego po zainstalowaniu
pakietów należy wydać komendę:
php bin/vendors lock
która zapisuje w pliku deps.lock bieżące wersje wszystkich pakietów. W ten sposób
otrzymana dystrybucja będzie niezależna od bieżących poprawek wprowadzanych na
serwerze GitHub. Jeśli za jakiś czas (np. za kilka tygodni) usuniesz zawartość folderu
vendor/ i ponownie wydasz komendę:
php bin/vendors install
Witryna
http://knpbundles.com
zawiera zestawienie bardzo wielu ciekawych pakietów Symfony 2.
Część IV
Praca z bazą danych
220 Część IV ♦ Praca z bazą danych
Rozdział 17. ♦ Pierwszy projekt wykorzystujący bazę danych 221
Rozdział 17.
Pierwszy projekt
wykorzystujący
bazę danych
Przykład 17.1. ma Cię zapoznać z:
tworzeniem pustej bazy danych;
tworzeniem konta dostępu do bazy danych;
sposobem sprawdzania poprawności utworzenia bazy danych;
konfiguracją połączenia z bazą danych projektu Symfony 2;
metodą generowania klas dostępu do bazy danych;
techniką wypełniania bazy danych przy użyciu skryptów fixtures;
kontrolerem, który pobierze z wybranej tabeli bazy danych wszystkie rekordy
i przekaże je do widoku;
przetwarzaniem w widoku akcji kolekcji obiektów pobranych z bazy danych.
Napisz aplikację, która będzie prezentowała wszystkie imiona pochodzące z pliku tek-
stowego w postaci listy wypunktowanej ul:
<ul>
<li>Jan</li>
<li>Krzysztof</li>
<li>Tomasz</li>
...
</ul>
Wykonując zadanie, wykorzystaj szablon HTML/CSS oraz pliki tekstowe zawarte w pliku
17-start.zip.
ROZWIĄZANIE
Krok 1. Utwórz nowy projekt i pakiet
W folderze przeznaczonym na aplikacje WWW utwórz folder zad-17/ i wypakuj do niego
zawartość archiwum symfony2-customized-v2.zip wykonanego w rozdziale 15.
Komendą:
php app/console generate:bundle
--namespace=My/FrontendBundle --dir=src --no-interaction
Zapytanie:
drop schema if exists names;
usuwa bazę danych o nazwie names, pod warunkiem, że taka baza istnieje.
Rozdział 17. ♦ Pierwszy projekt wykorzystujący bazę danych 223
Zapytanie:
create schema names default character set utf8 collate utf8_polish_ci;
tworzy pustą bazę danych names stosującą kodowanie znaków utf8. Dzięki parametrowi
collate sortowanie tekstów będzie zgodne z językiem polskim. Oto, jaki otrzymamy po-
rządek sortowania kilku przykładowych wyrazów:
Arbuz
Ćma
Kora
Świnka
Zebra
Żółw
Kolejne zapytanie:
grant all on names.* to editor@localhost identified by 'secretPASSWORD';
tworzy konto o nazwie editor i o haśle secretPASSWORD. Konto to będzie miało wszystkie
uprawnienia dostępu do bazy danych names.
W miejsce napisu AX1BY2CZ3 wprowadź hasło konta root serwera MySQL. Teraz utwo-
rzenie pustej bazy danych sprowadzi się do podwójnego kliknięcia pliku tworzenie-pustej-
-bazy-danych.bat.
Rysunek 17.2. Poprawność tworzenia pustej bazy danych sprawdzamy w programie phpMyAdmin
Na podstawie pliku danych podejmujemy decyzję, że baza danych będzie zawierała jed-
ną tabelę o nazwie name. W tabeli name umieścimy dwie kolumny:
klucz główny id (typu integer),
kolumnę caption (typu string o długości do 255 znaków).
Wydaj polecenie:
php app/console generate:doctrine:entity
W odpowiedzi na monit:
The Entity shortcut name:
po czym dodaj do klasy jedno pole2 o nazwie caption typu string o długości 255 znaków.
namespace My\FrontendBundle\Entity;
/**
* My\FrontendBundle\Entity\Name
*
* @ORM\Table()
* @ORM\Entity
*/
class Name
{
/**
* @var integer $id
*
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @var string $caption
*
* @ORM\Column(name="caption", type="string", length=255)
*/
private $caption;
/**
* Get id
*
* @return integer
*/
public function getId()
{
return $this->id;
}
1
W celu pozostawienia wartości domyślnej naciśnij ENTER.
2
W celu zakończenia dodawania pól pozostaw pustą nazwę pola i naciśnij ENTER.
Rozdział 17. ♦ Pierwszy projekt wykorzystujący bazę danych 227
/**
* Set caption
*
* @param string $caption
*/
public function setCaption($caption)
{
$this->caption = $caption;
}
/**
* Get caption
*
* @return string
*/
public function getCaption()
{
return $this->caption;
}
}
Wygenerowana klasa zawiera dwie właściwości. Pierwsza z nich nazywa się $id, a druga
— $caption. Dla właściwości $caption zostały wygenerowane metody getCaption()
oraz setCaption(), a dla właściwości $id – metoda getId(). Metody get() służą do od-
czytu wartości właściwości, a metody set() — do ustalenia nowej wartości właściwości.
Ponieważ właściwości $id oraz $caption są prywatne, jedynym sposobem uzyskania do-
stępu do nich jest użycie metod get() i set().
Spowoduje ono utworzenie w bazie danych names tabeli o nazwie name. Tabela name bę-
dzie zawierała dwie kolumny: id oraz caption. Przekonasz się o tym, odwiedzając za-
kładkę Struktura w aplikacji phpMyAdmin. Podgląd struktury tabeli name w bazie danych
przy użyciu aplikacji phpMyAdmin jest przedstawiony na rysunku 17.3.
namespace My\FrontendBundle\DataFixtures\ORM;
use Doctrine\Common\Persistence\ObjectManager;
use My\FrontendBundle\Entity\Name;
class LoadData implements FixtureInterface
{
function load(ObjectManager $manager)
{
Rozdział 17. ♦ Pierwszy projekt wykorzystujący bazę danych 229
$data = file('data/imiona.txt');
foreach ($data as $i) {
$Name = new Name();
$Name->setCaption(trim($i));
$manager->persist($Name);
}
$manager->flush();
}
}
Wewnątrz pętli zmienna $i zawiera kolejne imiona. W celu zapisania imienia w bazie
najpierw tworzymy obiekt $Name klasy Name:
$Name = new Name();
Należy pamiętać, że metoda persist() nie zapisuje rekordu w bazie danych, gdyż opera-
cje zapisu rekordów podlegają buforowaniu.
Spowoduje ona wykonanie kodu z listingu 17.6. W wyniku tego w bazie danych pojawi
się 479 rekordów. Przekonasz się o tym, odwiedzając zakładkę Przeglądaj dla tabeli name.
Procedura analizy zawartości tabeli name jest przedstawiona na rysunku 17.4.
W akcji index tworzymy obiekt $em, który umożliwia wydawanie zapytań do bazy danych:
$em = $this->getDoctrine()->getEntityManager();
Zmienna $entities jest kolekcją obiektów klasy Name, której kod jest widoczny na li-
stingu 17.4.
{% block body %}
<h2>Imiona</h2>
<ul>
{% for imie in entities %}
<li>
{{ imie.caption }}
</li>
{% endfor %}
</ul>
{% endblock %}
Wewnątrz pętli zmienna imie jest kolejnym obiektem z kolekcji entities. Jest to obiekt
klasy Name, więc ma on zdefiniowane m.in. metody getCaption() oraz getId(). W celu
wydrukowania imienia wywołujemy metodę getCaption():
<li>
{{ imie.caption }}
</li>
Oczywiście powyższy zapis jest zapisem skrótowym. Pełna postać wywołania wygląda
następująco:
{{ imie.getCaption() }}
Rysunek 17.5.
Witryna z przykładu 17.1
Rozdział 18.
ORM Doctrine 2
Pierwsza z nich tworzy, druga usuwa, a trzecia uaktualnia bazę danych. Oczywiście wszyst-
kie trzy polecenia dotyczą bazy danych, której parametry konfiguracyjne wprowadzimy
w pliku app/config/parameters.ini.
Parametr --force zabezpiecza nas przed przypadkowym usunięciem ważnych danych. Jeśli
go nie podamy, polecenie drop nie zostanie wykonane. W celu utworzenia bazy danych
wydajemy polecenie:
doctrine:schema:create --force
dlatego tworząc bazę danych, stosuję skrypty .sql oraz .bat z listingów 17.1 oraz 17.2.
234 Część IV ♦ Praca z bazą danych
Doctrine 2.1
Do wykonywania operacji dostępu do rekordów zapisanych w bazie danych Symfony 2
wykorzystuje bibliotekę ORM Doctrine 2.1. Pełna dokumentacja Doctrine 2.1 jest dostępna
na stronie:
http://www.doctrine-project.org/docs/orm/2.1/en/
Podobnie jak w przypadku kontrolerów i widoków, tak i tym razem będziemy posługiwali
się logiczną nazwą klasy. Logiczna nazwa klasy Name miała postać:
MyFrontendBundle:Name
Klasę Dolor możesz oczywiście przygotować ręcznie — nie musisz do tego wykorzy-
stywać polecenia generate:doctrine:entity.
1
Oczywiście generowanie klasy Dolor należy poprzedzić wygenerowaniem pakietu Lorem/IpsumBundle.
Rozdział 18. ♦ ORM Doctrine 2 235
oraz:
@ORM\Column(name="caption", type="string", length=255)
Do ustalenia nazwy tabeli bazy danych odpowiadającej danej klasie Entity służy adnotacja
@Table. Jeśli nie zawiera ona parametru name:
@ORM\Table()
wówczas nazwa tworzonej tabeli będzie taka jak nazwa klasy. Jeśli w adnotacji @Table po-
damy parametr name, ustalimy nazwę tabeli:
@ORM\Table(name="aaa")
236 Część IV ♦ Praca z bazą danych
Polecenie:
doctrine:schema:update --force
ma tę wadę, że nie usuwa nieaktualnych już informacji. Jeśli na przykład wygenerujesz klasę
User i na jej podstawie utworzysz odpowiadającą jej tabelę user, po czym adnotacją @Table
zmienisz nazwę tabeli user na myuser:
@ORM\Table(name="myuser")
Parametr name adnotacji @Table pozwala ustalić następujące nazewnictwo w bazie danych:
Nazwa tabeli w bazie danych jest rzeczownikiem w liczbie mnogiej, np. rivers,
users, clients lub books.
Klasa zapewniająca dostęp do tabeli jest rzeczownikiem w liczbie pojedynczej,
np. River, User, Client lub Book.
W ten sposób zarówno kod SQL, jak i PHP wygląda intuicyjnie. Liczba mnoga w nazwie
tabeli sugeruje, że jest w niej zawartych wiele rekordów. Zmienna tworzona w kodzie
PHP jest w liczbie pojedynczej, np.:
$User = New User();
Adnotacja:
@ORM\GeneratedValue(strategy="AUTO")
Dla pozostałych właściwości w klasie Entity wystąpią metody get() oraz set().
Następnie poleceniem:
php app/console doctrine:generate:entities My
Typy danych
Typ kolumny w tabeli danych dla danej właściwości ustalamy parametrem type adnotacji
@Column, np.:
@ORM\Column(name="ipsum", type="string", length=255)
Parametr nullable zezwala na stosowanie wartości NULL. Domyślnie kolumny nie mogą
przyjmować wartości NULL. Dlatego jeśli na początku skryptu LoadData.php z przykła-
du 17.1. dodamy kod z listingu 18.1, zakończy się to błędem.
...
Oczywiście bez względu na wartość parametru nullable do tabeli name zawsze możemy
wstawiać imiona będące napisami pustymi:
$Name = new Name();
$Name->setCaption('');
$manager->persist($Name);
Kolumny bazy danych mogą mieć ustalone wartości domyślne. Służy do tego słowo klu-
czowe DEFAULT języka SQL:
CREATE TABLE `lorem` (
`id` INT NOT NULL AUTO_INCREMENT ,
`ipsum` VARCHAR(45) DEFAULT 'dolor sit amet!' ,
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
Doctrine 2.1 nie umożliwia korzystania ze słowa kluczowego DEFAULT języka SQL.
Generowane zapytania SQL nigdy, bez względu na konfigurację, nie zawierają wartości
domyślnych zdefiniowanych przy użyciu słowa kluczowego DEFAULT języka SQL. Nie
oznacza to jednak, że nie można stosować wartości domyślnych dla właściwości. Domyślną
wartość właściwości definiujemy następująco:
private $imie = "jan";
Wartość domyślna będzie obsługiwana na poziomie klasy dostępu do bazy danych, a nie
na poziomie bazy danych.
nie powoduje wykonania żadnych zapytań SQL. W bazie danych nie pojawił się jeszcze
rekord o wartości Anna.
W ten sposób obiekt $x trafia do puli obiektów, których stan będzie synchronizowany
z bazą danych. Synchronizację dokonanych zmian wymuszamy, wywołując metodę flush()
obiektu EntityManager:
$manager->flush();
Obiekty pobrane z bazy danych metodą findAll() (listing 17.7) są w stanie MANAGED:
$entities = $manager->getRepository('MyFrontendBundle:Name')->findAll();
//obiekty z tabeli $entities są w stanie MANAGED
zawartość bazy danych różni się od zawartości obiektów zarządzanych przez obiekt
EntityManager . W celu zsynchronizowania zawartości wywołujemy metodę flush():
$manager->flush();
Kod tworzący nowy rekord w tabeli name (klasa dostępu do tabeli nazywa się Name) jest
przedstawiony na listingu 18.2.
...
Zwróć uwagę, że w przypadku tworzenia dużej liczby obiektów (kod pętli z listingu 17.6)
operacja flush() jest wykonywana tylko jeden raz, po utworzeniu wszystkich obiektów.
Rozdział 18. ♦ ORM Doctrine 2 243
Usuwanie rekordów
W celu usunięcia rekordu odpowiadającego obiektowi $x należy:
wywołać metodę remove(),
zsynchronizować stan obiektów zarządzanych przez obiekt EntityManager z bazą
danych (metoda flush()).
Wykonaj aplikację, która przedstawi zestawienie rzek w postaci tabelki HTML. Zadanie
rozwiąż, wykorzystując bazę danych. Dane zawarte w tabelce HTML mają pochodzić
z bazy danych.
Wykonując zadanie, wykorzystaj szablon HTML/CSS oraz pliki tekstowe zawarte w pliku
18-start.zip.
244 Część IV ♦ Praca z bazą danych
ROZWIĄZANIE
Krok 1. Utwórz nowy projekt i pakiet
W folderze przeznaczonym na aplikacje WWW utwórz folder zad-18/ i wypakuj do niego
zawartość archiwum symfony2-customized-v2.zip wykonanego w rozdziale 15.
Komendą:
php app/console generate:bundle
--namespace=My/FrontendBundle --dir=src --no-interaction
Wykorzystując plik z listingu 18.4, utwórz pustą bazę danych rivers. Poprawność tworze-
nia bazy sprawdź za pomocą programu phpMyAdmin.
W odpowiedzi na monit:
The Entity shortcut name:
Jako format konfiguracji pozostaw ustawienia domyślne, po czym dodaj do klasy jedno
pole o nazwie name typu string o długości 255 znaków. Pozostałe opcje pozostaw do-
myślne. W ten sposób wygenerujesz klasę src/My/FrontendBundle/Entity/River.php,
która będzie zawierała właściwości $id i $name oraz metody getId(), getName() i setName().
Zarys wygenerowanej klasy jest przedstawiony na listingu 18.7.
Spowoduje ono utworzenie w bazie danych rivers tabeli o nazwie river. Tabela river
będzie zawierała dwie kolumny: id oraz name. Poprawność tworzenia tabeli sprawdź za
pomocą programu phpMyAdmin.
/**
* @var string $name
*
* @ORM\Column(name="name", type="string", length=255)
*/
private $name;
/**
* @var integer $length
*
* @ORM\Column(name="length", type="integer")
*/
private $length;
...
Spowoduje ono wygenerowanie brakujących metod getLength() oraz setLength() dla wła-
ściwości klas z folderów Entity zawartych w przestrzeni nazewniczej My. Po wydaniu
polecenia sprawdź zawartość pliku River.php. W dolnej części pliku pojawią się przed-
stawione na listingu 18.9 metody getLength() oraz setLength().
...
/**
* Set length
*
* @param integer $length
*/
public function setLength($length)
{
$this->length = $length;
}
/**
* Get length
*
* @return integer
*/
public function getLength()
Rozdział 18. ♦ ORM Doctrine 2 247
{
return $this->length;
}
}
W celu dodania kolumny length w tabeli river w bazie danych wydaj polecenie:
php app/console doctrine:schema:update --force
namespace My\MountainBundle\DataFixtures\ORM;
use Doctrine\Common\Persistence\ObjectManager;
use My\FrontendBundle\Entity\River;
use Symfony\Component\Yaml\Yaml;
$yml = Yaml::parse('data/rivers.yml');
foreach ($yml as $r) {
$river = new River();
$river->setName($r['name']);
$river->setLength($r['length']);
$manager->persist($river);
}
$manager->flush();
}
}
Do odczytu pliku rivers.yml wykorzystana została klasa Yaml. Przed użyciem klasy Yaml
należy pamiętać o dodaniu w skrypcie instrukcji use:
use Symfony\Component\Yaml\Yaml;
Pętla foreach przetwarza wszystkie rekordy odczytane z pliku i na ich podstawie tworzy
rekordy w bazie danych.
248 Część IV ♦ Praca z bazą danych
i sprawdź zawartość bazy danych. Baza danych rivers powinna zawierać 17 rekordów.
{% block body %}
<div id="pojemnik">
{% block content %}
{% endblock %}
<div id="stopka"></div>
</div>
{% endblock %}
Skopiuj plik style.css do folderu zad-18/web/css/, a pliki graficzne logo.png oraz stopka.png
do folderu zad-18/web/images/.
/**
* @Route("/")
* @Template()
*/
Rozdział 18. ♦ ORM Doctrine 2 249
$entities = $em->getRepository('MyFrontendBundle:River')->findAll();
{% block title %}
Najdłuższe rzeki świata
{% endblock %}
{% block content %}
<h1>Najdłuższe rzeki świata</h1>
<table>
<thead>
<tr>
<th>lp.</th>
<th>Nazwa</th>
<th>Długość</th>
</tr>
</thead>
<tbody>
{% for entity in entities %}
<tr>
<td>{{ loop.index }}.</td>
<td>{{ entity.name }}</td>
<td>{{ entity.length }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}
generuje wyłącznie klasę Entity. W celu wygenerowania zarówno klasy Entity, jak i klasy
Repository należy w klasie Entity dodać parametr repositoryClass adnotacji @Entity:
/**
* @ORM\Table()
* @ORM\Entity(repositoryClass="My\FrontendBundle\Entity\LoremRepository")
*/
class Lorem
{
...
}
namespace My\FrontendBundle\Entity;
use Doctrine\ORM\EntityRepository;
/**
* LoremRepository
*
* This class was generated by the Doctrine ORM. Add your own custom
* repository methods below.
*/
class LoremRepository extends EntityRepository
{
Metoda find()
Metoda find() ma następujący nagłówek:
public function find($id, $lockMode = LockMode::NONE, $lockVersion = null)
Rozdział 19. ♦ Dostosowywanie klas dostępu do bazy danych 253
parametr $id jest kluczem głównym wyszukiwanego rekordu. W celu wyszukania rekor-
du o identyfikatorze 567 z tabeli river (przykład 18.1) należy wykonać instrukcję
z listingu 19.2.
Metoda findAll()
Treść oryginalnej metody findAll() jest przedstawiona na listingu 19.3.
Metoda findBy()
Nagłówek metody findBy() jest następujący:
public function findBy(
array $criteria, array $orderBy = null, $limit = null, $offset = null
)
Wynikiem działania metody findBy() jest kolekcja obiektów Entity klasy powiązanej
z daną klasą Repository, np. instrukcja:
$entities = $em->getRepository('MyFrontendBundle:Name')->findBy(array());
Metoda findOneBy()
Nagłówek metody findOneBy() jest następujący:
public function findOneBy(array $criteria)
Wynikiem działania metody jest pojedynczy obiekt klasy Entity powiązanej z daną klasą
Repository.
Metoda findByX()
Wewnątrz metody __call() klasy EntityRepository zaimplementowana jest obsługa
wywołań metod findByX(), gdzie X jest nazwą właściwości w klasie Entity. W przykła-
dzie 18.1, w którym wystąpiły właściwości:
$name;
$length;
Rozdział 19. ♦ Dostosowywanie klas dostępu do bazy danych 255
Wynikiem działania metod findByX() jest tablica obiektów odpowiedniej klasy Entity.
Metoda findOneByX()
Analogicznie do metod findByX() zaimplementowano metody findOneByX(). Dla każdej
właściwości X w klasie Entity dostępna jest w klasie Repository metoda findOneByX().
{
return $this->findBy(array(), array('name' => 'ASC');
}
ROZWIĄZANIE
Krok 1. Utwórz nowy projekt i pakiet
W folderze przeznaczonym na aplikacje WWW utwórz folder zad-19/ i wypakuj do
niego zawartość archiwum symfony2-customized-v2.zip wykonanego w rozdziale 15.
Komendą:
php app/console generate:bundle
--namespace=My/FrontendBundle --dir=src --no-interaction
Wykorzystując plik z listingu 19.9, utwórz pustą bazę danych mountains. Poprawność
tworzenia bazy sprawdź za pomocą programu phpMyAdmin.
utwórz model:
MyFrontendBundle:Mountain
zawierający pola:
name typu string o długości 255 znaków,
height typu integer.
...
Instrukcja:
$data = (array) $mnt;
Po wykonaniu skryptu z listingu 19.12 dla danych z pliku 19-dane.zip baza danych
powinna zawierać 16 rekordów.
{% block body %}
<div id="pojemnik">
{% block content %}
{% endblock %}
</div>
{% endblock %}
/**
* @Route("/")
* @Template()
*/
public function indexAction()
{
$em = $this->getDoctrine()->getEntityManager();
$entities = $em->getRepository('MyFrontendBundle:Mountain')->findAll();
return array('entities' => $entities);
}
{% block title %}
Tatry
{% endblock %}
{% block content %}
<h1 id="logo">Tatry</h1>
<table>
<thead>
<tr>
<th>lp.</th>
<th>Nazwa</th>
<th>Wysokość</th>
</tr>
</thead>
<tbody>
{% for szczyt in entities %}
<tr>
<td>{{ loop.index }}.</td>
<td>{{ szczyt }}</td>
<td>{{ szczyt.height }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}
W rozdziale 18. przyjrzeliśmy się nieco dokładniej bibliotece Doctrine 2.1. Najpierw
omówiliśmy polecenia służące do synchronizacji bazy danych MySQL z wygenerowa-
nymi klasami, a następnie poznaliśmy adnotacje konfigurujące strukturę bazy danych.
1
Przynajmniej w rozwiązaniach współdzielonych.
266 Część IV ♦ Praca z bazą danych
Omówiliśmy rolę klas Entity i EntityManager, ich wzajemne powiązanie oraz stan
obiektów Entity. Lekturę rozdziału zakończyliśmy, omawiając kod tworzący nowe
rekordy, usuwający rekordy oraz wyszukujący wszystkie rekordy.
Rozdział 21.
Instalacja
i konfiguracja rozszerzeń
DoctrineExtensions
Biblioteki ORM umożliwiają definiowanie operacji, które są automatycznie wykony-
wane podczas dostępu do rekordu w bazie danych. W ten sposób możemy na przykład,
zapisując obiekt w bazie danych, automatycznie wygenerować datę zapisu. Przykładem
operacji, którą automatycznie wykonujemy podczas odczytu rekordu z bazy danych, jest
tłumaczenie treści pól na wybrany język1.
1
Przetłumaczone komunikaty są zapisane w bazie danych. Tłumaczenie polega w istocie na wyborze
odpowiednich komunikatów, w zależności od ustalonego języka.
270 Część V ♦ Zachowania Doctrine
ROZWIĄZANIE
Krok 1. Wypakuj dystrybucję przygotowaną w rozdziale 15.
Wypakuj przygotowane w rozdziale 15. archiwum symfony2-customized-v2.zip, po czym
zmień nazwę otrzymanego folderu na symfony2-customized-v3/.
[DoctrineExtensionsBundle]
git=http://github.com/stof/StofDoctrineExtensionsBundle.git
target=/bundles/Stof/DoctrineExtensionsBundle
version=1.0.2
Listing 21.3. Rejestracja przestrzeni nazewniczych Stof oraz Gedmo w pliku autoload.php
...
$loader->registerNamespaces(array(
...
'Assetic' => __DIR__.'/../vendor/assetic/src',
'Metadata' => __DIR__.'/../vendor/metadata/src',
'Stof' => __DIR__.'/../vendor/bundles',
'Gedmo' => __DIR__.'/../vendor/gedmo-doctrine-extensions/lib',
));
...
272 Część V ♦ Zachowania Doctrine
orm:
auto_generate_proxy_classes: %kernel.debug%
auto_mapping: true
mappings:
StofDoctrineExtensionsBundle: false
...
stof_doctrine_extensions:
default_locale: en_US
orm:
default:
tree: false
loggable: false
timestampable: false
sluggable: false
translatable: false
Jeśli wartość powyższej opcji ustalisz na false, żadne dodatkowe tabele nie będą
tworzone. Dodatkowe tabele są konieczne wyłącznie do zachowań translatable
(tabela ext_translations) oraz loggable (tabela ext_log_entries).
2
Listing 21.4 przedstawia jedynie fragment pliku config.yml. Nie modyfikuj fragmentów, które nie są
przedstawione na listingu. Wpis rozpoczynający się od etykiety stof_doctrine_extensions możesz
dodać na samym końcu pliku.
Rozdział 21. ♦ Instalacja i konfiguracja rozszerzeń DoctrineExtensions 273
Identyfikatory slug
Identyfikatory slug są ciągami, które zawierają znaki dopuszczone w adresach URL.
Są one powszechnie stosowane w przyjaznych adresach URL1 postaci:
http://example.org/artykul/symfony-2-0-od-podstaw
1
Por. Wikipedia: http://en.wikipedia.org/wiki/Slug_(web_publishing).
276 Część V ♦ Zachowania Doctrine
Automatyczne generowanie
identyfikatorów slug w Symfony 2
W Symfony 2 konwersja z tabeli 22.1 jest zaimplementowana jako zachowanie (ang.
behaviour) o nazwie sluggable zawarte w bibliotece DoctrineExtensions, którą zainsta-
lowaliśmy w rozdziale 21. Po zainstalowaniu biblioteki i pakietu DoctrineExtensions
´Bundle włączamy w dowolnym pliku Entity automatyczne generowanie identyfikatorów
slug na podstawie wybranej kolumny. Najpierw dołączamy przestrzeń nazewniczą
adnotacji:
use Gedmo\Mapping\Annotation as Gedmo;
Teraz w odpowiedniej tabeli bazy danych powinna wystąpić kolumna slug oraz indeks
UNIQ_xxxx. Jeśli w bazie danych zapiszemy rekord:
$Word = new Word ();
$Word->setTitle('Żółty żółw')
$manager->persist($Word);
Zadanie rozwiąż w taki sposób, by każdy wiersz pliku został wykorzystany do utworzenia
osobnego rekordu w bazie danych. Każdy rekord bazy danych ma zawierać w jednej
kolumnie tekst odczytany z pliku, a w drugiej — automatycznie wygenerowany identyfi-
kator slug. Poprawność wstawiania rekordów do bazy sprawdź za pomocą programu
phpMyAdmin.
ROZWIĄZANIE
Krok 1. Przygotuj nowy projekt
Rozpakuj dystrybucję symfony2-customized-v3.zip , którą wykonaliśmy w rozdziale 21.
W wypakowanym folderze umieść folder data/dane-testowe.txt. Następnie utwórz nowy
pakiet My/FrontendBundle.
zawierający pole:
title typu string o długości 255 znaków.
278 Część V ♦ Zachowania Doctrine
namespace My\FrontendBundle\Entity;
/**
* My\FrontendBundle\Entity\Word
*
* @ORM\Table()
* @ORM\Entity
*/
class Word
{
...
/**
* @Gedmo\Slug(fields={"title"})
* @ORM\Column(length=128, unique=true)
*/
private $slug;
...
}
Po wykonaniu skryptu z listingu 22.2. baza danych words powinna zawierać w tabeli word
20 rekordów przedstawionych na rysunku 22.1.
Rysunek 22.1 zawiera trzykrotnie tekst Lorem ipsum. Pierwsza z wartości slug odpo-
wiada dokładnie tytułowi, a kolejne zawierają dodatkową liczbę, która powoduje, że war-
tości slug są unikatowe. Generowana numeracja rozpoczyna się od 1.
280 Część V ♦ Zachowania Doctrine
Rysunek 22.1.
Rekordy wstawione
do bazy danych
zawierają
automatycznie
wygenerowane
wartości slug
/**
* @var datetime $updated
*
* @Gedmo\Timestampable(on="update")
* @ORM\Column(type="datetime")
*/
private $updated;
282 Część V ♦ Zachowania Doctrine
Typ kolumny zdefiniowany adnotacją @ORM\Column ustala, czy zapisywany znacznik czasu
będzie zawierał wyłącznie datę (type="date"), czy także godzinę (type="datetime").
Parametr on adnotacji @Gedmo\Timestampable decyduje o tym, czy wartość kolumny
będzie uaktualniana podczas tworzenia rekordu (on="create"), czy podczas uaktualniania
(on="update").
ROZWIĄZANIE
Krok 1. W klasie Word dodaj właściwości updated oraz created
W klasie Word dodaj właściwości przedstawione na listingu 23.2, po czym wydaj ko-
mendę:
php app/console generate:doctrine:entities My
Następnie sprawdź zawartość bazy danych words. Rekordy tabeli word będą zawierały
kolumny created oraz updated zawierające bieżącą datę.
Rozdział 24.
Zachowanie translatable
Zachowania translatable ułatwiają zapisywanie w bazie danych pól tłumaczonych na
kilka języków. W celu użycia zachowań należy najpierw włączyć je w pliku konfigura-
cyjnym config.yml. Włączenie konfiguracji zachowań translatable jest przedstawione
na listingu 24.1.
stof_doctrine_extensions:
default_locale: pl_PL
orm:
default:
translatable: true
uaktualnimy strukturę bazy danych, w bazie pojawi się wówczas tabela o nazwie
ext_translations. W tabeli tej będą zapisywane wszystkie tłumaczenia.
Następnie w klasach Entity, które mają zawierać teksty w wielu językach, dodajemy
przedstawione na listingu 24.2 właściwość $locale oraz metodę setTranslatableLocale().
Właściwość $locale nie będzie zapisywana w bazie danych, dlatego nie ma ona adnotacji
@ORM. Metodą setTranslatableLocale() będziemy ustalali język, na podstawie którego
z tabeli ext_translations będą wybierane rekordy.
Właściwość name ma zostać przetłumaczona na kilka języków, np. angielski (red) oraz
włoski (rosso). W tym celu należy wykonać kod przedstawiony na listingu 24.4.
$Color->setTranslatableLocale('en');
$Color->setName('red');
$manager->persist($Color);
$manager->flush();
$Color->setTranslatableLocale('it');
Rozdział 24. ♦ Zachowanie translatable 285
$Color->setName('rosso');
$manager->persist($Color);
$manager->flush();
Rekord w języku polskim zapisujemy tak jak dotychczas. W celu zapisania tłumaczenia
na język angielski najpierw wywołujemy metodę setTranslatableLocale():
$Color->setTranslatableLocale('en');
Rysunek 24.1.
Zawartość tabeli color
po wykonaniu kodu
z listingu 24.4
Rysunek 24.2.
Zawartość tabeli
ext_translations
po wykonaniu kodu
z listingu 24.4
Analizując trzeci rekord z rysunku 24.2, możemy stwierdzić, że słowo rogue jest francu-
skim tłumaczeniem kolumny name z tabeli rekordu o kluczu głównym 1, zapisanego w tabeli
odpowiadającej klasie:
My\FrontendBundle\Entity\Color
Odczytywanie tłumaczeń
W celu pobrania z bazy danych tłumaczenia kolumn obiektu należy wywołać metody
setTranslatableLocale() oraz refresh(). Najpierw, wykorzystując obiekt EntityManager,
wyszukujemy w bazie danych dowolny rekord:
$entity = $em->getRepository('MyFrontendBundle:Color')->find(1);
Jeśli teraz przekażemy obiekt $entity do widoku i wydrukujemy wartość kolumny podle-
gającej tłumaczeniu, np. name:
{{ entity.name }}
wówczas w roli wartości właściwości name użyta zostanie wartość odczytana z odpo-
wiedniego rekordu tabeli ext_translations.
zielony:
rgb: 00FF00
name: zielony
translations:
pl: zielony
en: green
it: verde
fr: vert
de: grün
es: verde
ru:
W pliku tym występuje pewna liczba nazw kolorów przetłumaczonych na języki: polski,
angielski, włoski, francuski, niemiecki, hiszpański oraz rosyjski. Napisz aplikację, która
będzie prezentowała listę wszystkich kolorów zapisanych w bazie danych w wybranym
języku. W interfejsie witryny dodaj odsyłacze pozwalające na wybór języka. Po wybraniu
języka rosyjskiego na stronie WWW należy wyświetlić nazwy kolorów:
красный,
голубой.
ROZWIĄZANIE
Krok 1. Przygotuj nowy projekt
Rozpakuj dystrybucję symfony2-customized-v3.zip , którą wykonaliśmy w rozdziale 21.
W wypakowanym folderze umieść folder data/kolory.yml. Następnie utwórz nowy
pakiet My/FrontendBundle.
Zawierający pola:
rgb typu string o długości 16 znaków,
name typu string o długości 255 znaków.
namespace My\FrontendBundle\DataFixtures\ORM;
use Doctrine\Common\Persistence\ObjectManager;
use My\FrontendBundle\Entity\Color;
use Symfony\Component\Yaml\Yaml;
Po wykonaniu skryptu z listingu 24.6. baza danych colors powinna zawierać w tabeli
ext_translations rekordy widoczne na rysunku 24.2.
homepage:
pattern: /{culture}
defaults: { _controller: MyFrontendBundle:Default:index, culture: pl }
requirements:
culture: pl|en|fr|ru|de|es|it
Wartością domyślną zmiennej culture będzie ciąg znaków pl. Oba poniższe adresy są
równoważne:
.../web/
.../web/pl
.../web/pl
.../web/en
.../web/fr
...
if ($culture != 'pl') {
foreach ($entities as $entity) {
$entity->setTranslatableLocale($culture);
$em->refresh($entity);
}
}
return array('entities' => $entities);
}
}
Wartość parametru $culture będzie pochodziła z adresu URL odwiedzanej strony. Jeśli
odwiedzimy stronę o adresie:
.../web/fr
wówczas, zgodnie z regułą z listingu 24.7, zmienna $culture przyjmie wartość fr.
Następnie sprawdzamy, czy wartość zmiennej $culture jest różna od wartości pl:
if ($culture != 'pl') {
...
}
Rozdział 24. ♦ Zachowanie translatable 291
Widok akcji index zawiera menu główne oraz tabelkę z listą wszystkich rekordów z tabeli
color. Do wydrukowania opcji menu wykorzystujemy funkcję pomocniczą path(),
przekazując do niej nazwę reguły oraz tablicę asocjacyjną:
<a href="{{ path('homepage', {'culture': 'pl'}) }}">pl</a>
W tablicy:
{'culture': 'pl'}
Pętla for drukująca zawartość tabeli jest identyczna jak w przykładach, które nie stoso-
wały zachowań translatable.
Przykład z rozdziału 24. został wykonany w Symfony 2.0.9. W wersji 2.0.10 wprowa-
dzono modyfikacje, które na pewien czas spowodowały, że zachowanie translatable
nie działało poprawnie. Błędy te zostaną najprawdopodobniej usunięte w kolejnych wer-
sjach Symfony.
Rozdział 25.
Podsumowanie części V
Poznawanie zachowań Doctrine rozpoczęliśmy od zainstalowania biblioteki Doctrine
´Extensions oraz pakietu StofDoctrineExtensionsBundle. Pamiętaj, że implementacja
zachowań jest zawarta w bibliotece DoctrineExtensions. Zadaniem pakietu StofDoctrine
´ExtensionsBundle jest wyłącznie ułatwianie konfiguracji zachowań.
Przykłady opisane w rozdziałach 22., 23. oraz 24. demonstrują użycie zachowań:
sluggable,
timestampable
oraz translatable.
294 Część V ♦ Zachowania Doctrine
Część VI
Szczegółowe
dane rekordu
296 Część VI ♦ Szczegółowe dane rekordu
Rozdział 26. ♦ Akcja show 297
Rozdział 26.
Akcja show
W celu wykonania strony ze szczegółowymi danymi rekordu musimy umieć:
tworzyć adresy URL zawierające zmienne;
przekazywać do kontrolera zmienne identyfikujące rekord;
generować adresy URL zawierające zmienne;
wyszukiwać zadany rekord w bazie danych;
sprawdzać, czy podany rekord został odnaleziony;
w przypadku gdy wyszukiwanie zakończy się błędem, generować stronę błędu;
w przypadku gdy wyszukiwanie zakończy się sukcesem, przekazywać zadany
rekord do widoku;
w widoku wyświetlać pola rekordu.
oraz parametr:
name="akcja_ipsum
Konwersja wejściowa
Po odwiedzeniu adresu:
/moja/strona-xyz/index.html
Konwersja wyjściowa
W celu wydrukowania adresu:
/moja/strona-pqr123/index.html
W celu wyszukania w tabeli lorem rekordu o identyfikatorze 123 należy w akcji użyć
instrukcji:
Rozdział 26. ♦ Akcja show 299
$em = $this->getDoctrine()->getEntityManager();
$entity = $em->getRepository('MyFrontendBundle:Lorem')->find(123);
O tym, czy rekord został znaleziony, czy nie, decyduje wartość zmiennej $entity. Jeśli jest
to logiczny fałsz, rekord nie został odnaleziony. W takiej sytuacji przechodzimy na stronę
błędu 404:
if (!$entity) {
throw $this->createNotFoundException('Brak rekordu!');
}
<title>Piechota</title>
<contents>
Maszerują strzelcy, maszerują,
Karabiny błyszczą, szary strój,
A przed nimi drzewce salutują,
Bo za naszą Polskę idą w bój!
...
</contents>
</song>
<song>
<title>O mój rozmarynie</title>
<contents>
O mój rozmarynie, rozwijaj się
O mój rozmarynie, rozwijaj się
Pójdę do dziewczyny, pójdę do jedynej
...
</contents>
</song>
...
</songs>
Liczby użyte w adresach (np. /web/3.html) mają być wartościami kluczy głównych odpo-
wiednich rekordów.
ROZWIĄZANIE
Krok 1. Utwórz nowy projekt i pakiet
W folderze przeznaczonym na aplikacje WWW utwórz folder zad-26/ i wypakuj do niego
zawartość archiwum symfony2-customized-v3.zip wykonanego w rozdziale 21.
Komendą:
php app/console generate:bundle --namespace=My/FrontendBundle --dir=src --no-interaction
Rozdział 26. ♦ Akcja show 301
Kod metody fromArray() będzie identyczny jak na listingu 19.6. Metoda __toString()
powinna zwracać wartość właściwości title. Przykładowa metoda __toString() jest
przedstawiona na listingu 19.5.
Po wykonaniu skryptu z listingu 26.3. dla danych z pliku songs.xml baza danych powinna
zawierać 9 rekordów.
{% block body %}
<div id="pojemnik">
<h1 id="logo"><a href="{{ path('homepage') }}">Piosenki wojskowe</a></h1>
Rozdział 26. ♦ Akcja show 303
{% block content %}
<ul id="menu">
{% for entity in entities %}
<li>
<a href="{{ path('song_show', { 'id': entity.id }) }}">
{{ entity }}
</a>
</li>
{% endfor %}
</ul>
{% endblock %}
304 Część VI ♦ Szczegółowe dane rekordu
if (!$entity) {
throw $this->createNotFoundException('Podana strona nie istnieje!');
}
Adnotacja @Route() zawiera pojemnik {id} oraz parametr name o wartości show_page:
@Route("/{id}.html", name="song_show")
{% block html_title %}
{{ entity }}
{% endblock %}
{% block content %}
<div id="tresc">
<h2>{{ entity }}</h2>
<p>
{{ entity.contents|nl2br }}
</p>
Rozdział 26. ♦ Akcja show 305
</div>
{% endblock %}
Zmienna $entity przekazana do widoku jest obiektem klasy Song. Dlatego w widoku akcji
show możemy stosować odwołania:
{{ entity }}
{{ entity.contents|nl2br }}
Użyj adresów:
.../web/czerwone-maki.html
.../web/deszcz-jesienny-deszcz.html
.../web/dnia-pierwszego-wrzesnia.html
...
ROZWIĄZANIE
Krok 1. Rozpakuj gotowy przykład 26.1
W folderze przeznaczonym na aplikacje WWW utwórz folder zad-27/ i wypakuj do
niego zawartość archiwum 27-start.zip. Zawiera ono kompletny przykład wykonany
w rozdziale 26.
1
Jeśli zapomnisz o wydaniu poniższej komendy, w widokach odwołanie do kolumn slug, np. {{ entity.slug }},
będzie zwracało pusty ciąg znaków.
310 Część VI ♦ Szczegółowe dane rekordu
Przygotuj aplikację internetową, która będzie prezentowała treny w postaci witryny in-
ternetowej. Treść wszystkich trenów umieść w bazie danych. Zadanie wykonaj w taki
sposób, by na każdej stronie serwisu widoczne było menu główne pozwalające na przej-
ście do dowolnego trenu.
ROZWIĄZANIE
Krok 1. Utwórz nowy projekt i pakiet
W folderze przeznaczonym na aplikacje WWW utwórz folder zad-28/ i wypakuj do niego
zawartość archiwum symfony2-customized-v3.zip wykonanego w rozdziale 21. W pro-
jekcie utwórz pakiet My/FrontendBundle oraz umieść folder zad-28/data/ zawierający pliki
z trenami. Następnie w konfiguracji projektu włącz zachowania sluggable.
$numer = basename($plk);
$numer = str_replace('.txt', '', $numer);
$numer = ltrim($numer, '0');
$manager->persist($Tren);
}
$manager->flush();
}
}
Tablica $t zawiera w każdym elemencie jeden wiersz pliku. Tytuł trenu jest zawarty
w pierwszym wierszu. Funkcja array_shift() odrywa od tablicy jej pierwszy element
i zwraca jako wynik. Po wywołaniu:
$tytul = trim(array_shift($t));
zmienna $tytul zawiera pierwszy wiersz z pliku, zaś w zmiennej $t wiersz ten już nie
występuje. Jeśli teraz tablicę $t przekształcimy funkcją implode() w ciąg znaków:
$tresc = trim(implode('', $t));
Następnie funkcją str_replace() usuwamy rozszerzenie nazwy pliku (otrzymamy np. 05):
$numer = str_replace('.txt', '', $numer);
Zwróć uwagę, że menu główne witryny jest generowane w pliku layout.html.twig wy-
wołaniem {% render ...%}.
1
Treść pliku dedykacja.txt umieścimy w widoku akcji.
316 Część VI ♦ Szczegółowe dane rekordu
/**
* Lists all Tren entities as ul/li/a menu.
*
* @Template()
*/
public function menuAction()
{
$em = $this->getDoctrine()->getEntityManager();
$entities = $em->getRepository('MyFrontendBundle:Tren')->findAll();
return array('entities' => $entities);
}
}
Dzięki wywołaniu funkcji shuffle() kolejność rekordów w bazie danych będzie losowa.
Za ustalenie odpowiedniej kolejności odpowiada kolumna numer oraz nadpisana metoda
findAll() w klasie TrenRepository.
<p>
{{ entity.tresc | nl2br }}
</p>
{% endblock %}
1
Oczywiście rozwiązanie takie jest nieoptymalne. Dane binarne przekonwertowane do formatu
tekstowego zajmą około 1/3 miejsca więcej.
2
Por. http://docs.doctrine-project.org/projects/doctrine-orm/en/2.1/reference/basic-mapping.html.
320 Część VI ♦ Szczegółowe dane rekordu
ROZWIĄZANIE
Krok 1. Utwórz nowy projekt i pakiet
W folderze przeznaczonym na aplikacje WWW utwórz folder zad-29-01/ i wypakuj do
niego zawartość archiwum symfony2-customized-v3.zip. W projekcie utwórz pakiet
My/FrontendBundle oraz umieść folder data/ zawierający kilka plików różnych typów.
Pliki danych oraz szablon HTML/CSS znajdziesz w archiwum 29-01-start.zip.
W skrypcie z listingu 29.1 przetwarzamy iteracyjnie wszystkie pliki z folderu data/. Nazwę
pliku ustalamy, wywołując funkcję basename():
$nazwa = basename($filename):
Po wykonaniu skryptu z listingu 29.1 w bazie danych pojawią się rekordy widoczne na
rysunku 29.1.
Akcja index ma wyświetlać listę wszystkich rekordów z tabeli file. Akcja show odpowiada
za wysłanie pojedynczego pliku. Treść zmodyfikowanego pliku DefaultController.php
jest widoczna na listingu 29.2.
322 Część VI ♦ Szczegółowe dane rekordu
Rysunek 29.1. Rekordy wstawione do bazy danych download skryptem z listingu 29.1
W akcji show ustalamy regułę routingu, która będzie używana do generowania adresów
URL plików zapisanych w bazie danych:
@Route("/download/{filename}", name="file_show")
W kodzie akcji najpierw w tabeli file wyszukujemy rekord, dla którego wartość kolumny
filename odpowiada wartości parametru. Służy do tego metoda findOneByFilename():
$entity = $em->getRepository('MyFrontendBundle:File')-
>findOneByFilename($filename);
Zwróć uwagę, że akcja show nie jest poprzedzona adnotacją @Template. Akcja ta nie ma
własnego widoku. Wynikiem metody pozbawionej adnotacji @Template powinien być
obiekt klasy Response, który zwracamy jako wynik akcji:
return $response;
{% block content %}
<h1>Pliki do pobrania</h1>
<table>
<thead>
<tr>
<th>Plik</th>
</tr>
</thead>
<tbody>
324 Część VI ♦ Szczegółowe dane rekordu
Rysunek 29.2.
Witryna z przykładu 29.1
Rozdział 29. ♦ Udostępnianie plików binarnych 325
ROZWIĄZANIE
Krok 1. Zmodyfikuj klasę File.php oraz bazę danych
W klasie File.php usuń właściwość contents oraz metody getContents() i setContents().
W bazie danych zapisujemy wyłącznie dwie informacje: nazwę pliku oraz typ MIME.
Po uruchomieniu skryptu wypełniającego bazę danych zawartość tabeli file będzie taka
jak na rysunku 29.3.
Rysunek 29.3.
Zawartość tabeli file
w przykładzie 29.2
Omówienie akcji show rozpoczęliśmy od informacji na temat adresów URL. Jeśli mamy
pokazać na stronie szczegółowe dane pojedynczego rekordu, musimy wówczas przeka-
zać do metody akcji identyfikator wskazujący, o który rekord chodzi. W rozdziale 26. do
identyfikacji rekordów użyliśmy klucza głównego, a w rozdziale 27. — automatycznie
wygenerowanego ciągu slug.
Rozdział 28. pokazał, w jaki sposób przygotować menu, którego pozycje są pobierane
z bazy danych. Każda pozycja menu wskazuje akcję show wyświetlającą szczegółowe dane
konkretnego rekordu. W identyczny sposób możemy wykonać różnorodne komponenty,
takie jak:
lista pięciu najnowszych artykułów w serwisie,
lista dziesięciu najbardziej popularnych produktów w sklepie,
lista wszystkich kategorii,
lista osób, które się ostatnio zalogowały.
Ostatni z rozdziałów tej części zademonstrował, w jaki sposób wykonać akcje, które
udostępniają dane binarne. Pierwsze z rozwiązań polega na zapisaniu udostępnianych
plików w bazie danych przy użyciu kodowania tekstowego. Takie podejście jest oczywi-
ście nieefektywne, gdyż rekordy zajmą znacznie więcej miejsca, co wpłynie także na
zwiększenie transferu danych pomiędzy aplikacją PHP a bazą danych. Znacznie lepszym
rozwiązaniem jest zapisanie w bazie danych wyłącznie nazw plików. W ten sposób
zachowujemy pełną kontrolę nad udostępnianym zasobem, nie obciążając niepotrzebnie
bazy danych.
328 Część VI ♦ Szczegółowe dane rekordu
Część VII
Relacje
330 Część VII ♦ Relacje
Rozdział 31. ♦ Relacje 1:1 331
Rozdział 31.
Relacje 1:1
Relacja 1:1 wiąże jeden rekord z pierwszej tabeli z jednym rekordem z drugiej tabeli.
Powiązanie takie jest realizowane przez dodanie klucza obcego (ang. foreign key)
w pierwszej tabeli.
Jako przykład ilustrujący relację jeden do jednego przeanalizujmy bazę danych zawiera-
jącą dane o użytkownikach. Każdy użytkownik będzie opisany przez dwie właściwości:
name oraz info. Właściwość name zapiszemy w tabeli user, zaś właściwość info — w ta-
beli profil. W praktyce w tabeli user zapisywane są dane takie jak nazwa konta i hasło,
a w tabeli profil — adres, płeć, ikona użytkownika, ustawienia osobiste itd.
Powiązanie rekordów z tabel user oraz profil relacją jeden do jednego polega na tym,
że każdemu rekordowi z tabeli user przyporządkujemy dokładnie jeden rekord z tabeli
profil. Tabela user jest tabelą źródłową relacji (ang. source table lub parent table),
a tabela profil — tabelą docelową (ang. destination table lub dependent table). W tabeli
źródłowej relacji 1:1 (czyli w tabeli user) dodajemy klucz obcy profil_id wskazujący,
z którym rekordem z tabeli profil powiązany jest użytkownik.
Struktura tabel user oraz profil po dodaniu klucza obcego relacji 1:1 jest przedstawiona
na listingu 31.1.
Tabela profil
id - klucz główny typu integer
info - string o długości 255 znaków
z tabeli profil.
Listing 31.4. Właściwość $profil i adnotacja definiująca powiązanie tabel user oraz profil relacją 1:1
//fragment pliku Entity\User.php
/**
Rozdział 31. ♦ Relacje 1:1 333
*
* @ORM\OneToOne(targetEntity="Profil")
*/
private $profil;
jest równoważna:
@ORM\OneToOne(targetEntity="Profil")
@ORM\JoinColumn(name="profil_id", referencedColumnName="id")
Parametr name ustala nazwę kolumny dla klucza obcego w tabeli źródłowej, a pa-
rametr referencedColumnName — nazwę kolumny w tabeli docelowej.
Listing 31.5. Metody klasy User wygenerowane automatycznie dla właściwości z listingu 31.4
//fragment pliku Entity\User.php
/**
* Set profil
*
* @param My\FrontendBundle\Entity\Profil $profil
*/
public function setProfil(\My\FrontendBundle\Entity\Profil $profil)
{
$this->profil = $profil;
}
/**
* Get profil
*
* @return My\FrontendBundle\Entity\Profil
*/
public function getProfil()
{
return $this->profil;
}
Domyślnie adnotacja @ORM\OneToOne pozwala na użycie wartości NULL dla klucza obcego.
W celu wykluczenia takiej możliwości należy w adnotacji definiującej klucz obcy dodać
parametr nullable o wartości false. Przykład użycia parametru nullable dla relacji 1:1
jest przedstawiony na listingu 31.6.
334 Część VII ♦ Relacje
Operowanie rekordami
powiązanymi relacją
Tworzenie rekordów
W celu utworzenia rekordów i powiązania ich relacją należy wykorzystać metodę set()
z listingu 31.5. Listing 31.7 ilustruje procedurę wstawiania do bazy danych rekordów
Piotr oraz Nic ważnego.
$User->setProfil($Profil);
$manager->flush();
Jeśli wstawiając rekord do tabeli user, nie powiążemy go z żadnym rekordem w tabeli
profil, wówczas klucz obcy profil_id otrzyma wartość NULL. Po wykonaniu kodu z li-
stingu 31.8 w bazie danych pojawi się rekord:
id profil_id name
X NULL Paweł
Jeśli klucz obcy ma parametr nullable o wartości false, to kod z listingu 31.8 wygene-
ruje wyjątek.
Rozdział 31. ♦ Relacje 1:1 335
Rekord zależny
Jeśli dysponujemy obiektem z tabeli źródłowej User:
$User = $em->getRepository('MyFrontendBundle:User')->find(2);
wówczas w celu zyskania dostępu do powiązanego z nim rekordu w tabeli profil należy
wywołać metodę getProfil():
$Profil = $User->getProfil();
ROZWIĄZANIE
Krok 1. Utwórz nowy projekt i pakiet
Utwórz folder zad-31-01/ i wypakuj do niego zawartość archiwum symfony2-customized-
-v3.zip. Następnie utwórz pakiet My/FrontendBundle, po czym do folderu zad-31-01/data/
przekopiuj plik users.xml. Plik ten znajdziesz w archiwum 31-01-start.zip.
W klasie User dodaj jedną kolumnę name typu string o długości 255 znaków.
W klasie Profil dodaj jedną kolumnę info typu string o długości 255 znaków.
Spowoduje ono utworzenie tabel user oraz profil. Za pomocą programu phpMyAdmin
przekonaj się, że w tabeli user występuje klucz obcy o nazwie profil_id.
W bazie danych pojawi się sześć rekordów (trzy w tabeli user oraz trzy w tabeli profil).
$User->setProfil($Profil);
}
$manager->flush();
}
}
{% block body %}
<h1>Lista wszystkich użytkowników</h1>
<table>
<tr>
<th>Nazwa</th>
<th>Informacje</th>
</tr>
{% for entity in entities %}
<tr>
<td>{{ entity.name }}</td>
<td>{{ entity.profil.info }}</td>
</tr>
{% endfor %}
</table>
{% endblock %}
338 Część VII ♦ Relacje
Rysunek 31.1.
Witryna z przykładu 31.1
Listing 31.13. Akcja referencyjna ON DELETE CASCADE dla klucza obcego profil_id z tabeli user
ALTER TABLE `user` ADD CONSTRAINT `FK_xxx`
FOREIGN KEY (`profil_id`) REFERENCES `profil` (`id`) ON DELETE CASCADE;
Po dodaniu akcji referencyjnej z listingu 31.13 usunięcie rekordu z tabeli docelowej profil
spowoduje automatyczne usunięcie powiązanego z nim rekordu z tabeli źródłowej.
1
Źródło: http://en.wikipedia.org/wiki/Foreign_key.
Rozdział 31. ♦ Relacje 1:1 339
W Doctrine 2.1 w celu zdefiniowania akcji referencyjnej dla relacji 1:1 należy w adnotacji
@ORM\JoinColumn dodać przedstawiony na listingu 31.14 parametr onDelete.
Listing 31.14. Definicja akcji referencyjnej Doctrine, która odpowiada za wygenerowanie kodu
z listingu 31.13
/**
*
* @ORM\OneToOne(targetEntity="Profil")
* @ORM\JoinColumn(name="profil_id", referencedColumnName="id", onDelete="cascade")
*/
private $profil;
Parametr cascade
Akcję referencyjną do automatycznego usuwania rekordów z tabeli źródłowej definiujemy
przedstawionym na listingu 31.15 parametrem cascade adnotacji @ORM\OneToOne.
Skutek użycia parametru cascade z listingu 31.15 będzie identyczny jak skutek użycia pa-
rametru onDelete z listingu 31.14. Po usunięciu rekordu z tabeli profil automatycznie
zostanie usunięty powiązany z nim rekord z tabeli user. Kod z listingu 31.14 będzie
wykonywany na poziomie bazy danych, a kod z listingu 31.15 — na poziomie klasy
Doctrine.
Parametr orphanRemoval
Jeśli w adnotacji @ORM\OneToOne dodamy przedstawiony na listingu 31.16 parametr
orphanRemoval, usuwanie rekordu z tabeli źródłowej będzie powodowało automatyczne
usuwanie rekordu z tabeli docelowej. Operacja taka będzie realizowana programowo
na poziomie klasy Doctrine.
Listing 31.16. Adnotacja definiująca akcję automatycznego usuwania rekordu z tabeli docelowej po
usunięciu rekordu z tabeli źródłowej
/**
*
* @ORM\OneToOne(targetEntity="Profil", orphanRemoval=true)
*/
private $profil;
Relacje jednokierunkowe
i dwukierunkowe
W Doctrine 2.1 występują pojęcia relacji jednokierunkowych (ang. unidirectional)
oraz dwukierunkowych (ang. bidirectional). Relacja jednokierunkowa ma zaimplemen-
towaną obsługę powiązania relacyjnego wyłącznie w jednej z klas uczestniczących w relacji.
Relacja dwukierunkowa ma zaimplementowaną obsługę powiązań relacyjnych w obu
klasach uczestniczących w relacji.
/**
*
* @ORM\OneToOne(targetEntity="Profil", inversedBy="user")
*/
private $profil;
/**
* Set profil
*
* @param My\FrontendBundle\Entity\Profil $profil
*/
public function setProfil(\My\FrontendBundle\Entity\Profil $profil)
{
$this->profil = $profil;
}
/**
* Get profil
*
* @return My\FrontendBundle\Entity\Profil
*/
public function getProfil()
{
return $this->profil;
}
...
}
/**
* @ORM\OneToOne(targetEntity="User", mappedBy="profil")
*/
private $user;
/**
* Set user
*
* @param My\FrontendBundle\Entity\User $user
*/
public function setUser(\My\FrontendBundle\Entity\User $user)
342 Część VII ♦ Relacje
{
$this->user = $user;
}
/**
* Get user
*
* @return My\FrontendBundle\Entity\User
*/
public function getUser()
{
return $this->user;
}
...
}
//tabela profil
id info
123 Info Alana...
Listing 31.19. Zapisywanie w bazie danych powiązanych rekordów (powiązanie uaktualniamy po stronie
będącej właścicielem relacji, czyli w obiektach klasy User)
$User = new User();
$User->setName('Alan');
$manager->persist($User);
$User->setProfil($Profil);
$manager->flush();
Jeśli spróbujemy powiązać obiekty relacją, wywołując metody dla obiektu klasy Profil,
która jest klasą odwrotną dla relacji, wówczas powiązanie rekordów nie zostanie zapisane
w bazie danych. Zmiany po stronie klasy odwrotnej zostaną utracone. Po wykonaniu kodu
z listingu 31.20 w bazie danych pojawią się rekordy:
//tabela user
id profil_id name
X NULL Peter
//tabela profil
id info
Y Info Petera...
$Profil->setUser($User);
$manager->flush();
Listing 31.21. Metoda set profil(), która zapewnia dwukierunkową synchronizację obiektów
class Profil
{
...
/**
* Set user
*
* @param My\FrontendBundle\Entity\User $user
*/
public function setUser(\My\FrontendBundle\Entity\User $user)
{
$this->user = $user;
$user->setProfil($this);
}
...
}
344 Część VII ♦ Relacje
//tabela profil
id info
456 Info Petera...
Rozdział 32.
Relacje 1:n
(jeden do wielu)
Relacja 1:n wiąże jeden rekord z pierwszej tabeli z wieloma rekordami z drugiej tabeli.
Powiązanie takie jest realizowane przez dodanie klucza obcego (ang. foreign key) w tabeli
docelowej.
Jako przykład ilustrujący relację jeden do wielu przeanalizujmy bazę danych zawierającą
zestawienie kontynentów i państw. W bazie danych występują dwie tabele: kontynent
oraz panstwo o identycznej strukturze. Zawierają one klucz główny i kolumnę nazwa.
Tabele kontynent i panstwo są przedstawione na listingu 32.1.
Tabela panstwo
id - klucz główny typu integer
nazwa - string o długości 255 znaków
kontynent_id - klucz obcy
Tabela kontynent jest tabelą źródłową relacji (ang. source table lub parent table), a tabela
panstwo — tabelą docelową (ang. destination table lub dependent table). W tabeli do-
celowej relacji 1:n (czyli w tabeli panstwo) dodajemy klucz obcy kontynent_id wskazu-
jący, z którym kontynentem jest powiązane dane państwo.
346 Część VII ♦ Relacje
Po dodaniu klucza obcego w tabeli panstwo występują trzy kolumny: id, nazwa oraz
kontynent_id. Oto przykładowe rekordy:
id nazwa kontynent_id
1 Polska 1
2 Mongolia 2
3 Niemcy 1
4 Francja 1
5 Nigeria 3
6 Chiny 2
Klucz obcy kontynent_id informuje o tym, z jakiego kontynentu pochodzi dane państwo.
Polska pochodzi z kontynentu, dla którego kontynent_id = 1. Po sprawdzeniu zawartości
tabeli kontynent stwierdzamy, że kontynentem tym jest Europa. Mongolia pochodzi z kon-
tynentu, dla którego kontynent_id = 2, czyli z Azji. I tak dalej.
Listing 32.2. Właściwość i adnotacja definiujące relację 1:n w klasie źródłowej kontynent
class Kontynent
{
...
/**
* @ORM\OneToMany(targetEntity="Panstwo", mappedBy="kontynent")
*/
protected $panstwa;
...
}
Listing 32.3. Właściwość i adnotacja definiujące relację 1:n w klasie docelowej panstwo
class Panstwo
{
...
/**
* @ORM\ManyToOne(targetEntity="Kontynent", inversedBy="panstwa")
*/
protected $kontynent;
...
}
Po dodaniu w klasach Kontynent oraz Panstwo właściwości z listingów 32.2 oraz 32.3
wydajemy polecenie:
php app/console generate:doctrine:entities My
Spowoduje ono dodanie w klasie Kontynent metod przedstawionych na listingu 32.4 oraz
w klasie Panstwo metod przedstawionych na listingu 32.5.
Listing 32.4. Metody klasy Kontynent wygenerowane automatycznie dla właściwości z listingu 32.2
class Kontynent
{
...
/**
* Add panstwa
*
* @param My\FrontendBundle\Entity\Panstwo $panstwa
348 Część VII ♦ Relacje
*/
public function addPanstwo(\My\FrontendBundle\Entity\Panstwo $panstwa)
{
$this->panstwa[] = $panstwa;
}
/**
* Get panstwa
*
* @return Doctrine\Common\Collections\Collection
*/
public function getPanstwa()
{
return $this->panstwa;
}
...
}
Listing 32.5. Metody klasy Panstwo wygenerowane automatycznie dla właściwości z listingu 32.3
class Panstwo
{
...
/**
* Set kontynent
*
* @param My\FrontendBundle\Entity\Kontynent $kontynent
*/
public function setKontynent(\My\FrontendBundle\Entity\Kontynent $kontynent)
{
$this->kontynent = $kontynent;
}
/**
* Get kontynent
*
* @return My\FrontendBundle\Entity\Kontynent
*/
public function getKontynent()
{
return $this->kontynent;
}
...
}
Domyślnie adnotacje z listingów 32.2 oraz 32.3 pozwalają na użycie wartości NULL dla
klucza obcego kontynent_id w tabeli panstwo. W celu wykluczenia takiej możliwości
należy w adnotacji definiującej klucz obcy dodać parametr nullable o wartości false.
Przykład użycia parametru nullable dla relacji 1:n jest przedstawiony na listingu 32.6.
Rozdział 32. ♦ Relacje 1:n (jeden do wielu) 349
/**
* @ORM\ManyToOne(targetEntity="Kontynent", inversedBy="panstwa")
* @ORM\JoinColumn(name="kontynent_id", referencedColumnName="id",
nullable=false)
*/
protected $kontynent;
...
}
Operowanie rekordami
powiązanymi relacją
Tworzenie rekordów
Listing 32.7 ilustruje procedurę wstawiania do bazy danych rekordów Europa oraz Polska.
Zwróć uwagę, że powiązanie obiektów wykonujemy, wywołując metodę setKontynent()
klasy Panstwo. Użycie metody addPanstwo() klasy Kontynent będzie błędem: zmiany
dotyczące relacji i wykonane w obiektach klasy Kontynent zostaną utracone.
$manager->flush();
zakończy się błędem informującym o tym, że wartość klucza obcego nie została nadana.
Rekordy zależne
Widoczna na listingu 32.4 metoda getPanstwa() zwraca kolekcję obiektów klasy Panstwo
powiązanych relacyjnie z obiektem klasy Kontynent, dla którego wywołano metodę. Nazwa
metody getPanstwa() powstaje na podstawie nazwy właściwości z listingu 32.2.
$Kontynent = $manager
->getRepository('MyFrontendBundle:Kontynent)
->findOneByNazwa('Europa');
$panstwa = $Kontynent->getPanstwa();
foreach ($panstwa as $Panstwo) {
echo $panstwo->getNazwa();
}
Rozdział 32. ♦ Relacje 1:n (jeden do wielu) 351
Rekord nadrzędny
W klasie Panstwo występuje widoczna na listingu 32.5 metoda getKontynent(), która
zwraca kontynent powiązany z danym państwem. Wydruk szczegółowych danych
Polski jest przedstawiony na listingu 32.9.
$Panstwo = $manager
->getRepository('MyFrontendBundle:Panstwo')
->findOneByNazwa('Polska');
echo $Panstwo->getNazwa();
echo $Panstwo->getKontynent()->getNazwa();
Synchronizacja relacji
W celu zapewnienia synchronizacji powiązań relacyjnych bez względu na to, czy mo-
dyfikacje wykonujemy w obiektach klasy Kontynent, czy klasy Panstwo, należy w me-
todzie addPanstwa() klasy Kontynent dodać widoczną na listingu 32.10 instrukcję
setKontynent().
/**
* Add panstwa
*
* @param My\FrontendBundle\Entity\Panstwo $panstwa
*/
public function addPanstwo(\My\FrontendBundle\Entity\Panstwo $panstwa)
{
$this->panstwa[] = $panstwa;
$panstwa->setKontynent($this);
}
...
}
352 Część VII ♦ Relacje
Akcje referencyjne
Akcje SQL-owe
Akcje referencyjne wykonywane na poziomie bazy danych definiujemy właściwością
onDelete adnotacji @ORM\JoinColumn. Przykład definicji SQL-owej akcji referencyjnej
dla tabeli panstwo jest widoczny na listingu 32.11.
/**
* @ORM\ManyToOne(targetEntity="Kontynent", inversedBy="panstwa")
* @ORM\JoinColumn(name="kontynent_id", referencedColumnName="id",
onDelete="cascade")
*/
protected $kontynent;
...
}
Jeśli definicja relacji wygląda tak jak na listingu 32.12, wówczas po wykonaniu in-
strukcji z listingu 32.12 z bazy danych zostaną usunięte:
kontynent Europa,
wszystkie państwa europejskie.
Akcje Doctrine
W celu zdefiniowania programowych akcji referencyjnych Doctrine należy do adnotacji
@ORM\OneToMany dodać parametr cascade. Definicja z listingu 32.13 spowoduje, że kod
z listingu 32.12 usunie zarówno Europę, jak i wszystkie państwa europejskie.
/**
* @ORM\OneToMany(targetEntity="Panstwo", mappedBy="kontynent", cascade={"all"})
*/
protected $panstwa;
...
}
Napisz skrypt, który bazę danych kontynenty wypełni danymi odczytanymi z pliku
kontynenty.xml.
Następnie wykonaj witrynę WWW, która będzie zawierała dwie strony: listę wszystkich
kontynentów oraz listę wszystkich państw. Listę wszystkich kontynentów wykonaj
jako stronę akcji index w kontrolerze Kontynent, a listę wszystkich państw jako stronę
akcji index w kontrolerze Panstwo.
ROZWIĄZANIE
Krok 1. Utwórz nowy projekt i pakiet
W folderze przeznaczonym na aplikacje WWW utwórz folder zad-32/ i wypakuj do
niego zawartość archiwum symfony2-customized-v3.zip. Następnie utwórz pakiet
My/FrontendBundle oraz w folderze data/ umieść plik kontynenty.xml.
Tak zdefiniowana relacja jest dwukierunkowa. Właścicielem relacji jest klasa Panstwo.
Polecenie:
php app/console generate:doctrine:entities My
spowoduje dodanie:
w klasie Kontynent metod: __construct(), addPanstwo() i getPanstwa();
w klasie Panstwo metod: setKontynent() i getKontynent().
$xml = simplexml_load_file('data/kontynenty.xml');
foreach ($xml->kontynent as $kontynent) {
$Kontynent = new Kontynent();
$Kontynent->setNazwa($kontynent->nazwa);
$manager->persist($Kontynent);
foreach ($kontynent->panstwa->panstwo as $panstwo) {
$Panstwo = new Panstwo();
$Panstwo->setNazwa($panstwo->nazwa);
$Panstwo->setKontynent($Kontynent);
$manager->persist($Panstwo);
}
}
$manager->flush();
}
}
<ul>
{% for kontynent in entities %}
<li>
{{ kontynent }}
<ul>
{% for panstwo in kontynent.panstwa %}
<li>
{{ panstwo }}
</li>
{% endfor %}
</ul>
</li>
{% endfor %}
</ul>
{% endblock %}
{% block body %}
<ul>
<li><a href="{{ path('homepage') }}">Strona główna</a></li>
<li><a href="{{ path('kontynent_index') }}">Lista kontynentów</a></li>
<li><a href="{{ path('panstwo_index') }}">Lista państw</a></li>
</ul>
{% block content %}
{% endblock %}
{% endblock %}
Rysunek 32.1.
Witryna
z przykładu 32.1
Porządkowanie rekordów
Kolekcja rekordów zależnych relacji 1:n, która jest zwracana przez metodę getPanstwa(),
może być automatycznie sortowana dowolnymi kolumnami. W tym celu należy w klasie
Kontynent dodać adnotację @ORM\OrderBy przedstawioną na listingu 32.19.
358 Część VII ♦ Relacje
/**
* @ORM\OneToMany(targetEntity="Panstwo", mappedBy="kontynent")
* @ORM\OrderBy({"nazwa" = "ASC"})
*/
protected $panstwa;
...
}
Powiązanie takie jest realizowane przez utworzenie dodatkowej tabeli, nazywanej ta-
belą łączącą relacji, w której zawarte są informacje o powiązaniach.
Jako przykład ilustrujący relację wiele do wielu przeanalizujmy bazę danych zawierającą
zestawienie aktorów i filmów. W bazie danych występują dwie tabele: aktor oraz film.
Tabela aktor zawiera kolumny:
id — klucz główny;
imie — string o długości 255 znaków;
nazwisko — string o długości 255 znaków.
Tabela film
id - klucz główny typu integer
tytul - string o długości 255
W celu powiązania rekordu Miś z rekordem Stanisław Tym należy w tabeli film_aktor
umieścić rekord:
film_id aktor_id
9 11
Powiązanie tabel film oraz aktor jest zilustrowane na listingach 33.2 oraz 33.3.
Rozdział 33. ♦ Relacje n:m (wiele do wielu) 361
/**
* @ORM\ManyToMany(targetEntity="Aktor", inversedBy="filmy")
*/
private $aktorzy;
...
}
/**
* @ORM\ManyToMany(targetEntity="Film", mappedBy="aktorzy")
*/
protected $filmy;
...
}
Po dodaniu w klasach Film oraz Kontynent właściwości z listingów 33.2 oraz 33.3
wydajemy polecenie:
php app/console generate:doctrine:entities My
Pierwszym członem jest nazwa klasy, która jest właścicielem relacji. Po wydaniu po-
lecenia:
php app/console doctrine:schema:create
W celu ustalenia innej nazwy dla tabeli łączącej należy w klasie, która jest właścicielem
relacji, dodać przedstawioną na listingu 33.4 adnotację @ORM\JoinTable.
/**
* @ORM\ManyToMany(targetEntity="Aktor", inversedBy="filmy")
* @ORM\JoinTable(name="film_has_aktor")
*/
private $aktorzy;
...
}
Operowanie rekordami
powiązanymi relacją
Tworzenie rekordów
Listing 33.5 ilustruje procedurę wstawiania do bazy danych rekordów Miś oraz Stanisław
Tym. Zwróć uwagę, że powiązanie obiektów wykonujemy, wywołując metodę addAktor()
klasy Film. Użycie metody addFilm() klasy Aktor będzie błędem: zmiany dotyczące
relacji i wykonane w obiektach klasy Aktor zostaną utracone.
Rozdział 33. ♦ Relacje n:m (wiele do wielu) 363
Nazwy metod addAktor() oraz addFilm() powstają na podstawie nazw klas Aktor
oraz Film.
Rekordy zależne
Listę rekordów klasy Aktor powiązanych z danym rekordem klasy Film zwraca metoda
getAktorzy(). Kod z listingu 33.6 drukuje imiona i nazwiska wszystkich aktorów wystę-
pujących w filmie pt. „Miś”.
Listę rekordów klasy Film powiązanych z danym rekordem klasy Aktor zwraca metoda
getFilmy(). Kod z listingu 33.7 drukuje tytuły wszystkich filmów, w których wystąpił
Robert Redford.
Synchronizacja relacji
W celu zapewnienia synchronizacji powiązań relacyjnych bez względu na to, czy mody-
fikacje wykonujemy w obiektach klasy Film, czy klasy Aktor, należy w metodzie addFilm()
klasy Aktor dodać widoczną na listingu 33.8 instrukcję addAktor().
/**
* Add filmy
*
* @param My\FrontendBundle\Entity\Film $filmy
*/
public function addFilm(\My\FrontendBundle\Entity\Film $filmy)
{
$this->filmy[] = $filmy;
$filmy->addAktor($this);
}
...
}
Listing 33.9. Usuwanie powiązania relacyjnego pomiędzy rekordami Wielki Gatsby oraz Robert Redford
$Film = $manager
->getRepository('MyFrontendBundle:Film')
->findOneByTytul('Wielki Gatsby');
$Aktor = $manager
->getRepository('MyFrontendBundle:Aktor')
->findOneBy(array('imie' => 'Robert', 'nazwisko' => 'Redford'));
Rozdział 33. ♦ Relacje n:m (wiele do wielu) 365
$Film->getAktorzy()->removeElement($Aktor);
$manager->flush();
Następnie wykonaj witrynę WWW, która będzie zawierała dwie strony: listę wszystkich
filmów oraz listę wszystkich aktorów. Listę wszystkich filmów wykonaj jako stronę akcji
index w kontrolerze Film, a listę wszystkich aktorów — jako stronę akcji index w kontro-
lerze Aktor.
Zadanie wykonaj w taki sposób, by na liście wszystkich filmów obok tytułu filmu wi-
doczna była lista wszystkich aktorów, którzy wystąpili w danym filmie. Na stronie z listą
wszystkich aktorów obok imienia i nazwiska każdego aktora umieść natomiast tytuły
filmów, w których wystąpił.
ROZWIĄZANIE
Krok 1. Utwórz nowy projekt i pakiet
W folderze przeznaczonym na aplikacje WWW utwórz folder zad-33/ i wypakuj do
niego zawartość archiwum symfony2-customized-v3.zip. Następnie utwórz pakiet
My/FrontendBundle oraz w folderze data/ umieść plik XML.
366 Część VII ♦ Relacje
Poleceniem:
php app/console generate:doctrine:entities My
W bazie danych pojawią się trzy tabele: film, aktor oraz film_aktor.
Rozdział 33. ♦ Relacje n:m (wiele do wielu) 367
W pliku XML każdy film występuje dokładnie jeden raz. Z aktorami sytuacja wyglą-
da jednak inaczej. Aktor, który występuje w dwóch filmach, pojawi się w pliku XML
dwukrotnie, co ilustruje listing 33.11.
Listing 33.11. Aktor, który występuje w dwóch filmach, pojawia się w pliku XML dwukrotnie
<filmy>
<film>
<tytul>Żądło</tytul>
<aktorzy>
<aktor>
<imie>Robert</imie>
<nazwisko>Redford</nazwisko>
</aktor>
...
</aktorzy>
</film>
<film>
<tytul>O jeden most za daleko</tytul>
<aktorzy>
<aktor>
<imie>Robert</imie>
<nazwisko>Redford</nazwisko>
</aktor>
...
368 Część VII ♦ Relacje
</aktorzy>
</film>
...
</filmy>
Wstawiając rekordy do tabeli aktor, musimy sprawdzić, czy podana osoba nie została
wcześniej wstawiona. Wyszukiwanie aktora realizuje instrukcja:
$Aktor = $manager
->getRepository('MyFrontendBundle:Aktor')
->findOneBy(array('imie' => $a->imie, 'nazwisko' => $a->nazwisko));
Jeśli podany rekord nie został odnaleziony, tworzymy nowy obiekt klasy Aktor:
if (!$Aktor) {
$Aktor = new Aktor();
$Aktor->setImie($a->imie);
$Aktor->setNazwisko($a->nazwisko);
$manager->persist($Aktor);
};
Jeśli metoda flush() nie zostanie wywołana, to nowo utworzony obiekt $Aktor nie zo-
stanie zapisany w bazie danych. W takiej sytuacji wszystkie instrukcje wyszukiwania:
...->findOneBy(...);
{% block content %}
<h2>Lista wszystkich filmów</h2>
<ul>
{% for film in entities %}
<li>
{{ film }}
<ul>
{% for aktor in film.aktorzy %}
<li>
{{ aktor }}
</li>
{% endfor %}
</ul>
</li>
{% endfor %}
</ul>
{% endblock %}
{% block body %}
<ul>
<li><a href="{{ path('homepage') }}">Strona główna</a></li>
<li><a href="{{ path('aktor_index') }}">Lista aktorów</a></li>
<li><a href="{{ path('film_index') }}">Lista filmów</a></li>
</ul>
{% block content %}
{% endblock %}
{% endblock %}
370 Część VII ♦ Relacje
Rysunek 33.1.
Witryna
z przykładu 33.1
Porządkowanie rekordów
Kolekcję rekordów zależnych relacji n:m możemy porządkować przedstawioną na listingu
33.15 adnotacją @ORM\OrderBy.
Listing 33.15. Automatyczne sortowanie rekordów zwracanych przez metodę getAktorzy() klasy Film
class Film
{
...
Rozdział 33. ♦ Relacje n:m (wiele do wielu) 371
/**
* @ORM\ManyToMany(targetEntity="Aktor", inversedBy="filmy")
* @ORM\OrderBy({"nazwisko"="ASC", "imie"="ASC"})
*/
private $aktorzy;
...
}
372 Część VII ♦ Relacje
Rozdział 34.
Relacje,
akcje index i show
oraz widoki częściowe
Wykonując akcje show dla rekordów powiązanych relacjami, natrafimy wielokrotnie
na problem redundancji kodu w widokach. Takie same pętle przetwarzające kolekcje
obiektów pojawią się w akcjach index rekordów zależnych i w akcjach show rekordów
nadrzędnych.
Przeanalizujmy przykład z rozdziału 32. Jeśli w aplikacji dla obu klas Kontynent
i Panstwo wykonamy akcje index oraz show prezentujące listę wszystkich rekordów
oraz szczegółowe dane rekordu, wówczas:
Na stronie akcji kontynent/show pojawi się lista państw z danego kontynentu.
Na stronie akcji panstwo/index pojawi się lista wszystkich państw.
Terminem:
kontynent/show
Listing 34.1. Widok prezentujący kolekcję obiektów klasy Panstwo w postaci listy hiperłączy
<ul>
{% for panstwo in dane %}
<li>
<a href="{{ path('panstwo_show', { 'id': panstwo.id }) }}">
{{ panstwo }}
374 Część VII ♦ Relacje
</a>
</li>
{% endfor %}
</ul>
Widok ten możemy umieścić w dowolnym widoku, przekazując do niego kolekcję obiek-
tów klasy Panstwo. Aby użyć widoku z listingu 34.1 w widoku akcji kontynent/show,
należy użyć instrukcji z listingu 34.2.
Listing 34.2. Użycie widoku z listingu 34.1 wewnątrz widoku akcji kontynent/show
{% block content %}
<h2>Szczegółowe dane kontynentu</h2>
<h3>Nazwa: {{ entity }}</h3>
<h4>Państwa z podanego kontynentu:</h4>
{% include 'MyFrontendBundle:Panstwo:_list.html.twig' with {'dane':
entity.panstwa } %}
{% endblock %}
Jeszcze więcej redundancji wystąpi w przypadku akcji show wykonanych dla relacji n:m.
Jeśli w przykładzie z rozdziału 33. dodamy akcje film/show oraz aktor/show, wówczas:
Lista tytułów filmów pojawi się na stronach akcji:
film/index (lista wszystkich filmów);
aktor/show (lista filmów, w których zagrał wybrany aktor).
Lista nazwisk aktorów pojawi się na stronach akcji:
aktor/index (lista wszystkich aktorów);
film/show (lista aktorów, którzy zagrali w danym filmie).
Ten sam widok będzie wywołany na stronie akcji film/show w sposób przedstawiony na
listingu 34.5.
Dane zawarte w pliku XML nie są posortowane. Zwróć uwagę, by na wszystkich stronach
aplikacji zestawienie powieści było posortowane alfabetycznie. Nie powielaj kodu odpowie-
dzialnego za prezentację listy tytułów powieści. Użyj do tego widoków częściowych.
ROZWIĄZANIE
Krok 1. Utwórz nowy projekt i pakiet
W folderze przeznaczonym na aplikacje WWW utwórz folder zad-34/ i wypakuj
do niego zawartość archiwum symfony2-customized-v3.zip. Następnie utwórz pakiet
My/FrontendBundle oraz w folderze data/ umieść plik XML.
/**
* @ORM\OneToMany(targetEntity="Novel", mappedBy="detective")
* @ORM\OrderBy({"title" = "ASC"})
*/
protected $novels;
/**
* @ORM\ManyToOne(targetEntity="Detective", inversedBy="novels")
*/
protected $detective;
...
}
Właściwości i adnotacje z listingów 34.3 oraz 34.4 definiują dwukierunkową relację 1:n,
w której właścicielem relacji jest klasa Novel.
Rozdział 34. ♦ Relacje, akcje index i show oraz widoki częściowe 379
/**
* @ORM\ManyToMany(targetEntity="Method", inversedBy="novels")
*/
private $methods;
...
}
/**
* @ORM\ManyToMany(targetEntity="Novel", mappedBy="methods")
* @ORM\OrderBy({"title" = "ASC"})
*/
protected $novels;
...
}
Właściwości i adnotacje z listingów 34.5 oraz 34.6 definiują dwukierunkową relację n:m,
w której właścicielem relacji jest klasa Novel.
W bazie danych pojawią się cztery tabele: novel, detective, method oraz novel_method.
$xml = simplexml_load_file('data/novels.xml');
foreach ($xml->novel as $n) {
$Detective = $manager
->getRepository('MyFrontendBundle:Detective')
->findOneByName($n->detective);
if (!$Detective) {
$Detective = new Detective();
$Detective->setName($n->detective);
$manager->persist($Detective);
$manager->flush();
};
if (!$Method) {
$Method = new Method();
$Method->setName($m);
$manager->persist($Method);
$manager->flush();
};
$Novel->addMethod($Method);
$manager->flush();
}
}
}
Rozdział 34. ♦ Relacje, akcje index i show oraz widoki częściowe 381
Po wykonaniu polecenia:
php app/console doctrine:fixtures:load
$entities = $em->getRepository('MyFrontendBundle:Novel')->findAll();
/**
* Finds and displays a Novel entity.
*
* @Route("/{slug}.html", name="novel_show")
* @Template()
*/
public function showAction($slug)
{
$em = $this->getDoctrine()->getEntityManager();
$entity = $em->getRepository('MyFrontendBundle:Novel')->findOneBySlug($slug);
if (!$entity) {
throw $this->createNotFoundException('Unable to find Novel entity.');
}
}
382 Część VII ♦ Relacje
Listing 34.12. Widok odpowiedzialny za prezentację kolekcji obiektów klasy Novel w postaci tabelki hiperłączy
<table class="records_list">
<thead>
<tr>
<th>Title</th>
</tr>
</thead>
<tbody>
{% for novel in entities %}
<tr>
<td>
<a href="{{ path('novel_show', { 'slug': novel.slug }) }}">
{{ novel }}
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
Listing 34.13. Widok odpowiedzialny za prezentację kolekcji obiektów klasy Detective w postaci
tabelki hiperłączy
<table class="records_list">
<thead>
<tr>
<th>Name</th>
</tr>
</thead>
<tbody>
{% for detective in entities %}
<tr>
<td>
<a href="{{ path('detective_show', { 'slug': detective.slug }) }}">
{{ detective }}
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
Rozdział 34. ♦ Relacje, akcje index i show oraz widoki częściowe 383
Listing 34.14. Widok odpowiedzialny za prezentację kolekcji obiektów klasy Method w postaci
tabelki hiperłączy
<table class="records_list">
<thead>
<tr>
<th>Name</th>
</tr>
</thead>
<tbody>
{% for method in entities %}
<tr>
<td>
<a href="{{ path('method_show', { 'slug': method.slug }) }}">
{{ method }}
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% block contents %}
<h2>Method</h2>
<table class="record_properties">
<tbody>
<tr>
<th>Name</th>
384 Część VII ♦ Relacje
Rozdział 36.
Generowanie
paneli administracyjnych
CRUD
Panel administracyjny CRUD1 zapewnia możliwość wykonania czterech rodzajów
operacji:
create — tworzenie nowych rekordów;
read/retrieve — odczytywanie rekordów;
update — uaktualnianie rekordów;
delete/destroy — usuwanie rekordów.
podamy nazwę:
MyFrontendBundle:Name
1
Szczegółowy opis operacji CRUD znajdziesz na stronie: http://pl.wikipedia.org/wiki/CRUD.
390 Część VIII ♦ Panele CRUD i zabezpieczanie dostępu do aplikacji
Dla modelu:
MyFrontendBundle:Name
wygenerowane zostaną:
kontroler:
src/My/FrontendBundle/Controller/NameController.php
widoki w folderze:
src/My/FrontendBundle/Resources/views/Name/
formularz:
src/My/FrontendBundle/Form/NameType.php
Akcje index oraz show, które występowały w części VI, możemy generować pole-
ceniem:
php app/console generate:doctrine:crud
Jeśli na pytanie:
Do you want to generate the "write" actions [no]? yes
Akcje index, show, new oraz edit będą miały własne widoki:
index.html.twig
show.html.twig
new.html.twig
edit.html.twig
Akcja delete kończy się przekierowaniem, dlatego nie stosuje żadnego widoku.
Listing 36.2. Adnotacje @Route akcji panelu CRUD wygenerowanego dla klasy Name
@Route("/", name="name")
public function indexAction()
@Route("/{id}/show", name="name_show")
public function showAction($id)
@Route("/new", name="name_new")
public function newAction()
@Route("/create", name="name_create")
public function createAction()
@Route("/{id}/edit", name="name_edit")
public function editAction($id)
@Route("/{id}/update", name="name_update")
public function updateAction($id)
@Route("/{id}/delete", name="name_delete")
public function deleteAction($id)
Akcja index
Adresem akcji index będzie:
.../web/name
Akcja show
Adresem akcji show dla rekordu o wartości klucza głównego 123 będzie:
.../web/name/123/show
Akcja new
Adresem akcji new będzie:
.../web/name/new
Akcja create
Adresem akcji create będzie:
.../web/name/create
Adres ten jest generowany w widoku akcji new.html.twig do wskazania akcji odpo-
wiedzialnej za przetwarzanie formularza:
<form action="{{ path('name_create') }}" ...>
Akcja edit
Adresem akcji edit dla rekordu o wartości klucza głównego 123 będzie:
.../web/name/123/edit
Akcja update
Adresem akcji update uaktualniającej dane rekordu o wartości klucza głównego 123
będzie:
.../web/name/123/update
394 Część VIII ♦ Panele CRUD i zabezpieczanie dostępu do aplikacji
Adres ten jest generowany w widoku akcji edit.html.twig w celu wskazania akcji odpo-
wiedzialnej za przetwarzanie formularza:
<form action="{{ path('name_update', { 'id': 123 }) }}" ...>
Akcja delete
Adresem akcji delete usuwającej rekord, dla którego id = 123, będzie:
.../web/name/123/delete
W celu ponownego wygenerowania panelu CRUD należy najpierw ręcznie usunąć istnieją-
cy kontroler.
ROZWIĄZANIE
Krok 1. Usuń kontroler i widoki
W gotowym projekcie z rozdziału 17. usuń plik My/FrontendBundle/Controller/
DefaultController.php oraz folder My/FrontendBundle/Resources/views/Default/.
W odpowiedzi na monit:
The Entity shortcut name:
podaj nazwę:
MyFrontendBundle:Name
Rysunek 36.1.
Witryna z przykładu 36.1
Oczywiście wszystkie komunikaty oraz cały kod HTML witryny możesz dostosować,
modyfikując wygenerowane widoki.
ROZWIĄZANIE
Krok 1. Usuń kontroler i widoki
W gotowym projekcie z rozdziału 31. usuń plik My/FrontendBundle/Controller/
DefaultController.php oraz folder My/FrontendBundle/Resources/views/Default/.
W odpowiedzi na monit:
The Entity shortcut name:
podaj nazwę:
MyFrontendBundle:User
Rozdział 36. ♦ Generowanie paneli administracyjnych CRUD 397
}
398 Część VIII ♦ Panele CRUD i zabezpieczanie dostępu do aplikacji
Instrukcja:
->add('profil', new ProfilType(), array('label' => 'Profil użytkownika'))
Dzięki temu podczas zapisywania do bazy danych obiektu klasy User zapisany zostanie
także powiązany z nim relacyjnie obiekt klasy Profil.
Rysunek 36.2.
Witryna
z przykładu 36.2
ROZWIĄZANIE
Krok 1. Usuń kontrolery i widoki
W gotowym projekcie z rozdziału 32. usuń pliki KontynentController.php i Panstwo-
Kontroler.php oraz foldery My/FrontendBundle/Resources/views/Kontynent/ i My/Frontend-
Bundle/Resources/views/Panstwo/.
a na końcu:
{% endblock %}
Rysunek 36.3.
Witryna
z przykładu 36.3
Rozdział 36. ♦ Generowanie paneli administracyjnych CRUD 401
ROZWIĄZANIE
Krok 1. Usuń kontrolery i widoki
W gotowym projekcie z rozdziału 33. usuń kontrolery AktorController.php i FilmCon-
troller.php oraz ich widoki.
a na końcu:
{% endblock %}
Rysunek 36.4.
Witryna
z przykładu 36.4
Rozdział 37.
Instalacja pakietu
FOSUserBundle
Do zabezpieczania dostępu do aplikacji wykorzystamy pakiet FOSUserBundle. Pracę
z pakietem FOSUserBundle należy oczywiście rozpocząć od instalacji.
ROZWIĄZANIE
Krok 1. Wypakuj dystrybucję przygotowaną w rozdziale 21.
Wypakuj przygotowane w rozdziale 21. archiwum symfony2-customized-v3.zip, po
czym zmień nazwę otrzymanego folderu na symfony2-customized-v4/.
404 Część VIII ♦ Panele CRUD i zabezpieczanie dostępu do aplikacji
namespace My\UserBundle\Entity;
/**
* @ORM\Entity
* @ORM\Table(name="fos_user")
*/
class User extends BaseUser
{
/**
Rozdział 37. ♦ Instalacja pakietu FOSUserBundle 405
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
encoders:
"FOS\UserBundle\Model\UserInterface": sha512
firewalls:
main:
pattern: ^/
logout: true
anonymous: true
form_login:
provider: fos_userbundle
csrf_provider: form.csrf_provider
login_path: /login
use_forward: false
check_path: /login_check
post_only: true
always_use_default_target_path: false
default_target_path: /
target_path_parameter: _target_path
use_referer: false
failure_path: null
failure_forward: false
username_parameter: _username
password_parameter: _password
csrf_parameter: _csrf_token
intention: authenticate
access_control:
- { path: ^/login$, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/register, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/resetting, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/admin/, role: ROLE_ADMIN }
role_hierarchy:
ROLE_ADMIN: ROLE_USER
ROLE_SUPER_ADMIN: ROLE_ADMIN
1
Listing 37.5 przedstawia kompletny plik security.yml. Wszystkie inne wpisy należy usunąć.
Rozdział 37. ♦ Instalacja pakietu FOSUserBundle 407
framework:
translator: ~
session:
default_locale: pl
...
fos_user:
db_driver: orm
firewall_name: main
user_class: My\UserBundle\Entity\User
fos_user_profile:
resource: "@FOSUserBundle/Resources/config/routing/profile.xml"
prefix: /profile
fos_user_register:
resource: "@FOSUserBundle/Resources/config/routing/registration.xml"
prefix: /register
fos_user_resetting:
resource: "@FOSUserBundle/Resources/config/routing/resetting.xml"
prefix: /resetting
fos_user_change_password:
resource: "@FOSUserBundle/Resources/config/routing/change_password.xml"
prefix: /profile
2
Listing 37.6 przedstawia jedynie fragment pliku config.yml. Wszystkie inne wpisy należy pozostawić
bez zmian.
3
Listing 37.7 przedstawia kompletną zawartość, jaką należy wprowadzić w pliku routing.yml.
408 Część VIII ♦ Panele CRUD i zabezpieczanie dostępu do aplikacji
Reguła:
fos_user_security:
resource: "@FOSUserBundle/Resources/config/routing/security.xml"
Reguła:
fos_user_profile:
resource: "@FOSUserBundle/Resources/config/routing/profile.xml"
prefix: /profile
Reguła:
fos_user_register:
resource: "@FOSUserBundle/Resources/config/routing/registration.xml"
prefix: /register
Reguła:
fos_user_resetting:
resource: "@FOSUserBundle/Resources/config/routing/resetting.xml"
prefix: /resetting
Wreszcie reguła:
fos_user_change_password:
resource: "@FOSUserBundle/Resources/config/routing/change_password.xml"
prefix: /profile
Tworzenie kont
Nowe konto tworzymy komendą:
php app/console fos:user:create admin admin@example.com sEcrETpAssWord
Komenda:
php app/console fos:user:demote admin
ROZWIĄZANIE
Krok 1. Wypakuj dystrybucję symfony2-customized-v4.zip
Wypakuj archiwum symfony2-customized-v4.zip.
po czym sprawdź zawartość bazy danych symfony2sandbox. W bazie danych pojawi się
jedna tabela o nazwie fos_user zawierająca kolumny widoczne na rysunku 37.1.
Zwróć uwagę, że klasa z listingu 37.4 nie zawiera żadnych właściwości. Kolumny wi-
doczne na rysunku 37.1 powstają na podstawie klasy bazowej User zawartej w pakiecie
FOSUserBundle w pliku:
vendor/bundles/FOS/UserBundle/Model/User.php
po czym sprawdź zawartość tabeli fos_user. Pojawi się w niej rekord odpowiadający
utworzonemu kontu.
Rysunek 37.2.
Strona /web/login
Rysunek 37.3.
Strona /web/register
Rysunek 37.4.
Strona /web/profile
Po odwiedzeniu adresu:
.../web/profile/edit
ujrzysz stronę z rysunku 37.5.
Powyższe adresy są opisane przez reguły z listingu 37.7 oraz pliki konfiguracyjne
z folderu:
\vendor\bundles\FOS\UserBundle\Resources\config\routing
Rozdział 37. ♦ Instalacja pakietu FOSUserBundle 413
Rysunek 37.5.
Strona
/web/profile/edit
Rysunek 37.6.
Strona /web/profile/
change-password
Rysunek 37.7.
Strona /web/profile/
change-password
Uprawnienia dostępu
Uprawnienia dostępu do aplikacji są zawarte w pliku konfiguracyjnym app/config/
security.yml. Przykładowe reguły mają postać:
access_control:
- { path: ^/login$, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/, role: ROLE_ADMIN }
Każda reguła zawiera dwie właściwości: path oraz role. Parametr path jest wyrażeniem
regularnym ustalającym adresy URL. Parametr:
path: ^/login$
natomiast parametr:
path: ^/
Role użytkowników
Pojedynczym wpisem sekcji w access_control w pliku security.yml ustalamy wymagania
dla wszystkich adresów URL odpowiadających podanemu wyrażeniu regularnemu. Wpis:
access_control:
- { path: ^/abc/def/, role: ROLE_LOREM_IPSUM }
ustala, że w celu uzyskania dostępu do dowolnej strony, której adres rozpoczyna się od
/abc/def/, czyli m.in.:
/abc/def/index.html
/abc/def/usun_rekord/123.html
/abc/def/aktorzy/robert-redford.html
...
Po wydaniu powyższego polecenia w tabeli fos_user pojawi się nowy rekord, który nie
będzie miał żadnych uprawnień. Uprawnienia konta są zapisane w kolumnie roles.
418 Część VIII ♦ Panele CRUD i zabezpieczanie dostępu do aplikacji
Rysunek 38.1.
Edycja rekordu
użytkownika o nazwie
inny
ROZWIĄZANIE
ETAP I
W pierwszym etapie pracy przygotujemy aplikację dostępną bez ograniczeń.
zawierający właściwości:
name typu string o długości 255 znaków,
continent typu string o długości 255 znaków,
height typu integer.
W odpowiedzi na monit:
The Entity shortcut name:
Rozdział 38. ♦ Aplikacja dostępna wyłącznie dla zdefiniowanych użytkowników 421
Pamiętaj, by na pytanie:
Do you want to generate the "write" actions [no]?
ETAP II
Zabezpieczanie dostępu do aplikacji.
422 Część VIII ♦ Panele CRUD i zabezpieczanie dostępu do aplikacji
Rysunek 38.2.
Wygląd aplikacji
przygotowanej
w pierwszym etapie
fos_user_security:
resource: "@FOSUserBundle/Resources/config/routing/security.xml"
Rozdział 38. ♦ Aplikacja dostępna wyłącznie dla zdefiniowanych użytkowników 423
Jeśli w plikach konfiguracyjnych aplikacji nie występuje podany adres, nie musimy
definiować żadnych zabezpieczeń. Adres ten jest niedostępny, bez względu na ustalone
zabezpieczenia. Po usunięciu z pliku konfiguracyjnego routing.yml wpisu:
fos_user_register:
resource: "@FOSUserBundle/Resources/config/routing/registration.xml"
prefix: /register
Reguła:
- { path: ^/login$, role: IS_AUTHENTICATED_ANONYMOUSLY }
Reguła:
- { path: ^/, role: ROLE_SUPER_ADMIN }
</a>
</p>
{% endif %}
{% block body %}{% endblock %}
{% block javascripts %}{% endblock %}
</body>
{% block body %}
{% if error %}
<div>{{ error|trans({}, 'FOSUserBundle') }}</div>
{% endif %}
{% endblock body %}
Komenda:
php app/console fos:user:promote admin --super
Rysunek 38.4.
Tabela ułatwiająca
sprawdzanie, jakimi
uprawnieniami
dysponuje zalogowany
użytkownik
I sprawdź, czy w bazie danych w tabeli fos_user pojawił się rekord inny z uprawnieniami:
a:4:{
i:0;s:12:"ROLE_ABC_DEF";
i:1;s:8:"ROLE_XXX";
i:2;s:8:"ROLE_YYY";
i:3;s:16:"ROLE_SUPER_ADMIN";
}
Hierarchia ról
Wpis:
role_hierarchy:
ROLE_ADMIN: ROLE_USER
ROLE_SUPER_ADMIN: ROLE_ADMIN
W celu wprowadzania zmian w zawartości witryny konieczne jest zalogowanie się. Za-
logowany użytkownik uzyskuje dostęp do panelu CRUD, który umożliwia modyfikowa-
nie wybranych tabel lub rekordów. Fragment aplikacji udostępniający panel CRUD jest
określany terminem backend.
W Symfony 2 frontend oraz backend aplikacji możemy wykonać jako osobne pakiety.
ROZWIĄZANIE
Krok 1. Utwórz pakiet My/FrontendBundle
W ukończonym projekcie z rozdziału 38. utwórz pakiet My/FrontendBundle.
namespace My\FrontendBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
$entities = $em->getRepository('MyBackendBundle:Mountain')->findAll();
Następnie dostosuj widok akcji z listingu 39.1 tak, by prezentował tabelkę HTML.
MyBackendBundle:
resource: "@MyBackendBundle/Controller/"
type: annotation
prefix: /admin
fos_user_security:
resource: "@FOSUserBundle/Resources/config/routing/security.xml"
Reguła:
- { path: ^/admin, role: ROLE_SUPER_ADMIN }
Reguła:
- { path: ^/, role: IS_AUTHENTICATED_ANONYMOUSLY }
Kolejność podanych reguł jest bardzo ważna. Zastosowana zostanie pierwsza reguła, która
pasuje do adresu zasobu. Jeśli w pliku security.yml odwrócimy kolejność reguł, podając:
PRZYKŁAD BŁĘDNY
- { path: ^/, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/admin, role: ROLE_SUPER_ADMIN }
wówczas pierwsza reguła będzie pasowała do wszystkich adresów URL. W ten sposób
strony administracyjne nie będą zabezpieczone.
Przekierowania
Listing 37.5 zawiera dużą grupę parametrów opatrzonych kluczem form_login. Są to
między innymi:
form_login:
...
login_path: /login
Rozdział 39. ♦ Aplikacja dostępna publicznie w trybie do odczytu 433
use_forward: false
check_path: /login_check
...
Opcje:
login_path: /login
check_path: /login_check
ustalają adresy URL strony umożliwiającej zalogowanie oraz strony sprawdzającej wy-
pełniony formularz do logowania. Jeśli użytkownik, który nie jest zalogowany, wpisze
adres wymagający autoryzacji, np.:
.../web/admin/1/edit
nastąpi wówczas przekierowanie do adresu ustalonego opcją login_path.
Opcja:
use_forward: false
decyduje o tym, czy podczas logowania stosowane będą przekierowania HTTP, czy we-
wnętrzne wywołania forward. W przypadku formularza obsługiwanego asynchronicznie
przy użyciu Ajaksa konieczne jest stosowanie przekierowań typu forward.
Opcja:
post_only: true
Kolejne opcje:
always_use_default_target_path: false
default_target_path: /
434 Część VIII ♦ Panele CRUD i zabezpieczanie dostępu do aplikacji
target_path_parameter: _target_path
use_referer: false
Opcja:
use_referer: false
Ostatni z parametrów:
target_path_parameter: _target_path
Parametr:
failure_path: null
Osadzanie formularza
do logowania na stronie głównej
Witryny internetowe bardzo często zawierają formularz do logowania osadzony bezpo-
średnio na stronie głównej. Wykonanie takiego formularza sprowadza się do:
przygotowania widoku częściowego formularza,
Rozdział 39. ♦ Aplikacja dostępna publicznie w trybie do odczytu 435
ROZWIĄZANIE
Krok 1. Utwórz widok formularza
W pliku app/Resources/FOSUserBundle/views/Security/login.html.twig umieść kod
z listingu 39.5. Zwróć uwagę, że kod ten nie zawiera instrukcji {% extends %} ani
{% block %}. Oczywiście jest to kopia oryginalnego kodu zawartego w pliku:
vendor\bundles\FOS\UserBundle\Resources\views\Security\login.html.twig
</form>
always_use_default_target_path: true
default_target_path: /admin
failure_path: /
...
MyBackendBundle:
resource: "@MyBackendBundle/Controller/"
type: annotation
prefix: /admin
fos_user_security_check:
pattern: /login_check
defaults: { _controller: FOSUserBundle:Security:check }
fos_user_security_logout:
pattern: /logout
defaults: { _controller: FOSUserBundle:Security:logout }
Wszystkim stronom aplikacji nadaj wspólny szablon HTML, w którym treść strony bę-
dzie prezentowana w elemencie div o zielonym tle i grubym obramowaniu1.
ROZWIĄZANIE
Krok 1. Utwórz nowy projekt
W folderze przeznaczonym na aplikacje WWW utwórz folder zad-40-01/ i wypakuj
do niego zawartość archiwum symfony2-customized-v4.zip.
1
Użycie elementu div ma nas upewnić, że formularze do logowania, rejestracji, edycji profilu oraz
przypominania hasła są wyświetlane w odpowiednim miejscu strony WWW.
440 Część VIII ♦ Panele CRUD i zabezpieczanie dostępu do aplikacji
podaj:
/admin/panstwo
W ten sposób wszystkie adresy URL oraz nazwy reguł routingu z paneli CRUD otrzymają
przedrostek admin.
na:
MyBackendBundle:Kontynent
MyBackendBundle:Panstwo
ROZWIĄZANIE
Krok 1. Utwórz hiperłącze do formularza rejestracyjnego
Zmodyfikuj fragment widoku base.html.twig z listingu 40.1. W elemencie div zawierają-
cym formularz do logowania dodaj przedstawiony na listingu 40.4 element h3.
Rozdział 40. ♦ Rejestracja użytkowników i odzyskiwanie hasła 443
skopiuj do folderu:
app\Resources\FOSUserBundle\views\Registration
ROZWIĄZANIE
Krok 1. Ustal konfigurację konta pocztowego
W pliku app/config/config.yml zmodyfikuj konfigurację poczty elektronicznej. W sekcji
oznaczonej kluczem swiftmailer wprowadź opcję transport: gmail oraz dane dostę-
powe swojego konta Gmail. Zarys pliku config.yml jest przedstawiony na listingu 40.6.
Jeśli korzystasz z innego serwera pocztowego niż Gmail, na przykład z Nazwa.pl, wów-
czas w pliku config.yml umieść wpisy zbliżone do tych z listingu 40.7. Zwróć uwagę,
że w celu ustalenia nadawcy wiadomości należy zmodyfikować opcję address w sekcji
fos_user. Pełne zestawienie opcji konfiguracyjnych pakietu FOSUserBundle znajdziesz na
stronie:
https://github.com/FriendsOfSymfony/FOSUserBundle/blob/master/Resources/
doc/configuration_reference.md
fos_user:
db_driver: orm
firewall_name: main
user_class: My\UserBundle\Entity\User
from_email:
address: kontopocztowe@twojserwer.pl
oraz
vendor\bundles\FOS\UserBundle\Resources\views\Profile\show.html.twig
W jaki sposób uzyskać dostęp do zasobów dla konta utworzonego formularzem reje-
stracyjnym? Po zarejestrowaniu konta janek wydaj komendę:
php app/console fos:user:promote janek --super
Rozdział 42.
Instalacja pakietów Sonata
Oprogramowanie Symfony 1.4 zawierało wbudowane generatory pozwalające na szybkie
przygotowanie standardowego panelu administracyjnego aplikacji. W Symfony 2 rolę taką
odgrywa zestaw pakietów z projektu Sonata.
ROZWIĄZANIE
[SonataAdminBundle]
git=http://github.com/sonata-project/SonataAdminBundle.git
target=/bundles/Sonata/AdminBundle
version=origin/2.0
[SonataBlockBundle]
git=http://github.com/sonata-project/SonataBlockBundle.git
target=/bundles/Sonata/BlockBundle
[SonataCacheBundle]
git=http://github.com/sonata-project/SonataCacheBundle.git
target=/bundles/Sonata/CacheBundle
[SonatajQueryBundle]
git=http://github.com/sonata-project/SonatajQueryBundle.git
target=/bundles/Sonata/jQueryBundle
[SonataDoctrineORMAdminBundle]
git=http://github.com/sonata-project/SonataDoctrineORMAdminBundle.git
target=/bundles/Sonata/DoctrineORMAdminBundle
version=origin/2.0
[SonataUserBundle]
git=http://github.com/sonata-project/SonataUserBundle.git
target=/bundles/Sonata/UserBundle
version=origin/2.0
[SonataEasyExtendsBundle]
git=http://github.com/sonata-project/SonataEasyExtendsBundle.git
target=/bundles/Sonata/EasyExtendsBundle
[Exporter]
git=http://github.com/sonata-project/exporter.git
target=/exporter
[KnpMenuBundle]
git=http://github.com/KnpLabs/KnpMenuBundle.git
target=/bundles/Knp/Bundle/MenuBundle
[KnpMenu]
git=http://github.com/KnpLabs/KnpMenu.git
target=/knp/menu
Rozdział 42. ♦ Instalacja pakietów Sonata 453
sonata.block.service.text:
sonata.block.service.action:
sonata.block.service.rss:
sonata_user:
security_acl: true
fos_user:
db_driver: orm
firewall_name: main
user_class: Application\Sonata\UserBundle\Entity\User
group:
group_class: Application\Sonata\UserBundle\Entity\Group
sonata_doctrine_orm_admin:
entity_manager: ~
templates:
form:
- SonataDoctrineORMAdminBundle:Form:form_admin_fields.html.twig
filter:
- SonataDoctrineORMAdminBundle:Form:filter_admin_fields.html.twig
types:
list:
array: SonataAdminBundle:CRUD:list_array.html.twig
boolean: SonataAdminBundle:CRUD:list_boolean.html.twig
date: SonataAdminBundle:CRUD:list_date.html.twig
time: SonataAdminBundle:CRUD:list_time.html.twig
datetime: SonataAdminBundle:CRUD:list_datetime.html.twig
text: SonataAdminBundle:CRUD:base_list_field.html.twig
trans: SonataAdminBundle:CRUD:list_trans.html.twig
string: SonataAdminBundle:CRUD:base_list_field.html.twig
smallint: SonataAdminBundle:CRUD:base_list_field.html.twig
bigint: SonataAdminBundle:CRUD:base_list_field.html.twig
integer: SonataAdminBundle:CRUD:base_list_field.html.twig
decimal: SonataAdminBundle:CRUD:base_list_field.html.twig
identifier: SonataAdminBundle:CRUD:base_list_field.html.twig
Rozdział 42. ♦ Instalacja pakietów Sonata 455
show:
array: SonataAdminBundle:CRUD:show_array.html.twig
boolean: SonataAdminBundle:CRUD:show_boolean.html.twig
date: SonataAdminBundle:CRUD:show_date.html.twig
time: SonataAdminBundle:CRUD:show_time.html.twig
datetime: SonataAdminBundle:CRUD:show_datetime.html.twig
text: SonataAdminBundle:CRUD:base_show_field.html.twig
trans: SonataAdminBundle:CRUD:show_trans.html.twig
string: SonataAdminBundle:CRUD:base_show_field.html.twig
smallint: SonataAdminBundle:CRUD:base_show_field.html.twig
bigint: SonataAdminBundle:CRUD:base_show_field.html.twig
integer: SonataAdminBundle:CRUD:base_show_field.html.twig
decimal: SonataAdminBundle:CRUD:base_show_field.html.twig
encoders:
"FOS\UserBundle\Model\UserInterface": sha512
firewalls:
anonymous: true
# -> end custom configuration
form_login:
provider: fos_userbundle
csrf_provider: form.csrf_provider
login_path: /login
use_forward: false
check_path: /login_check
post_only: true
always_use_default_target_path: false
default_target_path: /admin/dashboard
target_path_parameter: _target_path
use_referer: true
failure_path: null
failure_forward: false
username_parameter: _username
password_parameter: _password
csrf_parameter: _csrf_token
intention: authenticate
access_control:
# URL of FOSUserBundle which need to be available to anonymous users
- { path: ^/_wdt, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/_profiler, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/login$, role: IS_AUTHENTICATED_ANONYMOUSLY }
# -> custom access control for the admin area of the URL
- { path: ^/admin/login$, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/admin/logout$, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/admin/login-check$, role: IS_AUTHENTICATED_ANONYMOUSLY }
# -> end
role_hierarchy:
ROLE_ADMIN: [ROLE_USER, ROLE_SONATA_ADMIN]
ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]
SONATA:
- ROLE_SONATA_PAGE_ADMIN_PAGE_EDIT # if you are using acl then this line
# must be commented
acl:
connection: default
Rozdział 42. ♦ Instalacja pakietów Sonata 457
ROZWIĄZANIE
Krok 1. Wypakuj dystrybucję i skonfiguruj bazę danych
Utwórz bazę danych o nazwie symfony2sandbox oraz konto dostępu do bazy editor
zabezpieczone hasłem secretPASSWORD. Następnie wypakuj archiwum symfony2-
-customized-v5.zip, po czym w pliku app/config/parameters.ini wprowadź dane dostępu
do bazy danych symfony2sandbox.
Po zalogowaniu na konto:
Użytkownik: admin
Hasło: password
Teraz zajmiemy się dostosowaniem paneli w taki sposób, by umożliwiały edycję rekor-
dów z dowolnej tabeli, dla której jest dostępna klasa Entity.
ROZWIĄZANIE
wyświetlane było zestawienie wszystkich rekordów z tabeli city. W przykładzie tym nie
wykonuj fikstur — wypełnianie bazy wykonamy, wykorzystując panel administracyjny.
namespace My\FrontendBundle\Admin;
use Sonata\AdminBundle\Admin\Admin;
use Sonata\AdminBundle\Datagrid\ListMapper;
use Sonata\AdminBundle\Datagrid\DatagridMapper;
use Sonata\AdminBundle\Validator\ErrorElement;
Rozdział 43. ♦ Użycie paneli administracyjnych Sonata do własnych tabel 463
use Sonata\AdminBundle\Form\FormMapper;
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services
http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="sonata.admin.city" class="My\FrontendBundle\Admin\CityAdmin">
<tag name="sonata.admin" manager_type="orm" group="Dane" label="Miasta"/>
<argument />
<argument>My\FrontendBundle\Entity\City</argument>
<argument>SonataAdminBundle:CRUD</argument>
<call method="setTranslationDomain">
<argument>MyFrontendBundle</argument>
</call>
464 Część IX ♦ Panele administracyjne Sonata
</service>
</services>
</container>
Dzięki temu będziemy mogli w prosty sposób rozpocząć pracę nad kolejnym projektem.
ROZWIĄZANIE
i odwiedzamy adres:
.../web/
ROZWIĄZANIE
W zadaniu tym należy utworzyć dwa pliki: KontynentAdmin.php oraz PanstwoAdmin.php.
W klasie KontynentAdmin konfigurujemy edycję jednej właściwości: name. W klasie
PanstwoAdmin konfigurujemy natomiast dwa pola formularza edycyjnego:
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->add('nazwa')
->add('kontynent')
;
}
1. XAMPP
Do zainstalowania oprogramowania:
Apache,
PHP
i MySQL
Podczas uruchamiania usług Apache oraz MySQL ujrzysz okno dialogowe przedsta-
wione na rysunku A.3. Zawiera ono informację o tym, że uruchamiane usługi pozwa-
lają na nawiązywanie połączeń sieciowych. Zezwól na nawiązywanie połączeń w sie-
ciach lokalnych.
474 Symfony 2 od podstaw
Rysunek A.1.
Panel administracyjny
XAMPP i przyciski
do uruchamiania
i zatrzymywania usług
Apache i MySQL
Rysunek A.2. Załadowanie się strony startowej pakietu XAMPP świadczy o poprawnej instalacji
Rysunek A.3.
Ostrzeżenie
informujące o tym,
że usługa Apache
zezwala na
nawiązywanie
połączeń sieciowych
Następnie włącz rozszerzenie XSL. W tym celu na końcu sekcji1 oznaczonej komen-
tarzem:
;;;;;;;;;;;;;;;;;;;;;;
; Dynamic Extensions ;
;;;;;;;;;;;;;;;;;;;;;;
dodaj wpis:
extension=php_xsl.dll
wówczas strona domowa XAMPP-a (tj. skrypty wyświetlające m.in. stronę z rysunku A.2)
nie będzie działała. Oczywiście nie stanowi to żadnej przeszkody w nauce Symfony 2.
Skrypty XAMPP-a zawarte w folderze C:\xampp\htdocs są zbędne i możesz je usunąć.
1
Na końcu serii wpisów extension=....
476 Symfony 2 od podstaw
przejdź do katalogu C:\xampp\php. Następnie wydaj polecenie, które utworzy nowy plik
konfiguracyjny PEAR:
pear config-create / C:\xampp\php\pear.ini
5. Code Sniffer
Pakiet Code Sniffer służy do kontroli standardów kodowania. Aby zainstalować pakiet
Code Sniffer w folderze c:\xampp\php za pomocą wiersza poleceń, wydaj komendę:
pear -c c:\xampp\php\pear.ini install --alldeps PHP_CodeSniffer-1.3.3
i pobierz plik:
opensky-Symfony2-coding-standard-XXXXXX.zip
Pobrane archiwum ZIP wypakuj do folderu o nazwie Symfony2, po czym otrzymany folder
przenieś do:
C:\xampp\php\PEAR\PHP\CodeSniffer\Standards\Symfony2
6. phpDocumentor
Najpierw odinstaluj pakiet phpDocumentor w wersji 1.4.4. W tym celu wydaj komendę:
pear -c c:\xampp\php\pear.ini uninstall PhpDocumentor-1.4.4
7. PHPUnit
W celu zainstalowania oprogramowania PHPUnit wydaj komendę:
pear -c c:\xampp\php\pear.ini install pear.phpunit.de/PHPUnit
478 Symfony 2 od podstaw
8. Cygwin
Instalacja pakietu Cygwin umożliwi nam korzystanie w wierszu poleceń z komend:
git
rsync
ssh
curl
find
Odwiedź adres:
http://www.cygwin.com
i pobierz plik:
http://cygwin.com/setup.exe
W analogiczny sposób włącz instalację pakietów openssh, curl oraz git. Procedura wyszu-
kiwania i włączania wymienionych pakietów jest przedstawiona na rysunkach A.5,
A.6 i A.7.
Dodatek A ♦ Instalacja oprogramowania 479
9. Ścieżki dostępu
Przejdź do panelu sterowania. Wybierz opcję System i zabezpieczenia, a następnie System.
W oknie dialogowym z rysunku A.8 wybierz opcję Zaawansowane ustawienia systemu.
Ujrzysz okno dialogowe widoczne na rysunku A.9. Wybierz w nim przycisk Zmienne
środowiskowe.
Następnie w oknie dialogowym z rysunku A.10 wybierz zmienną Path, po czym naciśnij
przycisk Edytuj.
Na początku wartości zmiennej dodaj ścieżki prowadzące do PHP oraz do pakietu Cygwin,
czyli:
C:\xampp\php;c:\cygwin\bin;
Rysunek A.9.
Przycisk pozwalający
na modyfikację
zmiennych
środowiskowych
Rysunek A.10.
Edycja zmiennej
środowiskowej Path
Rysunek A.11.
Modyfikacja zmiennej
Path
W celu sprawdzenia poprawności instalacji programów git, rsync i curl wydaj w wierszu
poleceń komendy:
git
rsync
curl
10. GraphViz
Program GraphViz służy do wizualizacji grafów. Dzięki niemu oprogramowanie phpDocu-
mentor generuje diagramy zależności klas.
Odwiedź adres:
http://www.graphviz.org
11. NetBeans
Odwiedź adres:
http://netbeans.org/downloads/
i pobierz oprogramowanie NetBeans przeznaczone dla języka PHP. Pobrany plik netbe-
ans-7. 1.1-ml-php-windows.exe zainstaluj, zachowując wszystkie opcje domyślne.
Skorowidz
A sitAction(), 118 PEAR, 476
update, 391, 393 swiftmailer, 19
adnotacja akcje Twig, 111, 119
@Gedmo\Translatable, 284, 286 Doctrine, 352 biblioteki ORM, 269
@ORM\Column, 238, 239 kontrolera DefaultController, blokowanie dostępu do plików, 95
@ORM\Entity, 301 322 błąd, 20–22
@ORM\JoinColumn, 339 referencyjne, referential actions, 403, 444
@ORM\JoinTable, 362 338, 352 404, 83, 104
@ORM\ManyToOne, 347 referencyjne SQL, 365
SQL-owe, 352
@ORM\OneToMany, 347
aktualizacja, ON UPDATE, 338
C
@ORM\OneToOne, 333, 340
@ORM\OrderBy, 358 aktywacja konta, 409 cachowanie, 127
@Route, 35, 68, 104, 125, 298, analiza odpowiedzi HTTP, 127 ciągi slug, 275
391 Apache, 473 ciągi znaków, 140
@Table, 235, 238 aplikacja
ACME demo, 101
@Template, 35, 114, 323
dostępna publicznie, 429 D
adres
do akcji, 431 atak typu definiowanie
Cross Site Request Forger, 132 adresu, 102
login, 431
Cross Site Scripting, 132 akcji referencyjnej, 339
login_check, 431
automatyczne ładowanie klas, 212, 215 relacji 1:n, 347
logout, 431
autoryzacja, 407 zmiennych, 144
URL, 68, 297, 391
adresy slug, 470 deklaracja przestrzeni nazewniczej,
akcelerator APC, 22 B 42
akcja, action, 25 DETACHED, 241
backend, 439 Doctrine 2.1, 339
create, 391, 393 baza danych, 222
dataAction(), 130 dodawanie pakietu, 209, 212, 214
achristie, 377 dokumentacja Doctrine 2, 235
delete, 391, 394 cities, 462
dolorAction(), 118 domena
colors, 287 projektu, 96
edit, 391, 393 download, 320
index, 102, 231, 390 wirtualna, 94
filmy, 366 dostęp do
indexAction(), 126 kontynenty, 353
ipsumAction(), 124 aplikacji, 421
koronaziemi, 420 bazy danych, 258
loremAction(), 118, 297 mountains, 258
menuAction(), 200, 316 rekordów, 234
names, 222, 228 tabeli, 236
new, 391, 393 rivers, 244, 245
dwukierunkowa relacja
Novel/index, 383 songs, 301
1:n, 354
referencyjna treny, 312
n:m, 366
cascade, 339 users, 335
dwukropki, 60
Doctrine, 339 biblioteka
dystrybucja
ON DELETE CASCADE, 338 Doctrine 2.1, 265
Symfony 2.0, 17, 217
sAction(), 126 DoctrineExtensions, 269, 273, 293
symfony2-customized-v6.zip, 467
show, 297, 304, 327, 373, 390 ORM Doctrine 2.1, 234
484 Symfony 2 od podstaw
dla dzieci, 77 S, Ś T
dziecięce, 185
wojskowe, 299, 308 serializacja, 418 tabea profil, 331
powieści Agaty Christie, 376, serwer tabela
470 Apache, 22 aktor, 359, 366
przygotowanie dystrybucji, 212, hostingowy, 93, 319 docelowa, 331, 345
270, 451, 467 Light Hosting, 97 ext_translations, 285
Pusta Dolinka, 49 MySQL, 223, 319 file, 320, 325
rzeki, 243, 468 NetArt, 95 film, 359, 366
sprawdzenie działania wirtualny film_aktor, 366
dystrybucji, 410 reguły konfigurujące, 94 fos_user, 410
tabela potęg, 161 skórka, 248, 260, 302, 314, 356, kontynent, 345, 354
tabliczka mnożenia, 157 369, 384 łącząca relacje, 359
Tatry, 257 skrypt łącząca relacji n:m, 362
treny, 311 app.php, 19, 23 mountain, 260
wyrazy, 277, 282 app_dev.php, 19, 23 name, 225, 228
zabezpieczanie zmiennych, 134 autoload.php, 212, 215 panstwo, 345, 354
zabytki Lublina, 72 config.php, 19, 23, 101 profil, 336
publikowanie projektu, 105 rsync-production.bat, 99, 100 river, 245
tworzenie-pustej-bazy- song, 301, 309
danych.sh, 223 tren, 312
R słowo kluczowe DEFAULT, 240
user, 331, 336
word, 279, 282
reguła sortowanie
źródłowa relacji, 331, 345
@Template(), 115 rekordów, 313, 358, 370
tabele dodatkowe, 272
konfiguracyjna autoescape, 133 tekstów, 223
tabelka hiperłączy, 382
translacji adresu, 289 SQL, 223, 240
tablica, 138
włączająca adres, 81 stan $data, 229
reguły routingu, 34, 422, 437, 457 DETACHED, 241 $menuData, 200
rejestracja MANAGED, 241 $t, 314
pakietu, 212, 215, 271, 453 NEW, 241 menuData, 201
przestrzeni nazw, 271, 453 REMOVED, 241 tablice
użytkownika, 439, 442 stany obiektu Entity, 241 asocjacyjne, 140
rekord strona błędu 404, 85 indeksowane, 140
nadrzędny, 351 strona główna, 356 termin
zależny, 335, 350, 363 strona rejestracji, 446 backend, 429
relacja struktura pakietu, 31 frontend, 429
1:1, 331, 336 synchronizacja relacji, 351, 364 tłumaczenie, 284, 414, 464
1:n, 345, 378, 399 synchronizowanie obiektów z bazą translacja adresu, 289
dwukierunkowa, bidirectional, danych, 342 Twig, 111
340 system szablonów, 19 drukowanie znaczników, 121
jednokierunkowa, szablon filtry, 169, 181
unidirectional, 340 base.html.twig, 174, 176, 436, funkcje, 169, 184
n:m, 359, 361, 379, 401 440 instrukcje sterujące, 120
REMOVED, 241 index.html.twig, 36 komentarze, 120
rola layout.html.twig, 61, 70, 75, 87, komentarze wielowierszowe,
IS_AUTHENTICATED_ANON 103 120
YMOUSLY, 416, 417 menu.html.twig, 201 operatory, 141
ROLE_SUPER_ADMIN, 417 PHP, 206 wyrażenia, 140
role użytkowników, 416 witryny, 57, 432 znaczniki, 121, 169
routing, 104 szablony błędów, 85 tworzenie
rozszerzanie ścieżki prowadzące akcji, 65
funkcjonalności modelu, 312 do pakietu Cygwin, 480 bazy danych, 222, 233
.html.twig, 28 do PHP, 480 kont, 409
środowisko konta administratora, 458
.twig, 119
deweloperskie, 40 kontrolerów, 67
DoctrineExtensions, 308
pakietów, 29, 42, 67
DoctrineFixturesBundle, 222 produkcyjne, 40, 127
projektu, 28
podwójne .html.twig, 112, 123
rekordów, 242, 334, 349, 362
tabel, 228, 235
488 Symfony 2 od podstaw