![]() |
|
Practical symfonyDzień 3: Model |
|
You are currently reading "Practical symfony" which is licensed under the Creative Commons Attribution-Share Alike 3.0 Unported License license.
sfDoctrinePlugin 
|
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 Unported License. |
Ci z Was, których świerzbi by otworzyć edytor i napisać trochę kodu PHP będą szczęśliwi, ponieważ w dzisiejszym tutorialu zajmiemy się kodowaniem. Zdefiniujemy model dla Jobeet, użyjemy ORM do interakcji z bazą i zbudujemy pierwszy moduł dla naszej plikacji. A dzięki temu, że symfony wykonuje dużo pracy za nas, otrzymamy w pełni funkcjonalny moduł bez pisania dużej ilości kodu PHP.
sfDoctrinePluginJeżeli to czytasz, to znaczy, że zdecydowałeś się ukończyć tutorial Jobeet dla
Doctrine ORM zamiast dla Propela. Jest to na tyle proste, że wystarczy na początek
włączyć sfDoctrinePlugin i wyłączyć sfPropelPlugin. Można to zrobić za pomocą
następującego kodu w pliku config/ProjectConfiguration.class.php.
public function setup() { $this->enablePlugins(array('sfDoctrinePlugin')); $this->disablePlugins(array('sfPropelPlugin')); }
Jeśli wolisz mieć wszystkie pluginy domyślnie włączone, możesz zrobić:
public function setup() { $this->enableAllPluginsExcept(array('sfPropelPlugin', 'sfCompat10Plugin')); }
Po tej zmianie będą się pojawiać błędy dopóki poźniej nie skonfigurujemy pliku
config/databases.ymldo współpracy zsfDoctrineDatabase.
Pamiętaj o wyczyszczeniu cacheu, po tych zmianiach.
$ php symfony cc
Historie użytkowników, które napisaliśmy wczoraj opisują główne obiekty w naszym projekcie: oferty pracy, współpracowników i kategorie. Poniżej jest przedstawiony diagram ERD (Entity Relationship Diagram):

Jako uzupełnienie kolumn opisanych w historiach dodaliśmy jeszcze kolumnę
created_at do niektórych tabel. Symfony rozpoznaje takie pola i ustawia wartość
czasu systemowego w czasie tworzenia rekordu. To samo się tyczy kolumn
updated_at: Ich wartości są ustawiane za każdą aktualizacją rekordu.
Żeby przechować ofert pracy, współpracowników i kategorie napewno będziemy potrzebować relacyjnej bazy danych.
Ale skoro symfony jest frameworkiem zorientowanym obiektowo, wolimy operować na obiektach kiedy to tylko możliwe. Na przykład, zamiast pisać zapytania SQL do wyciągnięcia rekordów z bazy, wolelibyśmy używać obiektów.
Informacje z bazy relacyjnej muszą być zmapowane na model obiektowy. Można tego dokonać za pomocą narzędzia ORM i na szczęście symfony ma w sobie aż dwa: Propel i Doctrine. W tym tutorialu użyjemy Doctrine.
ORM potrzebuje opisu tabel i ich powiązań by stworzyć powiązane klasy. Istnieją dwa sposoby stworzenia tego schematu: The ORM needs a description of the tables and their relationships to create the related classes. There are two ways to create this description schema: przez introspekcję (analizę) istniejącej bazy lub przez stworzenie jej ręcznie.
Ponieważ baza danych nie istnieje jeszcze i ponieważ chcemy utrzymać bazę
Jobeet agnostyczną, stwórzmy plik ze schematem ręcznie poprzez edycję
pustego pliku config/doctrine/schema.yml:
# config/doctrine/schema.yml
---
JobeetCategory:
actAs:
Timestampable: ~
columns:
name:
type: string(255)
notnull: true
JobeetJob:
actAs:
Timestampable: ~
columns:
category_id:
type: integer
notnull: true
type:
type: string(255)
company:
type: string(255)
notnull: true
logo:
type: string(255)
url:
type: string(255)
position:
type: string(255)
notnull: true
location:
type: string(255)
notnull: true
description:
type: string(4000)
notnull: true
how_to_apply:
type: string(4000)
notnull: true
token:
type: string(255)
notnull: true
unique: true
is_public:
type: boolean
notnull: true
default: 1
is_activated:
type: boolean
notnull: true
default: 0
email:
type: string(255)
notnull: true
expires_at:
type: timestamp
notnull: true
relations:
JobeetCategory:
onDelete: CASCADE
local: category_id
foreign: id
foreignAlias: JobeetJobs
JobeetAffiliate:
actAs:
Timestampable: ~
columns:
url:
type: string(255)
notnull: true
email:
type: string(255)
notnull: true
unique: true
token:
type: string(255)
notnull: true
is_active:
type: boolean
notnull: true
default: 0
relations:
JobeetCategories:
class: JobeetCategory
refClass: JobeetCategoryAffiliate
local: affiliate_id
foreign: category_id
foreignAlias: JobeetAffiliates
JobeetCategoryAffiliate:
columns:
category_id:
type: integer
primary: true
affiliate_id:
type: integer
primary: true
relations:
JobeetCategory:
onDelete: CASCADE
local: category_id
foreign: id
JobeetAffiliate:
onDelete: CASCADE
local: affiliate_id
foreign: id
Jeśli się zdecydowałeś stworzyć tabele pisząc polecenia SQL możesz wygenerować odpowiedni plik
schema.ymluruchamiając poleceniedoctrine:build-schema.
Schemat jest bezpośrednią translacją diagramu ERD do formatu YAML.
Format YAML
Zgodnie z oficjalną stroną YAML, YAML jest "przyjaznym dla człowieka standardem serializacji danych dla wszystkich języków programowania"
Inaczej mówiąc, YAML jest prostym językiem opisu danych (stringi, liczby calkowite, daty, tablice i hashe).
W YAML struktura jest pokazana za pomocą wcięć, elementy sekwencyjne są oznaczone poprzez myślnik, a pary klucz/wartość są oddzielane za pomocą dwukropka. YAML także ma krótszą składnię do opisania tej samej struktury w mnieszej ilości linii, gdzie tablice są wyraźnie pokazane za pomocą
[]a hashe za pomocą{}.Jeśli nie jesteś zaznajomiony z YAMLem, to najwyższy czas zacząć ponieważ framework symfony używa go obszernie do plików konfiguracyjnych.
Plik schema.yml zawiera opis wszystkich tabel i ich kolumn.
Każda kolumna jest opisana za pomocą nasepujących informacji:
type: Rodzaj kolumny (boolean, integer, float, decimal,
string, array, object, blob, clob, timestamp,
time, date, enum, gzip)notnull: Ustaw na true jeśli chcesz by kolumna była wymaganaunique: Ustaw na true jeśli chcesz stworzyć indeks unikalny dla kolumny.Atrybut
onDeletedefiniuje zachowanieON DELETEdla kluczy obcych, Doctrine wspieraCASCADE,SETNULLorazRESTRICT. Na przykład, gdy rekordjobjest usuwany, powiązane rekordy zjobeet_category_affiliatezostaną automatycznie usunięte przez bazę danych.
Framework symfony wspiera wszystkie obslugiwane przez PDO bazy danych (MySQL, PostgreSQL, SQLite, Oracle, MSSQL, ...). PDO jest warstwą abstracji dla baz danych dołączoną do PHP.
Użyjmy MySQL w tym tutorialu:
$ mysqladmin -uroot -pmYsEcret create jobeet
Jeśli chcesz, możesz oczywiście wybrać inny silnik bazodanowy. Nie będzie to trudne, żeby dostosować kod który napiszemy, ponieważ będziemy używać ORM, a to narzędzie napisze SQL za nas.
Musimy powiedzieć symfony, żeby używała tej bazy dla projektu Jobeet:
$ php symfony configure:database --name=doctrine --class=sfDoctrineDatabase "mysql:host=localhost;dbname=jobeet" root mYsEcret
Po skonfigurowaniu połaczenia z bazą, będziesz musiał ręcznie wyedytować plik
config/databases.ymli usunąć wszystkie połączenia związane z propelem.
Polecenie configure:database przyjmuje trzy parametry:
PDO DSN, nazwa użytkownika
i hasło do dostępu do bazy. Jeśli masz hasła na swoim serwerze deweloperskim
po prostu pomiń trzeci parametr.
Polecenie
configure:databaseprzechowuje konfigurację bazy w plikuconfig/databases.yml. Zamiast używania polecenia możesz wyedytować ten plik ręcznie.
Dzięki opisowi bazy z pliku schema.yml możemy użyć pewnych wbudowanych poleceń
Doctrine do wygenerowania poleceń SQL potrzebnych do stworzenia tabel w bazie:
Przed wygenerowaniem SQL musi zostać wygenerowany model na bazie plików schematu.
$ php symfony doctrine:build-model
Teraz gdy modele są gotowe można wygenerować SQL i wykonać go na bazie.
$ php symfony doctrine:build-sql
Polecenie doctrine:build-sql generuje polecenia SQL w katalogu data/sql
odpowiednie do wybranego silnika bazodanowego:
# fragment z data/sql/schema.sql CREATE TABLE jobeet_category (id BIGINT AUTO_INCREMENT, name VARCHAR(255) NOT NULL COMMENT 'test', created_at DATETIME, updated_at DATETIME, slug VARCHAR(255), UNIQUE INDEX sluggable_idx (slug), PRIMARY KEY(id)) ENGINE = INNODB;
By stworzyć tabele w bazie musisz wykonać polecenie doctrine:insert-sql:
$ php symfony doctrine:insert-sql
Jak przy każdym narzędziu konsolowym polecenia symfony mogą pobierać dodatkowe argumenty. Każde polecenie ma wbudowaną pomoc, którą można wyświetlić poleceniem
help:$ php symfony help doctrine:insert-sqlPokazuje wszystkie możliwe argumenty, podaje domyślne wartości dla nich i przykład użycia.
ORM dodatkowo generuje klasy PHP, które odpowiadają za mapowanie rekordów na obiekty:
$ php symfony doctrine:build-model
Polecenie doctrine:build-model generuje pliki PHP w katalogulib/model, których
można używać do interakcji z bazą.
Przeglądając wygenerowane pliki napewno zauważyłeś, że Doctrine generuje
trzy klasy na tabelę. Dla tebeli jobeet_job:
JobeetJob: Obiekt tej klasy reprezentuje jeden rekord z tabeli
jobeet_job. Klasa jest domyślnie pusta.BaseJobeetJob: Klasa bazowa dla JobeetJob. Za każdym razem gdy
uruchamiasz doctrine:build-model ta klasa jest nadpisywana,
więc wszystkie zmiany muszą być robione w klasie JobeetJob.
JobeetJobTable: Klasa definiuje metody, które w większości zwracają
kolekcje obiektów JobeetJob. Klasa jest domyślnie pusta.
Wartości kolumn rekordu mogą być zmieniane za pomocą obiektu modelu używając
akcesorów (metod get*()) i mutatorów (metod set*()):
$job = new JobeetJob(); $job->setPosition('Web developer'); $job->save(); echo $job->getPosition(); $job->delete();
Możesz również definiować klucze obce wprost łącząc obiekty ze sobą:
$category = new JobeetCategory(); $category->setName('Programming'); $job = new JobeetJob(); $job->setCategory($category);
Polecenie doctrine:build-all jest skrótem dla poleceń, które uruchamialiśmy
w tej sekcji i paru dodatkowych. Więc uruchom to polecenie teraz, żeby
wygenerować formularze i walidatory dla klasy modelu Jobeet:
$ php symfony doctrine:build-all
Zobaczysz walidatory w akcji na koniec dnia, a formularze będą dokladnie wytłumaczone 10 dnia.
Polecenie
doctrine:build-all-reloadjest skrótem dladoctrine:build-all, które wykonuje dodatkowodoctrine:data-load.
Jak zauważysz później, symfony ładuje automatycznie klasy PHP za Ciebie, przez
to nigdy nie będziesz musiał używać require w kodzie. To jest jedna z wielu
rzeczy jakie symfony wykonuje automatycznie za dewelopera, ale jest jedno "ale":
kiedykolwiek dodajesz nową klasę musisz wyczyścić cache symfony. Ponieważ
polecenie doctrine:build-model stworzylo dużo nowych klas, pora wyczyścić cache:
$ php symfony cache:clear
Polecenie symfony jest zrobione z przestrzeni nazw i nazwy polecenia. Każde z nich może być skrócone dopóki nie wystąpi niejednoznaczność z inymi poleceniami. Więc poniższe polecenia są równoważne do
cache:clear:$ php symfony cache:cl $ php symfony ca:cPonieważ polecenie
cache:clearjest tak często używane, ma jeszcze jeden zdefiniowany skrót:$ php symfony cc
Tabele zostały utworzone w bazie, ale nie zawierają danych. Dla każdej aplikacji webowej istnieją trzy typy danych:
Dane początkowe: Dane początkowe, które są wymagane do dzialania aplikacji. Np. Jobeet wymaga kilku początkowych kategorii. Jeśli ich nie będzie nikt nie będzie mógł zamieścić ogłoszenia. Dodatkowo potrzebujemy administratora, który będzie mógł się zalogować do backendu.
Dane testowe: Dane testowe są potrzebne do testowania aplikacji. Jako deweloper będziesz pisał testy, żeby mieć pewność że Jobeet zachowuje się w sposób opisany w historiach użytkowników, a najlepszym sposobem jest pisanie testów automatycznych. Więc przy każdym uruchomieniu testów potrzebna jest czysta baza danych z danymi testowymi.
Dane użytkownika: Dane użytkownika są tworzone przez użytkowników w czasie normalnego życia aplikacji.
Za każdym razem, gdy symfony tworzy tabele w bazie wszystkie dane są usuwane.
Aby wypełnić bazę danymi początkowymi powinniśmy storzyć skrypt PHP lub
wykonać SQL za pomocą programu mysql. Ale ponieważ jest to częste, istnieje
lepszy sposób za pomocą symfony: tworząc pliki YAML w katalogu data/fixtures/
oraz używając polecenia doctrine:data-load do załadowania ich do bazy danych:
# data/fixtures/categories.yml
JobeetCategory:
design:
name: Design
programming:
name: Programming
manager:
name: Manager
administrator:
name: Administrator
# data/fixtures/jobs.yml
JobeetJob:
job_sensio_labs:
JobeetCategory: programming
type: full-time
company: Sensio Labs
logo: /uploads/jobs/sensio_labs.png
url: http://www.sensiolabs.com/
position: Web Developer
location: Paris, France
description: |
You've already developed websites with symfony and you want to work
with Open-Source technologies. You have a minimum of 3 years
experience in web development with PHP or Java and you wish to
participate to development of Web 2.0 sites using the best
frameworks available.
how_to_apply: |
Send your resume to fabien.potencier [at] sensio.com
is_public: true
is_activated: true
token: job_sensio_labs
email: job@example.com
expires_at: '2008-10-10'
job_extreme_sensio:
JobeetCategory: design
type: part-time
company: Extreme Sensio
logo: /uploads/jobs/extreme_sensio.png
url: http://www.extreme-sensio.com/
position: Web Designer
location: Paris, France
description: |
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do
eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
enim ad minim veniam, quis nostrud exercitation ullamco laboris
nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor
in reprehenderit in.
Voluptate velit esse cillum dolore eu fugiat nulla pariatur.
Excepteur sint occaecat cupidatat non proident, sunt in culpa
qui officia deserunt mollit anim id est laborum.
how_to_apply: |
Send your resume to fabien.potencier [at] sensio.com
is_public: true
is_activated: true
token: job_extreme_sensio
email: job@example.com
expires_at: '2008-10-10'
Plik fixturów jest napisany w formacie YAML i definiuje obiekty modelu oznaczone
za pomocą unikalnych etykiet. Etykiety są bardzo przydatne przy łączeniu obiektów
powiązanych bez potrzeby definiowania kluczy głównych, które często są automatycznie
inkrementowane i nie można ich ustawić. Np. kategoria w ofercie job_sensio_labs
jest ustawiona na programming, a tą etykietą jest oznaczona kategoria 'Programming'.
Plik fixturów może zawierać obiekty z jednego lub wielu modeli.
Propel wymaga by pliki fixturów miały numeryczne prefiksy do zapewnienia kolejności ładowania plików. Przy Doctrine nie jest to wymagane, ponieważ wszystkie pliki będą załadowane i zapisane w odpowiedniej kolejności by mieć pewność, że klucze obce zostały ustawione właściwie.
W pliku fuxturów nie trzeba definiować wartości dla wszystkich kolumn.
Symfony użyje domyślnyhc wartości zawartych w schemacie bazy dla
nie zdefiniowanych kolumn. A ponieważ symfony używa Doctrine do ładowania
danych do bazy, wszystkie wbudowane behaviory (obsługa kolumn created_at
lub updated_at) lub dodatkowo zdefiniowane w klasach modelu, są obsługiwane.
Ładowanie danych początkowych do bazy jest proste, wystarczy użyć polecenia
doctrine:data-load:
$ php symfony doctrine:data-load
Uzywaliśmy dość dużo konsoli, ale to nie jest tak ekscytujące, zwłaszcza przy tworzeniu projektu webowego. Teraz mamy wszystko co potrzebne do stworzenia stron, które będą operować na bazie danych.
Zobaczmy jak wyświetlić listę ofert pracy, jak wyedytować istniejącą ofertę oraz jak usunąć. Jak zostało wyjaśnione w czasie pierwszego dnia, projekt w symfony jest stworzony z aplikacji. Każda aplikacja jest stworzona z modułów. Moduł jest samodzielnym zestawem kodu PHP, który reprezentuje daną funkcjonalność aplikacji (np. moduł API) lub zestawem funkcji, które może wykonywać użytkownik na obiektach modelu(np. moduł ofert pracy - job).
Symfony może automatycznie wygenerować moduł dla danego modelu, który pozwala wykonywać podstawowe operacje:
$ php symfony doctrine:generate-module --with-show --non-verbose-templates frontend job JobeetJob
Polecenie doctrine:generate-module generuje moduł job w aplikacji
frontend dla modelu JobeetJob. Jak w przypadku większości poleceń
symfony, zostały wygenerowane pliki i katalogi w katalogu
apps/frontend/modules/job:
| Katalog | Opis |
|---|---|
actions/ |
Akcje dla modułu |
templates/ |
Szablony dla modułu |
Plik actions/actions.class.php definiuje wszystkie dostępne akcje dla
modułu job:
| Nazwa akcji | Opis |
|---|---|
index |
Wyświetla rekordy tabeli |
show |
Wyświetla pola danego rekordu |
new |
Wyświetla formularz tworzenia nowego rekordu |
create |
Tworzy nowy rekord |
edit |
Wyświetla formularz edycji danego rekordu |
update |
Aktualizuje rekord wartościami zmienionymi przez użytkownika |
delete |
Usuwa dany rekord z tabeli |
Teraz możesz sprawdzić moduł job w przeglądarce:
http://jobeet.localhost/frontend_dev.php/job

Jeżeli spróbujesz wyedytować ofertę, zauważysz że lista rozwijana Category id
zawiera listę wszystkich nazw kategorii. Wartość każdej opcji jest pobierana
z metody __toString().
Doctrine spróbuje zapewnić bazową metodę __toString() zgadując nazwę kolumny
zawierającej opis np. title, name, subject, itd. Jeśli chcesz możesz dodać
swoją implementację metody __toString().
Model JobeetCategory jest w stanie zgadnąć metodę __toString() używając
kolumny name z tabeli jobeet_category.
// lib/model/doctrine/JobeetJob.class.php class JobeetJob extends BaseJobeetJob { public function __toString() { return sprintf('%s at %s (%s)', $this->getPosition(), $this->getCompany(), $this->getLocation()); } } // lib/model/doctrine/JobeetAffiliate.class.php class JobeetAffiliate extends BaseJobeetAffiliate { public function __toString() { return $this->getUrl(); } }
Możesz teraz stworzyć i edytować oferty pracy. Spróbuj pozostawić pole oznaczone jako wymagane puste lub spróbuj wpisać błędną datę. Tak, symfony stworzyło podstawowe reguly walidacji poprzez introspekcję schematu bazy danych.

To wszystko na dzisiaj. Ostrzegłem Ciebie na początku. Dzisiaj napisaliśmy trochę kodu PHP, ale mamy działający moduł dla modelu ofert pracy gotowy do zmian. Pamiętaj, brak kodu oznacza również brak błędów!
Jeśli masz w sobie jeszcze trochę energii, nie krępuj się i poczytaj sobie wygenerowany kod dla modułu i modelu oraz spróbuj zrozumieć jak to działa. Jeśli nie, nie przejmuj się i śpij dobrze, jutro porozmawiamy o jednym z najczęściej używanych wzorców projektowych w webowych frameworkach wzorcu projektowym MVC.
If you find a typo or an error, please register and open a ticket.
If you need support or have a technical question, please post to the official user mailing-list.