Vous êtes sur la page 1sur 490

Wszelkie prawa zastrzeżone.

Nieautoryzowane rozpowszechnianie całości lub fragmentu


niniejszej publikacji w jakiejkolwiek postaci jest zabronione. Wykonywanie kopii metodą
kserograficzną, fotograficzną, a także kopiowanie książki na nośniku filmowym, magnetycznym
lub innym powoduje naruszenie praw autorskich niniejszej publikacji.

Wszystkie znaki występujące w tekście są zastrzeżonymi znakami firmowymi


bądź towarowymi ich właścicieli.

Autor oraz Wydawnictwo HELION dołożyli wszelkich starań, by zawarte


w tej książce informacje były kompletne i rzetelne. Nie biorą jednak żadnej
odpowiedzialności ani za ich wykorzystanie, ani za związane z tym ewentualne
naruszenie praw patentowych lub autorskich. Autor oraz Wydawnictwo HELION
nie ponoszą również żadnej odpowiedzialności za ewentualne
szkody wynikłe z wykorzystania informacji zawartych w książce.

Redaktor prowadzący: Ewelina Burska


Projekt okładki: Jan Paluch
Materiały graficzne na okładce zostały wykorzystane za zgodą Shutterstock.

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

Przykłady omówione w książce są dostępne pod adresem:


ftp://ftp.helion.pl/przyklady/symfo2.zip

ISBN: 978-83-246-6241-8

Copyright © Helion 2012

Printed in Poland.

• Poleć książkę na Facebook.com • Księgarnia internetowa

• Kup w wersji papierowej • Lubię to! » Nasza społeczność

• Oceń książkę
Spis treści
Podziękowania .............................................................................. 13

Część I Tworzenie prostych stron WWW .................................... 15


Rozdział 1. Uruchomienie przykładowego projektu ........................................... 17
Dystrybucja with vendors — około 6 MB ....................................................................... 17
Dystrybucja without vendors — około 200 kB ............................................................. 17
Przykład 1.1. Aplikacja przykładowa ............................................................................ 18
ROZWIĄZANIE ..................................................................................................... 18
Podsumowanie .............................................................................................................. 22
Rozdział 2. Hello, world! .................................................................................. 25
Przestrzenie nazw .......................................................................................................... 25
Pakiet ............................................................................................................................. 26
Kontroler i akcja ............................................................................................................ 27
Widok ............................................................................................................................ 28
Przykład 2.1. Hello, world! ........................................................................................... 28
ROZWIĄZANIE ..................................................................................................... 28
Zmodyfikowane pliki .................................................................................................... 39
Środowiska pracy .......................................................................................................... 40
Tworzenie i usuwanie pakietów .................................................................................... 42
Użycie przestrzeni nazewniczych .................................................................................. 42
Cechy Symfony 2 .......................................................................................................... 44
Formaty konfiguracji ............................................................................................... 44
Uruchomienie gotowego przykładu ............................................................................... 46
Rozdział 3. Dołączanie zewnętrznych zasobów ................................................. 47
Przykład 3.1. Pusta Dolinka .......................................................................................... 49
ROZWIĄZANIE ..................................................................................................... 49
Przykład 3.2. Dolina Pięciu Stawów Polskich ............................................................... 53
ROZWIĄZANIE ..................................................................................................... 53
Rozdział 4. Szablon witryny ............................................................................. 57
Przykład 4.1. Dwa kabele .............................................................................................. 60
ROZWIĄZANIE ..................................................................................................... 61
Rozdział 5. Hiperłącza i struktura aplikacji ...................................................... 65
Tworzenie i usuwanie akcji ........................................................................................... 65
Tworzenie i usuwanie kontrolerów ............................................................................... 67
4 Symfony 2 od podstaw

Tworzenie i usuwanie pakietów .................................................................................... 67


Definiowanie adresów URL akcji ................................................................................. 68
Przykład 5.1. Fraszki ..................................................................................................... 69
ROZWIĄZANIE ..................................................................................................... 69
Przykład 5.2. Zabytki Lublina ....................................................................................... 72
ROZWIĄZANIE ..................................................................................................... 74
Przykład 5.3. Piosenki dla dzieci ................................................................................... 77
ROZWIĄZANIE ..................................................................................................... 78
Rozdział 6. Błędy 404 ..................................................................................... 83
Strony błędów w Symfony 2 ......................................................................................... 84
Przykład 6.1. Gady ........................................................................................................ 86
ROZWIĄZANIE ..................................................................................................... 86
Nadpisywanie widoków dowolnych pakietów .............................................................. 91
Programowe generowanie błędów 404 oraz 500 ........................................................... 92
Rozdział 7. Publikowanie projektu na serwerze hostingowym ........................... 93
Przykład 7.1. Gady — wersja lokalna z własną domeną ............................................... 93
ROZWIĄZANIE ..................................................................................................... 94
Przykład 7.2. Gady — wersja z serwera firmy NetArt .................................................. 95
ROZWIĄZANIE ..................................................................................................... 95
Przykład 7.3. Gady — wersja z serwera firmy Light Hosting ....................................... 97
ROZWIĄZANIE ..................................................................................................... 97
Rozdział 8. Podsumowanie części I ............................................................... 101
Dystrybucje Symfony 2 ............................................................................................... 101
Przykładowa aplikacja ACME demo .......................................................................... 101
Pierwszy samodzielnie wykonany projekt ................................................................... 102
Zewnętrzne zasoby ...................................................................................................... 103
Szablon witryny .......................................................................................................... 103
Podstawy routingu ....................................................................................................... 104
Błędy 404 .................................................................................................................... 104
Publikowanie projektu ................................................................................................. 105
Przykład 8.1. Przygotowanie pakietu symfony2-customized-v1.zip
(bez przykładu src/Acme) ......................................................................................... 106
ROZWIĄZANIE ................................................................................................... 106

Część II Widoki ....................................................................... 109


Rozdział 9. Twig ........................................................................................... 111
Logiczne nazwy widoków ........................................................................................... 111
Nadpisywanie widoków z folderu vendor ................................................................... 113
Nazwy widoków akcji ................................................................................................. 114
Przykład 9.1. Nazwy logiczne widoków, adnotacja @Template() i metoda render() ....... 116
ROZWIĄZANIE ................................................................................................... 116
Składnia widoków Twig .............................................................................................. 119
Wyłączanie interpretacji w szablonie .......................................................................... 120
Przykład 9.2. Wyłączanie interpretacji fragmentu szablonu ........................................ 121
ROZWIĄZANIE ................................................................................................... 122
Podwójne rozszerzenie .html.twig ............................................................................... 123
Modyfikacja nagłówka Content-Type przy użyciu parametru _format ................. 124
Modyfikacja nagłówka Content-Type metodą set() .............................................. 124
Przykład 9.3. Modyfikacja nagłówka Content-Type ................................................... 125
ROZWIĄZANIE ................................................................................................... 125
Spis treści 5

Rozdział 10. Zmienne, wyrażenia i operatory Twig ........................................... 129


Przekazywanie zmiennych do widoku ........................................................................ 129
Przykład 10.1. Data i godzina ...................................................................................... 130
ROZWIĄZANIE ................................................................................................... 131
Zabezpieczanie zmiennych .......................................................................................... 132
Przykład 10.2. Zabezpieczanie zmiennych .................................................................. 134
ROZWIĄZANIE ................................................................................................... 135
Przekazywanie do widoku tablic ................................................................................. 138
Przekazywanie do widoku obiektów ........................................................................... 139
Wyrażenia Twig .......................................................................................................... 139
Operatory Twig ........................................................................................................... 141
Definiowanie zmiennych wewnątrz widoku ................................................................ 144
Zmienne globalne ........................................................................................................ 145
Rozdział 11. Instrukcje sterujące for oraz if ..................................................... 147
Instrukcja for ............................................................................................................... 147
Instrukcja if ................................................................................................................. 150
Przykład 11.1. Korona ziemi ....................................................................................... 151
ROZWIĄZANIE ................................................................................................... 152
Przykład 11.2. Dzieła literatury światowej .................................................................. 155
ROZWIĄZANIE ................................................................................................... 155
Przykład 11.3. Tabliczka mnożenia ............................................................................. 157
ROZWIĄZANIE ................................................................................................... 157
Przykład 11.4. Tabela potęg ........................................................................................ 161
ROZWIĄZANIE ................................................................................................... 161
Przykład 11.5. Bezpieczna paleta kolorów .................................................................. 163
ROZWIĄZANIE ................................................................................................... 164
Rozdział 12. Znaczniki, filtry i funkcje ............................................................. 169
Znaczniki Twig ........................................................................................................... 169
Znaczniki for oraz if .............................................................................................. 171
Znaczniki macro, from i import ............................................................................ 171
Znacznik filter ....................................................................................................... 172
Znacznik set .......................................................................................................... 173
Znacznik extends ................................................................................................... 173
Znacznik block ...................................................................................................... 175
Znaczniki extends i block oraz dziedziczenie ....................................................... 175
Znacznik use ......................................................................................................... 178
Znacznik include ................................................................................................... 179
Znacznik spaceless ................................................................................................ 179
Znacznik autoescape ............................................................................................. 180
Znacznik raw ......................................................................................................... 180
Znacznik flush ....................................................................................................... 180
Znacznik do ........................................................................................................... 180
Znacznik render ..................................................................................................... 181
Filtry ............................................................................................................................ 181
Funkcje ........................................................................................................................ 184
Przykład 12.1. Piosenki dziecięce ............................................................................... 185
ROZWIĄZANIE ................................................................................................... 186
Rozdział 13. Trójstopniowy podział widoków .................................................... 195
Przykład 13.1. Opowiadania Edgara Allana Poe ......................................................... 197
ROZWIĄZANIE ................................................................................................... 198
Rozdział 14. Podsumowanie części II .............................................................. 205
6 Symfony 2 od podstaw

Część III Dostosowywanie Symfony 2 ........................................ 207


Rozdział 15. Dodawanie nowych pakietów ....................................................... 209
Lista pakietów zawartych w Symfony ......................................................................... 209
Zawartość folderu vendor/ ........................................................................................... 210
Pobieranie pakietów do folderu vendor/ .......................................................................... 211
Dołączanie pakietów do kodu ..................................................................................... 212
Przykład 15.1. Przygotowanie dystrybucji symfony2-customized-v2
zawierającej pakiet DoctrineFixturesBundle ............................................................ 212
ROZWIĄZANIE ................................................................................................... 213
Rozdział 16. Podsumowanie części III ............................................................. 217

Część IV Praca z bazą danych ................................................... 219


Rozdział 17. Pierwszy projekt wykorzystujący bazę danych .............................. 221
Przykład 17.1. Imiona ................................................................................................. 221
ROZWIĄZANIE ................................................................................................... 222
Rozdział 18. ORM Doctrine 2 .......................................................................... 233
Tworzenie i usuwanie bazy danych ............................................................................. 233
Doctrine 2.1 ................................................................................................................. 234
Tworzenie tabel w bazie danych ................................................................................. 235
Struktura klas dostępu do bazy danych ....................................................................... 236
Dodawanie nowych właściwości do istniejącej klasy ................................................. 237
Typy danych ................................................................................................................ 238
Operowanie klasami dostępu do bazy danych ............................................................. 240
Klasy Entity i EntityManager ................................................................................ 240
Stan obiektu Entity ................................................................................................ 241
Tworzenie nowych rekordów ................................................................................ 242
Usuwanie rekordów .............................................................................................. 243
Pobieranie wszystkich rekordów z bazy ................................................................ 243
Przykład 18.1. Rzeki ................................................................................................... 243
ROZWIĄZANIE ................................................................................................... 244
Rozdział 19. Dostosowywanie klas dostępu do bazy danych ............................. 251
Klasy Entity oraz Repository ....................................................................................... 251
Podstawowe metody klas Repository .......................................................................... 252
Metoda find() ........................................................................................................ 252
Metoda findAll() ................................................................................................... 253
Metoda findBy() .................................................................................................... 253
Metoda findOneBy() ............................................................................................. 254
Metoda findByX() ................................................................................................. 254
Metoda findOneByX() .......................................................................................... 255
Nadpisywanie metod klasy Entity ............................................................................... 255
Metoda __toString() klasy Entity .......................................................................... 255
Metoda fromArray () klasy Entity ......................................................................... 256
Nadpisywanie metod klasy Repository ....................................................................... 256
Przykład 19.1. Tatry .................................................................................................... 257
ROZWIĄZANIE ................................................................................................... 257
Rozdział 20. Podsumowanie części IV ............................................................. 265
Spis treści 7

Część V Zachowania Doctrine ................................................. 267


Rozdział 21. Instalacja i konfiguracja rozszerzeń DoctrineExtensions ................ 269
Przykład 21.1. Przygotowanie dystrybucji symfony2-customized-v3
zawierającej pakiet StofDoctrineExtensionsBundle .................................................. 270
ROZWIĄZANIE ................................................................................................... 270
Rozdział 22. Zachowanie sluggable ................................................................. 275
Identyfikatory slug ...................................................................................................... 275
Automatyczne generowanie identyfikatorów slug w Symfony 2 ................................ 276
Przykład 22.1. Wyrazy — test zachowania sluggable ................................................. 277
ROZWIĄZANIE ................................................................................................... 277
Parametry adnotacji konfigurujących wartości slug .................................................... 280
Rozdział 23. Zachowanie timestampable ......................................................... 281
Przykład 23.1. Wyrazy — test zachowania timestampable ......................................... 282
ROZWIĄZANIE ................................................................................................... 282
Rozdział 24. Zachowanie translatable ............................................................. 283
Wstawianie tłumaczeń do bazy danych ....................................................................... 284
Odczytywanie tłumaczeń ............................................................................................ 286
Przykład 24.1. Kolory — test zachowania timestampable .......................................... 286
ROZWIĄZANIE ................................................................................................... 287
Rozdział 25. Podsumowanie części V .............................................................. 293

Część VI Szczegółowe dane rekordu .......................................... 295


Rozdział 26. Akcja show ................................................................................. 297
Adresy URL zawierające zmienne .............................................................................. 297
Konwersja wejściowa ............................................................................................ 298
Konwersja wyjściowa ........................................................................................... 298
Wyszukiwanie pojedynczego rekordu na podstawie klucza głównego ....................... 298
Wyświetlanie właściwości rekordu ............................................................................. 299
Przykład 26.1. Piosenki wojskowe .............................................................................. 299
ROZWIĄZANIE ................................................................................................... 300
Rozdział 27. Identyfikacja rekordu na podstawie wartości slug ........................ 307
Przykład 27.1. Piosenki wojskowe — użycie identyfikatorów slug ............................ 308
ROZWIĄZANIE ................................................................................................... 308
Rozdział 28. Generowanie menu na podstawie zawartości bazy danych ............ 311
Przykład 28.1. Treny ................................................................................................... 311
ROZWIĄZANIE ................................................................................................... 312
Rozdział 29. Udostępnianie plików binarnych ................................................... 319
Przykład 29.1. Download — pliki zapisane w bazie danych ....................................... 320
ROZWIĄZANIE ................................................................................................... 320
Przykład 29.2. Download — pliki pobierane z folderu ............................................... 325
ROZWIĄZANIE ................................................................................................... 325
Rozdział 30. Podsumowanie części VI ............................................................. 327
8 Symfony 2 od podstaw

Część VII Relacje ...................................................................... 329


Rozdział 31. Relacje 1:1 ................................................................................. 331
Klucze obce o wartości NULL .................................................................................... 332
Użycie relacji 1:1 w Symfony 2 .................................................................................. 332
Operowanie rekordami powiązanymi relacją .............................................................. 334
Tworzenie rekordów ............................................................................................. 334
Rekord zależny ...................................................................................................... 335
Przykład 31.1. Dane użytkowników ............................................................................ 335
ROZWIĄZANIE ................................................................................................... 335
Akcje referencyjne SQL .............................................................................................. 338
Programowe akcje referencyjne Doctrine 2.1 .............................................................. 339
Parametr cascade ................................................................................................... 339
Parametr orphanRemoval ...................................................................................... 340
Relacje jednokierunkowe i dwukierunkowe ................................................................ 340
Synchronizacja obiektów z bazą danych ........................................................................ 342
Rozdział 32. Relacje 1:n (jeden do wielu) ........................................................ 345
Klucze obce o wartości NULL .................................................................................... 346
Użycie relacji 1:n w Symfony 2 .................................................................................. 346
Właściciel relacji 1:n ................................................................................................... 349
Operowanie rekordami powiązanymi relacją .............................................................. 349
Tworzenie rekordów ............................................................................................. 349
Rekordy zależne .................................................................................................... 350
Rekord nadrzędny ................................................................................................. 351
Synchronizacja relacji ................................................................................................. 351
Akcje referencyjne ...................................................................................................... 352
Akcje SQL-owe ..................................................................................................... 352
Akcje Doctrine ...................................................................................................... 352
Przykład 32.1. Kontynent i państwa ............................................................................ 353
ROZWIĄZANIE ................................................................................................... 353
Porządkowanie rekordów ............................................................................................ 357
Rozdział 33. Relacje n:m (wiele do wielu) ........................................................ 359
Użycie relacji n:m w Symfony 2 ................................................................................. 360
Właściciel relacji n:m .................................................................................................. 361
Tabela łącząca relacji n:m ........................................................................................... 362
Operowanie rekordami powiązanymi relacją .............................................................. 362
Tworzenie rekordów ............................................................................................. 362
Rekordy zależne .................................................................................................... 363
Synchronizacja relacji ........................................................................................... 364
Usuwanie powiązania relacyjnego ........................................................................ 364
Akcje referencyjne SQL .............................................................................................. 365
Akcje SQL-owe ..................................................................................................... 365
Przykład 33.1. Filmy i aktorzy .................................................................................... 365
ROZWIĄZANIE ................................................................................................... 365
Porządkowanie rekordów ............................................................................................ 370
Rozdział 34. Relacje, akcje index i show oraz widoki częściowe ....................... 373
Przykład 34.1. Kontynenty/Państwa — akcje show i widoki częściowe ..................... 375
Przykład 34.2. Filmy/Aktorzy — akcje show i widoki częściowe .............................. 376
Przykład 34.3. Powieści Agaty Christie ...................................................................... 376
ROZWIĄZANIE ................................................................................................... 377
Spis treści 9

Rozdział 35. Podsumowanie części VII ............................................................ 385

Część VIII Panele CRUD i zabezpieczanie dostępu do aplikacji .... 387


Rozdział 36. Generowanie paneli administracyjnych CRUD ............................... 389
Adresy URL akcji CRUD ..................................................................................... 391
Ponowne generowanie paneli CRUD .......................................................................... 394
Panele CRUD a relacje ................................................................................................ 394
Przykład 36.1. Imiona — panel CRUD ....................................................................... 394
ROZWIĄZANIE ................................................................................................... 395
Przykład 36.2. Panel CRUD i relacja 1:1 .................................................................... 396
ROZWIĄZANIE ................................................................................................... 396
Przykład 36.3. Panel CRUD i relacja 1:n .................................................................... 399
ROZWIĄZANIE ................................................................................................... 399
Przykład 36.4. Panel CRUD i relacja n:m ...................................................................... 401
ROZWIĄZANIE ................................................................................................... 401
Rozdział 37. Instalacja pakietu FOSUserBundle ............................................... 403
Przykład 37.1. Przygotowanie dystrybucji symfony2-customized-v4
zawierającej pakiet FOSUserBundle ......................................................................... 403
ROZWIĄZANIE ................................................................................................... 403
Tworzenie kont i nadawanie uprawnień ...................................................................... 408
Tworzenie kont ..................................................................................................... 409
Aktywacja i deaktywacja konta ............................................................................. 409
Nadawanie i usuwanie uprawnień administracyjnych ........................................... 409
Przykład 37.2. Sprawdzenie działania dystrybucji symfony2-customized-v4 ............. 410
ROZWIĄZANIE ................................................................................................... 410
Rozdział 38. Aplikacja dostępna wyłącznie dla zdefiniowanych użytkowników ...... 415
Uprawnienia dostępu ................................................................................................... 415
Role użytkowników ..................................................................................................... 416
Nadawanie, usuwanie i sprawdzanie uprawnień użytkownikom ................................. 417
Przykład 38.1. Korona ziemi ....................................................................................... 419
ROZWIĄZANIE ................................................................................................... 420
Hierarchia ról .............................................................................................................. 427
Rozdział 39. Aplikacja dostępna publicznie w trybie do odczytu ........................ 429
Przykład 39.1. Korona ziemi — podział na frontend oraz backend ............................ 429
ROZWIĄZANIE ................................................................................................... 430
Przekierowania ............................................................................................................ 432
Osadzanie formularza do logowania na stronie głównej ............................................. 434
Przykład 39.2. Korona ziemi — osadzenie formularza do logowania w pliku
base.html.twig ........................................................................................................... 435
ROZWIĄZANIE ................................................................................................... 435
Rozdział 40. Rejestracja użytkowników i odzyskiwanie hasła ........................... 439
Przykład 40.1. Kontynenty/państwa — frontend i backend ........................................ 439
ROZWIĄZANIE ................................................................................................... 439
Przykład 40.2. Kontynenty/państwa — rejestracja użytkowników ............................. 442
ROZWIĄZANIE ................................................................................................... 442
Przykład 40.3. Kontynenty/państwa — odzyskiwanie hasła ....................................... 444
ROZWIĄZANIE ................................................................................................... 444
Rozdział 41. Podsumowanie części VIII ........................................................... 447
10 Symfony 2 od podstaw

Część IX Panele administracyjne Sonata ................................... 449


Rozdział 42. Instalacja pakietów Sonata .............................................................. 451
Przykład 42.1. Przygotowanie dystrybucji symfony2-customized-v5
zawierającej pakiet SonataAdminBundle .................................................................. 451
ROZWIĄZANIE ................................................................................................... 452
Krok 1. Wypakuj dystrybucję Symfony 2.0.X without vendors ............................ 452
Krok 2. Zmodyfikuj plik deps ............................................................................... 452
Krok 3. Pobierz pakiety ......................................................................................... 453
Krok 4. Usuń foldery .git ...................................................................................... 453
Krok 5. Zarejestruj przestrzenie nazw ................................................................... 453
Krok 6. Zarejestruj pakiety .................................................................................... 454
Krok 7. Zmodyfikuj konfigurację projektu ........................................................... 454
Krok 8. Zmodyfikuj zabezpieczenia projektu ....................................................... 455
Krok 9. Utwórz pakiet Application/Sonata/UserBundle ....................................... 457
Krok 10. Zmodyfikuj reguły routingu ................................................................... 457
Krok 11. Zainstaluj style CSS oraz ikony ............................................................. 458
Krok 12. Skompresuj otrzymaną dystrybucję ....................................................... 458
Przykład 42.2. Sprawdź działanie dystrybucji symfony2-customized-v5 ................... 458
ROZWIĄZANIE ................................................................................................... 459
Krok 1. Wypakuj dystrybucję i skonfiguruj bazę danych ..................................... 459
Krok 2. Utwórz tabele w bazie danych ................................................................. 459
Krok 3. Utwórz konto administratora .................................................................... 459
Krok 4. Sprawdź wygląd panelu administracyjnego ............................................. 459
Rozdział 43. Użycie paneli administracyjnych Sonata do własnych tabel ............. 461
Przykład 43.1. Miasta .................................................................................................. 461
ROZWIĄZANIE ................................................................................................... 462
Krok 1. Wypakuj dystrybucję i skonfiguruj bazę danych ..................................... 462
Krok 2. Utwórz pakiet My/Frontend ..................................................................... 462
Krok 3. Utwórz klasę CityAdmin .......................................................................... 462
Krok 4. Włącz panel administracyjny do zarządzania rekordami City .................. 463
Krok 5. Przygotuj plik zawierający tłumaczenia ................................................... 464
Krok 6. Sprawdź wygląd panelu administracyjnego do edycji miast .................... 464
Rozdział 44. Podsumowanie części IX ............................................................. 467
Przykład 44.1. Przygotowanie dystrybucji symfony2-customized-v6
zawierającej omówione pakiety ................................................................................ 467
Przykład 44.2. Rzeki: aplikacja z panelem Sonata ...................................................... 468
ROZWIĄZANIE ................................................................................................... 468
Krok 1. Połącz przykład 18. z dystrybucją symfony2-customized-v6.zip ............. 468
Krok 2. Wykonaj panel Sonata ............................................................................. 469
Przykład 44.3. Kontynenty: aplikacja z panelem Sonata ............................................. 469
ROZWIĄZANIE ................................................................................................... 469
Przykład 44.4. Filmy: aplikacja z panelem Sonata ...................................................... 470
Przykład 44.5. Powieści Agaty Christie: aplikacja z panelem Sonata ......................... 470

Dodatki ..................................................................... 471


Dodatek A Instalacja oprogramowania .......................................................... 473
1. XAMPP ................................................................................................................... 473
2. Modyfikacja konfiguracji PHP ................................................................................ 475
3. Modyfikacja pakietu PEAR ..................................................................................... 476
4. Uaktualnienie biblioteki PEAR ............................................................................... 476
Spis treści 11

5. Code Sniffer ............................................................................................................ 477


6. phpDocumentor ....................................................................................................... 477
7. PHPUnit .................................................................................................................. 477
8. Cygwin .................................................................................................................... 478
9. Ścieżki dostępu ........................................................................................................ 480
10. GraphViz ............................................................................................................... 482
11. NetBeans ............................................................................................................... 482
Skorowidz ................................................................................... 483
12 Symfony 2 od podstaw
.
Podziękowania
Serdecznie dziękuję:
 Fabienowi Potencierowi i wszystkim programistom biorącym udział w rozwoju
Symfony 2 za wysiłek włożony w opracowanie, udokumentowanie i bezpłatne
udostępnienie wspaniałego oprogramowania;
 pracownikom Wydawnictwa Helion, szczególnie Pani Redaktor Ewelinie Burskiej,
za cierpliwość i profesjonalizm;
 studentom Katolickiego Uniwersytetu Lubelskiego im. Jana Pawła II, którzy
w latach 2011 – 2012 uczestniczyli w prowadzonych przeze mnie zajęciach
dotyczących Symfony 2;
 uczestnikom organizowanych przeze mnie szkoleń: Wojciechowi Cupie,
Mateuszowi Draganowi, Tomaszowi Fudali, Andrzejowi Krynieckiemu,
Wojciechowi Matyśkiewiczowi, Piotrowi Piskozubowi, Albertowi Rybackiemu,
Ireneuszowi Sachowi, Pawłowi Zalechowi, Michałowi Zboinie oraz Pawłowi
Zdyblowi, za opinie na temat materiału, który posłużył do opracowania niniejszego
podręcznika;
 moim najbliższym za wsparcie i mobilizację.

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.

Dystrybucje te różnią się wyłącznie zawartością folderu vendor/.

Dystrybucja with vendors — około 6 MB


Dystrybucja with vendors zawiera w folderze vendor/ komplet niezbędnych bibliotek.
Plik ten zajmuje ok. 6 MB i jest to gotowa aplikacja Symfony 2, którą możemy uruchomić.

Dystrybucja Symfony 2.0 with vendors jest odpowiednikiem dystrybucji sandbox


z Symfony 1.4.

Dystrybucja without vendors


— około 200 kB
Dystrybucja without vendors nie zawiera folderu vendor/. Zanim uruchomimy projekt
oparty na tej dystrybucji, musimy doinstalować wszystkie niezbędne pakiety.

W początkowym okresie nauki będziemy wykorzystywali dystrybucję with vendors.


Dystrybucja without vendors stanie się przydatna, gdy zaczniemy wykorzystywać dodat-
kowe pakiety.
18 Część I ♦ Tworzenie prostych stron WWW

Po odwiedzeniu strony http://symfony.com/download pobierz najnowszą wersję dystry-


bucji with vendors. W chwili pisania tego tekstu był to plik Symfony_Standard_Vendors_
2.0.10.zip.

Przykład 1.1. Aplikacja przykładowa


Zanim przejdziemy do nauki Symfony 2, upewnijmy się, że stanowisko pracy jest po-
prawnie skonfigurowane. W tym celu wystarczy uruchomić aplikację przykładową, która
jest zawarta wewnątrz dystrybucji Symfony 2.

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

Poszczególne foldery widoczne na rysunku 1.1 zawierają:


 app/ — pliki konfiguracyjne aplikacji;
 bin/ — polecenia wsadowe dotyczące pakietów dodatkowych (polecenie
bin/vendors wykorzystamy m.in. do instalacji pakietu doctrine-fixtures
ułatwiającego wypełnianie bazy danych rekordami);
 src/ — kod źródłowy aplikacji;
 vendor/ — pakiety dodatkowe, m.in.:

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

 doctrine — oprogramowanie ORM zapewniające dostęp do bazy danych;


 twig — system szablonów;
 swiftmailer — biblioteka ułatwiająca wysyłanie poczty elektronicznej;
 web/ — folder zawierający główny kontroler aplikacji — skrypt app.php
(ang. front controller), style CSS, pliki graficzne oraz pliki JavaScript (jest to jedyny
folder, który jest dostępny publicznie za pomocą protokołu HTTP).

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.

W folderze web/ znajdują się trzy pliki PHP:


 web/app.php,
 web/app_dev.php,
 web/config.php.

Skrypt app.php uruchamia aplikację w środowisku produkcyjnym3, a skrypt app_dev.php


— w środowisku deweloperskim. Skrypt config.php sprawdza natomiast, czy zainstalo-
wane oprogramowanie spełnia minimalne wymagania stawiane przez Symfony 2.

Podane trzy skrypty są dostępne pod adresami:


http://localhost/Symfony/web/config.php
http://localhost/Symfony/web/app.php
http://localhost/Symfony/web/app_dev.php

Krok 2. Sprawdź wygląd strony web/app_dev.php


Uruchom przeglądarkę i odwiedź w niej adres:
http://localhost/Symfony/web/app_dev.php

Powinieneś ujrzeć stronę przedstawioną na rysunku 1.2.

Zanim przejdziesz do kolejnego rozdziału, musisz poprawnie wyświetlić stronę z ry-


sunku 1.2.

Błędy, które mogą wystąpić


Podczas wyświetlania strony z rysunku 1.2 mogą wystąpić następujące błędy:
 Zainstalowane oprogramowanie nie spełnia wymagań Symfony 2.
 Podjęto próbę dostępu do skryptu hello-world/web/app_dev.php poprzez sieć.

3
Więcej o środowiskach w kolejnym rozdziale.
20 Część I ♦ Tworzenie prostych stron WWW

Rysunek 1.2. Strona o adresie web/app_dev.php

 Folder hello-world/app/cache/ zawiera nieaktualną wersję plików i wymaga


odświeżenia.
 Akcelerator4 zapamiętał niepoprawne ścieżki do plików.

Błąd: zbyt stare oprogramowanie


Jeśli po odwiedzeniu adresu:
http://localhost/Symfony/web/app_dev.php

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

Powinieneś ujrzeć stronę widoczną na rysunku 1.3.

4
Błąd ten występuje w systemie Windows 7, gdy w PHP zainstalowany jest akcelerator APC.
Rozdział 1. ♦ Uruchomienie przykładowego projektu 21

Rysunek 1.3. Strona sprawdzająca, czy zainstalowane oprogramowanie jest odpowiednie

Oczywiście należy usunąć wszelkie błędy zgłaszane przez skrypt widoczny na rysunku 1.3.

Błąd: próba zdalnego dostępu do app_dev.php


Jeśli stronę z rysunku 1.2 zechcesz odwiedzić z innego komputera, podając adres serwera,
na którym utworzyłeś folder Symfony/, np.:
http://192.168.0.5/Symfony/web/app_dev.php
http://moj.serwer.example.net/Symfony/web/app_dev.php

ujrzysz wówczas komunikat o zakazie dostępu:


You are not allowed to access this file...

W celu ominięcia powyższego zabezpieczenia usuń z pliku Symfony/web/app_dev.


php kod:
if (!in_array(@$_SERVER['REMOTE_ADDR'], array(
'127.0.0.1',
'::1',
))) {
header('HTTP/1.0 403 Forbidden');
exit('You are not allowed to access this file. Check '.basename(__FILE__).' for
´more information.');
}
22 Część I ♦ Tworzenie prostych stron WWW

Błąd: nieaktualne pliki w folderze app/cache/


Po odwiedzeniu adresu:
http://localhost/Symfony/web/app_dev.php

w folderze Symfony/app/cache/ tworzone są foldery i pliki zawierające przetworzone


informacje o konfiguracji projektu. W niektórych przypadkach (np. wtedy, gdy przenie-
siesz projekt do innego folderu) zawartość folderu Symfony/app/cache/ może być nieak-
tualna. W celu wyczyszczenia pamięci podręcznej projektu usuń wszystkie pliki i foldery
znajdujące się w folderze Symfony/app/cache/.

Błąd: akcelerator nie odświeża ścieżek


Jeśli korzystasz z akceleratora APC, to po przeniesieniu projektu do innego folderu mo-
żesz napotkać problemy polegające na odwoływaniu się przez projekt do nieistniejących
plików oraz folderów. Problem ten wyeliminujesz, restartując serwer Apache.

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

Nie świadczy to o żadnym błędzie. Wszystko przebiega poprawnie. Adres:


http://localhost/hello-world/web/app_dev.php
uruchamia aplikację w środowisku deweloperskim, w którym strona główna wygląda
tak jak na rysunku 1.2.
Adres:
http://localhost/hello-world/web/
uruchamia natomiast aplikację w środowisku produkcyjnym. Ponieważ w tym środowi-
sku aplikacja jest pusta, tj. nie zawiera żadnej strony WWW, wyświetlany jest ko-
munikat o nieodnalezionej stronie WWW: 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

Symfony 2 jest rozpowszechniane w postaci dwóch dystrybucji:


 with vendors,
 without vendors.

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.

Pamiętaj o roli, jaką odgrywają:


 folder app/cache/
 oraz skrypty:
web/config.php
web/app.php
web/app_dev.php

W folderze app/cache/ zapisywana jest zawartość pamięci podręcznej aplikacji. W celu


odświeżenia pamięci podręcznej możesz usunąć zawartość tego folderu.

Podane trzy skrypty PHP mają adresy:


http://localhost/Symfony/web/config.php
http://localhost/Symfony/web/app.php
http://localhost/Symfony/web/app_dev.php

Pierwszy z nich — config.php — sprawdza, czy zainstalowane oprogramowanie spełnia


warunki Symfony 2.

Skrypt app_dev.php wyświetla przykładową aplikację widoczną na rysunku 1.2.

Skrypt app.php powoduje wyświetlenie informacji o błędzie.


24 Część I ♦ Tworzenie prostych stron WWW
Rozdział 2.
Hello, world!
Pierwsza aplikacja, którą wykonamy, ma Cię zapoznać z procesem tworzenia i uru-
chamiania projektu oraz ze strukturą aplikacji. Oprogramowanie Symfony 2 jest zaim-
plementowane obiektowo z wykorzystaniem przestrzeni nazw (ang. namespace). Aplika-
cja tworzona w Symfony 2 jest podzielona na:
 pakiety (ang. bundle),
 kontrolery (ang. controller),
 akcje (ang. action),
 widoki (ang. view).

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

W celu utworzenia w aplikacji klasy o pełnej nazwie:


Lorem\Ipsum\Dolor\Sit.php

należy utworzyć foldery Lorem, Ipsum i Dolor oraz umieścić w nich plik Sit.php:
Lorem\Ipsum\Dolor\Sit.php

Plik Sit.php rozpoczynamy od deklaracji:


namespace Lorem\Ipsum\Dolor;
class Sit {
}

Pełną nazwą klasy jest:


Lorem\Ipsum\Dolor\Sit

W celu utworzenia obiektu klasy Sit możesz wykonać instrukcję:


new Lorem\Ipsum\Dolor\Sit();

W celu skrócenia powyższego zapisu w dowolnym pliku aplikacji możesz dodać in-
strukcję:
use Lorem\Ipsum\Dolor\Sit;

dzięki czemu tworzenie obiektu klasy Sit przyjmie krótszą formę:


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

Pojedynczy pakiet może stanowić małą cząstkę aplikacji, np.:


 KnpMarkdownBundle — zestaw klas do interpretacji plików w języku
Markdown;
 KnpPaginatorBundle — zestaw klas ułatwiających wykonywanie
stronicowania (odpowiednik klas Pager z Symfony 1.4 oraz Zend
Framework).

Przykładami pakietów nieco większych są:


 DoctrineFixturesBundle — obsługa plików fixtures.yml, które ułatwiają
wypełnianie bazy danych;
Rozdział 2. ♦ Hello, world! 27

 DoctrineExtensionsBundle — obsługa zachowań sluggable, timestampable


itd. dla obiektów Doctrine;
 DoctrineMigrationsBundle — obsługa migracji baz danych.

Dużymi fragmentami aplikacji są pakiety:


 SonataAdminBundle — panel administracyjny CRUD;
 FOSUserBundle — administracja kontami użytkowników (m.in. rejestracja
z potwierdzaniem e-mailem, resetowanie i zmiana hasła);
 SonataPageBundle — system CMS;
 FOSCommentBundle — obsługa wielowątkowych komentarzy.

Wspólną cechą wszystkich pakietów jest ich niezależność od konkretnej aplikacji. Celem
tworzenia pakietów jest ułatwienie ponownego wykorzystania fragmentu projektu.

Nazewnictwo pakietów jest dwuczłonowe. Pierwszy człon nazwy pakietu możemy


traktować jako nazwę autora pakietu, a drugi — jako nazwę samego pakietu.

Pakiet Symfony 2 jest odpowiednikiem wtyczki z Symfony 1.4.

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.

Na przykład w projekcie omawianym w bieżącym rozdziale wystąpi klasa Default


´Controller, a w niej metoda:
public function indexAction()
{
}

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.

Dokumentacja Symfony 2 stosuje termin „kontroler” w odniesieniu do zarówno


klasy (np. DefaultController), jak i samej metody (np. indexAction()).
28 Część I ♦ Tworzenie prostych stron WWW

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.

Przykład 2.1. Hello, world!


Wykorzystując oprogramowanie Symfony 2, wykonaj aplikację, która będzie prezento-
wała stronę WWW z tekstem Hello, world!. Zadanie rozwiąż w taki sposób, by adres
strony kończył się napisem /hello-world.html.

Jeśli podczas wykonywania przykładu ujrzysz białą stronę, pamiętaj, że pierwszym


krokiem do usunięcia powstałego błędu jest wyczyszczenie pamięci podręcznej i odświe-
żenie strony. W tym celu najlepiej wyczyścić zawartość folderu app/cache/. Komenda:
php app/console cache:clear
często zawodzi. Drugim krokiem, który stosuję w przypadku błędnego działania akcele-
ratora APC, jest restart serwera Apache. Jeśli i to nie pomaga, sprawdzam, jakie
informacje zwraca skrypt web/config.php.

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.

Krok 2. Usuń pakiet demo


Plik Symfony_Standard_Vendors_2.0.X.zip zawiera pakiet demonstracyjny, który nie
jest konieczny do tworzenia nowych aplikacji. W celu usunięcia pakietu demo:
 Usuń folder hello-world/src/Acme/.
 W pliku hello-world/app/AppKernel.php usuń wpis:
$bundles[] = new Acme\DemoBundle\AcmeDemoBundle();

 W pliku hello-world/app/config/routing_dev.php usuń wpisy przedstawione


na listingu 2.1.
 Usuń folder hello-world/web/bundles/acmedemo/.

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!

Listing 2.1. Reguły, które należy usunąć z pliku hello-world/app/config/routing_dev.php


_welcome:
pattern: /
defaults: { _controller: AcmeDemoBundle:Welcome:index }

_demo_secured:
resource: "@AcmeDemoBundle/Controller/SecuredController.php"
type: annotation

_demo:
resource: "@AcmeDemoBundle/Controller/DemoController.php"
type: annotation
prefix: /demo

Krok 3. Tworzenie pakietu

Pakiety tworzymy, uruchamiając w wierszu poleceń komendę:


php app/console generate:bundle

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.

Krok 3.1. Utwórz nowy pakiet My/HelloworldBundle


Uruchom wiersz poleceń i komendami cd przejdź do folderu hello-world/. Następnie
wydaj komendę:
php app/console generate:bundle

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

wprowadź nazwę pakietu:


My/HelloworldBundle

Nazwa pakietu jest dwuczłonowa. Pierwszy człon może być traktowany jako oznaczenie
autora pakietu. Nazwa:
My/HelloworldBundle

spowoduje utworzenie w folderze src/ folderów:


hello-world/
src/
My/
HelloworldBundle/

Cały kod tworzonego pakietu zostanie umieszczony wewnątrz folderu:


hello-world/src/My/HelloworldBundle/.

Nazwa pakietu musi się kończyć przyrostkiem Bundle. Poprawnymi nazwami pa-
kietów są:
Gajdaw/HelloworldBundle
GW/HelloBundle
Gajda/HelloWorldBundle

Komenda app/console generate:bundle działa w sposób interaktywny i umożliwia dosto-


sowanie kilku parametrów generowanego pakietu. Możemy:
 zmienić folder, w którym zostanie umieszczony pakiet;
 ustalić sposób konfiguracji pakietu (YML, XML, PHP lub adnotacje2);
 włączyć tworzenie kompletnej struktury pakietu (po włączeniu tej opcji
wygenerowane zostaną m.in. puste foldery public/css/ oraz public/images/
przeznaczone na style CSS oraz obrazy);
 dołączyć nowy pakiet do konfiguracji aplikacji;
 zmodyfikować reguły routingu adresów URL tak, by uwzględniony został
nowo tworzony pakiet.

Interaktywny generator podpowiada domyślne wartości opcji:


Bundle name [MyHelloworldBundle]:
Target directory [...\hello-world/src]:
Configuration format (yml, xml, php, or annotation) [annotation]:
Do you want to generate the whole directory structure [no]?
Do you confirm generation [yes]?
Confirm automatic update of your Kernel [yes]?
Confirm automatic update of the Routing [yes]?

2
Adnotacje są specjalnymi komentarzami umieszczanymi w kodzie PHP.
Rozdział 2. ♦ Hello, world! 31

Na wszystkie powyższe pytania możemy odpowiedzieć, naciskając przycisk Enter.


Opcje przyjmą wówczas wartości podane w nawiasach kwadratowych.

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:

php app/console generate:bundle --namespace=My/HelloworldBundle --dir=src


´--no-interaction

Wymaganymi parametrami polecenia generate:bundle są:


 --namespace — parametr ustalający nazwę pakietu;

 --dir — parametr ustalający położenie generowanego pakietu.

Ostatnia z opcji, --no-interaction, wyłącza tryb interaktywny.

Krok 3.2. Sprawdź strukturę wygenerowanego pakietu


Po wydaniu komendy app/console generate:bundle w folderze hello-world/src/ utwo-
rzony zostanie pakiet, którego foldery i pliki przedstawiono na rysunku 2.2.

Rysunek 2.2.
Foldery i pliki
utworzone po wydaniu
komendy app/console
generate:bundle

Bezpośrednio w folderze hello-world/src/ znajduje się plik .htaccess zawierający instrukcję:


deny from all

Blokuje on dostęp do zawartości folderu hello-world/src/ za pomocą protokołu HTTP3.

Pakiet hello-world/src/My/HelloworldBundle/ zawiera następujące foldery:

3
Innymi słowy do folderu hello-world/src/ nie da się zajrzeć za pomocą przeglądarki.
32 Część I ♦ Tworzenie prostych stron WWW

 Controller/ — folder przeznaczony na kontrolery;


 DependencyInjection/ — folder konfigurujący zależności pomiędzy
obiektami;
 Resources/ — folder zawierający zasoby pakietu (m.in. szablony, style CSS
oraz obrazy);
 Tests/ — folder przeznaczony na testy jednostkowe.

Jeśli na pytanie:
Do you want to generate the whole directory structure [no]?

odpowiesz:
yes

wówczas w folderze Resources/ wygenerowane zostaną dodatkowo foldery i pliki wi-


doczne na rysunku 2.3. Folder doc/ jest przeznaczony na dokumentację, public/ — na
style CSS, skrypty JavaScript oraz obrazy, a translations/ — na pliki tłumaczeń.

Rysunek 2.3.
Kompletna struktura
zasobów z folderu
Resources/

Krok 3.3. Sprawdź zawartość pliku app/AppKernel.php


Opcja konfiguracyjna:
Confirm automatic update of your Kernel [yes]?

powoduje włączenie nowo tworzonego pakietu w konfiguracji aplikacji. W pliku


hello-world/app/AppKernel.php w tablicy $bundles utworzony zostanie obiekt klasy
MyHelloworldBundle:
$bundles = array(
...
new My\HelloworldBundle\MyHelloworldBundle(),
);

Plik AppKernel.php jest przedstawiony na listingu 2.2.


Rozdział 2. ♦ Hello, world! 33

Listing 2.2. Plik AppKernel.php


<?php

use Symfony\Component\HttpKernel\Kernel;
use Symfony\Component\Config\Loader\LoaderInterface;

class AppKernel extends Kernel


{
public function registerBundles()
{
$bundles = array(
new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
new Symfony\Bundle\SecurityBundle\SecurityBundle(),
new Symfony\Bundle\TwigBundle\TwigBundle(),
new Symfony\Bundle\MonologBundle\MonologBundle(),
new Symfony\Bundle\SwiftmailerBundle\SwiftmailerBundle(),
new Symfony\Bundle\DoctrineBundle\DoctrineBundle(),
new Symfony\Bundle\AsseticBundle\AsseticBundle(),
new Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle(),
new JMS\SecurityExtraBundle\JMSSecurityExtraBundle(),
new My\HelloworldBundle\MyHelloworldBundle(),
);

if (in_array($this->getEnvironment(), array('dev', 'test'))) {


$bundles[] = new Symfony\Bundle\WebProfilerBundle\WebProfilerBundle();
$bundles[] = new Sensio\Bundle\DistributionBundle\SensioDistributionBundle();
$bundles[] = new Sensio\Bundle\GeneratorBundle\SensioGeneratorBundle();
}

return $bundles;
}

public function registerContainerConfiguration(LoaderInterface $loader)


{
$loader->load(__DIR__.'/config/config_'.$this->getEnvironment().'.yml');
}
}

W celu wyłączenia pakietu MyHelloworldBundle należy usunąć z pliku AppKernel.php


linijkę:
new My\HelloworldBundle\MyHelloworldBundle(),

Krok 3.4. Sprawdź zawartość pliku app/config/routing.yml


Opcja konfiguracyjna:
Confirm automatic update of the Routing [yes]?

dodaje wpis do reguł routingu adresów URL. W pliku hello-world/app/config/routing.yml


pojawia się reguła przedstawiona na listingu 2.3.
34 Część I ♦ Tworzenie prostych stron WWW

Listing 2.3. Reguły routingu dla pakietu MyHelloworldBundle


MyHelloworldBundle:
resource: "@MyHelloworldBundle/Controller/"
type: annotation
prefix: /

Opcja:
resource: "@MyHelloworldBundle/Controller/"

powoduje dołączenie reguł routingu zawartych we wszystkich kontrolerach z folderu


hello-world/src/My/HelloworldBundle/Controller/. Opcja:
type: annotation

ustala, że dołączane reguły routingu są opisane w plikach .php w formie adnotacji.


Wreszcie ostatnia opcja:
prefix: /

ustala przedrostek dołączanych reguł routingu.

Krok 3.5. Sprawdź zawartość kontrolera DefaultController.php


Nowo generowany pakiet zawiera jeden kontroler hello-world/src/My/HelloworldBundle/
Controller/DefaultController.php. W kontrolerze tym jest zdefiniowana jedna akcja o na-
zwie index. Zawartość domyślnego pliku DefaultController.php jest przedstawiona na
listingu 2.4.

Listing 2.4. Domyślny kontroler DefaultController.php


<?php

namespace My\HelloworldBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;

class DefaultController extends Controller


{
/**
* @Route("/hello/{name}")
* @Template()
*/
public function indexAction($name)
{
return array('name' => $name);
}
}
Rozdział 2. ♦ Hello, world! 35

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.

Krok 3.6. Sprawdź wygląd domyślnej strony wygenerowanego pakietu


Podczas tworzenia pakietu ustaliliśmy, że jego konfiguracja będzie zapisywana w postaci
adnotacji:
Configuration format (yml, xml, php, or annotation) [annotation]:

Na tej podstawie zapis z listingu 2.3 zawiera wiersz:


type: annotation

a kod z listingu 2.4 zawiera komentarze:


/**
* @Route("/hello/{name}")
* @Template()
*/

Adnotacje są specjalnym rodzajem komentarzy, które konfigurują pakiet. Zaletą adnotacji


jest to, że są one zapisane w plikach kontrolerów. W ten sposób kod kontrolera oraz jego
opcje są dostępne w jednym miejscu.

Adnotacja:
@Route("/hello/{name}")

definiuje regułę routingu, na podstawie której adres:


/hello/abc

odpowiada wykonaniu akcji index kontrolera DefaultController w pakiecie MyHelloworld


´Bundle4. Do metody akcji zostanie przekazany parametr abc. Widoczna na listingu 2.4
instrukcja return powoduje przekazanie do widoku akcji zmiennej o nazwie name
i wartości abc.

Adnotacja @Template() metody indexAction() powoduje, że po wykonaniu akcji


index kontrolera DefaultController przetwarzany będzie szablon hello-world/src/My/
HelloworldBundle/Resources/views/Default/index.html.twig. Zawartość szablonu jest
widoczna na listingu 2.5.

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

Listing 2.5. Szablon Resources/views/Default/index.html.twig


Hello {{ name }}!

Zapis:
{{ name }}

powoduje wydrukowanie zmiennej przekazanej do widoku z metody indexAction().

Na podstawie adnotacji z listingu 2.4 wiemy, że aplikacja stosuje adresy:


http://localhost/hello-world/web/hello/abc
http://localhost/hello-world/web/hello/lorem
http://localhost/hello-world/web/hello/john

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

Krok 4. Dostosuj kontroler DefaultController


Zmodyfikuj zawartość pliku hello-world/src/My/HelloworldBundle/Controller/Default
Controller.php tak, jak to zostało przedstawione na listingu 2.6.

Listing 2.6. Zmodyfikowany plik DefaultController.php


<?php

namespace My\HelloworldBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;

class DefaultController extends Controller


{
/**
* @Route("/hello-world.html")
* @Template()
*/
public function indexAction()
{
return array();
}
}

Adnotacja:
* @Route("/hello-world.html")

powoduje, że adresem akcji index kontrolera DefaultController w pakiecie MyHelloworld


´Bundle będzie:
http://localhost/hello-world/web/hello-world.html

Z nagłówka metody indexAction() usuwamy parametr $name. Metoda akcji jest bezpa-
rametrowa:
public function indexAction()

Tablica zwracana jako wynik akcji jest pusta:


return array();

zatem do widoku nie przekazujemy żadnych zmiennych.


38 Część I ♦ Tworzenie prostych stron WWW

Krok 5. Dostosuj widok akcji index


W pliku hello-world/src/My/HelloworldBundle/Resources/views/Default/index.html.twig
wprowadź kod przedstawiony na listingu 2.7.

Listing 2.7. Zmodyfikowany widok index.html.twig


<!DOCTYPE html>
<html>
<head>
<title>Hello, world!</title>
<meta charset="UTF-8" />
</head>
<body>

<h1>Hello, world!</h1>

</body>
</html>

Krok 6. Sprawdź wygląd strony


Po odwiedzeniu adresu:
http://localhost/hello-world/web/app_dev.php/hello-world.html
ujrzysz stronę przedstawioną na rysunku 2.5.

Rysunek 2.5. Strona otrzymana po wprowadzeniu modyfikacji z listingów 2.6 oraz 2.7
Rozdział 2. ♦ Hello, world! 39

Wykonana strona jest dostępna pod trzema różnymi adresami URL:


http://localhost/hello-world/web/app_dev.php/hello-world.html
http://localhost/hello-world/web/app.php/hello-world.html
http://localhost/hello-world/web/hello-world.html

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

Wykonanie przykładu rozpoczęliśmy od usunięcia pakietu demo. W tym celu zmodyfi-


kowaliśmy dwa pliki:
 W pliku routing_dev.yml usunęliśmy reguły dotyczące pakietu
Acme/DemoBundle.
 W pliku AppKernel.php usunęliśmy pakiet AcmeDemoBundle.

Następnie w pliku DefaultController.php zmodyfikowaliśmy metodę akcji index, po czym


zmieniliśmy widok akcji index, który jest zawarty w pliku index.html.twig.

Ś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).

W folderze web/ znajdziemy dwa front kontrolery:


 app.php — front kontroler uruchamiający aplikację w środowisku prod;
 app_dev.php — front kontroler uruchamiający aplikację w środowisku dev.

Zajrzyjmy do pliku app.php. Znajdziemy w nim instrukcję tworzącą obiekt $kernel:


$kernel = new AppKernel('prod', false);

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.

W pliku app_dev.php występuje instrukcja:


$kernel = new AppKernel('dev', true);

Uruchamia ona aplikację w środowisku dev i przy włączonych komunikatach diagno-


stycznych. Dlatego jeśli użyjemy adresu:
http://localhost/hello-world/web/app_dev.php/hello-world.html
wówczas w przypadku błędów w przeglądarce ujrzymy komunikaty, które ułatwią lo-
kalizację błędnego kodu.
Rozdział 2. ♦ Hello, world! 41

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

Jeśli natomiast parametrem jest dev, to konfiguracja zostanie odczytana z pliku:


app/config/config.yml
app/config/config_dev.yml
app/config/routing.yml
app/config/routing_dev.yml
...

Pliki:
app/config/config.yml
app/config/routing.yml

są przeznaczone dla wszystkich środowisk. Plik:


app/config/config_prod.yml

jest przeznaczony wyłącznie dla środowiska prod, a pliki:


app/config/config_dev.yml
app/config/routing_dev.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".

Innymi słowy konfiguracja środowiska produkcyjnego nie zawiera zdefiniowanej reguły


routingu dla adresu /.

Jeśli aplikacja zawiera błąd i została uruchomiona w środowisku produkcyjnym, wów-


czas w oknie przeglądarki może pojawić się pusta biała strona. Pamiętaj, że w takiej
sytuacji należy użyć środowiska deweloperskiego. W tym celu w adresie URL odwie-
dzonej strony dodaj ręcznie nazwę pliku:
.../web/app_dev.php/...
42 Część I ♦ Tworzenie prostych stron WWW

Tworzenie i usuwanie pakietów


Do tworzenia pakietów służy poznane polecenie:
php app/console generate:bundle

W większości przykładów użyjemy polecenia w wersji wsadowej, rezygnując z interakcji:


php app/console generate:bundle --namespace=My/HelloworldBundle --dir=src
´--no-interaction

Pełna nazwa tworzonego pakietu to:


My/HelloworldBundle

Nazwa ta będzie w wielu miejscach (m.in. w plikach konfiguracyjnych oraz w adnota-


cjach) zapisywana w postaci:
MyHelloworldBundle

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(),

 W pliku app/config/routing.yml dodaje instrukcje konfigurujące routing, np.:


MyFrontendBundle:
resource: "@MyHelloworldBundle/Controller/"
type: annotation
prefix: /

W celu usunięcia pakietu:


 Usuń folder pakietu, np. src/My/LoremBundle/.
 W pliku app/AppKernel.php usuń instrukcję tworzącą obiekt, np.:
$bundles[] = new My\LoremBundle\MyLoremBundle();

 W pliku app/config/routing.yml usuń reguły dotyczące pakietu.

Użycie przestrzeni nazewniczych


Cały kod Symfony 2, w tym także wygenerowany pakiet, stosuje przestrzenie nazewni-
cze. Widoczny na listingu 2.2 kod klasy DefaultController rozpoczyna się od dekla-
racji przestrzeni nazewniczej (instrukcja namespace) oraz dołączenia wybranych klas
(instrukcje use):
Rozdział 2. ♦ Hello, world! 43

<?php

namespace My\HelloworldBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;

class DefaultController extends Controller


{
...
}

Instrukcja namespace ustala przestrzeń nazewniczą dla klasy DefaultController. W do-


wolnym miejscu aplikacji możemy utworzyć obiekt klasy DefaultController, wywołując
operator new w następujący sposób:
new My\HelloworldBundle\Controller\DefaultController();

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();

Taką rolę odgrywają trzy instrukcje:


use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;

Pierwsza z nich powoduje, że odwołania do klasy o skróconej nazwie Controller będą


interpretowane jako odwołania do klasy, której pełna nazwa brzmi:
Symfony\Bundle\FrameworkBundle\Controller\Controller

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

W analogiczny sposób instrukcje:


use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;

pozwalają na stosowanie skróconych nazw Route oraz Template.

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

Dostępnymi formatami konfiguracji są:


 pliki w języku YAML,
 pliki w języku XML,
 pliki w języku PHP
 oraz adnotacje, czyli komentarze zawarte w plikach PHP.

Każdy pakiet Symfony 2 może stosować dowolny format konfiguracji. W ramach


jednego projektu możesz stosować pakiety wykorzystujące dowolne formaty. W pojedyn-
czym pakiecie musisz jednak stosować jeden wybrany format — nie możesz w obrębie
pakietu łączyć dwóch formatów (np. XML oraz adnotacji).

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));

Konfiguracja routingu zostanie natomiast zapisana w pliku:


src\My\HelloworldBundle\Resources\config\routing.yml

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">

<route id="MyHelloworldBundle_homepage" pattern="/hello/{name}">


<default key="_controller">MyHelloworldBundle:Default:index</default>
</route>
</routes>

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;

$collection = new RouteCollection();


$collection->add('MyHelloworldBundle_homepage', new Route('/hello/{name}', array(
'_controller' => 'MyHelloworldBundle:Default:index',
)));

return $collection;

Uruchomienie gotowego przykładu


Wszystkie omówione przykłady są dostępne pod adresem ftp://ftp.helion.pl/przyklady/
symfo2.zip. Przykład pierwszy jest umieszczony w pliku 02-rozwiazanie.zip. W celu uru-
chomienia projektu:
1. Rozpakuj archiwum 02-rozwiazanie.zip. Wypakowane pliki umieść w folderze
przeznaczonym na skrypty PHP (np. C:/xampp/htdocs/).
2. Odwiedź w przeglądarce plik 02-rozwiazanie/web/app_dev.php/hello-
world.html.
3. Powinieneś ujrzeć taką stronę jak na rysunku 2.5.

W folderze 02-rozwiazanie/00-dodatki/ znajdziesz plik polecenia.txt zawierający wszystkie


polecenia, które zostały wydane w konsoli w trakcie przygotowywania przykładu.
Rozdział 3.
Dołączanie
zewnętrznych zasobów
Strony WWW oprócz tekstu zawierają ilustracje, skrypty JavaScript oraz style CSS. Pra-
cując w Symfony 2, obrazy oraz pliki o rozszerzeniach .css i .js należy umieścić w folde-
rze [projekt]/web/. Jest to jedyny folder aplikacji, który udostępniamy za pomocą
protokołu HTTP.

W widokach ścieżki do zasobów z folderu web/ generujemy, wywołując funkcję pomoc-


niczą asset(). Jeśli w folderze web/ umieścimy foldery i pliki:
[projekt]/web/css/style.css
[projekt]/web/js/skrypt.js
[projekt]/web/images/zdjecie.jpg

to w celu wygenerowania znaczników1:


<link rel="stylesheet" href=".../web/css/style.css" />
<script src=".../web/js/skrypt.js"></script>
<img src=".../web/images/zdjecie.jpg" />

należy funkcję asset() wywołać następująco:


<link rel="stylesheet" href="{{ asset('css/style.css') }}" media="screen" />
<script src="{{ asset('js/skrypt.js') }}"></script>
<img src="{{ asset('images/zdjecie.jpg') }}" />

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

Jeśli po wydaniu komendy:


php app/console generate:bundle

na pytanie:
Do you want to generate the whole directory structure [no]?

odpowiesz:
yes

wówczas w folderze pakietu utworzone zostaną puste foldery2 przedstawione na ry-


sunku 3.1.

Rysunek 3.1.
Foldery przeznaczone
na pliki graficzne, style
CSS oraz skrypty
JavaScript

Pliki zawarte w folderach z rysunku 3.1:


[pakiet]/Resources/public/css/style.css
[pakiet]/Resources/public/images/zdjecie.jpg
[pakiet]/Resources/public/js/skrypt.js

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

Ścieżki prowadzące do powyższych zasobów możemy wygenerować wywołaniami:


<link href="{{ asset('bundles/[pakiet]/css/style.css') }}" ... />
<script src="{{ asset('bundles/[pakiet]/js/skrypt.js') }}" ... ></script>
<img src="{{ asset('bundles/[pakiet]/images/zdjecie.jpg') }}" ... />

Oczywiście w powyższych wywołaniach napis:


[pakiet]

należy zastąpić nazwą pakietu.

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

Przykład 3.1. Pusta Dolinka


Wykorzystując oprogramowanie Symfony 2, wykonaj aplikację, która będzie prezento-
wała stronę WWW ze zdjęciem przedstawiającym tatrzańską Pustą Dolinkę. Wykorzystaj
style CSS oraz skrypty JavaScript zawarte w pliku 03-01-start.zip. Zadanie wykonaj
w taki sposób, by zdjęcie Pustej Dolinki było wyświetlane od razu po odwiedzeniu
w przeglądarce folderu web/. Zasoby (tj. pliki .css, .js oraz .jpg) umieść ręcznie wewnątrz
folderu web/.

Plik 03-01-start.zip zawierający wszystkie niezbędne zasoby znajdziesz w archiwum,


które zawiera wszystkie przykłady opisane w książce: ftp://ftp.helion.pl/przyklady/
symfo2.zip.

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.

Krok 2. Z projektu usuń pakiet demo


Usuń folder:
mountains/src/Acme/
50 Część I ♦ Tworzenie prostych stron WWW

W pliku mountains/app/AppKernel.php usuń wiersz:


$bundles[] = new Acme\DemoBundle\AcmeDemoBundle();

W pliku mountains/app/config/routing_dev.yml usuń reguły:


_welcome:
pattern: /
defaults: { _controller: AcmeDemoBundle:Welcome:index }

_demo_secured:
resource: "@AcmeDemoBundle/Controller/SecuredController.php"
type: annotation

_demo:
resource: "@AcmeDemoBundle/Controller/DemoController.php"
type: annotation
prefix: /demo

Krok 3. Skopiuj zasoby .css, .js i .jpg


W folderze mountains/web/ utwórz foldery css/, js/ i images/ oraz przekopiuj do nich
pliki:
mountains/web/css/style.css
mountains/web/images/pusta-dolinka.jpg
mountains/web/js/jquery-1.6.4.min.js
mountains/web/js/animacja.js

Krok 4. Utwórz pakiet valley


W wierszu poleceń wydaj komendę:
php app/console generate:bundle

W odpowiedzi na monit:
Bundle namespace:

wprowadź nazwę pakietu:


My/ValleyBundle

Wszystkie opcje pakietu pozostaw domyślne.

Krok 5. Dostosuj kontroler DefaultController


W pliku mountains/src/My/ValleyBundle/Controller/DefaultController.php wprowadź
kod przedstawiony na listingu 3.1.

Listing 3.1. Zmodyfikowany plik DefaultController.php


class DefaultController extends Controller
{
/**
Rozdział 3. ♦ Dołączanie zewnętrznych zasobów 51

* @Route("/valley.html")
* @Template()
*/
public function indexAction()
{
return array();
}
}

W adnotacjach należy stosować cudzysłów:


PRZYKŁAD POPRAWNY
/**
* @Template("MyLoremBundle:Ipsum:dolor.html.twig")
*/
Użycie apostrofów jest niedozwolone:
PRZYKŁAD BŁĘDNY
/**
* @Template('MyLoremBundle:Ipsum:dolor.html.twig')
*/

Krok 6. Dostosuj widok akcji index


W pliku mountains/src/My/ValleyBundle/Resources/views/Default/index.html.twig wpro-
wadź kod przedstawiony na listingu 3.2.

Listing 3.2. Zmodyfikowany widok index.html.twig


<!DOCTYPE html>
<html>
<head>
<title>Pusta Dolinka</title>
<meta charset="UTF-8" />
<link rel="stylesheet" href="{{ asset('css/style.css') }}" media="screen" />
<script src="{{ asset('js/jquery-1.6.4.min.js') }}"></script>
<script src="{{ asset('js/animacja.js') }}"></script>
</head>
<body>

<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

Krok 7. Zdefiniuj adres /


Na początku pliku mountains/app/config/routing.yml dodaj reguły3:
_homepage:
pattern: /
defaults: { _controller: MyValleyBundle:Default:index }

Wartością parametru pattern jest adres URL, a parametru _controller — akcja index
w kontrolerze DefaultController pakietu My/ValleyBundle.

Po odwiedzeniu w przeglądarce folderu mountains/web/ ujrzysz stronę przedstawioną


na rysunku 3.2.

Rysunek 3.2. Strona z przykładu 3.1

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

Strona z rysunku 3.2 jest dostępna pod dwoma różnymi adresami:


http://localhost/mountains/web/
http://localhost/mountains/web/valley.html
Pierwszy z nich jest zdefiniowany w pliku mountains/app/config/routing.yml. Drugi
występuje w formie adnotacji na listingu 3.1.

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.

Krok 2. Z projektu usuń pakiet demo


Usuń folder:
tatras/src/Acme/

W pliku tatras/app/AppKernel.php usuń wiersz:


$bundles[] = new Acme\DemoBundle\AcmeDemoBundle();

W pliku tatras/app/config/routing_dev.yml usuń reguły:


_welcome:
pattern: /
defaults: { _controller: AcmeDemoBundle:Welcome:index }

_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

Krok 3. Utwórz pakiet My/LakeBundle


W wierszu poleceń wydaj komendę:
php app/console generate:bundle

W odpowiedzi na monit:
Bundle namespace:

wprowadź nazwę pakietu:


My/LakeBundle

W odpowiedzi na pytanie:
Do you want to generate the whole directory structure [no]?

odpowiedz:
yes

Na pozostałe pytania odpowiedz, naciskając przycisk Enter.

Krok 4. Skopiuj zasoby .css, .js i .jpg


W folderze tatras/src/My/LakeBundle/ umieść pliki:
tatras/src/My/LakeBundle/Resources/public/css/style.css
tatras/src/My/LakeBundle/Resources/public/images/dolina-pieciu-stawow.jpg
tatras/src/My/LakeBundle/Resources/public/js/jquery-1.6.4.min.js
tatras/src/My/LakeBundle/Resources/public/js/animacja.js

Wymienione powyżej pliki znajdziesz w archiwum 03-02-start.zip.

Krok 5. Dostosuj kontroler DefaultController


W pliku tatras/src/My/LakeBundle/Controller/DefaultController.php wprowadź kod przed-
stawiony na listingu 3.3.

Listing 3.3. Zmodyfikowany plik DefaultController.php


class DefaultController extends Controller
{
/**
* @Route("/lake.html")
* @Template()
*/
public function indexAction()
Rozdział 3. ♦ Dołączanie zewnętrznych zasobów 55

{
return array();
}
}

Krok 6. Dostosuj widok akcji index


W pliku tatras/src/My/LakeBundle/Resources/views/Default/index.html.twig wprowadź
kod przedstawiony na listingu 3.4.

Listing 3.4. Zmodyfikowany widok index.html.twig


<!DOCTYPE html>
<html>
<head>
<title>Tatry / Dolina Pięciu Stawów Polskich / Wielki Staw Polski</title>
<meta charset="UTF-8" />
<link rel="stylesheet" href="{{ asset('bundles/mylake/css/style.css') }}"
´media="screen" />
<script src="{{ asset('bundles/mylake/js/jquery-1.6.4.min.js') }}"></script>
<script src="{{ asset('bundles/mylake/js/animacja.js') }}"></script>
</head>
<body>

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

Krok 7. Zdefiniuj adres /


W pliku tatras/app/config/routing.yml wprowadź reguły:
_homepage:
pattern: /
defaults: { _controller: MyLakeBundle:Default:index }

Oczywiście adres / możesz również zdefiniować w postaci adnotacji w kontrolerze:


/**
* @Route("/")
* @Template()
*/
56 Część I ♦ Tworzenie prostych stron WWW

Krok 8. Opublikuj zasoby .css, .js i .jpg


Wydaj polecenie:
php app/console assets:install web

W folderze tatras/web/ pojawi się folder bundles/mylake/, zawierający zasoby .css, .js
oraz .jpg.

Po odwiedzeniu w przeglądarce folderu tatras/web/ ujrzysz stronę przedstawioną na


rysunku 3.3.

Rysunek 3.3. Strona z przykładu 3.2


Rozdział 4.
Szablon witryny
Witryny składające się z wielu stron WWW zazwyczaj zawierają wspólny układ graficzny
nazywany szablonem (ang. layout). Aby uniknąć powielania kodu HTML szablonu,
generowanie stron WWW odbywa się dwustopniowo. Widok akcji generuje treść cha-
rakterystyczną dla konkretnej strony, a następnie dekoruje ją, wykorzystując do tego
szablon współdzielony przez wiele stron.

Przeanalizujmy kod strony WWW z listingu 4.1. Strona ta prezentuje tekst jednego
wiersza.

Listing 4.1. Strona WWW prezentująca wiersz


<!DOCTYPE html>
<html>
<head>
<title>Wierszyki...</title>
</head>
<body>

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

Listing 4.4. Plik z tekstem wyliczanki


<h1>Ene, due</h1>
<p>
Ene, due, rike, fake<br />
Torbe, borbe, ósme, smake<br />
Eus, deus, kosmateus<br />
I morele baks.
</p>
Rozdział 4. ♦ Szablon witryny 59

Na tym polega idea dekoracji widoków akcji przy użyciu wspólnego szablonu stron.

W Symfony 2 dekorację wyniku przetwarzania akcji włączamy w widoku. Jeśli w projekcie


występuje pakiet My/LoremBundle zawierający kontroler DefaultController oraz akcję index,
wówczas struktura plików i folderów będzie następująca:
[projekt]/
My/
LoremBundle/
Controller/
DefaultController.php
Resources/
views/
Default/
index.html.twig

Współdzielony szablon nazywamy layout.html.twig i umieszczamy w folderze views/:


[projekt]/
My/
Resources/
views/
layout.html.twig

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.

Listing 4.5. Zarys pliku layout.html.twig


...

<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

Listing 4.6. Widok akcji index


{% extends "MyLoremBundle::layout.html.twig" %}

{% 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

która składa się z:


 nazwy pakietu (MyLoremBundle),
 pustej nazwy kontrolera,
 nazwy pliku (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.

Druga z instrukcji widocznych na listingu 4.6, czyli:


{% block content %}
...
{% endblock %}

ustala treść, która zostanie umieszczona w bloku content z listingu 4.5.

W ten sposób na podstawie dwóch plików, layout.html.twig oraz index.html.twig, wyge-


nerowana zostanie strona WWW o kodzie takim jak na listingu 4.1.

Przykład 4.1. Dwa kabele


Wykorzystując oprogramowanie Symfony 2, wykonaj aplikację, która będzie prezento-
wała stronę WWW z treścią wierszyka pt. Dwa kabele. Zadanie wykonaj w taki sposób,
by strona była dostępna pod adresem:
.../web/
Rozdział 4. ♦ Szablon witryny 61

oraz by kod HTML był generowany z wykorzystaniem pliku layout.html.twig, ustalającego


szablon HTML/CSS. Treść wiersza oraz style CSS znajdziesz w pliku 04-start.zip.

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

Krok 2. Utwórz pakiet My/PoemBundle


Komendą:
php app/console generate:bundle

utwórz pakiet My/PoemBundle. Wszystkie opcje pakietu pozostaw domyślne.

Krok 3. Dostosuj kontroler DefaultController


W pliku DefaultController.php wprowadź kod przedstawiony na listingu 4.7.

Listing 4.7. Zmodyfikowany plik DefaultController.php


class DefaultController extends Controller
{
/**
* @Route("/")
* @Template()
*/
public function indexAction()
{
return array();
}
}

Zwróć uwagę, że adnotacja @Route() zawiera adres /. Dzięki temu strona akcji index
będzie wyświetlana po odwiedzeniu w przeglądarce folderu:
.../web/

W ten sposób unikniemy konieczności modyfikowania pliku poems/app/config/routing.yml


w celu zdefiniowania adresu /.

Krok 4. Utwórz szablon layout.html.twig


W folderze poems/src/My/PoemBundle/Resources/views/ utwórz plik layout.html.twig
o zawartości takiej jak na listingu 4.8.
62 Część I ♦ Tworzenie prostych stron WWW

Listing 4.8. Plik layout.html.twig


<!DOCTYPE html>
<html>
<head>
<title>
Wiersze i wierszyki...
</title>
<meta charset="UTF-8" />
<link rel="stylesheet" href="{{ asset('css/style.css') }}" media="screen" />
<link rel="stylesheet" href="{{ asset('css/print.css') }}" media="print" />
</head>
<body>

<div id="pojemnik">
<h1 id="naglowek">Wiersze i wierszyki...</h1>
<div id="tekst">
{% block content %}
{% endblock %}
</div>
<div id="dol"></div>
</div>

</body>
</html>

Krok 5. Dostosuj widok akcji index


W pliku index.html.twig wprowadź kod przedstawiony na listingu 4.9.

Listing 4.9. Zmodyfikowany widok index.html.twig


{% extends "MyPoemBundle::layout.html.twig" %}

{% 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 %}

Krok 6. Skopiuj zasoby .css, .js i .jpg


W folderze poems/web/ umieść pliki:
poems/web/css/style.css
poems/web/css/print.css
Rozdział 4. ♦ Szablon witryny 63

poems/web/images/gora.png
poems/web/images/naglowek.png
poems/web/images/pojemnik.png
poems/web/images/stopka.png

Odwołania do plików graficznych występują wyłącznie w arkuszach stylów CSS i nie


pojawiają się w kodzie HTML. Dlatego ani w widoku akcji index, ani w szablonie
layout.html.twig nie występują wywołania funkcji asset() dołączające pliki graficzne.

Po odwiedzeniu w przeglądarce folderu poems/web/ ujrzysz stronę przedstawioną na


rysunku 4.1.

Rysunek 4.1. Strona z przykładu 4.1

W roli szablonu aplikacji możesz użyć pliku [projekt]/app/Resources/views/base.


html.twig. Instrukcja extends przyjmie wówczas postać:
{% extends "::base.html.twig" %}
Takie rozwiązanie będziemy stosowali w części drugiej, poświęconej widokom Twig.
64 Część I ♦ Tworzenie prostych stron WWW
Rozdział 5.
Hiperłącza
i struktura aplikacji
Wszystkie wykonane do tej pory aplikacje składały się z pojedynczej strony WWW.
Kolejnym etapem poznawania Symfony 2 będzie więc dodanie do witryny kilku stron
i opracowanie systemu nawigacji pomiędzy nimi. W tym rozdziale:
 Przećwiczymy tworzenie i usuwanie pakietów.
 Nauczymy się tworzyć i usuwać kontrolery oraz akcje.
 Dowiemy się, w jaki sposób generować adresy URL odwołujące się do
dowolnych akcji.

Tworzenie i usuwanie akcji


Najprostszym sposobem dodania do witryny kolejnych stron jest zdefiniowanie nowych
akcji i widoków. Jeśli w aplikacji występuje kontroler o nazwie DefaultController i jest
on zapisany w folderze:
[pakiet]/Controller/DefaultController.php

wówczas w celu zdefiniowania trzech dodatkowych stron o adresach /lorem.html,


/ipsum.html i /dolor.html należy w kontrolerze dodać metody przedstawione na listingu 5.1.

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();
}

Dla akcji z listingu 5.1 należy utworzyć trzy widoki:


[pakiet]/Resources/views/Default/lorem.html.twig
[pakiet]/Resources/views/Default/ipsum.html.twig
[pakiet]/Resources/views/Default/dolor.html.twig

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

Tworzenie i usuwanie kontrolerów


Akcje lorem, ipsum oraz dolor możemy zdefiniować w osobnych kontrolerach. W celu
wykonania kontrolera LoremController należy utworzyć plik [pakiet]/Controller/
LoremController.php, który będzie zawierał klasę LoremController dziedziczącą po
klasie Controller. W kontrolerze tym definiujemy dowolne akcje, np. ipsumAction().
Kod kontrolera LoremController jest przedstawiony na listingu 5.2.

Listing 5.2. Kontroler LoremController


class LoremController extends Controller
{
/**
* @Route("/ipsum.html")
* @Template()
*/
public function ipsumAction()
{
return array();
}
}

Widokiem akcji ipsum z listingu 5.2 będzie plik [pakiet]/Resources/views/Lorem/


ipsum.html.twig.

W celu usunięcia kontrolera należy usunąć plik LoremController.php oraz folder [pakiet]/
Resources/views/Lorem/.

Tworzenie i usuwanie pakietów


Strony o adresach /lorem.html, /ipsum.html oraz /dolor.html możemy również wykonać
w osobnych pakietach. Wiemy już, że do tworzenia pakietów służy polecenie:
php app/console generate:bundle

Wygenerowany pakiet będzie zawierał kontroler DefaultController oraz akcję index.


Opisanymi powyżej metodami możemy utworzyć dowolną liczbę kontrolerów oraz
akcji i odpowiadających im widoków.

W celu usunięcia pakietu nazwanego My/LoremBundle należy:


 usunąć folder src/My/LoremBundle/,
 w pliku AppKernel.php usunąć instrukcję dołączającą pakiet do aplikacji:
new My\LoremBundle\MyLoremBundle(),
68 Część I ♦ Tworzenie prostych stron WWW

 w pliku app/config/routing.yml usunąć instrukcję importującą reguły pakietu


LoremBundle:
MyLoremBundle:
resource: "@MyLoremBundle/Controller/"
type: annotation
prefix: /

Definiowanie adresów URL akcji


Adresy URL akcji ustalamy adnotacjami @Route():
/**
* @Route("/lorem.html")
* @Template()
*/
public function loremAction()
{
return array();
}

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)

W celu wykonania translacji odwrotnej, tj. wygenerowania adresu /lorem.html w kodzie


strony WWW, należy w adnotacji @Route() dodać parametr name:
/**
* @Route("/lorem.html", name="adres_lorem")
* @Template()
*/
public function loremAction()
{
return array();
}

Nazwa reguły translacji, czyli wartość parametru name, może być dowolnym unikalnym
ciągiem znaków.

W szablonach Twig adresy URL generujemy, wywołując funkcję pomocniczą path():


<a href="{{ path('adres_lorem') }}">...</a>

W najprostszym przypadku parametrem funkcji path() jest nazwa reguły podana w ad-
notacji @Route().
Rozdział 5. ♦ Hiperłącza i struktura aplikacji 69

W plikach konfiguracyjnych routing.yml reguła translacji rozpoczyna się od nazwy.


W regule:
_homepage:
pattern: /
defaults: { _controller: MyValleyBundle:Default:index }

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 widoku adres URL odnoszący się do reguły o nazwie strona_domowa generujemy


w następujący sposób:
<a href="{{ path('strona_domowa') }}">...</a>

Przykład 5.1. Fraszki


Wykorzystując oprogramowanie Symfony 2, wykonaj aplikację, która będzie prezento-
wała trzy fraszki Jana Kochanowskiego: Do gościa, Na swoje księgi i O żywocie ludzkim.
Aplikację wykonaj w taki sposób, by każda z fraszek była dostępna na osobnej stronie
WWW. Użyj adresów /do-goscia.html, /na-swoje-ksiegi.html oraz /o-zywocie-ludzkim.html.
Na każdej z trzech stron umieść menu pozwalające na przejście do dowolnej fraszki.

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

Krok 2. Utwórz pakiet My/FraszkaBundle


Komendą:
php app/console generate:bundle

utwórz pakiet My/FraszkaBundle. Wszystkie opcje pakietu pozostaw domyślne.


70 Część I ♦ Tworzenie prostych stron WWW

Krok 3. Dostosuj kontroler DefaultController


W pliku DefaultController.php wprowadź kod przedstawiony na listingu 5.3.

Listing 5.3. Zmodyfikowany plik DefaultController.php


class DefaultController extends Controller
{
/**
* @Route("/do-goscia.html", name="url_dogoscia")
* @Template()
*/
public function dogosciaAction()
{
return array();
}

/**
* @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().

Krok 4. Utwórz szablon layout.html.twig


W folderze fraszki/src/My/FraszkaBundle/Resources/views/ utwórz plik layout.html.twig
o zawartości takiej jak na listingu 5.4. Zwróć uwagę, w jaki sposób generujemy menu
główne. Wykorzystujemy do tego funkcję pomocniczą path() oraz nazwy reguł routingu
zdefiniowane parametrami name wewnątrz adnotacji @Route() (listing 5.3).

Listing 5.4. Plik layout.html.twig


<!DOCTYPE html>
<html>
<head>
<title>Fraszki</title>
Rozdział 5. ♦ Hiperłącza i struktura aplikacji 71

<meta charset="UTF-8" />


<link rel="stylesheet" href="{{ asset('css/style.css') }}" />
</head>
<body>

<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">
&copy;2011 Włodzimierz Gajda
</div>
</div>

</body>
</html>

Krok 5. Dostosuj widok akcji dogoscia


Utwórz plik fraszki/src/My/FraszkaBundle/Resources/views/Default/dogoscia.html.twig
o zawartości takiej jak na listingu 5.5.

Listing 5.5. Widok akcji dogoscia


{% extends "MyFraszkaBundle::layout.html.twig" %}

{% 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 %}

W analogiczny sposób wykonaj widoki naswojeksiegi.html.twig oraz ozywocieludzkim.


html.twig.
72 Część I ♦ Tworzenie prostych stron WWW

Oczywiście w przykładzie występuje jeden plik layout.html.twig. Widoki naswojeksiegi.


html.twig oraz ozywocieludzkim.html.twig będą rozpoczynały się od instrukcji:
{% extends "MyFraszkaBundle::layout.html.twig" %}

Krok 6. Skopiuj zasoby .css, .js i .jpg


W folderze fraszki/web/ umieść pliki:
fraszki/web/css/style.css
fraszki/web/images/fraszki.jpg
fraszki/web/images/tlo.png

Krok 7. Zdefiniuj adres /


Na początku pliku fraszki/app/config/routing.yml wstaw (nie usuwając innej zawartości)
regułę przedstawioną na listingu 5.6.

Listing 5.6. Reguła włączająca adres /


_welcome:
pattern: /
defaults: { _controller: MyFraszkaBundle:Default:dogoscia }

Dzięki regule z listingu 5.6 po odwiedzeniu w przeglądarce folderu fraszki/web/ ujrzysz


witrynę przedstawioną na rysunku 5.1.

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.

Przykład 5.2. Zabytki Lublina


Wykorzystując oprogramowanie Symfony 2 ,wykonaj aplikację zawierającą trzy strony
WWW prezentujące zabytki Lublina: Bramę Krakowską, Wieżę Trynitarską oraz Zamek
Królewski. Użyj adresów /brama.html, /wieza.html oraz /zamek.html. Na każdej z trzech
stron umieść menu pozwalające na przejście do dowolnej strony.

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.1. Witryna z przykładu 5.1

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

Krok 2. Utwórz pakiet zabytek


Komendą:
php app/console generate:bundle

utwórz pakiet My/ZabytekBundle. Wszystkie opcje pakietu pozostaw domyślne.

Krok 3. Utwórz trzy kontrolery: BramaController, WiezaController oraz


ZamekController
W folderze My/ZabytekBundle/Controller/ utwórz trzy pliki: BramaController.php,
WiezaController.php oraz ZamekController.php. Treść plików BramaController.php oraz
WiezaController.php jest przedstawiona na listingach 5.7 oraz 5.8. Plik ZamekController.
php tworzymy w identyczny sposób.

Listing 5.7. Plik BramaController.php


<?php

namespace My\ZabytekBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;

class BramaController extends Controller


{
/**
* @Route("/brama.html", name="url_brama")
* @Template()
*/
public function indexAction()
{
return array();
}
}

Listing 5.8. Plik WiezaController.php


<?php

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;

class WiezaController extends Controller


{
/**
* @Route("/wieza.html", name="url_wieza")
* @Template()
*/
public function indexAction()
{
return array();
}
}

Krok 4. Utwórz szablon layout.html.twig


W folderze zabytki/src/My/ZabytekBundle/Resources/views/ utwórz plik layout.html.twig
o zawartości takiej jak na listingu 5.9.

Listing 5.9. Plik layout.html.twig


<!DOCTYPE html>
<html>
<head>
<title>Zabytki Lublina</title>
<meta charset="UTF-8" />
<link rel="stylesheet" href="{{ asset('css/style.css') }}" />
</head>
<body>

<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

Krok 5. Dostosuj widoki akcji


Utwórz plik zabytki/src/My/ZabytekBundle/Resources/views/Brama/index.html.twig o za-
wartości takiej jak na listingu 5.10. W analogiczny sposób przygotuj dwa pozostałe widoki:
ZabytekBundle/Resources/views/Wieza/index.html.twig
ZabytekBundle/Resources/views/Zamek/index.html.twig

Listing 5.10. Widok akcji Resources/views/Brama/index.html.twig

{% 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 %}

Krok 6. Skopiuj zasoby .css, .js i .jpg


W folderze zabytki/web/ umieść foldery css/ oraz images/ zawierające arkusze stylów CSS
oraz pliki graficzne.

Krok 7. Zdefiniuj adres /


W pliku zabytki/app/config/routing.yml umieść regułę przedstawioną na listingu 5.11.

Listing 5.11. Reguła włączająca adres /

_welcome:
pattern: /
defaults: { _controller: MyZabytekBundle:Brama:index }

Po odwiedzeniu w przeglądarce folderu zabytki/web/ ujrzysz witrynę przedstawioną na


rysunku 5.3.
Rozdział 5. ♦ Hiperłącza i struktura aplikacji 77

Rysunek 5.3. Strona z przykładu 5.2

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.

Przykład 5.3. Piosenki dla dzieci


Wykorzystując oprogramowanie Symfony 2, wykonaj aplikację zawierającą trzy strony
WWW prezentujące teksty trzech dziecięcych piosenek. Użyj adresów /jada-jada-misie.
html, /kolko-graniaste.html oraz /ojciec-i-syn.html. Na każdej z trzech stron umieść
menu pozwalające na przejście do dowolnej strony.

W aplikacji wykonaj trzy pakiety o nazwach My/LoremBundle, My/IpsumBundle oraz


My/DolorBundle. W każdym pakiecie użyj domyślnego kontrolera DefaultController oraz
akcji index. Dane potrzebne do wykonania aplikacji znajdziesz w pliku 05-03-start.zip.
78 Część I ♦ Tworzenie prostych stron WWW

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

Krok 2. Utwórz pakiety


Komendą1:
php app/console generate:bundle

utwórz pakiety My/LoremBundle, My/IpsumBundle oraz My/DolorBundle. Wszystkie opcje


we wszystkich pakietach pozostaw domyślne.

1
Oczywiście komendę generate:bundle należy wydać trzykrotnie.
Rozdział 5. ♦ Hiperłącza i struktura aplikacji 79

Krok 3. Dostosuj kontrolery


W pliku My/LoremBundle/Controller/DefaultController.php wprowadź zawartość przed-
stawioną na listingu 5.12. W analogiczny sposób zmodyfikuj kontrolery w pakietach
IpsumBundle oraz DolorBundle.

Listing 5.12. Kontroler DefaultController z pakietu LoremBundle


<?php

namespace My\LoremBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;

class DefaultController extends Controller


{
/**
* @Route("/jada-jada-misie.html", name="_misie")
* @Template()
*/
public function indexAction()
{
return array();
}
}

Krok 4. Utwórz szablon layout.html.twig


W folderze piosenki/src/My/LoremBundle/Resources/views/ utwórz plik layout.html.twig
o zawartości takiej jak na listingu 5.13.

Listing 5.13. Plik layout.html.twig


<!DOCTYPE html>
<html>
<head>
<title>Piosenki dla dzieci</title>
<meta charset="UTF-8" />
<link rel="stylesheet" href="{{ asset('css/style.css') }}" />
<script src="{{ asset('js/jquery-1.6.4.min.js') }}"></script>
<script src="{{ asset('js/animacja.js') }}"></script>
</head>
<body>

<div id="pojemnik">
<h1 id="naglowek">Piosenki dla dzieci<span>&nbsp;</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" %}

Krok 5. Dostosuj widoki akcji


W pliku piosenki/src/My/LoremBundle/Resources/views/Default/index.html.twig wpro-
wadź zawartość przedstawioną na listingu 5.14. W analogiczny sposób przygotuj dwa
pozostałe widoki:
IpsumBundle/Resources/views/Default/index.html.twig
DolorBundle/Resources/views/Default/index.html.twig

Listing 5.14. Widok akcji LoremBundle/Resources/views/Default/index.html.twig


{% extends "MyLoremBundle::layout.html.twig" %}

{% block content %}
<h2>Jadą, jadą misie</h2>

<p>
Jadą, jadą misie,<br />
Tra la, tra la la,<br />
...
</p>

{% endblock %}

Krok 6. Skopiuj zasoby .css, .js i .jpg


W folderze piosenki/web/ umieść foldery css/, js/ oraz images/ zawierające arkusze
stylów CSS, skrypty JavaScript oraz pliki graficzne.
Rozdział 5. ♦ Hiperłącza i struktura aplikacji 81

Krok 7. Zdefiniuj adres /


W pliku piosenki/app/config/routing.yml umieść regułę przedstawioną na listingu 5.15.

Listing 5.15. Reguła włączająca adres /


_welcome:
pattern: /
defaults: { _controller: MyLoremBundle:Default:index }

Po odwiedzeniu w przeglądarce folderu piosenki/web/ ujrzysz witrynę przedstawioną na


rysunku 5.5.

Rysunek 5.5. Witryna z przykładu 5.3

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.

Jeśli w przeglądarce wpiszemy adres:


http://google.com/lorem/ipsum.html
to ujrzymy stronę przedstawioną na rysunku 6.1.

Rysunek 6.1. Strona błędu 404 dla domeny Google.com


84 Część I ♦ Tworzenie prostych stron WWW

Korzystając z wtyczki Live HTTP Headers1, możemy stwierdzić, że w odpowiedzi na


żądanie:
GET /lorem/ipsum.html

serwer http://www.google.com odpowiada:


HTTP/1.1 404 Not Found

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

Strony błędów w Symfony 2


W Symfony 2 domyślne szablony stron błędów znajdują się w folderze:
vendor/symfony/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/

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

wówczas ujrzymy stronę wygenerowaną na podstawie szablonu:


vendor/symfony/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/error.html.twig

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.

Symfony 2 pozwala także na tworzenie osobnych szablonów dla poszczególnych błędów.


Jeśli chcemy dostosować wygląd błędów o kodach 404, 500 oraz wszystkich pozostałych,
wówczas w folderze app/Resources/TwigBundle/views/Exception/ należy utworzyć trzy
pliki:
error404.html.twig
error500.html.twig
error.html.twig

Do wygenerowania strony błędu o kodzie 404 wykorzystany zostanie plik error404.


html.twig. Strona błędu o kodzie 500 powstanie na podstawie pliku error500.html.twig.
Wszystkie pozostałe strony błędów będą generowane na podstawie pliku error.html.twig.

W pliku web/.htaccess zawarte są następujące reguły:


<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ app.php [QSA,L]
</IfModule>
Działają one dwojako. Jeśli zapytanie HTTP dotyczy istniejącego pliku, jest on wysyłany
jako odpowiedź na otrzymane żądanie.
W przeciwnym razie, czyli gdy zapytanie dotyczy nieistniejącego pliku, zostaje ono prze-
kierowane do skryptu app.php.
Dlatego jeśli w folderze web/css/ umieścimy plik style.css, to żądanie:
http://localhost/projekt/web/css/style.css
spowoduje pobranie pliku web/css/style.css.
Jeśli natomiast wpiszemy w przeglądarce adres pliku, który nie istnieje, np.:
http://localhost/projekt/web/dane/klientow/jan-nowak.html
to zapytanie zostanie skierowane do skryptu app.php. W zależności od tego, czy w apli-
kacji występuje reguła routingu dla adresu /dane/klientow/Jan-nowak.html, ujrzy-
my albo wynik przetwarzania odpowiedniej akcji, albo stronę błędu 404.
86 Część I ♦ Tworzenie prostych stron WWW

Przykład 6.1. Gady


Wykorzystując oprogramowanie Symfony 2, wykonaj aplikację, która będzie prezentowała
cztery strony WWW: jaszczurka.html, zaskroniec.html, zmija.html i zolw.html. Na każdej ze
stron umieść menu umożliwiające nawigację pomiędzy stronami. Oprogramuj obsługę błędu
o kodzie 404. Po odwiedzeniu błędnego adresu użytkownik ma ujrzeć komunikat: Podana
strona nie istnieje. Dane potrzebne do wykonania aplikacji znajdziesz w pliku 06-start.zip.

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

Krok 2. Utwórz pakiet My/AnimalsBundle


Komendą:
php app/console generate:bundle

utwórz pakiet My/AnimalsBundle. Wszystkie opcje pakietu pozostaw domyślne.

Krok 3. Dostosuj kontroler DefaultController


W pliku DefaultController.php wprowadź kod przedstawiony na listingu 6.1.

Listing 6.1. Zmodyfikowany plik DefaultController.php


class DefaultController extends Controller
{
/**
* @Route("/jaszczurka.html", name="_animals_jaszczurka")
* @Template()
*/
public function jaszczurkaAction()
{
return array();
}

/**
* @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();
}
}

W aplikacji występuje jeden pakiet, My/AnimalsBundle, zawierający kontroler Default


´Controller. W kontrolerze występują cztery akcje: jaszczurkaAction(), zaskroniec
´Action(), zmijaAction() oraz zolwAction().

Krok 4. Utwórz szablon layout.html.twig


W folderze gady/src/My/AnimalsBundle/Resources/views/ utwórz plik layout.html.twig
o zawartości takiej jak na listingu 6.2. Menu główne generujemy, wykorzystując funkcję
path() oraz nazwy reguł routingu zdefiniowane parametrami name wewnątrz adnotacji
@Route() (listing 6.1).

Listing 6.2. Plik layout.html.twig


<!DOCTYPE html>
<html>
<head>
<title>
Gady
</title>
<meta charset="UTF-8" />
<link rel="stylesheet" href="{{ asset('css/style.css') }}" />
</head>
<body>

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

Krok 5. Dostosuj widok akcji jaszczurkaAction()


Utwórz plik gady/src/My/AnimalsBundle/Resources/views/Default/jaszczurka.html.twig
o zawartości takiej jak na listingu 6.3.

Listing 6.3. Widok akcji jaszczurka


{% extends "MyAnimalsBundle::layout.html.twig" %}

{% block content %}
<h2>Jaszczurka zwinka</h2>
<h3>Lacerta agilis</h3>
<p>
<img src="{{ asset('images/jaszczurka.jpg') }}" alt="Jaszczurka zwinka" />
</p>
{% endblock %}

W analogiczny sposób wykonaj widoki pozostałych trzech akcji: zaskroniec.html.twig,


zmija.html.twig oraz zolw.html.twig.

Krok 6. Skopiuj zasoby .css i .jpg


Utwórz foldery gady/web/css/ i gady/web/images/, po czym umieść w nich pliki .css i .jpg.

Krok 7. Zdefiniuj adres /


W pliku gady/app/config/routing.yml umieść regułę przedstawioną na listingu 6.4.

Listing 6.4. Reguła włączająca adres /


_welcome:
pattern: /
defaults: { _controller: MyAnimalsBundle:Default:jaszczurka }

Po odwiedzeniu w przeglądarce folderu gady/web/ ujrzysz witrynę przedstawioną na ry-


sunku 6.3.

Krok 8. Wykonaj szablon strony błędu 404


Utwórz plik app/Resources/TwigBundle/views/Exception/error404.html.twig o zawarto-
ści przedstawionej na listingu 6.5.
Rozdział 6. ♦ Błędy 404 89

Rysunek 6.3. Witryna z przykładu 6.1

Listing 6.5. Widok error404.html.twig


{% extends "MyAnimalsBundle::layout.html.twig" %}
{% block content %}
<h2>Błąd 404!</h2>
<p>Podana strona nie istnieje!</p>
{% endblock %}

Wyczyść pamięć podręczną aplikacji2, po czym odwiedź w przeglądarce nieistniejący


adres, np.:
http://localhost/gady/web/lorem/ipsum.html

Ujrzysz stronę przedstawioną na rysunku 6.4. Zawiera ona komunikat widoczny na li-
stingu 6.5.

Pamiętaj, że w środowisku deweloperskim strona błędu jest wyświetlana w postaci wy-


jątku. Po odwiedzeniu adresu:
http://localhost/gady/web/app_dev.php/lorem/ipsum.html
ujrzysz stronę z rysunku 6.5. Zwróć uwagę, że nie zawiera ona komunikatu Podana strona
nie istnieje!.

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

Rysunek 6.4. Strona wyświetlana po odwiedzeniu adresu lorem/ipsum.html

Rysunek 6.5. Strona lorem/ipsum.html wyświetlana w środowisku deweloperskim


Rozdział 6. ♦ Błędy 404 91

Krok 9. Wykonaj szablon strony błędu 500


Utwórz plik app/Resources/TwigBundle/views/Exception/error500.html.twig o zawartości
przedstawionej na listingu 6.6.

Listing 6.6. Widok error500.html.twig


<!DOCTYPE html>
<html>
<head>
<title>Błąd 500</title>
<meta charset="UTF-8" />
</head>
<body>
<h1>Błąd 500</h1>
</body>
</html>

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 celu nadpisania widoku akcji dolor należy utworzyć plik:


[projekt]/app/Resources/MyLoremBundle/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...');

Komunikat podany jako parametr będzie widoczny w oknie z rysunku 6.5.

W celu wygenerowania wyjątku 500 umieść w kodzie akcji instrukcję:


throw new \Exception('Komunikat...');

Błąd 403 możesz natomiast generować wywołaniem:


throw new AccessDeniedException('Brak dostępu!');
Rozdział 7.
Publikowanie projektu
na serwerze hostingowym
Poznawanie metody tworzenia prostych stron WWW w Symfony 2 zakończymy, pu-
blikując witrynę z przykładu 6.1. w Internecie. Procedurę publikacji projektu omówię,
wykorzystując do tego celu serwery firmy NetArt (http://nazwa.pl) oraz Light Hosting
(http://lh.pl).

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

Przykład 7.1. Gady


— wersja lokalna z własną domeną
Aplikację wykonaną w rozdziale 6. zmodyfikuj w taki sposób, by była ona dostępna pod
adresem:
http://gady.domena.pl.localhost
94 Część I ♦ Tworzenie prostych stron WWW

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.

Listing 7.1. Reguły konfigurujące wirtualny serwer stron WWW


<VirtualHost 127.0.0.1:80>
DocumentRoot "C:/xampp/htdocs/gady/web"
ServerName gady.domena.pl.localhost
<Directory "/">
Options FollowSymLinks
AllowOverride None
Order allow,deny
Allow from all
</Directory>
</VirtualHost>

Następnie na końcu pliku C:\Windows\System32\drivers\etc\hosts dodaj nowy adres do-


menowy dla adresu 127.0.0.1. Zawartość pliku hosts jest przedstawiona na listingu 7.2.

Listing 7.2. Plik C:\Windows\System32\drivers\etc\hosts


...

127.0.0.1 gady.domena.pl.localhost

Oczywiście domenę:
gady.domena.pl.localhost

możesz zastąpić dowolną inną domeną, np.:


wazny.serwis.pl.localhost

Wystarczy, że zmodyfikujesz regułę ServerName z listingu 7.1:


ServerName wazny.serwis.pl.localhost

oraz w pliku hosts dodasz regułę:


127.0.0.1 wazny.serwis.pl.localhost

W celu sprawdzenia działania przykładu zrestartuj serwer WWW, uruchom przeglądarkę


i odwiedź adres:
http://gady.domena.pl.localhost
Rozdział 7. ♦ Publikowanie projektu na serwerze hostingowym 95

Przykład 7.2. Gady


— wersja z serwera firmy NetArt
Przykład 7.1 opublikuj w Internecie pod adresem:
http://gady.domena.pl

Poniższa procedura została przetestowana na serwerze wirtualnym wykupionym w firmie


NetArt (http://nazwa.pl). Do testów wykorzystałem domenę gajdaw.pl. Omawiany
przykład został opublikowany pod adresem http://gady.gajdaw.pl. Transfer plików
aplikacji na serwer hostingowy został wykonany przy użyciu FTP.

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.

Listing 7.3. Plik .htaccess blokujący dostęp do wszystkich plików aplikacji


Deny from all

Krok 2. Zmodyfikuj plik web/.htaccess


Zmodyfikuj plik .htaccess zawarty w folderze gady/web/. Umieść w nim reguły przed-
stawione na listingu 7.4.

Listing 7.4. Zmodyfikowany plik web/.htaccess


Allow from all
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ app.php [QSA,L]
</IfModule>

Krok 3. Przekopiuj projekt na serwer


Za pomocą programu FTP przekopiuj na serwer pliki przykładu 7.1. Jeśli folder projektu
umieścisz w głównym folderze, na serwerze pojawią się m.in. pliki:
96 Część I ♦ Tworzenie prostych stron WWW

/home/domena/ftp/gady/app/AppKernel.php
/home/domena/ftp/gady/web/app.php
...

Po przekopiowaniu kompletnej aplikacji usuń z serwera dwa pliki:


/home/domena/ftp/gady/web/app_dev.php
/home/domena/ftp/gady/web/config.php

Krok 4. Zmodyfikuj domenę projektu


Uruchom program administracyjny zapewniający konfigurację Twojego serwera NetArt.
Dodaj nową domenę gady.domena.pl i przekieruj ją do folderu /gady/web/. Zawartość
okna dialogowego konfiguracji nowej domeny jest przedstawiona na rysunku 7.1.

Rysunek 7.1. Przekierowywanie nowej domeny gady.domena.pl do folderu /gady/web/

Na zakończenie uruchom przeglądarkę i odwiedź adres:


http://gady.domena.pl

Powinieneś ujrzeć witrynę z rysunku 7.2.


Rozdział 7. ♦ Publikowanie projektu na serwerze hostingowym 97

Rysunek 7.2. Witryna http://gady.gajdaw.pl

Przykład 7.3. Gady


— wersja z serwera firmy Light Hosting
Przykład 7.2 opublikuj w Internecie pod adresem:
http://domena.pl

Do transferu plików na serwer wykorzystaj komendę rsync.

Opisana procedura została przetestowana na serwerze wirtualnym wykupionym


w firmie Light Hosting (http://lh.pl).

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

na którym pracujemy, na serwer. Zaletą rsync jest to, że po wprowadzeniu modyfikacji


w aplikacji uaktualnienie plików na serwerze będzie wymagało przesłania wyłącznie
zmodyfikowanych plików lub ich fragmentów. rsync automatycznie stwierdzi, które pliki
zostały zmodyfikowane, i prześle na serwer wyłącznie modyfikacje. Instalacja oprogramo-
wania rsync w systemie Windows jest opisana w dodatku A.

Krok 2. Zmień nazwę folderu widocznego za pomocą protokołu HTTP


Na serwerach firmy Light Hosting publiczny serwer widoczny za pomocą protokołu HTTP
nazywa się public_html/. Zmień nazwę folderu gady/web/ na gady/public_html/.

Krok 3. Zmodyfikuj plik public_html/.htaccess


W pliku public_html/.htaccess dodaj widoczną na listingu 7.5 linijkę zawierającą dyrek-
tywę AddHandler. Spowoduje ona, że pliki o rozszerzeniu .php będą wykonywane przez
interpretator PHP w wersji 5.3.

Listing 7.5. Zmodyfikowany plik public_html/.htaccess


AddHandler x-httpd-php53 .php
Allow from all
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ app.php [QSA,L]
</IfModule>

Krok 4. Utwórz plik php.ini


Serwery firmy Light Hosting umożliwiają tworzenie plików php.ini konfigurujących
działanie PHP. W folderze public_html/ utwórz plik php.ini o zawartości takiej jak na
listingu 7.6.

Listing 7.6. Plik public_html/php.ini


short_open_tag = Off
magic_quotes_gpc = Off

[Date]
date.timezone = Europe/Berlin

Krok 5. Utwórz skrypt wywołujący polecenie rsync


W folderze głównym aplikacji utwórz plik o nazwie rsync-production.bat. Umieść
w nim polecenie przedstawione na listingu 7.7. Komenda ta spowoduje przekopiowanie
zawartości bieżącego folderu na serwer twojserwer.lh.pl. Utworzone pliki i foldery
otrzymają uprawnienia:
rwxr-xr-x
Rozdział 7. ♦ Publikowanie projektu na serwerze hostingowym 99

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

Takie uprawnienia pozwolą na uruchomienie aplikacji za pośrednictwem protokołu HTTP.

Listing 7.7. Skrypt rsync-production.bat2


rsync
--chmod=a+rwx,g-w,o-w -azC --force --delete
--progress --exclude-from=rsync_exclude.txt
-e "ssh -p40022" ./ twojserwer@twojserwer.lh.pl:

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.

Listing 7.8. Zawartość pliku rsync_exclude.txt


app/cache/*
public_html/app_dev.php
public_html/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

Krok 6. Prześlij pliki na serwer


Uruchom skrypt rsync-production.bat. W odpowiedzi na monit:
twojserwer@twojserwer.lh.pl’s password:

wprowadź hasło dostępu do serwera SSH. Po poprawnym wykonaniu polecenia odwiedź


adres skojarzony z serwerem hostingowym:
http://twojadomena.pl

Powinieneś ujrzeć witrynę z rysunku 7.2.

Jeśli teraz wprowadzisz modyfikacje w którychkolwiek plikach aplikacji, to w celu zak-


tualizowania projektu na serwerze ponownie uruchom skrypt rsync-production.bat.
Komenda ta spowoduje uaktualnienie na serwerze plików, które zostały zmodyfikowane.
Rozdział 8.
Podsumowanie części I

Dystrybucje Symfony 2
Naukę Symfony 2 rozpoczęliśmy od poznania dwóch dystrybucji:
 with vendors,
 without vendors.

Dystrybucja with vendors zawiera w folderze vendor/ wszystkie podstawowe pakiety


tworzące Framework Symfony 2. Rysunek 2.7 przedstawia skrypty PHP tworzące
projekt Symfony 2, które nie są zawarte w folderze vendor/. Oczywiście zawartość folderu
vendor/ jest konieczna do uruchomienia projektu. Dlatego w początkowym okresie nauki
wygodniej jest korzystać z dystrybucji with vendors.

Dystrybucji without vendors będziemy używali w części trzeciej, gdy zechcemy doinsta-
lować dodatkowe pakiety.

Przykładowa aplikacja ACME demo


Po omówieniu różnic w dystrybucjach Symfony przystąpiliśmy do uruchomienia przy-
kładowej aplikacji demo. Wiemy, że w obu dystrybucjach Symfony zawarte są skrypty
sprawdzające poprawność instalacji PHP oraz aplikacja przykładowa.

Do sprawdzania poprawności instalacji PHP służy skrypt config.php, który uruchamiamy,


odwiedzając w przeglądarce adres:
http://localhost/Symfony/web/config.php
102 Część I ♦ Tworzenie prostych stron WWW

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

Oczywiście zanim przejdziemy dalej, należy poprawić wszystkie błędy konfiguracji,


które są zgłaszane przez którykolwiek skrypt.

Gdy konfiguracja komputera jest poprawna, sprawdzamy wygląd strony aplikacji demo:
http://localhost/Symfony/web/app_dev.php

Powyższy skrypt ostatecznie upewnia nas, że Symfony 2 działa poprawnie.

Po szczegółowym omówieniu środowisk pracy (rozdział 2.) wiemy, że adresy:


http://localhost/Symfony/web/
http://localhost/Symfony/web/app.php
generują strony z informacjami o błędzie, gdyż w domyślnych regułach routingu nie ma
reguły obsługującej adres /.

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.

Wykonanie projektu Hello, word! zapoznało nas z:


 poleceniem generate:bundle, które służy do tworzenia pakietów;
 modyfikacją kodu akcji index w kontrolerze DefaultController;
 modyfikacją widoku akcji index;
 sposobem definiowania adresu /hello-world.html w postaci adnotacji w kontrolerze
DefaultController;
 środowiskami pracy;
 przestrzeniami nazewniczymi;
 formatami konfiguracji.
Rozdział 8. ♦ Podsumowanie części I 103

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

Zasoby wszystkich pakietów możemy automatycznie przekopiować do folderu web/,


wydając komendę:
php app/console assets:install web

Firmy hostingowe mogą nakładać ograniczenia na nazwy folderów, które są publicznie


dostępne. Na przykład w firmie LH folder publiczny nazywa się public_html/. Jeśli chcemy
opublikować projekt Symfony 2 na takim serwerze, zmieniamy nazwę folderu web/
na public_html/. W takim przypadku automatyczną instalację zasobów wykonujemy
poleceniem:
php app/console assets:install public_html

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

W szablonie layout.html.twig umieszczamy fragment, który będziemy wypełniali treścią.


Fragment ten definiujemy jako blok o nazwie content:
//fragment pliku layout.html.twig
<div id="tekst">
{% block content %}
{% endblock %}
</div>

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

//fragment pliku index.html.twig


{% extends "MyLoremBundle::layout.html.twig" %}

{% 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.

W trzech przykładach omówionych w tym rozdziale wystąpiły trzy różne rozwiązania


(pod względem struktury aplikacji):
 jeden pakiet, jeden kontroler, trzy akcje;
 jeden pakiet, trzy kontrolery, każdy po jednej akcji;
 trzy pakiety, każdy zawiera jeden kontroler, a w nim jedną akcję.

Zasadniczym tematem rozdziału były odsyłacze i wykonanie menu witryny.

Do zdefiniowania adresów URL stosowanych w aplikacji wykorzystaliśmy adnotacje


występujące w kontrolerach:
/**
* @Route("/lorem.html", name="adres_lorem")
*/

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.

Pamiętaj, że stosowanie adresów zawierających foldery prowadzące do projektu, np.:


http://localhost/strony/komercyjne/sklep-abc/web/

będzie wiązało się z utrudnieniem polegającym na tym, że serwis opublikowany na ser-


werze pod adresem:
http://sklep-abc.example.net
będzie miał inne ścieżki do zasobów CSS, JS itd. Będzie to widoczne na przykład w przy-
padku skryptów JavaScript. Jeśli w skrypcie JS wystąpią ścieżki do plików graficznych1:
$('a.lightbox').lightBox({
imageLoading: '/images/lightbox-ico-loading.gif',
imageBtnPrev: '/images/lightbox-btn-prev.gif',
...
});

to ścieżki na serwerze będą inne:


//przykładowa ścieżka na serwerze
imageLoading: '/images/lightbox-ico-loading.gif'

od ścieżek na komputerze localhost:


//przykładowa ścieżka na serwerze
imageLoading: '/strony/komercyjne/sklep-abc/web/images/lightbox-ico-loading.gif'

Najlepszym rozwiązaniem tego problemu jest przygotowanie hosta wirtualnego o adresie:


http://sklep-abc.example.net.localhost

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

Kod aplikacji na serwerze i na komputerze localhost będzie wówczas stosował dokładnie


te same adresy URL postaci:
/images/lightbox-ico-loading.gif
/css/style.css
itd.

Do kopiowania na serwer kompletu plików tworzących aplikację wygodnie jest użyć


programu rsync. Jeśli przygotujemy odpowiedni plik wsadowy, kopiowanie sprowadzi
się do uruchomienia jednego pliku wsadowego i będzie trwało naprawdę krótko. Nie bę-
dziemy także musieli pilnować, które pliki należy przekopiować, a które nie. Zajmie się
tym oprogramowanie rsync.

Przykład 8.1. Przygotowanie


pakietu symfony2-customized-v1.zip
(bez przykładu src/Acme)
Przygotuj pakiet symfony2-customized-v1.zip, który będzie zawierał najnowszą wersję
Symfony 2 pozbawioną kodu przykładowego src/Acme/.

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

Krok 2. Usuń pakiet src/Acme/


Kolejno:
 Usuń folder src/Acme/.
 Usuń folder web/bundles/acmedemo/.
 W pliku app/AppKernel.php usuń wiersz:
$bundles[] = new Acme\DemoBundle\AcmeDemoBundle();

 W pliku app/config/routing_dev.yml usuń wiersze:


_welcome:
pattern: /
Rozdział 8. ♦ Podsumowanie części I 107

defaults: { _controller: AcmeDemoBundle:Welcome:index }

_demo_secured:
resource: "@AcmeDemoBundle/Controller/SecuredController.php"
type: annotation

_demo:
resource: "@AcmeDemoBundle/Controller/DemoController.php"
type: annotation
prefix: /demo

Krok 3. Przygotuj pakiet symfony2-customized-v1.zip


Zmień nazwę folderu Symfony/ na symfony2-customized-v1/, po czym spakuj jego
zawartość do pliku symfony2-customized-v1.zip.

Kolejne projekty wykonamy, wykorzystując przygotowany pakiet symfony2-customized-


-v1.zip.
108 Część I ♦ Tworzenie prostych stron WWW
Część II
Widoki
110 Część II ♦ Widoki
Rozdział 9. ♦ Twig 111

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.

Logiczne nazwy widoków


Przykłady omówione w pierwszej części podręcznika przybliżyły nam trójstopniowy po-
dział aplikacji tworzonej w Symfony 2. Cały projekt jest podzielony na pakiety, kontrolery
i akcje. Pakiety są zawarte w osobnych folderach, kontrolery w osobnych plikach, a ak-
cje są metodami zdefiniowanymi w klasie kontrolera. Kod HTML generowany przez ak-
cje kontrolera powstaje na podstawie widoków — plików zawierających znaczniki
HTML (np. <body>, <title> itp.) oraz Twig (np. {% block %}, {% extends %} itp.).

Ogólny schemat nazewnictwa pliku kontrolera ma postać:


src/[producent]/[pakiet]/Controller/[kontroler]Controller.php

Jeśli w projekcie wystąpi pakiet My/LoremBundle oraz kontroler Ipsum, plik kontrolera
otrzyma wówczas nazwę:
src/My/LoremBundle/Controller/IpsumController.php

Domyślnym folderem, w którym należy zapisywać widoki danego kontrolera, jest:


src/[producent]/[pakiet]/Resources/views/[kontroler]/

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

Ogólnie nazwa logiczna widoku ma postać:


[producent][pakiet]Bundle:[kontroler]:[akcja].html.twig
Przykładami nazw logicznych widoków są:
MyLoremBundle:Ipsum:dolor.html.twig
GajdawAdminBundle:Crud:edit.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

Logiczne nazwy widoków z pakietu My/LoremBundle ilustruje rysunek 9.1.

Rysunek 9.1. Logiczne nazwy widoków z pakietu My/LoremBundle

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

natomiast nazwa logiczna:


:Nunc:pede.html.twig

wskazuje widok:
app/Resources/views/Nunc/pede.html.twig

Nazwy logiczne widoków z folderu app/Resources/views/ ilustruje rysunek 9.2.

Rysunek 9.2.
Logiczne nazwy
widoków z folderu
app/Resources/views/

Logiczna nazwa akcji


Akcje, podobnie jak widoki, mają swoje logiczne nazwy. Logiczna nazwa akcji powstaje w podobny
sposób jak logiczna nazwa widoku. Jeśli w aplikacji występuje pakiet My/LoremBundle, a w nim
kontroler Ipsum oraz akcja dolorAction(), to logiczną nazwą akcji dolor jest:
MyLoremBundle:Ipsum:dolor

Logiczne nazwy akcji wykorzystamy, stosując znacznik {% render %}.

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

Widoki te nadpisują zawartość domyślnych widoków zawartych w folderze:


vendor/symfony/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/

W podobny sposób możemy nadpisywać wszystkie widoki zawarte w pakietach z folderu


[project]/vendor/. Na przykład w celu nadpisania widoku:
/vendor/bundles/Knp/PaginatorBundle/Resources/views/Pagination/sliding.html.twig

zawartego w pakiecie:
https://github.com/KnpLabs/KnpPaginatorBundle
114 Część II ♦ Widoki

należy utworzyć własny widok:


app/Resources/KnpPaginatorBundle/views/Pagination/sliding.html.twig

Pakiet https://github.com/KnpLabs/KnpPaginatorBundle ułatwia wykonanie stro-


nicowania rekordów. Jest to odpowiednik klas sfDoctrinePager oraz sfPropelPager
z Symfony 1.4.

Nazwy widoków akcji


Nazwę widoku przetwarzanego przez akcję możemy ustalić, wykorzystując do tego
adnotację konfiguracyjną @Template(), której parametrem jest logiczna nazwa widoku:
@Template(logiczna_nazwa_widoku)

np.
@Template("MyLoremBundle:Ipsum:dolor.html.twig")
@Template("::base.html.twig")

Pamiętaj, że w adnotacjach należy stosować cudzysłów:


PRZYKŁAD POPRAWNY
/**
* @Template("MyLoremBundle:Ipsum:dolor.html.twig")
*/
Użycie apostrofów jest niedozwolone:
PRZYKŁAD BŁĘDNY
/**
* @Template('MyLoremBundle:Ipsum:dolor.html.twig')
*/

Domyślny parametr reguły @Template() jest pusty:


/**
* ...
* @Template()
*/

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

public function dolorAction()


{
return array();
}

ustala, że widokiem akcji dolor jest:


MyLoremBundle:Ipsum:dolor.html.twig

W takim przypadku zapisy przedstawione na listingach 9.1 oraz 9.2 są równoważne.

Listing 9.1. Domyślna reguła konfiguracyjna @Template()


/**
* @Template()
*/
public function dolorAction()
{
return array();
}

Listing 9.2. Parametrem reguły @Template() jest logiczna nazwa widoku


/**
* @Template("MyLoremBundle:Ipsum:dolor.html.twig")
*/
public function dolorAction()
{
return array();
}

Dokumentacja składni adnotacji konfiguracyjnych, które możemy umieszczać w kontro-


lerze przed metodami akcji, jest dostępna na stronie:
https://github.com/sensio/SensioFrameworkExtraBundle/tree/master/Resources/doc
oraz w folderze:
vendor/bundles/Sensio/Bundle/FrameworkExtraBundle/Resources/doc/

W szczególności opis reguły @Template() znajdziesz na stronie:


https://github.com/sensio/SensioFrameworkExtraBundle/blob/master/
Resources/doc/annotations/view.rst
oraz w pliku:
vendor/bundles/Sensio/Bundle/FrameworkExtraBundle/Resources/doc/view.rst

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

Listing 9.3. Pominięcie reguły @Template() wymusza wywołanie metody render()


public function dolorAction()
{
return $this->render("MyLoremBundle:Ipsum:dolor.html.twig");
}

Parametrem metody render() jest logiczna nazwa widoku.

Przykład 9.1. Nazwy logiczne widoków,


adnotacja @Template()
i metoda render()
Wykorzystując oprogramowanie Symfony 2, wykonaj aplikację, która będzie prezento-
wała cztery strony WWW o adresach:
/lorem.html
/ipsum.html
/dolor.html
/sit.html

Zadanie wykonaj w taki sposób, by w rozwiązaniu wystąpił jeden pakiet My/SprBundle,


a w nim jedna klasa kontrolera DefaultController.php. W klasie kontrolera zdefiniuj
cztery akcje:
loremAction()
ipsumAction()
dolorAction()
sitAction()

Widokami poszczególnych akcji mają być pliki:


app/Resources/views/lorem.html.twig
app/Resources/views/Abc/ipsum.html.twig
src/My/SprBundle/Resources/views/dolor.html.twig
src/My/SprBundle/Resources/views/Def/sit.html.twig

Dwa pierwsze widoki skonfiguruj, wykorzystując adnotację @Template(), a dwa ostatnie


— wywołując metodę render().

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

php app/console generate:bundle


--namespace=My/SprBundle --dir=src --no-interaction

utwórz pakiet My/SprBundle.

Oczywiście podaną komendę generate:bundle należy zapisać w jednym wierszu.


Znaki złamania wiersza zostały dodane w celu zwiększenia czytelności.

Krok 2. Dostosuj kontroler DefaultController


W pliku src/My/SprBundle/Controller/DefaultController.php wprowadź kod przedstawiony
na listingu 9.4.

Listing 9.4. Zmodyfikowany plik DefaultController.php


class DefaultController extends Controller
{

/**
* @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

Reguła akcji loremAction():


@Template("::lorem.html.twig")

zawiera logiczną nazwę widoku:


::lorem.html.twig

Widokiem akcji loremAction() jest zatem plik:


app/Resources/views/lorem.html.twig

W regule akcji ipsumAction():


@Template(":Abc:ipsum.html.twig")

nazwą logiczną widoku jest:


:Abc:ipsum.html.twig

Dlatego widok akcji pochodzi z pliku:


app/Resources/views/Abc/ipsum.html.twig

W adnotacjach akcji dolorAction() oraz sitAction() pominięta została reguła @Template().


Wewnątrz kodu akcji konieczne jest wywołanie metody render(). W akcji dolorAction()
pojawia się wywołanie:
return $this->render("MySprBundle::dolor.html.twig");

Logiczna nazwa widoku:


MySprBundle::dolor.html.twig

odpowiada plikowi:
src/My/SprBundle/Resources/views/dolor.html.twig

Akcja sitAction() zawiera instrukcję:


return $this->render("MySprBundle:Def:sit.html.twig");

Logiczna nazwa:
MySprBundle:Def:sit.html.twig

odpowiada plikowi:
src/My/SprBundle/Resources/views/Def/sit.html.twig

Krok 3. Utwórz widoki akcji


Utwórz cztery pliki:
app/Resources/views/lorem.html.twig
app/Resources/views/Abc/ipsum.html.twig
src/My/SprBundle/Resources/views/dolor.html.twig
src/My/SprBundle/Resources/views/Def/sit.html.twig
Rozdział 9. ♦ Twig 119

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

jest przedstawiona na listingu 9.5.

Listing 9.5. Widok app/Resources/views/lorem.html.twig


<!DOCTYPE html>
<html>
<head>
<title>Lorem</title>
<meta charset="UTF-8" />
</head>
<body>

<h1>Lorem</h1>

</body>
</html>

Zwróć uwagę, że plik HTML, który nie zawiera żadnych znaczników Twig, jest poprawnym
widokiem.

Krok 4. Sprawdź działanie aplikacji


Odwiedź w przeglądarce cztery adresy WWW:
.../web/lorem.html
.../web/ipsum.html
.../web/dolor.html
.../web/sit.html

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

Składnia widoków Twig


Pliki o rozszerzeniu .twig (np. lorem.html.twig, base.html.twig, layout.html.twig) zawierają
kod przetwarzany przez bibliotekę Twig. Znaczniki Twig przyjmują jedną z trzech
form:
120 Część II ♦ Widoki

 {# #}
komentarze Twig;
 {{ }}
wydruk zmiennych, wyrażeń oraz wartości zwracanych przez funkcje Twig;
 {% %}
instrukcje sterujące Twig.

Komentarze Twig są wielowierszowe i nie mogą być zagnieżdżane:


{#
Przykład poprawnego
wielowierszowego komentarza Twig
#}

Oczywiście zawartość komentarzy Twig nie występuje w kodzie HTML wygenerowa-


nych stron WWW.

Znaczniki {{ }} wykorzystaliśmy do umieszczenia w kodzie HTML zmiennych,


adresów URL zasobów statycznych (funkcja asset()) oraz odsyłaczy do poszczególnych
stron serwisu (funkcja path()):
<link rel="stylesheet" href="{{ asset('css/style.css') }}" />
...
<a href="{{ path('_animals_jaszczurka') }}">jaszczurka</a>

Poznanymi w pierwszej części instrukcjami o składni {% %} są {% extends %} oraz


{% block %}:
{% extends "MyAnimalsBundle::layout.html.twig" %}

{% block content %}
...
{% endblock %}

Instrukcja {% extends %} występuje w widokach akcji i włącza dekorację widoku akcji


szablonem layout.html.twig. Parametrem instrukcji {% extends %} jest logiczna nazwa
widoku, np.:
MyAnimalsBundle::layout.html.twig

Druga z poznanych instrukcji {% block %} ustala zawartość bloku o podanej nazwie.

Wyłączanie interpretacji w szablonie


Czasami może zachodzić konieczność wyłączenia interpretacji znaczników Twig w sza-
blonie. Służy do tego instrukcja { % raw %} przedstawiona na listingu 9.6.
Rozdział 9. ♦ Twig 121

Listing 9.6. Instrukcja wyłączająca przetwarzanie fragmentu szablonu przez Twig


<pre>
{% raw %}
{% extends "::layout.html.twig" %}
{{ asset('images/photo.jpg') }}
{{ asset('css/style.css') }}
{% endraw %}
</pre>

Instrukcje zawarte pomiędzy znacznikami:


{% raw %}
...
{% endraw %}

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.

Do wydrukowania znaczników Twig w generowanym kodzie HTML możemy także wyko-


rzystać instrukcje:
{{ '{{' }}
oraz
{{ '}}' }}
Instrukcja {{ }} drukuje podane wyrażenie. Wyrażenie '{{' jest napisem składającym
się z dwóch nawiasów klamrowych. Kod:
<p>{{ '{{' }} asset('images/photo.jpg') {{ '}}' }}</p>
umieści w wygenerowanej stronie WWW tekst:
<p>{{ asset('images/photo.jpg') }}</p>

Przykład 9.2. Wyłączanie interpretacji


fragmentu szablonu
Wykorzystując oprogramowanie Symfony 2, wykonaj stronę WWW, która będzie za-
wierała opis szablonów Twig. Na stronie umieść opis trzech rodzajów znaczników Twig:
 {# #}
 {{ }}
 {% %}
122 Część II ♦ Widoki

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

utwórz pakiet My/SprBundle.

Krok 2. Dostosuj kod akcji index w kontrolerze Default


Kod metody indexAction() w pliku src/My/SprBundle/Controller/DefaultController.php
zmodyfikuj zgodnie z listingiem 9.7.

Listing 9.7. Zmodyfikowana metoda indexAction()


/**
* @Route("/")
* @Template()
*/
public function indexAction()
{
return array();
}

Krok 3. Dostosuj widok akcji index


W pliku src/My/SprBundle/Resources/views/Default/index.html.twig umieść kod, któ-
rego zarys jest przedstawiony na listingu 9.8.

Listing 9.8. Widok akcji index


...
<body>

<h1>Twig - tutorial</h1>

<h2>{{ '{# #}' }}</h2>

<p>
Komentarze Twig oznaczamy znacznikami:
</p>

<pre>
{% raw %}
{# ...lorem ipsum... #}
{% endraw %}
</pre>

...
Rozdział 9. ♦ Twig 123

Krok 4. Sprawdź działanie aplikacji


Odwiedź w przeglądarce adres:
.../web/

Podwójne rozszerzenie .html.twig


Pliki widoków mają podwójne rozszerzenia, np.:
.html.twig
.txt.twig
.css.twig

Człony te informują o języku, w jakim przygotowano widok, oraz o formacie generowa-


nego dokumentu. Rozszerzenie, którego używaliśmy dotychczas, czyli:
.html.twig

stosujemy dla stron HTML napisanych w języku Twig.

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.

Domyślny format odpowiedzi HTTP w Symfony 2 jest zdefiniowany przez parametr2


o nazwie _format i o wartości html:
_format=html

Wartość parametru _format ma wpływ na:


 nazwę przetwarzanego widoku,
 nagłówek Content-Type odpowiedzi H.

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

Modyfikacja nagłówka Content-Type


przy użyciu parametru _format
Jeśli parametr _format zmodyfikujemy, nadając mu wartość:
_format=txt

wówczas:
 Widokiem akcji będzie plik o rozszerzeniu .txt.twig.
 Odpowiedź HTTP będzie zawierać nagłówek: Content-Type: text/plain.

Do modyfikacji parametru _format możemy wykorzystać przedstawioną na listingu 9.9


adnotację @Route().

Listing 9.9. Adnotacja ustalająca format wyników generowanych przez akcję


/**
* @Route("/lorem", defaults={"_format"="txt"})
* @Template()
*/
public function ipsumAction()
{
return array();
}

Zapis podany na listingu 9.9 ustala, że:


 Widokiem akcji ipsumAction() będzie plik o nazwie ipsum.txt.twig.
 Odpowiedź HTTP będzie zawierała nagłówek: Content-Type: text/plain.

Modyfikacja nagłówka Content-Type metodą set()


Poznana wcześniej metoda render() zwraca jako wynik obiekt klasy Response3, która
reprezentuje odpowiedź HTTP. Obiekt ten zawiera właściwość headers4 typu Response
´HeaderBag, która reprezentuje nagłówki odpowiedzi. Metoda set() klasy Response
´HeaderBag służy do modyfikacji nagłówków:
$response = $this->render(':win.txt.twig');
$response->headers->set('Content-Type', 'text/plain;charset=windows-1250');

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

Przykład 9.3. Modyfikacja


nagłówka Content-Type
Wykorzystując oprogramowanie Symfony 2, wykonaj witrynę WWW, która będzie
prezentowała trzy dokumenty o adresach: /, style.css oraz win.txt. Dokumenty o adresach /
oraz win.txt mają być wysyłane jako tekstowe, czyli należy je opatrzyć nagłówkiem HTTP:
Content-Type: text/plain

Dokument o adresie style.css ma stosować nagłówek:


Content-Type: text/css

W dokumencie o adresie / umieść jeden akapit lorem ipsum. W dokumencie o adresie


style.css umieść dowolne style CSS. W dokumencie win.txt wprowadź natomiast dowolny
tekst w języku polskim. Zadanie wykonaj w taki sposób, by odpowiedź dla adresu win.txt
była zakodowana w formacie windows-1250.

Strony o adresach / oraz style.css wykonaj, wykorzystując adnotację @Route(). W kodzie


strony win.txt wywołaj metodę set() klasy ResponseHeaderBag.

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

utwórz pakiet My/SprBundle.

Krok 2. Dostosuj kod akcji index w kontrolerze Default


Kod metody indexAction() w pliku src/My/SprBundle/Controller/DefaultController.php
zmodyfikuj zgodnie z listingiem 9.10.

Listing 9.10. Zmodyfikowana metoda indexAction()


class DefaultController extends Controller
{

/**
* @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 indexAction() ustalają, że:


 Adresem dokumentu jest /.
 Widokiem akcji jest plik index.txt.twig.
 Odpowiedź HTTP zawiera nagłówek:
Content-Type: text/plain

Adnotacje akcji sAction() ustalają, że:


 Adresem dokumentu jest /style.css.
 Widokiem akcji jest plik s.css.twig.
 Odpowiedź HTTP zawiera nagłówek:
Content-Type: text/css

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');

Nagłówek odpowiedzi ustalamy, wywołując metodę set():


$response->headers->set('Content-Type', 'text/plain;charset=windows-1250');

Krok 3. Dostosuj widok akcji index


Utwórz trzy pliki:
src/My/SprBundle/Resources/views/Default/index.txt.twig
src/My/SprBundle/Resources/views/Default/s.css.twig
src/My/SprBundle/Resources/views/Default/win.txt.twig
Rozdział 9. ♦ Twig 127

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.

Krok 4. Sprawdź działanie aplikacji


Odwiedź w przeglądarce adresy WWW:
.../web/
.../web/style.css
.../web/win.txt

Korzystając z wtyczki Live HTTP Headers, sprawdź nagłówki odpowiedzi HTTP.


Analiza odpowiedzi dla strony win.txt jest przedstawiona na rysunku 9.3.

Rysunek 9.3.
Analiza odpowiedzi
HTTP dla adresu
win.txt

Pamiętaj, że w środowisku produkcyjnym szablony Twig podlegają cachowaniu. Oznacza


to, że jeśli odwiedzisz stronę, wykorzystując front kontroler app.php:
.../web/
to zawartość wszystkich szablonów Twig zostanie przetworzona do postaci PHP i zapi-
sana w folderze app/cache/. Jeśli wprowadzisz zmiany w szablonach Twig i ponownie
odwiedzisz adres:
.../web/
to nie ujrzysz wprowadzonych zmian. W celu ujrzenia zmian należy wyczyścić pamięć
podręczną (ang. cache) projektu. W ten sposób wymusisz ponowne przetworzenie
szablonów Twig do postaci PHP.
W środowisku deweloperskim szablony Twig nie podlegają cachowaniu. Jeśli więc
użyjesz front kontrolera app_dev.php:
.../web/app_dev.php/
to przy każdej wizycie zawartość stron WWW będzie generowana na podstawie plików
Twig. W związku z tym w celu ujrzenia zmian wprowadzonych w plikach .twig wystarczy
odświeżyć stronę.
128 Część II ♦ Widoki
Rozdział 10.
Zmienne, wyrażenia
i operatory Twig

Przekazywanie zmiennych do widoku


Metoda przekazywania danych z kontrolera do widoku zależy od sposobu uruchamiania
procedury interpretacji widoku. W poprzednim rozdziale omówiliśmy dwa rozwiązania
dotyczące powiązania akcji z widokiem:
 adnotację @Template(),
 ręczne wywołanie metody render().

Jeśli interpretacja widoku jest włączona adnotacją @Template(), zmienne przekazujemy


wówczas do widoku jako tablicę asocjacyjną będącą wynikiem metody akcji. Listing 10.1
ilustruje, w jaki sposób przekazać do widoku akcji ipsumAction() ciąg znaków o nazwie
name i wartości Janek.

Listing 10.1. Przekazywanie danych do widoków konfigurowanych adnotacją @Template()


/**
* @Route("/lorem.html")
* @Template()
*/
public function ipsumAction()
{
return array('name' => 'Janek');
}

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

Listing 10.2. Przekazywanie danych do widoków przetwarzanych wywołaniem metody render()


/**
* @Route("/lorem.html")
*/
public function ipsumAction()
{
return $this->render('::ipsum.html.twig', array('name' => 'Janek'));
}

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

spowoduje wygenerowanie tekstu:


xxxyyy

Zwróć uwagę, że znacznik {{ }} nie generuje żadnych białych znaków.

W środowisku deweloperskim użycie zmiennych, które nie zostały przekazane do


widoku, powoduje wygenerowanie wyjątku.

Przykład 10.1. Data i godzina


Wykorzystując oprogramowanie Symfony 2, przygotuj aplikację, która będzie zawierała
dwie strony WWW o adresach:
…/web/data.html
…/web/godzina.html

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

utwórz pakiet My/DateTimeBundle.

Krok 2. Dostosuj kontroler DefaultController


W pliku src/My/DateTimeBundle/Controller/DefaultController.php usuń metodę index
´Action() oraz dodaj dwie bezparametrowe metody dataAction() i godzinaAction().
Metoda dataAction() zawiera adnotację @Template(), a jej wynikiem jest tablica asocja-
cyjna, w której występuje element o indeksie data. Dzięki temu w widoku akcji dostępna
będzie zmienna o nazwie data.

Metoda godzinaAction() nie zawiera adnotacji @Template(). Powiązanie akcji z wido-


kiem realizujemy, wywołując metodę render(). Pierwszym parametrem metody render()
będzie logiczna nazwa widoku, czyli MyDateTimeBundle:Default:godzina.html.twig,
a drugim — tablica asocjacyjna zawierająca element o indeksie godzina.

Plik DefaultController.php jest przedstawiony na listingu 10.3.

Listing 10.3. Zmodyfikowany plik DefaultController.php


class DefaultController extends Controller
{
/**
* @Route("/data.html")
* @Template()
*/
public function dataAction()
{
$data = date('d.m.Y');

return array('data' => $data);


}
/**
* @Route("/godzina.html")
*/
public function godzinaAction()
{
$godzina = date('h:i:s');
return $this->render('MyDateTimeBundle:Default:godzina.html.twig',
´array('godzina' => $godzina));
}

}
132 Część II ♦ Widoki

Krok 3. Wykonaj widoki akcji


Usuń zbędny plik src/My/DateTimeBundle/Resources/views/Default/index.html.twig,
a następnie przygotuj widoki:
src/My/DateTimeBundle/Resources/views/Default/data.html.twig
src/My/DateTimeBundle/Resources/views/Default/godzina.html.twig

W pierwszym z nich umieść kod pustej strony zawierający znacznik:


<h1>{{ data }}</h1>

W drugim, wewnątrz pustego dokumentu HTML, umieść znacznik:


<h1>{{ godzina }}</h1>

Krok 4. Sprawdź działanie aplikacji


Odwiedź w przeglądarce adresy WWW:
.../web/data.html
.../web/godzina.html

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:
&lt; &gt; &amp; &quot; &#039;

W PHP konwersje takie wykonujemy, wywołując funkcję htmlspecialchars(). W Sym-


fony 2 zmienne drukowane znacznikami {{ }} podlegają takim zabezpieczeniom auto-
matycznie. Jeśli do widoku przekażemy tekst zawierający znaczniki, np.:
public function ipsumAction()
{
return array('tekst' => '<h1>Witaj</h1>');
}

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

spowoduje wydrukowanie napisu:


&lt;h1&gt;Witaj&lt;/h1&gt;

Do wyłączenia powyższej konwersji w odniesieniu do jednej zmiennej służy filtr raw.


Instrukcja:
{{ tekst|raw }}

spowoduje wydrukowanie zmiennej tekst w niezmienionej postaci, czyli:


<h1>Witaj</h1>

Co ciekawe, wydruk wyrażeń tekstowych, które nie zawierają zmiennych, nie podlega
automatycznym zabezpieczeniom. Instrukcja:
{{ "<h3>Uwaga! Znaczniki niezabezpieczone!</h3>" }}

spowoduje wydrukowanie tekstu:


<h3>Uwaga! Znaczniki niezabezpieczone!</h3>

Automatyczne zabezpieczanie zmiennych możemy wyłączyć globalnie, dla całego środo-


wiska, umieszczając w pliku app/config/config.yml przedstawioną na listingu 10.4 regułę
konfiguracyjną autoescape.

Listing 10.4. Wyłączanie automatycznego zabezpieczania zmiennych umieszczamy w pliku konfiguracyjnym


app/config/config.yml
twig:
debug: %kernel.debug%
strict_variables: %kernel.debug%
autoescape: false

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:
&lt;h2&gt;Pożegnanie&lt;/h2&gt;

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

Filtr escape jest zaimplementowany na bazie funkcji htmlspecialchars().

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') }}

Aby włączyć lokalne automatyczne zabezpieczanie zmiennych, stosujemy dla fragmentu


wybranego widoku znaczniki:
{% autoescape true %}
{{ tytul }}
{{ opis }}
{% endautoescape %}

Zmienne zawarte wewnątrz powyższego bloku, tj. tytul oraz opis, będą podlegały
automatycznym zabezpieczeniom.

Znacznik autoescape może przyjmować dodatkowy parametr ustalający rodzaj za-


bezpieczeń. Tekst zawarty w bloku:
{% autoescape true js %}
...
{% endautoescape %}

zostanie zabezpieczony względem języka JavaScript.

Filtr raw:
{{ zm | raw }}

wyłącza konwersję zmiennej zm funkcją htmlspecialchars(), natomiast znacznik raw:


{% raw %}
{% endraw %}

wyłącza interpretację znaczników Twig.

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

utwórz pakiet My/HtmlBundle.

Krok 2. Dostosuj kontroler DefaultController


W pliku src/My/HtmlBundle/Controller/DefaultController.php usuń metodę indexAction()
oraz dodaj przedstawioną na listingu 10.5 bezparametrową metodę powitanieAction().

Listing 10.5. Zmodyfikowany plik DefaultController.php


class DefaultController extends Controller
{
/**
* @Route("/")
* @Template()
*/
public function powitanieAction()
{
$komunikat = '<h1>Cześć!</h1>';
return array('komunikat' => $komunikat);
}
}

W metodzie powitanieAction() tworzymy zmienną $komunikat, która zawiera kod HTML


oraz polskie znaki diakrytyczne. Dzięki instrukcji:
return array('komunikat' => $komunikat);

zmienna $komunikat zostaje przekazana do widoku.

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

Krok 3. Wykonaj widoki akcji


Usuń zbędny plik src/My/HtmlBundle/Resources/views/Default/index.html.twig, a na-
stępnie przygotuj widok:
136 Część II ♦ Widoki

src/My/HtmlBundle/Resources/views/Default/powitanie.html.twig

o zawartości przedstawionej na listingu 10.6.

Listing 10.6. Widok powitanie.html.twig


<!DOCTYPE html>
<html>
<head>
<title>Powitanie</title>
<meta charset="UTF-8" />
</head>
<body>

{{ 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 />

{{ " ---> <strong>brak cytowania</strong> <=== " }}

<hr />

{{ " ---> cytowanie (także napisów dołączonych do zmiennej komunikat): " ~


´komunikat ~ " <=== " }}

</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>

Tabela 10.1. Działanie instrukcji z listingu 10.6


Wartość autoescape w pliku
Instrukcja w widoku Wydrukowany napis
app/config/config.yml
{{ komunikat }} &lt;h1&gt;Cześć!&lt;/h1&gt; true
{{ komunikat|raw }} <h1>Cześć!</h1> Bez wpływu
{{ komunikat | e }} &lt;h1&gt;Cześć!&lt;/h1&gt; Bez wpływu
{{ komunikat | e('js') }} \x3ch1\x3eCześć\x21\x3c\x2fh1\x3e Bez wpływu
{% autoescape true %} &lt;h1&gt;Cześć!&lt;/h1&gt; Bez wpływu
{{ komunikat }}
{% endautoescape %}
{% autoescape true js %} \x3ch1\x3eCześć\x21\x3c\x2fh1\x3e Bez wpływu
{{ komunikat }}
{% endautoescape %}
{% autoescape false %} <h1>Cześć!</h1> Bez wpływu
{{ komunikat }}
{% endautoescape %}
{{ " ---> <strong>brak ---> <strong>brak Bez wpływu
´cytowania</strong> <=== " }} ´cytowania</strong> <===
{{ " ---> cytowanie " ~ ---&gt; cytowanie &lt;h1&gt; true
´komunikat ~ " <=== " }} ´Cześć!&lt;/h1&gt; &lt;===

Zwróć uwagę na dwie ostatnie pozycje w tabeli 10.1. Instrukcja:


{{ " ---> <strong>brak cytowania</strong> <=== " }}

drukuje niezabezpieczony napis:


---> <strong>brak cytowania</strong> <===

Napisy osadzone jako wyrażenia wewnątrz instrukcji {{ }} nie podlegają zatem au-
tomatycznym zabezpieczeniom.

Jeśli jednak wyrażenie zawarte wewnątrz instrukcji {{ }} powstaje przez połączenie


kilku napisów oraz zmiennych3:
{{ " ---> cytowanie" ~ komunikat ~ " <=== " }}

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 >:
---&gt; zabezpieczone &lt;h1&gt;Cześć!&lt;/h1&gt; &lt;===

3
W szablonach Twig operator ~ służy do konkatenacji napisów.
138 Część II ♦ Widoki

Krok 4. Sprawdź działanie aplikacji


Odwiedź w przeglądarce adres:
.../web/

Przekazywanie do widoku tablic


Jeśli zmienna przekazana do widoku jest tablicą, to dostęp do składowych realizujemy
przy użyciu odwołań:
{{ tablica['indeks'] }}

lub
{{ tablica.indeks }}

Jeśli z akcji do widoku przekażemy tablicę dane o indeksach 0, 1 oraz ipsum:


public function ipsumAction()
{
return array('dane' => array('Lorem', 1234, 'ipsum' => 'Dolor');
}

wówczas w widoku możemy użyć odwołań:


{{ dane[0] }}
{{ dane[1] }}
{{ dane['ipsum'] }}

oraz:
{{ dane.0 }}
{{ dane.1 }}
{{ dane.ipsum }}

Jeśli tablica przekazywana do widoku jest wielowymiarowa, np.:


public function ipsumAction()
{
return array(
'dane' => array(
'student1' => array(
'imie' => 'Jan',
'nazwisko' => 'Kowalski'
)
)
);
}

wówczas w odwołaniu pojawi się kilka indeksów, np.:


{{ dane['student1']['imie'] }}
{{ dane['student1']['nazwisko'] }}
Rozdział 10. ♦ Zmienne, wyrażenia i operatory Twig 139

lub:
{{ dane.student1.imie }}
{{ dane.student1.nazwisko }}

Przekazywanie do widoku obiektów


W odniesieniu do obiektów stosujemy odwołania postaci:
{{ obj.abc }}

Powyższe odwołanie powoduje próbę dostępu do:


 właściwości $obj->abc,
 metody $obj->abc(),
 metody $obj->getAbc(),
 metody $obj->isAbc().

Pierwsze odwołanie, które zakończy się sukcesem, spowoduje wydrukowanie zwróconej


wartości. Jeśli w obiekcie nie istnieje właściwość abc ani żadna z metod abc(), getAbc()
czy isAbc(), wówczas wydrukowana zostanie wartość null.

Jeśli obiekt $obj ma zaimplementowaną metodę __toString(), instrukcja:


{{ $obj }}
będzie równoważna instrukcji:
{{ $obj->__toString() }}

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);
}

to w widoku możemy te zmienne wykorzystać, tworząc wyrażenie:


{{ a * b * c + 6 }}
140 Część II ♦ Widoki

Powyższa instrukcja spowoduje wydrukowanie liczby 304. Zwróć uwagę, że znaczniki


{{ }} występują tylko jeden raz. Zmiennych nie otaczamy dodatkowymi klamrami.

W szablonach Twig zapis:


{{ ... }}

odgrywa rolę analogiczną do funkcji echo z języka PHP:


echo ...;

Podstawowymi typami, które tworzą wyrażenia Twig, są:


 liczby,
 ciągi znaków,
 tablice indeksowane,
 tablice asocjacyjne,
 wartości logiczne,
 specjalna wartość none.

Liczby w wyrażeniach Twig zapisujemy, wykorzystując cyfry dziesiętne oraz znak kropki:
1234
3.1415

Ciągi znaków definiujemy, stosując apostrofy lub cudzysłów:


"pierwszy napis"
'drugi napis'

Tablice indeksowane tworzymy, stosując nawiasy kwadratowe:


["pierwszy element", "drugi element"]

Tablice asocjacyjne definiujemy, wykorzystując nawiasy klamrowe:


{"klucz1": "wartość1", "klucz2": "wartość2"}

Tablice indeksowane i asocjacyjne mogą być zagnieżdżane:


["pierwszy element", "drugi element", {"k": "v"} ]
{"klucz1": "wartość1", "klucz2": ["a", "b"]}

Wartościami logicznymi są:


true
false

Specjalna wartość none jest odpowiednikiem wartości null z języka PHP.

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.

Operatory arytmetyczne są zawarte w tabeli 10.2. Zestawienie operatorów porównania


zawiera tabela 10.3. Tabela 10.4 zawiera zestawienie operatorów logicznych, a tabela
10.5 prezentuje operatory specjalne.

Tabela 10.2. Operatory arytmetyczne Twig


Operator Opis Przykład
+ Suma {{ 2 + 3 }} wydrukuje 5.
- Różnica {{ 10 - 4 }} wydrukuje 6.
/ Iloraz (wynik jest liczbą zmiennopozycyjną) {{ 1 / 2 }} wydrukuje 0.5.
% Reszta z dzielenia {{ 20 % 7 }} wydrukuje 6.
// Iloraz całkowity {{ 20 // 7 }} wydrukuje 2.
* Iloczyn {{ 5 * 6 }} wydrukuje 30.
** Potęgowanie {{ 3 ** 4 }} wydrukuje 81.

Tabela 10.3. Operatory porównania


Operator Opis Przykład
== Równość {{ 2 == 2 }} wydrukuje 1.
Operatora możemy użyć do porównywania {{ 2 == 3 }} nie wydrukuje nic (logiczny
dowolnych typów danych: liczb, napisów, fałsz jest konwertowany na napis pusty).
tablic, zmiennych logicznych oraz obiektów.
!= Różność {{ ["a"] != 4 }} wydrukuje 1.
Operatora możemy użyć do porównywania
dowolnych typów danych: liczb, napisów,
tablic, zmiennych logicznych oraz obiektów.
< Mniejszy {{ 2 < 10 }} wydrukuje 1.
> Większy {{ 20 > 7 }} wydrukuje 1.
<= Mniejszy lub równy {{ 20 <= 20 }} wydrukuje 1.
>= Większy lub równy {{ 6 >= 5 }} wydrukuje 1.
142 Część II ♦ Widoki

Tabela 10.4. Operatory logiczne Twig


Operator Opis Przykład
and Koniunkcja {{ true and true }} wydrukuje 1.
or Alternatywa {{ false or false }} nie wydrukuje nic.
not Negacja {{ not false }} wydrukuje 1.

Tabela 10.5. Operatory specjalne


Operator Opis Przykład
.. Zakres. [1..3] tworzy tablicę [1, 2, 3].
Uproszczony zapis wywołania funkcji PHP
range(). Tworzy tablicę wartości z podanego
zakresu.
in Bada występowanie zadanego elementu 5 in [2, 5, 8] Czy liczba 5 występuje
w tablicy lub wzorca w napisie oraz ułatwia w tablicy?
przetwarzanie obiektów implementujących 'bc' in 'abcde' Czy wzorzec bc występuje
interfejs Traversable. w łańcuchu abcde?
is Wykonuje test wartości wyrażenia. i is odd Czy liczba i jest nieparzysta?
i is divisibleby(5) Czy liczba i jest
podzielna przez 5?
~ Konkatenacja łańcuchów "lorem" ~ "ipsum" tworzy łańcuch
loremipsum.
?: Skrótowy zapis instrukcji5 if a < b ? "tak" : "nie" Jeśli a jest mniejsze
(odpowiednik operatora PHP ?:) od b, wartością wyrażenia jest łańcuch tak,
w przeciwnym razie wartością wyrażenia
jest nie.
| Operator stosujący filtr zm | raw Zmienna zm zostanie
przekształcona filtrem raw.
. Dostęp do właściwości lub składowej osoba.imie W przypadku obiektu:
tablicy właściwość o nazwie imie lub wynik
działania jednej z metod (pierwszej
znalezionej): imie(), getImie() lub isImie().
W przypadku tablicy: składowa o indeksie
imie.
[] Dostęp do składowej tablicy lub osoba["imie"] Składowa o indeksie imie.
utworzenie nowej tablicy [1, 2, 'lorem', 'ipsum'] tworzy tablicę
czteroelementową o indeksach 0, 1, 2 i 3
oraz wartościach 1, 2, lorem i ipsum.
{} Utworzenie nowej tablicy asocjacyjnej {'lorem': 'ipsum', 'dolor': 'sit'}
tworzy tablicę dwuelementową o indeksach
lorem oraz dolor i o wartościach ipsum
oraz sit.

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.

Tabela 10.6. Testy, które możemy stosować, wykorzystując operator is


Test Opis
divisibleby Test sprawdzający podzielność
null Test sprawdzający, czy wartością zmiennej jest null
even Test sprawdzający parzystość
odd Test sprawdzający nieparzystość
sameas Test sprawdzający, czy podana zmienna wskazuje ten sam adres pamięci co inna zmienna
constant Test sprawdzający, czy podana zmienna ma identyczną wartość jak stała o podanej nazwie
defined Test sprawdzający, czy podana zmienna jest zdefiniowana
empty Test sprawdzający, czy podana zmienna ma wartość null lub false, jest napisem
pustym lub tablicą niezawierającą elementów

Aktualna i kompletna lista testów jest dostępna na stronie http://twig.sensiolabs.org/


doc/tests/index.html.

Do grupowania fragmentów wyrażeń stosujemy nawiasy okrągłe, np.:


{{ (2 + 3) * (5 – 2) }}

W wyrażeniach, które nie zawierają nawiasów, obliczenia będą wykonywane zgodnie


z priorytetami operatorów:
or, and, ==, !=, <, >, >=, <=, in, .., +, -, ~, *, /, //, %, is, **

Najniższy priorytet ma operator or, a najwyższy — operator **. W przypadku pominięcia


nawiasów w wyrażeniu:
{{ 2 + 3 * 5 – 2 }}

wartość będzie zatem równoważna wyrażeniu:


{{ 2 + (3 * 5) – 2 }}

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 }}

lub użyta w dowolnym wyrażeniu:


{{ ile * 10 - 5}}

Powyższa instrukcja wydrukuje wartość 156.

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] %}

tworzy dwuelementową tablicę o nazwie t. Do elementów tablicy możemy uzyskać do-


stęp, stosując zapis:
t[0]
t.0

Obie poniższe instrukcje:


{{ t[0] }}
{{ t.0 }}

wydrukują wartość 11.

Instrukcja:
{% set q = {'lorem': 'ipsum', 'dolor': 'sit'} %}

definiuje natomiast tablicę o nazwie q. Oba poniższe wywołania:


{{ q['lorem'] }}
{{ q.lorem }}

wydrukują napis ipsum.

Należy pamiętać, że tablice są dostępne w widokach wyłącznie w trybie do odczytu.


Przypisanie wartości do elementu tablicy nie powiedzie się:
PRZYKŁADY BŁĘDNE
{% set t[0] = 999 %}
{% set q.lorem = 'abc' %}

6
15 = 2 · 10 – 5
Rozdział 10. ♦ Zmienne, wyrażenia i operatory Twig 145

podobnie jak utworzenie nowych elementów w tablicy:


PRZYKŁADY BŁĘDNE
{% set t[17] = 777 %}
{% set q.inny = 'xyz' %}

Instrukcja:
{% set t[0] = 999 %}

generuje wyjątek:
Unexpected token "punctuation" of value "[" ("end of statement block" expected) in
´{} at line X

Instrukcję {% set %} możemy także wykorzystać do utworzenia zmiennej, która będzie


zawierała generowany kod HTML. Wywołanie:
{% set fragment %}
<ul>
...
</ul>
{% endset %}

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

Zmienna _self jest przetwarzanym widokiem. Zmienna _context wskazuje bieżący


kontekst, a zmienna _charset zawiera kodowanie znaków. Zmiennej _self używamy
między innymi do wskazania, że makrodefinicja pochodzi z bieżącego widoku, a zmienna
_context pozwala na uzyskanie dostępu do zmiennych widoku, dzięki czemu wszystkie
zmienne widoku możemy przekazać np. do makrodefinicji. Przykłady użycia zmiennych
_self oraz _context są zawarte w rozdziale 12. w punkcie omawiającym makrodefinicje.
146 Część II ♦ Widoki
Rozdział 11.
Instrukcje sterujące
for oraz if

Instrukcja for
Znacznik for służy do iteracyjnego przetwarzania tablic. Ogólna składnia znacznika for
jest przedstawiona na listingu 11.1.

Listing 11.1. Ogólna składnia znacznika for


{% for element in tablica %}
{{ element }}
{% endfor %}

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.

Listing 11.2. Znacznik for zawierający blok else


{% for element in tablica %}
{{ element }}
{% else %}
brak elementów
{% endfor %}

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 %}

Przetwarzanie kolejnych elementów tablicy możemy ograniczyć do elementów, które


spełniają podany warunek. Ogólna postać znacznika for zawierającego ograniczenie
if jest przedstawiona na listingu 11.4.

Listing 11.4. Znacznik for zawierający instrukcję if


{% for element in tablica if warunek %}
{{ element }}
{% endfor %}

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.

Tabela 11.1. Składowe zmiennej loop dostępnej wewnątrz znacznika for


Zmienna Opis
loop.index Bieżący indeks iteracji (pierwszy obrót pętli ma indeks 1)
loop.index0 Bieżący indeks iteracji (pierwszy obrót pętli ma indeks 0)
loop.revindex Liczba iteracji liczona od końca (indeksacja rozpoczyna się od 1 — ostatni obrót
iteracji wygeneruje numer 1)
loop.revindex0 Liczba iteracji liczona od końca (indeksacja rozpoczyna się od 0 — ostatni obrót
iteracji wygeneruje numer 0)
loop.first Zwraca wartość true w pierwszym obrocie iteracji
loop.last Zwraca wartość true w ostatnim obrocie iteracji
loop.length Liczba elementów tablicy
loop.parent W przypadku iteracji zagnieżdżonych zapewnia dostęp do nadrzędnej iteracji

Zmienna loop.index może zostać wykorzystana do wygenerowania liczb porządkowych


dla przetwarzanych elementów tablicy:
{% for osoba in osoby %}
<tr>
<td>{{ loop.index }}.<td>
Rozdział 11. ♦ Instrukcje sterujące for oraz if 149

<td>{{ osoba.imie }}<td>


<td>{{ osoba.nazwisko }}<td>
</tr>
{% endfor %}

Natomiast zmiennych loop.first i loop.last w połączeniu z omówioną w kolejnym


punkcie instrukcją if możemy użyć do modyfikacji przetwarzania pierwszego oraz
ostatniego elementu. Pętla:
{% for imie in im imiona %}
{{ imie }}
{% if not loop.last %}
,
{% endif%}
{% endfor %}

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 %}

Kod z listingu 11.6 odpowiada następującej instrukcji w języku PHP:


foreach ($tablica as $k => $v) {
echo $k;
echo $v;
}

Przetwarzanie samych kluczy z tablicy asocjacyjnej możemy także wykonać, wyko-


rzystując filtr keys:
{% for k in tablica|keys %}
{{ k }}
{% 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

{% for numer in [5, 7, 13] %}


{{ numer }}
{% endfor %}

{% for kolor in ['red', 'green', 'blue'] %}


{{ kolor }}
{% endfor %}

{% for k, v in {'pierwszy': 'lorem', 'drugi':'ipsum'} %}


{{ k }} - {{ v }}
{% endfor %}

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.

Listing 11.8. Znacznik if


{% if warunek %}
...
{% endif %}

{% if warunek %}
...
{% else %}
...
{% endif %}

{% if warunek %}
...
{% elseif warunek %}
...
{% elseif warunek %}
...
{% else %}
...
{% endif %}

W warunku instrukcji if możemy użyć:


 operatorów porównania z tabeli 10.3,
 operatorów logicznych z tabeli 10.4,
Rozdział 11. ♦ Instrukcje sterujące for oraz if 151

 operatora specjalnego in,


 operatora specjalnego is w połączeniu z testami z tabeli 10.6.

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' }}

Przykład 11.1. Korona ziemi


Dany jest plik tekstowy korona-ziemi.txt, którego zarys jest przedstawiony na listingu 11.9.

Listing 11.9. Zarys pliku korona-ziemi.txt


Azja:Mount Everest:8850
Ameryka Południowa:Aconcagua:6959
Ameryka Północna:Mount McKinley:6194
...

Wykonaj aplikację, która będzie prezentowała na stronie głównej tabelę HTML za-
wierającą dane z pliku korona-ziemi.txt.

Do wykonania zadania wykorzystaj dane oraz szablon HTML z pliku 11-01-start.zip.


152 Część II ♦ Widoki

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

utwórz pakiet My/MountainsBundle.

Następnie w folderze projektu utwórz folder data/ i umieść w nim plik korona-ziemi.txt.

Krok 2. Dostosuj kontroler DefaultController


W metodzie indexAction() kontrolera DefaultController.php wprowadź kod z li-
stingu 11.10.

Listing 11.10. Zmodyfikowany plik DefaultController.php


class DefaultController extends Controller
{
/**
* @Route("/")
* @Template()
*/
public function indexAction()
{
$plk = file('../data/korona-ziemi.txt');
$tab = array();
foreach ($plk as $l) {
$e = explode(':', trim($l));
$tab[] = array(
'kontynent' => $e[0],
'nazwa' => $e[1],
'wysokosc' => $e[2],
);
}
return array('dane' => $tab);
}
}

Wewnątrz metody indexAction() odczytujemy plik korona-ziemi.txt, po czym two-


rzymy tablicę $tab. W pętli foreach przetwarzamy poszczególne wiersze pliku tekstowego.
Każdy wiersz jest najpierw przekształcany funkcją trim(), która usuwa wiodące i koń-
cowe białe znaki. Następnie funkcją explode() przekształcamy ciąg znaków zawierający
dwukropki, np.:
Azja:Mount Everest:8850
Rozdział 11. ♦ Instrukcje sterujące for oraz if 153

w trójelementową tablicę $e:


0 => Azja
1 => Mount Everest
2 => 8850

Na podstawie tablicy $e tworzymy nową tablicę o indeksach kontynent, nazwa i wysokosc:


array(
'kontynent' => $e[0],
'nazwa' => $e[1],
'wysokosc' => $e[2],
);

Utworzona tablica jest dołączana na końcu tablicy $tab:


$tab[] = array(
...
);

Na zakończenie dwuwymiarowa tablica $tab jest przekazywana do widoku jako zmienna


dane:
return array('dane' => $tab);

Krok 3. Wykonaj widoki akcji


W widoku akcji index należy wykorzystać znaczniki {% for %} {% endfor %} oraz
zmienną loop.index. Zarys pliku index.html.twig jest przedstawiony na listingu 11.11.
Zwróć uwagę, że widok z listingu 11.11 zawiera kompletną stronę WWW z osadzony-
mi stylami CSS. Szablon HTML zawierający odpowiednie style znajdziesz w pliku
11-01-start.zip.

Listing 11.11. Zarys widoku akcji index


<!DOCTYPE html>
<html>
<head>
<title>Korona Ziemi</title>
<meta charset="UTF-8" />
<style>
body {
font-family: Verdana, sans-serif;
font-size: medium;
}
...
</style>
</head>
<body>
<h1>Korona Ziemi</h1>

<table>
<tr>
<th>lp.</th>
<th>Kontynent</th>
<th>Szczyt</th>
154 Część II ♦ Widoki

<th>Wysokość<br />(m n.p.m.)</th>


</tr>
{% for gora in dane %}
<tr>
<td>{{ loop.index }}.</td>
<td>{{ gora.kontynent }}</td>
<td>{{ gora.nazwa }}</td>
<td>{{ gora.wysokosc }}</td>
</tr>
{% endfor %}
</table>

</body>
</html>

Krok 4. Sprawdź działanie aplikacji


Po odwiedzeniu w przeglądarce adresu:
.../web/
ujrzysz witrynę widoczną na rysunku 11.1.

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.

Listing 11.12. Fragmenty pliku dziela_literatury_swiatowej.txt


Steinbeck|Grona gniewu|amer.|1939
Hemingway|Komu bije dzwon|amer.|1940
Camus|Upadek|fr.|1956
...

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.

Do wykonania zadania wykorzystaj dane oraz szablon HTML z pliku 11-02-start.zip.

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

utwórz pakiet My/LiteratureBundle.

Następnie w folderze projektu utwórz folder data/ i umieść w nim plik dziela_literatury_
swiatowej.txt.

Krok 2. Dostosuj kontroler DefaultController


W metodzie indexAction() kontrolera DefaultController.php wprowadź kod z li-
stingu 11.13.

Listing 11.13. Zmodyfikowany plik DefaultController.php


class DefaultController extends Controller
{
/**
* @Route("/")
* @Template()
*/
public function indexAction()
{
$t = array();
156 Część II ♦ Widoki

foreach (file('../data/dziela_literatury_swiatowej.txt') as $l) {


$e = explode('|', trim($l));
$t[] = array(
'autor' => $e[0],
'tytul' => $e[1],
'literatura' => $e[2],
'data' => $e[3],
);
}
return array('literatura' => $t);
}
}

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.

Krok 3. Wykonaj widoki akcji


W widoku akcji index tablica literatura jest przetwarzana iteracyjnie przy użyciu
znaczników {% for %} {% endfor %}. W każdym obrocie pętli na podstawie zmiennej
loop.index generujemy dokładnie jeden ze znaczników:
<tr class="w10">
<tr class="w2">
<tr>

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.

Listing 11.14. Zarys widoku akcji index


<!DOCTYPE html>
<html>
<head>
<title>Dzieła literatury światowej</title>
<meta charset="UTF-8" />
<style>
body {
font-family: Verdana, sans-serif;
}
...
</style>
</head>
<body>
<h1>Dzieła literatury światowej</h1>
<table>
<tr>
<th>lp.</th>
<th>Autor</th>
<th>Tytuł</th>
<th>Literatura</th>
<th>Okres powstania lub data publikacji utworu</th>
Rozdział 11. ♦ Instrukcje sterujące for oraz if 157

</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 %}

<td>{{ loop.index }}.</td>


<td>{{ utwor.autor }}</td>
<td>{{ utwor.tytul }}</td>
<td>{{ utwor.literatura }}</td>
<td>{{ utwor.data }}</td>
</tr>
{% endfor %}
</table>

</body>
</html>

Krok 4. Sprawdź działanie aplikacji


Po odwiedzeniu w przeglądarce adresu:
.../web/
ujrzysz witrynę z rysunku 11.2.

Przykład 11.3. Tabliczka mnożenia


Wykonaj aplikację, która będzie prezentowała na stronie głównej tabliczkę mnożenia.
Zadanie wykonaj w taki sposób, by cały kod odpowiedzialny za generowanie tablicz-
ki mnożenia był zapisany w widokach Twig. Szablon HTML/CSS znajdziesz w pliku
11-03-start.zip.

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

utwórz pakiet My/MultiplicationBundle.


158 Część II ♦ Widoki

Rysunek 11.2. Dzieła literatury światowej — witryna z przykładu 11.2

Krok 2. Dostosuj kontroler DefaultController


W metodzie indexAction() kontrolera DefaultController.php wprowadź kod z li-
stingu 11.15.

Listing 11.15. Zmodyfikowany plik DefaultController.php


class DefaultController extends Controller
{
/**
* @Route("/")
* @Template()
*/
public function indexAction()
{
return array();
}
}
Rozdział 11. ♦ Instrukcje sterujące for oraz if 159

Krok 3. Wykonaj widoki akcji


W widoku akcji index najpierw definiujemy dwie zmienne, które ustalą liczbę wierszy
i kolumn tabliczki mnożenia:
{% set liczba_wierszy = 12 %}
{% set liczba_kolumn = 8 %}

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>

Poszczególne wiersze tabliczki mnożenia generujemy w podwójnej pętli:


{% for i in 1..liczba_wierszy %}
<tr>
<th>{{ i }}</th>
{% for j in 1..liczba_kolumn %}
<td>{{ i * j }}</td>
{% endfor %}
</tr>
{% endfor %}

Zarys widoku index.html.twig jest przedstawiony na listingu 11.16.

Listing 11.16. Zarys widoku akcji index


<!DOCTYPE html>
<html>
<head>
<title>Tabliczka mnożenia</title>
<meta charset="UTF-8" />
<style>
body {
font-family: Verdana, sans-serif;
text-align: center;
}
...
</style>
</head>
<body>

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

Krok 4. Sprawdź działanie aplikacji


Po odwiedzeniu w przeglądarce adresu:
.../web/
ujrzysz witrynę z rysunku 11.3.

Rysunek 11.3.
Tabliczka mnożenia
— witryna
z przykładu 11.3
Rozdział 11. ♦ Instrukcje sterujące for oraz if 161

Przykład 11.4. Tabela potęg


Wykonaj aplikację, która będzie prezentowała na stronie głównej tabelę potęg o pod-
stawach od 2 do 10 i o wykładnikach od 1 do 8. Wykorzystaj szablon zawarty w pliku
11-04-start.zip.

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

utwórz pakiet My/PowersBundle.

Krok 2. Dostosuj kontroler DefaultController


W metodzie indexAction() kontrolera DefaultController.php wprowadź kod z listin-
gu 11.15.

Krok 3. Wykonaj widoki akcji


W widoku akcji definiujemy zmienne, które ustalą zakres podstaw i wykładników:
{% set podstawa = 10 %}
{% set wykladnik = 5 %}

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>

Do wykonania symbolu an stosujemy element sup:


a<sup>n</sup>

Poszczególne wiersze zestawienia potęg generujemy podwójną pętlą:


{% for i in 2..podstawa %}
<tr>
<th>{{ i }}</th>
{% for j in 1..wykladnik %}
<td>{{ i ** j }}</td>
162 Część II ♦ Widoki

{% endfor %}
</tr>
{% endfor %}

Zarys widoku index.html.twig jest przedstawiony na listingu 11.17.

Listing 11.17. Zarys widoku akcji index


<!DOCTYPE html>
<html>
<head>
<title>Tabela potęg</title>
<meta charset="UTF-8" />
<style>
body {
font-family: Verdana, sans-serif;
text-align: center;
}
...
</style>
</head>
<body>

<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

Krok 4. Sprawdź działanie aplikacji


Po odwiedzeniu w przeglądarce adresu:
.../web/
ujrzysz witrynę z rysunku 11.4.

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

Przykładowymi bezpiecznymi kolorami są m.in. barwy o następujących kodach RGB:


0033FF
33FFCC
CC9900
999999
333399

Wykorzystaj szablon zawarty w pliku 11-05-start.zip.

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

utwórz pakiet My/ColorsBundle.

Krok 2. Dostosuj kontroler DefaultController


W metodzie indexAction() kontrolera DefaultController.php wprowadź kod z li-
stingu 11.15.

Krok 3. Wykonaj widoki akcji


Widok akcji rozpoczynamy od zdefiniowania trzech zmiennych:
{% set kody = ['00', '33', '66', '99', 'CC', 'FF'] %}
{% set kolumny = 6 %}
{% set numer = 0 %}

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>

Kod kolejnego koloru tworzymy, łącząc trzy wartości z tablicy kody:


{% set kolorek = kody[i] ~ kody[j] ~ kody[k] %}

Zmienna kolorek będzie przyjmowała kolejno wartości:


000000 000033 000066 000099 0000CC 0000FF
003300 003333 003366 003399 0033CC 0033FF
006600 006633 006666 006699 0066CC 0066FF
Rozdział 11. ♦ Instrukcje sterujące for oraz if 165

...
FFCC00 FFCC33 FFCC66 FFCC99 FFCCCC FFCCFF
FFFF00 FFFF33 FFFF66 FFFF99 FFFFCC FFFFFF

Znacznik otwierający wiersz tabeli drukujemy przed elementami:


<tr>000000 000033 000066 000099 0000CC 0000FF
<tr>003300 003333 003366 003399 0033CC 0033FF
<tr>006600 006633 006666 006699 0066CC 0066FF
...
<tr>FFCC00 FFCC33 FFCC66 FFCC99 FFCCCC FFCCFF
<tr>FFFF00 FFFF33 FFFF66 FFFF99 FFFFCC FFFFFF

Elementy te mają indeksy 0, 6, 12, 18 itd. Do wstawienia znacznika <tr> wykorzy-


stujemy zatem warunek podzielności numeru drukowanego koloru przez liczbę ko-
lumn:
{% if (numer % kolumny) == 0 %}
<tr>
{% endif %}

Znacznik zamykający wiersz tabeli drukujemy po elementach:


<tr>000000 000033 000066 000099 0000CC 0000FF</tr>
<tr>003300 003333 003366 003399 0033CC 0033FF</tr>
<tr>006600 006633 006666 006699 0066CC 0066FF</tr>
...
<tr>FFCC00 FFCC33 FFCC66 FFCC99 FFCCCC FFCCFF</tr>
<tr>FFFF00 FFFF33 FFFF66 FFFF99 FFFFCC FFFFFF</tr>

Elementy te mają indeksy 5, 11 i 17. Do wstawienia znacznika </tr> wykorzystujemy


zatem warunek, w którym reszta z dzielenia indeksu drukowanego koloru przez liczbę
kolumn jest równa liczbie kolumn pomniejszonej o jeden:
{% if (numer % kolumny) == kolumny - 1 %}
</tr>
{% endif %}

Oczywiście na zakończenie pętli należy zwiększyć indeks generowanego koloru:


{% set numer = numer + 1 %}

Drukowanie komórek zawierających kolor przyjmuje postać:


<td class="kolor" style="background: #{{ kolorek}}"></td>
<td>{{ kolorek}}</td>

Zarys widoku index.html.twig jest przedstawiony na listingu 11.18.

Listing 11.18. Zarys widoku akcji index


<!DOCTYPE html>
<html>
<head>
<title>Bezpieczna paleta kolorów</title>
166 Część II ♦ Widoki

<meta charset="UTF-8" />


<style>
body {
font-family: "Courier New", monospace;
}
...
</style>
</head>
<body>
{% set kody = ['00', '33', '66', '99', 'CC', 'FF'] %}
{% set kolumny = 6 %}
{% set numer = 0 %}

<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 %}

<td class="kolor" style="background: #{{ kolorek}}"></td>


<td>{{ kolorek}}</td>

{% if (numer % kolumny) == kolumny - 1 %}


</tr>
{% endif %}

{% set numer = numer + 1 %}


{% endfor %}
{% endfor %}
{% endfor %}
</table>

</body>
</html>

Krok 4. Sprawdź działanie aplikacji


Po odwiedzeniu w przeglądarce adresu:
.../web/

ujrzysz witrynę z rysunku 11.5.


Rozdział 11. ♦ Instrukcje sterujące for oraz if 167

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 przyjmują postać:


{% znacznik %}

Filtry stosujemy w odniesieniu do zmiennych, np.:


{{ zmienna | nazwa_filtru }}

Funkcje wywołujemy natomiast w wyrażeniach:


{{ nazwa_funkcji() }}

Dokumentacja znaczników, filtrów oraz funkcji Twig jest dostępna na stronach:


 http://twig.sensiolabs.org/doc/tags/index.html
 http://twig.sensiolabs.org/doc/filters/index.html
 http://twig.sensiolabs.org/doc/functions/index.html

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

Tabela 12.1. Pełna lista znaczników Twig


Znacznik Opis Składnia
for Iteracyjne przetwarzanie tablic {% for ... %}
{% endfor %}
if Instrukcja warunkowa {% if ... %}
{% endif %}
macro Makrodefinicja {% macro ... %}
{% endmacro %}
from Dołączanie wybranych makrodefinicji z innego widoku wraz {% from ... %}
z ewentualnym nadpisaniem nazw importowanych makr
import Dołączanie wszystkich makrodefinicji z innego widoku {% import ... %}
filter Zastosowanie filtra do bloku kodu {% filter ... %}
{% endfilter %}
set Definicja zmiennej {% set ... %}
lub
{% set ... %}
...
{% endset %}
extends Wskazanie szablonu, który zostanie użyty jako szablon {% extends ... %}
bazowy widoku
block Definicja zawartości bloku {% block ... %}
lub
{% block ... %}
{% endblock %}
use Dołączenie innego widoku w celu nadpisania zawartości wybranych {% use ... %}
bloków (umożliwia dziedziczenie wielobazowe widoków)
include Dołączenie innego widoku i wydrukowanie jego zawartości {% include ... %}
(odpowiednik widoków częściowych)
spaceless Wyłączenie generowania białych znaków {% spaceless %}
{% endspaceless %}
autoescape Włączenie lub wyłączenie automatycznego zabezpieczania {% autoescape ... %}
zmiennych {% endautoescape %}
raw Wyłączenie interpretacji kodu Twig {% raw %}
{% endraw %}
flush Wyczyszczenie bufora wyjściowego {% flush %}
do Ewaluacja wyrażenia bez generowania wydruku wyliczonej wartości {% do ... %}
render Umieszcza w widoku wynik przetwarzania podanej akcji {% render ... %}

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

Znaczniki {% set %} oraz {% block %} mogą być zapisywane w zarówno skróconej:


{% set ... %}
{% block ... %}

jak i rozszerzonej postaci:


{% set ... %}
...
{% endset %}

{% block ... %}
...
{% endblock %}

Znaczniki for oraz if


Znaczniki {% for ... %} oraz {% if ... %} zostały szczegółowo omówione w roz-
dziale 11.

Znaczniki macro, from i import


Znacznik {% macro %} tworzy makrodefinicję, którą możemy w dalszej części widoku
wywoływać identycznie jak funkcje widoku. Instrukcja widoczna na listingu 12.1 defi-
niuje makrodefinicję o nazwie autolink, która przyjmuje jeden parametr.

Listing 12.1. Makrodefinicja autolink


{% macro autolink(url) %}
<a href="{{ url | default('http://example.net') }}">
{{ url | default('http://example.net') }}
</a>
{% endmacro %}

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>

Powyższa instrukcja wygeneruje kod:


<a href="http://google.com">
http://google.com
</a>

W przypadku gdy w wywołaniu pominiemy parametr:


<p>{{ _self.autolink() }}</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

Parametry makrodefinicji są zawsze opcjonalne (nawet wówczas, gdy w makrodefinicji


nie został użyty filtr default). Należy pamiętać także, że wewnątrz makrodefinicji nie
ma dostępu do zmiennych widoku. W celu przekazania do makrodefinicji zmiennych
widoku możemy wykorzystać zmienną globalną _context. Jeśli w widoku wystąpi
zmienna strona, wywołanie:
{{ str(_context) }}

wygeneruje wówczas znacznik div zawierający jej wartość. Makrodefinicja str jest
przedstawiona na listingu 12.2.

Listing 12.2. Makrodefinicja str


{% macro str(c) %}
{% if c.strona is defined %}
<div>{{ c.strona }}</div>
{% endif %}
{% endmacro %}

Jeśli makrodefinicje str oraz autolink zdefiniujesz w widoku m.html.twig, w celu


użycia należy najpierw je zaimportować. Instrukcja:
{% import 'm.html.twig' as mojemakra %}

importuje wszystkie makra zawarte w pliku m.html.twig i udostępnia je w postaci:


{{ mojemakra.autolink(...) }}
{{ mojemakra.str(...) }}

Znacznik {% import %} importuje wszystkie makrodefinicje zawarte w podanym pliku.


Jeśli chcesz zaimportować wybrane makrodefinicje, użyj znacznika from. Instrukcja:
{% from 'm.html.twig' import autolink %}

dołącza do bieżącego widoku wyłącznie makrodefinicję autolink. Wywołanie ma-


krodefinicji przyjmuje postać:
{{ autolink(...) }}

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 %}

dołącza z pliku makrodefinicję spr i nadaje jej nazwę s. Wywołanie makrodefinicji


przyjmie postać:
{{ 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

Filtry podane jako parametr możemy łączyć, stosując znak |:


{% filter capitalize|nl2br %}
lorem
ipsum
dolor
{% endfilter %}

Powyższa instrukcja wygeneruje kod:


Lorem<br/>
Ipsum<br/>
Dolor<br/>

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'] %}

Wykorzystując znacznik {% extends %}, zdefiniowaliśmy w rozdziale 4. szablon layout.


html.twig, który zawierał układ strony WWW.

Korzystając ze znacznika {% extends %}, należy pamiętać o następujących ograniczeniach:


 Znacznik {% extends %} musi wystąpić jako pierwszy w pliku.
 Widok zawierający znacznik {% extends %} musi definiować bloki
(nie może zawierać kodu zawartego poza blokami).
 Dziedziczenie szablonów definiowane przy użyciu {% extends %}
jest jednobazowe.

1
Por. http://twig.sensiolabs.org/doc/tags/extends.html.
174 Część II ♦ Widoki

Kod Symfony 2 zawiera przedstawiony na listingu 12.3 widok app/Resources/views/


base.html.twig. Widok ten możemy wykorzystać do tworzenia układu strony WWW.

Listing 12.3. Domyślny szablon app/Resources/views/base.html.twig


<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>{% block title %}Welcome!{% endblock %}</title>
{% block stylesheets %}{% endblock %}
<link rel="shortcut icon" href="{{ asset('favicon.ico') }}" />
</head>
<body>
{% block body %}{% endblock %}
{% block javascripts %}{% endblock %}
</body>
</html>

Nazwą logiczną widoku z listingu 12.3 jest:


::base.html.twig

W celu użycia go do dekoracji widoku index.html.twig należy zatem w szablonie index.


html.twig umieścić (na samym początku) instrukcję:
{% extends '::base.html.twig' %}

Następnie w widoku index.html.twig umieszczamy znaczniki {% block %} definiujące


zawartość poszczególnych bloków. Przykładowa zawartość pliku index.html.twig jest
przedstawiona na listingu 12.4.

Listing 12.4. Przykładowa zawartość widoku index.html.twig dla pliku base.html.twig z listingu 12.3
{% extends '::base.html.twig' %}

{% block title %}Witaj!{% endblock %}

{% 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 %}

która może zawierać:


 cyfry,
 litery,
 znaki podkreślenia _.

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>' %}

W każdym widoku możemy umieścić dowolną liczbę bloków, np.:


{% block a %}
...
{% endblock %}

{% block b %}
...
{% endblock %}

{% block c %}
...
{% endblock %}

Znaczniki extends i block oraz dziedziczenie


Przyjmijmy, że w projekcie występują dwa szablony:
 nadrzędny o nazwie base.html.twig,
 dziedziczący o nazwie index.html.twig.
176 Część II ♦ Widoki

W szablonie nadrzędnym base.html.twig umieśćmy blok o nazwie lorem:


//fragment zmodyfikowanego pliku app/Resources/views/base.html.twig
<body>
{% block body %}{% endblock %}
{% block javascripts %}{% endblock %}
{% block lorem %}
<div id="lorem">Lorem</div>
{% endblock %}
</body>

Widok index.html.twig dziedziczy po szablonie base.html.twig, zatem zawiera znacznik


{% extends %} oraz znaczniki {% block %}, np.:
{% extends '::base.html.twig' %}

{% block body %}
<h1>Powitanie</h1>
{% endblock %}

Jeśli w szablonie index.html.twig nie wystąpi blok o nazwie lorem, w wygenerowanym


dokumencie pojawi się wówczas domyślna zawartość bloku lorem zdefiniowana w pliku
base.html.twig, czyli:
<div id="lorem">Lorem</div>

Jeśli w szablonie index.html.twig umieścimy blok:


{% block lorem %}
<div id="ipsum">Ipsum</div>
{% endblock %}

to wygenerowany dokument będzie zawierał kod:


<div id="ipsum">Ipsum</div>

zaś nie wystąpi w nim kod:


<div id="lorem">Lorem</div>

Jeśli natomiast w szablonie index.html.twig umieścimy blok:


{% block lorem %}
{{ parent() }}
<div id="ipsum">Ipsum</div>
{% endblock %}

to wygenerowany dokument będzie zawierał kod pochodzący z bloku lorem z pliku


base.html.twig oraz z bloku lorem z pliku index.html.twig, czyli:
<div id="lorem">Lorem</div>
<div id="ipsum">Ipsum</div>

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.

Działanie znacznika {% block %} w połączeniu ze znacznikiem {% extends %} podsu-


mowuje tabela 12.2.

Tabela 12.2. Nadpisywanie zawartości bloków w widoku dziedziczącym


Widok bazowy Kod generowany po przetworzeniu
Widok dziedziczący index.html.twig
base.html.twig widoku index.html.twig
<body> {% extends '::base.html.twig' %} <body>
{% block a %} {% block a %} b
a b </body>
{% endblock %} {% endblock %}
</body>
<body> {% extends '::base.html.twig' %} <body>
{% block a %} a
a </body>
{% endblock %}
</body>
<body> {% extends '::base.html.twig' %} <body>
{% block a %} {% block a %} a
a {{ parent() }} b
{% endblock %} b </body>
</body> {% endblock %}
<body> {% extends '::base.html.twig' %} <body>
{% block a %} {% block c %} a
a c </body>
{% endblock %} {% endblock %}
</body>
178 Część II ♦ Widoki

Należy pamiętać, że dziedziczenie definiowane przy użyciu znacznika {% extends %} jest


jednobazowe. Jeśli w aplikacji wystąpią trzy widoki: base.html.twig, layout.html.twig
oraz index.html.twig, wówczas wykorzystując znacznik {% extends %}, możemy zdefi-
niować następującą hierarchię:
base.html.twig
|
|-- layout.html.twig
|
|-- index.html.twig

W widoku layout.html.twig umieszczamy znacznik:


{% extends '::base.html.twig' %}

a w widoku index.html.twig znacznik:


{% extends '::layout.html.twig' %}

W ten sposób widok index.html.twig dziedziczy po widoku layout.html.twig, a widok


layout.html.twig dziedziczy po widoku base.html.twig.

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.

Przyjmijmy, że w aplikacji występują widoki base.html.twig, index.html.twig oraz


bloki.html.twig oraz że widok index.html.twig dziedziczy po widoku base.html.twig.
Oto przykładowa zawartość widoku base.html.twig:
//base.html.twig
<body>
{% block body %}
lorem
{% endblock %}
</body>

widoku bloki.html.twig:
//bloki.html.twig
{% block body %}
ipsum
{% endblock %}

oraz widoku index.html.twig:


//index.html.twig
{% extends '::base.html.twig' %}
{% use '::bloki.html.twig' %}
Rozdział 12. ♦ Znaczniki, filtry i funkcje 179

Widok index.html.twig dziedziczy po widoku base.html.twig i zawiera bloki z widoku


bloki.html.twig. Dokument generowany po przetworzeniu widoku index.html.twig będzie
zawierał kod:
<body>
ipsum
</body>

Blok body zdefiniowany w widoku base.html.twig zostanie wypełniony treścią z bloku


body z pliku bloki.html.twig.

Znacznik include
Znacznik {% include %} dołącza we wskazanym miejscu zewnętrzny widok:
{% include '::reklama.html.twig' %}

Parametrem znacznika jest nazwa logiczna widoku.

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'] %}

Domyślnie zmienne dostępne w widoku, który zawiera instrukcję {% include %}, są


także dostępne wewnątrz dołączanego widoku. Możemy zablokować dostęp do zmien-
nych w dołączanym widoku, stosując parametr only:
{% include '::reklama.html.twig' only %}

Powyższa instrukcja spowoduje dołączenie widoku reklama.html.twig. Podczas przetwa-


rzania widoku reklama.html.twig nie będą w nim dostępne żadne zmienne z widoku
dołączającego.

Wartości zmiennych w dołączanym widoku możemy ustalić, wykorzystując parametr


with:
{% include '::reklama.html.twig' with {'tekst': 'lorem ipsum'} %}

Powyższa instrukcja spowoduje dołączenie widoku reklama.html.twig. Wewnątrz wi-


doku reklama.html.twig wartością zmiennej o nazwie tekst będzie lorem ipsum.

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

zostały szczegółowo omówione w rozdziale 10.

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 }}

wyznacza wartość wyrażenia 2 · 3 · 4, po czym otrzymaną wartość (24) drukuje. Odpo-


wiada ona instrukcji:
echo 2 * 3 * 4;

Instrukcja:
{{ do 2 * 3 * 4 }}

wyznacza wartość wyrażenia, lecz nie drukuje otrzymanej wartości 24.


Rozdział 12. ♦ Znaczniki, filtry i funkcje 181

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' %}

umieści w widoku wynik przetwarzania akcji dolorAction(). Akcja dolor powinna


posiadać własny widok dolor.html.twig, który zostanie użyty do wykonania instrukcji
{% render %}. Jeśli metoda dolorAction() przyjmuje parametry, to możemy ją przekazać
z widoku do akcji, stosując składnię:
{% render 'MyLoremBundle:Ipsum:dolor' with {'sort': 'ASC', 'ile': 15} %}

Znacznik {% render %} wykorzystamy do generowania menu, którego pozycje po-


chodzą z bazy danych.

W Symfony 1.4. odpowiednikiem znacznika {% render %} były komponenty.

Filtry
Tabela 12.3 zawiera wszystkie filtry domyślnie dostępne w Twig.

Tabela 12.3. Filtry


Filtr Opis Przykład
capitalize Zmienia pierwszą literę na dużą. {{ 'lorem ipsum' | capitalize }}
W przypadku użycia kodowania UTF- wydrukuje:
8 poprawnie operuje na literach
z polskimi znakami diakrytycznymi. Lorem ipsum

convert_encoding Zmienia kodowanie znaków. {{ 'żółw' | convert_encoding('UTF-8',


´'iso-8859-2') }}
date Formatuje datę do zadanej postaci. {{ published_at|date("m/d/Y") }}
Akceptuje identyczne parametry jak
funkcja date() oraz klasa DateTime.
default Ustala domyślną wartość, która będzie {{ tytul | default("Lorem") }}
zwracana w przypadku, gdy zmienna
nie jest zdefiniowana lub ma wartość
pustą.
182 Część II ♦ Widoki

Tabela 12.3. Filtry — ciąg dalszy


Filtr Opis Przykład
escape Konwertuje niedozwolone znaki do {{ zm | escape }}
bezpiecznej postaci. W przypadku {{ zm | escape('js') }}
generowania formatu HTML działanie
filtru escape jest identyczne jak funkcji
htmlspecialchars().
format Formatuje napis, stosując {{ "Cena: %s" | format("123") }}
konwencje funkcji printf().
join Odpowiednik funkcji implode(): {{ [1, 2, 3] | join }}
łączy elementy tablicy w łańcuch. wydrukuje:
Opcjonalny parametr jest separatorem
poszczególnych elementów. 123

{{ [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

Tabela 12.3. Filtry — ciąg dalszy


Filtr Opis Przykład
replace Wymienia ciągi znaków. {{ 'lorem' | replace({'l': 'XX'}) }}
Parametrem jest tablica asocjacyjna. wydrukuje:
XXorem
reverse Odwraca kolejność elementów tablicy. —
sort Sortuje elementy tablicy. Działa na —
bazie funkcji PHP asort().
striptags Przekształca podany ciąg znaków {{ "<em>a</em>" | striptags }}
funkcją PHP strip_tags(), która wydrukuje:
usuwa znaczniki HTML.
a
title Przekształca pierwszą literę każdego {{ 'and then there were none' | title }}
wyrazu na dużą. Działanie takie jak wydrukuje:
funkcji PHP ucwords().
And Then There Were None
upper Konwertuje wszystkie litery na {{ 'ąćęłńóśźż' | upper }}
duże. Uwzględnia polskie znaki wydrukuje:
diakrytyczne.
ĄĆĘŁŃÓŚŹŻ
url_encode Przekształca podany ciąg znaków {{ 'a / b' | url_encode }}
funkcją PHP urlencode(). wydrukuje:
a+%2F+b
truncate Skraca długi ciąg znaków, {{ 'lorem ipsum dolor sit
pozostawiając początkowe litery. ´amet'|truncate(18, true) }}
Pierwszy parametr ustala maksymalną wydrukuje:
długość napisu (np. 18 liter), drugi
decyduje o tym, że napis zostanie lorem ipsum dolor sit...
skrócony pomiędzy wyrazami.
wordwrap Przekształca łańcuch znaków, {{ 'lorem ipsum dolor sit
wstawiając podany ciąg (np. <br/>) ´amet'|wordwrap(10, "<br/>") }}
co zadaną liczbę znaków (np. 10). wydrukuje:
lorem ipsu<br />m dolor si<br />t amet

Filtry wymienione w tabeli 12.3 możemy łączyć, np.:


{{ zmienna | upper | nl2br }}
{{ zmienna | wordwrap(10, "<br />") | raw }}

W celu włączenia filtrów nl2br, truncate i wordwrap należy w pliku konfiguracyjnym


app/config/config.yml dodać instrukcje:
services:
twig.extension.txt:
class: Twig_Extensions_Extension_Text
tags:
- { name: twig.extension }
Implementacja filtrów nl2br, truncate i wordwrap jest zawarta w pliku Symfony\
vendor\twig-extensions\lib\Twig\Extensions\Extension\Text.php.
184 Część II ♦ Widoki

Funkcje
Wszystkie funkcje Twig są zawarte w tabeli 12.4.

Tabela 12.4. Zestawienie wszystkich funkcji Twig


Funkcja Opis Przykład
attribute() Funkcja niedostępna —
w Symfony 2.
block() Zwraca zawartość bloku {{ block('title') }}
o podanej nazwie.
constant() Zwraca wartość stałej {{ constant('Namespace\\Classname::CONSTANT_NAME') }}
podanej jako parametr.
cycle() Przetwarza cyklicznie tablicę. {% for i in 1..7 %}
Ułatwia zadania takie jak {{ cycle(['raz', 'dwa', 'trzy'], i) }}
kolorowanie wierszy.
{% endfor %}
wydrukuje:
raz, dwa, trzy, raz, dwa, trzy, raz
dump() Funkcja niedostępna -
w Symfony 2.
parent() Zwraca zawartość bloku {{ parent() }}
z szablonu nadrzędnego.
random() Funkcja niedostępna -
w Symfony 2.
range() Odpowiednik operatora .. {{ range(2, 5)|join }}
wydrukuje:
2345

Oprócz standardowych funkcji Twig zawartych w tabeli 12.4 Symfony 2 udostępnia


funkcje zawarte w tabeli 12.5.

Działanie funkcji asset() zależy od ustalenia opcji konfiguracyjnej templating zawartej


w pliku app/config/config.yml:
framework:
templating: { engines: ['twig'] }

Jeśli w tablicy templating dołączysz wartość assets_version:


framework:
templating: { engines: ['twig'], assets_version: v1 }

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

Tabela 12.5. Funkcje Twig udostępniane przez Symfony 2


Funkcja Opis Przykład
asset() Generuje adres URL do statycznych {{ asset('css/style.css') }}
zasobów (np. CSS, JavaScript wydrukuje:
czy plików graficznych) zawartych
w folderze [projekt]/web/. /pelna/sciezka/do/projektu/css/style.css

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

Dzięki temu po zmianie numeru wersji w pliku config.yml na kolejną:


templating: { engines: ['twig'], assets_version: v2 }

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.

Przykład 12.1. Piosenki dziecięce


Dana jest pewna liczba plików tekstowych zawierających teksty piosenek. Każdy plik ma
identyczną strukturę: zawiera w pierwszym wierszu tytuł piosenki, a w kolejnych wier-
szach — tekst. Listing 12.5 prezentuje zawartość przykładowego pliku jada-jada-misie.txt.

Listing 12.5. Plik jada-jada-misie.txt


Jadą, jadą misie

Jadą, jadą misie,


Tra la, tra la la,
Śmieją im się pysie,
Ha ha, ha ha ha!
...
186 Część II ♦ Widoki

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

wówczas użytymi adresami mają być:


/web/lorem-ipsum.html
/web/dolor-sit-amet.html

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

utwórz pakiet My/SongBundle.

Następnie utwórz folder [projekt]/data/ i umieść w nim pliki tekstowe z folderu


12-start/data/.

Krok 2. Dostosuj kontroler DefaultController


W kontrolerze DefaultController.php usuń metodę indexAction() oraz dodaj przed-
stawioną na listingu 12.6 metodę showAction().

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

Listing 12.6. Metoda showAction()

/**
* @Route("/{slug}.{_format}", name="_show_")
* @Template()
*/
public function showAction($slug, $_format)
{

$slugs = array();
$data = array();

foreach (glob('../data/*.txt') as $fn) {


$tmp = file($fn);
$t = trim($tmp[0]);
$s = str_replace('.txt', '', basename($fn));

$data[] = array(
'title' => $t,
'slug' => $s
);

$slugs[] = $s;
}

if (!in_array($slug, $slugs)) {
throw $this->createNotFoundException('Podana strona nie istnieje!');
}

$p = file('../data/' . $slug . '.txt');


$title = trim($p[0]);
$p[0] = '';
$contents = trim(implode('', $p));
return compact('title', 'contents', 'slug', 'data');
}

Aplikacja będzie stosowała adres postaci:


/nazwa-pliku-tekstowego.rozszerzenie

Adnotacja konfigurująca adresy URL przyjmie postać:


@Route("/{slug}.{_format}", name="_show_")

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

parametry przyjmują wartość:


slug = jada-jada-misie
_format = txt

a w adresie:
kolko-graniaste.html
188 Część II ♦ Widoki

parametry będą następujące:


slug = kolko-graniaste
_format = html

Oba parametry przekazujemy do funkcji showAction():


public function showAction($slug, $_format)
{
...
}

Pierwszym etapem przetwarzania w funkcji showAction() jest ustalenie danych potrzeb-


nych do walidacji zmiennych URL oraz do wygenerowania menu. Do walidacji wy-
korzystamy tablicę o nazwie $slugs, a do generowania menu — tablicę o nazwie $data.
W tablicy $slugs umieścimy nazwy plików tekstowych pozbawione rozszerzenia. Tablica
$data będzie natomiast zawierała zarówno tytuły wierszy, jak i nazwy plików teksto-
wych. Jeśli w folderze [projekt]/data/ znajdują się pliki:
lorem-ipsum.txt
dolor-sit-amet.txt

wówczas w tablicy $slugs należy umieścić dwa ciągi:


$slugs = array('lorem-ipsum', 'dolor-sit-amet');

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 tym celu najpierw tworzymy puste tablice $slugs oraz $data:


$slugs = array();
$data = array();

Następnie w folderze [projekt]/data/ wyszukujemy wszystkie pliki tekstowe. Odnalezione


nazwy plików przetwarzamy w pętli:
foreach (glob('../data/*.txt') as $fn) {
...
}

W kolejnych obrotach pętli odczytujemy plik z piosenką:


$tmp = file($fn);

Na podstawie pierwszego wiersza pliku ustalamy w zmiennej $t tytuł piosenki:


$t = trim($tmp[0]);

Następnie na podstawie nazwy pliku zawartej w zmiennej $np tworzymy zmienną $s


zawierającą nazwę pliku pozbawioną rozszerzenia:
$s = str_replace('.txt', '', basename($fn));
Rozdział 12. ♦ Znaczniki, filtry i funkcje 189

Na zakończenie na podstawie zmiennych $t oraz $s tworzymy nową tablicę asocjacyjną


o indeksach title oraz slug. Utworzona tablica jest dołączana na końcu tablicy $data:
$data[] = array(
'title' => $t,
'slug' => $s
);

a na końcu tablicy $slugs dołączamy zmienną $s:


$slugs[] = $s;

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');

Na podstawie pierwszego wiersza ustalamy tytuł piosenki:


$title = trim($p[0]);

Następnie z tablicy $p usuwamy pierwszy wiersz:


$p[0] = '';

po czym tablicę $p przekształcamy w ciąg znaków $contents:


$contents = trim(implode('', $p));

Dzięki temu w zmiennej $contents wystąpi kompletny tekst piosenki pozbawiony tytułu.

Na zakończenie zmienne $title, $contents, $slug oraz $data przekazujemy do widoku:


return compact('title', 'contents', 'slug', 'data');

Funkcja compact() tworzy tablicę asocjacyjną zawierającą podane zmienne. Wyni-


kiem działania instrukcji:
compact('title', 'contents', 'slug', 'data');
jest tablica:
array('title' => $title, 'contents' => $contents, 'slug' => $slug, 'data' => $data);

Obie poniższe instrukcje są zatem równoważne:


return compact('title', 'contents', 'slug', 'data');
return array('title' => $title, 'contents' => $contents, 'slug' => $slug,
´'data' => $data);
190 Część II ♦ Widoki

Krok 3. Wykonaj widok base.html.twig


W pliku app/Resources/views/base.html.twig wprowadź kod z listingu 12.7.

Listing 12.7. Widok base.html.twig


<!DOCTYPE html>
<html>
<head>
<title>Piosenki / {{ title }}</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="{{ asset('css/style.css') }}" />
<link rel="stylesheet" href="{{ asset('css/print.css') }}" media="print" />
<script src="{{ asset('js/jquery-1.4.min.js') }}"></script>
<script src="{{ asset('js/animacja.js') }}"></script>
</head>
<body>

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

Tekst piosenki umieścimy w bloku o nazwie body:


<div id="tresc">
{% block body %}{% endblock %}
</div>

Menu witryny generujemy natomiast na podstawie zmiennej data jako listę:


<ul id="menuglowne">
...
</ul>

Zmienną data przetwarzamy w pętli:


{% for song in data %}
...
{% endfor %}
Rozdział 12. ♦ Znaczniki, filtry i funkcje 191

W każdym obrocie pętli drukujemy jedno hiperłącze:


<li>
<a href="{{ path('_show_', {'slug': song.slug, '_format': 'html'}) }}">
{{ song.title }}
</a>
</li>

Do funkcji path() przekazujemy nazwę reguły routingu _show_ oraz tablicę asocjacyjną
zawierającą klucze slug oraz _format.

Zwróć uwagę, że na listingu 12.7 występuje zmienna title przekazana do widoku


z akcji. Zmienne przekazane z akcji do widoku są dostępne w widoku akcji oraz we
wszystkich widokach bazowych.

Krok 4. Wykonaj widok show.html.twig


W pliku My/SongBundle/Resources/views/Default/show.html.twig wprowadź kod z li-
stingu 12.8.

Listing 12.8. Widok show.html.twig


{% extends '::base.html.twig' %}

{% block body %}
<h3>{{ title }}</h3>

<h4><a href="{{ path('_show_', {'slug': slug, '_format': 'txt'}) }}">Pobierz


´w formacie TXT</a></h4>

<p>
{{ contents|nl2br }}
</p>

{% endblock %}

Widokiem bazowym dla widoku show.html.twig jest base.html.twig:


{% extends '::base.html.twig' %}

W widoku show.html.twig nadpisujemy blok body:


{% block body %}
...
{% endblock %}

W treści bloku umieszczamy tytuł piosenki:


<h3>{{ title }}</h3>

hiperłącze do wersji tekstowej:


<h4><a href="{{ path('_show_', {'slug': slug, '_format': 'txt'}) }}">Pobierz w
´formacie TXT</a></h4>
192 Część II ♦ Widoki

oraz tekst piosenki:


<p>
{{ contents|nl2br }}
</p>

Krok 5. Wykonaj widok show.txt.twig


W pliku My/SongBundle/Resources/views/Default/show.txt.twig wprowadź kod z li-
stingu 12.9.

Listing 12.9. Widok show.txt.twig


{{ title }}

{{ contents }}

Krok 6. Skopiuj plik CSS, JavaScript oraz obrazy


Foldery:
12-start/templates/css/
12-start/templates/images/
12-start/templates/js/

skopiuj do folderu:
[project]/web/

Krok 7. Zmodyfikuj konfigurację projektu


W pliku app/config/config.yml dodaj regułę włączającą dostępność filtra nl2br:
services:
twig.extension.txt:
class: Twig_Extensions_Extension_Text
tags:
- { name: twig.extension }

W pliku app/config/routing.yml dodaj regułę ustalającą stronę główną aplikacji:


_homepage:
pattern: /
defaults: { _controller: MySongBundle:Default:show, slug: jada-jada-misie,
´_format: html }

Krok 8. Sprawdź działanie aplikacji


Odwiedź w przeglądarce adres:
.../web/

Powinieneś ujrzeć witrynę przedstawioną na rysunku 12.1.


Rozdział 12. ♦ Znaczniki, filtry i funkcje 193

Rysunek 12.1. Witryna z przykładu 12.1


194 Część II ♦ Widoki
Rozdział 13.
Trójstopniowy
podział widoków
Wykonując dotychczasowe ćwiczenia, stosowaliśmy kilka różnych rozwiązań podziału
aplikacji na widoki.

W przykładach 10.1 oraz 10.2 wystąpił jeden plik widoku:


[projekt]/src/My/[pakiet]/Resources/views/Default/index.html.twig

który zawierał cały generowany kod HTML. Przykłady te w ogóle nie wykorzystywały
pliku app/Resources/views/base.html.twig.

Omówiony w pierwszej części podręcznika przykład 6.1 stosował widoki akcji:


[projekt]/src/My/AnimalsBundle/Resources/views/Default/jaszczurka.html.twig
[projekt]/src/My/AnimalsBundle/Resources/views/Default/zaskroniec.html.twig
[projekt]/src/My/AnimalsBundle/Resources/views/Default/zmija.html.twig
[projekt]/src/My/AnimalsBundle/Resources/views/Default/zolw.html.twig

oraz szablon:
[projekt]/src/My/AnimalsBundle/Resources/views/layout.html.twig

Plik app/Resources/views/base.html.twig również nie był wykorzystywany.

Przykład omówiony w rozdziale 12. wykorzystywał widok akcji:


[projekt]/src/My/SongBundle/Resources/views/Default/show.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.

Listing 13.1. Widok base.html.twig


<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>{% block title %}Welcome!{% endblock %}</title>
{% block stylesheets %}{% endblock %}
<link rel="shortcut icon" href="{{ asset('favicon.ico') }}" />
</head>
<body>
{% block body %}{% endblock %}
{% block javascripts %}{% endblock %}
</body>
</html>

Drugim widokiem będzie plik:


[projekt]/app/Resources/views/layout.html.twig

Widok layout.html.twig będzie dziedziczył po widoku base.html.twig:


{% extends '::base.html.twig' %}

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.

Listing 13.2. Przykładowy widok layout.html.twig


{% extends '::base.html.twig' %}

{% 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>
&copy;2012 by gajdaw
</footer>
{% endblock %}
Rozdział 13. ♦ Trójstopniowy podział widoków 197

Zwróć uwagę na listingu 13.2, że znaczniki {% block %} można zagnieżdżać!

Ostatnim widokiem będzie widok akcji, np.:


[projekt]/src/My/[pakiet]/Resources/views/Default/index.html.twig

Widok ten będzie dziedziczył po widoku layout.html.twig, zatem rozpocznie się od


znacznika:
{% extends '::layout.html.twig' %}

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.

Listing 13.3. Przykładowy widok index.html.twig


{% extends '::layout.html.twig' %}

{% 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

Listing 13.4. Fragment pliku 00index.log


A PREDICAMENT|index
BERENICE|berenice
LOSS OF BREATH|loss-of-breath
...

Z zawartości pliku 00index.log przedstawionej na listingu 13.4 wynika, że dysponujemy


następującymi plikami tekstowymi:
index.txt
berenice.txt
loss-of-breath.txt
...

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.

Zadanie rozwiąż w taki sposób, by podział widoków był trójstopniowy:


base.html.twig
layout.html.twig
show.html.twig

Do umieszczenia kodu HTML menu wykorzystaj znacznik {% render %}, dodatkową


akcję menuAction() oraz widok menu.html.twig. Do identyfikacji utworów użyj ciągów
zawartych w pliku 00index.log. Na przykład utwór pt. „Loss of Breath” powinien być
dostępny pod adresem:
loss-of-breath.html

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

utwórz pakiet My/NovelBundle. Wszystkie opcje pakietu pozostaw domyślne.

Następnie utwórz folder [projekt]/data/ i umieść w nim dane z folderu 13-start/data/.


Rozdział 13. ♦ Trójstopniowy podział widoków 199

Krok 2. Dostosuj kontroler DefaultController


W kontrolerze DefaultController.php usuń metodę indexAction() oraz dodaj przed-
stawione na listingu 13.5 metody showAction() oraz menuAction().

Listing 13.5. Kontroler DefaultController.php


class DefaultController extends Controller
{
/**
* @Route("/{slug}.html", name="show")
* @Template()
*/
public function showAction($slug)
{
$slugs = array();
$indexFile = file('../data/00index.log');
foreach ($indexFile as $tmp) {
$e = explode('|', trim($tmp));
$slugs[] = $e[1];
}
if (!in_array($slug, $slugs)) {
throw $this->createNotFoundException('Podana strona nie istnieje!');
}
$contents = file('../data/' . $slug . '.txt');
$title = array_shift($contents);
$novel = array(
'title' => trim($title),
'contents' => $contents
);
return compact('novel');
}
/**
* @Template()
*/
public function menuAction()
{
$menuData = array();
$indexFile = file('../data/00index.log');
foreach ($indexFile as $tmp) {
$e = explode('|', trim($tmp));
$menuData[] = array(
'title' => $e[0],
'slug' => $e[1]
);
}
return compact('menuData');
}

Adnotacja konfiguracyjna metody showAction():


@Route("/{slug}.html", name="show")
200 Część II ♦ Widoki

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.

Na podstawie tablicy $slugs badamy poprawność zmiennej $slug:


if (!in_array($slug, $slugs)) {
throw $this->createNotFoundException('Podana strona nie istnieje!');
}

Jeśli wartość zmiennej $slug jest poprawna, odczytujemy plik z treścią odpowiednie-
go utworu:
$contents = file('../data/' . $slug . '.txt');

Pierwszy element tablicy $contents usuwamy z tablicy i umieszczamy w zmiennej $title:


$title = array_shift($contents);

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
);

Na zakończenie zmienną novel przekazujemy do widoku akcji:


return compact('novel');

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');

iteracyjnie przetwarzamy kolejne wiersze pliku:


foreach ($indexFile as $tmp) {
...
}

Każdy wiersz kroimy znakiem |:


$e = explode('|', trim($tmp));

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]
);

Tablica $menuData jest na końcu przekazywana do widoku:


return compact('menuData');
Rozdział 13. ♦ Trójstopniowy podział widoków 201

Zwróć uwagę, że metoda menuAction() nie zawiera adnotacji:


@Route("...")
Metody tej nie da się zatem wywołać bezpośrednio z przeglądarki, podając odpo-
wiedni adres URL.

Krok 3. Wykonaj widok menu.html.twig


W pliku My/NovelBundle/Resources/views/Default/menu.html.twig wprowadź kod
z listingu 13.6.

Listing 13.6. Widok menu.html.twig


<nav>
<ul>
{% for item in menuData %}
<li><a href="{{ path('show', {'slug': item.slug}) }}">{{ item.title
´}}</a></li>
{% endfor %}
</ul>
</nav>

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()
...

Krok 4. Wykonaj widok base.html.twig


W pliku app/Resources/views/base.html.twig wprowadź kod z listingu 13.7.

Listing 13.7. Widok base.html.twig


<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>{% block title %}Edgar Allan Poe{% endblock %}</title>
<link rel="stylesheet" href="{{ asset('css/style.css') }}" />
202 Część II ♦ Widoki

</head>
<body>
{% block body %}{% endblock %}
</body>
</html>

Krok 5. Wykonaj widok layout.html.twig


W pliku app/Resources/views/layout.html.twig wprowadź kod z listingu 13.8.

Listing 13.8. Widok layout.html.twig


{% extends '::base.html.twig' %}

{% block body %}
<section>
{% render 'MyNovelBundle:Default:menu' %}
<article>
{% block contents %}
{% endblock %}
</article>
</section>

{% endblock %}

Widokiem bazowym widoku layout.html.twig jest base.html.twig:


{% extends '::base.html.twig' %}

Wewnątrz bloku body umieszczamy kod menu:


{% render 'MyNovelBundle:Default:menu' %}

oraz blok contents przeznaczony na treść opowiadania:


{% block contents %}
{% endblock %}

Krok 6. Wykonaj widok show.html.twig


W pliku My/NovelBundle/Resources/views/Default/show.html.twig wprowadź kod
z listingu 13.9.

Listing 13.9. Widok show.html.twig


{% extends '::layout.html.twig' %}

{% block title %}
{{ novel.title}} / {{ parent() }}
{% endblock %}

{% block contents %}

<h1>{{ novel.title}}</h1>

{% for paragraph in novel.contents %}


Rozdział 13. ♦ Trójstopniowy podział widoków 203

<p>
{{ paragraph }}
</p>
{% endfor %}

{% endblock %}

Do widoku show.html.twig przekazana została jedna zmienna o nazwie novel. Na pod-


stawie tej zmiennej wypełniamy bloki title oraz contents. Składowa novel.contents
jest tablicą, której każdy element jest jednym akapitem utworu. Elementy tablicy prze-
twarzamy zatem w pętli, otaczając je znacznikami <p></p>:
{% for paragraph in novel.contents %}
<p>
{{ paragraph }}
</p>
{% endfor %}

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.

Krok 7. Skopiuj plik CSS


Folder:
13-start/templates/css/

skopiuj do folderu:
[project]/web/

Krok 8. Zmodyfikuj konfigurację projektu


W pliku app/config/routing.yml dodaj regułę ustalającą stronę główną aplikacji:
_homepage:
pattern: /
defaults: { _controller: MyNovelBundle:Default:show, slug: index }

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 }

oraz dodając w metodzie akcji showAction() domyślną wartość parametru slug:


public function showAction($slug = 'index')

Krok 9. Sprawdź działanie aplikacji


Odwiedź w przeglądarce adres:
.../web/

Powinieneś ujrzeć witrynę przedstawioną na rysunku 13.1.

Rysunek 13.1. Witryna z przykładu 13.1


Rozdział 14.
Podsumowanie części II
Przykłady opisane w drugiej części podręcznika szczegółowo zapoznały nas z szablonami
Twig. W szczególności w rozdziale 9. poznaliśmy:
 składnię logicznych nazw widoku,
 sposoby powiązania akcji z plikiem widoku,
 wpływ adnotacji @Template na sposób przekazywania danych do widoku,
 technikę nadpisywania widoków pochodzących z folderu vendor/,
 składnię znaczników i wyrażeń Twig.

W rozdziale 10. szczegółowo przeanalizowaliśmy zagadnienia dotyczące przekazywania


zmiennych z akcji do widoku:
 strukturę tablicy zwracanej jako wynik akcji,
 automatyczne zabezpieczanie zmiennych przed atakami Cross Site Scripting,
 sposoby włączania i wyłączania zabezpieczania zmiennych,
 przekazywanie do widoku tablic, tablic asocjacyjnych oraz obiektów,
 wyrażenia i operatory Twig,
 znacznik {% set %} służący do definiowania zmiennych w widokach,
 zmienne globalne Twig.

Rozdział 11. zapoznał nas szczegółowo z dwoma najważniejszymi znacznikami


Twig: pętlą {% for %} oraz instrukcją {% if %}. W rozdziale 12. omówiliśmy wszystkie
znaczniki, filtry i funkcje Twig. Pamiętaj, że niektóre filtry, np. nl2br, są domyślnie
wyłączone.

Rozdział 13. zapoznał nas z trójstopniowym podziałem na widoki base.html.twig,


layout.html.twig oraz index.html.twig.
206 Część II ♦ Widoki

Zapamiętaj, że do wykonania menu, które powstaje na podstawie danych (np. zawartości


pliku tekstowego lub bazy danych), należy użyć znacznika {% render %}, który umiesz-
cza w widoku wynik przetwarzania podanej akcji. Użycie znacznika {% render %} zostało
pokazane w przykładzie 13.1.

Zwróć także uwagę na sposób wygenerowania przekierowania do strony błędu 404.


Jeśli w kontrolerze stwierdzisz, że wartości zmiennych są niepoprawne, wywołaj
metodę createNotFoundException():
throw $this->createNotFoundException('Podana strona nie istnieje!');

Symfony 2 jest w całości zaimplementowane obiektowo. Widoki Twig są więc obiektami.


Plik konfiguracyjny config.yml zawiera opcję:
framework:
templating: { engines: ['twig'] }

Dostęp do obiektu Twig uzyskamy w akcji instrukcją:


$engine = $this->container->get('templating');

Przetwarzanie widoku:
return $this->render('Logiczna:Nazwa:widoku.html.twig');

możemy zaimplementować równoważnie w postaci:


$engine = $this->container->get('templating');
$content = $engine->render('Logiczna:Nazwa:widoku.html.twig');
return $response = new Response($content);

Symfony 2 umożliwia stosowanie szablonów PHP zamiast szablonów Twig. Szczegóły


są opisane w dokumentacji na stronie:
http://symfony.com/doc/current/cookbook/templating/PHP.html
W książce będziemy stosowali wyłącznie szablony Twig.
Część III
Dostosowywanie
Symfony 2
208 Część III ♦ Dostosowywanie Symfony 2
Rozdział 15. ♦ Dodawanie nowych pakietów 209

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.

Pakiet DoctrineFixturesBundle jest dostępny pod adresem:


https://github.com/symfony/DoctrineFixturesBundle

W celu zainstalowania pakietu DoctrineFixturesBundle musimy także zainstalować


bibliotekę dostępną pod adresem:
https://github.com/doctrine/data-fixtures

Lista pakietów zawartych w Symfony


Pakiety zawarte w dystrybucji Symfony 2 są wymienione w plikach [projekt]/deps
oraz [projekt]/deps.lock.

Plik [projekt]/deps ma składnię plików INI. Jego początkowy fragment jest przedstawio-
ny na listingu 15.1.

Listing 15.1. Początkowy fragment pliku deps zawartego w Symfony_Standard_Vendors_2.0.10.zip


[symfony]
git=http://github.com/symfony/symfony.git
version=v2.0.10
210 Część III ♦ Dostosowywanie Symfony 2

[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);

Plik [projekt]/deps.lock jest zwykłym plikiem tekstowym, który w każdym wierszu


zawiera nazwę pakietu oraz oznaczenie wersji, np.:
symfony v2.0.10
twig v1.6.0
...
twig-extensions 1dfff8e793f50f651c4f74f796c2c68a4aee3147
...

Zawartość folderu vendor/


Wiemy już, że Symfony 2 jest dostępne w postaci dwóch dystrybucji różniących się
zawartością folderu [projekt]/vendor/. Jeśli chcemy przygotować prosty projekt, po-
dobny do przykładów napisanych w pierwszej lub w drugiej części, wówczas najwy-
godniej skorzystać z dystrybucji Symfony Standard. Jeśli chcemy natomiast instalować
dodatkowe pakiety, wówczas pracę możemy rozpocząć od pakietu Symfony Standard
without vendors.

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

Pobieranie pakietów do folderu vendor/


W celu pobrania wszystkich pakietów wymienionych w pliku [projekt]/deps do folderu
[projekt]/vendor/ należy wydać komendę:
php bin/vendors install

Komenda ta spowoduje pobranie z serwera github.com wszystkich pakietów wraz z ich


pełną historią.

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.

Do usunięcia wszystkich folderów .git/ zawartych wewnątrz folderu [projekt]/vendor/


możemy użyć komendy przedstawionej na listingu 15.2.

Listing 15.2. Komenda usuwająca wszystkie foldery o nazwie .git


find vendor -name .git -type d -exec rm -fr {} \;

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!

Komendy find oraz rm są standardowymi poleceniami dostępnymi w konsoli shell


systemów rodziny u*ix. W systemach Windows są one dostępne w konsoli bash
instalowanej wraz z oprogramowaniem Cygwin.

1
Pod warunkiem, że nie zamierzasz modyfikować kodu pakietów i umieszczać ich na serwerze Git.
212 Część III ♦ Dostosowywanie Symfony 2

Dołączanie pakietów do kodu


Pakiety pobrane do folderu [projekt]/vendor/ należy skonfigurować. Konieczne jest
dodanie pakietu do listy zarejestrowanych pakietów. Oprócz tego może być konieczne
zmodyfikowanie skryptu odpowiedzialnego za automatyczne ładowanie klas.

Pakiety rejestrujemy w pliku [projekt]/app/AppKernel.php. W celu dodania pakietu


DoctrineFixturesBundle do listy zarejestrowanych pakietów należy w pliku [projekt]/
app/AppKernel.php dodać instrukcję przedstawioną na listingu 15.3.

Listing 15.3. Rejestracja pakietu DoctrineFixturesBundle


$bundles = array(
...
new Symfony\Bundle\DoctrineFixturesBundle\DoctrineFixturesBundle(),
);

Automatyczne ładowanie klas konfigurujemy w skrypcie [projekt]/app/autoload.php.


W celu rejestracji automatycznego ładowania klas z przestrzeni Doctrine\Common\
´Datafixtures należy w pliku [projekt]/app/autoload. php dodać instrukcję przed-
stawioną na listingu 15.4.

Listing 15.4. Rejestracja automatycznego ładowania klas w skrypcie autoload.php


...
$loader->registerNamespaces(array(
...
'JMS' => __DIR__.'/../vendor/bundles',
'Doctrine\\Common\\DataFixtures' => __DIR__.'/../vendor/doctrine-fixtures/lib',
'Doctrine\\Common' => __DIR__.'/../vendor/doctrine-common/lib',
...
));

Przykład 15. 1. Przygotowanie


dystrybucji symfony2-customized-v2
zawierającej pakiet
DoctrineFixturesBundle
Przygotuj pakiet symfony2-customized-v2.zip, który będzie zawierał najnowszą wersję
Symfony 2 pozbawioną kodu przykładowego src/Acme/ oraz zawierającą pakiet do
przetwarzania plików z danymi, tzw. fikstur (ang. fixtures).

W przygotowanej dystrybucji włącz także dostępność następujących filtrów tekstowych


Twig: nl2br, truncate i wordwrap.
Rozdział 15. ♦ Dodawanie nowych pakietów 213

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

Zwróć uwagę, że folder symfony2-customized-v2/ zajmuje 227 kB.

Krok 2. Usuń pakiet src/Acme/


Kolejno:
 Usuń folder src/Acme/.
 Usuń folder web/bundles/acmedemo/.
 W pliku app/AppKernel.php usuń wiersz:
$bundles[] = new Acme\DemoBundle\AcmeDemoBundle();

 W pliku app/config/routing_dev.yml usuń wiersze:


_welcome:
pattern: /
defaults: { _controller: AcmeDemoBundle:Welcome:index }

_demo_secured:
resource: "@AcmeDemoBundle/Controller/SecuredController.php"
type: annotation

_demo:
resource: "@AcmeDemoBundle/Controller/DemoController.php"
type: annotation
prefix: /demo

Krok 3. Włącz filtry nl2br, truncate i wordwrap


W pliku konfiguracyjnym [projekt]/app/config/config.yml dodaj instrukcje przedstawione
na listingu 15.5.

Listing 15.5. Konfiguracja filtrów nl2br, truncate i wordwrap


services:
twig.extension.txt:
class: Twig_Extensions_Extension_Text
tags:
- { name: twig.extension }
214 Część III ♦ Dostosowywanie Symfony 2

Krok 4. Dodaj nowe pakiety w pliku deps


Na końcu pliku [projekt]/deps dodaj pakiety wymienione na listingu 15.6.

Listing 15.6. Zawartość, którą należy dodać na końcu pliku deps


[doctrine-fixtures]
git=http://github.com/doctrine/data-fixtures.git

[DoctrineFixturesBundle]
git=http://github.com/doctrine/DoctrineFixturesBundle.git
target=/bundles/Symfony/Bundle/DoctrineFixturesBundle
version=origin/2.0

Krok 5. Pobierz pakiety


Wydaj komendę:
php bin/vendors install

Teraz folder symfony2-customized-v2/ zajmuje około 106 MB.

Zwróć uwagę, że dodany pakiet doctrine-fixtures nie zawiera oznaczenia wersji:


[doctrine-fixtures]
git=http://github.com/doctrine/data-fixtures.git

W takim wypadku komenda:


php bin/vendors install

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

otrzymasz identyczną wersję dystrybucji. Najnowsze zmiany zawarte w pakiecie na ser-


werze GitHub nie zostaną uwzględnione w Twojej dystrybucji.

Krok 6. Usuń foldery .git


Uruchom konsolę i przejdź do folderu symfony2-customized-v2/ wykonywanego przykładu.
Wydaj w nim komendę:
find vendor -name .git -type d -exec rm -fr {} \;

Teraz folder symfony2-customized-v2/ zajmuje około 20 MB.


Rozdział 15. ♦ Dodawanie nowych pakietów 215

Przed usunięciem zawartości folderów .git/ należy wydać komendę:


php bin/vendors lock
W przeciwnym razie otrzymamy dystrybucję, która po pewnym czasie najprawdopodobniej
stanie się nieaktualna.

Krok 7. Zarejestruj pakiet DoctrineFixturesBundle


W pliku [projekt]/app/AppKernel.php wprowadź zawartość przedstawioną na listingu 15.7.

Listing 15.7. Rejestracja nowych pakietów w pliku AppKernel.php


...
$bundles = array(
new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
new Symfony\Bundle\SecurityBundle\SecurityBundle(),
new Symfony\Bundle\TwigBundle\TwigBundle(),
new Symfony\Bundle\MonologBundle\MonologBundle(),
new Symfony\Bundle\SwiftmailerBundle\SwiftmailerBundle(),
new Symfony\Bundle\DoctrineBundle\DoctrineBundle(),
new Symfony\Bundle\AsseticBundle\AsseticBundle(),
new Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle(),
new JMS\SecurityExtraBundle\JMSSecurityExtraBundle(),
new Symfony\Bundle\DoctrineFixturesBundle\DoctrineFixturesBundle(),
);
...

Krok 8. Skonfiguruj automatyczne ładowanie klas


pakietu DoctrineFixturesBundle
W pliku [projekt]/app/autoload.php wprowadź zawartość przedstawioną na listingu 15.8.

Listing 15.8. Rejestracja przestrzeni nazewniczej w skrypcie autoload.php


$loader->registerNamespaces(array(
'Symfony' => array(__DIR__.'/../vendor/symfony/src',
´__DIR__.'/../vendor/bundles'),
'Sensio' => __DIR__.'/../vendor/bundles',
'JMS' => __DIR__.'/../vendor/bundles',
'Doctrine\\Common\\DataFixtures' => __DIR__.'/../vendor/doctrine-fixtures/lib',
'Doctrine\\Common' => __DIR__.'/../vendor/doctrine-common/lib',
'Doctrine\\DBAL' => __DIR__.'/../vendor/doctrine-dbal/lib',
'Doctrine' => __DIR__.'/../vendor/doctrine/lib',
'Monolog' => __DIR__.'/../vendor/monolog/src',
'Assetic' => __DIR__.'/../vendor/assetic/src',
'Metadata' => __DIR__.'/../vendor/metadata/src',
));
216 Część III ♦ Dostosowywanie Symfony 2

Krok 9. Skompresuj folder symfony2-customized-v2/


Skompresuj zawartość folderu symfony2-customized-v2/ do pliku symfony2-customized-
-v2.zip.

Otrzymana dystrybucja zajmuje około 10 MB i pozwala na korzystanie z:


 filtrów nl2br i truncate,
 skryptów do przetwarzania plików z danymi (ang. fixtures).
Rozdział 16.
Podsumowanie części III
Rozdział 15. zapoznał Cię z bardzo ważnym zagadnieniem: instalacją dodatkowych
pakietów. Dowiedziałeś się, że lista pakietów zawartych w dystrybucji Symfony 2 jest
zawarta w plikach deps i deps.lock, oraz poznałeś ich składnię. Wiesz już także, że plik
deps modyfikujemy ręcznie, zaś plik deps.lock uaktualniamy poleceniem php bin/
´vendors lock. Znasz polecenie ułatwiające usunięcie folderów .git/ oraz umiesz skonfi-
gurować przykładowy pakiet DoctrineFixturesBundle.

Wykorzystując poznaną wiedzę, przygotowaliśmy nową dystrybucję Symfony 2:


 symfony2-customized-v2.

Dystrybucja oznaczona numerem v2:


 Nie zawiera pakietu src/Acme.
 Ma w konfiguracji włączone następujące filtry tekstowe Twig: nl2br, truncate
i wordwrap.
 Zawiera pakiet do przetwarzania plików z danymi (ang. fixtures).

Wskazówka dla instruktorów i wykładowców


Podczas prowadzenia zajęć ze studentami przekonałem się, że ładowanie dodatkowych pakietów
komendą php bin/vendors install jest bardzo uciążliwe i dezorganizuje zajęcia. Po pierwsze,
w zależności od obciążenia sieci może trwać nawet kilka minut, po drugie, otrzymane foldery
zajmują ponad 100 MB. Zadowalające efekty dydaktyczne zacząłem osiągać dopiero wtedy, gdy
do prowadzenia zajęć wykorzystałem dostosowane pakiety w rodzaju symfony2-customized-v1.zip
oraz symfony2-customized-v2.zip.

W podobny sposób w kolejnych częściach przygotujemy dystrybucje ułatwiające:


 tworzenie skrótów slug,
 migracje bazy danych,
 stronicowanie wyników,
218 Część III ♦ Dostosowywanie Symfony 2

 tworzenie paneli administracyjnych


 oraz autoryzację użytkowników.

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.

Przykład 17.1. Imiona


Dany jest plik tekstowy o nazwie imiona.txt, który w każdym wierszu zawiera jedno
imię, np.:
Jan
Krzysztof
Tomasz
...
222 Część IV ♦ Praca z bazą 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>

Zadanie rozwiąż w taki sposób, by aplikacja wykorzystywała bazę danych. Zawartość


pliku tekstowego zapisz w bazie danych. Po odwiedzeniu strony głównej aplikacji w ko-
dzie akcji pobierz wszystkie rekordy z bazy danych i przekaż je do widoku. Wypełnianie
bazy danych oprogramuj, wykorzystując rozszerzenie DoctrineFixturesBundle.

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

utwórz pakiet My/FrontendBundle.

Następnie utwórz folder zad-17/data/ i umieść w nim plik imiona.txt.

Krok 2. Utwórz bazę danych names


Utwórz folder zad-17/00-dodatki/ i umieść w nim plik tworzenie-pustej-bazy-danych.sql
o zawartości przedstawionej na listingu 17.1.

Listing 17.1. Plik tworzenie-pustej-bazy-danych.sql z przykładu 17.1


drop schema if exists names;
create schema names default character set utf8 collate utf8_polish_ci;
grant all on names.* to editor@localhost identified by 'secretPASSWORD';
flush privileges;

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.

Ostatnie zapytanie przeładowuje uprawnienia na serwerze MySQL:


flush privileges;

W celu wykonania zapytań z listingu 17.1 uruchom program phpMyAdmin i przejdź do


zakładki SQL. W oknie dialogowym wklej kod SQL, po czym naciśnij przycisk Wykonaj.
Procedura wykonania kodu SQL z listingu 17.1 w programie phpMyAdmin jest przed-
stawiona na rysunku 17.1.

Jeśli program mysql jest dostępny w ścieżkach poszukiwań, procedurę wykonywania


kodu SQL możesz uprościć, przygotowując plik wsadowy. Utwórz plik o nazwie zad-17/
00-dodatki/tworzenie-pustej-bazy-danych.bat i wprowadź w nim polecenie z listingu 17.2.

Listing 17.2. Plik tworzenie-pustej-bazy-danych.bat


mysql -u root -pAX1BY2CZ3 < tworzenie-pustej-bazy-danych.sql

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.

Pracując w systemie u*ix, przygotuj skrypt tworzenie-pustej-bazy-danych.sh:


#!/usr/bin/sh
mysql -u root -pAX1BY2CZ3 < tworzenie-pustej-bazy-danych.sql

Po wykonaniu kodu z pliku tworzenie-pustej-bazy-danych.sql sprawdź, czy na serwe-


rze została utworzona pusta baza danych. Po odświeżeniu strony głównej programu
phpMyAdmin i wybraniu z listy rozwijanej bazy danych names ujrzysz stronę przedsta-
wioną na rysunku 17.2.
224 Część IV ♦ Praca z bazą danych

Rysunek 17.1. Wykonanie kodu SQL w programie phpMyAdmin

Wewnątrz projektów Symfony 2 połączenie z bazą danych będzie nawiązywane przy


użyciu konta editor utworzonego poleceniem grant. Moim zdaniem w kodzie aplikacji
nie należy stosować konta root. Dlatego we wszystkich przykładach tworzenie pustej
bazy wraz z kontem dostępu będę wykonywał skryptami SQL oraz BAT przedsta-
wionymi na listingach 17.1 oraz 17.2.

Krok 3. Przeanalizuj zawartość pliku danych imiona.txt


Plik zad-17/data/imiona.txt zawiera 479 imion. Każde imię jest zapisane w osobnym
wierszu. Fragment pliku imiona.txt jest przedstawiony na listingu 17.3.

Listing 17.3. Fragment pliku imiona.txt


Abel
Abraham
Ada
...
Rozdział 17. ♦ Pierwszy projekt wykorzystujący bazę danych 225

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

Krok 4. Wygeneruj klasę dostępu do bazy danych

W Symfony 2 klasę dostępu do bazy danych nazywamy modelem.

Wydaj polecenie:
php app/console generate:doctrine:entity

W odpowiedzi na monit:
The Entity shortcut name:

wprowadź logiczną nazwę modelu:


MyFrontendBundle:Name
226 Część IV ♦ Praca z bazą danych

Jako format konfiguracji pozostaw ustawienia domyślne1, czyli adnotacje:


Configuration format (yml, xml, php, or annotation) [annotation]:

po czym dodaj do klasy jedno pole2 o nazwie caption typu string o długości 255 znaków.

Pozostałe opcje pozostaw domyślne.

W ten sposób wygenerowana została klasa src/My/FrontendBundle/Entity/Name.php,


która jest przedstawiona na listingu 17.4.

Listing 17.4. Wygenerowana klasa Name.php


<?php

namespace My\FrontendBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
* 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().

Poznaliśmy już trzy rodzaje nazw logicznych:


 logiczne nazwy widoków (np. MyFrontendBundle:Default:index.html.twig),

 logiczne nazwy kontrolerów (np. MyFrontendBundle:Default:index),

 logiczne nazwy modeli (np. MyFrontendBundle:Name).

Krok 5. Ustal parametry połączenia z bazą danych


W pliku konfiguracyjnym app/config/parameters.ini wprowadź nazwę bazy danych names
oraz konto editor i hasło secretPASSWORD. Zarys zmodyfikowanego pliku parameters.ini
jest przedstawiony na listingu 17.5.

Listing 17.5. Zmodyfikowany plik parameters.ini


[parameters]
database_driver = pdo_mysql
database_host = localhost
database_port =
database_name = names
database_user = editor
database_password = secretPASSWORD
...
228 Część IV ♦ Praca z bazą danych

Krok 6. Utwórz tabelę name w bazie danych names


Wydaj polecenie:
php app/console doctrine:schema:update --force

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.

Rysunek 17.3. Struktura tabeli name w bazie danych names

Krok 7. Wypełnij tabelę name danymi z pliku tekstowego


Utwórz folder src/My/FrontendBundle/DataFixtures/ORM/ i umieść w nim plik LoadData.
php o zawartości takiej jak na listingu 17.6.

Listing 17.6. Plik DataFixtures/ORM/LoadData.php


<?php

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();
}
}

W pliku tym odczytujemy zawartość pliku tekstowego imiona.txt:


$data = file('data/imiona.txt');

Otrzymaną tablicę $data przetwarzamy iteracyjnie:


foreach ($data as $i) {
...
}

Wewnątrz pętli zmienna $i zawiera kolejne imiona. W celu zapisania imienia w bazie
najpierw tworzymy obiekt $Name klasy Name:
$Name = new Name();

Następnie ustalamy wartość właściwości caption:


$Name->setCaption(trim($i));

po czym ustalamy, że obiekt $Name ma być synchronizowany z bazą danych.


$manager->persist($Name);

Należy pamiętać, że metoda persist() nie zapisuje rekordu w bazie danych, gdyż opera-
cje zapisu rekordów podlegają buforowaniu.

Faktyczny zapis rekordu w bazie danych ma miejsce dopiero po wywołaniu instrukcji:


$manager->flush();

Gdy kod z listingu 17.6 jest gotowy, wydaj komendę:


php app/console doctrine:fixtures:load

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.

Krok 8. Dostosuj akcję index


W kontrolerze DefaultController.php zmodyfikuj metodę indexAction(). Wprowadź kod
przedstawiony na listingu 17.7.
230 Część IV ♦ Praca z bazą danych

Rysunek 17.4. Tabela name zawiera 479 rekordów

Listing 17.7. Kod akcji index


class DefaultController extends Controller
{
/**
* @Route("/")
* @Template()
*/
public function indexAction()
{
$em = $this->getDoctrine()->getEntityManager();
$entities = $em->getRepository('MyFrontendBundle:Name')->findAll();
return array('entities' => $entities);
}

W akcji index tworzymy obiekt $em, który umożliwia wydawanie zapytań do bazy danych:
$em = $this->getDoctrine()->getEntityManager();

Następnie pobieramy wszystkie rekordy z tabeli name. Wykonujemy to za pośrednic-


twem klasy MyFrontendBundle:Name, którą wygenerowaliśmy w kroku 4.:
$entities = $em->getRepository('MyFrontendBundle:Name')->findAll();
Rozdział 17. ♦ Pierwszy projekt wykorzystujący bazę danych 231

Zmienna $entities jest kolekcją obiektów klasy Name, której kod jest widoczny na li-
stingu 17.4.

Wykonanie akcji kończymy przekazaniem kolekcji $entities do widoku:


return array('entities' => $entities);

Krok 9. Dostosuj widok akcji index


W pliku My/FrontendBundle/Resources/views/Default/index.html.twig wprowadź kod
z listingu 17.8.

Listing 17.8. Widok akcji index


{% extends '::base.html.twig' %}

{% block body %}

<h2>Imiona</h2>

<ul>
{% for imie in entities %}
<li>
{{ imie.caption }}
</li>
{% endfor %}
</ul>

{% endblock %}

W widoku akcji index iteracyjnie przetwarzamy tablicę entities:


{% for imie in entities %}
...
{% endfor %}

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() }}

Krok 10. Sprawdź działanie aplikacji


Odwiedź w przeglądarce adres:
.../web/
232 Część IV ♦ Praca z bazą danych

Powinieneś ujrzeć witrynę przedstawioną na rysunku 17.5.

Rysunek 17.5.
Witryna z przykładu 17.1
Rozdział 18.
ORM Doctrine 2

Tworzenie i usuwanie bazy danych


Do tworzenia i usuwania bazy danych Symfony 2 udostępnia trzy komendy:
doctrine:schema:create
doctrine:schema:drop
doctrine:schema:update

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.

W celu usunięcia bazy danych należy wydać komendę:


doctrine:schema:drop --force

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

Ostatnia z komend uaktualnia natomiast strukturę bazy na podstawie plików z folderów


Entity/:
doctrine:schema:update --force

Działanie powyższych trzech komend należy poprzedzić wprowadzeniem w pliku


parameters.ini konta dostępu, a konto tworzymy zewnętrznym zapytaniem:
grant all on names.* to editor@localhost identified by 'secretPASSWORD';

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

W dowolnym przykładzie wydaj polecenie:


php app/console > symfony2-commands.txt
Otrzymasz w ten sposób plik symfony2-commands.txt zawierający zestawienie wszyst-
kich komend Symfony 2. Znajdziesz w nim m.in. wymienione powyżej komendy:
doctrine:schema:create
doctrine:schema:drop
doctrine:schema:update

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/

Do wygenerowania pojedynczej klasy dostępu do bazy służy polecenie:


php app/console generate:doctrine:entity

Klasy dostępu do bazy danych są domyślnie zapisywane w folderze Entity/ odpowiedniego


pakietu. W przykładzie omówionym w rozdziale 17. wygenerowaliśmy klasę o nazwie Name,
która była zapisana w pliku:
My/FrontendBundle/Entity/Name.php

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

Analizując przebieg przykładu 17.1., z łatwością zauważysz, że logiczną nazwę klasy


Name podaliśmy w odpowiedzi na monit:
The Entity shortcut name:

Jeśli więc po wydaniu polecenia generate:doctrine:entity podasz logiczną nazwę:


LoremIpsumBundle:Dolor

wówczas wygenerowana zostanie klasa1:


Lorem/IpsumBundle/Entity/Dolor.php

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

Przeanalizuj wszystkie komendy Symfony 2, które po wydaniu polecenia:


php app/console > symfony2-commands.txt
znajdziesz w pliku symfony2-commands.txt. Zwróć uwagę, że do tworzenia klas służą
komendy:
php app/console generate:doctrine:entity
php app/console doctrine:generate:entity
Komendy te są synonimami — mają identyczne działanie. Możesz korzystać z do-
wolnej z nich!

Tworzenie tabel w bazie danych


Struktura bazy danych aplikacji jest ustalona wyłącznie klasami z folderów Entity/. Jeśli
pracowałeś w Symfony 1, to bez względu na to, czy korzystałeś z ORM Propel, czy z ORM
Doctrine, strukturę bazy danych zapisywałeś w pliku schema.yml i na podstawie tego pliku
generowałeś kod SQL tworzący tabele oraz klasy dostępu do bazy danych. W systemie
Doctrine 2 praca przebiega w inny sposób. Nie występuje tu żaden dodatkowy plik usta-
lający strukturę bazy danych. Struktura bazy jest ustalona klasami z folderów Entity/.
W klasach znajdziesz adnotacje postaci:
@ORM\Table()
@ORM\Entity

oraz:
@ORM\Column(name="caption", type="string", length=255)

które odpowiadają za wygenerowanie odpowiedniego kodu SQL.

Zestawienie wszystkich adnotacji konfiguracyjnych jest zawarte na stronie:


http://www.doctrine-project.org/docs/orm/2.1/en/reference/annotations-reference.html
Adnotacje wymienione w dokumentacji Doctrine 2, np.:
@OneToMany(...)
należy w Symfony 2 zapisywać w postaci:
@ORM\OneToMany(...)

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

Do uaktualnienia bazy danych możesz użyć polecenia:


doctrine:schema:update --force

lub dwóch poleceń:


php app/console doctrine:schema:drop
php app/console doctrine:schema:create

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")

i uaktualnisz strukturę bazy:


doctrine:schema:update --force

to baza będzie zawierała dwie tabele: user oraz 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();

i odwołuje się do pojedynczego rekordu.

Struktura klas dostępu do bazy danych


Klasy dostępu do bazy zawierają pewną liczbę właściwości oraz metod. Domyślnie każda
klasa zawiera właściwość $id, przeznaczoną na klucz główny tabeli:
/**
* @var integer $id
*
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
Rozdział 18. ♦ ORM Doctrine 2 237

Dla właściwości tej wygenerowana zostaje metoda getId():


/**
* Get id
*
* @return integer
*/
public function getId()
{
return $this->id;
}

Adnotacja:
@ORM\GeneratedValue(strategy="AUTO")

powoduje, że kolumna $id w bazie danych będzie autoinkrementowana (AUTO INCREMENT).


W związku z tym w klasie nie występuje metoda setId().

Dla pozostałych właściwości w klasie Entity wystąpią metody get() oraz set().

Dodawanie nowych właściwości


do istniejącej klasy
W celu dodania nowej kolumny w tabeli bazy danych należy:
 dodać właściwość w odpowiedniej klasie,
 wygenerować metody get() oraz set(),
 uaktualnić strukturę bazy danych.

Jeśli w aplikacji występuje klasa src/My/FrontendBundle/Entity/Lorem.php, najpierw


dodajemy nową właściwość ipsum:
/**
* @var string $ipsum
*
* @ORM\Column(name="ipsum", type="string", length=255)
*/
private $ipsum;

Następnie poleceniem:
php app/console doctrine:generate:entities My

generujemy w klasie Lorem.php metody getIpsum() oraz setIpsum(). Parametr My odnosi


się do folderu My/ i powoduje przetworzenie wszystkich klas Entity zawartych w folderze
My/ (w dowolnym pakiecie, m.in. FrontendBundle).

Na zakończenie wydajemy polecenie:


doctrine:schema:update --force
238 Część IV ♦ Praca z bazą danych

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)

Zestawienie wszystkich dostępnych wartości dla parametru type wraz z odpowiadającymi


im typami SQL oraz PHP jest zawarte w tabeli 18.1.

Tabela 18.1. Zestawienie wszystkich dostępnych wartości type adnotacji @ORM\Column


Adnotacja
Typ SQL Typ PHP
@ORM\Column(type="...")
string VARCHAR string
integer INT integer
smallint SMALLINT integer
bigint BIGINT string
boolean BOOLEAN boolean
decimal DECIMAL double
date DATETIME DateTime object
time TIME DateTime object
datetime DATETIME/TIMESTAMP DateTime object
text CLOB string
object CLOB Obiekt
(stosowane są transformacje serialize()
i unserialize())
array CLOB Obiekt
(stosowane są transformacje serialize()
i unserialize())
float FLOAT double
podwójna precyzja Uwaga: działa wyłącznie wtedy, gdy ustawienia
regionalne stosują kropkę jako separator.

Do ustalenia różnych dodatkowych parametrów dla generowanych kolumn służą pa-


rametry wymienione w tabeli 18.2.

Parametr name odgrywa identyczną rolę jak w adnotacji @Table(). Adnotacja:


/**
* @ORM\Column(name="b", ...)
*/
private $a;

powoduje, że dla właściwości $a zostanie w tabeli bazy danych wygenerowana kolumna


o nazwie b.
Rozdział 18. ♦ ORM Doctrine 2 239

Tabela 18.2. Dodatkowe parametry adnotacji @ORM\Column


Parametr Wartość domyślna Uwagi
type string Typ kolumny. Por. tabela 18.1.
name Taka sama jak nazwa Nazwa kolumny w bazie danych
właściwości
length 255 Wyłącznie dla kolumn reprezentowanych jako ciąg znaków.
unique false Unikatowość kolumny.
nullable false Czy wartość NULL jest dopuszczalna?
precision 0 Precyzja. Stosuje się wyłącznie dla kolumn decimal.
scale 0 Skalowanie. Stosuje się wyłącznie dla kolumn decimal.

Parametr unique wymusza unikatowość kolumny. Po dodaniu parametru unique:


/**
* @var string $caption
*
* @ORM\Column(name="caption", type="string", length=255, unique="true")
*/
private $caption;

i uaktualnieniu struktury bazy danych w tabeli name pojawi się indeks:


UNIQUE KEY `UNIQ_...` (`caption`)

Oczywiście po dodaniu parametru unique nie można zapisywać w tabeli zdublowanych


wartości dla podanej kolumny.

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.

Listing 18.1. Kod wstawiający rekordy zawierające wartości NULL


public function load($manager)
{
$Name = new Name();
$manager->persist($Name);

$Name = new Name();


$manager->persist($Name);

...

Po dodaniu w adnotacji parametru nullable:


/**
* @var string $caption
*
* @ORM\Column(name="caption", type="string", length=255, nullable="true")
*/
private $caption;
240 Część IV ♦ Praca z bazą danych

i uaktualnieniu struktury bazy danych wykonanie kodu z listingu 18.1 przebiegnie


poprawnie. W tabeli name pojawią się dwa rekordy o wartościach NULL.

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.

Operowanie klasami dostępu


do bazy danych
Klasy Entity i EntityManager
Jak już widzieliśmy w poprzednim rozdziale, operacje dostępu do bazy danych wykonu-
jemy za pośrednictwem klas Entity (np. wygenerowanej klasy Name). Operacje na
obiektach Entity podlegają buforowaniu. Utworzenie obiektu klasy Name i przypisanie
wartości do kolumn:
$Name = new Name();
$Name->setCaption('Anna');

nie powoduje wykonania żadnych zapytań SQL. W bazie danych nie pojawił się jeszcze
rekord o wartości Anna.

Za zarządzanie stanem obiektów powiązanych z bazą danych odpowiada klasa Entity


´Manager. W skrypcie LoadData.php z listingu 17.6 EntityManager jest parametrem metody
load():
Rozdział 18. ♦ ORM Doctrine 2 241

function load(ObjectManager $manager)


{
...
}

W przedstawionej na listingu 17.7 akcji index obiekt EntityManager tworzymy nato-


miast, wywołując metodę getEntityManager():
$em = $this->getDoctrine()->getEntityManager();

Powiązanie obiektu Entity z obiektem EntityManager realizujemy, wywołując metodę


persist():
$manager->persist($x);

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();

Stan obiektu Entity


Każdy obiekt Entity znajduje się w jednym z czterech stanów oznaczanych jako:
 NEW
Nowy obiekt, utworzony operatorem new, który nie jest i nigdy nie był powiązany
z obiektem EntityManager (nie podlega zatem operacjom synchronizacji z bazą
danych).
 MANAGED
Obiekt jest powiązany z obiektem EntityManager (podlega operacjom
synchronizacji z bazą danych).
 DETACHED
Obiekt był powiązany z obiektem EntityManager, ale został odłączony
(już nie podlega synchronizacji z bazą danych).
 REMOVED
Obiekt jest powiązany z obiektem EntityManager, ale został usunięty z bazy
danych (podlega synchronizacji z bazą danych; usunięcie odpowiadającego
mu rekordu nastąpi po kolejnym zatwierdzeniu operacji obiektu EntityManager).

Obiekt jest w stanie NEW bezpośrednio po utworzeniu:


$x = new Name();
//obiekt $x jest w stanie NEW

Po wywołaniu metody persist() obiekt $x znajduje się w stanie MANAGED:


$manager->persist($x);
//obiekt $x jest w stanie MANAGED
242 Część IV ♦ Praca z bazą danych

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

Metoda detach() zmienia stan obiektu na DETACHED:


$manager->detach($x);
//obiekt $x jest w stanie DETACHED

Natomiast metoda remove() obiektu zmienia stan obiektu na REMOVED:


$manager->remove($x);
//obiekt $x jest w stanie REMOVED

Z powodu buforowania po wywołaniu metod:


$manager->persist(...);
$manager->detach(...);
$manager->remove(...);

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();

Tworzenie nowych rekordów


W celu utworzenia nowego rekordu należy:
 utworzyć obiekt odpowiedniej klasy,
 metodami set() przypisać wartości kolumn,
 powiązać utworzony obiekt z obiektem EntityManager (metoda persist()),
 zsynchronizować stan obiektów zarządzanych przez obiekt EntityManager z bazą
danych (metoda flush()).

Kod tworzący nowy rekord w tabeli name (klasa dostępu do tabeli nazywa się Name) jest
przedstawiony na listingu 18.2.

Listing 18.2. Tworzenie nowego rekordu w tabeli name


use My\FrontendBundle\Entity\Name;

...

$Name = new Name();


$Name->setCaption('Anna');
$manager->persist($Name);
$manager->flush();

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

Pamiętaj, że użycie klasy Name należy poprzedzić instrukcją use:


use My\FrontendBundle\Entity\Name;
lub podaniem nazwy globalnej:
$Name = new \My\FrontendBundle\Entity\Name();

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()).

Kod usuwający obiekt $x z bazy danych jest przedstawiony na listingu 18.3.

Listing 18.3. Usuwanie z bazy danych rekordu powiązanego z obiektem $x


$manager->remove($x);
$manager->flush();

Pobieranie wszystkich rekordów z bazy


Do pobrania wszystkich rekordów z bazy danych służy metoda findAll() klasy Repository
powiązanej z klasą Entity:
$entities = $em->getRepository('MyFrontendBundle:Name')->findAll();

Klasy Repository są omówione w kolejnym rozdziale.

Przykład 18.1. Rzeki


Dany jest plik rivers.yml, którego początkowe wiersze przedstawiono na listingu 18.4.

Listing 18.4. Plik rivers.yml


- { name: Parana, length: 4700 }
- { name: Mekong, length: 4500 }
- { name: Missisipi (od źródeł Missouri), length: 6418 }
...

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

utwórz pakiet My/FrontendBundle.

Następnie utwórz folder zad-18/data/ i umieść w nim plik rivers.yml.

Krok 2. Utwórz bazę danych rivers


Utwórz plik zad-18/00-dodatki/tworzenie-pustej-bazy-danych/tworzenie-pustej-bazy-danych.
sql o zawartości takiej jak na listingu 18.5.

Listing 18.5. Plik tworzenie-pustej-bazy-danych.sql z przykładu 18.1


drop schema if exists rivers;
create schema rivers default character set utf8 collate utf8_polish_ci;
grant all on rivers.* to editor@localhost identified by 'secretPASSWORD';
flush privileges;

Wykorzystując plik z listingu 18.4, utwórz pustą bazę danych rivers. Poprawność tworze-
nia bazy sprawdź za pomocą programu phpMyAdmin.

Krok 3. Ustal parametry połączenia z bazą danych


W pliku konfiguracyjnym app/config/parameters.ini wprowadź nazwę bazy danych rivers
oraz konto editor i hasło secretPASSWORD. Zarys zmodyfikowanego pliku parameters.ini
jest przedstawiony na listingu 18.6.

Listing 18.6. Zmodyfikowany plik parameters.ini


[parameters]
database_driver = pdo_mysql
database_host = localhost
database_port =
database_name = rivers
database_user = editor
database_password = secretPASSWORD
...
Rozdział 18. ♦ ORM Doctrine 2 245

Krok 4. Wygeneruj klasę dostępu do bazy danych


Wydaj polecenie:
php app/console generate:doctrine:entity

W odpowiedzi na monit:
The Entity shortcut name:

wprowadź logiczną nazwę modelu:


MyFrontendBundle:River

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.

Listing 18.7. Zarys wygenerowanej klasy River.php


class River
{
private $id;
private $name;
public function getId()
{
return $this->id;
}
public function setName($name)
{
$this->name = $name;
}
public function getName()
{
return $this->name;
}
}

Krok 5. W bazie danych rivers utwórz tabelę river


Wydaj polecenie:
php app/console doctrine:schema:update --force

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.

Krok 6. W tabeli river i w klasie River dodaj kolumnę length


W celu dodania nowej kolumny najpierw dodaj w klasie River przedstawioną na listingu
18.8 właściwość length.
246 Część IV ♦ Praca z bazą danych

Listing 18.8. Właściwość length w klasie River


class River
{
...

/**
* @var string $name
*
* @ORM\Column(name="name", type="string", length=255)
*/
private $name;

/**
* @var integer $length
*
* @ORM\Column(name="length", type="integer")
*/
private $length;

...

Następnie wydaj polecenie:


php app/console generate:doctrine:entities My

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().

Listing 18.9. Wygenerowane metody getLength() oraz setLength()


class River
{

...

/**
* 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

Poprawność dodawania kolumny sprawdź za pomocą programu phpMyAdmin.

Krok 7. Tabelę River wypełnij danymi z pliku tekstowego


Utwórz przedstawiony na listingu 18.10 plik src/My/FrontendBundle/DataFixtures/
ORM/LoadData.php.

Listing 18.10. Plik LoadData.php z przykładu 18.1


<?php

namespace My\MountainBundle\DataFixtures\ORM;

use Doctrine\Common\Persistence\ObjectManager;
use My\FrontendBundle\Entity\River;
use Symfony\Component\Yaml\Yaml;

class LoadData implements FixtureInterface


{
public function load(ObjectManager $manager)
{

$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;

Odczytanie pliku rivers.yml i konwersja na wielowymiarową tablicę są wykonane in-


strukcją:
$yml = Yaml::parse('data/rivers.yml');

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

Po przygotowaniu kodu z listingu 18.10 wykonaj polecenie:


php app/console doctrine:fixtures:load

i sprawdź zawartość bazy danych. Baza danych rivers powinna zawierać 17 rekordów.

Krok 8. Przygotuj skórkę aplikacji


Na podstawie szablonu 18-start.zip wykonaj przedstawione na listingach 18.11 oraz 18.12
widoki app/Resources/views/base.html.twig oraz app/Resources/views/layout.html.twig.

Listing 18.11. Widok app/Resources/views/base.html.twig


<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>{% block title %}Welcome!{% endblock %}</title>
<link rel="stylesheet" href="{{ asset('css/style.css') }}" />
</head>
<body>
{% block body %}{% endblock %}
</body>
</html>

Listing 18.12. Widok app/Resources/views/layout.html.twig


{% extends '::base.html.twig' %}

{% 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/.

Krok 9. Dostosuj akcję index


W kontrolerze DefaultController.php zmodyfikuj metodę indexAction(). Wprowadź kod
przedstawiony na listingu 18.13.

Listing 18.13. Kod akcji index


class DefaultController extends Controller
{

/**
* @Route("/")
* @Template()
*/
Rozdział 18. ♦ ORM Doctrine 2 249

public function indexAction()


{
$em = $this->getDoctrine()->getEntityManager();

$entities = $em->getRepository('MyFrontendBundle:River')->findAll();

return array('entities' => $entities);


}

Krok 10. Dostosuj widok akcji index


W pliku My/FrontendBundle/Resources/views/Default/index.html.twig wprowadź kod
z listingu 18.14.

Listing 18.14. Widok akcji index


{% extends "::layout.html.twig" %}

{% 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 %}

Krok 11. Sprawdź działanie aplikacji


Odwiedź w przeglądarce adres:
.../web/

Powinieneś ujrzeć witrynę przedstawioną na rysunku 18.1.


250 Część IV ♦ Praca z bazą danych

Rysunek 18.1. Witryna z przykładu 18.1


Rozdział 19.
Dostosowywanie
klas dostępu
do bazy danych

Klasy Entity oraz Repository


W Doctrine 2, podobnie jak w starszych systemach ORM, interfejs dostępu do bazy
danych jest podzielony na:
 Klasy Entity, które podlegają mapowaniu na rekordy.
 Klasy Repository, które służą do wyszukiwania rekordów.

Domyślnie cała funkcjonalność klasy Repository jest odziedziczona po klasach bazo-


wych. Dlatego polecenie:
php app/console generate:doctrine:entity

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
{
...
}

Jeśli teraz wydamy polecenie:


php app/console doctrine:generate:entities My
252 Część IV ♦ Praca z bazą danych

to w folderze My/FrontendBundle/Entity zostanie wygenerowany plik LoremRepository.


php, który będzie zawierał przedstawioną na listingu 19.1 klasę LoremRepository.

Listing 19.1. Wygenerowana klasa LoremRepository


<?php

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
{

Klasa LoremRepository z listingu 19.1 umożliwia nam nadpisywanie metod odpowie-


dzialnych za wyszukiwanie rekordów. Dziedziczy ona po klasie:
vendor/doctrine/lib/Doctrine/ORM/EntityRepository.php

Podstawowe metody klas Repository


Najważniejsze metody klasy Repository są zawarte w tabeli 19.1.

Tabela 19.1. Najważniejsze metody klasy Repository


Metoda Opis
find() Wyszukiwanie na podstawie klucza pierwotnego
findAll() Pobieranie wszystkich rekordów z tabeli
findBy() Wyszukiwanie na podstawie wartości kolumn; wynikiem jest kolekcja obiektów.
findOneBy() Wyszukiwanie na podstawie wartości kolumn; wynikiem jest pojedynczy obiekt.
findByX() Wyszukiwanie na podstawie wartości kolumny X; wynikiem jest kolekcja obiektów.
findOneByX() Wyszukiwanie na podstawie wartości kolumny X; wynikiem jest pojedynczy obiekt.

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.

Listing 19.2. Wyszukiwanie rekordu na podstawie klucza głównego


$entity = $em->getRepository('MyFrontendBundle:River')->find(567);
if (!$entity) {
throw $this->createNotFoundException('Brak rekordu!');
}

Metoda findAll()
Treść oryginalnej metody findAll() jest przedstawiona na listingu 19.3.

Listing 19.3. Metoda findAll()


public function findAll()
{
return $this->findBy(array());
}

Metoda ta jest bezparametrowa, więc użycie przyjmuje postać:


$entities = $em->getRepository('MyFrontendBundle:River')->findAll();

Metoda findBy()
Nagłówek metody findBy() jest następujący:
public function findBy(
array $criteria, array $orderBy = null, $limit = null, $offset = null
)

Parametr $criteria jest tablicą asocjacyjną, która zawiera warunki wyszukiwania.


Nazwy kolumn są kluczami, a wyszukiwane wartości — elementami tablicy. Jeśli para-
metr zostanie pominięty lub przyjmie wartość null, to zbiór wyników będzie pusty.

Parametr $orderBy ustala porządek sortowania rekordów. Jest to tablica asocjacyjna,


której klucze są nazwami kolumn, a wartości — ciągami ASC lub DESC. W celu pominię-
cia warunków sortowania należy podać tablicę pustą lub wartość null.

Parametr $limit ogranicza liczbę zwracanych wyników. W przypadku użycia parametru


$limit parametr $offset ustala indeks pierwszego zwracanego rekordu.

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());

zwraca kolekcję obiektów klasy Name.


254 Część IV ♦ Praca z bazą danych

Przykłady użycia metody findBy() są zawarte w tabeli 19.2.

Tabela 19.2. Przykłady użycia metody findBy()


Wywołanie Opis
->findBy(); Wynik będzie pusty.
->findBy(array()); Wszystkie rekordy, domyślny porządek sortowania
->findBy(array('caption' => 'Anna')); Rekordy, dla których kolumna caption ma
wartość Anna
->findBy(array(), array('caption' => 'DESC')); Wszystkie rekordy posortowane malejąco względem
kolumny caption
->findBy(array(), array(), 10); Pierwsze 10 rekordów
->findBy(array(), null, 10, 30); 10 rekordów, rozpoczynając od rekordu nr 30

Metoda findOneBy()
Nagłówek metody findOneBy() jest następujący:
public function findOneBy(array $criteria)

Parametr $criteria jest tablicą o identycznej strukturze jak w przypadku metody


findBy(). Metoda ta służy do wyszukiwania pojedynczego rekordu na podstawie wartości
kolumn.

Wynikiem działania metody jest pojedynczy obiekt klasy Entity powiązanej z daną klasą
Repository.

Przykład użycia metody jest przedstawiony na listingu 19.4.

Listing 19.4. Wyszukiwanie rekordu na podstawie wartości kolumn


//wyszukiwanie rzeki o nazwie Parana
//i długości 4700
//Przykład 18.1
$entity = $em->getRepository('MyFrontendBundle:River')
->findOneBy(array('name' => 'Parana', 'length' => 4700));
if (!$entity) {
throw $this->createNotFoundException('Brak rekordu!');
}

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

możemy zatem korzystać z metod:


->findByName('Niger');
->findByLength(123)

Parametrem metod findByX() jest wartość wybranej kolumny.

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().

W przykładzie 18.1, w którym wystąpiły właściwości:


$name;
$length;

możemy korzystać z metod:


->findOneByName('Niger');
->findOneByLength(123)

Parametrem metod findOneByX() jest wartość wybranej kolumny, a wynikiem — obiekt


odpowiedniej klasy Entity.

Nadpisywanie metod klasy Entity


W klasie Entity warto nadpisać dwie metody: __toString() oraz fromArray(). Pierwsza
z nich ułatwi drukowanie obiektów w widoku, a druga uprości implementację skryptu
fixtures.

Metoda __toString() klasy Entity


Przykładowa metoda __toString() dla klasy z przykładu 18.1 jest przedstawiona na
listingu 19.5.

Listing 19.5. Implementacja metody __toString() w klasie Entity


/**
* Get name
*
* @return string
*/
public function __toString()
{
return $this->getName();
}
256 Część IV ♦ Praca z bazą danych

Po dodaniu metody __toString() pętla z listingu 18.14 przyjmie postać:


{% for entity in entities %}
<tr>
<td>{{ loop.index }}.</td>
<td>{{ entity }}</td>
<td>{{ entity.length }}</td>
</tr>
{% endfor %}

Metoda fromArray () klasy Entity


Kod metody fromArray() jest przedstawiony na listingu 19.6.

Listing 19.6. Metoda fromArray() klasy Entity


/**
* Sets properties using array
*
* @param array $data
*/
public function fromArray(array $data)
{
foreach ($data as $key => $value) {
$methodName = 'set' . ucfirst($key);
$this->$methodName($value);
}
}

Po dodaniu metody fromArray() pętla z listingu 18.10 przyjmie postać:


foreach ($yml as $r) {
$river = new River();
$river->fromArray($r);
$manager->persist($river);
}

Nadpisywanie metod klasy Repository


W klasie Repository nadpiszemy metodę findAll(). Dzięki temu rekordy wyświetlane
na stronie będą posortowane względem odpowiednich kolumn. Implementacja metody
findAll() dla klasy z przykładu 18.1 jest przedstawiona na listingu 19.7.

Listing 19.7. Metoda findAll() klasy Entity


/**
* Finds all entities in the repository.
*
* @return array The entities.
*/
public function findAll()
Rozdział 19. ♦ Dostosowywanie klas dostępu do bazy danych 257

{
return $this->findBy(array(), array('name' => 'ASC');
}

Przykład 19.1. Tatry


Dany jest plik mountains.xml, którego początkowe wiersze przedstawiono na listingu 19.8.

Listing 19.8. Plik mountains.xml


<?xml version="1.0" encoding="utf-8"?>
<mountains>
<mountain>
<name>Żółta Turnia</name>
<height>2087</height>
</mountain>
<mountain>
<name>Pośredni Granat</name>
<height>2234</height>
</mountain>
<mountain>
<name>Kasprowy Wierch</name>
<height>1987</height>
</mountain>
...
<mountains>

Wykonaj aplikację, która zestawienie tatrzańskich szczytów zawartych w pliku XML


przedstawi w postaci posortowanej 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 dane zawarte w pliku


19-start.zip.

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

utwórz pakiet My/FrontendBundle.

Następnie utwórz folder zad-19/data/ i umieść w nim plik mountains.xml.


258 Część IV ♦ Praca z bazą danych

Krok 2. Utwórz bazę danych mountains


Przygotuj przedstawiony na listingu 19.9 plik tworzenie-pustej-bazy-danych.sql.

Listing 19.9. Plik tworzenie-pustej-bazy-danych.sql z przykładu 18.1


drop schema if exists mountains;
create schema mountains default character set utf8 collate utf8_polish_ci;
grant all on mountains.* to editor@localhost identified by 'secretPASSWORD';
flush privileges;

Wykorzystując plik z listingu 19.9, utwórz pustą bazę danych mountains. Poprawność
tworzenia bazy sprawdź za pomocą programu phpMyAdmin.

Krok 3. Ustal parametry połączenia z bazą danych


W pliku konfiguracyjnym app/config/parameters.ini wprowadź nazwę bazy danych
mountains oraz konto editor i hasło secretPASSWORD. Zarys zmodyfikowanego pliku
parameters.ini jest przedstawiony na listingu 19.10.

Listing 19.10. Zmodyfikowany plik parameters.ini


[parameters]
database_driver = pdo_mysql
database_host = localhost
database_port =
database_name = rivers
database_user = editor
database_password = secretPASSWORD
...

Krok 4. Wygeneruj klasę dostępu do bazy danych


Poleceniem:
php app/console generate:doctrine:entity

utwórz model:
MyFrontendBundle:Mountain

zawierający pola:
 name typu string o długości 255 znaków,
 height typu integer.

Pozostałe opcje pozostaw domyślne. W ten sposób wygenerujesz klasę Mountain.php,


która będzie zawierała właściwości:
$id getId()
$name getName() setName()
$height getHeight() setHeight()
Rozdział 19. ♦ Dostosowywanie klas dostępu do bazy danych 259

Rozszerz możliwości wygenerowanej klasy, dodając metody __toString() i fromArray()


oraz parametr repositoryClass adnotacji Entity. Zarys otrzymanej klasy Mountain
jest przedstawiony na listingu 19.11.

Listing 19.11. Zarys zmodyfikowanego pliku Mountain.php


/**
* My\FrontendBundle\Entity\Mountain
*
* @ORM\Table()
* @ORM\Entity(repositoryClass="My\FrontendBundle\Entity\MountainRepository")
*/
class Mountain
{
private $id;
private $name;
private $height;

public function __toString()


{
return $this->getName();
}

public function fromArray(array $data)


{
foreach ($data as $key => $value) {
$methodName = 'set' . ucfirst($key);
$this->$methodName($value);
}
}

public function getId()


{
...
}

public function setName($name)


{
...
}

...

Krok 5. Wygeneruj i dostosuj klasę MountainRepository


Wydaj polecenie:
php app/console doctrine:generate:entities My

W klasie MountainRepository.php dodaj przedstawioną na listingu 19.7 metodę findAll().


260 Część IV ♦ Praca z bazą danych

Krok 6. Utwórz tabelę mountain


Wydaj polecenie:
php app/console doctrine:schema:update --force

Poprawność tworzenia tabeli sprawdź za pomocą programu phpMyAdmin.

Krok 7. Wypełnij bazę danych zawartością odczytaną z pliku XML


Przygotuj przedstawiony na listingu 19.12 plik LoadData.php, a następnie wydaj polecenie:
php app/console doctrine:fixtures:load

Listing 19.12. Plik LoadData.php z przykładu 19.1


class LoadData implements FixtureInterface
{
public function load(ObjectManager $manager)
{
$xml = simplexml_load_file('data/mountains.xml');
foreach ($xml->mountain as $mnt) {
$data = (array) $mnt;
$Mountain = new Mountain();
$Mountain->fromArray($data);
$manager->persist($Mountain);
}
$manager->flush();
}
}

Instrukcja:
$data = (array) $mnt;

konwertuje obiekt klasy SimpleXMLElement utworzony przez metodę simplexml_load_


´file() na tablicę.

Po wykonaniu skryptu z listingu 19.12 dla danych z pliku 19-dane.zip baza danych
powinna zawierać 16 rekordów.

Krok 8. Przygotuj skórkę aplikacji


Na podstawie szablonu 19-start.zip wykonaj przedstawione na listingach 19.13 oraz 19.14
widoki app/Resources/views/base.html.twig oraz app/Resources/views/layout.html.twig.

Listing 19.13. Widok app/Resources/views/base.html.twig


<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
Rozdział 19. ♦ Dostosowywanie klas dostępu do bazy danych 261

<title>{% block title %}Tatry{% endblock %}</title>


<link rel="stylesheet" href="{{ asset('css/style.css') }}" />
</head>
<body>
{% block body %}{% endblock %}
</body>
</html>

Listing 19.14. Widok app/Resources/views/layout.html.twig


{% extends '::base.html.twig' %}

{% block body %}
<div id="pojemnik">
{% block content %}
{% endblock %}
</div>
{% endblock %}

W folderach zad-19/web/css/ oraz zad-19/web/images/ umieść style CSS oraz pliki


graficzne.

Krok 9. Dostosuj akcję index


W kontrolerze DefaultController.php zmodyfikuj metodę indexAction(). Wprowadź kod
przedstawiony na listingu 19.15.

Listing 19.15. Kod akcji index


class DefaultController extends Controller
{

/**
* @Route("/")
* @Template()
*/
public function indexAction()
{
$em = $this->getDoctrine()->getEntityManager();
$entities = $em->getRepository('MyFrontendBundle:Mountain')->findAll();
return array('entities' => $entities);
}

Krok 10. Dostosuj widok akcji index


W pliku My/FrontendBundle/Resources/views/Default/index.html.twig wprowadź kod
z listingu 19.16.
262 Część IV ♦ Praca z bazą danych

Listing 19.16. Widok akcji index


{% extends '::layout.html.twig' %}

{% 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 %}

Krok 11. Sprawdź działanie aplikacji


Odwiedź w przeglądarce adres:
.../web/

Powinieneś ujrzeć witrynę przedstawioną na rysunku 19.1.


Rozdział 19. ♦ Dostosowywanie klas dostępu do bazy danych 263

Rysunek 19.1. Witryna z przykładu 19.1


264 Część IV ♦ Praca z bazą danych
Rozdział 20.
Podsumowanie części IV
Naukę wykorzystywania bazy danych w projekcie Symfony 2 rozpoczęliśmy od omówie-
nia podstawowych zagadnień dotyczących MySQL.

Zanim rozpoczniesz tworzenie projektów Symfony 2, musisz opanować umiejętność


tworzenia, usuwania oraz przeglądania zawartości bazy. Wygodnym narzędziem do tego
jest aplikacja phpMyAdmin.

Ponieważ dostawcy rozwiązań hostingowych bardzo często nie udostępniają1 konta


root serwera MySQL, należy nauczyć się tworzyć konta oraz przyznawać im odpowiednie
uprawnienia.

Do tworzenia konta dostępu do bazy danych będziemy wykorzystywali skrypt SQL


z listingu 17.1. Do wykonania skryptu możesz użyć okna SQL programu phpMyAdmin
(rysunek 17.1) lub konsoli mysql.

Projekt omówiony w rozdziale 17. zapoznał nas z takimi zagadnieniami jak:


 konfiguracja dostępu do bazy danych w projekcie Symfony 2,
 generowanie klas dostępu do bazy,
 tworzenie tabel na podstawie wygenerowanych klas,
 wypełnianie bazy danych na podstawie plików,
 tworzenie nowych rekordów,
 pobieranie w akcji wszystkich rekordów z bazy i przekazywanie ich do widoku,
 przetwarzanie w widoku kolekcji rekordów pobranych z bazy danych.

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.

W rozdziale 19. uwagę skupiliśmy na klasach Entity oraz Repository. Po omówieniu


podstawowego interfejsu klasy Repository poznaliśmy technikę nadpisywania metod
w klasach Entity i Repository.
Część V
Zachowania Doctrine
268 Część V ♦ Zachowania Doctrine
Rozdział 21. ♦ Instalacja i konfiguracja rozszerzeń DoctrineExtensions 269

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.

Operacje wykonywane na obiektach podczas uzyskiwania dostępu do bazy danych są


nazywane zachowaniami (ang. behaviours). Najczęściej stosowanymi zachowaniami są:
 timestampable — podczas zapisywania obiektu w bazie danych generowana
jest data zapisu.
 sluggable — podczas zapisywania obiektu w bazie danych generowany jest
specjalny identyfikator slug, który wykorzystujemy w przyjaznych adresach URL.
 translatable — podczas odczytu rekordu z bazy danych dla wybranych
kolumn wybierane są odpowiednie wersje językowe.

W Doctrine 2.1 zachowania są oprogramowane w bibliotece DoctrineExtensions:


https://github.com/l3pp4rd/DoctrineExtensions

Integrację biblioteki DoctrineExtensions w projekcie Symfony 2 ułatwia pakiet


StofDoctrineExtensionsBundle:
https://github.com/stof/StofDoctrineExtensionsBundle

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

Pakiet StofDoctrineExtensionsBundle zawiera uproszczone mechanizmy konfiguracji


zachowań:
 tree,
 translatable,
 sluggable,
 timestampable,
 loggable.

Aby móc wykorzystać zachowania we własnych projektach, pracę rozpoczniemy od za-


instalowania wymienionej biblioteki i pakietu:
https://github.com/l3pp4rd/DoctrineExtensions
https://github.com/stof/StofDoctrineExtensionsBundle

Zachowania Doctrine będziemy wykorzystywali niemal we wszystkich projektach, więc


przygotujmy nową dystrybucję Symfony 2 oznaczoną nazwą symfony2-customized-v3.

Przykład 21.1. Przygotowanie


dystrybucji symfony2-customized-v3
zawierającej pakiet
StofDoctrineExtensionsBundle
Przygotuj dystrybucję symfony2-customized-v3.zip, która będzie umożliwiała korzystanie
z zachowań Doctrine zawartych w bibliotece DoctrineExtensions.

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

Krok 2. Dodaj nowe pakiety w pliku deps


Na końcu pliku [projekt]/deps dodaj pakiety wymienione na listingu 21.1.

Listing 21.1. Zawartość, którą należy dodać na końcu pliku deps


[gedmo-doctrine-extensions]
git=http://github.com/l3pp4rd/DoctrineExtensions.git
version=v2.3.0
Rozdział 21. ♦ Instalacja i konfiguracja rozszerzeń DoctrineExtensions 271

[DoctrineExtensionsBundle]
git=http://github.com/stof/StofDoctrineExtensionsBundle.git
target=/bundles/Stof/DoctrineExtensionsBundle
version=1.0.2

Krok 3. Pobierz pakiety


Wydaj komendę:
php bin/vendors install --reinstall

Krok 4. Usuń foldery .git


Uruchom konsolę bash i przejdź do folderu symfony2-customized-v3/. Wydaj w nim
komendę:
find vendor -name .git -type d -exec rm -fr {} \;

Krok 5. Zarejestruj pakiet


W pliku AppKernel.php zarejestruj pakiet StofDoctrineExtensionsBundle. Kod ilustrujący,
jak to wykonać, jest przedstawiony na listingu 21.2.

Listing 21.2. Rejestracja pakietu StofDoctrineExtensionsBundle w pliku AppKernel.php


...
$bundles = array(
...
new JMS\SecurityExtraBundle\JMSSecurityExtraBundle(),
new Symfony\Bundle\DoctrineFixturesBundle\DoctrineFixturesBundle(),
new Stof\DoctrineExtensionsBundle\StofDoctrineExtensionsBundle(),
);
...

Krok 6. Zarejestruj przestrzenie nazewnicze Stof oraz Gedmo


W pliku autoload.php zarejestruj przestrzenie nazewnicze Stof oraz Gedmo. Kod ilustrujący,
jak to wykonać, jest przedstawiony na listingu 21.3.

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

Krok 7. Zmodyfikuj konfigurację projektu


W pliku app/config/config.yml wprowadź modyfikacje2 przedstawione na listingu 21.4.

Listing 21.4. Modyfikacja konfiguracji projektu


...
# Doctrine Configuration
doctrine:
dbal:
driver: %database_driver%
host: %database_host%
port: %database_port%
dbname: %database_name%
user: %database_user%
password: %database_password%
charset: UTF8

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

Pakiet StofDoctrineExtensionsBundle tworzy automatycznie w bazie danych dwie


dodatkowe tabele:
ext_translations
ext_log_entries
Odpowiada za to opcja konfiguracyjna w pliku app/config/config.yml:
StofDoctrineExtensionsBundle: ~

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

Krok 8. Skompresuj folder symfony2-customized-v3/


Skompresuj zawartość folderu symfony2-customized-v3/ do pliku symfony2-customized-
-v3.zip.

W celu zapewnienia możliwości używania zachowań Doctrine wystarczy zainstalowanie


biblioteki DoctrineExtensions, bez pakietu DoctrineExtensionsBundle, jednak
konfiguracja będzie wówczas przebiegała w inny sposób.

W pliku deps należy dodać wyłącznie bibliotekę DoctrineExtensions:


[gedmo-doctrine-extensions]
git=http://github.com/l3pp4rd/DoctrineExtensions.git

W pliku autoload.php należy włączyć przestrzeń nazewniczą Gedmo:


$loader->registerNamespaces(array(
...
'Metadata' => __DIR__.'/../vendor/metadata/src',
'Gedmo' => __DIR__.'/../vendor/DoctrineExtensions/lib',
));
W pliku AppKernel.php nie wprowadzamy żadnych zmian.

W pliku konfiguracyjnym config.yml dodajemy konfigurację konkretnego zachowania,


np. sluggable:
services:
my.listener:
class: Gedmo\Sluggable\SluggableListener
tags:
- { name: doctrine.event_listener, event: onFlush }
274 Część V ♦ Zachowania Doctrine
Rozdział 22.
Zachowanie sluggable

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

W powyższym adresie identyfikatorem slug jest ciąg:


symfony-2-0-od-podstaw

Tabela 22.1 zawiera kilka przykładowych ciągów i odpowiadających im wartości slug.

Tabela 22.1. Przykładowe teksty i odpowiadające im ciągi slug


Tekst slug
Czerwone maki czerwone-maki
Deszcz, jesienny deszcz deszcz-jesienny-deszcz
Rozszumiały się wierzby płaczące rozszumialy-sie-wierzby-placzace
Żółw zolw
ĄĆĘ — ogonki w Internecie ace-ogonki-w-internecie
Za 10 minut 13 za-10-minut-13
…and Justice for All… and-justice-for-all

Wartości zawarte w tabeli 22.1 powstają przez wykonanie następujących konwersji:


 usunięcie polskich znaków diakrytycznych (litera „ą” jest zastępowana przez „a”,
litera „ć” — przez „c” itd.),
 zamiana liter dużych na małe,

1
Por. Wikipedia: http://en.wikipedia.org/wiki/Slug_(web_publishing).
276 Część V ♦ Zachowania Doctrine

 konwersja znaków różnych od liter i cyfr na znak -,


 usunięcie wiodących i końcowych znaków -,
 zamiana wielokrotnych znaków - w pojedynczy znak -.

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;

następnie dodajemy właściwość $slug, której wartość będzie automatycznie generowana


na podstawie wybranej kolumny, np. title:
/**
* @var string $slug
*
* @Gedmo\Slug(fields={"title"})
* @ORM\Column(length=255, unique=true)
*/
private $slug;

Po dodaniu w klasie modelu powyższych zmian należy wygenerować metody getSlug()


oraz setSlug(). W tym celu wydajemy polecenie:
php app/console generate:doctrine:entities My

Następnie aktualizujemy strukturę bazy danych:


php app/console doctrine:schema:drop --force
php app/console doctrine:schema:create

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);

wówczas w kolumnie slug zostanie automatycznie wygenerowany identyfikator o wartości:


zolty-zolw
Rozdział 22. ♦ Zachowanie sluggable 277

Przykład 22.1. Wyrazy


— test zachowania sluggable
Wykorzystując Symfony 2, wykonaj aplikację, która będzie zawierała skrypt wypeł-
niający bazę danych na podstawie pliku tekstowego dane-testowe.txt. Plik ten znaj-
dziesz w archiwum 22-start.zip.

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.

Do generowania identyfikatorów slug wykorzystaj zachowanie sluggable.

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.

Krok 2. Utwórz bazę danych words


Na bazie skryptu z listingu 17.1 przygotuj skrypt tworzący pustą bazę danych o nazwie
words oraz konto dostępu editor. Wykonaj przygotowany skrypt SQL, po czym za po-
mocą programu phpMyAdmin sprawdź poprawność tworzenia bazy.

Krok 3. Ustal parametry połączenia z bazą danych


W pliku konfiguracyjnym app/config/parameters.ini wprowadź nazwę bazy danych,
konto dostępu oraz hasło.

Krok 4. Wygeneruj klasę dostępu do bazy danych


Poleceniem:
php app/console generate:doctrine:entity

wygeneruj model o logicznej nazwie:


MyFrontendBundle:Word

zawierający pole:
 title typu string o długości 255 znaków.
278 Część V ♦ Zachowania Doctrine

Po wydaniu powyższej komendy utworzony zostanie plik src/My/FrontendBundle/


Entity/Word.php.

Krok 5. W klasie Word dodaj właściwość slug


W klasie Word dodaj następującą instrukcję use:
use Gedmo\Mapping\Annotation as Gedmo;

oraz właściwość $slug:


/**
* @Gedmo\Slug(fields={"title"})
* @ORM\Column(length=128, unique=true)
*/
private $slug;

Zarys zmodyfikowanego pliku Word.php jest przedstawiony na listingu 22.1.

Listing 22.1. Dodanie właściwości slug w klasie Word


<?php

namespace My\FrontendBundle\Entity;

use Doctrine\ORM\Mapping as ORM;


use Gedmo\Mapping\Annotation as Gedmo;

/**
* My\FrontendBundle\Entity\Word
*
* @ORM\Table()
* @ORM\Entity
*/
class Word
{
...

/**
* @Gedmo\Slug(fields={"title"})
* @ORM\Column(length=128, unique=true)
*/
private $slug;

...
}

Krok 6. W konfiguracji włącz zachowania sluggable


W pliku app/config/config.yml ustal wartość opcji sluggable na true:
stof_doctrine_extensions:
orm:
default:
sluggable: true
Rozdział 22. ♦ Zachowanie sluggable 279

Krok 7. Utwórz tabelę word


Wydaj polecenie:
php app/console doctrine:schema:update --force

Poprawność tworzenia tabeli sprawdź za pomocą programu phpMyAdmin.

Krok 8. Wypełnij bazę danych zawartością pliku tekstowego


Przygotuj przedstawiony na listingu 22.2 plik LoadData.php, a następnie wydaj polecenie:
php app/console doctrine:fixtures:load

Listing 22.2. Plik LoadData.php z przykładu 22.1


<?php
namespace My\FrontendBundle\DataFixtures\ORM;
use Doctrine\Common\DataFixtures\FixtureInterface;
use Doctrine\Common\Persistence\ObjectManager;
use My\FrontendBundle\Entity\Word;
class LoadData implements FixtureInterface
{
function load(ObjectManager $manager)
{
$plk = file('data/dane-testowe.txt');
foreach ($plk as $l) {
$Wyraz = new Word();
$Wyraz->setTitle($l);
$manager->persist($Wyraz);
$manager->flush();
}
}
}

Po wykonaniu skryptu z listingu 22.2. baza danych words powinna zawierać w tabeli word
20 rekordów przedstawionych na rysunku 22.1.

Zwróć uwagę, że identyfikatory slug są poprawnie generowane dla wyrazów we wszyst-


kich językach:
 polskich: Miś, Żółw;
 francuskich: à bientôt, Gâteau;
 niemieckich: Kränklichkeit, Götze, Possenreißer;
 rosyjskich: трепещу, подошва.

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

Parametry adnotacji konfigurujących


wartości slug
W celu wygenerowania identyfikatora slug na podstawie kilku kolumn należy w roli pa-
rametru fields użyć tablicy zawierającej nazwy właściwości klasy Entity. Parametr
length pozwala natomiast ograniczyć długość generowanych identyfikatorów. Kod z li-
stingu 22.3 włącza generowanie identyfikatorów slug o długości 6 znaków, opartych na
kolumnach imie i nazwisko.

Listing 22.3. Konfiguracja identyfikatorów slug o długości do 6 znaków, generowanych na podstawie


kolumn imie i nazwisko
/**
* @var string $slug
*
* @Gedmo\Slug(fields={"imie", "nazwisko"})
* @ORM\Column(length=6, unique=true)
*/
private $slug;
Rozdział 23.
Zachowanie
timestampable
Zachowania timestampable ułatwiają definiowanie w bazie danych pól zawierających
informacje o dacie utworzenia oraz modyfikacji rekordu. W celu użycia zachowań należy
najpierw włączyć je w pliku konfiguracyjnym config.yml. Włączenie konfiguracji zacho-
wań timestampable jest przedstawione na listingu 23.1.

Listing 23.1. Włączenie zachowań timestampable


stof_doctrine_extensions:
orm:
default:
timestampable: true

Następnie w klasach Entity dodajemy właściwości $updated oraz $created oznaczone


adnotacjami @Gedmo\Timestampable. Przykład użycia adnotacji @Gedmo\Timestampable
jest przedstawiony na listingu 23.2.

Listing 23.2. Adnotacje konfigurujące zachowanie timestampable


/**
* @var datetime $created
*
* @Gedmo\Timestampable(on="create")
* @ORM\Column(type="date")
*/
private $created;

/**
* @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").

Przykład 23.1. Wyrazy


— test zachowania timestampable
Zmodyfikuj przykład 22.1. tak, by każdy rekord w bazie danych zawierał pola created
oraz updated określające czas utworzenia oraz ostatniej modyfikacji rekordu. Wykorzy-
staj do tego zachowania Doctrine. Poprawność wstawiania rekordów do bazy sprawdź za
pomocą programu phpMyAdmin.

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

Krok 2. Włącz dostępność zachowań timestampable


W pliku app/config/config.yml ustal wartość opcji timestampable na true.

Krok 3. Utwórz tabelę word


Wydaj polecenie:
php app/console doctrine:schema:update --force

Poprawność tworzenia tabeli sprawdź za pomocą programu phpMyAdmin.

Krok 4. Wypełnij bazę danych zawartością pliku tekstowego


Wydaj polecenie:
php app/console doctrine:fixtures:load

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.

Listing 24.1. Włączenie zachowań translatable


doctrine:
orm:
mappings:
StofDoctrineExtensionsBundle: ~

stof_doctrine_extensions:
default_locale: pl_PL
orm:
default:
translatable: true

Jeśli po dodaniu w konfiguracji wpisu:


StofDoctrineExtensionsBundle: ~

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.

Listing 24.2. Właściwość $locale oraz metoda setTranslatableLocale() w klasie Entity


/**
* @Gedmo\Locale
*/
private $locale = 'pl_PL';
284 Część V ♦ Zachowania Doctrine

public function setTranslatableLocale($locale)


{
$this->locale = $locale;
}

Na zakończenie w klasach Entity musimy oznaczyć kolumny, które będą podlegały


tłumaczeniu. Służy do tego adnotacja @Gedmo\Translatable. Przykład użycia jest przed-
stawiony na listingu 24.3.

Listing 24.3. Użycie adnotacji @Gedmo\Translatable


/**
* @var string $name
*
* @ORM\Column(name="name", type="string", length=255)
* @Gedmo\Translatable
*/
private $name;

Wstawianie tłumaczeń do bazy danych


Przyjmijmy, że w bazie danych występuje tabela color oraz że dysponujemy klasą Color,
która zapewnia dostęp do tej tabeli. Dodatkowo przyjmijmy, że w tabeli color wystąpi
widoczna na listingu 24.3 właściwość name.

W bazie danych chcemy zapisać jeden rekord:


czerwony

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.

Zachowanie translatable umożliwia tłumaczenie dowolnych właściwości z dowolnych


klas Entity na dowolną liczbę języków.

Listing 24.4. Wstawianie do bazy danych dwóch tłumaczeń jednego rekordu


$Color = new Color();
$Color->setName('czerwony');
$manager->persist($Color);
$manager->flush();

$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');

Kolejne wywołania metod set() w odniesieniu do pól podlegających tłumaczeniu


(np. setName()) będą dotyczyły ustalonego języka. Instrukcje:
$Color->setName('red');
$manager->persist($Color);
$manager->flush();

spowodują zapisanie słowa red w tabeli ext_translations. Po wykonaniu kodu z listingu


24.4 w tabeli color pojawi się rekord z rysunku 24.1, a w tabeli ext_translations — dwa
pierwsze rekordy z rysunku 24.2.

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

Struktura tabeli ext_translations nie zależy od struktury tabel, których właściwości


będziemy poddawali tłumaczeniu. Zawiera ona następujące kolumny:
 Kolumna id jest kluczem głównym.
 Kolumna locale zawiera oznaczenie języka, w jakim zapisano dane
tłumaczenie.
 Kolumna object_class zawiera nazwę klasy modelu.
 Kolumna field zawiera nazwę tłumaczonej właściwości.
 Kolumna foreign_key zawiera wartość klucza obcego z tabeli odpowiadającej
obiektowi object_class.
 Kolumna content zawiera tekst w języku locale. Tekst ten jest tłumaczeniem
właściwości field rekordu, którego klucz główny ma wartość foreign_key w tabeli
generowanej dla obiektu object_class.
286 Część V ♦ Zachowania Doctrine

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

(czyli w tabeli 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);

Następnie ustalamy, że kolumny podlegające tłumaczeniu (czyli opatrzone adnotacjami


@Gedmo\Translatable) mają być przetłumaczone na odpowiedni język:
$entity->setTranslatableLocale('fr');
$em->refresh($entity);

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.

Przykład 24.1. Kolory


— test zachowania timestampable
Dany jest plik kolory.yml, którego początkowy fragment został przedstawiony na li-
stingu 24.5.

Listing 24.5. Plik kolory.yml


czerwony:
rgb: FF0000
name: czerwony
translations:
pl: czerwony
en: red
it: rosso
fr: rouge
de: rot
es: rojo
ru:
Rozdział 24. ♦ Zachowanie translatable 287

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:
 красный,
 голубой.

Wykorzystaj omówione zachowanie translatable.

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.

Krok 2. Utwórz bazę danych colors


Na bazie skryptu z listingu 17.1 przygotuj skrypt tworzący pustą bazę danych o nazwie
colors oraz konto dostępu editor. Wykonaj przygotowany skrypt SQL, po czym za po-
mocą programu phpMyAdmin sprawdź poprawność tworzenia bazy.

Krok 3. Ustal parametry połączenia z bazą danych


W pliku konfiguracyjnym app/config/parameters.ini wprowadź nazwę bazy danych,
konto dostępu oraz hasło.

Krok 4. Wygeneruj klasę dostępu do bazy danych


Poleceniem:
php app/console generate:doctrine:entity
288 Część V ♦ Zachowania Doctrine

wygeneruj model o logicznej nazwie:


MyFrontendBundle:Color

Zawierający pola:
 rgb typu string o długości 16 znaków,
 name typu string o długości 255 znaków.

Po wydaniu powyższej komendy utworzony zostanie plik src/My/FrontendBundle/


Entity/Color.php.

Krok 5. Zmodyfikuj klasę Color


W klasie Color dodaj instrukcję use:
use Gedmo\Mapping\Annotation as Gedmo;

oraz widoczne na listingach 24.2 i 24.3:


 właściwość $locale,
 metodę setTranslatableLocale()
 oraz adnotację @Gedmo\Translatable.

Krok 6. Zmodyfikuj konfigurację projektu


W pliku app/config/config.yml wprowadź modyfikację z listingu 24.1.

Krok 7. Utwórz tabelę w bazie danych


Wydaj polecenie:
php app/console doctrine:schema:update --force

Krok 8. Wypełnij bazę danych zawartością pliku tekstowego


Przygotuj przedstawiony na listingu 24.6 plik LoadData.php, a następnie wydaj polecenie:
php app/console doctrine:fixtures:load

Listing 24.6. Plik LoadData.php z przykładu 24.1


<?php

namespace My\FrontendBundle\DataFixtures\ORM;

use Doctrine\Common\Persistence\ObjectManager;

use My\FrontendBundle\Entity\Color;
use Symfony\Component\Yaml\Yaml;

class LoadData implements FixtureInterface


{
Rozdział 24. ♦ Zachowanie translatable 289

public function load(ObjectManager $manager)


{
$yml = Yaml::parse('data/kolory.yml');
foreach ($yml as $c) {
$Color = new Color();
$Color->setRgb($c['rgb']);
$Color->setName($c['name']);
$manager->persist($Color);
$manager->flush();

foreach ($c['translations'] as $lang => $translation) {


$Color->setTranslatableLocale($lang);
$Color->setName($translation);
$manager->persist($Color);
$manager->flush();
}
}
}

Po odczytaniu pliku kolory.yml iteracyjnie przetwarzamy poszczególne kolory. Ustalamy


wartość RGB oraz nazwę i zapisujemy obiekt w bazie danych. Następnie iteracyjnie
przetwarzamy dane zawarte pod indeksem translations. Klucz $lang zawiera nazwę
języka (np. en), a wartość $translation jest tłumaczeniem (np. red).

Po wykonaniu skryptu z listingu 24.6. baza danych colors powinna zawierać w tabeli
ext_translations rekordy widoczne na rysunku 24.2.

Krok 9. Ustal regułę translacji adresu strony domowej


W pliku app/config/routing.yml dodaj regułę widoczną na listingu 24.7.

Listing 24.7. Reguła translacji zawierająca zmienną $culture

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

Parametr requirements nakłada ograniczenia na dopuszczone wartości zmiennej culture.


Dzięki temu poprawnymi adresami będą wyłącznie adresy zakończone jednym z wymie-
nionych języków, np:
290 Część V ♦ Zachowania Doctrine

.../web/pl
.../web/en
.../web/fr
...

Krok 10. Wykonaj akcję index


W pliku DefaultController.php wprowadź kod widoczny na listingu 24.8.

Listing 24.8. Kod akcji index


class DefaultController extends Controller
{
/**
* @Route("/")
* @Template()
*/
public function indexAction($culture)
{
$em = $this->getDoctrine()->getEntityManager();
$entities = $em->getRepository('MyFrontendBundle:Color')->findAll();

if ($culture != 'pl') {
foreach ($entities as $entity) {
$entity->setTranslatableLocale($culture);
$em->refresh($entity);
}
}
return array('entities' => $entities);
}
}

Nagłówek metody indexAction() zawiera parametr $culture:


public function indexAction($culture)

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.

Wewnątrz akcji index pobieramy wszystkie rekordy z tabeli color:


$em = $this->getDoctrine()->getEntityManager();
$entities = $em->getRepository('MyFrontendBundle:Color')->findAll();

Następnie sprawdzamy, czy wartość zmiennej $culture jest różna od wartości pl:
if ($culture != 'pl') {
...
}
Rozdział 24. ♦ Zachowanie translatable 291

Jeśli tak, to na każdym obiekcie z kolekcji $entities wykonujemy operacje set


´TranslatableLocale() oraz refresh():
foreach ($entities as $entity) {
$entity->setTranslatableLocale($culture);
$em->refresh($entity);
}

Parametrem metody setTranslatableLocale() jest zmienna $culture pochodząca z ad-


resu URL. W ten sposób kolekcja rekordów $entities zostanie przetłumaczona na wy-
brany język.

Krok 11. Wykonaj widok akcji index


W widoku akcji index wprowadź kod z listingu 24.9.

Listing 24.9. Widok akcji index


<body>
<ul>
<li><a href="{{ path('homepage', {'culture': 'pl'}) }}">pl</a></li>
<li><a href="{{ path('homepage', {'culture': 'es'}) }}">es</a></li>
...
</ul>
<table>
<thead>
<tr>
<th>lp.</th>
<th>RGB</th>
<th>Nazwa</th>
</tr>
</thead>
<tbody>
{% for entity in entities %}
<tr>
<td>{{ loop.index }}.</td>
<td>{{ entity.rgb }}</td>
<td>{{ entity.name }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</body>

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'}

kluczem jest nazwa zmiennej występującej w regule routingu z listingu 24.7.


292 Część V ♦ Zachowania Doctrine

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.

Adresy URL zawierające zmienne


Adresy URL zawierające zmienne tworzymy, umieszczając w regule konfiguracyjnej
pojemnik {} oraz dodając parametry metody akcji. Jeśli chcemy, by adres:
/web/moja/strona-123/index.html
powodował wykonanie akcji loremAction() i przekazywał do akcji zmienną o nazwie
ipsum i wartości 123, wówczas należy:
 w regule routingu dodać pojemnik {ipsum},
 w nagłówku metody akcji dodać parametr $ipsum.

Zarys metody akcji loremAction() jest przedstawiony na listingu 26.1.


298 Część VI ♦ Szczegółowe dane rekordu

Listing 26.1. Akcja obsługująca adres /moja/strona-123/index.html


/**
* @Route("/moja/strona-{lorem}/index.html", name="akcja_ipsum")
* @Template()
*/
public function ipsumAction($lorem)
{
...
}

Adnotacja @Route() z listingu 26.1 zawiera adres:


/moja/strona-{lorem}/index.html

oraz parametr:
name="akcja_ipsum

Konwersja wejściowa
Po odwiedzeniu adresu:
/moja/strona-xyz/index.html

wykonana zostanie metoda ipsumAction() z listingu 26.1. Parametr $lorem przekazany


do metody przyjmie wartość xyz.

Konwersja wyjściowa
W celu wydrukowania adresu:
/moja/strona-pqr123/index.html

należy w widoku wywołać funkcję pomocniczą path(), przekazując do niej nazwę


akcja_ipsum oraz tablicę zawierającą element o indeksie lorem i wartości pqr123:
<a href="{{ path('akcja_ipsum', {'lorem': 'pqr123'}) }}">

Wyszukiwanie pojedynczego rekordu


na podstawie klucza głównego
Do wyszukania pojedynczego rekordu w bazie danych na podstawie klucza głównego
służy poznana w rozdziale 19. metoda:
find()

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!');
}

Jeżeli natomiast rekord został odnaleziony, to przekazujemy go do widoku akcji:


return array('entity' => $entity);

Wyświetlanie właściwości rekordu


Jeśli tabela bazy danych ma kolumny id, name oraz title, wówczas w celu wydrukowania
właściwości obiektu $entity w widoku stosujemy instrukcje:
{{ entity.id }}
{{ entity.name }}
{{ entity.title }}

Jeśli w klasie Entity zdefiniowano metodę __toString() zwracającą wartość właści-


wości name, to instrukcję:
{{ entity.name }}

możemy równoważnie zapisać jako:


{{ entity }}

Przykład 26.1. Piosenki wojskowe


Dany jest plik songs.yml, którego początkowe wiersze przedstawiono na listingu 26.2.

Listing 26.2. Plik songs.yml


<?xml version="1.0" encoding="utf-8"?>
<songs>
<song>
<title>Deszcz, jesienny deszcz</title>
<contents>
Deszcz, jesienny deszcz
Smutne pieśni gra,
Mokną na nim karabiny,
Hełmy kryje rdza.
...
</contents>
</song>
<song>
300 Część VI ♦ Szczegółowe dane 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>

Wykonaj aplikację, która będzie zawierała na stronie głównej zestawienie tytułów


piosenek. Każdy tytuł ma być odsyłaczem do strony prezentującej tekst wybranej pio-
senki. Teksty piosenek zapisz w bazie danych.

Zadanie rozwiąż w taki sposób, by w aplikacji wykorzystane zostały adresy:


.../web/ strona zawierająca listę tytułów piosenek
.../web/1.html strona z tekstem pierwszej piosenki
.../web/2.html strona z tekstem drugiej piosenki
.../web/3.html strona z tekstem trzeciej piosenki
...

Liczby użyte w adresach (np. /web/3.html) mają być wartościami kluczy głównych odpo-
wiednich rekordów.

Wykonując zadanie, wykorzystaj szablon HTML/CSS oraz dane zawarte w pliku


26-start.zip.

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

utwórz pakiet My/FrontendBundle. Następnie utwórz folder zad-26/data/ i umieść w nim


plik songs.xml.

Krok 2. Utwórz bazę danych songs


Na bazie skryptu z listingu 17.1 przygotuj skrypt tworzący pustą bazę danych o nazwie
songs oraz konto dostępu editor. W pliku konfiguracyjnym parameters.ini wprowadź
nazwę bazy danych, konto dostępu oraz hasło.

Krok 4. Wygeneruj klasę dostępu do bazy danych


Wygeneruj model MyFrontendBundle:Song zawierający pola:
 title typu string o długości 255 znaków,
 contents typu text.

Krok 5. Rozszerz funkcjonalność klasy Song


W klasie Song dodaj metody:
 __toString(),
 fromArray().

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.

Krok 6. Wygeneruj i dostosuj klasę SongRepository


W klasie Song zmodyfikuj adnotację Entity:
@ORM\Entity(repositoryClass="My\FrontendBundle\Entity\SongRepository")

po czym wydaj polecenie:


php app/console doctrine:generate:entities My

W klasie SongRepository.php dodaj przedstawioną na listingu 19.7 metodę findAll().


Zmodyfikuj drugi parametr wywoływanej metody findBy(). Tablica zwracanych obiek-
tów ma być posortowana alfabetycznie względem kolumny title:
return $this->findBy(array(), array('title' => 'ASC'));

Krok 7. Utwórz tabelę song


Wydaj polecenie:
php app/console doctrine:schema:update --force

W bazie danych songs pojawi się tabela song.


302 Część VI ♦ Szczegółowe dane rekordu

Krok 8. Wypełnij bazę danych zawartością odczytaną z pliku XML


Przygotuj przedstawiony na listingu 26.3 plik LoadData.php, a następnie wydaj polecenie:
php app/console doctrine:fixtures:load

Listing 26.3. Plik LoadData.php z przykładu 26.1


class LoadData implements FixtureInterface
{
function load(ObjectManager $manager)
{
$xml = simplexml_load_file('data/songs.xml');
foreach ($xml->song as $s) {
$data = (array) $s;
$Song = new Song();
$Song->fromArray($data);
$manager->persist($Song);
}
$manager->flush();
}
}

Po wykonaniu skryptu z listingu 26.3. dla danych z pliku songs.xml baza danych powinna
zawierać 9 rekordów.

Krok 9. Przygotuj skórkę aplikacji


Na podstawie szablonu zawartego w pliku 26-start.zip wykonaj przedstawione na listin-
gach 26.4 oraz 26.5 widoki base.html.twig oraz layout.html.twig. Oba pliki umieść w fol-
derze app/Resources/views/.

Listing 26.4. Widok base.html.twig


<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>{% block title %}Piosenki wojskowe{% endblock %}</title>
<link rel="stylesheet" href="{{ asset('css/style.css') }}" />
</head>
<body>
{% block body %}{% endblock %}
</body>
</html>

Listing 26.5. Widok layout.html.twig


{% extends '::base.html.twig' %}

{% block body %}
<div id="pojemnik">
<h1 id="logo"><a href="{{ path('homepage') }}">Piosenki wojskowe</a></h1>
Rozdział 26. ♦ Akcja show 303

{% block content %}{% endblock %}


</div>
{% endblock %}

W folderach zad-26/web/css/ oraz zad-26/web/images/ umieść style CSS oraz pliki


graficzne.

Krok 10. Dostosuj akcję index


W kontrolerze DefaultController.php zmodyfikuj metodę indexAction(). Wprowadź kod
przedstawiony na listingu 26.6.

Listing 26.6. Kod akcji index


/**
* @Route("/", name="homepage")
* @Template()
*/
public function indexAction()
{
$em = $this->getDoctrine()->getEntityManager();
$entities = $em->getRepository('MyFrontendBundle:Song')->findAll();
return array('entities' => $entities);
}

Zwróć uwagę, że adnotacja @Route() zawiera parametr name o wartości homepage. To


dzięki niemu możemy na listingu 26.5 użyć wywołania:
<a href="{{ path('homepage') }}">Piosenki wojskowe</a>

Krok 11. Dostosuj widok akcji index


W widoku akcji index wprowadź kod z listingu 26.7.

Listing 26.7. Widok akcji index


{% extends "::layout.html.twig" %}

{% 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

Krok 12. Wykonaj akcję show


W kontrolerze DefaultController.php dodaj przedstawioną na listingu 26.8 akcję
showAction().

Listing 26.8. Kod akcji show


/**
* @Route("/{id}.html", name="song_show")
* @Template()
*/
public function showAction($id)
{
$em = $this->getDoctrine()->getEntityManager();
$entity = $em->getRepository('MyFrontendBundle:Song')->find($id);

if (!$entity) {
throw $this->createNotFoundException('Podana strona nie istnieje!');
}

return array('entity' => $entity);


}

Adnotacja @Route() zawiera pojemnik {id} oraz parametr name o wartości show_page:
@Route("/{id}.html", name="song_show")

Pojemnik {id} występujący w adnotacji @Route() decyduje o tym, że parametrem metody


akcji showAction() jest $id:
public function showAction($id)

Parametr name pozwala natomiast na stosowanie na listingu 26.7 wywołań:


<a href="{{ path('song_show', { 'id': entity.id }) }}">

Krok 13. Dostosuj widok akcji show


W pliku widoku akcji show wprowadź kod z listingu 26.9.

Listing 26.9. Widok akcji show


{% extends "::layout.html.twig" %}

{% block html_title %}
{{ entity }}
{% endblock %}

{% block content %}
<div id="tresc">
<h2>{{ entity }}</h2>
<p>
{{ entity.contents|nl2br }}
</p>
Rozdział 26. ♦ Akcja show 305

<p><a href="{{ path('homepage') }}">powrót</a>

</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 }}

Pierwsze z nich wywoła metodę __toString() klasy Song, co spowoduje wydrukowanie


tytułu wybranej piosenki. Druga instrukcja wywoła metodę getContents() klasy Song.

Krok 14. Sprawdź działanie aplikacji


Odwiedź w przeglądarce adres:
.../web/

Powinieneś ujrzeć witrynę przedstawioną na rysunku 26.1.

Rysunek 26.1. Witryna z przykładu 26.1


306 Część VI ♦ Szczegółowe dane rekordu
Rozdział 27.
Identyfikacja rekordu
na podstawie
wartości slug
Adresy URL opisane w poprzednim rozdziale wykorzystywały wartości kluczy głównych.
Adres:
/web/987.html
wskazywał piosenkę, dla której wartość zapisana w kolumnie id wynosi 987. Teraz przej-
dziemy do omówienia zagadnień, których znajomość pozwoli nam w miejsce adresu:
/web/987.html
użyć:
/web/kolysanka-lesna.html

W tym celu połączymy poznane wcześniej zagadnienia, czyli:


 zachowania sluggable,
 wyszukiwanie w bazie danych rekordów na podstawie podanej wartości
wybranej kolumny.

Zachowania sluggable spowodują, że każdy rekord dodawany do bazy danych będzie


miał automatycznie wygenerowany identyfikator slug. Do wyszukiwania rekordów na
podstawie zadanej wartości slug użyjemy natomiast omówionej w rozdziale 19. metody
findOneBySlug().
308 Część VI ♦ Szczegółowe dane rekordu

Przykład 27.1. Piosenki wojskowe


— użycie identyfikatorów slug
Zmodyfikuj przykład 26.1 w taki sposób, by stosował adresy URL wykorzystujące
identyfikatory slug. Dla piosenek o tytułach:
Czerwone maki
Deszcz, jesienny deszcz
Dnia pierwszego września
...

Użyj adresów:
.../web/czerwone-maki.html
.../web/deszcz-jesienny-deszcz.html
.../web/dnia-pierwszego-wrzesnia.html
...

Zadanie rozwiąż, wykorzystując zachowanie sluggable oprogramowane w rozszerzeniu


DoctrineExtensions.

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.

Krok 2. Skonfiguruj zachowanie sluggable


W pliku konfiguracyjnym app/config/config.yml zmodyfikuj wpis włączający zacho-
wanie sluggable:
stof_doctrine_extensions:
orm:
default:
sluggable: true

Następnie w pliku Song.php dodaj instrukcję use oraz właściwość $slug:


use Gedmo\Mapping\Annotation as Gedmo;
...
/**
* @Gedmo\Slug(fields={"title"})
* @ORM\Column(length=128, unique=true)
*/
private $slug;
Rozdział 27. ♦ Identyfikacja rekordu na podstawie wartości slug 309

Na zakończenie wygeneruj metody1 getSlug() oraz setSlug():


php app/console generate:doctrine:entities My

Krok 3. Uaktualnij strukturę i zawartość bazy danych


Wykonaj polecenia:
php app/console doctrine:schema:update --force
php app/console doctrine:fixtures:load

Po wykonaniu powyższych poleceń baza danych powinna zawierać 9 rekordów. Po od-


wiedzeniu strony tabeli song powinieneś ujrzeć, że są w niej zawarte wygenerowane
wartości slug. Wygląd tabeli song jest przedstawiony na rysunku 27.1.

Rysunek 27.1. Tabela song zawiera automatycznie wygenerowane wartości slug

Krok 4. Dostosuj widok akcji index


W widoku akcji index zmodyfikuj instrukcję generującą adresy URL zawarte w menu.
Tablica przekazana do funkcji path() powinna zawierać parametr slug. Zarys kodu
widoku akcji index jest przedstawiony na listingu 27.1.

Listing 27.1. Zarys widoku akcji index


...
{% for entity in entities %}
<li>
<a href="{{ path('song_show', { 'slug': entity.slug }) }}">
{{ entity }}
</a>
</li>
{% endfor %}
...

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

Krok 5. Zmodyfikuj kod akcji show


W akcji show zmień metodę wyszukiwania rekordu. Zmodyfikowana metoda showAction()
jest przedstawiona na listingu 27.2.

Listing 27.2. Kod akcji show


/**
* @Route("/{slug}.html", name="song_show")
* @Template()
*/
public function showAction($slug)
{
$em = $this->getDoctrine()->getEntityManager();
$entity = $em->getRepository('MyFrontendBundle:Song')->findOneBySlug($slug);
if (!$entity) {
throw $this->createNotFoundException('Podana strona nie istnieje!');
}
return array('entity' => $entity);
}

Krok 6. Sprawdź działanie aplikacji


Odwiedź w przeglądarce adres:
.../web/

Powinieneś ujrzeć witrynę przedstawioną na rysunku 26.1. Sprawdź w kodzie HTML,


jak wyglądają wygenerowane adresy URL. Po wykonaniu w przeglądarce internetowej
operacji Źródło strony ujrzysz kod zbliżony do listingu 27.3.

Listing 27.3. Kod menu generowany na stronie głównej przykładu 27.1


<ul id="menu">
<li>
<a href=".../web/czerwone-maki.html">
Czerwone maki
</a>
</li>
<li>
<a href=".../web/deszcz-jesienny-deszcz.html">
Deszcz, jesienny deszcz
</a>
</li>
<li>
<a href=".../web/dnia-pierwszego-wrzesnia.html">
Dnia pierwszego września
</a>
</li>
...
</ul>
Rozdział 28.
Generowanie menu
na podstawie zawartości
bazy danych
W celu wygenerowania menu na podstawie zawartości bazy danych należy przygoto-
wać metodę akcji oraz jej widok. W metodzie akcji pobierzemy z bazy danych i przeka-
żemy do widoku rekordy, na podstawie których ma powstać menu. W widoku kolekcję
rekordów przetworzymy w listę odsyłaczy.

Tak wykonane menu dołączymy do układu strony, umieszczając w pliku layout.html.twig


wywołanie:
{% render "MyFrontendBundle:Default:menu" %}

Przykład 28.1. Treny


W archiwum 28-start.zip znajdziesz pliki tekstowe z treścią trenów Jana Kochanow-
skiego oraz dokument dedykacja.txt, który zawiera słowo wstępne. Pliki z treścią trenów
nazywają się 01.txt, 02.txt, 03.txt itd. Każdy z nich zawiera w pierwszym wierszu tytuł
utworu, a w kolejnych wierszach — treść.

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.

Na stronie głównej umieść tekst pochodzący z pliku dedykacja.txt. W aplikacji użyj


przyjaznych adresów URL zawierających identyfikatory slug.

Szablon HTML/CSS znajdziesz w pliku 28-start.zip.


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

Krok 2. Utwórz bazę danych treny


Utwórz pustą bazę danych o nazwie treny oraz konto dostępu editor. W pliku konfigu-
racyjnym parameters.ini wprowadź dane dostępu do bazy.

Krok 3. Wygeneruj klasę dostępu do bazy danych


Wygeneruj model MyFrontendBundle:Tren zawierający pola:
 tytul typu string o długości 64 znaków,
 tresc typu text,
 numer typu integer.

Krok 4. Rozszerz funkcjonalność modelu


W klasie Tren dodaj:
 instrukcję use dołączającą przestrzeń nazewniczą adnotacji;
 adnotację odpowiedzialną za generowanie klasy Repository;
 właściwość $slug, której wartość będzie automatycznie generowana na
podstawie właściwości tytul;
 metodę __toString(), która będzie zwracała tytuł trenu.

Krok 5. Wygeneruj i dostosuj klasę TrenRepository


Wydaj polecenie:
php app/console doctrine:generate:entities My

W klasie TrenRepository.php dodaj metodę findAll(), która będzie zwracała rekordy


posortowane rosnąco względem kolumny numer.

Krok 6. Utwórz tabelę tren


Wydaj polecenie:
php app/console doctrine:schema:update --force

W bazie danych treny pojawi się tabela tren.


Rozdział 28. ♦ Generowanie menu na podstawie zawartości bazy danych 313

Krok 7. Wypełnij bazę danych zawartością odczytaną z plików tekstowych


Przygotuj przedstawiony na listingu 28.1 plik LoadData.php, a następnie wydaj polecenie:
php app/console doctrine:fixtures:load

Listing 28.1. Plik LoadData.php z przykładu 28.1


class LoadData implements FixtureInterface
{
function load(ObjectManager $manager)
{
$plks = glob('data/treny/*.txt');
shuffle($plks);
foreach ($plks as $plk) {
$t = file($plk);
$tytul = trim(array_shift($t));
$tresc = trim(implode('', $t));

$numer = basename($plk);
$numer = str_replace('.txt', '', $numer);
$numer = ltrim($numer, '0');

$Tren = new Tren();


$Tren->setTytul($tytul);
$Tren->setTresc($tresc);
$Tren->setNumer($numer);

$manager->persist($Tren);
}
$manager->flush();
}
}

Po wykonaniu skryptu z listingu 28.1 baza danych powinna zawierać 19 rekordów.

W skrypcie z listingu 28.1 wyszukujemy wszystkie pliki tekstowe z folderu data/treny/.


Otrzymaną tablicę przekształcamy funkcją shuffle(), która ustala losowy porządek
elementów tablicy:
$plks = glob('data/treny/*.txt');
shuffle($plks);

W ten sposób sprawdzamy poprawność kodu sortującego rekordy.

Odnalezione pliki przetwarzamy w pętli:


foreach ($plks as $plk) {
...
}

Najpierw odczytujemy plik:


$t = file($plk);
314 Część VI ♦ Szczegółowe dane rekordu

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));

to w zmiennej $tresc otrzymamy treść trenu pozbawioną tytułu.

Nazwa pliku z treścią trenu ma postać ../data/treny/05.txt. Funkcją basename() usuwamy


z nazwy foldery, otrzymując samą nazwę pliku (np. 05.txt):
$numer = basename($plk);

Następnie funkcją str_replace() usuwamy rozszerzenie nazwy pliku (otrzymamy np. 05):
$numer = str_replace('.txt', '', $numer);

Na zakończenie usuwamy wiodące zera (otrzymamy np. 5):


$numer = ltrim($numer, '0');

Po przygotowaniu trzech zmiennych:


$tytul
$tresc
$numer

tworzymy nowy obiekt klasy Tren i zapisujemy go w bazie danych.

Krok 8. Przygotuj skórkę aplikacji


Na podstawie szablonu zawartego w pliku 28-start.zip wykonaj przedstawione na listin-
gach 28.2 oraz 28.3 widoki base.html.twig oraz layout.html.twig. Oba pliki umieść w folde-
rze app/Resources/views/.

Zwróć uwagę, że menu główne witryny jest generowane w pliku layout.html.twig wy-
wołaniem {% render ...%}.

Listing 28.2. base.html.twig


<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>{% block title %}Jan Kochanowski: Treny{% endblock %}</title>
<link rel="stylesheet" href="{{ asset("css/style.css") }}" />
</head>
<body>
{% block body %}{% endblock %}
</body>
</html>
Rozdział 28. ♦ Generowanie menu na podstawie zawartości bazy danych 315

Listing 28.3. Widok layout.html.twig


{% extends "::base.html.twig" %}
{% block body %}
<div id="pojemnik">
<h1>Jan Kochanowski</h1>
<h2><a href="{{ path('homepage') }}">Treny</a></h2>
{% render "MyFrontendBundle:Default:menu" %}
<div id="tresc">
{% block contents %}
{% endblock %}
</p>
</div>
<br class="clear" />
</div>
{% endblock %}

W folderze zad-28/web/css/ umieść style CSS.

Krok 9. Dostosuj kontroler Default


W kontrolerze DefaultController.php wykonaj trzy akcje:
 index,
 show,
 menu.

Pierwsza z nich ma wyświetlać tekst z pliku1 dedykacja.txt, druga — treść wybranego


trenu, a trzecia — menu zawierające tytuły wszystkich trenów. Treść zmodyfikowanego
pliku DefaultController.php jest widoczna na listingu 28.4.

Listing 28.4. Trzy akcje kontrolera DefaultController


class DefaultController extends Controller
{
/**
* Homepage
*
* @Route("/", name="homepage")
* @Template()
*/
public function indexAction()
{
return array();
}
/**
* Finds and displays a Tren entity.
*
* @Route("/{slug}.html", name="tren_show")
* @Template()
*/

1
Treść pliku dedykacja.txt umieścimy w widoku akcji.
316 Część VI ♦ Szczegółowe dane rekordu

public function showAction($slug)


{
$em = $this->getDoctrine()->getEntityManager();
$entity = $em->getRepository('MyFrontendBundle:Tren')->findOneBySlug($slug);
if (!$entity) {
throw $this->createNotFoundException('Podana strona nie istnieje!');
}
return array('entity' => $entity);
}

/**
* 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);
}
}

Rekordy, na podstawie których powstaje menu, są pobierane z bazy danych w akcji


menuAction() metodą findAll():
$entities = $em->getRepository('MyFrontendBundle:Tren')->findAll();

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.

Krok 10. Wykonaj widoki akcji


Przygotuj przedstawione na listingach 28.5, 28.6 i 28.7 widoki akcji index, show i menu.

Listing 28.5. Widok akcji index (plik index.html.twig)


{% extends "::layout.html.twig" %}
{% block contents %}
<p>
Tales sunt hominum mentes...
...
</p>
{% endblock %}

Listing 28.6. Widok akcji show (plik show.html.twig)


{% extends "::layout.html.twig" %}
{% block title %}Jan Kochanowski: {{ entity }} {% endblock %}
{% block contents %}
<h3>{{ entity }}</h3>
Rozdział 28. ♦ Generowanie menu na podstawie zawartości bazy danych 317

<p>
{{ entity.tresc | nl2br }}
</p>
{% endblock %}

Listing 28.7. Widok akcji menu (plik menu.html.twig)


<ul>
{% for entity in entities %}
<li>
<a href="{{ path('tren_show', { 'slug': entity.slug }) }}">
{{ entity }}
</a>
</li>
{% endfor %}
</ul>

Krok 11. Sprawdź działanie aplikacji


Odwiedź w przeglądarce adres:
.../web/

Powinieneś ujrzeć witrynę przedstawioną na rysunku 28.1.

Rysunek 28.1. Witryna z przykładu 28.1


318 Część VI ♦ Szczegółowe dane rekordu
Rozdział 29.
Udostępnianie
plików binarnych
Domyślnie Doctrine 2.1 nie pozwala na korzystanie z adnotacji mapujących właściwość
klasy modelu na typ blob bazy danych. Dwoma najprostszymi metodami obejścia takiego
ograniczenia jest:
 użycie funkcji konwertujących dane binarne do postaci tekstowej1:
base64_encode() i base64_decode(),
 zapisywanie w bazie danych wyłącznie nazw plików; treść plików
umieszczamy w specjalnym folderze.

Serwer MySQL na serwerze hostingowym może mieć ustawioną maksymalną wielkość


pakietu komunikacyjnego, np.:
max_allowed_packet = 1M

dlatego w praktyce często stosuję drugie z powyższych rozwiązań. Ograniczenia nałożo-


ne przez max_allowed_packet nie mają wówczas wpływu na to, jak dużymi plikami
możemy operować.

W celu efektywnego zapisywania danych binarnych w bazie należy zaimplementować


własną klasę do mapowania typu blob. Szczegółowy opis takich rozwiązań jest zawarty
w dokumentacji Doctrine 2.1 w punkcie 5.6. pt. Custom Mapping Types2. Oczywi-
ście takie rozwiązanie będzie podlegało ograniczeniom nakładanym przez parametr
max_allowed_packet.

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

Przykład 29.1. Download


— pliki zapisane w bazie danych
Przygotuj aplikację, która będzie udostępniała do pobrania pliki różnych typów (np.
PDF, TXT, JS, PNG, JPG, EXE). Zadanie rozwiąż w taki sposób, by pliki były zapisane
w bazie danych w polach typu text. Do konwersji danych binarnych na format tekstowy
użyj funkcji base64_encode() i base64_decode().

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.

Krok 2. Utwórz bazę danych download


Utwórz pustą bazę danych o nazwie download oraz konto dostępu editor. W pliku
konfiguracyjnym parameters.ini wprowadź dane dostępu do bazy.

Krok 3. Wygeneruj klasę dostępu do bazy danych


Wygeneruj model MyFrontendBundle:File zawierający pola:
 filename typu string o długości 128 znaków,
 mime typu string o długości 128 znaków,
 contents typu text.

Po utworzeniu klasy File.php zmodyfikuj właściwości kolumny filename. Dodaj parametr


unique wymuszający unikatowość:
/**
* @var string $filename
*
* @ORM\Column(name="filename", type="string", length=128, unique=true)
*/
private $filename;

Krok 4. Utwórz tabelę file


Wydaj polecenie:
php app/console doctrine:schema:update --force

W bazie danych download pojawi się tabela file.


Rozdział 29. ♦ Udostępnianie plików binarnych 321

Krok 5. Wypełnij bazę danych plikami z folderu data


Przygotuj przedstawiony na listingu 29.1 plik LoadData.php, a następnie wydaj polecenie:
php app/console doctrine:fixtures:load

Listing 29.1. Plik LoadData.php z przykładu 29.1


class LoadFileData implements FixtureInterface
{
function load(ObjectManager $manager)
{
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$filenames = glob('data/*');
foreach ($filenames as $filename) {
$file = new File();
$file->setFilename(basename($filename));
$file->setMime(finfo_file($finfo, $filename));
$file->setContents(base64_encode(file_get_contents($filename)));
$manager->persist($file);
}
$manager->flush();
finfo_close($finfo);
}
}

W skrypcie z listingu 29.1 przetwarzamy iteracyjnie wszystkie pliki z folderu data/. Nazwę
pliku ustalamy, wywołując funkcję basename():
$nazwa = basename($filename):

Typ MIME pliku odczytujemy funkcją finfo_file():


$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($finfo, 'fotka.jpg');

natomiast zawartość pliku — funkcją file_get_contents():


$tresc = file_get_contents($filename);

Po wykonaniu skryptu z listingu 29.1 w bazie danych pojawią się rekordy widoczne na
rysunku 29.1.

Krok 6. Dostosuj kontroler Default


W kontrolerze DefaultController.php wykonaj dwie akcje:
 index,
 show.

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

Listing 29.2. Dwie akcje kontrolera DefaultController


class DefaultController extends Controller
{
/**
* Lists all File entities.
*
* @Route("/", name="homepage")
* @Template()
*/
public function indexAction()
{
$em = $this->getDoctrine()->getEntityManager();
$entities = $em->getRepository('MyFrontendBundle:File')->findAll();
return array('entities' => $entities);
}
/**
* Finds and displays a File entity.
*
* @Route("/download/{filename}", name="file_show")
*/
public function showAction($filename)
{
$em = $this->getDoctrine()->getEntityManager();
$entity = $em->getRepository('MyFrontendBundle:File')
->findOneByFilename($filename);
if (!$entity) {
throw $this->createNotFoundException('Unable to find File entity.');
}
$response = new Response();
$response->setContent(base64_decode($entity->getContents()));
$response->setStatusCode(200);
$response->headers->set('Content-Type', $entity->getMime());
return $response;
}
}
Rozdział 29. ♦ Udostępnianie plików binarnych 323

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")

Pojemnik {filename}, który jest przeznaczony na nazwę pliku, zostaje przekazany do


metody showAction():
public function showAction($filename)

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);

Jeśli podany rekord zostanie odnaleziony, tworzymy odpowiedź HTTP:


$response = new Response();

Na podstawie obiektu $entity, który odpowiada wyszukanemu w bazie danych plikowi,


ustalamy treść odpowiedzi HTTP:
$response->setContent(base64_decode($entity->getContents()));
$response->setStatusCode(200);
$response->headers->set('Content-Type', $entity->getMime());

Przygotowany obiekt $response wysyłamy do przeglądarki:


return $response;

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;

Krok 7. Wykonaj widok akcji index


W widoku akcji index należy tablicę $entities przetworzyć w listę hiperłączy. Zarys
widoku index.html.twig jest przedstawiony na listingu 29.3.

Listing 29.3. Widok akcji index (plik index.html.twig)


{% extends "::layout.html.twig" %}

{% block content %}
<h1>Pliki do pobrania</h1>
<table>
<thead>
<tr>
<th>Plik</th>
</tr>
</thead>
<tbody>
324 Część VI ♦ Szczegółowe dane rekordu

{% set klasy = ['a', 'b'] %}


{% for entity in entities %}
<tr class="{{ cycle(klasy, loop.index) }}">
<td>
<a href="{{ path('file_show', { 'filename': entity.filename }) }}">
{{ entity.filename }}
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}

Do pokolorowania co drugiego wiersza tabeli stosujemy zmienną klasy:


{% set klasy = ['a', 'b'] %}

oraz funkcję cycle():


<tr class="{{ cycle(klasy, loop.index) }}">

Krok 8. Sprawdź działanie aplikacji


Po dodaniu do aplikacji skórki HTML/CSS z pliku 29-01-start.zip oraz odwiedzeniu
w przeglądarce adresu:
.../web/

ujrzysz witrynę przedstawioną na rysunku 29.2.

Rysunek 29.2.
Witryna z przykładu 29.1
Rozdział 29. ♦ Udostępnianie plików binarnych 325

Przykład 29.2. Download


— pliki pobierane z folderu
Zmodyfikuj przykład 29.1 w taki sposób, by w bazie danych zapisywane były wy-
łącznie nazwy plików oraz typ MIME.

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().

Następnie wydaj polecenia:


php app/console doctrine:schema:drop --force
php app/console doctrine:schema:create:

Krok 2. Zmodyfikuj skrypt wypełniający bazę danych


W skrypcie z listingu 29.1 usuń instrukcję:
$file->setContents(base64_encode(file_get_contents($filename)));

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

Krok 3. Zmodyfikuj kod akcji show


W skrypcie z listingu 29.2 zmodyfikuj instrukcję ustalającą treść odpowiedzi HTTP:
$response->setContent(file_get_contents('../data/' . $entity->getFilename()));
326 Część VI ♦ Szczegółowe dane rekordu
Rozdział 30.
Podsumowanie części VI
W tej części zajęliśmy się przygotowaniem akcji odpowiedzialnej za generowanie
strony dotyczącej pojedynczego rekordu z bazy danych. Akcję taką będziemy w więk-
szości przykładów nazywali show.

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.

Listing 31.1. Struktura tabel user oraz profil


Tabela user
id - klucz główny typu integer
profil_id – klucz obcy
name - string o długości 255 znaków

Tabela profil
id - klucz główny typu integer
info - string o długości 255 znaków

Przyjmijmy, że w tabelach user oraz profil wprowadzono przykładowe rekordy wi-


doczne na listingach 31.2 oraz 31.3.
332 Część VII ♦ Relacje

Listing 31.2. Przykładowe rekordy w tabeli user


id profil_id name
1 7 Jan
2 11 George
3 19 Hans

Listing 31.3. Przykładowe rekordy w tabeli profil


id info
7 Lorem ipsum...
11 Dolor sit amet...
19 Consectetuer...

Przeanalizujmy drugi rekord z tabeli user:


id profil_id name
2 11 George

Wartość 11 w kolumnie profil_id mówi, że rekord użytkownika George jest powiązany


z rekordem:
id info
11 Dolor sit amet...

z tabeli profil.

Klucze obce o wartości NULL


Tworząc relację 1:1, należy zwrócić uwagę na użycie kluczy obcych o wartości NULL.
Klucz obcy o wartości NULL interpretujemy jako brak powiązania relacyjnego. Użytkownik,
dla którego klucz obcy profil_id ma wartość NULL, nie jest przypisany do żadnego
rekordu z tabeli profil.

Użycie relacji 1:1 w Symfony 2


W Symfony 2 relacje 1:1 definiujemy adnotacjami:
@ORM\OneToOne(targetEntity="...")

w klasie Entity odpowiadającej tabeli źródłowej. Jeśli w aplikacji wygenerowano


klasy User oraz Profil, wówczas w klasie User należy dodać kolumnę $profil przedsta-
wioną na listingu 31.4.

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;

Adnotacja z listingu 31.4 zapisana w sposób skrócony jako:


@ORM\OneToOne(targetEntity="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.

Po dodaniu w klasie User właściwości z listingu 31.4 wydajemy polecenie:


php app/console generate:doctrine:entities My

Spowoduje ono dodanie w klasie User metod przedstawionych na listingu 31.5.

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

Listing 31.6. Użycie parametru nullable dla relacji 1:1


/**
*
* @ORM\OneToOne(targetEntity="Profil")
* @ORM\JoinColumn(name="profil_id", referencedColumnName="id", nullable=false)
*/
private $profil;

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.

Listing 31.7. Wstawianie powiązanych rekordów Piotr i Nic ważnego


$User = new User();
$User->setName('Piotr');
$manager->persist($User);

$Profil = new Profil();


$Profil->setInfo('Nic ważnego');
$manager->persist($Profil);

$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ł

Listing 31.8. Domyślną wartością klucza obcego profil_id jest NULL


$User = new User();
$User->setName('Paweł');
$manager->persist($User);
$manager->flush();

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();

Przykład 31.1. Dane użytkowników


Dany jest plik users.xml, którego początkowe wiersze przedstawiono na listingu 31.9.

Listing 31.9. Plik users.xml


<users>
<user>
<name>Jan</name>
<info>Lorem ipsum...</info>
</user>
<user>
<name>George</name>
<info>Dolor sit amet...</info>
</user>
...
</users>

Wykonaj aplikację, która zestawienie użytkowników zapisze w bazie danych. Zawartość


znaczników name umieść w kolumnie name w tabeli user, a zawartość znaczników info
— w kolumnie info tabeli profil. Tabele user oraz profil połącz relacją 1:1. Na stronie
akcji index kontrolera Default wyświetl pełne zestawienie danych zawartych w bazie.

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.

Krok 2. Utwórz bazę danych users


Utwórz bazę danych users oraz konto dostępu editor. W pliku konfiguracyjnym
parameters.ini wprowadź dane do połączenia z bazą.
336 Część VII ♦ Relacje

Krok 3. Wygeneruj klasy dostępu do bazy danych


Wykorzystując polecenie:
php app/console generate:doctrine:entity

wygeneruj dwie klasy o nazwach User oraz Profil.

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.

Krok 4. Zdefiniuj relację 1:1 łączącą klasy User oraz Profil


W klasie User.php dodaj przedstawioną na listingu 31.4 kolumnę $profil oznaczoną
adnotacją @ORM\OneToOne. Następnie wydaj polecenie:
php app/console generate:doctrine:entities My

Spowoduje ono dodanie w klasie User metod przedstawionych na listingu 31.5.

Krok 5. Utwórz tabele user oraz profil


Wydaj polecenie:
php app/console doctrine:schema:update --force

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.

Krok 6. Wypełnij tabelę user danymi z pliku tekstowego


Przygotuj przedstawiony na listingu 31.10 plik LoadData.php. Następnie wydaj polecenie:
php app/console doctrine:fixtures:load

W bazie danych pojawi się sześć rekordów (trzy w tabeli user oraz trzy w tabeli profil).

Listing 31.10. Plik LoadData.php z przykładu 31.1


class LoadData implements FixtureInterface
{
function load(ObjectManager $manager)
{
$xml = simplexml_load_file('data/users.xml');
foreach ($xml->user as $u) {

$User = new User();


$User->setName($u->name);
$manager->persist($User);

$Profil = new Profil();


$Profil->setInfo($u->info);
$manager->persist($Profil);
Rozdział 31. ♦ Relacje 1:1 337

$User->setProfil($Profil);

}
$manager->flush();
}
}

Krok 7. Dostosuj akcję index


W kontrolerze DefaultController.php zmodyfikuj metodę indexAction(). Wprowadź kod
przedstawiony na listingu 31.11.

Listing 31.11. Kod akcji index


/**
* Lista rekordow user
*
* @Route("/")
* @Template()
*/
public function indexAction()
{
$em = $this->getDoctrine()->getEntityManager();
$entities = $em->getRepository('MyFrontendBundle:User')->findAll();
return array('entities' => $entities);
}

Krok 8. Dostosuj widok akcji index


W widoku akcji index wprowadź kod z listingu 31.12.

Listing 31.12. Widok akcji index


{% extends "::base.html.twig" %}

{% 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

Krok 9. Sprawdź działanie aplikacji


Odwiedź w przeglądarce adres:
.../web/

Powinieneś ujrzeć witrynę przedstawioną na rysunku 31.1.

Rysunek 31.1.
Witryna z przykładu 31.1

Akcje referencyjne SQL


W przypadku usuwania lub modyfikacji rekordów w tabelach powiązanych relacją
1:1 należy zwrócić uwagę na to, co będzie się działo z rekordami zależnymi. Co ma się
dziać z rekordem tabeli źródłowej (np. Jan z tabeli user) w przypadku usunięcia odpo-
wiadającego mu rekordu w tabeli docelowej (np. Lorem ipsum z tabeli profil)? Co ma się
dziać z rekordem tabeli docelowej w przypadku usunięcia rekordu z tabeli źródłowej?

Język SQL umożliwia zdefiniowanie akcji referencyjnych1 (ang. referential actions)


występujących po operacjach usuwania (ON DELETE) oraz aktualizacji (ON UPDATE) rekor-
dów tabeli docelowej. Kod definiujący akcję referencyjną dla klucza profil_id z tabeli
user przyjmuje postać taką jak na listingu 31.13.

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;

Akcja referencyjna z listingu 31.14 spowoduje wygenerowanie kodu z listingu 31.13.


Akcja zdefiniowana w ten sposób będzie wykonywana na poziomie bazy danych.

Akcje referencyjne wykonywane na poziomie bazy danych działają w jednym kie-


runku. Usunięcie rekordu z tabeli docelowej będzie powodowało usunięcie rekordu
z tabeli źródłowej.

Programowe akcje referencyjne


Doctrine 2.1
Doctrine 2.1 pozwala także na definiowanie akcji referencyjnych obsługiwanych pro-
gramowo przez klasy Doctrine. W odróżnieniu od akcji referencyjnych wykonywanych
na poziomie bazy danych, akcje tego typu możemy definiować w dwóch kierunkach:
 W przypadku usuwania rekordu z tabeli docelowej usuń rekord z tabeli
źródłowej (parametr cascade).
 W przypadku usuwania rekordu z tabeli źródłowej usuń rekord z tabeli
docelowej (parametr orphanRemoval — usuwanie sierot).

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.

Listing 31.15. Definicja programowej akcji referencyjnej cascade


/**
*
* @ORM\OneToOne(targetEntity="Profil", cascade={"persist", "remove"})
*/
private $profil;
340 Część VII ♦ Relacje

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;

Jeśli użyjemy parametru z listingu 31.16, wykonanie instrukcji:


$User = $manager->getRepository('MyFrontendBundle:User')->find(1);
$manager->remove($User);
$manager->flush();

spowoduje wówczas usunięcie z tabeli profil rekordu powiązanego z rekordem, który


wystąpił w tabeli user i miał wartość klucza głównego 1.

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.

Relacja występująca na listingu 31.4 jest relacją jednokierunkową. W celu zdefiniowa-


nia analogicznej relacji dwukierunkowej należy w tabeli docelowej dodać adnotację
@ORM\OneToOne. Po dodaniu adnotacji należy oczywiście wykonać polecenie:
php app/console generate:doctrine:entities My
Rozdział 31. ♦ Relacje 1:1 341

które wygeneruje metody dostępu do właściwości. Przykład definicji dwukierunkowej


relacji 1:1 (po wydaniu polecenia generate:doctrine:entities) jest przedstawiony na
listingach 31.17 oraz 31.18.

Listing 31.17. Klasa User: definicja dwukierunkowej relacji 1:1


class User
{
...

/**
*
* @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;
}

...
}

Listing 31.18. Klasa Profil: definicja dwukierunkowej relacji 1:1


class 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;
}

...
}

Dwukierunkowość relacji powoduje, że każdą z dwóch klas połączonych relacją możemy


scharakteryzować jako właściciela relacji (ang. owing side) lub jako klasę odwrotną
relacji (ang. inverse side). W przypadku opisywanej relacji łączącej klasy User i Profil
klasa User jest właścicielem relacji, a klasa Profil — klasą odwrotną relacji.

W klasie, która jest właścicielem relacji, występuje parametr inversedBy.

Synchronizacja obiektów z bazą danych


Pojęcia właściciela relacji i klasy odwrotnej dla relacji są bardzo istotne, gdyż domyślnie
obiekty są synchronizowane z bazą danych wyłącznie w przypadku, gdy modyfikacje
powiązań relacyjnych wykonujemy po stronie właściciela relacji. Jeśli dla klas z listin-
gów 31.17 oraz 31.18 wykonamy instrukcje z listingu 31.19, w bazie danych pojawią
się dwa rekordy:
//tabela user
id profil_id name
X 123 Alan

//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);

$Profil = new Profil();


$Profil->setInfo('Info Alana...');
$manager->persist($Profil);
Rozdział 31. ♦ Relacje 1:1 343

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

Listing 31.20. 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('Peter');
$manager->persist($User);

$Profil = new Profil();


$Profil->setInfo('Info Petera...');
$manager->persist($Profil);

$Profil->setUser($User);

$manager->flush();

W celu zapewnienia synchronizacji obiektów bez względu na to, w których klasach


wprowadzamy modyfikacje, należy w metodzie setUser() klasy Profil dodać widoczną
na listingu 31.21 instrukcję setProfil().

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

Po wprowadzeniu modyfikacji z listingu 31.21 kod z listingu 31.20 spowoduje zapisanie


w bazie danych rekordów:
//tabela user
id profil_id name
X 456 Peter

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

Listing 32.1. Struktura tabel kontynent oraz panstwo


Tabela kontynent
id - klucz główny typu integer
nazwa - string o długości 255 znaków

Tabela panstwo
id - klucz główny typu integer
nazwa - string o długości 255 znaków
kontynent_id - klucz obcy

Powiązanie kontynentów i państw relacją jeden do wielu polega na tym, że:


 Każdemu kontynentowi przyporządkowujemy dowolną liczbę państw.
 Każde państwo ma być przyporządkowane do dokładnie jednego kontynentu.

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

Wprowadźmy przykładowe rekordy w tabeli kontynent:


id nazwa
1 Europa
2 Azja
3 Afryka

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.

Wszystkie państwa pochodzące z wybranego kontynentu ustalimy, wybierając z tabeli


panstwo te rekordy, które mają konkretną wartość klucza obcego kontynent_id. Pań-
stwami europejskimi są:
id nazwa kontynent_id
1 Polska 1
3 Niemcy 1
4 Francja 1

Klucze obce o wartości NULL


Tworząc relację 1:n, należy zwrócić uwagę na użycie kluczy obcych o wartości NULL.
Klucz obcy o wartości NULL interpretujemy jako brak powiązania relacyjnego. Państwo,
dla którego klucz obcy kontynent_id ma wartość NULL, nie jest przypisane do żadnego
kontynentu. Powiązanie relacyjne nie zostało zdefiniowane.

Domyślnie relacje 1:n tworzone w Doctrine 2.1 pozwalają na stosowanie wartości


NULL dla klucza obcego.

Użycie relacji 1:n w Symfony 2


W Symfony 2 relacje 1:n definiujemy adnotacjami:
@ORM\OneToMany(...)
@ORM\ManyToOne(...)
Rozdział 32. ♦ Relacje 1:n (jeden do wielu) 347

W klasie Entity odpowiadającej tabeli źródłowej dodajemy adnotację @ORM\OneToMany,


a w klasie odpowiadającej tabeli docelowej — adnotację @ORM\ManyToOne. Powiązanie
tabel kontynent oraz panstwo jest zilustrowane na listingach 32.2 oraz 32.3.

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

public function __construct()


{
$this->panstwa = new \Doctrine\Common\Collections\ArrayCollection();
}

/**
* 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

Listing 32.6. Użycie parametru nullable dla relacji 1:n


Class Panstwo
{
...

/**
* @ORM\ManyToOne(targetEntity="Kontynent", inversedBy="panstwa")
* @ORM\JoinColumn(name="kontynent_id", referencedColumnName="id",
nullable=false)
*/
protected $kontynent;

...
}

Właściciel relacji 1:n


Relacja występująca na listingach 32.2 oraz 32.3 jest relacją dwukierunkową, w której
klasa Panstwo jest właścicielem relacji (ang. owing side), a klasa Kontynent — klasą
odwrotną relacji (ang. inverse side). Zwróć uwagę, że pojęcia te nie pokrywają się z po-
jęciami tabeli źródłowej i docelowej. Klasę, która jest właścicielem relacji, rozpoznajemy
po wystąpieniu parametru inversedBy w adnotacji definiującej powiązanie:
@ORM\ManyToOne(targetEntity="Kontynent", inversedBy="panstwa")

Konsekwencją jest to, że aktualizacja powiązań rekordów powinna być wykonywana


w obiektach klasy Panstwo.

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.

Listing 32.7. Wstawianie powiązanych rekordów Europa i Polska


$Kontynent = new Kontynent();
$Kontynent->setNazwa('Europa');
$manager->persist($Kontynent);
350 Część VII ♦ Relacje

$Panstwo = new Panstwo();


$Panstwo->setNazwa('Polska');
$Panstwo->setKontynent($Kontynent);
$manager->persist($Panstwo);

$manager->flush();

Jeśli klucz obcy kontynent_id ma ograniczenie nullable=false, wówczas instrukcja:


$Panstwo = new Panstwo();
$Panstwo->setNazwa('Niemcy');
$manager->persist($Panstwo);
$manager->flush();

zakończy się błędem informującym o tym, że wartość klucza obcego nie została nadana.

Brak ograniczenia nullable=false dla klucza obcego kontynent_id spowoduje, że in-


strukcja:
$Panstwo = new Panstwo();
$Panstwo->setNazwa('Francja');
$manager->persist($Panstwo);
$manager->flush();

umieści w tabeli panstwo rekord:


panstwo_id nazwa kontynent_id
1 Francja NULL

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.

Wydruk nazw wszystkich państw europejskich jest przedstawiony na listingu 32.8.

Listing 32.8. Wydruk państw europejskich

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

Listing 32.9. Wydruk szczegółowych danych Polski

$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().

Listing 32.10. Wywołanie metody setKontynent() zapewni synchronizację powiązań relacyjnych


definiowanych wywołaniem metody addPanstwo()
class Kontynent
{
...

/**
* 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.

Listing 32.11. Definicja SQL-owych akcji referencyjnych


class Panstwo
{
...

/**
* @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.

Listing 32.12. Usunięcie państw europejskich


$Kontynent = $manager
->getRepository('MyFrontendBundle:Kontynent')
->findOneByNazwa('Europa');
$manager->remove($Kontynent);
$manager->flush();

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.

Listing 32.13. Definicja programowych akcji referencyjnych


class Kontynent
{
...
Rozdział 32. ♦ Relacje 1:n (jeden do wielu) 353

/**
* @ORM\OneToMany(targetEntity="Panstwo", mappedBy="kontynent", cascade={"all"})
*/
protected $panstwa;

...
}

Przykład 32.1. Kontynent i państwa


Dany jest plik kontynenty.xml, który zawiera listę kontynentów oraz państw. Przygotuj
bazę danych kontynenty, która będzie pozwalała na zapisywanie danych o kontynentach
i państwach z zachowaniem informacji o tym, na którym kontynencie leży każde z państw.

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.

Zadanie wykonaj w taki sposób, by na liście wszystkich kontynentów obok nazwy


kontynentu widoczna była lista wszystkich państw z danego kontynentu. Na liście
wszystkich państw obok nazwy każdego państwa umieść natomiast nazwę kontynentu,
z którego dane państwo pochodzi.

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.

Krok 2. Utwórz bazę danych kontynenty


Utwórz bazę danych o nazwie kontynenty oraz konto dostępu editor, po czym w pliku
parameters.ini zmodyfikuj opcje ustalające parametry połączenia z bazą.

Krok 3. Wygeneruj klasy dostępu do bazy danych


Wygeneruj modele:
MyFrontendBundle:Kontynent
MyFrontendBundle:Panstwo
354 Część VII ♦ Relacje

Każdy z nich powinien zawierać jedną właściwość:


 nazwa typu string o długości 255 znaków.

W wygenerowanych klasach dodaj metody __toString(), które będą zwracały wła-


ściwość $nazwa.

Krok 4. Zdefiniuj dwukierunkową relację 1:n łączącą tabele kontynent


oraz panstwo
W klasie Kontynent dodaj właściwość $panstwa:
/**
* @ORM\OneToMany(targetEntity="Panstwo", mappedBy="kontynent")
*/
protected $panstwa;

W klasie Panstwo dodaj właściwość $kontynent:


/**
* @ORM\ManyToOne(targetEntity="Kontynent", inversedBy="panstwa")
*/
protected $kontynent;

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().

Krok 5. Utwórz tabele kontynent oraz panstwo


Wydaj polecenie:
php app/console doctrine:schema:update --force

W bazie danych pojawią się tabele kontynent oraz panstwo.

Krok 6. Wypełnij bazę danych zawartością odczytaną z pliku XML


Przygotuj przedstawiony na listingu 32.14 plik LoadData.php, a następnie wydaj polecenie:
php app/console doctrine:fixtures:load

Listing 32.14. Plik LoadData.php z przykładu 32.1


class LoadData implements FixtureInterface
{
function load(ObjectManager $manager)
{
Rozdział 32. ♦ Relacje 1:n (jeden do wielu) 355

$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();

}
}

Krok 7. Dostosuj akcję index


W folderze src/My/FrontendBundle/Controller/ utwórz plik KontynentController.php
zawierający jedną akcję indexAction(). Zarys kontrolera jest przedstawiony na li-
stingu 32.15.

Listing 32.15. Kontroler Kontynent


class KontynentController extends Controller
{
/**
* Lista kontynentow
*
* @Route("/kontynenty.html", name="kontynent_index")
* @Template()
*/
public function indexAction()
{
$em = $this->getDoctrine()->getEntityManager();
$entities = $em->getRepository('MyFrontendBundle:Kontynent')->findAll();
return array('entities' => $entities);
}
}

Krok 8. Dostosuj widok akcji index


Utwórz plik src/My/FrontendBundle/Resources/views/Kontynent/index.html.twig o zawar-
tości takiej jak na listingu 32.16.

Listing 32.16. Widok akcji index


{% extends "::layout.html.twig" %}
{% block content %}
<h2>Lista wszystkich kontynentów</h2>
356 Część VII ♦ Relacje

<ul>
{% for kontynent in entities %}
<li>
{{ kontynent }}
<ul>
{% for panstwo in kontynent.panstwa %}
<li>
{{ panstwo }}
</li>
{% endfor %}
</ul>
</li>
{% endfor %}
</ul>
{% endblock %}

Krok 9. Przygotuj kontroler Panstwo


W analogiczny sposób przygotuj kontroler Panstwo zawierający akcję index.

Krok 10. Dostosuj skórkę aplikacji


Utwórz plik layout.html.twig i umieść w nim kod widoczny na listingu 32.17.

Listing 32.17. Zawartość pliku layout.html.twig


{% extends "::base.html.twig" %}

{% 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 %}

Krok 11. Wykonaj stronę główną


Zmodyfikuj kod akcji index w kontrolerze Default. Zarys kontrolera DefaultController
jest przedstawiony na listingu 32.18.

Listing 32.18. Akcja index kontrolera default


class DefaultController extends Controller
{
/**
* @Route("/", name="homepage")
* @Template()
*/
Rozdział 32. ♦ Relacje 1:n (jeden do wielu) 357

public function indexAction()


{
return array();
}
}

W widoku akcji index wprowadź jeden akapit tekstu Lorem ipsum.

Krok 12. Sprawdź działanie aplikacji


Odwiedź w przeglądarce adres:
.../web/

Powinieneś ujrzeć witrynę przedstawioną na rysunku 32.1.

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

Listing 32.19. Automatyczne sortowanie rekordów zwracanych przez metodę getPanstwa()


class Kontynent
{
...

/**
* @ORM\OneToMany(targetEntity="Panstwo", mappedBy="kontynent")
* @ORM\OrderBy({"nazwa" = "ASC"})
*/
protected $panstwa;

...
}

Parametrem adnotacji @ORM\OrderBy jest tablica asocjacyjna, w której indeksami są


nazwy właściwości w klasie Panstwo, a wartościami — ciągi ASC lub DESC.
Rozdział 33.
Relacje n:m
(wiele do wielu)
Relacja n:m wiąże rekordy zawarte w dwóch tabelach w następujący sposób:
 Każdemu rekordowi z pierwszej tabeli może być przyporządkowana dowolna
liczba rekordów z drugiej tabeli.
 Każdemu rekordowi z drugiej tabeli może być przyporządkowana dowolna
liczba rekordów z pierwszej tabeli.

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 zawiera kolumny:


 id — klucz główny;
 tytul — string o długości 255 znaków.

Tabele aktor i film są przedstawione na listingu 33.1.

Listing 33.1. Struktura tabel aktor oraz film


Tabela aktor
id - klucz główny typu integer
imie - string o długości 255
nazwisko - string o długości 255
360 Część VII ♦ Relacje

Tabela film
id - klucz główny typu integer
tytul - string o długości 255

Powiązanie aktorów i filmów relacją wiele do wielu polega na tym, że:


 Każdemu filmowi może być przyporządkowana dowolna liczba aktorów.
 Każdemu aktorowi może być przyporządkowana dowolna liczba filmów.

Informacje o powiązaniach relacyjnych rekordów będziemy zapisywali w tabeli łączącej


o nazwie film_aktor. Wystąpią w niej dwie kolumny:
 film_id — klucz obcy z tabeli film_id;
 aktor_id — klucz obcy z tabeli aktor_id.

Wprowadźmy przykładowe rekordy w tabeli film:


id tytul
9 Miś
17 Kanał

oraz w tabeli aktor:


id imie nazwisko
3 Stanisław Mikulski
11 Stanisław Tym
25 Krzysztof Kowalewski

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

W analogiczny sposób powiązanie rekordów Kanał oraz Stanisław Mikulski realizuje


rekord:
film_id aktor_id
17 3

Użycie relacji n:m w Symfony 2


W Symfony 2 relacje n:m definiujemy adnotacjami:
@ORM\ManyToMany(...)

Powiązanie tabel film oraz aktor jest zilustrowane na listingach 33.2 oraz 33.3.
Rozdział 33. ♦ Relacje n:m (wiele do wielu) 361

Listing 33.2. Właściwość i adnotacja definiujące relację n:m w klasie Film


class Film
{
...

/**
* @ORM\ManyToMany(targetEntity="Aktor", inversedBy="filmy")
*/
private $aktorzy;

...
}

Listing 33.3. Właściwość i adnotacja definiujące relację n:m w klasie Aktor


class Aktor
{
...

/**
* @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

Spowoduje ono dodanie:


 w klasie Film metod __construct(), addAktor() i getAktorzy();
 w klasie Aktor metod __construct(), addFilm() i getFilmy().

Właściciel relacji n:m


Relacja występująca na listingach 33.2 oraz 33.3 jest relacją dwukierunkową, w której
klasa Film jest właścicielem relacji (ang. owing side), a klasa Aktor — klasą odwrotną
relacji (ang. inverse side). Klasę, która jest właścicielem relacji, rozpoznajemy po wy-
stąpieniu parametru inversedBy w adnotacji definiującej powiązanie:
@ORM\ManyToMany(targetEntity="Aktor", inversedBy="filmy")

Konsekwencją jest to, że aktualizacja powiązań rekordów powinna być wykonywana


w obiektach klasy Film.
362 Część VII ♦ Relacje

Tabela łącząca relacji n:m


Domyślnie tabela łącząca relacji n:m otrzyma nazwę, która powstaje przez połączenie
nazw tabel połączonych relacją. W omawianym przykładzie tabela łącząca otrzyma
nazwę:
film_aktor

Pierwszym członem jest nazwa klasy, która jest właścicielem relacji. Po wydaniu po-
lecenia:
php app/console doctrine:schema:create

w bazie danych pojawią się trzy tabele:


 film,
 aktor,
 film_aktor.

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.

Listing 33.4. Adnotacja ustalająca nazwę tabeli łączącej


class Film
{
...

/**
* @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

Listing 33.5. Wstawianie powiązanych rekordów Miś oraz Stanisław Tym


$Film = new Film();
$manager->persist($Film);
$Film->setTytul('Miś');
$Aktor = new Aktor();
$manager->persist($Aktor);
$Aktor->setImie('Stanisław');
$Aktor->setNazwisko('Tym');
$Film->addAktor($Aktor);
$manager->flush();

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ś”.

Listing 33.6. Wydruk aktorów występujących w zadanym filmie


$Film = $manager
->getRepository('MyFrontendBundle:Film')
->findOneByTytul('Miś');

foreach ($Film->getAktorzy() as $Aktor) {


echo $Aktor->getImie();
echo $Aktor->getNazwisko();
}

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.

Listing 33.7. Wydruk aktorów występujących w zadanym filmie


$Aktor = $manager
->getRepository('MyFrontendBundle:Aktor')
->findOneBy(
array('imie' => 'Robert', 'nazwisko' => 'Redford')
);

foreach ($Aktor->getFilmy() as $Film) {


echo $Film->getTytul();
}
364 Część VII ♦ Relacje

Nazwy metod getFilmy() oraz getAktorzy() powstają na podstawie nazw właści-


wości:
private $aktorzy;
private $filmy;

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().

Listing 33.8. Wywołanie metody addAktor() zapewni synchronizację powiązań relacyjnych


definiowanych wywołaniem metody addFilm()
class Aktor
{
...

/**
* Add filmy
*
* @param My\FrontendBundle\Entity\Film $filmy
*/
public function addFilm(\My\FrontendBundle\Entity\Film $filmy)
{
$this->filmy[] = $filmy;
$filmy->addAktor($this);
}

...
}

Usuwanie powiązania relacyjnego


W celu usunięcia relacji łączącej dwa obiekty należy wywołać metodę removeElement()
kolekcji po stronie klasy będącej właścicielem relacji. Przykładowy kod ilustrujący usu-
wanie powiązania rekordów Wielki Gatsby oraz Robert Redford jest przedstawiony na
listingu 33.9.

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();

Należy pamiętać, że usunięcie powiązania wykonujemy po stronie klasy będącej wła-


ścicielem relacji. Dodanie instrukcji z listingu 33.8 powoduje synchronizację wyłącznie
podczas tworzenia powiązania relacyjnego.

Akcje referencyjne SQL


Akcje SQL-owe
Relacje n:m mają domyślnie włączone akcje referencyjne SQL, które powodują, że usu-
nięcie rekordu z jednej z tabel powiązanych (tj. film lub aktor) spowoduje automatyczne
usunięcie odpowiednich rekordów z tabeli łączącej film_aktor.

Przykład 33.1. Filmy i aktorzy


Dany jest plik filmy.xml, który zawiera listę filmów oraz aktorów. Przygotuj bazę danych
filmy, która będzie pozwalała na zapisywanie danych o filmach i aktorach z zachowa-
niem informacji o tym, w których filmach wystąpił każdy z aktorów.

Napisz skrypt, który wypełni bazę danymi odczytanymi z pliku XML.

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

Krok 2. Utwórz bazę danych filmy


Utwórz bazę danych o nazwie filmy oraz konto dostępu editor, po czym w pliku
parameters.ini zmodyfikuj opcje ustalające parametry połączenia z bazą.

Krok 3. Wygeneruj klasy dostępu do bazy danych


Wygeneruj modele:
MyFrontendBundle:Film
MyFrontendBundle:Aktor

Klasa Film powinna zawierać właściwość:


 tytul typu string o długości 255 znaków.

Klasa Aktor powinna zawierać dwie właściwości:


 imie typu string o długości 255 znaków,
 nazwisko typu string o długości 255 znaków.

W wygenerowanych klasach dodaj metody __toString(). Metoda klasy Film ma zwracać


tytuł, a metoda klasy Aktor — imię i nazwisko połączone spacją.

Krok 4. Zdefiniuj dwukierunkową relację n:m


łączącą klasy Film oraz Aktor
W klasie Film dodaj przedstawioną na listingu 33.2 właściwość $aktorzy.

W klasie Aktor dodaj przedstawioną na listingu 33.3 właściwość $filmy.

Poleceniem:
php app/console generate:doctrine:entities My

wygeneruj następujące metody:


 w klasie Film: __construct(), addAktor() i getAktorzy();
 w klasie Aktor: __construct(), addFilm() i getFilmy().

Krok 5. Utwórz tabele film, aktor i film_aktor


Wydaj polecenie:
php app/console doctrine:schema:update --force

W bazie danych pojawią się trzy tabele: film, aktor oraz film_aktor.
Rozdział 33. ♦ Relacje n:m (wiele do wielu) 367

Krok 6. Wypełnij bazę danych zawartością odczytaną z pliku XML


Przygotuj przedstawiony na listingu 33.10 plik LoadData.php, a następnie wydaj polecenie:
php app/console doctrine:fixtures:load

Listing 33.10. Plik LoadData.php z przykładu 33.1


function load(ObjectManager $manager)
{
$xml = simplexml_load_file('data/filmy.xml');
foreach ($xml->film as $f) {
$Film = new Film();
$Film->setTytul($f->tytul);
$manager->persist($Film);
foreach ($f->aktorzy->aktor as $a) {
$Aktor = $manager
->getRepository('MyFrontendBundle:Aktor')
->findOneBy(array('imie' => $a->imie, 'nazwisko' => $a->nazwisko));
if (!$Aktor) {
$Aktor = new Aktor();
$Aktor->setImie($a->imie);
$Aktor->setNazwisko($a->nazwisko);
$manager->persist($Aktor);
};
$Film->addAktor($Aktor);
$manager->flush();
}
}
$manager->flush();
}

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);
};

Bardzo ważne jest, by po utworzeniu nowego rekordu w tabeli aktor i powiązaniu go


z rekordem z tabeli film wywołać metodę flush():
$manager->flush();

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(...);

wykonywane w pętli zwrócą wynik false!

Krok 7. Dostosuj akcję index


W folderze src/My/FrontendBundle/Controller/ utwórz plik FilmController.php zawierają-
cy jedną akcję indexAction(). Zarys kontrolera jest przedstawiony na listingu 33.12.

Listing 33.12. Kontroler Film


class FilmController extends Controller
{
/**
* Lista wszystkich filmow
*
* @Route("/filmy.html", name="film_index")
* @Template()
*/
public function indexAction()
{
$em = $this->getDoctrine()->getEntityManager();
$entities = $em->getRepository('MyFrontendBundle:Film')->findAll();
return array('entities' => $entities);
}
}
Rozdział 33. ♦ Relacje n:m (wiele do wielu) 369

Krok 8. Dostosuj widok akcji index


Utwórz plik src/My/FrontendBundle/Resources/views/Film/index.html.twig o zawartości
takiej jak na listingu 33.13.

Listing 33.13. Widok akcji index


{% extends "::layout.html.twig" %}

{% 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 %}

Krok 9. Przygotuj kontroler Aktor


W analogiczny sposób przygotuj kontroler Aktor zawierający akcję index.

Krok 10. Dostosuj skórkę aplikacji


Utwórz plik layout.html.twig i umieść w nim kod widoczny na listingu 33.14.

Listing 33.14. Zawartość pliku layout.html.twig


{% extends "::base.html.twig" %}

{% 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

Krok 11. Utwórz stronę główną


Zmodyfikuj kod akcji index w kontrolerze Default tak, generowana strona zawierała
akapit Lorem ipsum i miała adres /.

Krok 12. Sprawdź działanie aplikacji


Odwiedź w przeglądarce adres:
.../web/

Powinieneś ujrzeć witrynę przedstawioną na rysunku 33.1.

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

określam skrótowo akcję showAction() z kontrolera KontynentController.

W celu uniknięcia powielania kodu widoku odpowiedzialnego za przetwarzanie listy


wszystkich państw należy w folderze /views/Panstwo/ przygotować widok przedstawiony
na listingu 34.1.

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

Powielanie kodu prezentującego listę rekordów klasy Aktor wyeliminujemy, przygoto-


wując dwa widoki częściowe:
 w folderze /views/Aktor/ widok _list.html.twig,
 w folderze /views/Film/ widok _list.html.twig.

Kod widoku /views/Aktor/_list.html.twig jest przedstawiony na listingu 34.3.

Listing 34.3. Widok częściowy /views/Aktor/_list.html.twig prezentujący listę aktorów


<ul>
{% for aktor in dane %}
<li>
<a href="{{ path('aktor_show', { 'id': aktor.id }) }}">
{{ aktor }}
</a>
</li>
{% endfor %}
</ul>
Rozdział 34. ♦ Relacje, akcje index i show oraz widoki częściowe 375

Widok z listingu 34.3 wywołujemy w widoku akcji aktor/index w sposób przedstawiony


na listingu 34.4.

Listing 34.4. Wywołanie widoku /views/Aktor/_list.html.twig w widoku akcji aktor/index


{% block content %}
<h2>Lista wszystkich aktorów</h2>
{% include 'MyFrontendBundle:Aktor:_list.html.twig' with {'dane': entities } %}
{% endblock %}

Ten sam widok będzie wywołany na stronie akcji film/show w sposób przedstawiony na
listingu 34.5.

Listing 34.5. Wywołanie widoku /views/Aktor/_list.html.twig w widoku akcji film/show


{% block content %}
<h2>Szczegółowe dane filmu</h2>
<h3>Tytuł: {{ entity }}</h3>
<h4>Aktorzy, którzy zagrali w filmie:</h4>
{% include 'MyFrontendBundle:Aktor:_list.html.twig' with {'dane': entity.aktorzy } %}
{% endblock %}

W analogiczny sposób przygotujemy widok prezentujący listę tytułów filmów i użyjemy


go w widokach akcji film/index oraz aktor/show.

Przykład 34.1. Kontynenty/Państwa


— akcje show i widoki częściowe
Zmodyfikuj przykład z rozdziału 32. W projekcie dodaj akcje kontynent/show oraz
panstwo/show.

Wykonaj dwa widoki częściowe:


 /views/Kontynent/_list.html.twig — widok generujący listę nazw
kontynentów;
 /views/Panstwo/_list.html.twig — widok generujący listę nazw państw.

Widok /views/Kontynent/_list.html.twig wywołaj w kodzie akcji kontynent/index.

Widok /views/Panstwo/_list.html.twig wywołaj w kodzie akcji kontynent/show oraz


panstwo/index.
376 Część VII ♦ Relacje

Przykład 34.2. Filmy/Aktorzy


— akcje show i widoki częściowe
Zmodyfikuj przykład z rozdziału 33. W projekcie dodaj akcje aktor/show oraz film/show.

Wykonaj dwa widoki częściowe:


 /views/Aktor/_list.html.twig — widok generujący listę aktorów;
 /views/Film/_list.html.twig — widok generujący listę tytułów filmów.

Widok /views/Aktor/_list.html.twig wywołaj w kodzie akcji aktor/index oraz film/show.

Widok /views/Film/_list.html.twig wywołaj w kodzie akcji film/index oraz aktor/show.

Przykład 34.3. Powieści Agaty Christie


Plik novels.xml zawiera informacje o powieściach Agaty Christie. Przygotuj aplikację,
która dane dotyczące twórczości Agaty Christie udostępni w postaci witryny WWW.
W serwisie wykonaj:
 trzy modele: Novel, Detective i Method;
 trzy kontrolery: NovelController, DetectiveController i MethodController;
 oraz akcje:
 Novel/index — strona prezentująca listę wszystkich powieści;
 Novel/show — strona prezentująca szczegółowe dane pojedynczej
powieści;
 Detective/index — strona prezentująca listę wszystkich detektywów;
 Detective/show — strona prezentująca szczegółowe dane pojedynczego
detektywa;
 method/index — strona prezentująca listę wszystkich metod zbrodni;
 method/show — strona prezentująca szczegółowe dane pojedynczej metody
zbrodni.

Modele Novel oraz Method połącz relacją wiele do wielu.

Modele Detective oraz Novel połącz relacją jeden do wielu.

Na stronie akcji Novel/show umieść:


 tytuł powieści,
 nazwisko detektywa,
Rozdział 34. ♦ Relacje, akcje index i show oraz widoki częściowe 377

 listę wszystkich metod zbrodni z danej powieści.

Na stronie akcji Detective/show umieść:


 nazwisko detektywa,
 listę wszystkich powieści z udziałem detektywa.

Na stronie akcji Method/show umieść:


 nazwę metody,
 listę wszystkich powieści, w których wystąpiła podana metoda zbrodni.

Do adresowania akcji show we wszystkich trzech kontrolerach użyj automatycznie gene-


rowanych ciągów slug.

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.

Wykorzystaj dane oraz szablon z pliku 34-03-start.zip.

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.

Krok 2. Utwórz bazę danych achristie


Utwórz bazę danych o nazwie achristie oraz konto dostępu editor, po czym w pliku
parameters.ini zmodyfikuj opcje ustalające parametry połączenia z bazą.

Krok 3. Wygeneruj klasy dostępu do bazy danych


Wygeneruj modele:
MyFrontendBundle:Novel
MyFrontendBundle:Detective
MyFrontendBundle:Method

Klasa Novel powinna zawierać:


 właściwość title typu string o długości 255 znaków.

Klasa Detective powinna zawierać:


 dwie właściwości name typu string o długości 255 znaków.
378 Część VII ♦ Relacje

Klasa Method powinna zawierać:


 dwie właściwości name typu string o długości 255 znaków.

Krok 4. Dostosuj modele


W wygenerowanych modelach dodaj:
 metody __toString() zwracające właściwość title lub name,
 adnotację, która spowoduje wygenerowanie klas Repository.

Adnotacja, którą należy umieścić w klasie Novel, ma postać:


@ORM\Entity(repositoryClass="My\FrontendBundle\Entity\NovelRepository")

Krok 5. Zdefiniuj relację 1:n łączącą klasy Detective i Novel


W klasie Detective dodaj właściwość z listingu 34.6, a w klasie Novel — właściwość
z listingu 34.7.

Listing 34.6. Modyfikacja klasy Detective


class Detective
{
...

/**
* @ORM\OneToMany(targetEntity="Novel", mappedBy="detective")
* @ORM\OrderBy({"title" = "ASC"})
*/
protected $novels;

Listing 34.7. Modyfikacja klasy Novel


class Novel
{
...

/**
* @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

Krok 6. Zdefiniuj relację n:m łączącą klasy Novel i Method


W klasie Novel dodaj właściwość z listingu 34.8, a w klasie Method — właściwość z li-
stingu 34.9.

Listing 34.8. Modyfikacja klasy Novel


class Novel
{
...

/**
* @ORM\ManyToMany(targetEntity="Method", inversedBy="novels")
*/
private $methods;

...
}

Listing 34.9. Modyfikacja klasy Method


class Method
{
...

/**
* @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.

Krok 7. Włącz generowanie identyfikatorów slug


W konfiguracji projektu włącz generowanie ciągów slug. Następnie w klasach Novel,
Detective oraz Method dodaj właściwość slug opatrzoną odpowiednimi adnotacjami.

Krok 8. Wygeneruj i dostosuj klasy Repository


Wydaj polecenie:
php app/console generate:doctrine:entities My

a następnie w wygenerowanych klasach Repository nadpisz metodę findAll().


380 Część VII ♦ Relacje

Krok 9. Utwórz tabele bazy danych


Wydaj polecenie:
php app/console doctrine:schema:update --force

W bazie danych pojawią się cztery tabele: novel, detective, method oraz novel_method.

Krok 10. Wypełnij bazę danych zawartością odczytaną z pliku XML


Przygotuj przedstawiony na listingu 34.10 skrypt LoadData.php, a następnie wydaj
polecenie:
php app/console doctrine:fixtures:load

Listing 34.10. Plik LoadData.php z przykładu 34.1


function load(ObjectManager $manager)
{

$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();
};

$Novel = new Novel();


$Novel->setTitle($n->title);
$Novel->setDetective($Detective);
$manager->persist($Novel);
$manager->flush();

foreach ($n->methods->method as $m) {


$Method = $manager
->getRepository('MyFrontendBundle:Method')
->findOneByName($m);

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

w bazie danych powinno pojawić się 236 rekordów.

Krok 11. Wykonaj trzy kontrolery


Utwórz plik NovelController.php o zawartości przedstawionej na listingu 34.11.

Listing 34.11. Kontroler Novel


/**
* Novel controller.
*
* @Route("/novel")
*/
class NovelController extends Controller
{
/**
* Lists all Novel entities.
*
* @Route("/index.html", name="novel")
* @Template()
*/
public function indexAction()
{
$em = $this->getDoctrine()->getEntityManager();

$entities = $em->getRepository('MyFrontendBundle:Novel')->findAll();

return array('entities' => $entities);


}

/**
* 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.');
}

return array('entity' => $entity);


}

}
382 Część VII ♦ Relacje

W analogiczny sposób przygotuj kontrolery DetectiveController.php oraz Method


Controller.php.

Krok 12. Wykonaj widoki częściowe


Utwórz plik src/My/FrontendBundle/Resources/views/Novel/_list.html.twig o zawartości
takiej jak na listingu 34.12.

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>

Utwórz plik src/My/FrontendBundle/Resources/views/Detective/_list.html.twig o zawar-


tości takiej jak na listingu 34.13.

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

Utwórz plik src/My/FrontendBundle/Resources/views/Method/_list.html.twig o zawartości


takiej jak na listingu 34.14.

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>

Krok 13. Wykonaj widoki akcji Novel/index


W widoku akcji Novel/index umieść wywołanie widoku częściowego _list.html.twig z li-
stingu 34.9. Kod widoku Novel/index jest przedstawiony na listingu 34.15.

Listing 34.15. Widok akcji Novel/index


{% extends '::layout.html.twig' %}
{% block contents %}
<h2>Novels</h2>
{% include 'MyFrontendBundle:Novel:_list.html.twig' with {'entities': entities } %}
{% endblock contents %}

Krok 14. Wykonaj widoki akcji Method/show


W widoku akcji Novel/show umieść wywołanie widoku częściowego z listingu 34.9. Kod
widoku Novel/index jest przedstawiony na listingu 34.16.

Listing 34.16. Widok akcji Novel/index


{% extends '::layout.html.twig' %}

{% block contents %}
<h2>Method</h2>
<table class="record_properties">
<tbody>
<tr>
<th>Name</th>
384 Część VII ♦ Relacje

<td>{{ entity }}</td>


</tr>
</tbody>
</table>
<h2>Novels</h2>
{% include 'MyFrontendBundle:Novel:_list.html.twig'
with {'entities': entity.novels } %}
{% endblock contents %}

W analogiczny sposób wykonaj pozostałe widoki akcji show oraz index.

Krok 15. Dostosuj skórkę aplikacji


Pracę nad aplikacją zakończ, wykonując stronę powitalną z tekstem Lorem ipsum oraz mo-
dyfikując skórkę aplikacji. Wykorzystaj szablon zawarty w pliku 34-03-start.zip.

Krok 16. Sprawdź działanie aplikacji


Odwiedź w przeglądarce adres:
.../web/

Powinieneś ujrzeć witrynę przedstawioną na rysunku 34.1.

Rysunek 34.1. Witryna z przykładu 34.1


Rozdział 35.
Podsumowanie części VII
W części tej omówiliśmy relacje łączące klasy modelu. Szczegółowo omówiliśmy relacje:
 jeden do jednego,
 jeden do wielu,
 wiele do wielu.

W Doctrine 2 relacje definiujemy przy użyciu adnotacji zawartych w klasach Entity.


Każda relacja łączy dwie klasy. Jedną z tych klas nazywamy właścicielem relacji
(ang. owing side), a drugą — klasą odwrotną relacji (ang. inverse side). Właścicielem
relacji jest zawsze klasa, w której występuje parametr adnotacji inversedBy. Pojęcia
właściciela i klasy odwrotnej są istotne, gdyż aktualizację powiązań relacyjnych należy
wykonywać w klasie, która jest właścicielem relacji. W przeciwnym przypadku mo-
dyfikacje zostaną utracone.

Ostatni z rozdziałów przybliżył nam użycie poznanego w rozdziale 12. znacznika


Twig: {% include %}. Wykorzystując znacznik include, możemy przygotowywać widoki
częściowe, które wyeliminują redundancję kodu prezentującego rekordy zależne relacji.
386 Część VII ♦ Relacje
Część VIII
Panele CRUD
i zabezpieczanie dostępu
do aplikacji
388 Część VIII ♦ Panele CRUD i zabezpieczanie dostępu do aplikacji
Rozdział 36. ♦ Generowanie paneli administracyjnych CRUD 389

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.

W Symfony 2 panele CRUD generujemy poleceniem:


php app/console generate:doctrine:crud

Po wydaniu komendy generate:doctrine:crud należy ustalić nazwę modelu, dla którego


generujemy panel administracyjny. Jeśli w odpowiedzi na monit:
The Entity shortcut name:

podamy nazwę:
MyFrontendBundle:Name

wówczas panel administracyjny CRUD zostanie wygenerowany w pakiecie:


My/FrontendBundle

dla klasy zawartej w pliku:


My/FrontendBundle/Entity/Name.php

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

Wygenerowanie panelu administracyjnego polega na:


 utworzeniu kontrolera,
 utworzeniu widoków,
 utworzeniu klas formularzy.

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

W zależności od odpowiedzi, jakiej udzielimy na kolejne pytanie:


Do you want to generate the "write" actions [no]? yes

wygenerowane zostaną różne akcje. Jeśli na powyższe pytanie udzielimy odpowiedzi


domyślnej no, wówczas wygenerowane zostaną wyłącznie akcje:
 index — akcja wyświetlająca listę wszystkich rekordów;
 show — akcja wyświetlająca szczegółowe dane rekordu.

Tak wygenerowany kontroler nie będzie pozwalał na:


 tworzenie nowych rekordów,
 modyfikację rekordów,
 usuwanie rekordów.

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

odpowiemy yes, wówczas wygenerowane zostaną akcje zapewniające pełny zestaw


operacji CRUD:
 index — akcja wyświetlająca listę wszystkich rekordów;
 show — akcja wyświetlająca szczegółowe dane rekordu;
Rozdział 36. ♦ Generowanie paneli administracyjnych CRUD 391

 new — akcja wyświetlająca formularz do tworzenia nowego rekordu;


 create — akcja przetwarzająca formularz z akcji new;
 edit — akcja wyświetlająca formularz do edycji rekordu;
 update — akcja przetwarzająca formularz z akcji edit;
 delete — akcja usuwająca wybrany rekord.

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 create jest poprzedzona następującą adnotacją @Template():


@Template("MyFrontendBundle:Name:new.html.twig")
public function createAction()

zatem wykorzystuje ona widok akcji new.

W analogiczny sposób akcja update wykorzystuje widok akcji edit:


@Template("MyFrontendBundle:Name:edit.html.twig")
public function updateAction($id)

Akcja delete kończy się przekierowaniem, dlatego nie stosuje żadnego widoku.

Adresy URL akcji CRUD


Klasa kontrolera jest oznaczona adnotacją @Route przedstawioną na listingu 36.1. W ten
sposób wszystkie adresy URL wymienione w akcjach kontrolera będą dzieliły wspólny
przedrostek:
/name

Listing 36.1. Adnotacja @Route klasy NameController


/**
* Name controller.
*
* @Route("/name")
*/
class NameController extends Controller
{
...
}

Akcje panelu CRUD będą oznaczone adnotacjami @Route przedstawionymi na li-


stingu 36.2.
392 Część VIII ♦ Panele CRUD i zabezpieczanie dostępu do aplikacji

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

Adres ten wygenerujemy, umieszczając w widoku instrukcję:


<a href="{{ path('name') }}">
Lista wszystkich rekordów
</a>

Akcja show
Adresem akcji show dla rekordu o wartości klucza głównego 123 będzie:
.../web/name/123/show

Adres ten wygenerujemy, umieszczając w widoku instrukcję:


<a href="{{ path('name_show', { 'id': 123 }) }}">
pokaż szczegółowe dane rekordu
</a>
Rozdział 36. ♦ Generowanie paneli administracyjnych CRUD 393

Oczywiście do wygenerowania adresu możemy użyć obiektu klasy Name:


<a href="{{ path('name_show', { 'id': entity.id }) }}">
pokaż szczegółowe dane rekordu
</a>

Akcja new
Adresem akcji new będzie:
.../web/name/new

Adres ten wygenerujemy, umieszczając w widoku instrukcję:


<a href="{{ path('name_new') }}">
Utwórz nowy rekord
</a>

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

Adres ten wygenerujemy, umieszczając w widoku instrukcję:


<a href="{{ path('name_edit', { 'id': 123 }) }}">
Edytuj rekord
</a>

Oczywiście do wygenerowania adresu możemy użyć obiektu klasy Name:


<a href="{{ path('name_edit', { 'id': entity.id }) }}">
Edytuj rekord
</a>

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 }) }}" ...>

Do wygenerowania adresu możemy użyć obiektu klasy Name:


<form action="{{ path('name_update', { 'id': entity.id }) }}" ...>

Akcja delete
Adresem akcji delete usuwającej rekord, dla którego id = 123, będzie:
.../web/name/123/delete

Adres ten jest generowany w widoku akcji edit.html.twig:


<form action="{{ path('name_delete', { 'id': entity.id }) }}" method="post">

Ponowne generowanie paneli CRUD


Polecenie:
php app/console generate:doctrine:crud

zawiera zabezpieczenie chroniące przed utratą modyfikacji wprowadzonych w panelu


CRUD. Jeśli w pakiecie występuje już kontroler o nazwie pokrywającej się z generowa-
nym panelem CRUD, wówczas generowanie zakończy się błędem:
[RuntimeException]
Unable to generate the controller as it already exists.

W celu ponownego wygenerowania panelu CRUD należy najpierw ręcznie usunąć istnieją-
cy kontroler.

Panele CRUD a relacje


Panele CRUD zawierają podstawowe kontrolki umożliwiające edycję zależności rela-
cyjnych. Edycja zależności relacyjnych odbywa się po stronie klasy, która jest właści-
cielem relacji.

Przykład 36.1. Imiona — panel CRUD


Zmodyfikuj przykład z rozdziału 17. Wygeneruj w nim panel administracyjny umoż-
liwiający edycję rekordów z tabeli Name.
Rozdział 36. ♦ Generowanie paneli administracyjnych CRUD 395

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

Krok 2. Wygeneruj panel CRUD


Wydaj polecenie:
php app/console generate:doctrine:crud

W odpowiedzi na monit:
The Entity shortcut name:

podaj nazwę:
MyFrontendBundle:Name

Krok 3. Dostosuj adresy URL


W wygenerowanym kontrolerze My/FrontendBundle/Controller/NameController.php
usuń adnotację:
@Route("/name")

która poprzedza klasę:


class NameController extends Controller

Krok 4. Dostosuj etykiety formularza


W klasie formularza My/FrontendBundle/Form/NameType.php dodaj parametr meto-
dy add(), który ustali etykietę pola zawierającego imię. Kod zmodyfikowanej metody
buildForm() z pliku NameType.php jest przedstawiony na listingu 36.3.

Listing 36.3. Modyfikacja etykiety formularza


public function buildForm(FormBuilder $builder, array $options)
{
$builder
->add('caption', 'text', array('label' => 'Etykieta'))
;
}

Krok 5. Sprawdź działanie aplikacji


Odwiedź w przeglądarce adres:
.../web/
396 Część VIII ♦ Panele CRUD i zabezpieczanie dostępu do aplikacji

Po przejściu do edycji rekordu Abel ujrzysz stronę przedstawioną na rysunku 36.1.

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.

Przykład 36.2. Panel CRUD i relacja 1:1


Zmodyfikuj przykład z rozdziału 31. Wygeneruj w nim panel administracyjny umoż-
liwiający edycję rekordów z tabeli User.

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

Krok 2. Wygeneruj panel CRUD


Wydaj polecenie:
php app/console generate:doctrine:crud

W odpowiedzi na monit:
The Entity shortcut name:

podaj nazwę:
MyFrontendBundle:User
Rozdział 36. ♦ Generowanie paneli administracyjnych CRUD 397

Krok 3. Dostosuj adresy URL


W wygenerowanym kontrolerze My/FrontendBundle/Controller/UserController.php
usuń adnotację:
@Route("/name")

która poprzedza klasę:


class UserController extends Controller

Krok 4. Dostosuj formularze edycyjne


Domyślnie wygenerowany panel edycyjny dla relacji 1:1 nie pozwala edytować rekor-
dów z tabeli profil. Problem ten możemy rozwiązać na dwa sposoby:
 generując osobny panel CRUD dla modelu MyFrontendBundle:Profil,
 osadzając formularz do edycji modelu MyFrontendBundle:Profil w formularzu
do edycji modelu MyFrontendBundle:User.

Wykonajmy drugie z rozwiązań. Poleceniem:


php app/console generate:doctrine:form MyFrontendBundle:Profil

wygeneruj plik My/FrontendBundle/Form/ProfilType.php, który będzie zawierał kla-


sę formularza do edycji rekordów z tabeli profil. W wygenerowanej klasie ustal ety-
kietę pola o nazwie info oraz dodaj metodę getDefaultOptions(). Zarys kodu z pliku
ProfilType.php jest przedstawiony na listingu 36.4.

Listing 36.4. Klasa formularza do edycji rekordów z tabeli ProfilType


class ProfilType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder
->add('info', 'text', array('label' => 'Informacja o użytkowniku'))
;
}

public function getName()


{
return 'my_frontendbundle_profiltype';
}

public function getDefaultOptions(array $options)


{
return array(
'data_class' => 'My\FrontendBundle\Entity\Profil',
);
}

}
398 Część VIII ♦ Panele CRUD i zabezpieczanie dostępu do aplikacji

Następnie w klasie UserType, która jest zawarta w pliku My/FrontendBundle/Form/


UserType.php, wprowadź modyfikacje widoczne na listingu 36.5. Klasa UserType zo-
stała wygenerowana poleceniem generate:doctrine:crud.

Listing 36.5. Modyfikacje klasy UserType


class UserType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder
->add('name', 'text', array('label' => 'Imię użytkownika'))
->add('profil', new ProfilType(), array('label' => 'Profil
użytkownika'))
;
}

public function getName()


{
return 'my_frontendbundle_usertype';
}
}

Instrukcja:
->add('profil', new ProfilType(), array('label' => 'Profil użytkownika'))

modyfikuje pole profil zawarte w formularzu. Domyślnie pole to było wyświetlane


w postaci listy rozwijanej pozwalającej na wybór rekordu z tabeli profil. Powyższa
zmiana powoduje, że pole profil będzie osadzonym formularzem, który umożliwi
edycję rekordów z tabeli profil.

Krok 5. Włącz kaskadowość operacji zapisu dla obiektów User


W klasie Entity/User.php dodaj parametr cascade adnotacji @ORM\OneToOne:
/**
*
* @ORM\OneToOne(targetEntity="Profil", inversedBy="user", cascade={"all"})
* @ORM\JoinColumn(name="profil_id", referencedColumnName="id")
*/
private $profil;

Dzięki temu podczas zapisywania do bazy danych obiektu klasy User zapisany zostanie
także powiązany z nim relacyjnie obiekt klasy Profil.

Krok 6. Sprawdź działanie aplikacji


Odwiedź w przeglądarce adres:
.../web/

Po przejściu do edycji rekordu Jan ujrzysz stronę przedstawioną na rysunku 36.2.


Rozdział 36. ♦ Generowanie paneli administracyjnych CRUD 399

Rysunek 36.2.
Witryna
z przykładu 36.2

Przykład 36.3. Panel CRUD i relacja 1:n


Zmodyfikuj przykład z rozdziału 32. Wygeneruj w nim panel administracyjny umoż-
liwiający edycję rekordów z tabel kontynent oraz panstwo.

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

Krok 2. Wygeneruj panele CRUD


Poleceniem:
php app/console generate:doctrine:crud

wygeneruj panele administracyjne dla modeli:


MyFrontendBundle:Kontynent
MyFrontendBundle:Panstwo
400 Część VIII ♦ Panele CRUD i zabezpieczanie dostępu do aplikacji

Krok 3. Dostosuj widoki akcji


We wszystkich wygenerowanych widokach, tj. w plikach z folderów My/FrontendBundle/
Resources/views/Kontynent/ i My/FrontendBundle/Resources/views/Panstwo/, dodaj in-
strukcje włączające dekorację szablonem layout.html.twig. Na początku każdego pliku dodaj
instrukcje:
{% extends "::layout.html.twig" %}
{% block content %}

a na końcu:
{% endblock %}

Krok 4. Sprawdź działanie aplikacji


Odwiedź w przeglądarce adres:
.../web/

Po przejściu do edycji rekordu Kolumbia z tabeli panstwo ujrzysz stronę przedstawioną


na rysunku 36.3.

Rysunek 36.3.
Witryna
z przykładu 36.3
Rozdział 36. ♦ Generowanie paneli administracyjnych CRUD 401

Przykład 36.4. Panel CRUD i relacja n:m


Zmodyfikuj przykład z rozdziału 33. Wygeneruj w nim panel administracyjny umożli-
wiający edycję rekordów z tabel aktor oraz film.

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.

Krok 2. Wygeneruj panele CRUD


Poleceniem:
php app/console generate:doctrine:crud

wygeneruj panele administracyjne dla modeli:


MyFrontendBundle:Aktor
MyFrontendBundle:Film

Krok 3. Dostosuj widoki akcji


We wszystkich wygenerowanych widokach dodaj instrukcje włączające dekorację
szablonem layout.html.twig. Na początku każdego pliku dodaj instrukcje:
{% extends "::layout.html.twig" %}
{% block content %}

a na końcu:
{% endblock %}

Krok 4. Sprawdź działanie aplikacji


Odwiedź w przeglądarce adres:
.../web/

Po przejściu do edycji rekordu Szczęki z tabeli film ujrzysz stronę przedstawioną na


rysunku 36.4.

Domyślnie kontrolki edycyjne pozwalają wyłącznie na modyfikację rekordów w tabeli,


której klasa jest właścicielem relacji, czyli w tabeli film.
402 Część VIII ♦ Panele CRUD i zabezpieczanie dostępu do aplikacji

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.

Szczegółowy opis instalacji pakietu FOSUserBundle jest dostępny pod adresem:


https://github.com/FriendsOfSymfony/FOSUserBundle/blob/master/
Resources/doc/index.md

Przykład 37.1. Przygotowanie


dystrybucji symfony2-customized-v4
zawierającej pakiet FOSUserBundle
Przygotuj dystrybucję symfony2-customized-v4.zip, która będzie zawierała pakiet FOSU-
serBundle.

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

Krok 2. Zmodyfikuj plik deps


Na końcu pliku [projekt]/deps dodaj pakiet wymieniony na listingu 37.1.

Listing 37.1. Zawartość, którą należy dodać na końcu pliku deps


[FOSUserBundle]
git=git://github.com/FriendsOfSymfony/FOSUserBundle.git
target=bundles/FOS/UserBundle
version=1.2.0

Krok 3. Pobierz pakiety


Wydaj komendę:
php bin/vendors install --reinstall

Krok 4. Usuń foldery .git


Uruchom konsolę bash i przejdź do folderu symfony2-customized-v4/. Wydaj w nim
komendę:
find vendor -name .git -type d -exec rm -fr {} \;

Krok 5. Utwórz pakiet UserBundle


Komendą:
php app/console generate:bundle
--namespace=My/UserBundle --dir=src --no-interaction

utwórz pakiet My/UserBundle. Następnie utwórz folder My/UserBundle/Entity/ i umieść


w nim plik User.php o zawartości przedstawionej na listingu 37.2. W ten sposób w aplikacji
wystąpi model o logicznej nazwie:
MyUserBundle:User

Listing 37.2. Zawartość pliku My/UserBundle/Entity/User.php


<?php

namespace My\UserBundle\Entity;

use FOS\UserBundle\Entity\User as BaseUser;


use Doctrine\ORM\Mapping as ORM;

/**
* @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;

public function __construct()


{
parent::__construct();
}

Krok 6. Zarejestruj pakiet


W pliku AppKernel.php zarejestruj pakiet FOSUserBundle. Kod ilustrujący, jak to wy-
konać, jest przedstawiony na listingu 37.3.

Listing 37.3. Rejestracja pakietu FOSUserBundle w pliku AppKernel.php


...
$bundles = array(
...
new JMS\SecurityExtraBundle\JMSSecurityExtraBundle(),
new Symfony\Bundle\DoctrineFixturesBundle\DoctrineFixturesBundle(),
new Stof\DoctrineExtensionsBundle\StofDoctrineExtensionsBundle(),
new FOS\UserBundle\FOSUserBundle(),
);
...

Krok 7. Zarejestruj przestrzeń nazw


W pliku autoload.php zarejestruj przestrzeń nazw FOS. Kod ilustrujący, jak to wyko-
nać, jest przedstawiony na listingu 37.4.

Listing 37.4. Rejestracja przestrzeni nazw FOS 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',
'FOS' => __DIR__.'/../vendor/bundles',
));
...
406 Część VIII ♦ Panele CRUD i zabezpieczanie dostępu do aplikacji

Krok 8. Zmodyfikuj konfigurację zabezpieczeń


W pliku app/config/security.yml wprowadź kod1 przedstawiony na listingu 37.5.

Listing 37.5. Plik app/config/security.yml


security:
providers:
fos_userbundle:
id: fos_user.user_manager

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

Omówieniem opcji konfiguracyjnych formularza do logowania zajmiemy się w roz-


dziale 39.

Krok 9. Zmodyfikuj plik config.yml


W pliku app/config/config.yml wprowadź kod2 przedstawiony na listingu 37.6.

Listing 37.6. Plik app/config/config.yml


...

framework:
translator: ~
session:
default_locale: pl

...

fos_user:
db_driver: orm
firewall_name: main
user_class: My\UserBundle\Entity\User

Krok 10. Skonfiguruj adresy stron odpowiedzialnych za autoryzację


W pliku app/config/routing.yml wprowadź kod3 przedstawiony na listingu 37.7.

Listing 37.7. Plik app/config/routing.yml


fos_user_security:
resource: "@FOSUserBundle/Resources/config/routing/security.xml"

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"

odpowiada za adresy stron logowania oraz wylogowywania.

Reguła:
fos_user_profile:
resource: "@FOSUserBundle/Resources/config/routing/profile.xml"
prefix: /profile

definiuje adres strony do edycji profilu użytkownika.

Reguła:
fos_user_register:
resource: "@FOSUserBundle/Resources/config/routing/registration.xml"
prefix: /register

ustala adres strony rejestracji.

Reguła:
fos_user_resetting:
resource: "@FOSUserBundle/Resources/config/routing/resetting.xml"
prefix: /resetting

umożliwia resetowanie hasła.

Wreszcie reguła:
fos_user_change_password:
resource: "@FOSUserBundle/Resources/config/routing/change_password.xml"
prefix: /profile

wskazuje stronę zmiany hasła.

Krok 11. Skompresuj folder symfony2-customized-v4/


Skompresuj zawartość folderu symfony2-customized-v4/ do pliku symfony2-customized-
-v4.zip.

Tworzenie kont i nadawanie uprawnień


Informacje o użytkownikach, takie jak imię, nazwisko, adres e-mail oraz zaszyfrowane
hasło, będą zapisywane w bazie danych. Pakiet FOSUserBundle wykorzysta do tego klasę
zdefiniowaną wpisem user_class widocznym na listingu 37.6:
fos_user:
user_class: My\UserBundle\Entity\User
Rozdział 37. ♦ Instalacja pakietu FOSUserBundle 409

Z powyższego wpisu wynika, że do kontaktu z bazą danych wykorzystany zostanie


model User widoczny na listingu 37.4.

Do zarządzania kontami służą polecenia:


php app/console fos:user:create
php app/console fos:user:activate
php app/console fos:user:deactivate
php app/console fos:user:promote
php app/console fos:user:demote

Tworzenie kont
Nowe konto tworzymy komendą:
php app/console fos:user:create admin admin@example.com sEcrETpAssWord

Komenda ta może przyjąć dodatkowe parametry:


--super-admin
--inactive

Parametr --super-admin powoduje, że tworzone konto będzie miało pełne uprawnienia


administracyjne.

Parametr --inactive powoduje, że nowo utworzone konto będzie nieaktywne. Konto


będzie nieczynne aż do wydania komendy fos:user:activate.

Aktywacja i deaktywacja konta


Komenda:
php app/console fos:user:activate admin

zmienia status konta admin na aktywne, a komenda:


php app/console fos:user:deactivate admin

zmienia status konta na nieaktywne.

Nadawanie i usuwanie uprawnień administracyjnych


Komenda:
php app/console fos:user:promote admin

nadaje pełne uprawnienia administracyjne kontu admin.

Komenda:
php app/console fos:user:demote admin

usuwa uprawnienia administracyjne.


410 Część VIII ♦ Panele CRUD i zabezpieczanie dostępu do aplikacji

Przykład 37.2. Sprawdzenie działania


dystrybucji symfony2-customized-v4
Sprawdź działanie stron dotyczących autoryzacji użytkowników, które są zawarte w dys-
trybucji symfony2-customized-v4.zip. Utwórz bazę danych i sprawdź zawartość wygenero-
wanej tabeli user. Następnie poleceniem fos:user:create dodaj konto administratora
i odwiedź wszystkie adresy wymienione na listingu 37.7.

ROZWIĄZANIE
Krok 1. Wypakuj dystrybucję symfony2-customized-v4.zip
Wypakuj archiwum symfony2-customized-v4.zip.

Krok 2. Utwórz bazę danych


Wykorzystując plik 00-dodatki/tworzenie-pustej-bazy-danych/tworzenie-pustej-bazy-
-danych.sql, utwórz pustą bazę danych o nazwie symfony2sandbox. Konfiguracja dostępu
do bazy jest już wprowadzona w pliku config/properties.ini.

Krok 3. Uaktualnij strukturę bazy danych


Wydaj komendę:
php app/console doctrine:schema:update --force

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.

Rysunek 37.1. Struktura tabeli fos_user


Rozdział 37. ♦ Instalacja pakietu FOSUserBundle 411

Nazwa tabeli fos_user jest zdefiniowana adnotacją:


* @ORM\Table(name="fos_user")

zawartą w pliku User.php widocznym na listingu 37.4.

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

Krok 4. Utwórz konto admin


Wydaj komendy:
php app/console fos:user:create admin admin@example.com sEcrETpAssWord
php app/console fos:user:promote admin --super

po czym sprawdź zawartość tabeli fos_user. Pojawi się w niej rekord odpowiadający
utworzonemu kontu.

Krok 5. Sprawdź działanie adresów URL


W przeglądarce internetowej odwiedź adresy:
.../web/login
.../web/register

Pierwszy z nich powoduje wyświetlenie strony widocznej na rysunku 37.2, a drugi


— strony z rysunku 37.3.

Rysunek 37.2.
Strona /web/login

Zaloguj się w serwisie, po czym odwiedź adres:


.../web/profile

Ujrzysz wówczas stronę z rysunku 37.4.


412 Część VIII ♦ Panele CRUD i zabezpieczanie dostępu do aplikacji

Rysunek 37.3.
Strona /web/register

Rysunek 37.4.
Strona /web/profile

Po odwiedzeniu adresu:
.../web/profile/edit
ujrzysz stronę z rysunku 37.5.

Następnie odwiedź adres:


.../web/profile/change-password

Teraz strona będzie wyglądała tak jak na rysunku 37.6.

Na zakończenie odwiedź adres:


.../web/resetting/request

Ujrzysz stronę z rysunku 37.7.

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

Pakiet FOSUserBundle definiuje następujące adresy URL:


 /web/login — strona do logowania;
 /web/login_check — strona przetwarzająca formularz do logowania;
 /web/logout — strona do wylogowywania;
414 Część VIII ♦ Panele CRUD i zabezpieczanie dostępu do aplikacji

 /web/register — strona rejestracji;


 /web/profile — wyświetlenie danych użytkownika;
 /web/profile/edit — edycja danych użytkownika;
 /web/profile/change-password — zmiana hasła użytkownika;
 /web/resetting/request — odzyskiwanie hasła.

Powyższe adresy będą dostępne wyłącznie wtedy, gdy w pliku app/config/routing.yml


dodamy reguły z listingu 37.7. Jeśli w pliku app/config/routing.yml pojawi się wyłącznie
jedna reguła:
fos_user_security:
resource: "@FOSUserBundle/Resources/config/routing/security.xml"

wówczas jedynymi dostępnymi adresami będą adresy zawarte w pliku FOSUserBundle/


Resources/config/routing/security.yml, czyli:
.../web/login
.../web/login_check
.../web/logout

Polskie komunikaty są wyświetlane dzięki temu, że w pliku konfiguracyjnym app/config/


config.yml zmodyfikowaliśmy wpis:
framework:
translator: ~

Tłumaczenia są pobierane z plików:


\vendor\bundles\FOS\UserBundle\Resources\translations\FOSUserBundle.pl.yml
\vendor\bundles\FOS\UserBundle\Resources\translations\validators.pl.yml
Rozdział 38.
Aplikacja
dostępna wyłącznie
dla zdefiniowanych
użytkowników
Wykonanie aplikacji, która jest dostępna wyłącznie dla zalogowanych użytkowników,
sprowadza się do:
 utworzenia konta dostępu z uprawnieniami administracyjnymi;
 usunięcia reguł routingu, które pozwalają na rejestrację użytkowników;
 ustalenia uprawnień dostępu do aplikacji.

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$

ustala zabezpieczenia dla jednego konkretnego adresu URL:


/login
416 Część VIII ♦ Panele CRUD i zabezpieczanie dostępu do aplikacji

natomiast parametr:
path: ^/

dotyczy wszystkich adresów URL rozpoczynających się od znaku /. Podana reguła


obejmie więc wszystkie adresy URL w aplikacji.

Drugi parametr role ustala zakres uprawnień. Wartość:


role: IS_AUTHENTICATED_ANONYMOUSLY

pozwala na uzyskanie dostępu do danego adresu URL niezalogowanym użytkownikom.

Kolejność reguł w pliku security.yml jest bardzo istotna. Do weryfikacji uprawnień


użytkownika zastosowana będzie pierwsza reguła, której parametr path odpowiada ad-
resowi URL odwiedzanego zasobu.

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

użytkownik musi posiadać rolę ROLE_LOREM_IPSUM. Jeśli korzystamy z wtyczki FOSUser


´Bundle, role użytkowników należy rozpoczynać od przedrostka ROLE_. Poprawnymi
nazwami ról są:
ROLE_LOREM_IPSUM
ROLE_EDITOR
ROLE_RESELLER
ROLE_TAG_ADMIN

Standardowo w Symfony 2 dostępne są następujące role:


IS_AUTHENTICATED_ANONYMOUSLY
ROLE_SUPER_ADMIN

Pierwsza z nich jest zdefiniowana w pliku:


\vendor\symfony\src\Symfony\Component\Security\
Core\Authorization\Voter\AuthenticatedVoter.php
Rozdział 38. ♦ Aplikacja dostępna wyłącznie dla zdefiniowanych użytkowników 417

w postaci stałej w klasie AuthenticatedVoter:


...
class AuthenticatedVoter implements VoterInterface
{
...
const IS_AUTHENTICATED_FULLY = 'IS_AUTHENTICATED_FULLY';
const IS_AUTHENTICATED_REMEMBERED = 'IS_AUTHENTICATED_REMEMBERED';
const IS_AUTHENTICATED_ANONYMOUSLY = 'IS_AUTHENTICATED_ANONYMOUSLY';
...
}

Druga, ROLE_SUPER_ADMIN, występuje w pliku:


vendor\bundles\FOS\UserBundle\Model\User.php

jako stała w klasie User:


...
abstract class User implements UserInterface, GroupableInterface
{
...
const ROLE_DEFAULT = 'ROLE_USER';
const ROLE_SUPER_ADMIN = 'ROLE_SUPER_ADMIN';
...
}

Rola IS_AUTHENTICATED_ANONYMOUSLY pozwala na korzystanie z adresu URL niezalogo-


wanym użytkownikom. Tego typu zabezpieczenie będziemy stosowali m.in. do formula-
rza logowania.

Rola ROLE_SUPER_ADMIN będzie przyznawana użytkownikom o pełnych uprawnieniach


administracyjnych. Posiadanie uprawnień ROLE_SUPER_ADMIN pozwoli na wykonywanie
wszelkich operacji dostępnych w interfejsie aplikacji.

Nadawanie, usuwanie i sprawdzanie


uprawnień użytkownikom
W jaki sposób nadawać, usuwać oraz sprawdzać uprawnienia? Służą do tego poznane już
polecenia:
php app/console fos:user:promote
php app/console fos:user:demote

Pierwsze z nich nadaje uprawnienia, a drugie — usuwa.

Utwórzmy konto o nazwie inny:


php app/console fos:user:create inny inny@example.com secretPassword

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

Jeśli w programie phpMyAdmin przejdziemy do strony edycji rekordu inny, ujrzymy


zawartość przedstawioną na rysunku 38.1.

Rysunek 38.1.
Edycja rekordu
użytkownika o nazwie
inny

W kolumnie roles znajdziemy zawartość:


a:0:{}

Jest to poddana serializacji pusta tablica. Po wydaniu polecenia:


php app/console fos:user:promote inny ROLE_ABC_DEF

w kolumnie roles użytkownika inny zapisana zostanie wartość:


a:1:{i:0;s:12:"ROLE_ABC_DEF";}

Wartość ta odpowiada tablicy:


array("ROLE_ABC_DEF")

Innymi słowy: użytkownik posiada uprawnienie ROLE_ABC_DEF.


Rozdział 38. ♦ Aplikacja dostępna wyłącznie dla zdefiniowanych użytkowników 419

Wydajmy kolejne dwa polecenia:


php app/console fos:user:promote inny ROLE_XXX
php app/console fos:user:promote inny ROLE_YYY

Teraz w kolumnie roles znajdziemy:


a:3:{i:0;s:12:"ROLE_ABC_DEF";i:1;s:8:"ROLE_XXX";i:2;s:8:"ROLE_YYY";}

Powyższa wartość jest otrzymywana po serializacji tablicy:


array("ROLE_ABC_DEF","ROLE_XXX", "ROLE_YYY")

Teraz użytkownik posiada trzy uprawnienia:


ROLE_ABC_DEF
ROLE_XXX
ROLE_YYY

W ten sposób wydając polecenie fos:user:promote, możemy nadać użytkownikowi do-


wolną liczbę uprawnień. Specjalny parametr --super:
php app/console fos:user:promote inny --super

nadaje uprawnienie ROLE_SUPER_ADMIN, co możemy stwierdzić, zaglądając do tabeli


fos_user:
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";
}

Poleceniem fos:user:demote możemy przyznać dowolne z przyznanych wcześniej


uprawnień:
php app/console fos:user:demote inny ROLE_ABC_DEF
php app/console fos:user:demote inny ROLE_XXX
php app/console fos:user:demote inny ROLE_YYY
php app/console fos:user:demote inny --super

Przykład 38.1. Korona ziemi


Wykonaj aplikację prezentującą zestawienie najwyższych szczytów górskich na wszyst-
kich kontynentach. Zadanie wykonaj w taki sposób, by aplikacja była dostępna wyłącz-
nie po zalogowaniu. W aplikacji utwórz jedno konto admin oraz wyłącz możliwość
rejestracji w systemie. Zalogowany użytkownik powinien mieć prawo do pełnej edy-
cji wszystkich danych zawartych w bazie.
420 Część VIII ♦ Panele CRUD i zabezpieczanie dostępu do aplikacji

ROZWIĄZANIE
ETAP I
W pierwszym etapie pracy przygotujemy aplikację dostępną bez ograniczeń.

Krok 1. Utwórz nowy projekt i pakiet


W folderze przeznaczonym na aplikacje WWW utwórz folder zad-38/ i wypakuj do
niego zawartość archiwum symfony2-customized-v4.zip. Następnie utwórz pakiet
My/BackendBundle. W wygenerowanym pakiecie usuń kontroler DefaultController
(wraz z jego widokami). Na zakończenie pierwszego kroku w folderze data/ umieść
plik XML.

Krok 2. Utwórz bazę danych koronaziemi


Utwórz bazę danych o nazwie koronaziemi oraz konto dostępu editor, po czym w pliku
parameters.ini zmodyfikuj opcje ustalające parametry połączenia z bazą.

Krok 3. Wygeneruj model


Wygeneruj model:
MyBackendBundle:Mountain

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.

Po wygenerowaniu modelu MyBackendBundle:Mountain wydaj polecenie:


php app/console doctrine:schema:update --force

Krok 4. Wypełnij bazę danych zawartością odczytaną z pliku XML


Przygotuj skrypt LoadData.php, który bazę danych koronaziemi wypełni zawartością
pliku XML.

Krok 5. Wygeneruj panel CRUD


Wydaj polecenie:
php app/console doctrine:generate:crud

W odpowiedzi na monit:
The Entity shortcut name:
Rozdział 38. ♦ Aplikacja dostępna wyłącznie dla zdefiniowanych użytkowników 421

podaj nazwę modelu:


MyBackendBundle:Mountain

Pamiętaj, by na pytanie:
Do you want to generate the "write" actions [no]?

odpowiedzieć twierdząco (yes).

W wygenerowanym kontrolerze MountainController.php usuń adnotację @Route


´("/mountain"), która poprzedza klasę MountainController. Zarys zmodyfikowane-
go kontrolera jest przedstawiony na listingu 38.1.

Listing 38.1. Zmodyfikowany kontroler MountainController


...
/**
* Mountain controller.
*/
class MountainController extends Controller
{
...
}

Krok 6. Zmodyfikuj widoki panelu CRUD


W czterech wygenerowanych widokach panelu CRUD:
edit.html.twig
index.html.twig
new.html.twig
show.html.twig

dodaj instrukcje włączające dekorację szablonem base.html.twig:


{% extends '::base.html.twig' %}
{% block body %}
...
{% endblock body %}

Krok 7. Sprawdź działanie aplikacji


Odwiedź w przeglądarce adres:
.../web/

Ujrzysz stronę przedstawioną na rysunku 38.2.

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

Krok 8. Utwórz konto dostępu do aplikacji


Wydaj polecenia:
php app/console fos:user:create admin admin@example.com sEcrETpAssWord
php app/console fos:user:promote admin --super

Pierwsze polecenie tworzy konto admin, które nie ma żadnych uprawnień.

Drugie polecenie nadaje kontu admin uprawnienie ROLE_SUPER_ADMIN, zapewniające pełny


dostęp do wszystkich adresów aplikacji.

Krok 9. Usuń zbędne reguły routingu


W pliku app/config/routing.yml usuń wpisy włączające adresy stron rejestracji oraz edy-
cji profilu. Zmodyfikowany plik routing.yml będzie zawierał wyłącznie dwie reguły
oznaczone etykietami MyBackendBundle oraz fos_user_security. Treść zmodyfikowane-
go pliku app/config/routing.yml jest przedstawiona na listingu 38.2.

Listing 38.2. Zmodyfikowany plik app/config/routing.yml


MyBackendBundle:
resource: "@MyBackendBundle/Controller/"
type: annotation
prefix: /

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

żaden użytkownik (nawet posiadający uprawnienie ROLE_SUPER_ADMIN) nie uzyska dostę-


pu do strony o adresie:
/register

Krok 10. Zmodyfikuj uprawnienia dostępu do aplikacji


W pliku app/config/security.yml zmodyfikuj wpisy oznaczone etykietą access_control
oraz usuń wpisy oznaczone etykietą role_hierarchy:
role_hierarchy:
ROLE_ADMIN: ROLE_USER
ROLE_SUPER_ADMIN: ROLE_ADMIN

Fragment zmodyfikowanego pliku security.yml jest przedstawiony na listingu 38.3.

Listing 38.3. Zmodyfikowany plik app/config/security.yml


...
access_control:
- { path: ^/login$, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/, role: ROLE_SUPER_ADMIN }

Reguła:
- { path: ^/login$, role: IS_AUTHENTICATED_ANONYMOUSLY }

udostępnia publicznie adres /login.

Reguła:
- { path: ^/, role: ROLE_SUPER_ADMIN }

Udostępnia wszystkie adresy rozpoczynające się od / (czyli wszystkie adresy w aplikacji)


wyłącznie użytkownikowi zalogowanemu z uprawnieniami ROLE_SUPER_ADMIN.

Krok 11. Dodaj hiperłącze do wylogowania


W widoku base.html.twig dodaj hiperłącze służące do wylogowania. Zarys widoku
base.html.twig jest przedstawiony na listingu 38.4.

Listing 38.4. Widok base.html.twig


<body>
{% if is_granted('ROLE_SUPER_ADMIN') %}
<p>
<a href="{{ path('fos_user_security_logout') }}">
{{ 'layout.logout'|trans({}, 'FOSUserBundle') }}
424 Część VIII ♦ Panele CRUD i zabezpieczanie dostępu do aplikacji

</a>
</p>
{% endif %}
{% block body %}{% endblock %}
{% block javascripts %}{% endblock %}
</body>

Instrukcja if gwarantuje, że hiperłącze Wyloguj będzie dostępne wyłącznie po zalo-


gowaniu na konto admin.

Tekst zawarty w hiperłączu drukujemy instrukcją:


{{ 'layout.login'|trans({}, 'FOSUserBundle') }}

Instrukcja ta wydrukuje komunikat Zaloguj w odpowiednim języku. Tłumaczenie odby-


wać się będzie w następujący sposób:
 Tłumaczony tekst będzie pobrany ze słownika zawartego w pakiecie
FOSUserBundle (mówi o tym drugi parametr funkcji trans).
 Na podstawie parametru FOSUserBundle wiadomo, że do tłumaczenia wykorzystane
zostaną słowniki z folderu vendor\bundles\FOS\UserBundle\Resources\
translations.
 Opcja konfiguracyjna default_locale: pl (plik app/config.yml) ustala,
że językiem docelowym jest język polski.
 Wykorzystanym słownikiem będzie więc plik FOSUserBundle.pl.yml.
 Parametr layout.login jest kluczem, którego wartość będzie wydrukowana.

Krok 12. Dostosuj widok strony do logowania


W celu przygotowania własnego formularza do logowania utwórz folder app\Resources\
FOSUserBundle\views\Security i umieść w nim plik login.html.twig o zawartości
przedstawionej na listingu 38.5.

Listing 38.5. Dostosowany formularz do logowania


{% extends "::base.html.twig" %}

{% block body %}
{% if error %}
<div>{{ error|trans({}, 'FOSUserBundle') }}</div>
{% endif %}

<form action="{{ path("fos_user_security_check") }}" method="post">


<input type="hidden" name="_csrf_token" value="{{ csrf_token }}" />

<label for="username">{{ 'security.login.username'|trans({},


´'FOSUserBundle') }}</label>
<input type="text" id="username" name="_username" value="{{
´last_username }}" />

<label for="password">{{ 'security.login.password'|trans({},


´'FOSUserBundle') }}</label>
Rozdział 38. ♦ Aplikacja dostępna wyłącznie dla zdefiniowanych użytkowników 425

<input type="password" id="password" name="_password" />

<input type="checkbox" id="remember_me" name="_remember_me" value="on" />


<label for="remember_me">{{ 'security.login.remember_me'|trans({},
´'FOSUserBundle') }}</label>

<input type="submit" id="_submit" name="_submit" value="{{


´'security.login.submit'|trans({}, 'FOSUserBundle') }}" />
</form>

{% for key, message in app.session.getFlashes() %}


<div class="{{ key }}">
{{ message|trans({}, 'FOSUserBundle') }}
</div>
{% endfor %}

{% endblock body %}

Nadpisanie widoku dowolnej akcji z dowolnego pakietu sprowadza się do utworzenia


odpowiedniego pliku w folderze app/Resources. Oryginalny widok akcji logowania
jest zawarty w pliku:
vendor\bundles\FOS\UserBundle\Resources\views\Security\login.html.twig

dlatego w celu nadpisania go należy utworzyć plik:


app\Resources\FOSUserBundle\views\Security\login.html.twig

Krok 13. Wyczyść pamięć podręczną


Usuń zawartość folderu app/cache/.

Krok 14. Sprawdź działanie aplikacji


Odwiedź w przeglądarce adres:
.../web/

Tym razem ujrzysz formularz do logowania. Po poprawnym zalogowaniu się na konto:


Nazwa użytkownika: admin
Hasło: sEcrETpAssWord
ujrzysz stronę przedstawioną na rysunku 38.2. Nazwę konta oraz hasło ustaliliśmy w kro-
ku 8. poleceniem:
php app/console fos:user:create

Komenda:
php app/console fos:user:promote admin --super

modyfikuje uprawnienia konta admin, nadając mu status administratora (czyli ROLE_


´SUPER_ADMIN).
426 Część VIII ♦ Panele CRUD i zabezpieczanie dostępu do aplikacji

Jeśli użyjesz skryptu działającego w środowisku dev:


.../web/app_dev.php/

i po zalogowaniu na konto klikniesz odsyłacz wskazany na rysunku 38.3, ujrzysz wów-


czas tabelę prezentującą uprawnienia, które posiada zalogowany użytkownik. Tabela taka
jest przedstawiona na rysunku 38.4.

Rysunek 38.3. Odsyłacz umożliwiający sprawdzenie uprawnień bieżącego użytkownika

Rysunek 38.4.
Tabela ułatwiająca
sprawdzanie, jakimi
uprawnieniami
dysponuje zalogowany
użytkownik

Krok 15. Sprawdź działanie komend nadających


i usuwających uprawnienia
Wydaj komendy:
php app/console fos:user:create inny inny@example.com secretPassword
php app/console fos:user:promote inny ROLE_ABC_DEF
php app/console fos:user:promote inny ROLE_XXX
php app/console fos:user:promote inny ROLE_YYY
php app/console fos:user:promote inny --super
Rozdział 38. ♦ Aplikacja dostępna wyłącznie dla zdefiniowanych użytkowników 427

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";
}

Następnie wydaj polecenia:


php app/console fos:user:demote inny ROLE_ABC_DEF
php app/console fos:user:demote inny --super

i przekonaj się, że teraz użytkownik inny ma uprawnienia:


a:2:{
i:0;s:8:"ROLE_XXX";
i:1;s:8:"ROLE_YYY";
}

Hierarchia ról
Wpis:
role_hierarchy:
ROLE_ADMIN: ROLE_USER
ROLE_SUPER_ADMIN: ROLE_ADMIN

tworzy zależność hierarchiczną ról. Reguła:


ROLE_ADMIN: ROLE_USER

ustala, że uprawnienie ROLE_ADMIN zawiera w sobie wszystkie uprawnienia ROLE_USER.


Reguła:
ROLE_SUPER_ADMIN: ROLE_ADMIN

ustala natomiast, że uprawnienie ROLE_SUPER_ADMIN zawiera wszystkie uprawnienia


ROLE_ADMIN.

W ten sposób w aplikacji będą zdefiniowane trzy poziomy zabezpieczeń:


ROLE_USER < ROLE_ADMIN < ROLE_SUPER_ADMIN
428 Część VIII ♦ Panele CRUD i zabezpieczanie dostępu do aplikacji
Rozdział 39.
Aplikacja
dostępna publicznie
w trybie do odczytu
W przypadku aplikacji internetowych bardzo często stosowany jest dwustopniowy
podział uprawnień. Zasoby aplikacji są dostępne publicznie w trybie do odczytu. Każdy
odwiedzający użytkownik może przeglądać strony WWW zawarte w aplikacji bez ko-
nieczności logowania się. Za udostępnianie zasobów w trybie do odczytu odpowiada
fragment aplikacji określany terminem frontend.

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.

W poprzednich częściach podręcznika dowiedzieliśmy się, w jaki sposób wykonać


projekt zawierający tylko frontend lub tylko backend. Teraz przejdziemy do połączenia
obu rozwiązań.

Przykład 39.1. Korona ziemi


— podział na frontend oraz backend
Zmodyfikuj przykład z rozdziału 38. w taki sposób, by zestawienie szczytów było do-
stępne publicznie w trybie do odczytu. Na stronie głównej aplikacji umieść hiperłącze
umożliwiające zalogowanie się. Użytkownik zalogowany z uprawnieniami ROLE_ADMIN
powinien uzyskać dostęp do panelu administracyjnego CRUD umożliwiającego edycję
rekordów zawartych w tabeli mountain.
430 Część VIII ♦ Panele CRUD i zabezpieczanie dostępu do aplikacji

ROZWIĄZANIE
Krok 1. Utwórz pakiet My/FrontendBundle
W ukończonym projekcie z rozdziału 38. utwórz pakiet My/FrontendBundle.

Krok 2. Dostosuj akcję index w pakiecie FrontendBundle


Zmodyfikuj zawartość pliku FrontendBundle/Controller/DefaultController.php. Wpro-
wadź w nim zawartość przedstawioną na listingu 39.1.

Listing 39.1. Kontroler DefaultController pakietu FrontendBundle


<?php

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;

class DefaultController extends Controller


{
/**
* Lists all Mountain entities.
*
* @Route("/", name="frontend")
* @Template()
*/
public function indexAction()
{
$em = $this->getDoctrine()->getEntityManager();

$entities = $em->getRepository('MyBackendBundle:Mountain')->findAll();

return array('entities' => $entities);


}
}

Następnie dostosuj widok akcji z listingu 39.1 tak, by prezentował tabelkę HTML.

Krok 3. Zmodyfikuj adresy panelu CRUD


Aplikacja zawiera w tej chwili trzy utworzone przez nas pakiety:
 FrontendBundle,
 BackendBundle,
 UserBundle.

W pliku routing.yml zmodyfikuj parametr prefix w regule dotyczącej pakietu Backend


´Bundle. Zmodyfikowany plik routing.yml jest przedstawiony na listingu 39.2.
Rozdział 39. ♦ Aplikacja dostępna publicznie w trybie do odczytu 431

Listing 39.2. Plik routing.yml po zmodyfikowaniu adresów panelu CRUD


MyFrontendBundle:
resource: "@MyFrontendBundle/Controller/"
type: annotation
prefix: /

MyBackendBundle:
resource: "@MyBackendBundle/Controller/"
type: annotation
prefix: /admin

fos_user_security:
resource: "@FOSUserBundle/Resources/config/routing/security.xml"

W ten sposób w aplikacji zdefiniowane są następujące adresy URL:


 /login — adres do formularza logowania;
 /login_check — adres skryptu przetwarzającego formularz logowania;
 /logout — adres umożliwiający wylogowanie;
 / — adres do akcji index w kontrolerze DefaultController pakietu FrontendBundle;
 /admin/xxx — adresy do akcji w kontrolerze MountainController pakietu
BackendBundle.

Krok 4. Ustal uprawnienia do aplikacji


W pliku app/config/security.yml wprowadź zawartość listingu 39.3.

Listing 39.3. Ustalenie uprawnień do witryny


access_control:
- { path: ^/login$, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/admin, role: ROLE_SUPER_ADMIN }
- { path: ^/, role: IS_AUTHENTICATED_ANONYMOUSLY }

Reguła:
- { path: ^/admin, role: ROLE_SUPER_ADMIN }

powoduje, że do stron, których adres rozpoczyna się od /admin, np.:


/web/admin/1/edit
/web/admin/2/show
/web/admin/3/delete
dostęp uzyskają tylko użytkownicy posiadający uprawnienie ROLE_SUPER_ADMIN.

Reguła:
- { path: ^/, role: IS_AUTHENTICATED_ANONYMOUSLY }

powoduje, że wszystkie adresy będą dostępne bez konieczności logowania się.


432 Część VIII ♦ Panele CRUD i zabezpieczanie dostępu do aplikacji

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.

Krok 5. Dostosuj szablon witryny


W pliku base.html.twig dodaj menu zawierające opcje edytuj, przeglądaj oraz wyloguj.
Fragment pliku base.html.twig jest przedstawiony na listingu 39.4.

Listing 39.4. Zarys pliku base.html.twig


<body>
{% if is_granted('ROLE_SUPER_ADMIN') %}
<ul>
<li><a href="{{ path('mountain') }}">edytuj</a></li>
<li><a href="{{ path('frontend') }}">przeglądaj</a></li>
<li><a href="{{ path('fos_user_security_logout') }}">wyloguj</a></li>
</ul>
{% else %}
<ul>
<li><a href="{{ path('fos_user_security_login') }}">zaloguj</a></li>
</ul>
{% endif %}
{% block body %}{% endblock %}
{% block javascripts %}{% endblock %}
</body>

Oczywiście jeśli przygotowujesz witrynę wyłącznie w języku polskim, wówczas teksty


zaloguj i wyloguj możesz drukować bez wykorzystania translatora.

Krok 6. Sprawdź działanie aplikacji


Odwiedź w przeglądarce adres:
.../web/

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 te ustalają sposób działania formularza do logowania, w szczególności przekie-


rowania.

Dokumentacja opcji konfiguracyjnych formularza do logowania (klucz login_form)


jest dostępna na stronie:
http://symfony.com/doc/current/cookbook/security/form_login.html

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.

Wykonaj następujące ćwiczenie: w aplikacji z przykładu 39.1 ustal wartość opcji:


use_forward: false
następnie wyloguj się z aplikacji i odwiedź adres wymagający logowania:
.../web/admin/1/edit
Na stronie ujrzysz formularz logowania, ale adres strony nie ulegnie zmianie. Ciągle
będzie to:
.../web/admin/1/edit

Opcja:
post_only: true

ustala, że formularz do logowania musi być przekazany metodą post.

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

decydują o tym, co ma się dziać w przypadku poprawnego zakończenia operacji logowania.


Domyślnie użytkownik zostanie przekierowany do strony, którą ostatnio odwiedził po zalo-
gowaniu. Strona ta jest zapamiętana w sesji. Parametr:
default_target_path: /

ustala domyślny adres strony, na którą zostanie przekierowany użytkownik po zalogowaniu.


Parametr ten będzie użyty wyłącznie wtedy, gdy w sesji użytkownika nie ma zapamiętanego
ostatnio odwiedzonego adresu. Jeśli parametrowi:
always_use_default_target_path: false

nadamy wartość true, to zawsze po zalogowaniu użytkownik będzie kierowany na stronę


ustaloną parametrem default_target_path.

Opcja:
use_referer: false

wskazuje, że po zalogowaniu użytkownik powinien być przekierowany do strony, którą


chciał odwiedzić (nie mając uprawnień).

Ostatni z parametrów:
target_path_parameter: _target_path

umożliwia ustalenie w formularzu adresu, na który mamy przekierować użytkownika.


Wartość parametru jest nazwą dla kontrolki ukrytej. Jeśli w formularzu wystąpi kontrolka
o podanej nazwie, wówczas przekierowanie nastąpi pod adres, który jest wartością kontrolki.

Parametr:
failure_path: null

ustala adres, na który mamy przekierować użytkownika po nieudanej próbie logowania.


Parametr:
failure_forward: false

odgrywa natomiast rolę identyczną jak use_forward — wyłącza wykonywanie przekie-


rowań HTTP po nieudanej próbie logowania.

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

 osadzenia kontrolera FOSUserBundle:Security:login w szablonie strony


base.html.twig (lub layout.html.twig),
 modyfikacji właściwości formularza przy użyciu opcji form_login,
 modyfikacji dostępnych adresów URL.

Przykład 39.2. Korona ziemi


— osadzenie formularza
do logowania w pliku base.html.twig
Zmodyfikuj przykład 39.1 w taki sposób, by formularz do logowania był zawarty w pliku
base.html.twig.

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

Listing 39.5. Widok częściowy formularza do logowania


app\Resources\FOSUserBundle\views\Security\login.html.twig
{% if error %}
<div>{{ error|trans({}, 'FOSUserBundle') }}</div>
{% endif %}

<form action="{{ path("fos_user_security_check") }}" method="post">


<input type="hidden" name="_csrf_token" value="{{ csrf_token }}" />

<label for="username">{{ 'security.login.username'|trans({}, 'FOSUserBundle')


´}}</label>
<input type="text" id="username" name="_username" value="{{ last_username }}" />

<label for="password">{{ 'security.login.password'|trans({}, 'FOSUserBundle')


´}}</label>
<input type="password" id="password" name="_password" />

<input type="checkbox" id="remember_me" name="_remember_me" value="on" />


<label for="remember_me">{{ 'security.login.remember_me'|trans({},
´'FOSUserBundle') }}</label>

<input type="submit" id="_submit" name="_submit" value="{{


´'security.login.submit'|trans({}, 'FOSUserBundle') }}" />
436 Część VIII ♦ Panele CRUD i zabezpieczanie dostępu do aplikacji

</form>

{% for key, message in app.session.getFlashes() %}


<div class="{{ key }}">
{{ message|trans({}, 'FOSUserBundle') }}
</div>
{% endfor %}

Krok 2. Osadź formularz w pliku base.html.twig


W pliku base.html.twig umieść kod z listingu 39.6. Instrukcja:
{% render 'FOSUserBundle:Security:login' %}

przetworzy akcję FOSUserBundle:Security:login i wygeneruje niezbędne zmienne


(m.in. csrf_token). Zmienne te zostaną wykorzystane do przetworzenia widoku z listingu
39.5. W ten sposób wewnątrz widoku base.html.twig pojawi się kod HTML formularza
zawierający znacznik cyfr oraz ewentualne informacje o błędach.

Listing 39.6. Osadzenie formularza do logowania w szablonie base.html.twig


<body>
{% if is_granted('ROLE_SUPER_ADMIN') %}
<p>
Jesteś zalogowany jako: {{ app.user.username }}
</p>
<ul>
<li><a href="{{ path('mountain') }}">edytuj</a></li>
<li><a href="{{ path('frontend') }}">przeglądaj</a></li>
<li><a href="{{ path('fos_user_security_logout') }}">wyloguj</a></li>
</ul>
{% else %}
<div>
{% render 'FOSUserBundle:Security:login' %}
</div>
{% endif %}
{% block body %}{% endblock %}
{% block javascripts %}{% endblock %}
</body>

Informację o tym, kto jest zalogowany, drukujemy instrukcją:


<p>
Jesteś zalogowany jako: {{ app.user.username }}
</p>
Jeśli przygotowujesz witrynę wielojęzyczną korzystającą z automatycznych transla-
cji, użyj kodu:
<p>
{{ 'layout.logged_in_as'|trans({'%username%': app.user.username},
´'FOSUserBundle') }}
</p>
W podobny sposób wydrukuj etykiety zawarte w odsyłaczach zaloguj i wyloguj.
Rozdział 39. ♦ Aplikacja dostępna publicznie w trybie do odczytu 437

Krok 3. Zmodyfikuj parametry formularza do logowania


W pliku app/config/security.yml wprowadź zmiany przedstawione na listingu 39.7.

Listing 39.7. Modyfikacje parametrów formularza do logowania


form_login:
...
login_path: /

always_use_default_target_path: true
default_target_path: /admin

failure_path: /
...

Formularz do logowania będzie dostępny na stronie głównej, dlatego parametry login_


´path oraz failure_path przyjmują wartość /. Dodatkowo parametry:
always_use_default_target_path: true
default_target_path: /admin

powodują, że zawsze po zalogowaniu użytkownik zostanie skierowany na stronę:


/admin

Krok 4. Zmodyfikuj reguły routingu


W pliku app/config/routing.yml wprowadź zmiany przedstawione na listingu 39.8.

Listing 39.8. Modyfikacje pliku routing.yml


MyFrontendBundle:
resource: "@MyFrontendBundle/Controller/"
type: annotation
prefix: /

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 }

Jedynymi dostępnymi adresami URL zawartymi w pakiecie FOSUserBundle są:


/login_check
/log out
438 Część VIII ♦ Panele CRUD i zabezpieczanie dostępu do aplikacji
Rozdział 40.
Rejestracja użytkowników
i odzyskiwanie hasła
W rozdziale tym zajmiemy się przygotowaniem aplikacji, która będzie zawierała frontend
oraz backend i pozwoli na rejestrację użytkowników.

Przykład 40.1. Kontynenty/państwa


— frontend i backend
Wykonaj aplikację prezentującą zestawienie kontynentów i państw, takie jak w przy-
kładach 36.3 oraz 34.1. W aplikacji utwórz dwa pakiety: frontend oraz backend. Pakiet
frontend ma umożliwiać przeglądanie w trybie do odczytu zawartości tabel kontynent
oraz panstwo. Pakiet backend ma umożliwiać edycję rekordów z tabel kontynent oraz
panstwo przy użyciu standardowego panelu CRUD. Dostęp do panelu backend powinni
mieć wyłącznie zalogowani użytkownicy posiadający uprawnienie ROLE_ADMIN.

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

Krok 2. Pakiet BackendBundle


W projekcie utwórz pakiet BackendBundle i skopiuj do niego klasy Kontynent i Panstwo
oraz skrypt z folderu DataFixtures/ z przykładu 34.1. Pamiętaj, by w skopiowanych
plikach wymienić wszystkie wystąpienia wyrazu Frontend na Backend.

Na zakończenie wygeneruj panele CRUD dla modeli:


MyBackendBundle:Kontynent
MyBackendBundle:Panstwo

Generując panel CRUD dla klasy Kontynent, ujrzysz monit:


Routes prefix [/kontynent]:

W odpowiedzi wprowadź adres:


/admin/kontynent

Analogicznie generując panel dla klasy Panstwo, w odpowiedzi na monit:


Routes prefix [/panstwo]:

podaj:
/admin/panstwo

W ten sposób wszystkie adresy URL oraz nazwy reguł routingu z paneli CRUD otrzymają
przedrostek admin.

Krok 3. Pakiet FrontendBundle


W projekcie utwórz pakiet FrontendBundle. Umieść w nim kontrolery oraz widoki z pro-
jektu 34.1. W skopiowanych kontrolerach wymień nazwy klas:
MyFrontendBundle:Kontynent
MyFrontendBundle:Panstwo

na:
MyBackendBundle:Kontynent
MyBackendBundle:Panstwo

Krok 4. Przygotuj formularz do logowania


W projekcie utwórz plik:
vendor\bundles\FOS\UserBundle\Resources\views\Security\login.html.twig

o zawartości identycznej jak na listingu 39.5.

Krok 5. Przygotuj szablon base.html.twig


Zmodyfikuj plik base.html.twig. Wprowadź w nim zawartość przedstawioną na li-
stingu 40.1.
Rozdział 40. ♦ Rejestracja użytkowników i odzyskiwanie hasła 441

Listing 40.1. Zmodyfikowany plik base.html.twig


<!DOCTYPE html>
<html>
<head>
<title>{% block title %}Kontynenty/Państwa{% endblock %}</title>
<meta charset="UTF-8" />
<link rel="stylesheet" href="{{ asset('css/style.css') }}" />
</head>
<body>
<div id="pojemnik">
{% if is_granted("IS_AUTHENTICATED_REMEMBERED") %}
<p>
Jesteś zalogowany jako: {{ app.user.username }}
</p>
<ul>
<li><a href="{{ path('admin_kontynent') }}">edytuj kontynenty</a></li>
<li><a href="{{ path('admin_panstwo') }}">edytuj państwa</a></li>
<li><a href="{{ path('fos_user_security_logout') }}">wyloguj</a></li>
</ul>
{% else %}
<div>
{% render 'FOSUserBundle:Security:login' %}
</div>
{% endif %}
<ul>
<li><a href="{{ path('homepage') }}">strona główna</a></li>
<li><a href="{{ path('kontynent_index') }}">kontynenty</a></li>
<li><a href="{{ path('panstwo_index') }}">państwa</a></li>
</ul>
{% block body %}{% endblock %}
{% block javascripts %}{% endblock %}
</div>
</body>
</html>

W szablonie base.html.twig umieszczamy dwa menu: jedno z nich pozwala na prze-


glądanie zawartości witryny w trybie do odczytu, drugie umożliwia edycję rekordów.
Hiperłącza edycyjne udostępniamy wyłącznie zalogowanym użytkownikom, co usta-
lamy instrukcją:
{% if is_granted("IS_AUTHENTICATED_REMEMBERED") %}

We wszystkich widokach pakietów FrontendBundle oraz BackendBundle dodaj przedsta-


wione na listingu 40.2 instrukcje włączające dekorację szablonem base.html.twig.

Listing 40.2. Dekoracja widoków szablonem base.html.twig


{% extends "::base.html.twig" %}
{% block body %}
...
{% endblock body %}

Tworzenie szablonu zakończ, przygotowując style CSS, które elementowi div#pojemnik


nadadzą zielone tło oraz obramowanie.
442 Część VIII ♦ Panele CRUD i zabezpieczanie dostępu do aplikacji

Krok 6. Przygotuj bazę danych


Wydaj polecenia:
php app/console doctrine:schema:update --force
php app/console doctrine:fixtures:load
php app/console fos:user:create admin admin@example.com sEcrETpAssWord
php app/console fos:user:promote admin --super

Krok 7. Zmodyfikuj parametry przekierowań


W pliku security.yml wprowadź modyfikacje przedstawione na listingu 40.3.

Listing 40.3. Modyfikacje przekierowań


form_login:
login_path: /
default_target_path: /admin
use_referer: true
failure_path: /

Krok 8. Sprawdź działanie aplikacji


Odwiedź w przeglądarce adres:
.../web/

Ujrzysz witrynę przedstawioną na rysunku 40.1. Najpierw odwiedź wszystkie strony


dostępne bez zalogowania, a następnie zaloguj się na konto admin i odwiedź wszystkie
strony administracyjne.

Przykład 40.2. Kontynenty/państwa


— rejestracja użytkowników
Zmodyfikuj aplikację z przykładu 40.1 w taki sposób, by umożliwiała rejestrację użyt-
kowników.

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

Rysunek 40.1. Aplikacja kontynenty/państwa zawierająca frontend oraz backend

Listing 40.4. Element h3 zawierający hiperłącze do formularza rejestracyjnego


<div>
<h3>
<a href="{{ path('fos_user_registration_register') }}">
Zarejestruj się
</a>
lub zaloguj
</h3>
{% render 'FOSUserBundle:Security:login' %}
</div>

Krok 2. Nadpisz widoki dotyczące rejestracji


Widoki:
vendor\bundles\FOS\UserBundle\Resources\views\Registration\register.html.twig
vendor\bundles\FOS\UserBundle\Resources\views\Registration\confirmed.html.twig

skopiuj do folderu:
app\Resources\FOSUserBundle\views\Registration

Następnie dodaj w nich przedstawione na listingu 40.2 znaczniki włączające dekorację


szablonem base.html.twig.
444 Część VIII ♦ Panele CRUD i zabezpieczanie dostępu do aplikacji

Krok 3. Nadpisz widok strony błędu 403


Utwórz plik:
app\Resources\TwigBundle\views\Exception\error403.html.twig

o zawartości przedstawionej na listingu 40.5.

Listing 40.5. Widok error403.html.twig


{% extends "::base.html.twig" %}
{% block body %}
<p>
Brak uprawnień!
</p>
{% endblock %}

Krok 4. Sprawdź działanie aplikacji


Wyczyść pamięć podręczną projektu, a następnie odwiedź stronę główną. Zarejestruj się,
po czym spróbuj odwiedzić wszystkie strony aplikacji.

Przy próbie odwiedzenia paneli CRUD ujrzysz komunikat z listingu 40.5.

Przykład 40.3. Kontynenty/państwa


— odzyskiwanie hasła
Zmodyfikuj aplikację z przykładu 40.2 w taki sposób, by umożliwiała odzyskiwanie ha-
sła dostępu do witryny.

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.

Listing 40.6. Konfiguracja konta pocztowego Gmail


# Swiftmailer Configuration
swiftmailer:
transport: gmail
username: twoje.konto.pocztowe@gmail.com
password: "hasloTwojegoKontaPocztowegoGmail"
Rozdział 40. ♦ Rejestracja użytkowników i odzyskiwanie hasła 445

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

Listing 40.7. Konfiguracja konta pocztowego z firmy Netart


swiftmailer:
transport: smtp
host: twojserwer.nazwa.pl
username: kontopocztowe@twojserwer.pl
password: "tajneHaslo"
encryption: ssl
port: 465
auth_mode: login

fos_user:
db_driver: orm
firewall_name: main
user_class: My\UserBundle\Entity\User
from_email:
address: kontopocztowe@twojserwer.pl

Krok 2. Dostosuj widoki


Nadpisz następujące widoki:
vendor\bundles\FOS\UserBundle\Resources\views\Resetting\checkEmail.html.twig
vendor\bundles\FOS\UserBundle\Resources\views\Resetting\request.html.twig
vendor\bundles\FOS\UserBundle\Resources\views\Resetting\reset.html.twig

oraz
vendor\bundles\FOS\UserBundle\Resources\views\Profile\show.html.twig

Dodaj w nich przedstawione na listingu 40.2 instrukcje włączające dekorację szablonem


base.html.twig.

Pamiętaj, że nadpisywanie widoków rozpoczynamy od utworzenia widoków w folderze


app/Resources. Na przykład w celu nadpisania widoku:
vendor\bundles\FOS\UserBundle\Resources\views\Resetting\checkEmail.html.twig
tworzymy plik:
app\Resources\FOSUserBundle\views\Resetting\checkEmail.html.twig
W utworzonym pliku wprowadzamy opisane modyfikacje.
446 Część VIII ♦ Panele CRUD i zabezpieczanie dostępu do aplikacji

Krok 3. Dodaj odsyłacz do strony rejestracji


W widoku base.html.twig dodaj przedstawiony na listingu 40.8 element h4 zawierający
odsyłacz do strony rejestracji.

Listing 40.8. Odsyłacz do strony rejestracji


<div>
...
{% render 'FOSUserBundle:Security:login' %}
<h4>
<a href="{{ path('fos_user_resetting_request') }}">
Zapomniałeś hasło?
</a>
</h4>
</div>

Krok 4. Sprawdź działanie aplikacji


Wyczyść pamięć podręczną projektu, a następnie odwiedź stronę główną. Zarejestruj się,
podając jeden ze swoich prawdziwych adresów e-mail. Wyloguj się, po czym korzystając
z hiperłącza Zapomniałeś hasło?, spróbuj odzyskać hasło do konta.

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

Następnie wyloguj się i ponownie zaloguj na konto janek. Po ponownym zalogowaniu


konto janek będzie miało pełne uprawnienia administracyjne. W podobny sposób mo-
żesz modyfikować inne uprawnienia:
php app/console fos:user:promote janek ROLE_ABC_DEF
Rozdział 41.
Podsumowanie części VIII
W części tej zapoznałeś się z panelami administracyjnymi CRUD oraz dowiedziałeś się,
w jaki sposób wykorzystać je do edycji zależności relacyjnych. Ponieważ panele admini-
stracyjne pozwalają na edycję zawartości serwisu, dostęp do nich należy zabezpieczyć.
Zadanie to ułatwia pakiet FOSUserBundle, którego instalację oraz podstawową konfi-
gurację omówiliśmy w rozdziale 37. Wykorzystując pakiet FOSUserBundle, omówili-
śmy trzy przykłady reguł dostępu do witryny WWW. W rozdziale 38. wykonaliśmy
aplikację dostępną wyłącznie dla zdefiniowanych użytkowników. Rozdział 39. wyjaśnił,
w jaki sposób wykonać aplikację, która będzie dostępna publicznie w trybie do odczytu.
W ostatnim rozdziale omówiliśmy natomiast zagadnienia dotyczące rejestracji użytkow-
ników w serwisie oraz odzyskiwania haseł.
448 Część VIII ♦ Panele CRUD i zabezpieczanie dostępu do aplikacji
Część IX
Panele administracyjne
Sonata
450 Część IX ♦ Panele administracyjne Sonata
Rozdział 42. ♦ Instalacja pakietów Sonata 451

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.

Szczegółowa dokumentacja pakietów wchodzących w skład projektu Sonata jest


dostępna na stronie:
http://sonata-project.org/about

Przykład 42.1. Przygotowanie


dystrybucji symfony2-customized-v5
zawierającej pakiet
SonataAdminBundle
Przygotuj dystrybucję symfony2-customized-v5.zip, która będzie zawierała panel admini-
stracyjny SonataAdminBundle.

ROZWIĄZANIE

Krok 1. Wypakuj dystrybucję


Symfony 2.0.X without vendors
Odwiedź stronę główną Symfony 2 i pobierz dystrybucję Symfony 2.0.X without vendors.
Pobrany plik wypakuj do folderu symfony2-customized-v5/.
452 Część IX ♦ Panele administracyjne Sonata

Z wypakowanej dystrybucji usuń pakiet src/Acme/DemoBundle.

Krok 2. Zmodyfikuj plik deps


Na końcu pliku [projekt]/deps dodaj pakiety wymienione na listingu 42.1.

Listing 42.1. Zawartość, którą należy dodać na końcu pliku deps


[FOSUserBundle]
git=http://github.com/FriendsOfSymfony/FOSUserBundle.git
target=bundles/FOS/UserBundle

[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

Krok 3. Pobierz pakiety


Wydaj komendy:
php bin/vendors install
php bin/vendors lock

Krok 4. Usuń foldery .git


Uruchom konsolę bash, przejdź do folderu symfony2-customized-v5/ i wydaj w nim komendę:
find vendor -name .git -type d -exec rm -fr {} \;

Krok 5. Zarejestruj przestrzenie nazw


W pliku autoload.php zarejestruj przestrzenie przedstawione na listingu 42.2.

Listing 42.2. Rejestracja przestrzeni nazw FOS w pliku autoload.php


...
$loader->registerNamespaces(array(
...
'FOS' => __DIR__.'/../vendor/bundles',
'Sonata' => __DIR__.'/../vendor/bundles',
'Exporter' => __DIR__.'/../vendor/exporter/lib',
'Knp\Bundle' => __DIR__.'/../vendor/bundles',
'Knp\Menu' => __DIR__.'/../vendor/knp/menu/src',
));
...

Krok 6. Zarejestruj pakiety


W pliku AppKernel.php zarejestruj pakiety widoczne na listingu 42.3.

Listing 42.3. Rejestracja pakietów w pliku AppKernel.php


...
$bundles = array(
...
new FOS\UserBundle\FOSUserBundle(),
new Sonata\BlockBundle\SonataBlockBundle(),
new Sonata\AdminBundle\SonataAdminBundle(),
new Sonata\CacheBundle\SonataCacheBundle(),
new Sonata\jQueryBundle\SonatajQueryBundle(),
new Knp\Bundle\MenuBundle\KnpMenuBundle(),
new Sonata\DoctrineORMAdminBundle\SonataDoctrineORMAdminBundle(),
new Sonata\UserBundle\SonataUserBundle('FOSUserBundle'),
new Sonata\EasyExtendsBundle\SonataEasyExtendsBundle(),
);
...
454 Część IX ♦ Panele administracyjne Sonata

Krok 7. Zmodyfikuj konfigurację projektu


W pliku app/config/config.yml wprowadź modyfikacje przedstawione na listingu 42.4.

Listing 42.4. Modyfikacje konfiguracji projektu app/config/config.yml


#wpisy, które należy zmodyfikować
framework:
translator: ~
session:
default_locale: pl

#wpisy, które należy dodać na końcu pliku


sonata_block:
default_contexts: [cms]
blocks:
sonata.admin.block.admin_list:
contexts: [admin]

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

Krok 8. Zmodyfikuj zabezpieczenia projektu


W pliku app/config/security.yml wprowadź zawartość przedstawioną na listingu 42.5.

Listing 42.5. Zmodyfikowany plik konfiguracyjny app/config/security.yml


security:
providers:
fos_userbundle:
id: fos_user.user_manager

encoders:
"FOS\UserBundle\Model\UserInterface": sha512

firewalls:

# -> custom firewall for the admin area of the URL


admin:
switch_user: true
context: user
pattern: /admin(.*)
form_login:
provider: fos_userbundle
login_path: /admin/login
use_forward: false
check_path: /admin/login_check
failure_path: null
use_referer: true
logout:
path: /admin/logout
target: /admin/login

anonymous: true
# -> end custom configuration

# default login area for standard users


main:
switch_user: true
context: user
pattern: .*
logout: true
anonymous: true
456 Część IX ♦ Panele administracyjne Sonata

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

- { path: ^/register, role: IS_AUTHENTICATED_ANONYMOUSLY }


- { path: ^/resetting, role: IS_AUTHENTICATED_ANONYMOUSLY }

# Secured part of the site


# This config requires being logged for the whole site and having the admin role for
# the admin part.
# Change these rules to adapt them to your needs
- { path: ^/admin, role: [ROLE_ADMIN, ROLE_SONATA_ADMIN] }
- { path: ^/.*, role: IS_AUTHENTICATED_ANONYMOUSLY }

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

Krok 9. Utwórz pakiet Application/Sonata/UserBundle


W wierszu poleceń wydaj komendę:
php app/console sonata:easy-extends:generate SonataUserBundle --dest=src

Spowoduje ona utworzenie pakietu Application/Sonata/UserBundle, który zawiera klasy


User oraz Group łączące pakiet FOSUserBundle z pakietami projektu Sonata. Utworzony
pakiet dołącz do listy pakietów aplikacji. W pliku app/AppKernel.php dodaj instrukcję:
$bundles = array(
...
new Application\Sonata\UserBundle\ApplicationSonataUserBundle(),
);

Krok 10. Zmodyfikuj reguły routingu


W pliku app/config/routing.yml wprowadź reguły routingu widoczne na listingu 42.6.

Listing 42.6. Reguły routingu, które należy dodać w pliku app/config/routing.yml


sonata_user_impersonating:
pattern: /
defaults: { _controller: SonataAdminBundle:Core:dashboard }
admin:
resource: '@SonataAdminBundle/Resources/config/routing/sonata_admin.xml'
prefix: /admin
_sonata_admin:
resource: .
type: sonata_admin
prefix: /admin
sonata_user:
resource: '@SonataUserBundle/Resources/config/routing/admin_security.xml'
prefix: /admin
fos_user_security:
resource: "@FOSUserBundle/Resources/config/routing/security.xml"
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
458 Część IX ♦ Panele administracyjne Sonata

Krok 11. Zainstaluj style CSS oraz ikony


W wierszu poleceń wydaj komendę:
php app/console assets:install web

Krok 12. Skompresuj otrzymaną dystrybucję


Skompresuj folder symfony2-customized-v5/.

Przykład 42.2. Sprawdź działanie


dystrybucji symfony2-customized-v5
Sprawdź wygląd paneli administracyjnych, które są zawarte w dystrybucji symfony2-
-customized-v5.zip.

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.

Krok 2. Utwórz tabele w bazie danych


Wydaj polecenie:
php app/console doctrine:schema:update --force

W bazie danych zostaną utworzone tabele przedstawione na rysunku 42.1:


fos_user_group
fos_user_user
fos_user_user_group

Krok 3. Utwórz konto administratora


W wierszu poleceń wydaj komendy:
php app/console fos:user:create admin admin@example.com password --super-admin
php app/console fos:user:promote admin --super
Rozdział 42. ♦ Instalacja pakietów Sonata 459

Rysunek 42.1. Tabele przeznaczone na informacje o kontach użytkowników i uprawnieniach

Krok 4. Sprawdź wygląd panelu administracyjnego


Uruchom przeglądarkę i odwiedź adres:
.../web/admin/dashboard

Po zalogowaniu na konto:
Użytkownik: admin
Hasło: password

ujrzysz panel administracyjny przedstawiony na rysunku 42.2.

Rysunek 42.2. Panel administracyjny do zarządzania kontami


460 Część IX ♦ Panele administracyjne Sonata
Rozdział 43.
Użycie paneli
administracyjnych Sonata
do własnych tabel
Panel administracyjny przedstawiony na rysunku 42.2 umożliwiał zarządzanie rekordami
tabel:
fos_user_group
fos_user_user
fos_user_user_group

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.

Przykład 43.1. Miasta


Wykonaj witrynę, która będzie prezentowała dane dotyczące miast:
 nazwę miasta,
 populację.

Witrynę wykonaj w taki sposób, by po odwiedzeniu adresu:


.../web/

wyświetlane było zestawienie informacji o wszystkich miastach. Dane na stronie .../web/


mają być dostępne w trybie do odczytu dla wszystkich odwiedzających ten adres.

Ponadto pod adresem:


.../web/admin/dashboard
462 Część IX ♦ Panele administracyjne Sonata

wykonaj panel administracyjny pozwalający na edycję informacji o miastach. Panel ten


ma być dostępny wyłącznie po zalogowaniu na konto:
Użytkownik: admin
Hasło: password

Wykorzystaj wykonaną w poprzednim rozdziale dystrybucję symfony2-customized-v5.zip.

ROZWIĄZANIE

Krok 1. Wypakuj dystrybucję i skonfiguruj bazę danych


Utwórz bazę danych o nazwie cities 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 cities.

Krok 2. Utwórz pakiet My/Frontend


Analogicznie jak w przykładzie z rozdziału 18. wykonaj pakiet My/Frontend. W pakiecie
tym ma się znajdować jedna klasa Entity o nazwie City. W klasie City utwórz dwie
właściwości:
 name — typu string o długości 255 znaków;
 population — typu integer.

Wykonaj kontroler oraz skonfiguruj routing w taki sposób, by po odwiedzeniu strony:


.../web/

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.

Krok 3. Utwórz klasę CityAdmin


W folderze My/FrontendBundle/Admin utwórz plik CityAdmin.php zawierający klasę
przedstawioną na listingu 43.1. Klasa ta konfiguruje panel administracyjny do zarzą-
dzania rekordami z tabeli city.

Listing 43.1. Klasa My/FrontendBundle/Admin/CityAdmin.php


<?php

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;

class CityAdmin extends Admin


{
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->add('name')
->add('population', 'number')
;
}

protected function configureDatagridFilters(DatagridMapper $datagridMapper)


{
$datagridMapper
->add('name')
;
}

protected function configureListFields(ListMapper $listMapper)


{
$listMapper
->addIdentifier('name')
->add('population', 'number')
;
}

Krok 4. Włącz panel administracyjny


do zarządzania rekordami City
Utwórz plik My/FrontendBundle/Resources/config/services.xml, o zawartości takiej jak
na listingu 43.2.

Listing 43.2. Plik My/FrontendBundle/Resources/config/services.xml


<?xml version="1.0" ?>

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

Krok 5. Przygotuj plik zawierający tłumaczenia


Utwórz plik My/FrontendBundle/Resources/translations/MyFrontendBundle.pl.xliff,
o zawartości takiej jak na listingu 43.3.

Listing 43.3. Plik My/FrontendBundle/Resources/translations/MyFrontendBundle.pl.xliff


<?xml version="1.0"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" datatype="plaintext" original="" >
<body>
<trans-unit id="1">
<source>Dashboard</source>
<target>Pulpit</target>
</trans-unit>
<trans-unit id="2">
<source>Name</source>
<target>Nazwa</target>
</trans-unit>
<trans-unit id="3">
<source>Population</source>
<target>Populacja</target>
</trans-unit>
<trans-unit id="4">
<source>City List</source>
<target>Lista miast</target>
</trans-unit>
<trans-unit id="5">
<source>City Edit</source>
<target>Edytuj miasto</target>
</trans-unit>
<trans-unit id="6">
<source>City Delete</source>
<target>Usuń miasto</target>
</trans-unit>
</body>
</file>
</xliff>

Krok 6. Sprawdź wygląd panelu administracyjnego


do edycji miast
Po odwiedzeniu adresu:
.../web/admin/washboard

i zalogowaniu na konto administratora ujrzysz panel przedstawiony na rysunku 43.1.


Rozdział 43. ♦ Użycie paneli administracyjnych Sonata do własnych tabel 465

Rysunek 43.1. Panel administracyjny do zarządzania rekordami z tabeli city

Menu wskazane strzałką jest zdefiniowane wpisami:


<tag name="sonata.admin" manager_type="orm" group="Dane" label="Miasta"/>

z listingu 43.2. Tłumaczenia, m.in.:


Lista miast
Populacja

powstają natomiast na podstawie wpisów z listingu 43.3:


<trans-unit id="3">
<source>Population</source>
<target>Populacja</target>
</trans-unit>
<trans-unit id="4">
<source>City List</source>
<target>Lista miast</target>
</trans-unit>
466 Część IX ♦ Panele administracyjne Sonata
Rozdział 44.
Podsumowanie części IX
Rozdziały 42. oraz 43. opisują konfigurację oraz użycie pakietów z projektu Sonata.
Dzięki nim możemy w prosty sposób wzbogacić aplikację o panele administracyjne.
W ten sposób zakończyliśmy omawianie podstawowych możliwości oprogramowania
Symfony 2.

Na zakończenie warto przygotować ostatnią dystrybucję, symfony2-customized-v6.zip,


która będzie zawierała wszystkie omówione w książce pakiety, czyli:
 fikstury z rozdziału 15.,
 zachowania Doctrine z części V,
 pakiet FOSUserBundle z rozdziału 37.
 oraz omówione w rozdziale 42. pakiety projektu Sonata.

Dzięki temu będziemy mogli w prosty sposób rozpocząć pracę nad kolejnym projektem.

Przykład 44.1. Przygotowanie


dystrybucji symfony2-customized-v6
zawierającej omówione pakiety
Przygotuj dystrybucję symfony2-customized-v6.zip, która będzie zawierała wszystkie pa-
kiety omówione w książce.
468 Część IX ♦ Panele administracyjne Sonata

Przykład 44.2. Rzeki:


aplikacja z panelem Sonata
Wykonaj aplikację omówioną w rozdziale 18., wzbogacając ją o panel administracyjny
Sonata. Do wykonania zadania wykorzystaj dystrybucję symfony2-customized-v6.zip.

ROZWIĄZANIE

Krok 1. Połącz przykład 18.


z dystrybucją symfony2-customized-v6.zip
W przykładzie 18.1 wykonaliśmy pakiet My/FrontendBundle. W celu użycia tego pakietu
w nowym projekcie należy:
 skopiować folder My/ zawarty w przykładzie 18.1 do folderu src/
wypakowanej dystrybucji symfony2-customized-v6.zip;
 w pliku AppKernel.php dodać instrukcję włączającą pakiet FrontendBundle:
new My\FrontendBundle\MyFrontendBundle(),

 skopiować szablony zawarte w folderze app/Resources/views/ przykładu 18.1


do wypakowanej dystrybucji;
 skopiować style CSS oraz pliki graficzne z folderu web/ przykładu 18.1
do wypakowanej dystrybucji;
 w pliku app/config/routing.yml dodać na samej górze instrukcje włączające
routing z pakietu FrontendBundle:
MyFrontendBundle:
resource: "@MyFrontendBundle/Controller/"
type: annotation
prefix: /

 w pliku app/config/parameters.ini skonfigurować dostęp do bazy danych;


 na zakończenie skopiować folder z danymi data/.

Po tych operacjach wydajemy polecenia tworzące bazę danych:


php app/console doctrine:schema:update --force
php app/console doctrine:fixtures:load

i odwiedzamy adres:
.../web/

Powinniśmy ujrzeć stronę z rysunku 18.1.


Rozdział 44. ♦ Podsumowanie części IX 469

Krok 2. Wykonaj panel Sonata


Najpierw przygotowujemy klasę My/FrontendBundle/Admin/RiverAdmin.php o treści ana-
logicznej do listingu 43.1. Następnie tworzymy plik My/FrontendBundle/Resources/
config/services.xml o zawartości analogicznej do listingu 43.2. Jako trzeci wykonujemy
plik tłumaczeń My/FrontendBundle/Resources/translations/MyFrontendBundle.pl.xliff.
Jego zawartość będzie analogiczna do listingu 43.3.

W dalszej kolejności tworzymy konto administratora:


php app/console fos:user:create admin admin@example.com password --super-admin
php app/console fos:user:promote admin --super

i w pliku layout.html.twig dodajemy odsyłacz do panelu administracyjnego:


<a href="{{ path('sonata_admin_dashboard') }}">Panel administracyjny</a>

Na zakończenie w klasie My/FrontendBundle/Entity/River.php dodajemy metodę


__toString(), która zwraca wartość właściwości name.

Przykład 44.3. Kontynenty:


aplikacja z panelem Sonata
Wykonaj aplikację omówioną w przykładzie 34.1, wzbogacając ją o panel administra-
cyjny Sonata. Do identyfikacji rekordów wyświetlanych na stronach akcji show użyj
ciągów slug generowanych przez zachowania Doctrine. Do wykonania zadania wyko-
rzystaj dystrybucję symfony2-customized-v6.zip.

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')
;
}

Pole kontynent umożliwi edycję zależności relacyjnej.

W pliku services.xml pojawią się dwa wpisy:


<services>

<service id="sonata.admin.kontynent" class="My\FrontendBundle\Admin\KontynentAdmin">


470 Część IX ♦ Panele administracyjne Sonata

<tag name="sonata.admin" manager_type="orm" group="Dane" label="Kontynenty"/>


<argument />
<argument>My\FrontendBundle\Entity\Kontynent</argument>
<argument>SonataAdminBundle:CRUD</argument>
<call method="setTranslationDomain">
<argument>MyFrontendBundle</argument>
</call>
</service>

<service id="sonata.admin.panstwo" class="My\FrontendBundle\Admin\PanstwoAdmin">


<tag name="sonata.admin" manager_type="orm" group="Dane" label="Państwa"/>
<argument />
<argument>My\FrontendBundle\Entity\Panstwo</argument>
<argument>SonataAdminBundle:CRUD</argument>
<call method="setTranslationDomain">
<argument>MyFrontendBundle</argument>
</call>
</service>
</services>

Adresy slug włączamy tak, jak to zostało opisane w rozdziale 27.

Adresy slug są automatycznie generowane na podstawie właściwości name, zatem


nie występują w panelu administracyjnym.

Przykład 44.4. Filmy:


aplikacja z panelem Sonata
Wykonaj aplikację omówioną w przykładzie 34.2, wzbogacając ją o panel administra-
cyjny Sonata. Do identyfikacji rekordów wyświetlanych na stronach akcji show użyj
ciągów slug generowanych przez zachowania Doctrine. Do wykonania zadania wyko-
rzystaj dystrybucję symfony2-customized-v6.zip.

Przykład 44.5. Powieści Agaty


Christie: aplikacja z panelem Sonata
Wykonaj aplikację omówioną w przykładzie 34.3, wzbogacając ją o panel administra-
cyjny Sonata. Do identyfikacji rekordów wyświetlanych na stronach akcji show użyj
ciągów slug generowanych przez zachowania Doctrine. Do wykonania zadania wyko-
rzystaj dystrybucję symfony2-customized-v6.zip.
I
Dodatki
472 Część I ♦ Tworzenie prostych stron WWW
Dodatek A
Instalacja
oprogramowania

1. XAMPP
Do zainstalowania oprogramowania:
 Apache,
 PHP
 i MySQL

wykorzystamy pakiet XAMPP, który jest dostępny na stronie http://www.apachefriends.org.


Pobierz, a następnie uruchom program instalacyjny xampp-win32-1.7.7-VC9-installer.exe.
Wszystkie opcje pozostaw domyślne. W szczególności pamiętaj, by nie zmieniać folderu
przeznaczonego na oprogramowanie XAMPP, czyli c:\xampp.

Po zakończeniu instalacji uruchom panel administracyjny XAMPP. Przyciski wskazane na


rysunku A.1 służą do uruchamiania i zatrzymywania usług Apache oraz MySQL.

Uruchom usługi Apache i MySQL, po czym odwiedź w przeglądarce adres:


http://localhost

Ujrzysz wówczas okno widoczne na rysunku A.2.

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

Jeśli zechcesz zainstalować oprogramowanie XAMPP w innym folderze, np. wewnątrz


C:\Pliki programów (x86), pamiętaj o wyłączeniu ograniczeń konta użytkownika.
Dodatek A ♦ Instalacja oprogramowania 475

Rysunek A.3.
Ostrzeżenie
informujące o tym,
że usługa Apache
zezwala na
nawiązywanie
połączeń sieciowych

2. Modyfikacja konfiguracji PHP


Konfiguracja PHP jest zapisana w pliku C:\xampp\php\php.ini. W pliku tym najpierw
włącz rozszerzenie XDebug. W tym celu usuń średnik występujący na początku wiersza:
[XDebug]
zend_extension = "C:\xampp\php\ext\php_xdebug.dll"

Następnie włącz rozszerzenie XSL. W tym celu na końcu sekcji1 oznaczonej komen-
tarzem:
;;;;;;;;;;;;;;;;;;;;;;
; Dynamic Extensions ;
;;;;;;;;;;;;;;;;;;;;;;

dodaj wpis:
extension=php_xsl.dll

Domyślna konfiguracja XAMPP-a zawiera wpis włączający krótkie znaczniki PHP:


short_open_tag = On

Pamiętaj, że jeśli zgodnie z zaleceniami Symfony 2 wyłączysz krótkie znaczniki PHP,


czyli zmienisz powyższą opcję na:
short_open_tag = Off

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

3. Modyfikacja pakietu PEAR


Niemal wszystkie wersje XAMPP-a zawierają błędnie skonfigurowane archiwum PEAR.
Dlatego większość poleceń:
pear install ...

zakończy się błędem.

W celu naprawienia konfiguracji archiwum PEAR uruchom wiersz poleceń, po czym


komendami:
C:
cd \xampp\php

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

Następnie wydaj polecenia widoczne na listingu A.1.

Listing A.1. Polecenia modyfikujące plik C:\xampp\php\pear.ini


pear -c c:\xampp\php\pear.ini config-set doc_dir c:\xampp\php\pear\docs
pear -c c:\xampp\php\pear.ini config-set bin_dir c:\xampp\php
pear -c c:\xampp\php\pear.ini config-set ext_dir c:\xampp\php\ext
pear -c c:\xampp\php\pear.ini config-set php_dir c:\xampp\php\pear
pear -c c:\xampp\php\pear.ini config-set cache_dir c:\xampp\php\cache
pear -c c:\xampp\php\pear.ini config-set cfg_dir c:\xampp\php\cfg
pear -c c:\xampp\php\pear.ini config-set data_dir c:\xampp\php\data
pear -c c:\xampp\php\pear.ini config-set download_dir c:\xampp\php\download
pear -c c:\xampp\php\pear.ini config-set php_bin c:\xampp\php\php.exe
pear -c c:\xampp\php\pear.ini config-set temp_dir c:\xampp\php\tmp
pear -c c:\xampp\php\pear.ini config-set test_dir c:\xampp\php\pear\tests
pear -c c:\xampp\php\pear.ini config-set www_dir c:\xampp\php\pear\www

W ten sposób w pliku C:\xampp\php\pear.ini zostaną zapisane poprawne ścieżki. Przeko-


nasz się o tym, wydając polecenie:
pear -c c:\xampp\php\pear.ini config-show

4. Uaktualnienie biblioteki PEAR


Uruchom wiersz poleceń. Przejdź do folderu C:\xampp\php:
C:
cd \xampp\php

a następnie wydaj komendę:


pear -c c:\xampp\php\pear.ini upgrade-all

W ten sposób uaktualnisz wszystkie pakiety PEAR.


Dodatek A ♦ Instalacja oprogramowania 477

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

Następnie odwiedź adres:


https://github.com/opensky/Symfony2-coding-standard

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

Po wykonaniu powyższej operacji w systemie powinny pojawić się m.in. pliki:


C:\xampp\php\PEAR\PHP\CodeSniffer\Standards\Symfony2\ruleset.xml
C:\xampp\php\PEAR\PHP\CodeSniffer\Standards\Symfony2\Sniffs\WhiteSpace\
´DiscourageFitzinatorSniff.php

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

Następnie zainstaluj pakiet phpDocumentor w wersji 2:


pear -c c:\xampp\php\pear.ini channel-discover pear.phpdoc.org
pear -c c:\xampp\php\pear.ini install --alldeps phpdoc/phpDocumentor-alpha

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

Uruchom pobrany program instalacyjny.

W oknie dialogowym Choose installation type/Choose a download source wybierz opcję:


Install from internet

W oknie dialogowym do wyboru pakietów wyszukaj pakiet rsync. Wyszukane oprogra-


mowanie dołącz do listy instalowanych pakietów. W tym celu kliknij w ikonę widoczną
obok numeru wersji 3.0.9-1. Proces wyszukiwania i włączania instalacji pakietu rsync jest
przedstawiony na rysunku A.4.

Rysunek A.4. Instalacja programu rsync

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

Rysunek A.5. Instalacja oprogramowania openssh

Rysunek A.6. Instalacja programu curl

Rysunek A.7. Instalacja oprogramowania git


480 Symfony 2 od podstaw

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.

Rysunek A.8. Odsyłacz do okna z zaawansowanymi ustawieniami 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;

Zmodyfikowana zawartość okna edycyjnego jest przedstawiona na rysunku A.11.


Dodatek A ♦ Instalacja oprogramowania 481

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 przetestowania poprawności modyfikacji ścieżek dostępu uruchom wiersz poleceń,


i będąc w dowolnym folderze, wydaj komendę php. Następnie wprowadź skrypt:
<?php
echo 5 *4;
482 Symfony 2 od podstaw

po czym naciśnij przycisk F6 oraz Enter. Po wykonaniu skryptu wydrukowana zosta-


nie wartość 20. Świadczy to o tym, że interpretator php.exe jest dostępny w ścieżkach
poszukiwań.

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

i pobierz program graphviz-2.28.0.msi. Program ten zainstaluj, pozostawiając wszystkie


opcje domyślne.

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

dystrybucja menuAction(), 201 File.php, 325


with vendors, 17, 23, 101 parent(), 177 Film, 366
without vendors, 17, 23, 44 path(), 68, 191 formularza, 397
dziedziczenie, 175 showAction(), 188 Lorem.php, 237
shuffle(), 313 LoremRepository, 252
E str_replace(), 314 Method, 379
funkcje, functions, 169 MountainRepository, 259
etykiety formularza, 395 konwertujące dane, 319 Name, 392
Twig, 184 Name.php, 226
NameController, 391
F Novel, 378, 379
G odwrotna relacji, inverse side,
fikstur, fixtures, 212
filtr generowanie 342, 361, 385
escape, 133 błędów, 92 Profil, 341
keys, 149 identyfikatorów slug, 276, 277 Repository, 379
nl2br, 212 klasy dostępu, 245, 258 ResponseHeaderBag, 125
truncate, 212 menu, 311 River.php, 245
wordwrap, 212 paneli CRUD, 394 RiverAdmin.php, 469
filtry, filters, 169 panelu administracyjnego, 390 Sit, 26
Twig, 181 Song, 301
folder SongRepository, 301
H TrenRepository, 312
app, 18
bin, 18 hasło dostępu do serwera, 100 User, 341
bundles, 48 hierarchia ról, 427 UserType, 398
cache, 20 Word, 278
css, 48 klasy
doc, 32
I dostępu do bazy danych, 234, 236
Entity, 234, 252 identyfikator slug, 275, 308, 379 Entity, 251
js, 48 instalacja Repository, 251
mylake, 56 biblioteki, 270 klucz
public, 32 oprogramowania, 473 główny, 285
public_html, 98 pakietów Sonata, 452 obcy, foreign key, 331, 345
src, 18 pakietu FOSUserBundle, 403 obcy o wartości NULL, 332, 346
Symfony, 21 programu kod
tatras, 53 curl, 479 akcji index, 230, 248, 261, 290,
translations, 32 git, 479 303, 337
vendor, 17, 18, 210 openssh, 479 akcji show, 304, 310
web, 19 rsync, 478 menu, 310
views, 60 instrukcja, 137 SQL, 223
folder zawierający projekt, 211 { % raw %}, 120 kodowanie, 477
foldery .git, 214 {% block %}, 120 kodowanie znaków utf8, 223
format {% extends %}, 120 kolory RGB, 164
konfiguracji pakietu, 44 {% set %}, 144 komenda find, 211
PHP, 46 {{ }}, 121, 132, 137 komenda rm, 211
XML, 45 for, 147 kompresowanie folderu, 216
YAML, 45 if, 148, 150 komunikat Zaloguj, 424
formularz, 395 namespace, 43 konfiguracja
logowania, 424, 433, 437, 440 use, 43, 243 identyfikatorów slug, 280
rejestracyjny, 442 konta pocztowego, 444
formularze edycyjne, 397 routingu, 45
frontend, 439
K zabezpieczeń, 406
funkcja kaskadowość operacji zapisu, 398 zachowań, 270
array_shift(), 314 klasa konfigurowanie
asset(), 47, 63 Aktor, 366 pakietu, 217
base64_decode(), 320 AuthenticatedVoter, 417 PHP, 475
base64_encode(), 320 CityAdmin, 462 konsola bash, 271
basename(), 314, 321 Color, 284, 288 konto administratora, 411, 458
extends, 60 Detective, 378 konto dostępu
file_get_contents(), 321 Dolor, 234 do aplikacji, 422
finfo_file(), 321 Entity, 240, 299 do bazy, 462
htmlspecialchars(), 132 EntityManager., 240 pocztowe, 444
Skorowidz 485

kontroler, controller, 25, 27 fromArray (), 256 nagłówek Content-Type, 125


Aktor, 369 get(), 227, 237 narzędzie rsync, 97
app.php, 40 getAktorzy(), 363 nawiasy okrągłe, 143
app_dev.php, 40 getAll(), 243 nazwa logiczna widoku, 111, 114
BramaController, 74 getContents(), 325 nazwy skrócone, 44
DefaultController, 35 getFilmy(), 363 nazwy widoków akcji, 114
Film, 368 getId(), 237 NEW, 241
Kontynent, 355 getIpsum(), 237 numer portu 40022, 99
LoremController, 67 getKontynent(), 351
MountainController, 421 getLength(), 246 O
Novel, 381 getPanstwa(), 350
Panstwo, 356 getProfil(), 335 obiekt
WiezaController, 74 getSlug(), 276 $entity, 299
ZamekController, 74 godzinaAction(), 131 Entity, 241
konwersja indexAction(), 35, 122, 125, odwołania do plików graficznych, 63
wejściowa, 298 199, 229, 290 odzyskiwanie hasła, 444
wyjściowa, 298 menuAction(), 201 operacja
konwertowanie znaków, 132 persist(), 241, 242 create, 389
kopiowanie zasobów, 103 refresh(), 286 delete/destroy, 389
remove(), 242, 243 read/retrieve, 389
L, Ł removeElement(), 364 update, 389
render(), 116, 124 operator is, 143
layout, 57 set(), 124, 227, 237 operatory
liczby, 140 setContents(), 325 arytmetyczne, 141
lista setIpsum(), 237 logiczne, 142
aktorów, 376 setKontynent(), 349 porównania, 141
filmów, 376 setLength(), 246 specjalne, 142
hiperłączy, 373 setSlug(), 276 oprogramowanie
kontynentów, 375 setTranslatableLocale(), ORM, 19
państw, 375 283–286 PHPUnit, 477
znaczników, 170 setUser(), 343 osadzanie formularza, 434, 435
localhost, 105 showAction(), 187, 199
logiczne nazwy metody klasy
kontrolerów, 227 Kontynent, 347
P
modeli, 227 Panstwo, 348 pakiet, bundle, 25
widoków, 227 Repository, 252 Acme, 106
logowanie, 424 User, 333 BackendBundle, 440
ładowanie pakietów, 217 model Code Sniffer, 477
Detective, 376 Cygwin, 478
Method, 377 demo, 49
M MyFrontendBundle:File, 320 DoctrineExtensionsBundle, 27,
makrodefinicja MyBackendBundle:Mountain, 276
autolink, 171 420 DoctrineFixturesBundle, 26, 215
str, 172 MyFrontendBundle:Name, 390 DoctrineMigrationsBundle, 27
MANAGED, 241 Novel, 376 FOSCommentBundle, 27
metoda modyfikacja konfiguracji projektu, FOSUserBundle, 27, 447
__toString(), 255, 299, 469 272 FrontendBundle, 430, 440
addFilm(), 362, 364 modyfikowanie KnpMarkdownBundle, 26
addPanstwo(), 349 nagłówków, 124 KnpPaginatorBundle, 26
createNotFoundException(), 189 pakietu PEAR, 476 LoremBundle, 79
dataAction(), 131 przekierowań, 442 My/AnimalsBundle, 86
detach(), 242 zabezpieczeń, 455 My/FraszkaBundle, 69
dolorAction(), 114, 115 MySQL, 223, 473 My/Frontend, 462
find(), 252 My/FrontendBundle, 257, 440
findAll(), 242, 253, 256, 316 N My/HelloworldBundle, 29
findBy(), 253 My/LakeBundle, 54
findByX(), 254 nadawanie uprawnień, 408 My/MultiplicationBundle, 157
findOneBy(), 254 nadpisywanie My/NovelBundle, 198
findOneBySlug(), 307 metod, 255, 256 My/PoemBundle, 61
findOneByX(), 255 widoków, 91, 113 MyHelloworldBundle, 34
flush(), 241 zawartości bloków, 177 PEAR, 476
486 Symfony 2 od podstaw

pakiet, bundle dogoscia.html.twig, 71 hosts, 94


phpDocumentor, 477 error.html.twig, 84, 85 z trenami, 312
rsync, 478 error404.html.twig, 85, 88 pobieranie
SonataAdminBundle, 27, 451 error500.html.twig, 85 pakietów, 211, 214, 271
SonataPageBundle, 27 filmy.xml, 365 rekordów, 243
StofDoctrineExtensionsBundle, FOSUserBundle.pl.yml, 424 Symfony 2, 213
270–272, 293 imiona.txt, 221 podział uprawnień, 429
symfony2-customized-v1.zip, 106 index.html.twig, 40, 316 podział widoków, 195, 198
symfony2-customized-v2.zip, 212 ipsum.txt.twig, 124 polecenia modyfikujące plik, 476
UserBundle, 457 jada-jada-misie.txt, 185 polecenie
valley, 50 Kernel.php, 32 deny from all, 31
XAMPP, 473 kolory.yml, 286 generate:bundle, 31, 42, 102
zabytek, 74 KontynentAdmin.php, 469 rsync, 98
pakiety kontynenty.xml,, 353 połączenie z bazą danych, 227, 258
do przetwarzania plików, 212 korona-ziemi.txt, 151 powiązanie tabel relacją 1:1, 332
komunikacyjne, 319 layout.html, 58 priorytet operatorów, 143
Sonata, 451 layout.html.twig, 59, 311 program
Symfony 2, 209, 218 LoadData.php, 247, 260, 279, GraphViz, 482
pamięć podręczna, cache, 127, 425 288, 302, 313, 321, 336, 354, instalacyjny, 473
panel administracyjny, 459, 461, 463 367, 380 NetBeans, 482
CRUD, 389, 394–396, 420, 430 LoremController.php, 67 phpMyAdmin, 224
XAMPP, 474 menu.html.twig, 201, 317 projekt Sonata, 451, 457
panel Sonata, 469 Mountain.php, 259 protokół SSH, 99
parametr mountains.xml, 257 przedrostek ROLE_, 416
$culture, 290 MyFrontendBundle.pl.xliff, 464 przekazywanie danych, 130
cascade, 339 Name.php, 389 przekazywanie do widoku
layout.login, 424 PanstwoAdmin.php, 469 obiektów, 139
nullable, 333, 348 parameters.ini, 227, 233, 244, 258 tablic, 138
orphanRemoval, 340 php.ini, 98 przekierowanie, 432
path, 415 ProfilType.php, 397 przeładowanie uprawnień, 223
repositoryClass, 251 properties.ini, 410 przestrzeń nazewnicza
role, 416 rivers.yml, 243 Gedmo, 271
pasek narzędzi routing.yml, 33, 69, 289, 407, 431 Stof, 271
developerskich, 37 routing_dev.yml, 40 przestrzeń nazw, namespace, 25
Web Debug Toolbar, 39 rsync_exclude.txt, 99 przetwarzanie widoków, 111
pętla rsync-production.bat, 98 przykład
{% for %}, 150, 201 schema.yml, 235 bezpieczna paleta kolorów, 163
for, 292 security.yml, 406, 415, 423, 455 dane użytkowników, 335
PHP, 473 services.xml, 463 data i godzina, 130
plik show.html.twig, 316 Dolina Pięciu Stawów Polskich,
00index.log, 197 Sit.php, 26 53
app.php, 19, 40 songs.yml, 299 download, 320, 325
app_dev.php, 19, 40 Dwa kabele, 60
style.css, 85
działanie dystrybucji, 458
AppKernel.php, 32, 40, 271, Symfony_Standard_2.0.x.zip, 17
dzieła literatury światowej, 155
453, 457 Symfony_Standard_Vendors_
filmy, 470
autoload.php, 215, 271, 453 2.0.X.zip, 17, 18, 28, 53
filmy i aktorzy, 365
base.html.twig, 195, 212, 432, tekst.html, 58
Fraszki, 69
436, 441 tworzenie-pustej-bazy-
gady, 86, 93, 95, 97
BramaController.php, 74 danych.bat, 223
Hello, world!, 28
Color.php, 288 tworzenie-pustej-bazy-
imiona, 221, 394
config.yml, 192, 272, 407 danych.sql, 244
kolory, 286
config.php, 19 users.xml, 335 kontynent i państwa, 353
config_prod.yml, 41 WiezaController.php, 74 kontynenty, 469
Controller.php, 37 Word.php, 278 kontynenty/państwa, 375, 439,
CSS, 203 ZamekController.php, 74 442, 444
dedykacja.txt, 311 zawierający tłumaczenia, 464 korona ziemi, 151, 419, 429, 435
DefaultController.php, 34, 40, 61, pliki miasta, 461
70, 86, 117, 131, 135, 152, .htaccess, 31, 95 opowiadania Edgara Allana Poe,
155, 158 .twig, 127 197
deps, 210, 270 deps, 210 panel CRUD, 396, 401
deps.lock, 210 deps.lock, 210 piosenki
Skorowidz 487

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

typ blob, 319 show.html.twig, 191, 202 znacznik, tag, 169


typ kolumny, 282 show.txt.twig, 192 {% block %}, 177
typ MIME, 321, 325 właściciel relacji, owing side, 342, {% extends %}, 178
typy danych, 238 349, 385 {% include %}, 385
1:n, 349 {% render %}, 201
n:m, 361 {{ }}, 130, 139
U właściwości rekordu, 299 autoescape, 134, 180
układ graficzny, 57 właściwość block, 175
uprawnienia $caption, 227 do, 180
do aplikacji, 431 $id, 227 extends, 173, 175
do witryny, 431 $locale, 283 filter, 172
dostępu, 415 $profil, 332 flush, 180
konta, 417 $slug, 276 for, 149, 171
plików i folderów, 98 created, 282 from, 171
użytkownika, 417 length, 246 head, 196
uprawnienie name, 284 if, 171
ROLE_ADMIN, 427, 439 slug, 278 import, 171
ROLE_USER, 427 updated, 282 include, 179
uruchamianie projektu, 46 włączanie filtrów, 213 link, 196
ustalanie strony głównej, 203 włączenie zachowań timestampable, macro, 171
usuwanie, ON DELETE, 338 281 meta, 196
akcji, 66 wtyczka raw, 180
bazy danych, 233 FOSUserBundle, 416 render, 181
kontrolera, 67 Live HTTP Headers, 84 set, 173
pakietu, 42, 67, 213 wyjątek, 334 spaceless, 179
pakietu demo, 28 wylogowanie, 423 use, 178
powiązania relacyjnego, 364 wypełnianie tabeli, 228 znaczniki, 169
rekordów, 243 wyrażenia Twig, 139 czasu, 282
wyszukiwanie, 368 HTML, 111
uprawnień, 409, 426
wyszukiwanie rekordu, 298 Twig, 121
użytkownik
znak |, 200
uprawnienia, 417
Z
W zabezpieczanie zmiennych, 134
zabezpieczenia, 406
wartość
zachowania, behaviours, 269
logiczna, 140
Doctrine, 293
NULL, 239
zachowanie
slug, 280, 307
sluggable, 269, 275–278, 308
wersja Symfony 2, 213
timestampable, 269, 281
widok, view, 25, 28
translatable, 269, 283–284, 292
akcji, 123
zalogowany użytkownik, 415
akcji index, 249, 261, 291, 303, zasoby zewnętrzne, 47, 103
316, 337, 355, 369 Zend Framework, 27
akcji jaszczurka, 88 zmienna
akcji menu, 317 $culture, 289
akcji show, 304, 316 $entities, 231
base.html.twig, 178, 190, 196, $entity, 299
201, 248, 260, 302, 423 $novel, 200
częściowy, 373, 374, 382 $slug, 200
error403.html.twig, 444 $title, 200
error404.html.twig, 89 $tytul, 314
error500.html.twig, 91 ipsum, 130
formularza, 435 loop, 148
index.html.twig, 38, 51, 55, 62, loop.first, 149
174, 178, 197 loop.index, 148
layout.html.twig, 178, 196, 202, loop.last, 149
248, 261, 315 slug, 203
lorem.html.twig, 119 zmienne
menu.html.twig, 201 globalne, 145
powitanie.html.twig, 136 tablicowe, 138
Notatki

Vous aimerez peut-être aussi