Caution: You are browsing the legacy symfony 1.x part of this website.
Cover of the book Symfony 5: The Fast Track

Symfony 5: The Fast Track is the best book to learn modern Symfony development, from zero to production. +300 pages showcasing Symfony with Docker, APIs, queues & async tasks, Webpack, SPAs, etc.

Dzień 3: Model

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.

Model relacyjny

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

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.

Schemat

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

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.

note

Niektóre narzędzia pozwalają budować bazę graficznie (np. Fabforce Dbdesigner) i wygenerowanie pliku schema.xml (za pomocą DB Designer 4 TO Propel Schema Converter).

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/schema.yml:

# config/schema.yml
propel:
  jobeet_category:
    id:           ~
    name:         { type: varchar(255), required: true }
 
  jobeet_job:
    id:           ~
    category_id:  { type: integer, foreignTable: jobeet_category, foreignReference: id, required: true }
    type:         { type: varchar(255) }
    company:      { type: varchar(255), required: true }
    logo:         { type: varchar(255) }
    url:          { type: varchar(255) }
    position:     { type: varchar(255), required: true }
    location:     { type: varchar(255), required: true }
    description:  { type: longvarchar, required: true }
    how_to_apply: { type: longvarchar, required: true }
    token:        { type: varchar(255), required: true, index: unique }
    is_public:    { type: boolean, required: true, default: 1 }
    is_activated: { type: boolean, required: true, default: 0 }
    email:        { type: varchar(255), required: true }
    expires_at:   { type: timestamp, required: true }
    created_at:   ~
    updated_at:   ~
 
  jobeet_affiliate:
    id:           ~
    url:          { type: varchar(255), required: true }
    email:        { type: varchar(255), required: true, index: unique }
    token:        { type: varchar(255), required: true }
    is_active:    { type: boolean, required: true, default: 0 }
    created_at:   ~
 
  jobeet_category_affiliate:
    category_id:  { type: integer, foreignTable: jobeet_category, foreignReference: id, required: true, primaryKey: true, onDelete: cascade }
    affiliate_id: { type: integer, foreignTable: jobeet_affiliate, foreignReference: id, required: true, primaryKey: true, onDelete: cascade }

tip

Jeśli się zdecydowałeś stworzyć tabele pisząc polecenia SQL możesz wygenerować odpowiedni plik schema.yml uruchamiając polecenie propel:build-schema.

Schemat jest bezpośrednią translacją diagramu ERD do formatu YAML.

sidebar

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, tinyint, smallint, integer, bigint, double, float, real, decimal, char, varchar(size), longvarchar, date, time, timestamp, blob, and clob)
  • required: Ustaw na true jeśli chcesz by kolumna była wymagana
  • index: Ustaw na true jeśli chcesz stworzyć indeks dla kolumny lub na unique jeśli chcesz stworzyć indeks unikalny dla kolumny.

Dla kolumn ustawionych na ~ (id, created_at, and updated_at), symfony zgadnie najlepszą konfigurację (klucz główny dla id oraz timestamp dla created_at i updated_at).

note

Atrybut onDelete definiuje zachowanie ON DELETE dla kluczy obcych, Propel wspiera CASCADE, SETNULL oraz RESTRICT. Na przykład, gdy rekord job jest usuwany, powiązane rekordy z jobeet_category_affiliate zostaną automatycznie usunięte przez bazę danych lub przez Propela jeśli używany silnik bazodanowy tego nie obsługuje.

Baza 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

note

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 "mysql:host=localhost;dbname=jobeet" root mYsEcret

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.

note

Polecenie configure:database przechowuje konfigurację bazy w pliku config/databases.yml. Zamiast używania polecenia możesz wyedytować ten plik ręcznie.

ORM

Dzięki opisowi bazy z pliku schema.yml możemy użyć pewnych wbudowanych poleceń Propel do wygenerowania poleceń SQL potrzebnych do stworzenia tabel w bazie:

$ php symfony propel:build-sql

Polecenie propel:build-sql generuje polecenia SQL w katalogu data/sql odpowiednie do wybranego silnika bazodanowego:

# fragment z data/sql/lib.model.schema.sql
CREATE TABLE `jobeet_category`
(
  `id` INTEGER  NOT NULL AUTO_INCREMENT,
  `name` VARCHAR(255)  NOT NULL,
  PRIMARY KEY (`id`)
)Type=InnoDB;

By stworzyć tabele w bazie musisz wykonać polecenie propel:insert-sql:

$ php symfony propel:insert-sql

Ponieważ to polecenie usuwa istniejące tabele przed stworzeniem ich od nowa musisz potwierdzić tą operację. Możesz również dodać parametr --no-confirmation, żeby pominąć to pytanie, co przydaje się przy uruchamianiu kilku poleceń po sobie:

$ php symfony propel:insert-sql --no-confirmation

tip

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 propel:insert-sql

Pokazuje 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 propel:build-model

Polecenie propel: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 Propel generuje cztery 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 propel:build-model ta klasa jest nadpisywana, więc wszystkie zmiany muszą być robione w klasie JobeetJob.

  • JobeetJobPeer: Klasa definiuje statyczne metody, które w większości zwracają kolekcje obiektów JobeetJob. Klasa jest domyślnie pusta.

  • BaseJobeetJobPeer: Klasa bazowa dla JobeetJobPeer. Za każdym razem gdy uruchamiasz propel:build-model ta klasa jest nadpisywana, więc wszystkie zmiany muszą być robione w klasie JobeetJobPeer.

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 propel: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 propel:build-all

Zobaczysz walidatory w akcji na koniec dnia, a formularze będą dokladnie wytłumaczone 10 dnia.

tip

Polecenie propel:build-all-load jest skrótem dla propel:build-all, które wykonuje dodatkowo propel: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 propel:build-model stworzylo dużo nowych klas, pora wyczyścić cache:

 $ php symfony cache:clear

tip

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

Ponieważ polecenie cache:clear jest tak często używane, ma jeszcze jeden zdefiniowany skrót:

$ php symfony cc

Dane początkowe

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 propel:data-load do załadowania ich do bazy danych:

# data/fixtures/010_categories.yml
JobeetCategory:
  design:        { name: Design }
  programming:   { name: Programming }
  manager:       { name: Manager }
  administrator: { name: Administrator }
 
# data/fixtures/020_jobs.yml
JobeetJob:
  job_sensio_labs:
    category_id:  programming
    type:         full-time
    company:      Sensio Labs
    logo:         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:        [email protected]
    expires_at:   2010-10-10
 
  job_extreme_sensio:
    category_id:  design
    type:         part-time
    company:      Extreme Sensio
    logo:         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:        [email protected]
    expires_at:   2010-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.

tip

Zwróć uwagę na prefiksy liczbowe nazw plików. To prosta metoda na kontrolę kolejności ładowanych danych. Później w projekcie, jeśli będziemy potrzebować dodać nowy plik fixturów, będzie łatwo ponieważ będą jeszcze wolne liczby pomiędzy istniejącymi.

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 Propel 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 propel:data-load:

$ php symfony propel:data-load

Zobacz tow akcji w przeglądarce

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 propel:generate-module --with-show --non-verbose-templates frontend job JobeetJob

Polecenie propel: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

Job module

Jeżeli spróbujesz wyedytować ofertę, dostaniesz wyjątek ponieważ symfony potrzebuje reprezentacji tekstowej kategorii. W PHP można to zrobić za pomocą metody magicznej __toString(). Powinno to zostać zdefiniowane w klasie modelu JobeetCategory:

// lib/model/JobeetCategory.php
class JobeetCategory extends BaseJobeetCategory
{
  public function __toString()
  {
    return $this->getName();
  }
}

Teraz za każdym razem, gdy symfony potrzebuje reprezentacji tekstowej kategorii wywołuje metodę __toString(), która zwraca nazwę kategorii. Ponieważ prędzej czy później będzie to potrzebne dla wszystkich klas modelu zdefiniujmy metody __toString() dla każdej z nich:

// lib/model/JobeetJob.php
class JobeetJob extends BaseJobeetJob
{
  public function __toString()
  {
    return sprintf('%s at %s (%s)', $this->getPosition(), $this->getCompany(), $this->getLocation());
  }
}
 
// lib/model/JobeetAffiliate.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.

validation

Do zobaczenia jutro

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.