Development

Documentation/it_IT/book/1.1/08-Inside-the-Model-Layer

You must first sign up to be able to contribute.

Version 1 (modified by garak, 9 years ago)
--

Capitolo 8 - All'interno del layer Modello

Fino ad ora gran parte della discussione è stata dedicata alla costruzione delle pagine ed all'elaborazione delle richieste e delle risposte. Ma la business logic di un'applicazione web si basa per lo più sul modello dei dati. Il modello in symfony si basa su una mappatura oggetto/relazione conosciuta come progetto Propel (http://propel.phpdb.org). In un'applicazione symfony, accedi ai dati memorizzati in un database tramite oggetti; non ti riferisci mai al database direttamente. Questo permette un alto livello di astrazione e portabilità.

Questo capitolo spiega come creare un modello dei dati, ed il modo di accedere e modificare dati in Propel. Dimostra inoltre l'integrazione di Propel in symfony.

Perché usare un ORM ed un livello di astrazione?

I database sono relazionali. Symfony e PHP 5 sono orientati agli oggetti. Per poter accedere efficacemente al database in un contesto orientato agli oggetti, occorre disporre di un'interfaccia che traduca l'oggetto logico in relazionale. Come spiegato nel Capitolo 1, tale interfaccia è chiamata object-relational mapping (ORM), ed è composta di oggetti che forniscono accesso ai dati e contengono le business rules al proprio interno.

Il più grande vantaggio di ORM è la riusabilità, che permette ai metodi dei data object di essere chiamati da varie parti dell'applicazione, o anche da diverse applicazioni. Inoltre il layer ORM incapsula la data logic; ad esempio, il calcolo del rating di un utente di un forum basato sul numero dei suoi contributi dalla loro popolarità. Quando una pagina deve mostrare tale rating, chiama semplicemente un metodo del modello, senza preoccuparsi dei dettagli del calcolo. Se l'algoritmo di tale calcolo dovesse cambiare in futuro, dovresti solo modificarlo nel modello, lasciando inalterato il resto dell'applicazione.

Utilizzare oggetti invece di record e classi invece di tabelle ha un altro beneficio: ti permette di aggiungere componenti ai tuoi oggetti senza che questi siano necessariamente colonne in una tabella. Ad esempio, se tu avessi una tabella client con due campi chiamati first_name e last_name, potresti avere la capacità di richiedere semplicemente Name. In un mondo orientato agli oggetti, è semplice quanto aggiungere un metodo alla classe Client, come mostrato nel Listato 8-1. Dal punto di vista dell'applicazione, non c'è differenza tra gli attributi Name, FirstName o LastName della classe Client. Solo la classe stessa può determinare quale corrispondenza esiste tra colonna e attributo.

Listato 8-1 - Gli accessori nascondono l'effettiva struttura di una tabella

[php]
public function getName()
{
  return $this->getFirstName.' '.$this->getLastName();
}

Tutte le ripetitive funzioni di accesso ai dati e la business logic dei dati stessi possono venire memorizzate in oggetti simili. Supponi di avere un oggetto ShoppingCart nel quale puoi inserire Items (che sono oggetti). Per sapere il costo totale dell'acquisto, puoi scrivere un metodo per il calcolo, come mostrato nel Listato 8-2.

Listato 8-2 - Gli accessori nascondono la data logic

[php]
public function getTotal()
{
  $total = 0;
  foreach ($this->getItems() as $item)
  {
    $total += $item->getPrice() * $item->getQuantity();
  }

  return $total;
}

C'è un altro punto importante da considerare quando si costruiscono procedure di accesso ai dati: i produttori di database usano differenti varianti della sintassi SQL. Cambiare database management system (DBMS) significherebbe dover riscrivere parte delle query che erano state scritte per quello precedente. Se invece costruisci le tue query con una sintassi indipendente dal database, e lasci l'esecuzione effettiva della transazione SQL ad un altro componente, puoi cambiare database in modo indolore. Questa è la finalità del layer di astrazione del database. Esso ti forza ad usare una sintassi specifica per le query, e fa tutto il lavoro sporco di collegamento al db ed ottimizzazione del codice SQL.

Il vantaggio principale di tale layer di astrazione è quindi la portabilità, perché ti permette di cambiare database anche nel mezzo di un progetto. Immagina di dover scrivere rapidamente un prototipo di un'applicazione quando il cliente ancora non ha deciso quale sia il database più adatto allo scopo. Puoi cominciare a scrivere l'applicazione son SQLite, ad esempio, e cambiare in seguito con MySQL, PostgreSQL, od Oracle quando il cliente ha deciso cosa utilizzare. C'è semplicemente da cambiare una linea in un file di configurazione, e tutto continua a funzionare perfettamente.

Symfony usa Propel come ORM, e Propel usa Creole come astrazione del database. Queste due componenti di terze parti, entrambe sviluppate dal team di Propel, sono integrate trasparentemente in Symfony, e le puoi considerare come fossero parte del framework. Le loro sintassi e convenzioni, descritte in questo capitolo, sono state adattate per essere il meno dissimile possibile da quelle di Symfony.

NOTE In un progetto di symfony tutte le applicazioni condividono lo stesso modello. Questo è il punto del livello progetto: raggruppare applicazioni che si basano sulle stesse business rules. Questa è la ragione per cui il modello è indipendente dalle applicazioni ed i file del modello sono memorizzati nella cartella lib/model/ nella root del progetto.

Schema del database di Symfony

Per poter creare il modello dei dati che verrà usato da Symfony, occorre tradurre qualsiasi modello relazionale possieda il tuo db in un object data model. ORM necessita di una descrizione del modello relazionale per poter fare la mappatura, che viene chiamata schema. In tale schema, si descrivono le tabelle, le relazioni e le caratteristiche delle loro colonne.

La sintassi di symfony per gli schemi utilizza il formato YAML. Il file schema.yml deve essere posizionato nella cartella myproject/config/.

NOTE Symfony è in grado di utilizzare anche il formato XML, come spiegato più avanti in questo capitolo nella sezione "Dietro schema.yml: schema.xml".

Esempi di schema

Come traduci la struttura di un database in uno schema? Un esempio è il metodo migliore per capirlo. Immagina di avere il database di un blog con due tabelle: blog_article e blog_comment, con la struttura mostrato nella Figura 8-1.

Figura 8-1 - Struttura del database di un blog

Il relativi file schema.yml dovrebbe essere come quello del Listato 8-3.

Listato 8-3 - Esempio di schema.yml

[php]
propel:
  blog_article:
    _attributes: { phpName: Article }
    id:
    title:       varchar(255)
    content:     longvarchar
    created_at:
  blog_comment:
    _attributes: { phpName: Comment }
    id:
    article_id:
    author:      varchar(255)
    content:     longvarchar
    created_at:

Da notare che il nome del database stesso (blog) non appare nel file. Invece, il database è descritto tramite il nome di una connessione (propel in questo esempio). Questo perché le effettive impostazioni di connessione dipendono dall'ambiente nel quale sta girando. Ad esempio, quando la tua applicazione gira nell'ambiente di sviluppo si collegherà ad un database di sviluppo (come blog_dev), ma con lo stesso schema del database di produzione. Le impostazioni di connessione saranno specificate nel file databases.yml, descritte nella sezione "Connessioni al database" più avanti in questo capitolo. Lo schema non contiene alcun riferimento alla connessione, tranne il nome, per mantenere l'astrazione del database.

Sintassi di base dello schema

In un file schema.yml, la prima chiave rappresenta il nome della connessione. Può contenere diverse tabelle, ognuna avente un insieme di colonne. Secondo la sintassi YAML, la chiave finisce con i due punti, e la struttura viene mostrata tramite indentazione (uno o più spazi vuoti, non tabulazioni).

Una chiave può possedere attributi speciali, come phpName (nome della classe che verrà generata). Se non specifichi un phpName per una tabella, symfony ne creerà una basata sulla versione camelCase della tabella.

TIP La convenzione camelCase elimina gli underscore dalle parole e rende maiuscola la prima lettera delle parole interne. Le versioni camelCase di default di blog_article e blog_comment sono blogArticle e blogComment. Il nome di questa convenzione viene dall'alternarsi di lettere maiuscole all'interno di parole lunghe, che ricordano le gobbe di un cammello.

Una tabella contiene colonne. Il valore di una colonna può essere definito in tre modi diversi:

  • Se non specifichi niente, symfony cercherà di assegnare gli attributi secondo una serie di convenzioni che verranno spiegate più avanti in questo capitolo sotto la sezione "Colonne vuote". Ad esempio, la colonna id del Listato 8-3 non ha bisogno di essere definita. Symfony assegnerà integer auto-increment, chiave primaria della tabella. Nella tabella blog_comment, article_id verrà riconosciuta come chiave importata della tabella blog_article (le colonne che finiscono con _id sono considerate come chiavi importate, e la tabella relativa viene specificata dalla parte prima del nome della colonna). Colonne che si chiamano created_at (o updated_at) vengono impostate automaticamente come di tipo timestamp. Per tutte queste colonne, non hai bisogno di specificare il tipo. Questa è una delle ragioni per cui schema.yml è facile da scrivere.
  • Se specifichi un solo attributo, è il tipo di colonna. Symfony capisce i tipi standard: boolean, integer, float, date, varchar(size), longvarchar (convertito, ad esempio, in text in MySQL). Nota che i tipi date e timestamp soffrono delle solite limitazioni delle date di Unix, e non possono essere impostate a date precedenti al 01/01/1970. Se avessi bisogno di utilizzare date più vecchie, sono disponibili i tipi "before Unix" bu_date e bu_timestamp.
  • Se avessi bisogno di definire altri attributi per le colonne (come valore di default, required e così via), dovresti scriverli come coppie key: value. Tale sintassi estesa dello schema è spiegata più avanti in questo capitolo.

Le colonne possono anche avere l'attributo phpName, che è la versione in maiuscolo del nome (Id, Title, Content, ecc...) e nella maggior parte dei casi non ha bisogno di override.

Le tabelle possono anche avere chiavi importate ed indici espliciti, come anche definizioni di strutture specifiche del database. Troverai maggiori dettagli in proposito nella sezione "Sintassi estesa dello schema" più avanti in questo capitolo.

Classi del modello

Lo schema viene utilizzato per costruire il modello delle classi del layer ORM. Per risparmiare tempo di esecuzione, tali classi vengono generate a linea di comando con la chiamata propel:build-model.

> symfony propel:build-model

Lanciando questo comando verrà effettuata l'analisi dello schema e la generazione delle classi base del modello nella cartella lib/model/om/ del tuo progetto:

  • BaseArticle.php
  • BaseArticlePeer.php
  • BaseComment.php
  • BaseCommentPeer.php

Inoltre, le classi effettive del modello verranno create nella cartella lib/model/:

  • Article.php
  • ArticlePeer.php
  • Comment.php
  • CommentPeer.php

Hai definito solo due tabelle e ottieni otto file. Non c'è niente di sbagliato, ma merita una spiegazione.

Classi di base e personalizzate

Perché tenere due versioni dello stesso modello in due cartelle differenti?

Probabilmente avrai la necessità di aggiungere proprietà e metodi personalizzati al modello (ricorda il metodo getName() del Listato 8-1). Ma lo sviluppo del progetto porterà sicuramente anche all'aggiunta di tabelle e/o colonne. Ogni volta che cambi il file schema.yml, dovrai rigenerare le classi del modello tramite una chiamata propel:build-model. Se i tuoi metodi fossero scritti nelle classi effettivamente generate, andrebbero persi ogni volta.

Le classi Base nella cartella lib/model/om/ sono quelle generate direttamente dallo schema. Non dovresti mai modificarle, dato che ogni nuova ri-generazione le riscriverà da zero.

D'altra parte, le classi personalizzate dentro lib/model/ erediteranno direttamente dalle Base. Quando viene chiamato il comando propel:build-model su un modello esistente, queste classi non vengono toccate. Quindi esse sono il posto giusto dove aggiungere i tuoi metodi.

Il Listato 8-4 presenta un modello personalizzato di classe creato alla prima chiamata di propelbuild-model.

Listato 8-4 - Esempio di classe di modello, in lib/model/Article.php

[php]
<?php

class Article extends BaseArticle
{
}

Esso eredita tutti i metodi della classe BaseArticle, ma una modifica allo schema non lo toccherà.

Questo meccanismo di classi personalizzate che estendono le Base ti permette di cominciare a scrivere del codice anche senza conoscere il modello relazionale finale. La relativa struttura di file rende il modello sia personalizzabile che evolvibile.

Oggetti e classi Peer

Article e Comment sono classi di oggetti che rappresentano record nel database. Essi forniscono accesso alle colonne di un record ed ai record relativi. Ciò significa che tu potrai sapere il titolo di un articolo chiamando un metodo dell'oggetto Article, come mostrato nel Listato 8-5.

Listato 8-5 - Sono disponibili Getter per le colonne nella classe oggetto

[php]
$article = new Article();
// ...
$title = $article->getTitle();

ArticlePeer e CommentPeer sono classi peer; ovvero classi che contengono metodi statici per operare sulle tabelle. Esse forniscono un modo di recuperare dati dalle tabelle. I loro metodi di solito restituiscono un oggetto od una collezione di oggetti della classe relativa, come mostrato nel Listato 8-6.

Listato 8-6 - Metodi statici per recuperare record nella classe oggetto

[php]
// $articles è un array di oggetti della classe Article
$articles = ArticlePeer::retrieveByPks(array(123, 124, 125));

NOTE Dal punto di vista del modello, non ci potrebbe essere alcun oggetto peer. Per tale motivo i metodi delle classi peer vengono acceduti tramite :: (chiamata statica), invece del solito -> (istanza del metodo).

Così, tra le classi oggetto e quelle peer si ottengono quattro classi generate per tabella descritte nello schema. In effetti, esiste un quinto file nella cartella lib/model/map/, che contiene metadata sulle tabelle per l'ambiente di runtime. Ma probabilmente non dovrai mai cambiare questo file, per cui non è interessante.

Accedere ai dati

In symfony si accede ai tuoi dati tramite oggetti. Se sei abituato al modello relazionale ed all'utilizzo di query SQL per il recupero dei dati, il modello ad oggetti probabilmente ti potrà sembrare complicato. Ma, ancora più probabilmente, dopo aver capito la potenza del modello ad oggetti ti piacerà molto di più.

Prima di tutto vediamo di utilizzare lo stesso vocabolario. I modelli relazionale e ad oggetti sono concetti simili, ma entrambi possiedono una propria nomenclatura:

Relazionale | Object-Oriented -------------- | --------------- Tabella | Classe Riga, record | Oggetto Campo, colonna | Proprietà

Recuperare il valore di una colonna

Quando symfony costruisce il modello, crea una classe base per ogni tabella definita nello schema.yml. Ognuna di queste classi possiede i costruttori, accessori e mutatori di default, basati sulla definizione delle colonne: i metodi new, getXXX() e setXXX() aiutano a creare oggetti ed accedere alle loro proprietà, come mostrato nel Listato 8-7.

Listato 8-7 - Metodi delle classi generati

[php]
$article = new Article();
$article->setTitle('My first article');
$article->setContent('This is my very first article.\n Hope you enjoy it!');

$title   = $article->getTitle();
$content = $article->getContent();

NOTE La classe generata si chiama Article, che è il phpName dato alla tabella blog_article. Se il phpName non fosse stato definito nello schema, la classe si sarebbe chiamata BlogArticle. Gli accessori ed i mutatori utilizzano una variante camelCase del nome delle colonne, quindi il metodo getTitle() recupera il valore della colonna title.

Per impostare diversi campi in una volta, puoi usare il metodo fromArray(), anch'esso generato per ogni classe, come mostrato nel Listato 8-8.

Listato 8-8 - Il metodo fromArray() è un setter multiplo

[php]
$article->fromArray(array(
  'title'   => 'My first article',
  'content' => 'This is my very first article.\n Hope you enjoy it!',
));

Recuperare record relativi

La colonna article_id della tabella blog_comment definisce implicitamente una chiave importata dalla tabella blog_article. Un commento è relativo ad un articolo, ed ogni articolo può avere diversi commenti. Le classi generate contengono cinque metodi che traducono questo tipo di relazione in un modo orientato agli oggetti:

  • $comment->getArticle(): per recuperare l'oggetto Article relativo
  • $comment->getArticleId(): per recuperare l'ID dell'oggetto Article relativo
  • $comment->setArticle($article): per impostare l'oggetto Article relativo
  • $comment->setArticleId($id): per impostare l'oggetto Article relativo da un ID
  • $article->getComments(): per recuperare i relativi oggetti Comment

I metodi getArticleId() e setArticleId() mostrano che tu puoi considerare la colonna article_id come una colonna normale e impostare le relazioni manualmente, ma ciò non è molto interessante. I benefici del mondo object-oriented sono molto più evidenti negli altri tre metodi. Il Listato 8-9 mostra l'utilizzo dei setter generati.

Listato 8-9 - Chiavi importate trasformate in setter speciali

[php]
$comment = new Comment();
$comment->setAuthor('Steve');
$comment->setContent('Gee, dude, you rock: best article ever!);

// Collega questo commento al precedente oggetto $article
$comment->setArticle($article);

// Sintassi alternativa
// Ha senso solo se l'oggetto è già memorizzato nel db
$comment->setArticleId($article->getId());

Il Listato 8-10 mostra l'utilizzo dei getter generati; inoltre mostra anche come concatenare le chiamate ai metodi al modello.

Listato 8-10 - Chiavi importate trasformate in getter speciali

[php]
// Relazione molti-a-molti
echo $comment->getArticle()->getTitle();
 => My first article
echo $comment->getArticle()->getContent();
 => This is my very first article.
    Hope you enjoy it!

// Relazione uno-a-molti
$comments = $article->getComments();

Il metodo getArticle() restituisce un oggetto di tipo Article, che trae beneficio dall'accessorio getTitle(). Questo è molto meglio che fare il join manualmente, il che richiederebbe qualche linea di codice in più (cominciando dalla chiamata $comment->getArticleId()).

La variabile $comments nel Listato 8-10 contiene un array di oggetti della classe Comment. Puoi mostrare il primo con $comments[0] oppure iterare al suo interno tramite un ciclo foreach ($comments as $comment).

NOTE Gli oggetti del modello sono definiti da un nome singolare per convenzione, ed ora ne puoi capire il motivo. La chiave importata definita nella tabella blog_comment causa la creazione di un metodo getComments() che prende il proprio nome aggiungendo una s al nome dell'oggetto Comment. Se tu avessi dato al modello il nome al plurale, avrebbe generato un metodo chiamato getCommentss() che non ha senso.

Salvare ed eliminare dati

Chiamando il costruttore new, hai creato un nuovo oggetto, che però non è ancora salvato nella tabella blog_article. Anche modificare l'oggetto non ha alcuna implicazione sul database. Per poter memorizzare dati nel database, occorre chiamare il metodo save() dell'oggetto.

[php]
$article->save();

ORM è abbastanza intelligente da capire la relazione tra gli oggetti, per cui salvare l'oggetto $article significa che anche l'oggetto $comment viene salvato. Inoltre ORM sa anche se l'oggetto ha già un omologo nel database, per cui certe volte la chiamata save() porta ad una query di INSERT, mentre altre ad un UPDATE. La chiave primaria viene impostata automaticamente dal metodo save(), per cui dopo aver salvato l'oggetto ne puoi recuperare l'ID con $article->getId().

NOTE Puoi controllare se un oggetto è nuovo chiamando isNew(). Se inoltre desideri sapere se un oggetto sia stato modificato ed abbia bisogno di essere salvato, puoi chiamare isModified().

Leggendo i commenti ai tuoi articoli, potresti cambiare idea riguarda alla pubblicazione su Internet. E se non apprezzi l'ironia dei commentatori degli articoli, puoi facilmente cancellare un commento tramite il metodo delete(), come mostrato nel Listato 8-11.

Listato 8-11 - Cancellare record dal database tramite il metodo delete()

[php]
foreach ($article->getComments() as $comment)
{
  $comment->delete();
}

NOTE Anche dopo aver cancellato un oggetto con il metodo delete(), tale oggetto rimane disponibile fino alla fine della richiesta. Per sapere se un oggetto è stato eliminato dal database, chiama il metodo isDeleted().

Recuperare record tramite chiave primaria

Se conosci la chiave primaria di un record particolare, puoi usare il metodo retrieveByPk() della classe peer dell'oggetto relativo.

[php]
$article = ArticlePeer::retrieveByPk(7);

Il file schema.yml definisce il campo id come chiave primaria della tabella blog_article, per cui questo statement restituisce effettivamente il record con ID 7. Dato che hai usato la chiave primaria, sai che verrà restituito esattamente un record; la variabile $article contiene un oggetto della classe Article.

In qualche caso, una chiave primaria può essere composta da più di una colonna. In tali casi, il metodo retrieveByPK() accetta più parametri, uno per ogni colonna parte della chiave primaria.

Puoi anche selezionare più oggetti tramite le loro chiavi primarie, con il metodo retrieveByPKs(), che si aspetta come parametro un array di chiavi primarie.

Recuperare record tramite Criteria

Quando vuoi recuperare più di un record, devi utilizzare il metodo doSelect() della classe peer dell'oggetto. Ad esempio, per avere oggetti della classe Article, devi chiamare ArticlePeer::doSelect().

Il primo parametro del metodo doSelect() è un oggetto della classe Criteria, che è una semplice classe di definizione delle query costruita senza SQL nell'interesse dell'astrazione dal database.

Un Criteria vuoto restituisce tutti gli oggetti della classe. Ad esempio, il Listato 8-12 mostra come ottenere tutti gli articoli.

Listato 8-12 - Recuperare record con doSelect() -- Criteria vuoto

[php]
$c = new Criteria();
$articles = ArticlePeer::doSelect($c);

// Si trasformerà nella seguente query SQL
SELECT blog_article.ID, blog_article.TITLE, blog_article.CONTENT,
       blog_article.CREATED_AT
FROM   blog_article;

SIDEBAR Hydrating

La chiamata a ::doSelect() è molto più potente di una semplice query SQL. Tanto per cominciare, il codice SQL risultante è ottimizzato per il database in uso. Inoltre, i valori passati a Criteria vengono filtrati tramite escape per evitare rischi di SQL injection. Infine, il metodo restituisce un array di oggetti, invece di un result set. ORM si occupa di creare e popolare automaticamente oggetti basandosi sul result set. Questo processo si chiama Hydrating.

Per una selezione più complicata hai bisogno dell'equivalente di WHERE, ORDER BY, GROUP BY ed altri operatori SQL. L'oggetto Criteria possiede parametri e metodi per tali operatori. Ad esempio, per recuperare tutti i commenti scritti da Steve ordinati per data, costruisci un Criteria come nel Listato 8-13.

Listato 8-13 - Recuperare record con doSelect() -- Criteria con condizioni

[php]
$c = new Criteria();
$c->add(CommentPeer::AUTHOR, 'Steve');
$c->addAscendingOrderByColumn(CommentPeer::CREATED_AT);
$comments = CommentPeer::doSelect($c);

// Si trasformerà nella seguente query SQL
SELECT blog_comment.ARTICLE_ID, blog_comment.AUTHOR, blog_comment.CONTENT,
       blog_comment.CREATED_AT
FROM   blog_comment
WHERE  blog_comment.author = 'Steve'
ORDER BY blog_comment.CREATED_AT ASC;

Le costanti passate come parametri ai metodi add() si riferiscono ai nomi delle proprietà. Sono nominati con la versione maiuscola del nome delle colonne. Ad esempio, per indirizzare la colonna content della tabella blog_article, usa la costante ArticlePeer::CONTENT.

NOTE Perché utilizzare CommentPeer::AUTHOR invece di blog_comment.AUTHOR, che è in ogni caso il modo in cui verrà trasformata nella query in output? Supponi di dover cambiare il nome del campo dell'autore nel database in contributor. Se tu avessi usato blog_comment.AUTHOR, dovresti cambiare tutte le chiamate nel modello. D'altra parte, usando CommentPeer::AUTHOR, hai bisogno di cambiare solo il nome della colonna nel file schema.yml, mantenendo come phpName AUTHOR, e fare il rebuild del modello.

La Tabella 8-1 confronta la sintassi SQL con quella di Criteria.

Tabella 8-1 - Sintassi SQL e di Criteria

SQL | Criteria ------------------------------------------------------------ | ----------------------------------------------- WHERE column = value | ->add(column, value); WHERE column <> value | ->add(column, value, Criteria::NOT_EQUAL); Altri operatori di comparazione | > , < | Criteria::GREATER_THAN, Criteria::LESS_THAN >=, <= | Criteria::GREATER_EQUAL, Criteria::LESS_EQUAL IS NULL, IS NOT NULL | Criteria::ISNULL, Criteria::ISNOTNULL LIKE, ILIKE | Criteria::LIKE, Criteria::ILIKE IN, NOT IN | Criteria::IN, Criteria::NOT_IN Altre parole chiave SQL | ORDER BY column ASC | ->addAscendingOrderByColumn(column); ORDER BY column DESC | ->addDescendingOrderByColumn(column); LIMIT limit | ->setLimit(limit) OFFSET offset | ->setOffset(offset) FROM table1, table2 WHERE table1.col1 = table2.col2 | ->addJoin(col1, col2) FROM table1 LEFT JOIN table2 ON table1.col1 = table2.col2 | ->addJoin(col1, col2, Criteria::LEFT_JOIN) FROM table1 RIGHT JOIN table2 ON table1.col1 = table2.col2 | ->addJoin(col1, col2, Criteria::RIGHT_JOIN)

TIP Il modo migliore per scoprire e capire i metodi disponibili nelle classi generate è quello di dare un'occhiata ai file Base nella cartella lib/model/om/ dopo la generazione. I metodi sono abbastanza espliciti, ma se hai bisogno di più commenti imposta a true il parametro propel.builder.addComments nel file config/propel.ini e fai il rebuild del modello.

Il Listato 8-14 mostra un altro esempio dell'utilizzo di Criteria con condizioni multiple. Recupera tutti i commenti di Steve ad articoli che contengono la parola "enjoy", ordinati per data.

Listato 8-14 - Un altro esempio di recupero dati con doSelect() -- Criteria con condizioni multiple

[php]
$c = new Criteria();
$c->add(CommentPeer::AUTHOR, 'Steve');
$c->addJoin(CommentPeer::ARTICLE_ID, ArticlePeer::ID);
$c->add(ArticlePeer::CONTENT, '%enjoy%', Criteria::LIKE);
$c->addAscendingOrderByColumn(CommentPeer::CREATED_AT);
$comments = CommentPeer::doSelect($c);

// Si trasformerà nella seguente query SQL
SELECT blog_comment.ID, blog_comment.ARTICLE_ID, blog_comment.AUTHOR,
       blog_comment.CONTENT, blog_comment.CREATED_AT
FROM   blog_comment, blog_article
WHERE  blog_comment.AUTHOR = 'Steve'
       AND blog_article.CONTENT LIKE '%enjoy%'
       AND blog_comment.ARTICLE_ID = blog_article.ID
ORDER BY blog_comment.CREATED_AT ASC

Proprio come SQL è di un linguaggio semplice che ti permette di costruire query complesse, anche l'oggetto Criteria può gestire condizioni a qualsiasi livello di complessità. Ma dato che molti sviluppatori pensano in SQL per poi trasformare la condizione in una logica orientata agli oggetti, Criteria può risultare ostico all'inizio. Il modo migliore per capirlo è tramite esempi. Il sito di symfony, ad esempio, ne è pieno e ti aiuterà a fare luce in diversi modi.

In aggiunta al metodo doSelect(), ogni classe peer possiede un metodo doCount(), che restituisce (come intero) il numero di record che soddisfano i criteri passati come parametri. Dato che non c'è oggetto da restituire, in questo caso non viene effettuato il processo di Hydrating, così il metodo doCount() è più veloce di doSelect().

Le classi peer forniscono anche i metodi doDelete(), doInsert(), e doUpdate(), che si aspettano un Criteria come parametro. Tali metodi ti permettono di eseguire delle query di DELETE, INSERT, e UPDATE. Per maggiori informazioni su questi metodi Propel, controlla il codice delle classi peer del tuo modello.

Infine, se ti serve semplicemente il primo oggetto restituito, sostituisci doSelect() con doSelectOne(). Questo potrebbe essere ad esempio il caso in cui sai a priori che Criteria restituirà solo un record, ed il vantaggio di tale metodo è che restituisce un solo oggetto invece di un array di oggetti.

TIP Quando una query doSelect() restituisce un gran numero di risultati, potresti volerne visualizzare solo una parte. Symfony fornisce una classe per la paginazione chiamata sfPropelPager, che automatizza tale processo. Controlla la documentazione della paginazione su http://www.symfony-project.org/cookbook/1_1/pager per maggiori dettagli in merito.

Eseguire query SQL grezze

Potrebbe capitare di non voler recuperare oggetti, ma solo un risultato sintetico calcolato dal database. Ad esempio, per avere l'ultima data di creazione fra tutti gli articoli, non avrebbe senso recuperarli tutti ed iterare nell'array. Sarebbe meglio chiedere direttamente il dato al db, ed evitare il processo di Hydrating.

D'altra parte, non vuoi utilizzare direttamente comandi PHP per la gestione del database, così da perdere il livello di astrazione. Questo significa che devi scavalcare ORM (Propel) ma non l'astrazione del database (Creole).

Eseguire query con Creole richiede: 1. Avere una connessione al db. 2. Costruire una stringa contenente la query. 3. Costruire uno statement dai punti precedenti. 4. Iterare nel result set restituito dall'esecuzione dello statement.

Se questo ti pare troppo complicato, probabilmente il Listato 8-15 ti aiuterà a capire.

Listato 8-15 - Query SQL con Creole

[php]
$connection = Propel::getConnection();
$query = 'SELECT MAX(%s) AS max FROM %s';
$query = sprintf($query, ArticlePeer::CREATED_AT, ArticlePeer::TABLE_NAME);
$statement = $connnection->prepareStatement($query);
$resultset = $statement->executeQuery();
$resultset->next();
$max = $resultset->getInt('max');

Proprio come selezioni Propel, le query Creole sono un po' ostiche quando cominci ad usarle; ancora una volta, l'aiuto migliore arriverà dagli esempi esistenti.

CAUTION Se sei tentato di bypassare questo processo ed accedere al database direttamente, rischi di perdere la sicurezza e l'astrazione forniti da Creole. Utilizzare la sintassi di Creole richiede più tempo, ma ti da la garanzia di efficienza, portabilità e sicurezza della tua applicazione. Questo è particolarmente vero per query con parametri provenienti da sorgenti sconosciute (come un utente Internet). Creole si occupa della sicurezza del tuo database, e non utilizzarlo significa essere a rischio di attacchi di SQL injection.

Utilizzare le colonne speciali per le date

Di solito, quando una tabella ha una colonna chiamata created_at, è utilizzata per memorizzare un timestamp della data in cui il record è stato creato. Lo stesso vale per colonne chiamate updated_at, che vengono aggiornate all'ora corrente al momento in cui il record viene aggiornato.

La buona notizia è che symfony riconoscerà automaticamente i nomi di tali colonne e gestirà gli aggiornamenti al posto tuo. Non hai bisogno di impostare i valori dei campi created_at e updated_at; come mostrato nel Listato 8-16, ciò avverrà automaticamente. Lo stesso succede per le colonne che si chiamano created_on e updated_on.

Listato 8-16 - Le colonne created_at e updated_at vengono aggiornate automaticamente

[php]
$comment = new Comment();
$comment->setAuthor('Steve');
$comment->save();

// Mostra la data di creazione
echo $comment->getCreatedAt();
  => [date of the database INSERT operation]

In aggiunta, i getter per le date accettano un parametro per il formato:

[php]
echo $comment->getCreatedAt('Y-m-d');

SIDEBAR Refactoring al livello dati

Quando svilupperai un progetto con symfony, spesso comincerai con lo scrivere la domain logic nelle azioni. Ma le query al database e la manipolazione del modello non dovrebbero essere memorizzate nel layer del controller. Tutta la logica relativa ai dati dovrebbe essere spostata nel layer del modello. Ogni qualvolta avrai bisogno della stessa richiesta in più di un punto nelle tue azioni, pensa all'idea di trasferire il codice relativo nel modello. Fare ciò aiuta a mantenere le azioni corte e facili da leggere.

Ad esempio, immagina il codice necessario in un blog per recuperare i dieci articoli più popolari per un dato tag (passato come parametro). Tale codice non dovrebbe essere in un'azione, ma nel modello. Se avessi bisogno di mostrare tale lista in un template, l'azione dovrebbe essere semplicemente:

[php]
public function executeShowPopularArticlesForTag($request)
{
  $tag = TagPeer::retrieveByName($request->getParameter('tag'));
  $this->foward404Unless($tag);
  $this->articles = $tag->getPopularArticles(10);
}

L'azione crea un oggetto della classe Tag dal parametro di richiesta. Quindi tutto il codice necessario alla query è situato nel metodo getPopularArticles() di tale classe. Così l'azione è più leggibile, ed il codice nel modello è facilmente riutilizzabile in un'altra azione.

Spostare il codice in un luogo più appropriato fa parte della tecnica di refactoring. Se lo fai spesso, il tuo codice sarà più facile da manutenere e da comprendere per gli altri sviluppatori. Una buona regola per capire quando c'è bisogno di spostare del codice, è quando una azione contiene più di dieci linee di codice PHP.

Connessioni al database

Il modello dei dati è indipendente dal database utilizzato, ma sicuramente ne userai uno. Le informazioni di cui symfony necessita per eseguire richieste al db sono il nome del database stesso, il tipo e utente e password per il collegamento. Tali impostazioni possono essere configurate passando un data source name (DSN) al task configure:database

php symfony --app=frontend configure:database "mysql://login:passwd@localhost/blog"

Per ogni ambiente, puoi definire più di una connessione. Ogni connessione si riferisce ad uno schema etichettato con lo stesso nome. Il nome predefinito per la connessione predefinita è propel e si referisce allo schema propel del Listato 8-3. L'opzione name consente di creare un'altra connessione:

php symfony --name=main configure:database "mysql://login:passwd@localhost/blog"

Puoi anche inserire queste impostazioni di connnessione manualmente nel file databases.yml che si trova nella cartella config/ Il Listato 8-17 mostra un esempio di tale file ed il Listato 8-18 mostra lo stesso esempio con la notazione estesa.

Listato 8-17 - Impostazioni di connessione in forma abbreviata

[php]
all:
  propel:
    class:          sfPropelDatabase
    param:
      dsn:          mysql://login:passwd@localhost/blog

Listato 8-18 - Esempio di impostazioni di connessione al database, in myproject/config/databases.yml

[php]
prod:
  propel:
    param:
      host:               mydataserver
      username:           myusername
      password:           xxxxxxxxxx

all:
  propel:
    class:                sfPropelDatabase
    param:
      phptype:            mysql     # Tipo di database
      hostspec:           localhost
      database:           blog
      username:           login
      password:           passwd
      port:               80
      encoding:           utf-8     # Charset di default per la creazione delle tabelle
      persistent:         true      # Usa connessioni persistenti

Le impostazioni di connessione dipendono dall'ambiente. Puoi definire impostazioni differenti per gli ambienti prod, dev, test o qualsiasi altro ambiente della tua applicazione. Un'applicazione può anche sovrascrivere tali impostazioni, ad esempio impostandone valori diversi nel file apps/myapp/config/databases.yml. Ad esempio, puoi utilizzare questo approccio per avere diverse politiche di sicurezza per il frontend ed il backend della tua applicazione, e definire diversi utenti (con diversi privilegi) per questa gestione.

Puoi definire varie connessioni per ogni ambiente. Ogni connessione si riferisce ad uno schema con lo stesso nome. Nell'esempio del Listato 8-17, la connessione propel si riferisce allo schema propel del Listato 8-3.

I valori di phptype permessi sono quelli dei tipi di database supportati da Creole:

  • mysql
  • sqlserver
  • pgsql
  • sqlite
  • oracle

hostspec, database, username, e password sono le solite impostazioni di connessione. Puoi anche scriverle con una sintassi più breve (data source name, o DSN). Il Listato 8-18 è equivalente alla sezione all: del Listato 8-17.

Se usi un database SQLite, il parametro hostspec deve essere impostato sul percorso del file che contiene il database. Ad esempio, se il database del tuo blog è nel file data/blog.db, il file databases.yml deve essere impostato come nel Listato 8-19.

Listato 8-19 - Impostazioni di connessione per SQLite con il database in un file

[php]
all:
      propel:
        class:          sfPropelDatabase
        param:
          phptype:  sqlite
          database: %SF_DATA_DIR%/blog.db

Estendere il modello

I metodi del modello generati sono utili ma spesso non sufficienti. Dato che implementerai la tua business logic, avrai bisogno di estendere il modello, creando i tuoi metodi o facendo l'override di quelli esistenti.

Aggiungere nuovi metodi

Puoi aggiungere nuovi metodi alle classi del modello vuote generate nella cartella lib/model/. Utilizza $this per chiamare metodi dell'oggetto corrente, e self:: per chiamare metodi statici della classe corrente. Ricorda che le classi personalizzate ereditano dalle classi Base situate nella cartella lib/model/om/.

Ad esempio, per l'oggetto Article del Listato 8-3, puoi aggiungere un metodo magico __toString() cosicché stampare un oggetto di tale classe ne visualizzerà il titolo, come mostrato nel Listato 8-20.

Listato 8-20 - Personalizzare il modello, in lib/model/Article.php

[php]
<?php

class Article extends BaseArticle
{
  public function __toString()
  {
    return $this->getTitle();  // getTitle() è ereditato da BaseArticle
  }
}

Puoi anche estendere le classi peer, ad esempio per avere un metodo che recupera tutti gli articoli ordinati per data di creazione, come mostrato nel Listato 8-21.

Listato 8-21 - Personalizzare il modello, in lib/model/ArticlePeer.php

[php]
<?php

class ArticlePeer extends BaseArticlePeer
{
  public static function getAllOrderedByDate()
  {
    $c = new Criteria();
    $c->addAscendingOrderByColumn(self:CREATED_AT);
    return self::doSelect($c);

  }
}

I nuovi metodi saranno disponibili come quelli generati, come mostrato nel Listato 8-22.

listato 8-22 - Usare nuovi metodi è come usare quelli generati

[php]
foreach (ArticlePeer::getAllOrderedByDate() as $article)
{
  echo $article;      // Chiama il metodo magico __toString()
}

Fare l'override di metodi esistenti

Se qualcuno dei metodi generati nelle classi Base non soddisfano i tuoi bisogni, ne puoi fare l'override nelle tue classi. Devi solo essere sicuro di usare gli stessi argomenti.

Ad esempio, $article->getComments() restituisce un array di oggetti Comment, senza alcun ordine particolare. Se vuoi avere il risultato ordinato per data di creazione, con l'ultimo commento inserito che appare come primo, devi fare l'override del metodo getComments(), come mostrato nel Listato 8-23. Fai attenzione al fatto che il metodo originale getComments() (situato nel file lib/model/om/BaseArticle.php) si aspetta come parametri un criterio ed una connessione, per cui anche il tuo metodo deve fare lo stesso.

Listato 8-23 - Override di metodi esistenti, in lib/model/Article.php

[php]
public function getComments($criteria = null, $con = null )
{
  // Gli oggetti in PHP5 sono passati per riferimento, quindi per evitare di modificare l'originale lo devi clonare
  $criteria = clone $criteria;
  $criteria->addDescendingOrderByColumn(ArticlePeer::CREATED_AT);

  return parent::getComments($criteria, $con);
}

Il metodo personalizzato alla fine chiama il genitore della classe Base, e questa è una buona pratica, anche se in generale puoi scavalcarla completamente e restituire il risultato che preferisci.

Utilizzare behaviors del modello

Alcune modifiche del modello sono generiche e possono essere riutilizzate. Ad esempio, i metodi per rendere un oggetto ordinabile od un lock ottimistico per prevenire conflitti fra salvataggi concorrenti sono estensioni generiche che possono venire aggiunte a diverse classi.

Symfony pacchettizza tali estensioni in behaviors. Essi sono classi esterne che forniscono metodi addizionali alle classi del modello. Esse contengono già degli hook, e symfony sa come estenderli tramite sfMixer (per dettagli vedi il Capitolo 17).

Per abilitare i behaviors nelle classi del tuo modello, devi modificare un'impostazione nel file config/propel.ini:

[php]
propel.builder.AddBehaviors = true     // Il valore di default è false

Non ci sono behavior pacchettizzati di default in symfony, ma sono installabili tramite plugin. Una volta installato, lo puoi assegnare ad una classe tramite una riga di codice. Ad esempio, se installi nella tua applicazione sfPropelParanoidBehaviorPlugin, puoi estendere una classe Article con questo behavior aggiungendo alla fine del file Article.class.php:

[php]
sfPropelBehavior::add('Article', array(
  'paranoid' => array('column' => 'deleted_at')
));

Dopo aver fatto il rebuilding del modello, gli oggetti Article eliminati rimarranno nel database, invisibili alle query eseguite tramite ORM, a meno di disabilitare temporaneamente il behavior tramite sfPropelParanoidBehavior::disable().

'''Nuovo in symfony 1.1''': In alternativa, puoi anche dichiarare i comportamenti direttamente in schema.yml, elencandoli sotto la chiave _behaviors (vedi Listato 8-34 sotto).

Controlla nel wiki la lista dei plugin di symfony per vedere quali sono i behavior esistenti (http://www.symfony-project.com/trac/wiki/SymfonyPlugins#Propelbehaviorplugins). Ognuno ha la propria guida di installazione ed utilizzo.

Estendere la sintassi dello schema

Un file schema.yml può essere semplice, come mostrato nel Listato 8-3. Ma i modelli relazionali spesso sono complicati. Questo è il motivo per cui lo schema possiede una sintassi estensiva che permetti di gestire quasi tutti i casi.

Attributi

Connessioni e tabelle possono avere attributi specifici, come mostrato nel Listato 8-24. Essi vengono impostati sotto una chiave _attributes.

Listato 8-24 - Attributi per connessioni e tabelle

[php]
propel:
  _attributes:   { noXsd: false, defaultIdMethod: none, package: lib.model }
  blog_article:
    _attributes: { phpName: Article }

Potresti volere che lo schema venga validato prima della generazione del codice. Per fare ciò, disabilita l'attributo noXSD per la connessione. Essa supporta anche l'attributo defaultIdMethod. Se alcun metodo per la generazione di ID viene fornito, verrà utilizzato quello di default del database; ad esempio, autoincrement per MySQL o sequences per PostgreSQL. L'altro valore possibile è none.

L'attributo package è come un namespace; determina il percorso nel quale vengono generate le classi. Il default è lib/model/, ma puoi cambiarlo per organizzare il tuo modello in sottopacchetti. Ad esempio, se non vuoi tenere le classi che contengono il core business con quelle delle statistiche nella stessa cartella, definisci due schemi con i pacchetti lib.model.business e lib.model.stats.

Hai già visto l'attributo della tabella phpName, usato per mapparla con le classi generate.

Anche le tabelle che contengono contenuti localizzati (ovvero diverse versioni degli stessi contenuti, per l'internazionalizzazione) posseggono due attributi ulteriori (vedi il Capitolo 13 per i dettagli), come mostrato nel Listato 8-25.

Listato 8-25 - Attributi per le tabelle i18N

[php]
propel:
  blog_article:
    _attributes: { isI18N: true, i18nTable: db_group_i18n }

SIDEBAR

Gestire schemi multipli

Puoi avere più di uno schema per applicazione. Symfony prende in considerazione tutti i file della cartella config/ il cui nome finisce con schema.yml oppure schema.xml. Se la tua applicazione ha molte tabelle, o se alcune di esse non condividono la stessa connessione, troverai questo approccio molto utile. Considera i due schemi seguenti:

 // In config/business-schema.yml
 propel:
   blog_article:
     _attributes: { phpName: Article }
   id:
   title: varchar(50)

 // In config/stats-schema.yml
 propel:
   stats_hit:
     _attributes: { phpName: Hit }
   id:
   resource: varchar(100)
   created_at:

Entrambi gli schemi usano la stessa connessione (propel), e le classi Article e Hit saranno generate nella stessa cartella lib/model/. Tutto sarà come se avessi scritto un unico schema. Puoi anche avere diversi schemi che usano diverse connessioni (ad esempio, propel e propel_bis, da definire in databases.yml) ed organizzare le classi generate in sottocartelle:

 // In config/business-schema.yml
 propel:
   blog_article:
     _attributes: { phpName: Article, package: lib.model.business }
   id:
   title: varchar(50)

 // In config/stats-schema.yml
 propel_bis:
   stats_hit:
     _attributes: { phpName: Hit, package.lib.model.stat }
   id:
   resource: varchar(100)
   created_at:

Molte applicazioni usano più di uno schema. In particolare, qualche plugin possiede il proprio schema e sistema di pacchetti per evitare di creare confusione con le tue classi (vedi il Capitlo 17 per dettagli).

Dettagli delle colonne

La sintassi di base ti da due scelte: lasciare che symfony capisca dal nome delle colonne le loro caratteristiche (lasciando un valore vuoto) o definirne il tipo, come mostrato nel Listato 8-26.

Listato 8-26 - Attributi di base delle colonne

[php]
propel:
  blog_article:
    id:                 # Lascia fare il lavoro a symfony
    title: varchar(50)  # Specifica tu stesso il tipo

Ma tu puoi definire molto di più per una colonna. Se lo fai, dovrai utilizzare la sintassi degli array associativi, come mostrato nel Listato 8-27.

Listato 8-27 - Attributi complessi per le colonne

[php]
propel:
  blog_article:
    id:       { type: integer, required: true,primaryKey: true, autoIncrement: true }
    name:     { type: varchar(50), default: foobar, index: true }
    group_id: { type: integer, foreignTable: db_group,foreignReference: id, onDelete: cascade }

I parametri delle colonne sono i seguenti:

  • type: tipo di colonna. La scelta è fra boolean, tinyint, smallint, integer, bigint, double, float, real, decimal, char, varchar(size), longvarchar, date, time, timestamp, bu_date, bu_timestamp, blob, e clob.
  • required: booleano. Se vuoi che la colonna sia obbligatoria impostalo su true.
  • default: valore di default.
  • primaryKey: booleano. Impostalo su true se vuoi che la colonna sia parte della chiave primaria.
  • autoIncrement: booleano. Impostalo su true per colonne di tipo integer che hanno bisogno di valori auto-increment.
  • sequence: per database che utilizzano sequenze per colonne autoIncrement (come PostgreSQL e Oracle).
  • index: booleano. Impostalo su true se vuoi un indice semplice o a unique se vuoi un indice unico sulla colonna.
  • foreignTable: nome di tabella usato per creare chiavi importate su un'altra tabella.
  • foreignReference: nome della colonna relativa se è stata definita una chiave importata con foreignTable.
  • onDelete: determina l'azione da eseguire quando viene eliminato un record dalla tabella. Se impostato su setnull, il record della chiave importata è impostata a null. Se impostato su cascade, il record viene eliminato. Se il db non supporta tali comportamenti, ORM li emula. Questo attributo è rilevante solo per colonne che hanno impostato una foreignTable e una foreignReference.
  • isCulture: booleano. Impostalo su true per colonne in tabelle di localizzazione (v. Capitolo 13).

Chiavi importate

Come alternativa agli attributi foreignTable e foreignReference, puoi aggiungere chiavi importate in una tabella sotto la chiave _foreignKeys:. Lo schema del Listato 8-28 creerà una chiave importata sulla colonna user_id, che si corrisponde alla colonna id della tabella blog_user.

Listato 8-28 - Sintassi alternativa per chiavi importate

[php]
propel:
  blog_article:
    id:
    title:   varchar(50)
    user_id: { type: integer }
    _foreignKeys:
      -
        foreignTable: blog_user
        onDelete:     cascade
        references:
          - { local: user_id, foreign: id }

La sintassi alternativa è utile per riferimenti multipli di chiavi importate, e per dargli un nome, come mostrato nel Listato 8-29.

Listato 8-29 - Sintassi alternativa per chiavi importate applicata a riferimenti multipli

[php]
 _foreignKeys:
      my_foreign_key:
        foreignTable:  db_user
        onDelete:      cascade
        references:
          - { local: user_id, foreign: id }
          - { local: post_id, foreign: id }

Indici

Come alternativa all'attributo index, puoi aggiungere indici ad una tabella sotto la chiave _indexes:. Per definire indici unici, devi usare invece l'header _uniques:. Il Listato 8-30 mostra la sintassi alternativa per gli indici.

Listato 8-30 - Sintassi alternative per gli indici

[php]
propel:
  blog_article:
    id:
    title:            varchar(50)
    created_at:
    _indexes:
      my_index:       [title, user_id]
    _uniques:
      my_other_index: [created_at]

La sintassi alternativa è utile per indici su più colonne.

Colonne vuote

Quando incontra una colonna senza valore, symfony aggiunge magicamente un valore. Il Listato 8-31 ne mostra un esempio.

Listato 8-31 - Dettagli di colonne dedotti dal loro nome

[php]
// Colonne vuote chiamate id sono considerate chiavi primarie
id:         { type: integer, required: true, primaryKey: true, autoIncrement: true }

// Colonne vuote chiamate XXX_id sono considerate chiavi importate
foobar_id:  { type: integer, foreignTable: db_foobar, foreignReference: id }

// Colonne vuote chiamate created_at, updated at, created_on e updated_on
// sono considerate date e automaticamente vengono impostate di tipo timestamp
created_at: { type: timestamp }
updated_at: { type: timestamp }

Per le chiavi importate, symfony cercherà una tabella che abbia il phpName uguale alla prima parte del nome della chiave, e se la trova, ne imposterà il nome come foreignTable.

Tabelle i18N

Symfony supporta l'internazionalizzazione del contenuto nelle tabelle relative. Ciò significa che quando hai del contenuto soggetto a internazionalizzazione, esso viene memorizzato in due tabelle separate: una con colonne invariate, e un'altra con colonne internazionalizzate.

Nel file schema.yml, tutto ciò è implicito quando chiami una tabella foobar_i18n. Ad esempio, lo schema mostrato nel Listato 8-32 sarà completato automaticamente con colonne e attributi in modo da far funzionare il meccanismo di internazionalizzazione. Internamente, symfony lo capirà come se fosse stato scritto come nel Listato 8-33. Il Capitolo 13 fornirà maggiori informazioni riguardo l'internazionalizzazione.

Listato 8-32 - Meccanismo i18N implicito

[php]
propel:
  db_group:
    id:
    created_at:

  db_group_i18n:
    name:        varchar(50)

Listato 8-33 - Meccanismo i18N esplicito

[php]
propel:
  db_group:
    _attributes: { isI18N: true, i18nTable: db_group_i18n }
    id:
    created_at:

  db_group_i18n:
    id:       { type: integer, required: true, primaryKey: true,foreignTable: db_group, foreignReference: id, onDelete: cascade }
    culture:  { isCulture: true, type: varchar(7), required: true,primaryKey: true }
    name:     varchar(50)

Comportamenti (nuovo in symfony 1.1)

I comportamenti sono modificatori del modello forniti da plugin che aggiungono nuove capacità alle tue classi Propel. Il Capitolo 17 spiega di più sui comportamenti. Puoi definire i comportamenti nell schema, elencandoli per ogni tabella, insieme ai loro parametri, sotto la chiave _behaviors. Il Listato 8-34 dà un esempio di estensione della classe BlogArticle con il comportamento paranoid behavior.

Listato 8-34 - Dichiarazione dei Comportamenti

{{{ propel: blog_article: title: varchar(50) _behaviors: paranoid: { column: deleted_at } }}}

Oltre schema.yml: schema.xml

Di fatto, il formato di schema.yml è interno a symfony. Quando chiami un comando propel:, symfony in effetti traduce questo file in generated-schema.xml, che è il tipo di file che Propel si aspetta per poter eseguire operazioni sul modello.

Il file schema.xml contiene le stesse informazioni del suo equivalente YAML. Ad esempio, il Listato 8-3 viene convertito nel file xml mostrato nel Listato 8-35.

Listato 8-35 - Esempio di schema.xml, corrispondente al Listato 8-3

[xml]
<?xml version="1.0" encoding="UTF-8"?>
 <database name="propel" defaultIdMethod="native" noXsd="true" package="lib.model">
    <table name="blog_article" phpName="Article">
      <column name="id" type="integer" required="true" primaryKey="true"autoIncrement="true" />
      <column name="title" type="varchar" size="255" />
      <column name="content" type="longvarchar" />
      <column name="created_at" type="timestamp" />
    </table>
    <table name="blog_comment" phpName="Comment">
      <column name="id" type="integer" required="true" primaryKey="true"autoIncrement="true" />
      <column name="article_id" type="integer" />
      <foreign-key foreignTable="blog_article">
        <reference local="article_id" foreign="id"/>
      </foreign-key>
      <column name="author" type="varchar" size="255" />
      <column name="content" type="longvarchar" />
      <column name="created_at" type="timestamp" />
    </table>
 </database>

La descrizione del formato di schema.xml può essere trovata nella documentazione e nella sezione "Getting Started" del sito di Propel (http://propel.phpdb.org/docs/user_guide/chapters/appendices/AppendixB-SchemaReference.html).

Il formato YAML è stato pensato per mantenere gli schemi facili da leggere e scrivere, ma il compromesso è il fatto che un schema molto complesso non può essere descritto in un file schema.yml. D'altra parte, il formato XML permette una descrizione completa, per ogni complessità, ed include impostazioni specifiche dei commercianti di database, ereditarietà delle tabelle e così via.

In effetti symfony capisce schemi scritti in XML. Così, se il tuo schema è troppo complesso per la sintassi YAML, se hai già uno schema XML esistente o se hai già familiarità con la sintassi XML di Propel, non devi per forza scriver in YAML. Metti il tuo schema.xml nella cartella config/, fai il rebuild del modello e via.

SIDEBAR Propel in symfony

Tutti i dettagli forniti in questo capitolo non sono tanto relativi a symfony, quanto piuttosto a Propel. Esso è il livello di astrazione oggetto/relazione preferito in symfony, ma ne puoi scegliere uno alternativo. Comunque, symfony lavora molto trasparentemente con Propel, per le seguenti ragioni: Tutte le classi del modello e quelle Criteria sono autoloading. Appena le usi, symfony si occuperà di tutto e tu non avrai bisogni di inclusioni. In symfony, Propel non ha bisogno di essere lanciato o inizializzato. Quando un oggetto usa Propel, la libreria inizializza se stessa. Alcuni plugin utilizzano oggetti Propel come parametri per raggiungere un alto livello di esecuzione (come la paginazione o i filtri). Gli oggetti Propel forniscono una rapida generazione di backend per la tua applicazione (v. Capitolo 14 per dettagli). Lo schema è più veloce da scrivere utilizzando la sintassi YAML. Infine, Propel è indipendente dal database utilizzato, come symfony.

Non creare il modello due volte

Il prezzo da pagare solitamente per l'utilizzo di un ORM è il fatto che devi definire la struttura dei dati due volte: una per il database e una per il modello. Fortunatamente, symfony mette a disposizione comandi per generarne uno sulla base dell'altro, in modo da evitare di fare lo stesso lavoro due volte.

Costruire la struttura del database in SQL basandosi su uno schema esistente

Se cominci la tua applicazione scrivendo il file schema.yml, symfony può generare una query che crea le tabelle direttamente dal modello YAML. Per fare ciò, scrivi il seguente comando dalla shell (ricorda di posizionarti prima nella cartella del tuo progetto):

> symfony propel:build-sql

Verrà creato un file chiamato lib.model.schema.sql dentro la cartella myproject/data/sql/. Nota che il codice così generato sarà ottimizzato per il tipo di database specificato dal parametro phptype del file propel.ini.

Puoi usare il file schema.sql per costruire direttamente le tabelle. Ad esempio, in MySQL puoi digitare:

> mysqladmin -u root -p create blog
> mysql -u root -p blog < data/sql/lib.model.schema.sql

Il codice SQL generato può essere anche utile per ricostruire il db in un altro ambiente, o per cambiare DBMS. Se le impostazioni di connessione sono definite correttamente nel file propel.ini, puoi anche usare il comando symfony propel:insert-sql per farlo automaticamente.

CAUTION La linea di comando mette a disposizione anche delle opzioni per popolare il db tramite file di testo. Nel Capitolo 16 sono fornite maggiori informazioni riguardo il task propel:data-load ed i file accessori di YAML.

Generare un modello YAML da un database esistente

Symfony può utilizzare il layer di accesso al database di Creole per generare un file schema.yml da un db esistente, grazie all'introspezione (la capacità dei database di capire la struttura delle proprie tabelle). Questo diventa particolarmente utile quando devi fare reverse-engineering, o se preferisci lavorare sul db prima che sul modello.

Per fare ciò, devi chiamare il comando propel:build-schema, dopo aver controllato di avere impostato correttamente tutti i settaggi relativi alla connessione nel file propel.ini:

> symfony propel:build-schema

Un file schema.yml nuovo di zecca generato dalla struttura del database sarà situato nella cartella config/. Puoi costruire il tuo modello partendo da questo schema.

I comandi di generazione dello schema sono molto potenti e ti permettono di aggiungere molto informazioni relative al database. Ma dato che YAML non gestisce tali informazioni, in questi casi devi utilizzare il formato XML. Puoi farlo semplicemente aggiungendo un parametro xml al comando build-schema:

> symfony propel:build-schema xml

Così invece di generare uno schema.yml, creerai un file xml pienamente compatibile con Propel, contenente tutte le informazioni sul vendor. Ma fai attenzione al fatto che i file xml tendono ad essere prolissi e difficili da leggere.

SIDEBAR La configurazione di propel.ini

I comandi propel:build-sql e propel:build-schema non usano le impostazioni di connessione specificate in databases.yml. Essi utilizzano invece le impostazioni memorizzate nel file propel.ini situato nella cartella config/:

 propel.database.createUrl = mysql://login:passwd@localhost
 propel.database.url       = mysql://login:passwd@localhost/blog

Tale file contiene le impostazioni necessarie al generatore di Propel per ottenere classi compatibili con symfony. La maggior parte di tali settaggi sono interni e non molto interessanti per l'utente, a parte alcuni:

 // Le classi Base sono autocaricate in symfony
 // Imposta questo a true per usare invece degli statement include_once
 // (Piccolo impatto negativo sulle performance)
 propel.builder.addIncludes = false

 // Le classi generate non sono commentate per default
 // Imposta questo a true per aggiungere commenti alle classi Base
 // (Piccolo impatto negativo sulle performance)
 propel.builder.addComments = false

 // Behaviors non sono gestiti per default
 // Imposta questo a true per gestirli
 propel.builder.AddBehaviors = false

Dopo che hai modificato le impostazioni di propel.ini non dimenticare di fare il rebuild del modello affinché le modifiche abbiano effetto.

Sommario

Symfony usa Propel come ORM e Creole come layer di astrazione del database. Significa che prima di generare le classi del modello devi descrivere lo schema relazionale in YAML. Quindi, a runtime, utilizza i metodi degli oggetti e delle classi peer per recuperare informazioni su un record od un insieme di record. Puoi farne l'override ed estendere il modello facilmente con classi personalizzate. Le impostazioni di connessione sono definite nel file databases.yml, che può supportarne anche più di una. E la linea di comando contiene comandi speciali per evitare di definire due volte la stessa struttura.

Il livello del modello è il più complesso del framework. Una delle ragioni di tale complessità è dovuta alla natura intrinsecamente intricata dell'argomento. Le questioni relative alla sicurezza sono cruciali per un'applicazione web e non devono essere ignorate. Un'altra ragione è il fatto che symfony si adatta meglio ad applicazioni che scalano su dimensioni medio-grandi a livello enterprise. In tali applicazioni, le automazioni messe a disposizione da symfony rappresentano un vero guadagno di tempo, per cui vale la pena investirne per impararle.

Per cui non esitare ad impiegare un po' di tempo nel testare gli oggetti ed i metodi del modello per capirli pienamente. La solidità e la scalabilità saranno una grande ricompensa.