![]() |
|
Practical symfonyGiorno 3: Il Modello dei dati |
|
You are currently reading "Practical symfony" which is licensed under the Creative Commons Attribution-Share Alike 3.0 Unported License license.

|
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 Unported License. |
Quelli di voi che non vedono l'ora di aprire l'editor di testo e scrivere un po' di PHP saranno contenti di sapere che il tutorial di oggi darà la possibilità di fare un po' di sviluppo. Definiremo il modello dei dati di Jobeet, utilizzeremo un ORM per interagire con il database e creeremo il primo modulo della nostra applicazione. Siccome symfony fa il grosso del lavoro per noi, avremo un modulo completamente funzionante scrivendo solo poche righe di codice PHP.
Le user story che abbiamo scritto ieri descrivono le entità principali del nostro progetto: annunci di lavoro, affiliati, categorie. Di seguito abbiamo il corrispondente diagramma Entità-Relazione:

In aggiunta alle colonne descritte nelle user stories, abbiamo aggiunto
il campo created_at ad alcune tabelle. Symfony riconosce questi campi e ne
imposta il valore all'attuale timestamp di sistema quando un record viene
creato. Vale lo stesso per il campi updated_at: il loro valore è impostato
all'attuale timestamp di sistema ogni volta che un record viene aggiornato.
Per memorizzare gli annunci di lavoro, gli affiliati e le categorie abbiamo ovviamente bisogno di un database relazionale.
Dato che symfony è un framework orientato agli oggetti, ci farebbe piacere manipolare oggetti il più possibile. Per esempio, invece che scrivere query SQL per ottenere record dal database, preferiamo utilizzare gli oggetti.
Le informazioni del database relazionale devono essere mappate da un modello ad oggetti. Questo può essere fatto con un ORM e fortunatamente symfony viene distribuito con due soluzioni di questo tipo: Propel e Doctrine. In questo tutorial useremo Propel.
L'ORM ha bisogno della descrizione delle tabelle e delle loro relazioni per creare le relative classi. Esistono due metodi per creare questo schema descrittivo: analizzando un database esistente o creandone uno a mano.
Alcuni tool permettono di creare un database graficamente (per esempio Fabforce's Dbdesigner) e di generare direttamente uno
schema.xml(con DB Designer 4 TO Propel Schema Converter).
Siccome il database non esiste ancora e non vogliamo prendere decisioni su
di esso (come realizzarlo), creeremo lo schema a mano modificando un file vuoto
config/schema.yml:
# config/schema.yml
propel:
jobeet_category:
id: ~
name: { type: varchar(255), required: true, index: unique }
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 }
Se avete deciso di creare le tabelle scrivendo il codice SQL, sappiate che si può generare il corrispondente file di configurazione
schema.ymlutilizzando il taskpropel:build-schema.$ php symfony propel:build-schemaQuesto task richiede di avere un database configurato in
databases.yml. Mostreremo come configurare il database in un passo successivo. Se si prova ad eseguire questo task ora, non funzionerà, perché non sa per quale database costruire lo schema.
Lo schema è la traduzione diretta del diagramma entità-relazioni nel formato YAML.
Il formato YAML
Secondo il sito ufficiale di YAML, YAML "è uno standard di serializzazione dei dati human friendly per tutti i linguaggi di programmazione"
Detto in altro modo, YAML è un semplice linguaggio per descrivere dati (stringhe, interi, date, array e hash).
In YAML la struttura è mostrata in base all'indentazione, entità consecutive sono indicate con un trattino e le coppie chiave/valore sono separate dai due punti. YAML ha anche una sintassi ridotta per descrivere la stessa struttura con meno linee, dove gli array sono espressi con
[]e gli hash con{}.Se non avete familiarità con YAML, è il momento di iniziare ad usarlo, visto che il framework symfony lo utilizza ampiamente per i suoi file di configurazione.
C'è una cosa importante da ricordare quando si modifica un file YAML: l'indentazione deve essere fatta con uno o più spazi, mai con le tabulazioni.
Il file schema.yml contiene la descrizione di tutte le tabelle e delle loro
colonne. Ogni colonna è descritta con le seguenti informazioni:
type: Il tipo di colonna (boolean, tinyint, smallint, integer,
bigint, double, float, real, decimal, char, varchar(size),
longvarchar, date, time, timestamp, blob e clob)required: Impostarlo a true se la colonna è obbligatoria (not null)~index|Indici del database~: Impostarlo a true se si vuole creare un indice per
la colonna o a unique se si vuole una chiave univoca per la colonna.primaryKey: Definisce una colonna come chiave primaria per la tabella.foreignTable, foreignReference: Definisce una colonna come
chiave esterna verso un'altra tabella.Per le colonne impostate a ~, che in YAML significa null,
(id, created_at e updated_at) symfony deciderà la migliore
configurazione (chiave primaria per id e timestamp per
created_at e updated_at).
L'attributo
onDeletedefinisce il comportamentoON DELETEdelle chiavi esterne, Propel supportaCASCADE,SETNULLeRESTRICT. Per esempio quando un recordjobviene eliminato, tutti i recordjobeet_category_affiliateassociati verranno automaticamente eliminati dal database, o da Propel se il sottostante engine non dovesse supportare tale funzionalità.
Il framework symfony supporta tutti i database supportati da PDO (MySQL, PostgreSQL, SQLite, Oracle, MSSQL, ...). PDO è il livello di astrazione per database incluso di default in PHP.
Per questo tutorial useremo MySQL.
$ mysqladmin -uroot -p create jobeet
Enter password: mYsEcret ## La password sarà mostrata come ********
Si può tranquillamente usare un altro tipo di database, se lo si vuole. Non sarà difficile adattare il codice che scriveremo, dato che l'ORM scriverà il codice SQL al nostro posto.
Occorre dire a symfony di usare questo database per Jobeet:
$ php symfony configure:database "mysql:host=localhost;dbname=jobeet" root mYsEcret
Il task configure:database accetta tre parametri: il
DSN di PDO, il nome utente
e la password per accedere al database. Se non si ha bisogno di password
per accedere al database nel server di sviluppo, basta
omettere il terzo parametro.
Il task
configure:databasesalva la configurazione all'interno del file config/databases.yml. Invece di usare questo task, si può modificare il file a mano.
Passare la password del database nella linea di comando è comodo, ma non sicuro. A seconda di chi accede al vostro ambiente, potrebbe essere meglio modificare
config/databases.ymlper cambiare la password. Ovviamente, per mantenere la password al sicuro, l'accesso al file di configurazione dovrebbe essere ristretto.
Grazie alla descrizione del database nel file schema.yml, possiamo usare
alcuni dei task integrati di Propel per generare l'SQL necessario alla creazione
delle tabelle.
$ php symfony propel:build-sql
Il task propel:build-sql genera le istruzioni SQL nella cartella data/sql,
ottimizzate per il database che abbiamo configurato.
# snippet from data/sql/lib.model.schema.sql CREATE TABLE `jobeet_category` ( `id` INTEGER NOT NULL AUTO_INCREMENT, `name` VARCHAR(255) NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `jobeet_category_U_1` (`name`) )Type=InnoDB;
Per creare le tabelle nel database, si deve eseguire il task propel:insert-sql:
$ php symfony propel:insert-sql
Dato che questo task elimina le tabelle correnti dal database prima di ricrearle, è richiesta una conferma dell'operazione. Si può comunque aggiungere il parametro --no-confirmation per saltare questa domanda, cosa che potrebbe essere utile durante l'esecuzione di un processo batch non interattivo.
$ php symfony propel:insert-sql --no-confirmation
Come ogni tool da riga di comando, symfony accetta parametri ed opzioni. Ogni task ha un messaggio d'aiuto che può essere visualizzato utilizzando il task
help:$ php symfony help propel:insert-sqlIl messaggio d'aiuto elenca tutti i possibili parametri ed opzioni, mostra i valori di default per ognuno di essi e fornisce degli utili esempi.
L'ORM genera anche le classi PHP per mappare i record delle tabelle all'interno di oggetti.
$ php symfony propel:build-model
Il task propel:build-model genera i file PHP all'interno della cartella
lib/model, che può essere utilizzata per interagire con il database.
Curiosando tra i file generati, probabilmente noterete che Propel genera quattro
classi per ogni tabella. Per la tabella jobeet_job:
JobeetJob: Un oggetto di questa classe rappresenta un singolo
record
della tabella jobeet_job. La classe è vuota di default.BaseJobeetJob: La classe estesa da JobeetJob. Ogni volta che si esegue
propel:build-model, questa classe è sovrascritta, per cui
le personalizzazione dovranno essere eseguite nella classe
JobeetJob.JobeetJobPeer: La classe definisce metodi statici che restituiscono
di solito insiemi di oggetti JobeetJob. Questa classe è
vuota di default.BaseJobeetJobPeer: La classe estesa da JobeetJobPeer. Ogni volta
che si esegue propel:build-model, questa classe è sovrascritta,
per cui le personalizzazioni dovranno essere eseguite nella
classe JobeetJobPeer.I valori delle colonne di un record possono essere manipolati tramite un oggetto
del modello, utilizzando dei getter (metodi get*()) e dei setter (metodi set*()):
$job = new JobeetJob(); $job->setPosition('Web developer'); $job->save(); echo $job->getPosition(); $job->delete();
Si possono inoltre definire chiavi esterne direttamente collegando oggetti tra loro:
$category = new JobeetCategory(); $category->setName('Programming'); $job = new JobeetJob(); $job->setCategory($category);
Il task propel:build-all è una scorciatoia per i task che abbiamo eseguito in
questa sezione, più alcuni altri. Per cui eseguite questo task per generare form e
validatori per le classi model di Jobeet.
$ php symfony propel:build-all --no-confirmation
Vedremo i validatori in azione alla fine della giornata, mentre i form verranno discussi nel dettaglio nel giorno 10.
Il task
propel:build-all-loadè una scorciatoia per il taskpropel:build-allseguito dal taskpropel:data-load.
Come vedremo in seguito, symfony carica automaticamente le classi per noi, ciò
significa che non si avrà mai bisogno di usare un require nel codice. È una
delle numerose funzionalità che symfony automatizza per lo sviluppatore, ma
purtroppo questo implica il dover svuotare la cache ogni volta si aggiunge una
nuova classe. Dato che propel:build-model ha creato molte nuove classi,
puliamo la cache:
$ php symfony cache:clear
Un task di symfony è composto da un namespace e da un nome. Ognuno può essere accorciato se non c'è ambiguità con altri task. Per cui il seguente comando è equivalente a
cache:clear:$ php symfony cache:cl $ php symfony ca:cSiccome il task
cache:clearè usato spesso, è stato ulteriormente accorciato in:$ php symfony cc
Le tabelle create nel database ci sono, ma al momento sono vuote. Per ogni applicazione web, ci sono tre tipi da dati:
Dati iniziali: I dati iniziali sono necessari dall'applicazione per funzionare. Per esempio, Jobeet necessita di alcune categorie iniziali. Altrimenti nessuno sarebbe capace di inserire un annuncio. Abbiamo inoltre bisogno di un utente amministratore, per eseguire il login al backend.
Dati per i test: I dati per i test sono necessari alle applicazioni per essere testate. Come sviluppatore, si scriveranno dei test per assicurarsi che Jobeet si comporti come descritto nelle previsioni ed il modo migliore è scrivere dei test automatizzati. Così, ogni volta che eseguiremo un test, avremo bisogno di un database pulito con dei dati da testare.
Dati degli utenti: I dati degli utenti sono i dati creati dagli utenti durante il normale ciclo di vita di un'applicazione.
Ogni volta che symfony crea le tabelle nel database, tutti i dati saranno
eliminati. Per popolare un database con i dati iniziali, dovrebbe create uno
script PHP o eseguire delle query con il programma mysql. Ma dato che questo
è un bisogno abbastanza comune, in symfony c'è un modo migliore per farlo:
creare dei file YAML nella cartella data/fixtures/ ed usare il task
propel:data-load per caricarli nel database.
Prima, creiamo i seguenti file di fixture:
# 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: /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: 2010-10-10
job_extreme_sensio:
category_id: 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: 2010-10-10
Il file fixture contiene riferimenti a due immagini. Possono essere scaricate (
http://www.symfony-project.org/get/jobeet/sensio-labs.gif,http://www.symfony-project.org/get/jobeet/extreme-sensio.gif) ed inserite nella cartellaweb/uploads/jobs/.
Un file fixture è scritto in YAML e definisce gli oggetti del modello,
etichettati con un nome univoco (ad esempio, abbiamo definito due lavori
etichettati come job_sensio_labs e job_extreme_sensio). Questa
etichetta è molto utile per collegare gli oggetti correlati senza
dover definire delle chiavi primarie (che sono spesso degli
auto-increment e quindi non possono essere impostate). Per esempio,
la categoria job_sensio_labs è programming, che è l'etichetta
assegnata alla categoria 'Programming'.
In un file YAML, quando una stringa contiene degli "a capo" (come la colonna
descriptionnel file fixture dei lavori), si può usare la barra verticale (|) per indicare che la stringa si estende su più righe.
Sebbene un file fixture possa contenere oggetti provenienti da diversi modelli, abbiamo deciso di creare un solo file per ogni modello di Jobeet.
Notate i numeri che precedono i nomi dei file. Questo è un modo semplice per controllare l'ordine di caricamento dei dati. Più avanti nel progetto, se avremo bisogno di inserire nuovi file fixture, sarà facile perché avremo alcuni numeri liberi tra quelli esistenti.
In un file fixture, non si ha bisogno di definire tutti i valori delle colonne.
Se non lo si fa, symfony userà i valori di default definiti nello schema del
database. E siccome symfony usa Propel per caricare i dati nel database,
tutti i comportamenti predefiniti (come impostare automaticamente le
colonne created_at o updated_at), ed i comportamenti personalizzati che
potreste aver aggiunto alle classi del modello saranno attivati.
Caricare i dati iniziali nel database è facile come eseguire il task:
propel:data-load:
$ php symfony propel:data-load
Il task
propel:build-all-loadè una scorciatoria per il taskpropel:build-allseguito dal taskpropel:data-load.
Abbiamo usato molto la linea di comando ma questo non è molto eccitante, specialmente per un progetto web. Ora abbiamo tutto ciò che ci serve per creare pagine web che interagiscano col database.
Vediamo come mostrare una lista di lavori, come modificare un lavoro esistente,
e come cancellare un lavoro. Come spiegato nel giorno 1, un progetto
symfony è composto da applicazioni. Ciascuna applicazione è ulteriormente
suddivisa in moduli. Un modulo è un insieme autonomo di codice PHP
che rappresenta una feature dell'applicazione (ad esempio il modulo
delle API), o un insieme di manipolazioni che l'utente può fare su un
oggetto del modello (ad esempio il modulo job).
Symfony è in grado di generare automaticamente per un modello dato un modulo che fornisce delle feature basilari di manipolazione:
$ php symfony propel:generate-module --with-show --non-verbose-templates frontend job JobeetJob
Il comando propel:generate-module genera un modulo job nell'applicazione frontend
per il modello JobeetJob. Come per molti task di symfony, alcuni file e cartelle sono
stati creati per voi sotto la cartella apps/frontend/modules/job/:
| Cartella | Descrizione |
|---|---|
| actions/ | Le azioni del modulo |
| templates/ | I template del modulo |
Il file actions/actions.class.php definisce tutte le azioni
disponibili per il modulo job:
| Nome azione | Descrizione |
|---|---|
| index | Mostra le righe della tabella |
| show | Mostra i campi ed i loro valori per una data riga |
| new | Mostra una form per creare una nuova riga |
| create | Crea una nuova riga |
| edit | Mostra una form per modificare una riga esistente |
| update | Aggiorna una riga con i valori inseriti dall'utente |
| delete | Cancella una data riga dalla tabella |
Ora si può provare il modulo job in un browser:
http://jobeet.localhost/frontend_dev.php/job

Se si prova a modificare un lavoro, si avrà un'eccezione, perché symfony ha bisogno di una
rappresentazione testuale di una categoria. La rappresentazione testuale di un oggetto
PHP può essere definita col metodo magico ~__toString()~. La
rappresentazione testuale di una categoria andrebbe definita nella
classe del modello JobeetCategory:
// lib/model/JobeetCategory.php class JobeetCategory extends BaseJobeetCategory { public function __toString() { return $this->getName(); } }
Ora ogni volta che symfony ha bisogno della rappresentazione testuale di una categoria,
chiama il metodo __toString(), che restituisce il nome della categoria. Siccome abbiamo
bisogno di una rappresentazione testuale di tutte le classi del modello in un punto o
nell'altro, definiamo un metodo __toString() per ogni classe del modello:
// 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(); } }
Ora potete creare e modificare i lavori. Provate a lasciare vuoto un campo richiesto, o a immettere una data non valida. Tutto bene, symfony ha creato delle regole di validazione di base, partendo dallo schema del database.

Per oggi è tutto. Siete stati avvertiti nell'introduzione. Oggi abbiamo scritto poco
codice PHP, ma abbiamo un modulo web funzionante per il modello job, pronto per essere
aggiustato e personalizzato. Ricorda, niente codice PHP significa anche niente bug!
Se vi è rimasta un po' di energia, potete anche leggere il codice generato per il modulo e per il modello e provare a capire come funziona. Altrimenti, non preoccupatevi e dormite bene, perché domani parleremo di uno dei paradigmi più usati nei framework per il web, il design pattern MVC.
Come per gli altri giorni, il codice di oggi è disponibile sul
repository SVN di Jobeet. Basta fare il checkout del tag release_day_03:
$ svn co http://svn.jobeet.org/propel/tags/release_day_03/ jobeet/
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.