Symfony2 documentation
Documentation
Release 2
SensioLabs
March 12, 2012
CONTENTS
1
Giro rapido
1.1 Giro rapido . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1
1
2
Libro
2.1 Libro . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
23
23
3
Ricettario
251
3.1 Ricettario . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 251
4
Componenti
435
4.1 I componenti . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 435
5
Documenti di riferimento
471
5.1 Documenti di riferimento . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 471
6
Bundle
591
6.1 Bundle di Symfony SE . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 591
7
Contributi
633
7.1 Contribuire . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 633
Index
651
i
ii
CHAPTER
ONE
GIRO RAPIDO
Iniziare subito con il giro rapido di Symfony2:
1.1 Giro rapido
1.1.1 Un quadro generale
Volete provare Symfony2 avendo a disposizione solo dieci minuti? Questa prima parte di questa guida è stata
scritta appositamente: spiega come partire subito con Symfony2, mostrando la struttura di un semplice progetto
già pronto.
Chi ha già usato un framework per il web si troverà come a casa con Symfony2.
Tip: Si vuole imparare perché e quando si ha bisogno di un framework? Si legga il documento “Symfony in 5
minuti”.
Scaricare Symfony2
Prima di tutto, verificare di avere almeno PHP 5.3.2 (o successivo) installato e configurato correttamente per
funzionare con un server web, come Apache.
Pronti? Iniziamo scaricando “Symfony2 Standard Edition”, una distribuzione di Symfony preconfigurata per gli
usi più comuni e che contiene anche del codice che dimostra come usare Symfony2 (con l’archivio che include i
venditori, si parte ancora più velocemente).
Scaricare l’archivio e scompattarlo nella cartella radice del web. Si dovrebbe ora avere una cartella Symfony/,
come la seguente:
www/ <- cartella radice del web
Symfony/ <- archivio scompattato
app/
cache/
config/
logs/
Resources/
bin/
src/
Acme/
DemoBundle/
Controller/
Resources/
...
vendor/
symfony/
1
Symfony2 documentation Documentation, Release 2
doctrine/
...
web/
app.php
...
Note: Se è stata scaricata la Standard Edition senza venditori, basta eseguire il comando seguente per scaricare
tutte le librerie dei venditori:
php bin/vendors install
Verifica della configurazione
Per evitare mal di testa successivamente, Symfony2 dispone di uno strumento per testare la configurazione, per
verificare configurazioni errate di PHP o del server web. Usare il seguente URL per avviare la diagnosi sulla
propria macchina:
http://localhost/Symfony/web/config.php
Se ci sono dei problemi, correggerli. Si potrebbe anche voler modificare la propria configurazione, seguendo le
raccomandazioni fornite. Quando è tutto a posto, cliccare su “Bypass configuration and go to the Welcome page”
per richiedere la prima “vera” pagina di Symfony2:
http://localhost/Symfony/web/app_dev.php/
Symfony2 dovrebbe congratularsi per il duro lavoro svolto finora!
Capire i fondamenti
Uno degli obiettivi principali di un framework è quello di assicurare la Separazione degli ambiti. Ciò mantiene
il proprio codice organizzato e consente alla propria applicazione di evolvere facilmente nel tempo, evitando il
miscuglio di chiamate al database, tag HTML e logica di business nello stesso script. Per raggiungere questo
obiettivo con Symfony, occorre prima imparare alcuni termini e concetti fondamentali.
2
Chapter 1. Giro rapido
Symfony2 documentation Documentation, Release 2
Tip: Chi volesse la prova che usare un framework sia meglio che mescolare tutto nello stesso script, legga il
capitolo “Symfony2 contro PHP puro” del libro.
La distribuzione offre alcuni esempi di codice, che possono essere usati per capire meglio i concetti fondamentali
di Symfony. Si vada al seguente URL per essere salutati da Symfony2 (sostituire Fabien col proprio nome):
http://localhost/Symfony/web/app_dev.php/demo/hello/Fabien
Cosa sta accadendo? Dissezioniamo l’URL:
• app_dev.php: È un front controller. È l’unico punto di ingresso dell’applicazione e risponde a ogni
richiesta dell’utente;
• /demo/hello/Fabien: È il percorso virtuale alla risorsa a cui l’utente vuole accedere .
È responsabilità dello sviluppatore scrivere il codice che mappa la richiesta
(/demo/hello/Fabien) alla risorsa a essa associata (la pagina HTML Hello Fabien!).
dell’utente
Rotte
Symfony2 dirige la richiesta al codice che la gestisce, cercando la corrispondenza tra l’URL richiesto e alcuni schemi configurati. Per impostazione predefinita, questi schemi (chiamate “rotte”) sono definite nel file
di configurazione app/config/routing.yml. Se si è nell’ambiente dev, indicato dal front controller
app_**dev**.php, viene caricato il file di configurazione app/config/routing_dev.yml. Nella Standard
Edition, le rotte delle pagine di demo sono in quel file:
# app/config/routing_dev.yml
_welcome:
pattern: /
defaults: { _controller: AcmeDemoBundle:Welcome:index }
_demo:
resource: "@AcmeDemoBundle/Controller/DemoController.php"
type:
annotation
prefix:
/demo
# ...
1.1. Giro rapido
3
Symfony2 documentation Documentation, Release 2
Le prime righe (dopo il commento) definiscono quale codice richiamare quanto l’utente richiede
la risorsa “/” (come la pagina di benvenuto vista prima).
Quando richiesto, il controllore
AcmeDemoBundle:Welcome:index sarà eseguito. Nella prossima sezione, si imparerà esattamente quello
che significa.
Tip: La Standard Edition usa YAML per i suoi file di configurazione, ma Symfony2 supporta nativamente
anche XML, PHP e le annotazioni. I diversi formati sono compatibili e possono essere usati alternativamente in
un’applicazione. Inoltre, le prestazioni dell’applicazione non dipendono dal formato scelto, perché tutto viene
messo in cache alla prima richiesta.
Controllori
Il controllore è una funzione o un metodo PHP che gestisce le richieste in entrata e restituisce delle risposte (spesso
codice HTML). Invece di usare variabili e funzioni globali di PHP (come $_GET o header()) per gestire
questi messaggi HTTP, Symfony usa degli oggetti: Symfony\Component\HttpFoundation\Request
e Symfony\Component\HttpFoundation\Response. Il controllore più semplice possibile potrebbe
creare la risposta a mano, basandosi sulla richiesta:
use Symfony\Component\HttpFoundation\Response;
$name = $request->query->get(’name’);
return new Response(’Hello ’.$name, 200, array(’Content-Type’ => ’text/plain’));
Note: Symfony2 abbraccia le specifiche HTTP, che sono delle regole che governano tutte le comunicazioni sul
web. Si legga il capitolo “Symfony2 e fondamenti di HTTP” del libro per sapere di più sull’argomento e sulle sue
potenzialità.
Symfony2 sceglie il controllore basandosi sul valore _controller della configurazione delle rotte:
AcmeDemoBundle:Welcome:index. Questa stringa è il nome logico del controllore e fa riferimento al
metodo indexAction della classe Acme\DemoBundle\Controller\WelcomeController:
// src/Acme/DemoBundle/Controller/WelcomeController.php
namespace Acme\DemoBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class WelcomeController extends Controller
{
public function indexAction()
{
return $this->render(’AcmeDemoBundle:Welcome:index.html.twig’);
}
}
Tip:
Si
sarebbero
potuti
usare
i
nomi
completi
di
classe
e
metodi,
Acme\DemoBundle\Controller\WelcomeController::indexAction,
per il valore di
_controller. Ma se si seguono alcune semplici convenzioni, il nome logico è più breve e consente
maggiore flessibilità.
La classe WelcomeController estende la classe predefinita Controller, che fornisce alcuni utili metodi
scorciatoia, come il metodo :method:‘Symfony\\Bundle\\FrameworkBundle\\Controller\\Controller::render‘,
che carica e rende un template (AcmeDemoBundle:Welcome:index.html.twig). Il valore restituito è
un oggetto risposta, popolato con il contenuto resto. Quindi, se ci sono nuove necessità, l’oggetto risposta può
essere manipolato prima di essere inviato al browser:
4
Chapter 1. Giro rapido
Symfony2 documentation Documentation, Release 2
public function indexAction()
{
$response = $this->render(’AcmeDemoBundle:Welcome:index.txt.twig’);
$response->headers->set(’Content-Type’, ’text/plain’);
return $response;
}
Indipendentemente da come lo si raggiunge, lo scopo finale del proprio controllore è sempre quello di restituire l’oggetto Response da inviare all’utente. Questo oggetto Response può essere popolato con codice
HTML, rappresentare un rinvio del client o anche restituire il contenuto di un’immagine JPG, con un header
Content-Type del valore image/jpg.
Tip: Estendere la classe base Controller è facoltativo. Di fatto, un controllore può essere una semplice
funzione PHP, o anche una funzione anonima PHP. Il capitolo “Il controllore” del libro dice tutto sui controllori
di Symfony2.
Il nome del template, AcmeDemoBundle:Welcome:index.html.twig, è il nome logico del template e fa
riferimento al file Resources/views/Welcome/index.html.twig dentro AcmeDemoBundle (localizzato in src/Acme/DemoBundle). La sezione successiva sui bundle ne spiega l’utilità.
Diamo ora un altro sguardo al file di configurazione delle rotte e cerchiamo la voce _demo:
# app/config/routing_dev.yml
_demo:
resource: "@AcmeDemoBundle/Controller/DemoController.php"
type:
annotation
prefix:
/demo
Symfony2 può leggere e importare informazioni sulle rotte da diversi file,
scritti in
YAML, XML, PHP o anche inseriti in annotazioni PHP. Qui, il nome logico del file
è
@AcmeDemoBundle/Controller/DemoController.php
e
si
riferisce
al
file
src/Acme/DemoBundle/Controller/DemoController.php. In questo file, le rotte sono definite come annotazioni sui metodi delle azioni:
// src/Acme/DemoBundle/Controller/DemoController.php
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
class DemoController extends Controller
{
/**
* @Route("/hello/{name}", name="_demo_hello")
* @Template()
*/
public function helloAction($name)
{
return array(’name’ => $name);
}
// ...
}
L’annotazione @Route() definisce una nuova rotta con uno schema /hello/{name}, che esegue il metodo
helloAction quando trovato. Una stringa racchiusa tra parentesi graffe, come {name}, è chiamata segnaposto. Come si può vedere, il suo valore può essere recuperato tramite il parametro $name del metodo.
Note: Anche se le annotazioni sono sono supportate nativamente da PHP, possono essere usate in Symfony2
come mezzo conveniente per configurare i comportamenti del framework e mantenere la configurazione accanto
al codice.
1.1. Giro rapido
5
Symfony2 documentation Documentation, Release 2
Dando un’occhiata più attenta al codice del controllore, si può vedere che invece di rendere un template e restituire un oggetto Response come prima, esso restituisce solo un array di parametri.
L’annotazione @Template() dice a Symfony di rendere il template al posto nostro, passando ogni variabili dell’array al template. Il nome del template resto segue il nome del controllore. Quindi, nel
nostro esempio, viene reso il template AcmeDemoBundle:Demo:hello.html.twig (localizzato in
src/Acme/DemoBundle/Resources/views/Demo/hello.html.twig).
Tip: Le annotazioni @Route() e @Template() sono più potenti dei semplici esempi mostrati in questa guida.
Si può approfondire l’argomento “annotazioni nei controllori” nella documentazione ufficiale.
Template
Il controllore rende il template src/Acme/DemoBundle/Resources/views/Demo/hello.html.twig
(oppure AcmeDemoBundle:Demo:hello.html.twig, se si usa il nome logico):
{# src/Acme/DemoBundle/Resources/views/Demo/hello.html.twig #}
{% extends "AcmeDemoBundle::layout.html.twig" %}
{% block title "Hello " ~ name %}
{% block content %}
<h1>Hello {{ name }}!</h1>
{% endblock %}
Per impostazione predefinita, Symfony2 usa Twig come sistema di template, ma si possono anche usare i tradizionali template PHP, se si preferisce. Il prossimo capitolo introdurrà il modo in cui funzionano i template in in
Symfony2.
Bundle
Forse vi siete chiesti perché il termine bundle viene usato così tante volte finora. Tutto il codice che si scrive per
la propria applicazione è organizzato in bundle. Nel linguaggio di Symfony2, un bundle è un insieme strutturato
di file (file PHP, fogli di stile, JavaScript, immagini, ...) che implementano una singola caratteristica (un blog,
un forum, ...) e che può essere condivisa facilmente con altri sviluppatori. Finora abbiamo manipolato un solo
bundle, AcmeDemoBundle. Impareremo di più sui bundle nell’ultimo capitolo di questa guida.
Lavorare con gli ambienti
Ora che si possiede una migliore comprensione di come funziona Symfony2, è ora di dare un’occhiata più da
vicino al fondo della pagina: si noterà una piccola barra con il logo di Symfony2. Questa barra è chiamata “barra
di debug del web” ed è il miglior amico dello sviluppatore.
6
Chapter 1. Giro rapido
Symfony2 documentation Documentation, Release 2
Ma quello che si vede all’inizio è solo la punta dell’iceberg: cliccando sullo strano numero esadecimale, rivelerà
un altro strumento di debug veramente utile di Symfony2: il profilatore.
Ovviamente, questo strumento non deve essere mostrato quando si rilascia l’applicazione su un server di produzione. Per questo motivo, si troverà un altro front controller (app.php) nella cartella web/, ottimizzato per
l’ambiente di produzione:
http://localhost/Symfony/web/app.php/demo/hello/Fabien
Se si usa Apache con mod_rewrite abilitato, si può anche omettere la parte app.php dell’URL:
http://localhost/Symfony/web/demo/hello/Fabien
Infine, ma non meno importante, sui server di produzione si dovrebbe far puntare la cartella radice del web alla
cartella web/,per rendere l’installazione sicura e avere URL più allettanti:
http://localhost/demo/hello/Fabien
Note: Si noti che i tre URL qui forniti sono solo esempi di come un URL potrebbe apparire in produzione usando
un front controller (con o senza mod_rewrite). Se li si prova effettivamente in un’installazione base della Standard
1.1. Giro rapido
7
Symfony2 documentation Documentation, Release 2
Edition di Symfony, si otterrà un errore 404, perché AcmeDemoBundle è abilitato solo in ambiente dev e le sue
rotte importate in app/config/routing_dev.yml.
Per rendere l’ambiente di produzione più veloce possibile, Symfony2 mantiene una cache sotto la cartella
app/cache/. Quando si fanno delle modifiche al codice o alla configurazione, occorre rimuovere a mano i
file in cache. Per questo si dovrebbe sempre usare il front controller di sviluppo (app_dev.php) mentre si
lavora al progetto.
Diversi ambienti di una stessa applicazione differiscono solo nella loro configurazione. In effetti, una configurazione può ereditare da un’altra:
# app/config/config_dev.yml
imports:
- { resource: config.yml }
web_profiler:
toolbar: true
intercept_redirects: false
L’ambiente dev (che carica il file di configurazione config_dev.yml) importa il file globale config.yml
e lo modifica, in questo caso, abilitando la barra di debug del web.
Considerazioni finali
Congratulazioni! Avete avuto il vostro primo assaggio di codice di Symfony2. Non era così difficile, vero? C’è
ancora molto da esplorare, ma dovreste già vedere come Symfony2 rende veramente facile implementare siti web
in modo migliore e più veloce. Se siete ansiosi di saperne di più, andate alla prossima sezione: “la vista”.
1.1.2 La vista
Dopo aver letto la prima parte di questa guida, avete deciso che Symfony2 vale altri dieci minuti. Bene! In questa
seconda parte, si imparerà di più sul sistema dei template di Symfony2, Twig. Twig è un sistema di template
veloce, flessibile e sicuro per PHP. Rende i propri template più leggibili e concisi e anche più amichevoli per i
designer.
Note: Invece di Twig, si può anche usare PHP per i proprio template. Entrambi i sistemi di template sono
supportati da Symfony2.
Familiarizzare con Twig
Tip: Se si vuole imparare Twig, suggeriamo caldamente di leggere la sua documentazione ufficiale. Questa
sezione è solo un rapido sguardo ai concetti principali.
Un template Twig è un file di test che può generare ogni tipo di contenuto (HTML, XML, CSV, LaTeX, ...). Twig
definisce due tipi di delimitatori:
• {{ ...
}}: Stampa una variabile o il risultato di un’espressione;
• {% ... %}: Controlla la logica del template; è usato per eseguire dei cicli for e delle istruzioni if,
per esempio.
Segue un template minimale, che illustra alcune caratteristiche di base, usando due variabili, page_title e
navigation, che dovrebbero essere passate al template:
8
Chapter 1. Giro rapido
Symfony2 documentation Documentation, Release 2
<!DOCTYPE html>
<html>
<head>
<title>La mia pagina web</title>
</head>
<body>
<h1>{{ page_title }}</h1>
<ul id="navigation">
{% for item in navigation %}
<li><a href="{{ item.href }}">{{ item.caption }}</a></li>
{% endfor %}
</ul>
</body>
</html>
Tip: Si possono inserire commenti nei template, usando i delimitatori {# ...
#}.
Per rendere un template in Symfony, usare il metodo render dal controllore e passargli qualsiasi variabile necessaria al template:
$this->render(’AcmeDemoBundle:Demo:hello.html.twig’, array(
’name’ => $name,
));
Le variabili passate a un template possono essere stringhe, array o anche oggetti. Twig astrae le differenze tra essi
e consente di accedere agli “attributi” di una variabie con la notazione del punto (.):
{# array(’name’ => ’Fabien’) #}
{{ name }}
{# array(’user’ => array(’name’ => ’Fabien’)) #}
{{ user.name }}
{# forza la ricerca nell’array #}
{{ user[’name’] }}
{# array(’user’ => new User(’Fabien’)) #}
{{ user.name }}
{{ user.getName }}
{# forza la ricerca del nome del metodo #}
{{ user.name() }}
{{ user.getName() }}
{# passa parametri a un metodo #}
{{ user.date(’Y-m-d’) }}
Note: È importante sapere che le parentesi graffe non sono parte della variabile, ma istruzioni di stampa. Se si
accede alle variabili dentro ai tag, non inserire le parentesi graffe.
Decorare i template
Molto spesso, i template in un progetto condividono alcuni elementi comuni, come i ben noti header e footer.
In Symfony2, il problema è affrontato in modo diverso: un template può essere decorato da un altro template.
Funziona esattamente come nelle classi di PHP: l’ereditarietà dei template consente di costruire un template di
base “layout”, che contiene tutti gli elementi comuni del proprio sito e definisce dei “blocchi”, che i template figli
possono sovrascrivere.
1.1. Giro rapido
9
Symfony2 documentation Documentation, Release 2
Il template hello.html.twig eredita da layout.html.twig, grazie al tag extends:
{# src/Acme/DemoBundle/Resources/views/Demo/hello.html.twig #}
{% extends "AcmeDemoBundle::layout.html.twig" %}
{% block title "Hello " ~ name %}
{% block content %}
<h1>Hello {{ name }}!</h1>
{% endblock %}
La notazione AcmeDemoBundle::layout.html.twig suona familiare, non è vero? È la stessa notazione
usata per riferirsi a un template. La parte :: vuol dire semplicemente che l’elemento controllore è vuoto, quindi
il file corrispondente si trova direttamente sotto la cartella Resources/views/.
Diamo ora un’occhiata a una versione semplificata di layout.html.twig:
{# src/Acme/DemoBundle/Resources/views/layout.html.twig #}
<div class="symfony-content">
{% block content %}
{% endblock %}
</div>
I tag {% block %} definiscono blocchi che i template figli possono riempire. Tutto ciò che fa un tag di blocco
è dire al sistema di template che un template figlio può sovrascrivere quelle porzioni di template.
In questo esempio, il template hello.html.twig sovrascrive il blocco content, quindi il testo “Hello Fabien” viene reso all’interno dell’elemento div.symfony-content.
Usare tag, filtri e funzioni
Una delle migliori caratteristiche di Twig è la sua estensibilità tramite tag, filtri e funzioni. Symfony2 ha dei
bundle con molti di questi, per facilitare il lavoro dei designer.
Includere altri template
Il modo migliore per condividere una parte di codice di un template è quello di definire un template che possa
essere incluso in altri template.
Creare un template embedded.html.twig:
{# src/Acme/DemoBundle/Resources/views/Demo/embedded.html.twig #}
Hello {{ name }}
E cambiare il template index.html.twig per includerlo:
{# src/Acme/DemoBundle/Resources/views/Demo/hello.html.twig #}
{% extends "AcmeDemoBundle::layout.html.twig" %}
{# override the body block from embedded.html.twig #}
{% block content %}
{% include "AcmeDemoBundle:Demo:embedded.html.twig" %}
{% endblock %}
Inserire altri controllori
Cosa fare se si vuole inserire il risultato di un altro controllore in un template? Può essere molto utile quando si
lavora con Ajax o quando il template incluso necessita di alcune variabili, non disponibili nel template principale.
Se si crea un’azione fancy e la si vuole includere nel template index, basta usare il tag render:
10
Chapter 1. Giro rapido
Symfony2 documentation Documentation, Release 2
{# src/Acme/DemoBundle/Resources/views/Demo/index.html.twig #}
{% render "AcmeDemoBundle:Demo:fancy" with { ’name’: name, ’color’: ’verde’ } %}
Qui la stringa AcmeDemoBundle:Demo:fancy si riferisce all’azione fancy del controllore Demo. I
parametri (name e color) si comportano come variabili di richiesta simulate (come se fancyAction stesse
gestendo una richiesta del tutto nuova) e sono rese disponibili al controllore:
// src/Acme/DemoBundle/Controller/DemoController.php
class DemoController extends Controller
{
public function fancyAction($name, $color)
{
// creare un oggetto, in base alla variabile $color
$object = ...;
return $this->render(’AcmeDemoBundle:Demo:fancy.html.twig’, array(’name’ => $name, ’object
}
// ...
}
Creare collegamenti tra le pagine
Parlando di applicazioni web, i collegamenti tra pagine sono una parte essenziale. Invece di inserire a mano gli
URL nei template, la funzione path sa come generare URL in base alla configurazione delle rotte. In questo
modo, tutti gli URL saranno facilmente aggiornati al cambiare della configurazione:
<a href="{{ path(’_demo_hello’, { ’name’: ’Thomas’ }) }}">Ciao Thomas!</a>
La funzione path() accetta come parametri un nome di rotta e un array di parametri. Il nome della rotta è la
chiave principale sotto cui le rotte sono elencate e i parametri sono i valori dei segnaposto definiti nello schema
della rotta:
// src/Acme/DemoBundle/Controller/DemoController.php
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
/**
* @Route("/hello/{name}", name="_demo_hello")
* @Template()
*/
public function helloAction($name)
{
return array(’name’ => $name);
}
Tip: La funzione url genera URL assoluti: {{ url(’_demo_hello’, { ’name’:
}}.
’Thomas’ })
Includere risorse: immagini, JavaScript e fogli di stile
Cosa sarebbe Internet senza immagini, JavaScript e fogli di stile? Symfony2 fornisce la funzione asset per
gestirli facilmente.
<link href="{{ asset(’css/blog.css’) }}" rel="stylesheet" type="text/css" />
<img src="{{ asset(’images/logo.png’) }}" />
1.1. Giro rapido
11
Symfony2 documentation Documentation, Release 2
Lo scopo principale della funzione asset è quello di rendere le applicazioni maggiormente portabili. Grazie a
questa funzione, si può spostare la cartella radice dell’applicazione ovunque, sotto la propria cartella radice del
web, senza cambiare nulla nel codice dei template.
Escape delle variabili
Twig è configurato in modo predefinito per l’escape automatico di ogni output. Si legga la documentazione di
Twig per sapere di più sull’escape dell’output e sull’estensione Escaper.
Considerazioni finali
Twig è semplice ma potente. Grazie a layout, blocchi, template e inclusioni di azioni, è molto facile organizzare i
propri template in un modo logico ed estensibile. Tuttavia, chi non si trova a proprio agio con Twig può sempre
usare i template PHP in Symfony, senza problemi.
Stiamo lavorando con Symfony2 da soli venti minuti e già siamo in grado di fare cose incredibili. Questo è il potere
di Symfony2. Imparare le basi è facile e si imparerà presto che questa facilità è nascosta sotto un’architettura molto
flessibile.
Ma non corriamo troppo. Prima occorre imparare di più sul controllore e questo è esattamente l’argomento della
prossima parte di questa guida. Pronti per altri dieci minuti di Symfony2?
1.1.3 Il controllore
Ancora qui, dopo le prime due parti? State diventano dei Symfony2-dipendenti! Senza ulteriori indugi, scopriamo
cosa sono in grado di fare i controllori.
Usare i formati
Oggigiorno, un’applicazione web dovrebbe essere in grado di servire più che semplici pagine HTML. Da XML
per i feed RSS o per web service, a JSON per le richieste Ajax, ci sono molti formati diversi tra cui scegliere.
Il supporto di tali formati in Symfony2 è semplice. Modificare il file routing.yml e aggiungere un formato
_format, con valore xml:
// src/Acme/DemoBundle/Controller/DemoController.php
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
/**
* @Route("/hello/{name}", defaults={"_format"="xml"}, name="_demo_hello")
* @Template()
*/
public function helloAction($name)
{
return array(’name’ => $name);
}
Usando il formato di richiesta (come definito nel valore _format), Symfony2 sceglie automaticamente il template giusto, in questo caso hello.xml.twig:
<!-- src/Acme/DemoBundle/Resources/views/Demo/hello.xml.twig -->
<hello>
<name>{{ name }}</name>
</hello>
È tutto. Per i formati standard, Symfony2 sceglierà anche l’header Content-Type migliore per la risposta.
Se si vogliono supportare diversi formati per una singola azione, usare invece il segnaposto {_format} nello
schema della rotta:
12
Chapter 1. Giro rapido
Symfony2 documentation Documentation, Release 2
// src/Acme/DemoBundle/Controller/DemoController.php
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
/**
* @Route("/hello/{name}.{_format}", defaults={"_format"="html"}, requirements={"_format"="html|xm
* @Template()
*/
public function helloAction($name)
{
return array(’name’ => $name);
}
Ora
il
controller
sarà
richiamato
/demo/hello/Fabien.json.
per
URL
come
/demo/hello/Fabien.xml
o
La voce requirements definisce delle espressioni regolari che i segnaposto devono soddisfare. In questo
esempio, se si prova a richiedere la risorsa /demo/hello/Fabien.js, si otterrà un errore 404, poiché essa
non corrisponde al requisito di _format.
Rinvii e rimandi
Se si vuole rinviare l’utente a un’altra pagina, usare il metodo redirect():
return $this->redirect($this->generateUrl(’_demo_hello’, array(’name’ => ’Lucas’)));
Il metodo generateUrl() è lo stesso della funzione path() che abbiamo usato nei template. Accetta come
parametri il nome della rotta e un array di parametri e restituisce l’URL amichevole associato.
Si può anche facilmente rimandare l’azione a un’altra, col metodo forward(). Internamente, Symfony effettua
una “sotto-richiesta” e restituisce un oggetto Response da tale sotto-richiesta:
$response = $this->forward(’AcmeDemoBundle:Hello:fancy’, array(’name’ => $name, ’color’ => ’green’
// fare qualcosa con la risposta o restituirla direttamente
Ottenere informazioni dalla richiesta
Oltre ai valori dei segnaposto delle rotte, il controllore ha anche accesso all’oggetto Request:
$request = $this->getRequest();
$request->isXmlHttpRequest(); // è una richiesta Ajax?
$request->getPreferredLanguage(array(’en’, ’fr’));
$request->query->get(’page’); // prende un parametro $_GET
$request->request->get(’page’); // prende un parametro $_POST
In un template, si può anche avere accesso all’oggetto Request tramite la variabile app.request:
{{ app.request.query.get(’page’) }}
{{ app.request.parameter(’page’) }}
Persistere i dati nella sessione
Anche se il protocollo HTTP non ha stato, Symfony2 fornisce un bell’oggetto sessione, che rappresenta il client
(sia esso una persona che usa un browser, un bot o un servizio web). Tra due richieste, Symfony2 memorizza gli
attributi in un cookie, usando le sessioni native di PHP.
1.1. Giro rapido
13
Symfony2 documentation Documentation, Release 2
Si possono memorizzare e recuperare informazioni dalla sessione in modo facile, da un qualsiasi controllore:
$session = $this->getRequest()->getSession();
// memorizza un attributo per riusarlo più avanti durante una richiesta utente
$session->set(’foo’, ’bar’);
// in un altro controllore per un’altra richiesta
$foo = $session->get(’foo’);
// imposta la localizzazione dell’utente
$session->setLocale(’fr’);
Si possono anche memorizzare piccoli messaggi che saranno disponibili solo per la richiesta successiva:
// memorizza un messaggio per la richiesta successiva (in un controllore)
$session->setFlash(’notice’, ’Congratulazioni, azione eseguita con successo!’);
// mostra il messaggio nella richiesta successiva (in un template)
{{ app.session.flash(’notice’) }}
Ciò risulta utile quando occorre impostare un messaggio di successo, prima di rinviare l’utente a un’altra pagina
(la quale mostrerà il messaggio).
Proteggere le risorse
La Standard Edition di Symfony possiede una semplice configurazione di sicurezza, che soddisfa i bisogni più
comuni:
# app/config/security.yml
security:
encoders:
Symfony\Component\Security\Core\User\User: plaintext
role_hierarchy:
ROLE_ADMIN:
ROLE_USER
ROLE_SUPER_ADMIN: [ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]
providers:
in_memory:
memory:
users:
user: { password: userpass, roles: [ ’ROLE_USER’ ] }
admin: { password: adminpass, roles: [ ’ROLE_ADMIN’ ] }
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
login:
pattern: ^/demo/secured/login$
security: false
secured_area:
pattern:
^/demo/secured/
form_login:
check_path: /demo/secured/login_check
login_path: /demo/secured/login
logout:
path:
/demo/secured/logout
target: /demo/
14
Chapter 1. Giro rapido
Symfony2 documentation Documentation, Release 2
Questa configurazione richiede agli utenti di effettuare login per ogni URL che inizi per /demo/secured/ e
definisce due utenti validi: user e admin. Inoltre, l’utente admin ha il ruolo ROLE_ADMIN, che include il
ruolo ROLE_USER (si veda l’impostazione role_hierarchy).
Tip: Per leggibilità, le password sono memorizzate in chiaro in questa semplice configurazione, ma si può usare
un qualsiasi algoritmo di hash, modificando la sezione encoders.
Andando all’URL http://localhost/Symfony/web/app_dev.php/demo/secured/hello, si
verrà automaticamente rinviati al form di login, perché questa risorsa è protetta da un firewall.
Si può anche forzare l’azione a richiedere un dato ruolo, usando l’annotazione @Secure nel controllore:
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use JMS\SecurityExtraBundle\Annotation\Secure;
/**
* @Route("/hello/admin/{name}", name="_demo_secured_hello_admin")
* @Secure(roles="ROLE_ADMIN")
* @Template()
*/
public function helloAdminAction($name)
{
return array(’name’ => $name);
}
Ora, si entri come utente user (che non ha il ruolo ROLE_ADMIN) e, dalla pagina sicura “hello”, si clicchi sul
collegamento “Hello resource secured”. Symfony2 dovrebbe restituire un codice di stato HTTP 403 (“forbidden”),
indicando che l’utente non è autorizzato ad accedere a tale risorsa.
Note: Il livello di sicurezza di Symfony2 è molto flessibile e fornisce diversi provider per gli utenti (come quello
per l’ORM Doctrine) e provider di autenticazione (come HTTP basic, HTTP digest o certificati X509). Si legga il
capitolo “Sicurezza” del libro per maggiori informazioni su come usarli e configurarli.
Mettere in cache le risorse
Non appena il proprio sito inizia a generare più traffico, si vorrà evitare di dover generare la stessa risorsa più
volte. Symfony2 usa gli header di cache HTTP per gestire la cache delle risorse. Per semplici strategie di cache,
si può usare l’annotazione @Cache():
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache;
/**
* @Route("/hello/{name}", name="_demo_hello")
* @Template()
* @Cache(maxage="86400")
*/
public function helloAction($name)
{
return array(’name’ => $name);
}
In questo esempio, la risorsa sarà in cache per un giorno. Ma si può anche usare la validazione invece della
scadenza o una combinazione di entrambe, se questo soddisfa meglio le proprie esigenze.
La cache delle risorse è gestita dal reverse proxy predefinito di Symfony2. Ma poiché la cache è gestita usando
i normali header di cache di HTTP, è possibile rimpiazzare il reverse proxy predefinito con Varnish o Squid e
scalare facilmente la propria applicazione.
1.1. Giro rapido
15
Symfony2 documentation Documentation, Release 2
Note: E se non si volesse mettere in cache l’intera pagina? Symfony2 ha una soluzione, tramite Edge Side
Includes (ESI), supportate nativamente. Si possono avere maggiori informazioni nel capitolo “Cache HTTP” del
libro.
Considerazioni finali
È tutto, e forse non abbiamo nemmeno speso tutti e dieci i minuti previsti. Nella prima parte abbiamo introdotto
brevemente i bundle e tutte le caratteristiche apprese finora fanno parte del bundle del nucleo del framework. Ma,
grazie ai bundle, ogni cosa in Symfony2 può essere estesa o sostituita. Questo è l’argomento della prossima parte
di questa guida.
1.1.4 L’architettura
Sei il mio eroe! Chi avrebbe pensato che tu fossi ancora qui dopo le prime tre parti? I tuoi sforzi saranno presto
ricompensati. Le prime tre parti non danno uno sguardo approfondito all’architettura del framework. Poiché essa
rende unico Symfony2 nel panorama dei framework, vediamo in cosa consiste.
Capire la struttura delle cartelle
La struttura delle cartelle di un’applicazione Symfony2 è alquanto flessibile, ma la struttura delle cartelle della
distribuzione Standard Edition riflette la struttura tipica e raccomandata di un’applicazione Symfony2:
• app/: La configurazione dell’applicazione;
• src/: Il codice PHP del progetto;
• vendor/: Le dipendenze di terze parti;
• web/: La cartella radice del web.
La cartella web/
La cartella radice del web è la casa di tutti i file pubblici e statici, come immagini, fogli di stile, file JavaScript. È
anche il posto in cui stanno i front controller:
// web/app.php
require_once __DIR__.’/../app/bootstrap.php.cache’;
require_once __DIR__.’/../app/AppKernel.php’;
use Symfony\Component\HttpFoundation\Request;
$kernel = new AppKernel(’prod’, false);
$kernel->loadClassCache();
$kernel->handle(Request::createFromGlobals())->send();
Il kernel inizialmente richiede il file bootstrap.php.cache, che lancia l’applicazione e registra l’autoloader
(vedi sotto).
Come ogni front controller, app.php usa una classe Kernel, AppKernel, per inizializzare l’applicazione.
La cartella app/
La classe AppKernel è il punto di ingresso principale della configurazione dell’applicazione e quindi è memorizzata nella cartella app/.
Questa classe deve implementare due metodi:
16
Chapter 1. Giro rapido
Symfony2 documentation Documentation, Release 2
• registerBundles() deve restituire un array di tutti i bundle necessari per eseguire l’applicazione;
• registerContainerConfiguration() carica la configurazione dell’applicazione (approfondito
più avanti);
Il caricamento automatico di PHP può essere configurato tramite app/autoload.php:
// app/autoload.php
use Symfony\Component\ClassLoader\UniversalClassLoader;
$loader = new UniversalClassLoader();
$loader->registerNamespaces(array(
’Symfony’
=> array(__DIR__.’/../vendor/symfony/src’, __DIR__.’/../vendor/bundles’),
’Sensio’
=> __DIR__.’/../vendor/bundles’,
’JMS’
=> __DIR__.’/../vendor/bundles’,
’Doctrine\\Common’ => __DIR__.’/../vendor/doctrine-common/lib’,
’Doctrine\\DBAL’
=> __DIR__.’/../vendor/doctrine-dbal/lib’,
’Doctrine’
=> __DIR__.’/../vendor/doctrine/lib’,
’Monolog’
=> __DIR__.’/../vendor/monolog/src’,
’Assetic’
=> __DIR__.’/../vendor/assetic/src’,
’Metadata’
=> __DIR__.’/../vendor/metadata/src’,
));
$loader->registerPrefixes(array(
’Twig_Extensions_’ => __DIR__.’/../vendor/twig-extensions/lib’,
’Twig_’
=> __DIR__.’/../vendor/twig/lib’,
));
// ...
$loader->registerNamespaceFallbacks(array(
__DIR__.’/../src’,
));
$loader->register();
La classe Symfony\Component\ClassLoader\UniversalClassLoader di Symfony2 è usata per auto-caricare i file
che rispettano gli standard di interoperabilità per gli spazi dei nomi di PHP 5.3 oppure la convenzione dei nomi
di PEAR per le classi. Come si può vedere, tutte le dipendenze sono sotto la cartella vendor/, ma questa è solo
una convenzione. Si possono inserire in qualsiasi posto, globalmente sul proprio server o localmente nei propri
progetti.
Note: Se si vuole approfondire l’argomento flessibilità dell’autoloader di Symfony2, si può leggere il capitolo
“Il componente ClassLoader”.
Capire il sistema dei bundle
Questa sezione è un’introduzione a una delle più grandi e potenti caratteristiche di Symfony2, il sistema dei
bundle.
Un bundle è molto simile a un plugin in un altro software. Ma perché allora si chiama bundle e non plugin? Perché
ogni cosa è un bundle in Symfony2, dalle caratteristiche del nucleo del framework al codice scritto per la propria
applicazione. I bundle sono cittadini di prima classe in Symfony2. Essi forniscono la flessibilità di usare delle
caratteristiche pre-costruite impacchettate in bundle di terze parti o di distribuire i propri bundle. Questo rende
molto facile scegliere quali caratteristiche abilitare nella propria applicazione e ottimizzarle nel modo preferito. A
fine giornata, il codice della propria applicazione è importante quanto il nucleo stesso del framework.
Registrare un bundle
Un’applicazione è composta di bundle, come definito nel metodo registerBundles() della classe
AppKernel . Ogni bundle è una cartella che contiene una singola classe Bundle che la descrive:
1.1. Giro rapido
17
Symfony2 documentation Documentation, Release 2
// app/AppKernel.php
public function registerBundles()
{
$bundles = array(
new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
new Symfony\Bundle\SecurityBundle\SecurityBundle(),
new Symfony\Bundle\TwigBundle\TwigBundle(),
new Symfony\Bundle\MonologBundle\MonologBundle(),
new Symfony\Bundle\SwiftmailerBundle\SwiftmailerBundle(),
new Symfony\Bundle\DoctrineBundle\DoctrineBundle(),
new Symfony\Bundle\AsseticBundle\AsseticBundle(),
new Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle(),
new JMS\SecurityExtraBundle\JMSSecurityExtraBundle(),
);
if (in_array($this->getEnvironment(), array(’dev’, ’test’))) {
$bundles[] = new Acme\DemoBundle\AcmeDemoBundle();
$bundles[] = new Symfony\Bundle\WebProfilerBundle\WebProfilerBundle();
$bundles[] = new Sensio\Bundle\DistributionBundle\SensioDistributionBundle();
$bundles[] = new Sensio\Bundle\GeneratorBundle\SensioGeneratorBundle();
}
return $bundles;
}
Oltre a AcmeDemoBundle, di cui abbiamo già parlato, si noti che il kernel abilita anche FrameworkBundle,
DoctrineBundle, SwiftmailerBundle e AsseticBundle. Fanno tutti parte del nucleo del framework.
Configurare un bundle
Ogni bundle può essere personalizzato tramite file di configurazione scritti in YAML, XML o PHP. Si veda la
configurazione predefinita:
# app/config/config.yml
imports:
- { resource: parameters.yml }
- { resource: security.yml }
framework:
#esi:
#translator:
secret:
charset:
router:
form:
csrf_protection:
validation:
templating:
default_locale:
session:
auto_start:
~
{ fallback: %locale% }
%secret%
UTF-8
{ resource: "%kernel.root_dir%/config/routing.yml" }
true
true
{ enable_annotations: true }
{ engines: [’twig’] } #assets_version: SomeVersionScheme
%locale%
true
# Configurazione di Twig
twig:
debug:
%kernel.debug%
strict_variables: %kernel.debug%
# Configurazione di
assetic:
debug:
use_controller:
bundles:
18
Assetic
%kernel.debug%
false
[ ]
Chapter 1. Giro rapido
Symfony2 documentation Documentation, Release 2
# java: /usr/bin/java
filters:
cssrewrite: ~
# closure:
#
jar: %kernel.root_dir%/java/compiler.jar
# yui_css:
#
jar: %kernel.root_dir%/java/yuicompressor-2.4.2.jar
# Configurazione di Doctrine
doctrine:
dbal:
driver:
%database_driver%
host:
%database_host%
port:
%database_port%
dbname:
%database_name%
user:
%database_user%
password: %database_password%
charset: UTF8
orm:
auto_generate_proxy_classes: %kernel.debug%
auto_mapping: true
# Configurazione di Swiftmailer
swiftmailer:
transport: %mailer_transport%
host:
%mailer_host%
username: %mailer_user%
password: %mailer_password%
jms_security_extra:
secure_controllers: true
secure_all_services: false
Ogni voce come framework definisce la configurazione per uno specifico bundle. Per esempio, framework
configura FrameworkBundle, mentre swiftmailer configura SwiftmailerBundle.
Ogni ambiente può sovrascrivere la configurazione predefinita, fornendo un file di configurazione specifico.
Per esempio, l’ambiente dev carica il file config_dev.yml, che carica la configurazione principale (cioè
config.yml) e quindi la modifica per aggiungere alcuni strumenti di debug:
# app/config/config_dev.yml
imports:
- { resource: config.yml }
framework:
router:
{ resource: "%kernel.root_dir%/config/routing_dev.yml" }
profiler: { only_exceptions: false }
web_profiler:
toolbar: true
intercept_redirects: false
monolog:
handlers:
main:
type:
path:
level:
firephp:
type:
level:
1.1. Giro rapido
stream
%kernel.logs_dir%/%kernel.environment%.log
debug
firephp
info
19
Symfony2 documentation Documentation, Release 2
assetic:
use_controller: true
Estendere un bundle
Oltre a essere un modo carino per organizzare e configurare il proprio codice, un bundle può estendere
un altro bundle. L’ereditarietà dei bundle consente di sovrascrivere un bundle esistente, per poter personalizzare i suoi controllori, i template o qualsiasi altro suo file. Qui sono d’aiuto i nomi logici (come
@AcmeDemoBundle/Controller/SecuredController.php), che astraggono i posti in cui le risorse
sono effettivamente memorizzate.
Nomi logici di file Quando si vuole fare riferimento a un file da un bundle, usare questa notazione:
@NOME_BUNDLE/percorso/del/file; Symfony2 risolverà @NOME_BUNDLE nel percorso reale del bundle. Per esempio, il percorso logico @AcmeDemoBundle/Controller/DemoController.php verrebbe
convertito in src/Acme/DemoBundle/Controller/DemoController.php, perché Symfony conosce
la locazione di AcmeDemoBundle.
Nomi logici di controllori Per i controllori, occorre fare riferimento ai nomi dei metodi
usando
il
formato
NOME_BUNDLE:NOME_CONTROLLORE:NOME_AZIONE.
Per
esempio,
AcmeDemoBundle:Welcome:index mappa il metodo indexAction della classe
Acme\DemoBundle\Controller\WelcomeController.
Nomi logici di template Per i template, il nome logico AcmeDemoBundle:Welcome:index.html.twig
è convertito al percorso del file src/Acme/DemoBundle/Resources/views/Welcome/index.html.twig.
I template diventano ancora più interessanti quando si realizza che i file non hanno bisogno di essere memorizzati
su filesystem. Si possono facilmente memorizzare, per esempio, in una tabella di database.
Estendere i bundle Se si seguono queste convenzioni, si può usare l’ereditarietà dei bundle per “sovrascrivere” file, controllori o template. Per esempio, se un nuovo bundle chiamato AcmeNewBundle estende
AcmeDemoBundle, Symfony proverà a caricare prima il controllore AcmeDemoBundle:Welcome:index
da AcmeNewBundle e poi cercherà il secondo AcmeDemoBundle. Questo vuol dire che un bundle può
sovrascrivere quasi ogni parte di un altro bundle!
Capite ora perché Symfony2 è così flessibile? Condividere i propri bundle tra le applicazioni, memorizzarli
localmente o globalmente, a propria scelta.
Usare i venditori
Probabilmente la propria applicazione dipenderà da librerie di terze parti. Queste ultime dovrebbero essere memorizzate nella cartella vendor/. Tale cartella contiene già le librerie di Symfony2, SwiftMailer, l’ORM Doctrine,
il sistema di template Twig e alcune altre librerie e bundle di terze parti.
Capire la cache e i log
Symfony2 è forse uno dei framework completi più veloci in circolazione. Ma come può essere così veloce, se
analizza e interpreta decine di file YAML e XML a ogni richiesta? In parte, per il suo sistema di cache. La
configurazione dell’applicazione è analizzata solo per la prima richiesta e poi compilata in semplice file PHP,
memorizzato nella cartella app/cache/ dell’applicazione. Nell’ambiente di sviluppo, Symfony2 è abbastanza
intelligente da pulire la cache quando cambiano dei file. In produzione, invece, occorre pulire la cache manualmente quando si aggiorna il codice o si modifica la configurazione.
Sviluppando un’applicazione web, le cose possono andar male in diversi modi. I file di log nella cartella
app/logs/ dicono tutto a proposito delle richieste e aiutano a risolvere il problema in breve tempo.
20
Chapter 1. Giro rapido
Symfony2 documentation Documentation, Release 2
Usare l’interfaccia a linea di comando
Ogni applicazione ha uno strumento di interfaccia a linea di comando (app/console), che aiuta nella manutenzione dell’applicazione. La console fornisce dei comandi che incrementano la produttività, automatizzando dei
compiti noiosi e ripetitivi.
Richiamandola senza parametri, si può sapere di più sulle sue capacità:
php app/console
L’opzione --help aiuta a scoprire l’utilizzo di un comando:
php app/console router:debug --help
Considerazioni finali
Dopo aver letto questa parte, si dovrebbe essere in grado di muoversi facilmente dentro Symfony2 e farlo funzionare. Ogni cosa in Symfony2 è fatta per rispondere alle varie esigenze. Quindi, si possono rinominare e
spostare le varie cartelle, finché non si raggiunge il risultato voluto.
E questo è tutto per il giro veloce. Dai test all’invio di email, occorre ancora imparare diverse cose per padroneggiare Symfony2. Pronti per approfondire questi temi? Senza indugi, basta andare nella pagine del libro e scegliere
un argomento a piacere.
• Un quadro generale >
• La vista >
• Il controllore >
• L’architettura
1.1. Giro rapido
21
Symfony2 documentation Documentation, Release 2
22
Chapter 1. Giro rapido
CHAPTER
TWO
LIBRO
Approfondire Symfony2 con le guide per argomento:
2.1 Libro
2.1.1 Symfony2 e fondamenti di HTTP
Congratulazioni! Imparando Symfony2, si tende a essere sviluppatori web più produttivi, versatili e popolari (in
realtà, per quest’ultimo dovete sbrigarvela da soli). Symfony2 è costruito per tornare alle basi: per sviluppare
strumenti che consentono di sviluppare più velocemente e costruire applicazioni più robuste, anche andando fuori
strada. Symfony è costruito sulle migliori idee prese da diverse tecnologie: gli strumenti e i concetti che si
stanno per apprendere rappresentano lo sforzo di centinaia di persone, in molti anni. In altre parole, non si sta
semplicemente imparando “Symfony”, si stanno imparando i fondamenti del web, le pratiche migliori per lo
sviluppo e come usare tante incredibili librerie PHP, all’interno o dipendenti da Symfony2. Tenetevi pronti.
Fedele alla filosofia di Symfony2, questo capitolo inizia spiegando il concetto fondamentale comune allo sviluppo
web: HTTP. Indipendentemente dalla propria storia o dal linguaggio di programmazione preferito, questo capitolo
andrebbe letto da tutti.
HTTP è semplice
HTTP (Hypertext Transfer Protocol) è un linguaggio testuale che consente a due macchine di comunicare tra
loro. Tutto qui! Per esempio, quando controllate l’ultima vignetta di xkcd, ha luogo la seguente conversazione
(approssimata):
E mentre il linguaggio veramente usato è un po’ più formale, è ancora assolutamente semplice. HTTP è il termine
usato per descrivere tale semplice linguaggio testuale. Non importa in quale linguaggio si sviluppi sul web, lo
23
Symfony2 documentation Documentation, Release 2
scopo del proprio server è sempre quello di interpretare semplici richieste testuali e restituire semplici risposte
testuali.
Symfony2 è costruito fin dalle basi attorno a questa realtà. Che lo si comprenda o meno, HTTP è qualcosa che si
usa ogni giorno. Con Symfony2, si imparerà come padroneggiarlo.
Passo 1: il client invia una richiesta
Ogni conversazione sul web inizia con una richiesta. La richiesta è un messaggio testuale creato da un client (per
esempio un browser, un’applicazione mobile, ecc.) in uno speciale formato noto come HTTP. Il client invia la
richiesta a un server e quindi attende una risposta.
Diamo uno sguardo alla prima parte dell’interazione (la richiesta) tra un browser e il server web di xkcd:
Nel gergo di HTTP, questa richiesta apparirebbe in realtà in questo modo:
GET / HTTP/1.1
Host: xkcd.com
Accept: text/html
User-Agent: Mozilla/5.0 (Macintosh)
Questo semplice messaggio comunica ogni cosa necessaria su quale risorsa esattamente il client sta richiedendo.
La prima riga di ogni richiesta HTTP è la più importante e contiene due cose: l’URI e il metodo HTTP.
L’URI (p.e. /, /contact, ecc.) è l’indirizzo univoco o la locazione che identifica la risorsa che il client vuole.
Il metodo HTTP (p.e. GET) definisce cosa si vuole fare con la risorsa. I metodi HTTP sono verbi della richiesta e
definiscono i pochi modi comuni in cui si può agire sulla risorsa:
GET
POST
PUT
DELETE
Recupera la risorsa dal server
Crea una risorsa sul server
Aggiorna la risorsa sul server
Elimina la risorsa dal server
Tenendo questo a mente, si può immaginare come potrebbe apparire una richiesta HTTP per cancellare una specifica voce di un blog, per esempio:
DELETE /blog/15 HTTP/1.1
Note: Ci sono in realtà nove metodi HTTP definiti dalla specifica HTTP, ma molti di essi non sono molto usati o
supportati. In realtà, molti browser moderni non supportano nemmeno i metodi PUT e DELETE.
In aggiunta alla prima linea, una richiesta HTTP contiene sempre altre linee di informazioni, chiamate header. Gli
header possono fornire un ampio raggio di informazioni, come l’Host richiesto, i formati di risposta accettati dal
client (Accept) e l’applicazione usata dal client per eseguire la richiesta (User-Agent). Esistono molti altri
header, che possono essere trovati nella pagina di Wikipedia Lista di header HTTP.
24
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
Passo 2: Il server restituisce una risposta
Una volta che il server ha ricevuto la richiesta, sa esattamente la risorsa di cui il client ha bisogno (tramite l’URI)
e cosa vuole fare il client con tale risorsa (tramite il metodo). Per esempio, nel caso di una richiesta GET, il server
prepara la risorsa e la restituisce in una risposta HTTP. Consideriamo la risposta del server web di xkcd:
Tradotto in HTTP, la risposta rimandata al browser assomiglierà a questa:
HTTP/1.1 200 OK
Date: Sat, 02 Apr 2011 21:05:05 GMT
Server: lighttpd/1.4.19
Content-Type: text/html
<html>
<!-- HTML for the xkcd comic -->
</html>
La risposta HTTP contiene la risorsa richiesta (il contenuto HTML, in questo caso). oltre che altre informazioni
sulla risposta. La prima riga è particolarmente importante e contiene il codice di stato della risposta HTTP (200,
in questo caso). Il codice di stato comunica il risultato globale della richiesta al client. La richiesta è andata a buon
fine? C’è stato un errore? Diversi codici di stato indicano successo, errore o che il client deve fare qualcosa (p.e.
rimandare a un’altra pagina). Una lista completa può essere trovata nella pagina di Wikipedia Elenco dei codici di
stato HTTP.
Come la richiesta, una risposta HTTP contiene parti aggiuntive di informazioni, note come header. Per esempio,
un importante header di risposta HTTP è Content-Type. Il corpo della risorsa stessa potrebbe essere restituito
in molti formati diversi, inclusi HTML, XML o JSON, mentre l’header Content-Type usa i tipi di media di
Internet, come text/html, per dire al client quale formato è restituito. Ua lista di tipi di media comuni si può
trovare sulla voce di Wikipedia Lista di tipi di media comuni.
Esistono molti altri header, alcuni dei quali molto potenti. Per esempio, alcuni header possono essere usati per
creare un potente sistema di cache.
Richieste, risposte e sviluppo web
Questa conversazione richiesta-risposta è il processo fondamentale che guida tutta la comunicazione sul web.
Questo processo è tanto importante e potente, quanto inevitabilmente semplice.
L’aspetto più importante è questo: indipendentemente dal linguaggio usato, il tipo di applicazione costruita (web,
mobile, API JSON) o la filosofia di sviluppo seguita, lo scopo finale di un’applicazione è sempre quello di capire
ogni richiesta e creare e restituire un’appropriata risposta.
L’architettura di Symfony è strutturata per corrispondere a questa realtà.
2.1. Libro
25
Symfony2 documentation Documentation, Release 2
Tip: Per saperne di più sulla specifica HTTP, si può leggere la RFC HTTP 1.1 originale o la HTTP Bis, che è uno
sforzo attivo di chiarire la specifica originale. Un importante strumento per verificare sia gli header di richiesta
che quelli di risposta durante la navigazione è l’estensione Live HTTP Headers di Firefox.
Richieste e risposte in PHP
Dunque, come interagire con la “richiesta” e creare una “risposta” quando si usa PHP? In realtà, PHP astrae un
po’ l’intero processo:
<?php
$uri = $_SERVER[’REQUEST_URI’];
$pippo = $_GET[’pippo’];
header(’Content-type: text/html’);
echo ’L\’URI richiesto è: ’.$uri;
echo ’Il valore del parametro "pippo" è: ’.$pippo;
Per quanto possa sembrare strano, questa piccola applicazione di fatto prende informazioni dalla richiesta HTTP
e le usa per creare una risposta HTTP. Invece di analizzare il messaggio di richiesta HTTP grezzo, PHP prepara
della variabili superglobali, come $_SERVER e $_GET, che contengono tutte le informazioni dalla richiesta.
Similmente, inece di restituire un testo di risposta formattato come da HTTP, si può usare la funzione header()
per creare header di risposta e stampare semplicemente il contenuto, che sarà la parte di contenuto del messaggio
di risposta. PHP creerà una vera risposta HTTP e la restituirà al client:
HTTP/1.1 200 OK
Date: Sat, 03 Apr 2011 02:14:33 GMT
Server: Apache/2.2.17 (Unix)
Content-Type: text/html
L’URI richiesto è: /testing?pippo=symfony
Il valore del parametro "pippo" è: symfony
Richieste e risposte in Symfony
Symfony fornisce un’alternativa all’approccio grezzo di PHP, tramite due classi che consentono di interagire con richiesta e risposta HTTP in modo più facile.
La classe
Symfony\Component\HttpFoundation\Request è una semplice rappresentazione orientata agli
oggetti del messaggio di richiesta HTTP. Con essa, si hanno a portata di mano tutte le informazioni sulla richiesta:
use Symfony\Component\HttpFoundation\Request;
$request = Request::createFromGlobals();
// l’URI richiesto (p.e. /about) tranne ogni parametro
$request->getPathInfo();
// recupera rispettivamente le variabili GET e POST
$request->query->get(’pippo’);
$request->request->get(’pluto’);
// recupera le variabili SERVER
$request->server->get(’HTTP_HOST’);
// recupera un’istanza di UploadedFile identificata da pippo
$request->files->get(’pippo’);
// recupera il valore di un COOKIE
$request->cookies->get(’PHPSESSID’);
26
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
// recupera un header di risposta HTTP, con chiavi normalizzate e minuscole
$request->headers->get(’host’);
$request->headers->get(’content_type’);
$request->getMethod();
$request->getLanguages();
// GET, POST, PUT, DELETE, HEAD
// un array di lingue accettate dal client
Come bonus, la classe Request fa un sacco di lavoro in sottofondo, di cui non ci si dovrà mai preoccupare.
Per esempio, il metodo isSecure() verifica tre diversi valori in PHP che possono indicare se l’utente si stia
connettendo o meno tramite una connessione sicura (cioè https).
ParameterBags e attributi di Request
Come visto in precedenza, le variabili $_GET e $_POST sono accessibili rispettivamente tramite le proprietà pubbliche query e request.
Entrambi questi oggetti sono
oggetti
della
classe
Symfony\Component\HttpFoundation\ParameterBag,
che
ha
metodi
come
:method:‘Symfony\\Component\\HttpFoundation\\ParameterBag::get‘,
:method:‘Symfony\\Component\\HttpFoundation\\ParameterBag::has‘,
:method:‘Symfony\\Component\\HttpFoundation\\ParameterBag::all‘ e altri. In effetti, ogni proprietà pubblica usata nell’esempio precedente è un’istanza di ParameterBag. La classe Request ha anche
una proprietà pubblica attributes, che contiene dati speciali relativi a come l’applicazione funziona internamente. Per il framework Symfony2, attributes contiene valori restituiti dalla rotta corrispondente,
come _controller, id (se si ha un parametro {id}), e anche il nome della rotta stessa (_route). La
proprietà attributes è pensata apposta per essere un posto in cui preparare e memorizzare informazioni
sulla richiesta relative al contesto.
Symfony fornisce anche una classe Response: una semplice rappresentazione PHP di un messaggio di risposta
HTTP. Questo consente alla propria applicazione di usare un’interfaccia orientata agli oggetti per costruire la
risposta che occorre restituire al client:
use Symfony\Component\HttpFoundation\Response;
$response = new Response();
$response->setContent(’<html><body><h1>Ciao mondo!</h1></body></html>’);
$response->setStatusCode(200);
$response->headers->set(’Content-Type’, ’text/html’);
// stampa gli header HTTP seguiti dal contenuto
$response->send();
Se Symfony offrisse solo questo, si avrebbe già a disposizione un kit di strumenti per accedere facilmente alle
informazioni di richiesta e un’interfaccia orientata agli oggetti per creare la risposta. Anche imparando le molte
potenti caratteristiche di Symfony, si tenga a mente che lo scopo della propria applicazione è sempre quello di
interpretare una richiesta e creare l’appropriata risposta, basata sulla logica dell’applicazione.
Tip: Le classi Request e Response fanno parte di un componente a sé stante incluso con Symfony, chiamato
HttpFoundation. Questo componente può essere usato in modo completamente indipendente da Symfony e
fornisce anche classi per gestire sessioni e caricamenti di file.
Il viaggio dalla richiesta alla risposta
Come lo stesso HTTP, gli oggetti Request e Response sono molto semplici. La parte difficile nella costruzione
di un’applicazione è la scrittura di quello che sta in mezzo. In altre parole, il vero lavoro consiste nello scrivere il
codice che interpreta l’informazione della richiesta e crea la risposta.
La propria applicazione probabilmente fa molte cose, come inviare email, gestire invii di form, salvare dati in un
database, rendere pagine HTML e proteggere contenuti. Come si può gestire tutto questo e mantenere al contempo
il proprio codice organizzato e mantenibile?
2.1. Libro
27
Symfony2 documentation Documentation, Release 2
Symfony è stato creato per risolvere questi problemi.
Il front controller
Le applicazioni erano tradizionalmente costruite in modo che ogni “pagina” di un sito fosse un file fisico:
index.php
contact.php
blog.php
Ci sono molti problemi con questo approccio, inclusa la flessibilità degli URL (che succede se si vuole cambiare
blog.php con news.php senza rompere tutti i collegamenti?) e il fatto che ogni file deve includere manualmente alcuni file necessari, in modo che la sicurezza, le connessioni al database e l’aspetto del sito possano
rimanere coerenti.
Una soluzione molto migliore è usare un front controller: un unico file PHP che gestisce ogni richiesta che arriva
alla propria applicazione. Per esempio:
/index.php
/index.php/contact
/index.php/blog
esegue index.php
esegue index.php
esegue index.php
Tip: Usando il modulo mod_rewrite di Apache (o moduli equivalenti di altri server), gli URL possono essere
facilmente puliti per essere semplicemente /, /contact e /blog.
Ora ogni richiesta è gestita esattamente nello stesso modo. Invece di singoli URL che eseguono diversi file PHP,
è sempre eseguito il front controller, e il dirottamento di URL diversi sulle diverse parti della propria applicazione
è gestito internamente. Questo risolve entrambi i problemi dell’approccio originario. Quasi tutte le applicazioni
web moderne fanno in questo modo, incluse applicazioni come WordPress.
Restare organizzati
Ma all’interno del nostro front controller, come possiamo sapere quale pagina debba essere resa e come poterla
renderla in modo facile? In un modo o nell’altro, occorre verificare l’URI in entrata ed eseguire parti diverse di
codice, a seconda di tale valore. Le cose possono peggiorare rapidamente:
// index.php
$request = Request::createFromGlobals();
$path = $request->getPathInfo(); // l’URL richiesto
if (in_array($path, array(’’, ’/’)) {
$response = new Response(’Benvenuto nella homepage.’);
} elseif ($path == ’/contact’) {
$response = new Response(’Contattaci’);
} else {
$response = new Response(’Pagina non trovata.’, 404);
}
$response->send();
La soluzione a questo problema può essere difficile. Fortunatamente, è esattamente quello che Symfony è studiato
per fare.
Il flusso di un’applicazione Symfony
Quando si lascia a Symfony la gestione di ogni richiesta, la vita è molto più facile. Symfony segue lo stesso
semplice schema per ogni richiesta:
28
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
Figure 2.1: Le richieste in entrata sono interpretate dal routing e passate alle funzioni del controllore, che restituisce oggetti Response.
Ogni “pagina” del proprio sito è definita in un file di configurazione delle rotte, che mappa diversi URL su diverse
funzioni PHP. Il compito di ogni funzione PHP, chiamata controllore, è di usare l’informazione della richiesta,
insieme a molti altri strumenti resi disponibili da Symfony, per creare e restituire un oggetto Response. In altre
parole, il controllore è il posto in cui va il proprio codice: è dove si interpreta la richiesta e si crea la risposta.
È così facile! Rivediamolo:
• Ogni richiesta esegue un file front controller;
• Il sistema delle rotte determina quale funzione PHP deve essere eseguita, in base all’informazione proveniente dalla richiesta e alla configurazione delle rotte creata;
• La giusta funzione PHP è eseguita, con il proprio codice che crea e restituisce l’oggetto Response appropriato.
Un richiesta Symfony in azione
Senza entrare troppo in dettaglio, vediamo questo processo in azione. Supponiamo di voler aggiungere una pagina
/contact alla nostra applicazione Symfony. Primo, iniziamo aggiungendo una voce per /contact nel file di
configurazione delle rotte:
contact:
pattern: /contact
defaults: { _controller: AcmeDemoBundle:Main:contact }
Note: L’esempio usa YAML per definire la configurazione delle rotte. La configurazione delle rotte può essere
scritta anche in altri formati, come XML o PHP.
Quando qualcuno vista la pagina /contact, questa rotta viene corrisposta e il controllore specificato è eseguito.
Come si imparerà nel capitolo delle rotte, la stringa AcmeDemoBundle:Main:contact è una sintassi breve
che punta a uno specifico metodo PHP contactAction in una classe chiamata MainController:
class MainController
{
public function contactAction()
{
return new Response(’<h1>Contattaci!</h1>’);
}
}
2.1. Libro
29
Symfony2 documentation Documentation, Release 2
In questo semplice esempio, il controllore semplicemente crea un oggetto Response con il codice HTML
“<h1>Contacttaci!</h1>”. Nel capitolo sul controllore, si imparerà come un controllore possa rendere dei template, consentendo al proprio codice di “presentazione” (cioè a qualsiasi cosa che scrive effettivamente HTML)
di vivere in un file template separato. Questo consente al controllore di preoccuparsi solo delle cose difficili:
interagire col database, gestire l’invio di dati o l’invio di messaggi email.
Symfony2: costruire la propria applicazione, non i propri strumenti.
Sappiamo dunque che lo scopo di un’applicazione è interpretare ogni richiesta in entrata e creare un’appropriata
risposta. Al crescere di un’applicazione, diventa sempre più difficile mantenere il proprio codice organizzato e
mantenibile. Invariabilmente, gli stessi complessi compiti continuano a presentarsi: persistere nella base dati,
rendere e riusare template, gestire invii di form, inviare email, validare i dati degli utenti e gestire la sicurezza.
La buona notizia è che nessuno di questi problemi è unico. Symfony fornisce un framework pieno di strumenti che
consentono di costruire un’applicazione, non di costruire degli strumenti. Con Symfony2, nulla viene imposto: si
è liberi di usare l’intero framework oppure un solo pezzo di Symfony.
Strumenti isolati: i componenti di Symfony2
Cos’è dunque Symfony2? Primo, è un insieme di oltre venti librerie indipendenti, che possono essere usate in
qualsiasi progetto PHP. Queste librerie, chiamate componenti di Symfony2, contengono qualcosa di utile per quasi
ogni situazione, comunque sia sviluppato il proprio progetto. Solo per nominarne alcuni:
• HttpFoundation - Contiene le classi Request e Response, insieme ad altre classi per gestire sessioni e
caricamenti di file;
• Routing - Sistema di rotte potente e veloce, che consente di mappare uno specifico URI (p.e.
/contact) ad alcune informazioni su come tale richiesta andrebbe gestita (p.e. eseguendo il metodo
contactAction());
• Form - Un framework completo e flessibile per creare form e gestire invii di dati;
• Validator Un sistema per creare regole sui dati e quindi validarli, sia che i dati inviati dall’utente seguano o
meno tali regole;
• ClassLoader Una libreria di autoloading che consente l’uso di classi PHP senza bisogno di usare manualmente require sui file che contengono tali classi;
• Templating Un insieme di strumenti per rendere template, gestire l’ereditarietà dei template (p.e. un template è decorato con un layout) ed eseguire altri compiti comuni sui template;
• Security - Una potente libreria per gestire tutti i tipi di sicurezza all’interno di un’applicazione;
• Translation Un framework per tradurre stringhe nella propria applicazione.
Tutti questi componenti sono disaccoppiati e possono essere usati in qualsiasi progetto PHP, indipendentemente
dall’uso del framework Symfony2. Ogni parte di essi è stata realizzata per essere usata se necessario e sostituita
in caso contrario.
La soluzione completa il framework Symfony2
Cos’è quindi il framework Symfony2? Il framework Symfony2 è una libreria PHP che esegue due compiti distinti:
1. Fornisce una selezione di componenti (cioè i componenti di Symfony2) e librerie di terze parti (p.e.
Swiftmailer per l’invio di email);
2. Fornisce una pratica configurazione e una libreria “collante”, che lega insieme tutti i pezzi.
Lo scopo del framework è integrare molti strumenti indipendenti, per fornire un’esperienza coerente allo sviluppatore. Anche il framework stesso è un bundle (cioè un plugin) che può essere configurato o sostituito interamente.
30
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
Symfony2 fornisce un potente insieme di strumenti per sviluppare rapidamente applicazioni web, senza imposizioni sulla propria applicazione. Gli utenti normali possono iniziare velocemente a sviluppare usando una distribuzione di Symfony2, che fornisce uno scheletro di progetto con configurazioni predefinite ragionevoli. Gli
utenti avanzati hanno il cielo come limite.
2.1.2 Symfony2 contro PHP puro
Perché Symfony2 è meglio che aprire un file e scrivere PHP puro?
Questo capitolo è per chi non ha mai usato un framework PHP, non ha familiarità con la filosofia MVC, oppure
semplicemente si chiede il motivo di tutto il clamore su Symfony2. Invece di raccontare che Symfony2 consente
di sviluppare software più rapidamente e in modo migliore che con PHP puro, ve lo faremo vedere.
In questo capitolo, scriveremo una semplice applicazione in PHP puro e poi la rifattorizzeremo per essere più
organizzata. Viaggeremo nel tempo, guardando le decisioni che stanno dietro ai motivi per cui lo sviluppo web si
è evoluto durante gli ultimi anni per diventare quello che è ora.
Alla fine, vedremo come Symfony2 possa salvarci da compiti banali e consentirci di riprendere il controllo del
nostro codice.
Un semplice blog in PHP puro
In questo capitolo, costruiremo un’applicazione blog usando solo PHP puro. Per iniziare, creiamo una singola
pagina che mostra le voci del blog, che sono state memorizzate nel database. La scrittura in puro PHP è sporca e
veloce:
<?php
// index.php
$link = mysql_connect(’localhost’, ’mioutente’, ’miapassword’);
mysql_select_db(’blog_db’, $link);
$result = mysql_query(’SELECT id, title FROM post’, $link);
?>
<html>
<head>
<title>Lista dei post</title>
</head>
<body>
<h1>Lista dei post</h1>
<ul>
<?php while ($row = mysql_fetch_assoc($result)): ?>
<li>
<a href="/show.php?id=<?php echo $row[’id’] ?>">
<?php echo $row[’title’] ?>
</a>
</li>
<?php endwhile; ?>
</ul>
</body>
</html>
<?php
mysql_close($link);
Veloce da scrivere, rapido da eseguire e, al crescere dell’applicazione, impossibile da mantenere. Ci sono diversi
problemi che occorre considerare:
• Niente verifica degli errori: Che succede se la connessione al database fallisce?
2.1. Libro
31
Symfony2 documentation Documentation, Release 2
• Scarsa organizzazione: Se l’applicazione cresce, questo singolo file diventerà sempre più immantenibile.
Dove inserire il codice per gestire la compilazione di un form? Come validare i dati? Dove mettere il codice
per inviare delle email?
• Difficoltà nel riusare il codice: Essendo tutto in un solo file, non c’è modo di riusare alcuna parte
dell’applicazione per altre “pagine” del blog.
Note: Un altro problema non menzionato è il fatto che il database è legato a MySQL. Sebbene non affrontato
qui, Symfony2 integra in pieno Doctrine, una libreria dedicata all’astrazione e alla mappatura del database.
Cerchiamo di metterci al lavoro per risolvere questi e altri problemi.
Isolare la presentazione
Il codice può beneficiare immediatamente dalla separazione della “logica” dell’applicazione dal codice che prepara
la “presentazione” in HTML:
<?php
// index.php
$link = mysql_connect(’localhost’, ’mioutente’, ’miapassword’);
mysql_select_db(’blog_db’, $link);
$result = mysql_query(’SELECT id, title FROM post’, $link);
$posts = array();
while ($row = mysql_fetch_assoc($result)) {
$posts[] = $row;
}
mysql_close($link);
// include il codice HTML di presentazione
require ’templates/list.php’;
Il codice HTML ora è in un file separato (templates/list.php), che è essenzialmente un file HTML che
usa una sintassi PHP per template:
<html>
<head>
<title>Lista dei post</title>
</head>
<body>
<h1>Lista dei post</h1>
<ul>
<?php foreach ($posts as $post): ?>
<li>
<a href="/read?id=<?php echo $post[’id’] ?>">
<?php echo $post[’title’] ?>
</a>
</li>
<?php endforeach; ?>
</ul>
</body>
</html>
Per convenzione, il file che contiene tutta la logica dell’applicazione, cioè index.php, è noto come “controllore”. Il termine controllore è una parola che ricorrerà spesso, quale che sia il linguaggio o il framework scelto.
Si riferisce semplicemente alla parte del proprio codice che processa l’input proveniente dall’utente e prepara la
risposta.
32
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
In questo caso, il nostro controllore prepara i dati estratti dal database e quindi include un template, per presentare
tali dati. Con il controllore isolato, è possibile cambiare facilmente solo il file template necessario per rendere le
voci del blog in un qualche altro formato (p.e. list.json.php per il formato JSON).
Isolare la logica dell’applicazione (il dominio)
Finora l’applicazione contiene una singola pagina. Ma se una seconda pagina avesse bisogno di usare la stessa
connessione al database, o anche lo stesso array di post del blog? Rifattorizziamo il codice in modo che il comportamento centrale e le funzioni di accesso ai dati dell’applicazioni siano isolati in un nuovo file, chiamato
model.php:
<?php
// model.php
function open_database_connection()
{
$link = mysql_connect(’localhost’, ’mioutente’, ’miapassword’);
mysql_select_db(’blog_db’, $link);
return $link;
}
function close_database_connection($link)
{
mysql_close($link);
}
function get_all_posts()
{
$link = open_database_connection();
$result = mysql_query(’SELECT id, title FROM post’, $link);
$posts = array();
while ($row = mysql_fetch_assoc($result)) {
$posts[] = $row;
}
close_database_connection($link);
return $posts;
}
Tip: Il nome model.php è usato perché la logica e l’accesso ai dati di un’applicazione sono tradizionalmente
noti come il livello del “modello”. In un’applicazione ben organizzata, la maggior parte del codice che rappresenta
la “logica di business” dovrebbe stare nel modello (invece che stare in un controllore). Diversamente da questo
esempio, solo una parte (o niente) del modello riguarda effettivamente l’accesso a un database.
Il controllore (index.php) è ora molto semplice:
<?php
require_once ’model.php’;
$posts = get_all_posts();
require ’templates/list.php’;
Ora, l’unico compito del controllore è prendere i dati dal livello del modello dell’applicazione (il modello) e richiamare un template per rendere tali dati. Questo è un esempio molto semplice del pattern model-view-controller.
2.1. Libro
33
Symfony2 documentation Documentation, Release 2
Isolare il layout
A questo punto, l’applicazione è stata rifattorizzata in tre parti distinte, offrendo diversi vantaggi e l’opportunità
di riusare quasi tutto su pagine diverse.
L’unica parte del codice che non può essere riusata è il layout. Sistemiamo questo aspetto, creando un nuovo file
layout.php:
<!-- templates/layout.php -->
<html>
<head>
<title><?php echo $title ?></title>
</head>
<body>
<?php echo $content ?>
</body>
</html>
Il template (templates/list.php) ora può essere semplificato, per “estendere” il layout:
<?php $title = ’Lista dei post’ ?>
<?php ob_start() ?>
<h1>Lista dei post</h1>
<ul>
<?php foreach ($posts as $post): ?>
<li>
<a href="/read?id=<?php echo $post[’id’] ?>">
<?php echo $post[’title’] ?>
</a>
</li>
<?php endforeach; ?>
</ul>
<?php $content = ob_get_clean() ?>
<?php include ’layout.php’ ?>
Qui abbiamo introdotto una metodologia che consente il riuso del layout. Sfortunatamente, per poterlo fare, si è
costretti a usare alcune brutte funzioni PHP (ob_start(), ob_get_clean()) nel template. Symfony2 usa
un componente Templating, che consente di poter fare ciò in modo pulito e facile. Lo vedremo in azione tra
poco.
Aggiungere al blog una pagina “show”
La pagina “elenco” del blog è stata ora rifattorizzata in modo che il codice sia meglio organizzato e riusabile. Per
provarlo, aggiungiamo al blog una pagina “mostra”, che mostra un singolo post del blog identificato dal parametro
id.
Per iniziare, creiamo nel file model.php una nuova funzione, che recupera un singolo risultato del blog a partire
da un id dato:
// model.php
function get_post_by_id($id)
{
$link = open_database_connection();
$id = mysql_real_escape_string($id);
$query = ’SELECT date, title, body FROM post WHERE id = ’.$id;
$result = mysql_query($query);
$row = mysql_fetch_assoc($result);
close_database_connection($link);
34
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
return $row;
}
Quindi, creiamo un file chiamato show.php, il controllore per questa nuova pagina:
<?php
require_once ’model.php’;
$post = get_post_by_id($_GET[’id’]);
require ’templates/show.php’;
Infine, creiamo un nuovo file template, templates/show.php, per rendere il singolo post del blog:
<?php $title = $post[’title’] ?>
<?php ob_start() ?>
<h1><?php echo $post[’title’] ?></h1>
<div class="date"><?php echo $post[’date’] ?></div>
<div class="body">
<?php echo $post[’body’] ?>
</div>
<?php $content = ob_get_clean() ?>
<?php include ’layout.php’ ?>
La creazione della seconda pagina è stata molto facile e non ha implicato alcuna duplicazione di codice. Tuttavia,
questa pagina introduce alcuni altri problemi, che un framework può risolvere. Per esempio, un parametro id
mancante o non valido causerà un errore nella pagina. Sarebbe meglio se facesse rendere una pagina 404, ma
non possiamo ancora farlo in modo facile. Inoltre, avendo dimenticato di pulire il parametro id con la funzione
mysql_real_escape_string(), il database è a rischio di attacchi di tipo SQL injection.
Un altro grosso problema è che ogni singolo controllore deve includere il file model.php. Che fare se poi
occorresse includere un secondo file o eseguire un altro compito globale (p.e. garantire la sicurezza)? Nella
situazione attuale, tale codice dovrebbe essere aggiunto a ogni singolo file. Se lo si dimentica in un file, speriamo
che non sia qualcosa legato alla sicurezza.
Un “front controller” alla riscossa
La soluzione è usare un front controller: un singolo file PHP attraverso il quale tutte le richieste sono processate.
Con un front controller, gli URI dell’applicazione cambiano un poco, ma iniziano a diventare più flessibili:
Senza un front controller
/index.php
=> Pagina della lista dei post (index.php eseguito)
/show.php
=> Pagina che mostra il singolo post (show.php eseguito)
Con index.php come front controller
/index.php
=> Pagina della lista dei post (index.php eseguito)
/index.php/show
=> Pagina che mostra il singolo post (index.php eseguito)
Tip: La parte dell’URI index.php può essere rimossa se si usano le regole di riscrittura di Apache (o equivalente). In questo caso, l’URI risultante della pagina che mostra il post sarebbe semplicemente /show.
Usando un front controller, un singolo file PHP (index.php in questo caso) rende ogni richiesta. Per la pagina
che mostra il post, /index.php/show eseguirà in effetti il file index.php, che ora è responsabile per gestire
internamente le richieste, in base all’URI. Come vedremo, un front controller è uno strumento molto potente.
2.1. Libro
35
Symfony2 documentation Documentation, Release 2
Creazione del front controller
Stiamo per fare un grosso passo avanti con l’applicazione. Con un solo file a gestire tutte le richieste, possiamo
centralizzare cose come gestione della sicurezza, caricamento della configurazione, rotte. In questa applicazione,
index.php deve essere abbastanza intelligente da rendere la lista dei post oppure il singolo post, in base all’URI
richiesto:
<?php
// index.php
// carica e inizializza le librerie globali
require_once ’model.php’;
require_once ’controllers.php’;
// dirotta internamente la richiesta
$uri = $_SERVER[’REQUEST_URI’];
if ($uri == ’/index.php’) {
list_action();
} elseif ($uri == ’/index.php/show’ && isset($_GET[’id’])) {
show_action($_GET[’id’]);
} else {
header(’Status: 404 Not Found’);
echo ’<html><body><h1>Pagina non trovata</h1></body></html>’;
}
Per una migliore organizzazione, entrambi i controllori (precedentemente index.php e show.php) sono ora
funzioni PHP, entrambe spostate in un file separato, controllers.php:
function list_action()
{
$posts = get_all_posts();
require ’templates/list.php’;
}
function show_action($id)
{
$post = get_post_by_id($id);
require ’templates/show.php’;
}
Come front controller, index.php ha assunto un nuovo ruolo, che include il caricamento delle librerie principali e la gestione delle rotte dell’applicazione, in modo che sia richiamato uno dei due controllori (le funzioni
list_action() e show_action()). In realtà. il front controller inizia ad assomigliare molto al meccanismo con cui Symfony2 gestisce le richieste.
Tip: Un altro vantaggio di un front controller sono gli URL flessibili. Si noti che l’URL della pagina del singolo
post può essere cambiato da /show a /read solo cambiando un unico punto del codice. Prima, occorreva
rinominare un file. In Symfony2, gli URL sono ancora più flessibili.
Finora, l’applicazione si è evoluta da un singolo file PHP a una struttura organizzata e che consente il riuso del
codice. Dovremmo essere contenti, ma non ancora soddisfatti. Per esempio, il sistema delle rotte è instabile e
non riconosce che la pagina della lista (/index.php) dovrebbe essere accessibile anche tramite / (con le regole
di riscrittura di Apache). Inoltre, invece di sviluppare il blog, abbiamo speso diverso tempo sull“‘architettura”
del codice (p.e. rotte, richiamo dei controllori, template, ecc.). Ulteriore tempo sarebbe necessario per gestire
l’invio di form, la validazione dell’input, i log e la sicurezza. Perché dovremmo reinventare soluzioni a tutti questi
problemi comuni?
36
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
Aggiungere un tocco di Symfony2
Symfony2 alla riscossa! Prima di usare effettivamente Symfony2, occorre accertarsi che PHP sappia come trovare
le classi di Symfony2. Possiamo farlo grazie all’autoloader fornito da Symfony. Un autoloader è uno strumento
che rende possibile l’utilizzo di classi PHP senza includere esplicitamente il file che contiene la classe.
Primo, scaricare symfony e metterlo in una cartella vendor/symfony/.
Poi, creare un file
app/bootstrap.php. Usarlo per il require dei due file dell’applicazione e per configurare l’autoloader:
<?php
// bootstrap.php
require_once ’model.php’;
require_once ’controllers.php’;
require_once ’vendor/symfony/src/Symfony/Component/ClassLoader/UniversalClassLoader.php’;
$loader = new Symfony\Component\ClassLoader\UniversalClassLoader();
$loader->registerNamespaces(array(
’Symfony’ => __DIR__.’/../vendor/symfony/src’,
));
$loader->register();
Questo dice all’autoloader dove sono le classi Symfony. In questo modo, si può iniziare a usare le classi di
Symfony senza usare l’istruzione require per i file che le contengono.
Una delle idee principali della filosofia di Symfony è che il compito principale di un’applicazione
sia quello di interpretare ogni richiesta e restituire una risposta.
A tal fine, Symfony2 fornice sia una classe Symfony\Component\HttpFoundation\Request che una classe
Symfony\Component\HttpFoundation\Response. Queste classi sono rappresentazioni orientate
agli oggetti delle richieste grezze HTTP processate e delle risposte HTTP restituite. Usiamole per migliorare il
nostro blog:
<?php
// index.php
require_once ’app/bootstrap.php’;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
$request = Request::createFromGlobals();
$uri = $request->getPathInfo();
if ($uri == ’/’) {
$response = list_action();
} elseif ($uri == ’/show’ && $request->query->has(’id’)) {
$response = show_action($request->query->get(’id’));
} else {
$html = ’<html><body><h1>Pagina non trovata</h1></body></html>’;
$response = new Response($html, 404);
}
// mostra gli header e invia la risposta
$response->send();
I controllori sono ora responsabili di restituire un oggetto Response. Per rendere le cose più facili, si può
aggiungere una nuova funzione render_template(), che si comporta un po’ come il sistema di template di
Symfony2:
// controllers.php
use Symfony\Component\HttpFoundation\Response;
function list_action()
{
2.1. Libro
37
Symfony2 documentation Documentation, Release 2
$posts = get_all_posts();
$html = render_template(’templates/list.php’, array(’posts’ => $posts));
return new Response($html);
}
function show_action($id)
{
$post = get_post_by_id($id);
$html = render_template(’templates/show.php’, array(’post’ => $post));
return new Response($html);
}
// funzione helper per rendere i template
function render_template($path, array $args)
{
extract($args);
ob_start();
require $path;
$html = ob_get_clean();
return $html;
}
Prendendo una piccola parte di Symfony2, l’applicazione è diventata più flessibile e più affidabile. La classe
Request fornisce un modo di accedere alle informazioni sulla richiesta HTTP. Nello specifico, il metodo
getPathInfo() restituisce un URI più pulito (restituisce sempre /show e mai /index.php/show). In
questo modo, anche se l’utente va su /index.php/show, l’applicazione è abbastanza intelligente per dirottare
la richiesta a show_action().
L’oggetto Response dà flessibilità durante la costruzione della risposta HTTP, consentendo di aggiungere header
e contenuti HTTP tramite un’interfaccia orientata agli oggetti. Mentre in questa applicazione le risposte molto
semplici, tale flessibilità ripagherà quando l’applicazione cresce.
L’applicazione di esempio in Symfony2
Il blog ha fatto molta strada, ma contiene ancora troppo codice per un’applicazione così semplice. Durante
il cammino, abbiamo anche inventato un semplice sistema di rotte e un metodo che usa ob_start() e
ob_get_clean() per rendere i template. Se, per qualche ragione, si avesse bisogno di continuare a costruire questo “framework” da zero, si potrebbero almeno utilizzare i componenti Routing e Templating, che già
risolvono questi problemi.
Invece di risolvere nuovamente problemi comuni, si può lasciare a Symfony2 il compito di occuparsene. Ecco la
stessa applicazione di esempio, ora costruita in Symfony2:
<?php
// src/Acme/BlogBundle/Controller/BlogController.php
namespace Acme\BlogBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class BlogController extends Controller
{
public function listAction()
{
$posts = $this->get(’doctrine’)->getEntityManager()
->createQuery(’SELECT p FROM AcmeBlogBundle:Post p’)
->execute();
return $this->render(’AcmeBlogBundle:Blog:list.html.php’, array(’posts’ => $posts));
38
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
}
public function showAction($id)
{
$post = $this->get(’doctrine’)
->getEntityManager()
->getRepository(’AcmeBlogBundle:Post’)
->find($id);
if (!$post) {
// cause the 404 page not found to be displayed
throw $this->createNotFoundException();
}
return $this->render(’AcmeBlogBundle:Blog:show.html.php’, array(’post’ => $post));
}
}
I due controllori sono ancora leggeri. Ognuno usa la libreria ORM Doctrine per recuperare oggetti dal database e
il componente Templating per rendere un template e restituire un oggetto Response. Il template della lista è
ora un po’ più semplice:
<!-- src/Acme/BlogBundle/Resources/views/Blog/list.html.php -->
<?php $view->extend(’::layout.html.php’) ?>
<?php $view[’slots’]->set(’title’, ’List of Posts’) ?>
<h1>Lista dei post</h1>
<ul>
<?php foreach ($posts as $post): ?>
<li>
<a href="<?php echo $view[’router’]->generate(’blog_show’, array(’id’ => $post->getId()))
<?php echo $post->getTitle() ?>
</a>
</li>
<?php endforeach; ?>
</ul>
Il layout è quasi identico:
<!-- app/Resources/views/layout.html.php -->
<html>
<head>
<title><?php echo $view[’slots’]->output(’title’, ’Titolo predefinito’) ?></title>
</head>
<body>
<?php echo $view[’slots’]->output(’_content’) ?>
</body>
</html>
Note: Lasciamo il template di show come esercizio, visto che dovrebbe essere banale crearlo basandosi sul
template della lista.
Quando il motore di Symfony2 (chiamato Kernel) parte, ha bisogno di una mappa che gli consenta di sapere
quali controllori eseguire, in base alle informazioni della richiesta. Una configurazione delle rotte fornisce tali
informazioni in un formato leggibile:
# app/config/routing.yml
blog_list:
pattern: /blog
defaults: { _controller: AcmeBlogBundle:Blog:list }
2.1. Libro
39
Symfony2 documentation Documentation, Release 2
blog_show:
pattern: /blog/show/{id}
defaults: { _controller: AcmeBlogBundle:Blog:show }
Ora che Symfony2 gestisce tutti i compiti più comuni, il front controller è semplicissimo. E siccome fa così poco,
non si avrà mai bisogno di modificarlo una volta creato (e se si usa una distribuzione di Symfony2, non servirà
nemmeno crearlo!):
<?php
// web/app.php
require_once __DIR__.’/../app/bootstrap.php’;
require_once __DIR__.’/../app/AppKernel.php’;
use Symfony\Component\HttpFoundation\Request;
$kernel = new AppKernel(’prod’, false);
$kernel->handle(Request::createFromGlobals())->send();
L’unico compito del front controller è inizializzare il motore di Symfony2 (il Kernel) e passargli un oggetto
Request da gestire. Il nucleo di Symfony2 quindi usa la mappa delle rotte per determinare quale controllore
richiamare. Proprio come prima, il metodo controllore è responsabile di restituire l’oggetto Response finale.
Non resta molto altro da fare.
Per una rappresentazione visuale di come Symfony2 gestisca ogni richiesta, si veda il diagramma di flusso della
richiesta.
Dove consegna Symfony2
Nei capitoli successivi, impareremo di più su come funziona ogni pezzo di Symfony e sull’organizzazione raccomandata di un progetto. Per ora, vediamo come migrare il blog da PHP puro a Symfony2 ci abbia migliorato la
vita:
• L’applicazione ora ha un codice organizzato chiaramente e coerentemente (sebbene Symfony non obblighi a farlo). Questo promuove la riusabilità e consente a nuovi sviluppatori di essere produttivi nel
progetto in modo più rapido.
• Il 100% del codice che si scrive è per la propria applicazione. Non occorre sviluppare o mantenere utilità
a basso livello, come autoloading, routing o rendere i controllori.
• Symfony2 dà accesso a strumenti open source, come Doctrine e i componenti Templating, Security, Form,
Validation e Translation (solo per nominarne alcuni).
• L’applicazione ora gode di URL pienamente flessibili, grazie al componente Routing.
• L’architettura HTTP-centrica di Symfony2 dà accesso a strumenti potenti, come la cache HTTP fornita
dalla cache HTTP interna di Symfony2 o a strumenti ancora più potenti, come Varnish. Questi aspetti
sono coperti in un capitolo successivo, tutto dedicato alla cache.
Ma forse la parte migliore nell’usare Symfony2 è l’accesso all’intero insieme di strumenti open source di alta
qualità sviluppati dalla comunità di Symfony2! Si possono trovare dei buoni bundle su KnpBundles.com
Template migliori
Se lo si vuole usare, Symfony2 ha un motore di template predefinito, chiamato Twig, che rende i template più
veloci da scrivere e più facili da leggere. Questo vuol dire che l’applicazione di esempio può contenere ancora
meno codice! Prendiamo per esempio il template della lista, scritto in Twig:
{# src/Acme/BlogBundle/Resources/views/Blog/list.html.twig #}
{% extends "::layout.html.twig" %}
{% block title %}Lista dei post{% endblock %}
40
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
{% block body %}
<h1>Lista dei post</h1>
<ul>
{% for post in posts %}
<li>
<a href="{{ path(’blog_show’, { ’id’: post.id }) }}">
{{ post.title }}
</a>
</li>
{% endfor %}
</ul>
{% endblock %}
Il template corrispondente layout.html.twig è anche più facile da scrivere:
{# app/Resources/views/layout.html.twig #}
<html>
<head>
<title>{% block title %}Titolo predefinito{% endblock %}</title>
</head>
<body>
{% block body %}{% endblock %}
</body>
</html>
Twig è ben supportato in Symfony2. Pur essendo sempre supportati i template PHP, continueremo a discutere dei
molti vantaggi offerti da Twig. Per ulteriori informazioni, vedere il capitolo dei template.
Imparare di più con le ricette
• Come usare PHP al posto di Twig nei template
• Definire i controllori come servizi
2.1.3 Installare e configurare Symfony
Lo scopo di questo capitolo è quello di ottenere un’applicazione funzionante basata su Symfony. Fortunatamente,
Symfony offre delle “distribuzioni”, che sono progetti Symfony di partenza funzionanti, che possono essere scaricati per iniziare immediatamente a sviluppare.
Tip: Se si stanno cercando le istruzioni per creare un nuovo progetto e memorizzarlo con un sistema di versionamento, si veda Usare un controllo di sorgenti.
Scaricare una distribuzione Symfony2
Tip: Verificare innanzitutto di avere un server web (come Apache) installato e funzionante con PHP 5.3.2 o
successivi. Per ulteriori informazioni sui requisiti di Symfony2, si veda il riferimento sui requisiti.
Symfony2 ha dei pacchetti con delle “distribuzioni”, che sono applicazioni funzionanti che includono le librerie
del nucleo di Symfony2, una selezione di bundle utili e alcune configurazioni predefinite. Scaricando una distribuzione di Symfony2, si ottiene uno scheletro di un’applicazione funzionante, che può essere subito usata per
sviluppare la propria applicazione.
Si può iniziare visitando la pagina di scaricamento di Symfony2, http://symfony.com/download. Su questa pagina,
si vedrà la Symfony Standard Edition, che è la distribuzione principale di Symfony2. Si possono fare due scelte:
2.1. Libro
41
Symfony2 documentation Documentation, Release 2
• Scaricare l’archivio, .tgz o .zip sono equivalenti, si può scegliere quello che si preferisce;
• Scaricare la distribuzione con o senza venditori. Se si ha Git installato sulla propria macchina, si dovrebbe
scaricare Symfony2 senza venditori, perché aggiunge più flessibilità nell’inclusione delle librerie di terze
parti.
Si scarichi uno degli archivi e lo si scompatti da qualche parte sotto la cartella radice del web del proprio server.
Da una linea di comando UNIX, si può farlo con uno dei seguenti comandi (sostituire ### con il vero nome del
file):
# per il file .tgz
tar zxvf Symfony_Standard_Vendors_2.0.###.tgz
# per il file .zip
unzip Symfony_Standard_Vendors_2.0.###.zip
Finito il procedimento, si dovrebbe avere una cartella Symfony/, che assomiglia a questa:
www/ <- la propria cartella radice del web
Symfony/ <- l’archivio scompattato
app/
cache/
config/
logs/
src/
...
vendor/
...
web/
app.php
...
Aggiornare i venditori
Alla fine, se la scelta è caduta sull’archivio senza venditori, installare i venditori eseguendo dalla linea di comando
la seguente istruzione:
php bin/vendors install
Questo comando scarica tutte le librerie dei venditori necessarie, incluso Symfony stesso, nella cartella vendor/.
Per ulteriori informazioni sulla gestione delle librerie di venditori di terze parti in Symfony2, si veda “cookbookmanaging-vendor-libraries”.
Configurazione
A questo punto, tutte le librerie di terze parti che ci occorrono sono nella cartella vendor/. Abbiamo anche una
configurazione predefinita dell’applicazione in app/ e un po’ di codice di esempio in src/.
Symfony2 dispone di uno strumento visuale per la verifica della configurazione del server, per assicurarsi che il
server web e PHP siano configurati per usare Symfony2. Usare il seguente URL per la verifica della configurazione:
http://localhost/Symfony/web/config.php
Se ci sono problemi, correggerli prima di proseguire.
42
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
Impostare i permessi
Un problema comune è che le cartelle app/cache e app/logs devono essere scrivibili sia dal server web
che dall’utente della linea di comando. Su sistemi UNIX, se l’utente del server web è diverso da quello della
linea di comando, si possono eseguire i seguenti comandi una sola volta sul proprio progetto, per assicurarsi
che i permessi siano impostati correttamente. Cambiare www-data con l’utente del server web e tuonome
con l’utente della linea di comando:
1. Usare ACL su un sistema che supporta chmod +a
Molti sistemi consento di usare il comando chmod +a. Provare prima questo e, in caso di errore, provare
il metodo successivo:
rm -rf app/cache/*
rm -rf app/logs/*
sudo chmod +a "www-data allow delete,write,append,file_inherit,directory_inherit" app/cache app/l
sudo chmod +a "‘whoami‘ allow delete,write,append,file_inherit,directory_inherit" app/cache app/l
2. Usare ACL su un sistema che non supporta chmod +a
Alcuni sistemi non supportano chmod +a, ma supportano un altro programma chiamato setfacl. Si
potrebbe aver bisogno di abilitare il supporto ACL sulla propria partizione e installare setfacl prima di usarlo
(come nel caso di Ubuntu), in questo modo:
sudo setfacl -R -m u:www-data:rwx -m u:tuonome:rwx app/cache app/logs
sudo setfacl -dR -m u:www-data:rwx -m u:tuonome:rwx app/cache app/logs
3. Senza usare ACL
Se non è possibile modificare l’ACL delle cartelle, occorrerà modificare l’umask in modo che le cartelle
cache e log siano scrivibili dal gruppo o da tutti (a seconda che gli utenti di server web e linea di comando siano o meno nello stesso gruppo). Per poterlo fare, inserire la riga seguente all’inizio dei file
app/console, web/app.php e web/app_dev.php:
umask(0002); // Imposta i permessi a 0775
// oppure
umask(0000); // Imposta i permessi a 0777
Si noti che l’uso di ACL è raccomandato quando si ha accesso al server, perché la modifica di umask non è
thread-safe.
Quando tutto è a posto, cliccare su “Go to the Welcome page” per accedere alla prima “vera” pagina di Symfony2:
http://localhost/Symfony/web/app_dev.php/
Symfony2 dovrebbe dare il suo benvenuto e congratularsi per il lavoro svolto finora!
2.1. Libro
43
Symfony2 documentation Documentation, Release 2
Iniziare lo sviluppo
Ora che si dispone di un’applicazione Symfony2 pienamente funzionante, si può iniziare lo sviluppo. La distribuzione potrebbe contenere del codice di esempio, verificare il file README.rst incluso nella distribuzione
(aprendolo come file di testo) per sapere quale codice di esempio è incluso nella distribuzione scelta e come poterlo
rimuovere in un secondo momento.
Per chi è nuovo in Symfony, in “Creare pagine in Symfony2” si può imparare come creare pagine, cambiare
configurazioni e tutte le altre cose di cui si avrà bisogno nella nuova applicazione.
Usare un controllo di sorgenti
Se si usa un sistema di controllo di versioni, come Git o Subversion, lo si può impostare e iniziare a fare
commit nel proprio progetto, come si fa normalmente. Symfony Standard edition è il punto di partenza per il
nuovo progetto.
Per istruzioni specifiche su come impostare al meglio il proprio progetto per essere memorizzato in git, si veda
Come creare e memorizzare un progetto Symfony2 in git.
Ignorare la cartella vendor/
Chi ha scelto di scaricare l’archivio senza venditori può tranquillamente ignorare l’intera cartella vendor/ e non
inviarla in commit al controllo di sorgenti. Con Git, lo si può fare aggiungendo al file .gitignore la seguente
riga:
vendor/
Ora la cartella dei venditori non sarà inviata in commi al controllo di sorgenti. Questo è bene (anzi, benissimo!)
perché quando qualcun altro clonerà o farà checkout del progetto, potrà semplicemente eseguire lo script php
bin/vendors install per scaricare tutte le librerie dei venditori necessarie.
44
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
2.1.4 Creare pagine in Symfony2
La creazione di una nuova pagina in Symfony2 è un semplice processo in due passi:
• Creare una rotta: Una rotta definisce l’URL (p.e. /about) verso la pagina e specifica un controllore (che
è una funzione PHP) che Symfony2 dovrebbe eseguire quando l’URL della richiesta in arrivo corrisponde
allo schema della rotta;
• Creare un controllore: Un controllore è una funzione PHP che prende la richiesta in entrata e la trasforma
in un oggetto Response di Symfony2, che viene poi restituito all’utente.
Questo semplice approccio è molto bello, perché corrisponde al modo in cui funziona il web. Ogni interazione sul
web inizia con una richiesta HTTP. Il lavoro della propria applicazione è semplicemente quello di interpretare la
richiesta e restituire l’appropriata risposta HTTP.
Symfony2 segue questa filosofia e fornisce strumenti e convenzioni per mantenere la propria applicazione organizzata, man mano che cresce in utenti e in complessità.
Sembra abbastanza semplice? Approfondiamo!
La pagina “Ciao Symfony!”
Iniziamo con una variazione della classica applicazione “Ciao mondo!”. Quando avremo finito, l’utente sarà in
grado di ottenere un saluto personale (come “Ciao Symfony”) andando al seguente URL:
http://localhost/app_dev.php/hello/Symfony
In realtà, si potrà sostituire Symfony con qualsiasi altro nome da salutare. Per creare la pagina, seguiamo il
semplice processo in due passi.
Note: La guida presume che Symfony2 sia stato già scaricato e il server web configurato. L’URL precedente presume che localhost punti alla cartella web del proprio nuovo progetto Symfony2. Per informazioni dettagliate
su questo processo, si veda Installare Symfony2.
Prima di iniziare: creare il bundle
Prima di iniziare, occorrerà creare un bundle. In Symfony2, un bundle è come un plugin, tranne per il fatto che
tutto il codice nella propria applicazione starà dentro a un bundle.
Un bundle non è nulla di più di una cartella che ospita ogni cosa correlata a una specifica caratteristica, incluse
classi PHP, configurazioni e anche fogli di stile e file JavaScript (si veda Il sistema dei bundle).
Per creare un bundle chiamato AcmeHelloBundle (un bundle creato appositamente in questo capitolo), eseguire il seguente comando e seguire le istruzioni su schermo (usando tutte le opzioni predefinite):
php app/console generate:bundle --namespace=Acme/HelloBundle --format=yml
Dietro le quinte, viene creata una cartella per il bundle in src/Acme/HelloBundle. Inoltre viene aggiunta
automaticamente una riga al file app/AppKernel.php, in modo che il bundle sia registrato nel kernel:
// app/AppKernel.php
public function registerBundles()
{
$bundles = array(
// ...
new Acme\HelloBundle\AcmeHelloBundle(),
);
// ...
return $bundles;
}
2.1. Libro
45
Symfony2 documentation Documentation, Release 2
Ora che si è impostato il bundle, si può iniziare a costruire la propria applicazione, dentro il bundle stesso.
Passo 1: creare la rotta
Per impostazione predefinita, il file di configurazione delle rotte in un’applicazione Symfony2 si trova in
app/config/routing.yml. Come ogni configurazione in Symfony2, si può anche scegliere di usare XML
o PHP per configurare le rotte.
Se si guarda il file principale delle rotte, si vedrà che Symfony ha già aggiunto una voce, quando è stato generato
AcmeHelloBundle:
• YAML
# app/config/routing.yml
AcmeHelloBundle:
resource: "@AcmeHelloBundle/Resources/config/routing.yml"
prefix:
/
• XML
<!-- app/config/routing.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/r
<import resource="@AcmeHelloBundle/Resources/config/routing.xml" prefix="/" />
</routes>
• PHP
// app/config/routing.php
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;
$collection = new RouteCollection();
$collection->addCollection(
$loader->import(’@AcmeHelloBundle/Resources/config/routing.php’),
’/’,
);
return $collection;
Questa voce è molto basica: dice a Symfony2 di caricare la configurazione delle rotte dal file
Resources/config/routing.yml, che si trova dentro AcmeHelloBundle. Questo vuol dire che si
mette la configurazione delle rotte direttamente in app/config/routing.yml o si organizzano le proprie
rotte attraverso la propria applicazione, e le si importano da qui.
Ora che il file routing.yml del bundle è stato importato, aggiungere la nuova rotta, che definisce l’URL della
pagina che stiamo per creare:
• YAML
# src/Acme/HelloBundle/Resources/config/routing.yml
hello:
pattern: /hello/{name}
defaults: { _controller: AcmeHelloBundle:Hello:index }
• XML
<!-- src/Acme/HelloBundle/Resources/config/routing.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
46
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
<routes xmlns="http://symfony.com/schema/routing"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/r
<route id="hello" pattern="/hello/{name}">
<default key="_controller">AcmeHelloBundle:Hello:index</default>
</route>
</routes>
• PHP
// src/Acme/HelloBundle/Resources/config/routing.php
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;
$collection = new RouteCollection();
$collection->add(’hello’, new Route(’/hello/{name}’, array(
’_controller’ => ’AcmeHelloBundle:Hello:index’,
)));
return $collection;
Il routing consiste di due pezzi di base: lo schema (pattern), che è l’URL a cui la rotta corrisponderà, e un array
defaults, che specifica il controllore che sarà eseguito. La sintassi dei segnaposto nello schema ({name}) è
un jolly. Vuol dire che /hello/Ryan, /hello/Fabien o ogni altro URL simile corrisponderanno a questa
rotta. Il parametro del segnaposto {name} sarà anche passato al controllore, in modo da poter usare il suo valore
per salutare personalmente l’utente.
Note: Il sistema delle rotte ha molte altre importanti caratteristiche per creare strutture di URL flessibili e potenti
nella propria applicazioni. Per maggiori dettagli, si veda il capitolo dedicato alle Rotte.
Passo 2: creare il controllore
Quando un URL come /hello/Ryan viene gestita dall’applicazione, la rotta hello viene corrisposta e il
controllore AcmeHelloBundle:Hello:index eseguito dal framework. Il secondo passo del processo di
creazione della pagina è quello di creare tale controllore.
Il controllore ha il nome logico AcmeHelloBundle:Hello:index ed è mappato sul metodo indexAction
di una classe PHP chiamata Acme\HelloBundle\Controller\Hello. Iniziamo creando questo file dentro
il nostro AcmeHelloBundle:
// src/Acme/HelloBundle/Controller/HelloController.php
namespace Acme\HelloBundle\Controller;
use Symfony\Component\HttpFoundation\Response;
class HelloController
{
}
In realtà il controllore non è nulla di più di un metodo PHP, che va creato e che Symfony eseguirà. È qui che il
proprio codice usa l’informazione dalla richiesta per costruire e preparare la risorsa che è stata richiesta. Tranne per
alcuni casi avanzati, il prodotto finale di un controllore è sempre lo stesso: un oggetto Response di Symfony2.
Creare il metodo indexAction, che Symfony2 eseguirà quando la rotta hello sarà corrisposta:
// src/Acme/HelloBundle/Controller/HelloController.php
// ...
class HelloController
{
2.1. Libro
47
Symfony2 documentation Documentation, Release 2
public function indexAction($name)
{
return new Response(’<html><body>Ciao ’.$name.’!</body></html>’);
}
}
Il controllore è semplice: esso crea un nuovo oggetto Response, il cui primo parametro è il contenuto che sarà
usato dalla risposta (in questo esempio, una piccola pagina HTML).
Congratulazioni! Dopo aver creato solo una rotta e un controllore, abbiamo già una pagina pienamente funzionante! Se si è impostato tutto correttamente, la propria applicazione dovrebbe salutare:
http://localhost/app_dev.php/hello/Ryan
Tip: Si può anche vedere l’applicazione nell’ambiente “prod”, visitando:
http://localhost/app.php/hello/Ryan
Se si ottiene un errore, è probabilmente perché occorre pulire la cache, eseguendo:
php app/console cache:clear --env=prod --no-debug
Un terzo passo, facoltativo ma comune, del processo è quello di creare un template.
Note: I controllori sono il punto principale di ingresso del proprio codice e un ingrediente chiave della creazione
di pagine. Si possono trovare molte più informazioni nel Capitolo sul controllore.
Passo 3 (facoltativo): creare il template
I template consentono di spostare tutta la presentazione (p.e. il codice HTML) in un file separato e riusare diverse
porzioni del layout della pagina. Invece di scrivere il codice HTML dentro al controllore, meglio rendere un
template:
1
2
// src/Acme/HelloBundle/Controller/HelloController.php
namespace Acme\HelloBundle\Controller;
3
4
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
5
6
7
8
9
10
class HelloController extends Controller
{
public function indexAction($name)
{
return $this->render(’AcmeHelloBundle:Hello:index.html.twig’, array(’name’ => $name));
11
// render a PHP template instead
// return $this->render(’AcmeHelloBundle:Hello:index.html.php’, array(’name’ => $name));
12
13
}
14
15
}
Note:
Per poter usare il metodo render(), il controllore deve estendere la classe
Symfony\Bundle\FrameworkBundle\Controller\Controller
(documentazione
API:
Symfony\Bundle\FrameworkBundle\Controller\Controller), che aggiunge scorciatoie per
compiti comuni nei controllori. Ciò viene fatto nell’esempio precedente aggiungendo l’istruzione use alla riga 4
ed estendendo Controller alla riga 6.
Il metodo render() crea un oggetto Response riempito con il contenuto del template dato. Come ogni altro
controllore, alla fine l’oggetto Response viene restituito.
48
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
Si noti che ci sono due diversi esempi su come rendere il template. Per impostazione predefinita, Symfony2
supporta due diversi linguaggi di template: i classici template PHP e i template, concisi ma potenti, Twig. Non ci
si allarmi, si è liberi di scegliere tra i due, o anche tutti e due nello stesso progetto.
Il controllore rende il template AcmeHelloBundle:Hello:index.html.twig, che usa la seguente convenzioni dei nomi:
NomeBundle:NomeControllore:NomeTemplate
Questo è il nome logico del template, che è mappato su una locazione fisica, usando la seguente convenzione:
/percorso/di/NomeBundle/Resources/views/NomeControllore/NomeTemplate
In questo caso, AcmeHelloBundle è il nome del bundle, Hello è il controllore e index.html.twig il
template:
• Twig
1
2
{# src/Acme/HelloBundle/Resources/views/Hello/index.html.twig #}
{% extends ’::base.html.twig’ %}
3
4
5
6
{% block body %}
Ciao {{ name }}!
{% endblock %}
• PHP
<!-- src/Acme/HelloBundle/Resources/views/Hello/index.html.php -->
<?php $view->extend(’::base.html.php’) ?>
Ciao <?php echo $view->escape($name) ?>!
Analizziamo il template Twig riga per riga:
• riga 2: Il token extends definisce un template padre. Il template definisce esplicitamente un file di layout,
dentro il quale sarà inserito.
• riga 4: Il token block dice che ogni cosa al suo interno va posta dentro un blocco chiamato body. Come
vedremo, è responsabilità del template padre (base.html.twig) rendere alla fine il blocco chiamato
body.
Il template padre, ::base.html.twig, manca delle porzioni NomeBundle e NomeControllore del suo nome
(per questo ha il doppio duepunti (::) all’inizio). Questo vuol dire che il template risiede fuori dai bundle, nella
cartella app:
• Twig
{# app/Resources/views/base.html.twig #}
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>{% block title %}Benvenuto!{% endblock %}</title>
{% block stylesheets %}{% endblock %}
<link rel="shortcut icon" href="{{ asset(’favicon.ico’) }}" />
</head>
<body>
{% block body %}{% endblock %}
{% block javascripts %}{% endblock %}
</body>
</html>
• PHP
<!-- app/Resources/views/base.html.php -->
<!DOCTYPE html>
<html>
2.1. Libro
49
Symfony2 documentation Documentation, Release 2
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title><?php $view[’slots’]->output(’title’, ’Benvenuto!’) ?></title>
<?php $view[’slots’]->output(’stylesheets’) ?>
<link rel="shortcut icon" href="<?php echo $view[’assets’]->getUrl(’favicon.ico’) ?>"
</head>
<body>
<?php $view[’slots’]->output(’_content’) ?>
<?php $view[’slots’]->output(’stylesheets’) ?>
</body>
</html>
Il template di base definisce il layout HTML e rende il blocco body, che era stato definito nel template
index.html.twig. Rende anche un blocco title, che si può scegliere di definire nel template nel template
index.html.twig. Poiché non è stato definito il blocco title nel template figlio, il suo valore predefinito è
“Benvenuto!”.
I template sono un modo potente per rendere e organizzare il contenuto della propria pagina. Un template può
rendere qualsiasi cosa, dal codice HTML al CSS, o ogni altra cosa che il controllore abbia bisogno di restituire.
Nel ciclo di vita della gestione di una richiesta, il motore dei template è solo uno strumento opzionale. Si ricordi
che lo scopo di ogni controllore è quello di restituire un oggetto Response. I template sono uno strumento
potente, ma facoltativo, per creare il contenuto per un oggetto Response.
Struttura delle cartelle
Dopo solo poche sezioni, si inizia già a capire la filosofia che sta dietro alla creazione e alla resa delle pagine in
Symfony2. Abbiamo anche già iniziato a vedere come i progetti Symfony2 siano strutturati e organizzati. Alla
fine di questa sezione, sapremo dove cercare e inserire i vari tipi di file, e perché.
Sebbene interamente flessibili, per impostazione predefinita, ogni application Symfony ha la stessa struttura di
cartelle raccomandata:
• app/: Questa cartella contiene la configurazione dell’applicazione;
• src/: Tutto il codice PHP del progetto sta all’interno di questa cartella;
• vendor/: Ogni libreria dei venditori è inserita qui, per convenzione;
• web/: Questa è la cartella radice del web e contiene ogni file accessibile pubblicamente;
La cartella web
La cartella radice del web è la casa di tutti i file pubblici e statici, inclusi immagini, fogli di stile, file JavaScript.
È anche li posto in cui stanno tutti i front controller:
// web/app.php
require_once __DIR__.’/../app/bootstrap.php.cache’;
require_once __DIR__.’/../app/AppKernel.php’;
use Symfony\Component\HttpFoundation\Request;
$kernel = new AppKernel(’prod’, false);
$kernel->loadClassCache();
$kernel->handle(Request::createFromGlobals())->send();
Il file del front controller (app.php in questo esempio) è il file PHP che viene eseguito quando si usa
un’applicazione Symfony2 e il suo compito è quello di usare una classe kernel, AppKernel, per inizializzare
l’applicazione.
Tip: Aver un front controller vuol dire avere URL diverse e più flessibili rispetto a una tipica applicazione in
puro PHP. Quando si usa un front controller, gli URL sono formattati nel modo seguente:
50
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
http://localhost/app.php/hello/Ryan
Il front controller, app.php, viene eseguito e l’URL “interno” /hello/Ryan è dirottato internamente, usando
la configurazione delle rotte. Usando mod_rewrite di Apache, si può forzare l’esecuzione del file app.php
senza bisogno di specificarlo nell’URL:
http://localhost/hello/Ryan
Sebbene i front controller siano essenziali nella gestione di ogni richiesta, raramente si avrà bisogno di modificarli
o anche di pensarci. Saranno brevemente menzionati ancora nella sezione Ambienti.
La cartella dell’applicazione (app)
Come visto nel front controller, la classe AppKernel è il punto di ingresso principale dell’applicazione ed è
responsabile di tutta la configurazione. Per questo è memorizzata nella cartella app/.
Questa classe deve implementare due metodi, che definiscono tutto ciò di cui Symfony ha bisogno di sapere sulla
propria applicazione. Non ci si deve preoccupare di questi metodi all’inizio, Symfony li riempe al posto nostro
con delle impostazioni predefinite.
• registerBundles(): Restituisce un array di tutti bundle necessari per eseguire l’applicazione (vedere
Il sistema dei bundle);
• registerContainerConfiguration():
Carica il file della
dell’applicazione (vedere la sezione Configurazione dell’applicazione).
configurazione
principale
Nello sviluppo quotidiano, per lo più si userà la cartella app/ per modificare i file di configurazione e delle
rotte nella cartella app/config/ (vedere Configurazione dell’applicazione). Essa contiene anche la cartella
della cache dell’applicazione (app/cache), la cartella dei log (app/logs) e la cartella dei file risorsa a livello
di applicazione, come i template (app/Resources). Ognuna di queste cartella sarà approfondita nei capitoli
successivi.
Autoload
Quando Symfony si carica, un file speciale chiamato app/autoload.php viene incluso. Questo file è
responsabile di configurare l’autoloader, che auto-caricherà i file dell’applicazione dalla cartella src/ e le
librerie di terze parti dalla cartella vendor/.
Grazie all’autoloader, non si avrà mai bisogno di usare le istruzioni include o require. Al posto loro,
Symfony2 usa lo spazio dei nomi di una classe per determinare la sua posizione e includere automaticamente
il file al posto nostro, nel momento in cui la classe è necessaria.
L’autoloader è già configurato per cercare nella cartella src/ tutte le proprie classi PHP. Per poterlo far
funzionare, il nome della classe e quello del file devono seguire lo stesso schema:
Nome della classe:
Acme\HelloBundle\Controller\HelloController
Percorso:
src/Acme/HelloBundle/Controller/HelloController.php
Tipicamente, l’unica volta in cui si avrà bisogno di preoccuparsi del file app/autoload.php sarà al
momento di includere nuove librerie di terze parti nella cartella vendor/. Per maggiori informazioni
sull’autoload, vedere Come auto-caricare le classi.
La cartella dei sorgenti (src)
Detto semplicemente, la cartella src/ contiene tutto il codice (codice PHP, template, file di configurazione, fogli
di stile, ecc.) che guida la propria applicazione. Quando si sviluppa, la gran parte del proprio lavoro sarà svolto
dentro uno o più bundle creati in questa cartella.
Ma cos’è esattamente un bundle?
2.1. Libro
51
Symfony2 documentation Documentation, Release 2
Il sistema dei bundle
Un bundle è simile a un plugin in altri software, ma anche meglio. La differenza fondamentale è che tutto è un
bundle in Symfony2, incluse le funzionalità fondamentali del framework o il codice scritto per la propria applicazione. I bundle sono cittadini di prima classe in Symfony2. Questo fornisce la flessibilità di usare caratteristiche
già pronte impacchettate in bundle di terze parti o di distribuire i propri bundle. Rende facile scegliere quali
caratteristiche abilitare nella propria applicazione per ottimizzarla nel modo preferito.
Note: Pur trovando qui i fondamentali, un’intera ricetta è dedicata all’organizzazione e alle pratiche migliori in
bundle.
Un bundle è semplicemente un insieme strutturato di file dentro una cartella, che implementa una singola caratteristica. Si potrebbe creare un BlogBundle, un ForumBundle o un bundle per la gestione degli utenti (molti di
questi già esistono come bundle open source). Ogni cartella contiene tutto ciò che è relativo a quella caratteristica,
inclusi file PHP, template, fogli di stile, JavaScript, test e tutto il resto. Ogni aspetto di una caratteristica esiste in
un bundle e ogni caratteristica risiede in un bundle.
Un’applicazione è composta di bundle, come definito nel metodo registerBundles() della classe
AppKernel:
// app/AppKernel.php
public function registerBundles()
{
$bundles = array(
new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
new Symfony\Bundle\SecurityBundle\SecurityBundle(),
new Symfony\Bundle\TwigBundle\TwigBundle(),
new Symfony\Bundle\MonologBundle\MonologBundle(),
new Symfony\Bundle\SwiftmailerBundle\SwiftmailerBundle(),
new Symfony\Bundle\DoctrineBundle\DoctrineBundle(),
new Symfony\Bundle\AsseticBundle\AsseticBundle(),
new Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle(),
new JMS\SecurityExtraBundle\JMSSecurityExtraBundle(),
);
if (in_array($this->getEnvironment(), array(’dev’, ’test’))) {
$bundles[] = new Acme\DemoBundle\AcmeDemoBundle();
$bundles[] = new Symfony\Bundle\WebProfilerBundle\WebProfilerBundle();
$bundles[] = new Sensio\Bundle\DistributionBundle\SensioDistributionBundle();
$bundles[] = new Sensio\Bundle\GeneratorBundle\SensioGeneratorBundle();
}
return $bundles;
}
Col metodo registerBundles(), si ha il controllo totale su quali bundle siano usati dalla propria applicazione
(inclusi i bundle del nucleo di Symfony).
Tip: Un bundle può stare ovunque, purché possa essere auto-caricato (tramite l’autoloader configurato in
app/autoload.php).
Creare un bundle
Symfony Standard Edition contiene un task utile per creare un bundle pienamente funzionante. Ma anche creare
un bundle a mano è molto facile.
Per dimostrare quanto è semplice il sistema dei bundle, creiamo un nuovo bundle, chiamato AcmeTestBundle,
e abilitiamolo.
52
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
Tip: La parte Acme è solo un nome fittizio, che andrebbe sostituito da un nome di “venditore” che rappresenti la
propria organizzazione (p.e. ABCTestBundle per un’azienda chiamata ABC).
Iniziamo creando una cartella src/Acme/TestBundle/ e aggiungendo un nuovo file chiamato
AcmeTestBundle.php:
// src/Acme/TestBundle/AcmeTestBundle.php
namespace Acme\TestBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class AcmeTestBundle extends Bundle
{
}
Tip: Il nome AcmeTestBundle segue le convenzioni sui nomi dei bundle. Si potrebbe anche scegliere di
accorciare il nome del bundle semplicemente a TestBundle, chiamando la classe TestBundle (e chiamando
il file TestBundle.php).
Questa classe vuota è l’unico pezzo necessario a creare un nuovo bundle. Sebbene solitamente vuota, questa classe
è potente e può essere usata per personalizzare il comportamento del bundle.
Ora che abbiamo creato il bundle, abilitiamolo tramite la classe AppKernel:
// app/AppKernel.php
public function registerBundles()
{
$bundles = array(
// ...
// register your bundles
new Acme\TestBundle\AcmeTestBundle(),
);
// ...
return $bundles;
}
Sebbene non faccia ancora nulla, AcmeTestBundle è ora pronto per essere usato.
Symfony fornisce anche un’interfaccia a linea di comando per generare uno scheletro di base per un bundle:
php app/console generate:bundle --namespace=Acme/TestBundle
Lo scheletro del bundle è generato con controllore, template e rotte, tutti personalizzabili. Approfondiremo più
avanti la linea di comando di Symfony2.
Tip: Ogni volta che si crea un nuovo bundle o che si usa un bundle di terze parti, assicurarsi sempre che il bundle
sia abilitato in registerBundles(). Se si usa il comando generate:bundle, l’abilitazione è automatica.
Struttura delle cartelle dei bundle
La struttura delle cartelle di un bundle è semplice e flessibile. Per impostazione predefinita, il sistema dei bundle
segue un insieme di convenzioni, che aiutano a mantenere il codice coerente tra tutti i bundle di Symfony2. Si dia
un’occhiata a AcmeHelloBundle, perché contiene alcuni degli elementi più comuni di un bundle:
• Controller/ contiene i controllori del (p.e. HelloController.php);
• Resources/config/ ospita la configurazione, compresa la configurazione delle rotte (p.e.
routing.yml);
2.1. Libro
53
Symfony2 documentation Documentation, Release 2
• Resources/views/ contiene
Hello/index.html.twig);
i
template,
organizzati
per
nome
di
controllore
(p.e.
• Resources/public/ contiene le risorse per il web (immagini, fogli di stile, ecc.) ed è copiata o collegata simbolicamente alla cartella web/ del progetto, tramite il comando assets:install;
• Tests/ contiene tutti i test del bundle.
Un bundle può essere grande o piccolo, come la caratteristica che implementa. Contiene solo i file che occorrono
e niente altro.
Andando avanti nel libro, si imparerà come persistere gli oggetti in un database, creare e validare form, creare
traduzioni per la propria applicazione, scrivere test e molto altro. Ognuno di questi ha il suo posto e il suo ruolo
dentro il bundle.
Configurazione dell’applicazione
Un’applicazione è composta da un insieme di bundle, che rappresentano tutte le caratteristiche e le capacità
dell’applicazione stessa. Ogni bundle può essere personalizzato tramite file di configurazione, scritti in YAML,
XML o PHP. Per impostazione predefinita, il file di configurazione principale risiede nella cartella app/config/
è si chiama config.yml, config.xml o config.php, a seconda del formato scelto:
• YAML
# app/config/config.yml
imports:
- { resource: parameters.yml }
- { resource: security.yml }
framework:
secret:
charset:
router:
# ...
%secret%
UTF-8
{ resource: "%kernel.root_dir%/config/routing.yml" }
# Configurazione di Twig
twig:
debug:
%kernel.debug%
strict_variables: %kernel.debug%
# ...
• XML
<!-- app/config/config.xml -->
<imports>
<import resource="parameters.yml" />
<import resource="security.yml" />
</imports>
<framework:config charset="UTF-8" secret="%secret%">
<framework:router resource="%kernel.root_dir%/config/routing.xml" />
<!-- ... -->
</framework:config>
<!-- Configurazione di Twig -->
<twig:config debug="%kernel.debug%" strict-variables="%kernel.debug%" />
<!-- ... -->
• PHP
$this->import(’parameters.yml’);
$this->import(’security.yml’);
54
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
$container->loadFromExtension(’framework’, array(
’secret’
=> ’%secret%’,
’charset’
=> ’UTF-8’,
’router’
=> array(’resource’ => ’%kernel.root_dir%/config/routing.php’),
// ...
),
));
// Configurazione di Twig
$container->loadFromExtension(’twig’, array(
’debug’
=> ’%kernel.debug%’,
’strict_variables’ => ’%kernel.debug%’,
));
// ...
Note: Vedremo esattamente come caricare ogni formato di file nella prossima sezione, Ambienti.
Ogni voce di primo livello, come framework o twig, definisce la configurazione per un particolare bundle. Per esempio, la voce framework definisce la configurazione per il bundle del nucleo di Symfony
FrameworkBundle e include configurazioni per rotte, template e altri sistemi fondamentali.
Per ora, non ci preoccupiamo delle opzioni di configurazione specifiche di ogni sezione. Il file di configurazione ha
delle opzioni predefinite impostate. Leggendo ed esplorando ogni parte di Symfony2, le opzioni di configurazione
specifiche saranno man mano approfondite.
Formati di configurazione
Nei vari capitoli, tutti gli esempi di configurazione saranno mostrati in tutti e tre i formati (YAML, XML e
PHP). Ciascuno ha i suoi vantaggi e svantaggi. La scelta è lasciata allo sviluppatore:
• YAML: Semplice, pulito e leggibile;
• XML: Più potente di YAML e supportato nell’autocompletamento dagli IDE;
• PHP: Molto potente, ma meno leggibile dei formati di configurazione standard.
Esportazione della configurazione predefinita
New in version 2.1: Il comando config:dump-reference è stato aggiunto in Symfony 2.1 Si
può esportare la configurazione predefinita per un bundle in yaml sulla console, usando il comando
config:dump-reference. Ecco un esempio di esportazione della configurazione predefinita di FrameworkBundle:
app/console config:dump-reference FrameworkBundle
Note: Vedere la ricetta Come esporrre una configurazione semantica per un bundle per informazioni sull’aggiunta
di configurazioni per il proprio bundle.
Ambienti
Un’applicazione può girare in vari ambienti. I diversi ambienti condividono lo stesso codice PHP (tranne per il
front controller), ma usano differenti configurazioni. Per esempio, un ambiente dev salverà nei log gli avvertimenti e gli errori, mentre un ambiente prod solamente gli errori. Alcuni file sono ricostruiti a ogni richiesta
nell’ambiente dev (per facilitare gli sviluppatori=, ma salvati in cache nell’ambiente prod. Tutti gli ambienti
stanno insieme nella stessa macchina e sono eseguiti nella stessa applicazione.
2.1. Libro
55
Symfony2 documentation Documentation, Release 2
Un progetto Symfony2 generalmente inizia con tre ambienti (dev, test e prod), ma creare nuovi ambienti è
facile. Si può vedere la propria applicazione in ambienti diversi, semplicemente cambiando il front controller nel
proprio browser. Per vedere l’applicazione in ambiente dev, accedere all’applicazione tramite il front controller
di sviluppo:
http://localhost/app_dev.php/hello/Ryan
Se si preferisce vedere come l’applicazione si comporta in ambiente di produzione, richiamare invece il front
controller prod:
http://localhost/app.php/hello/Ryan
Essendo l’ambiente prod ottimizzato per la velocità, la configurazione, le rotte e i template Twig sono compilato
in classi in puro PHP e messi in cache. Per vedere delle modifiche in ambiente prod, occorrerà pulire tali file in
cache e consentire che siano ricostruiti:
php app/console cache:clear --env=prod --no-debug
Note: Se si apre il file web/app.php, si troverà che è configurato esplicitamente per usare l’ambiente prod:
$kernel = new AppKernel(’prod’, false);
Si può creare un nuovo front controller per un nuovo ambiente, copiando questo file e cambiando prod con un
altro valore.
Note: L’ambiente test è usato quando si eseguono i test automatici e non può essere acceduto direttamente
tramite il browser. Vedere il capitolo sui test per maggiori dettagli.
Configurazione degli ambienti
La classe AppKernel è responsabile del caricare effettivamente i file di conigurazione scelti:
// app/AppKernel.php
public function registerContainerConfiguration(LoaderInterface $loader)
{
$loader->load(__DIR__.’/config/config_’.$this->getEnvironment().’.yml’);
}
Sappiamo già che l’estensione .yml può essere cambiata in .xml o .php, se si preferisce usare XML o PHP
per scrivere la propria configurazione. Si noti anche che ogni ambiente carica i propri file di configurazione.
Consideriamo il file di configurazione per l’ambiente dev.
• YAML
# app/config/config_dev.yml
imports:
- { resource: config.yml }
framework:
router:
{ resource: "%kernel.root_dir%/config/routing_dev.yml" }
profiler: { only_exceptions: false }
# ...
• XML
<!-- app/config/config_dev.xml -->
<imports>
<import resource="config.xml" />
</imports>
56
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
<framework:config>
<framework:router resource="%kernel.root_dir%/config/routing_dev.xml" />
<framework:profiler only-exceptions="false" />
</framework:config>
<!-- ... -->
• PHP
// app/config/config_dev.php
$loader->import(’config.php’);
$container->loadFromExtension(’framework’, array(
’router’
=> array(’resource’ => ’%kernel.root_dir%/config/routing_dev.php’),
’profiler’ => array(’only-exceptions’ => false),
));
// ...
La voce imports è simile all’istruzione include di PHP e garantisce che il file di configurazione principale
(config.yml) sia caricato per primo. Il resto del file gestisce la configurazione per aumentare il livello di log,
oltre ad altre impostazioni utili all’ambiente di sviluppo.
Sia l’ambiente prod che quello test seguono lo stesso modello: ogni ambiente importa il file di configurazione
di base e quindi modifica i suoi file di configurazione per soddisfare le esigenze dello specifico ambiente. Questa
è solo una convenzione, ma consente di riusare la maggior parte della propria configurazione e personalizzare solo
le parti diverse tra gli ambienti.
Riepilogo
Congratulazioni! Ora abbiamo visto ogni aspetto fondamentale di Symfony2 e scoperto quanto possa essere facile
e flessibile. Pur essendoci ancora moltissime caratteristiche da scoprire, assicuriamoci di tenere a mente alcuni
aspetti fondamentali:
• creare una pagine è un processo in tre passi, che coinvolge una rotta, un controllore e (opzionalmente) un
template.
• ogni progetto contienre solo alcune cartelle principali: web/ (risorse web e front controller), app/ (configurazione), src/ (i propri bundle) e vendor/ (codice di terze parti) (c’è anche la cartella bin/, usata
per aiutare nell’aggiornamento delle librerire dei venditori);
• ogni caratteristica in Symfony2 (incluso in nucleo del framework stesso) è organizzata in bundle, insiemi
strutturati di file relativi a tale caratteristica;
• la configurazione per ciascun bundle risiede nella cartella app/config e può essere specificata in YAML,
XML o PHP;
• ogni ambiente è accessibile tramite un diverso front controller (p.e. app.php e app_dev.php) e carica
un diverso file di configurazione.
Da qui in poi, ogni capitolo introdurrà strumenti sempre più potenti e concetti sempre più avanzati. Più si imparerà
su Symfony2, più si apprezzerà la flessibilità della sua architettura e la potenza che dà nello sviluppo rapido di
applicazioni.
2.1.5 Il controllore
Un controllore è una funzione PHP da creare, che prende le informazioni dalla richiesta HTTP e dai costruttori e
restituisce una risposta HTTP (come oggetto Response di Symfony2). La risposta potrebbe essere una pagina
HTML, un documento XML, un array serializzato JSON, una immagine, un rinvio, un errore 404 o qualsiasi altra
cosa possa venire in mente. Il controllore contiene una qualunque logica arbitraria di cui la propria applicazione
necessita per rendere il contenuto di una pagina.
2.1. Libro
57
Symfony2 documentation Documentation, Release 2
Per vedere quanto questo è semplice, diamo un’occhiata a un controllore di Symfony2 in azione. Il seguente
controllore renderebbe una pagina che stampa semplicemente Ciao mondo!:
use Symfony\Component\HttpFoundation\Response;
public function helloAction()
{
return new Response(’Ciao mondo!’);
}
L’obiettivo di un controllore è sempre lo stesso: creare e restituire un oggetto Response. Lungo il percorso,
potrebbe leggere le informazioni dalla richiesta, caricare una risorsa da un database, inviare un’email, o impostare
informazioni sulla sessione dell’utente. Ma in ogni caso, il controllore alla fine restituirà un oggetto Response
che verrà restituito al client.
Non c’è nessuna magia e nessun altro requisito di cui preoccuparsi! Di seguito alcuni esempi comuni:
• Il controllore A prepara un oggetto Response che rappresenta il contenuto della homepage di un sito.
• Il controllore B legge il parametro slug da una richiesta per caricare un blog da un database e creare un
oggetto Response che visualizza quel blog. Se lo slug non viene trovato nel database, crea e restituisce
un oggetto Response con codice di stato 404.
• Il controllore C gestisce l’invio di un form contatti. Legge le informazioni del form dalla richiesta, salva le
informazioni del contatto nella base dati e invia una email con le informazioni del contatto al webmaster.
Infine, crea un oggetto Response, che rinvia il browser del client alla pagina di ringraziamento del form
contatti.
Richieste, controllori, ciclo di vita della risposta
Ogni richiesta gestita da un progetto Symfony2 passa attraverso lo stesso semplice ciclo di vita. Il framework si
occupa dei compiti ripetitivi ed infine esegue un controllore, che ospita il codice personalizzato dell’applicazione:
1. Ogni richiesta è gestita da un singolo file con il controllore principale (ad esempio app.php o
app_dev.php) che inizializza l’applicazione;
2. Il Router legge le informazioni dalla richiesta (ad esempio l’URI), trova una rotta che corrisponde a tali
informazioni e legge il parametro _controller dalla rotta;
3. Viene eseguito il controllore della rotta corrispondente e il codice all’interno del controllore crea e restituisce
un oggetto Response;
4. Le intestazioni HTTP e il contenuto dell’oggetto Response vengono rispedite al client.
Creare una pagina è facile, basta creare un controllore (#3) e fare una rotta che mappa un URL su un controllore
(#2).
Note: Anche se ha un nome simile, il “controllore principale” (front controller) è diverso dagli altri “controllori”
di cui si parla in questo capitolo. Un controllore principale è un breve file PHP che è presente nella propria cartella
web e sul quale sono dirette tutte le richieste. Una tipica applicazione avrà un controllore principale di produzione
(ad esempio app.php) e un controllore principale per lo sviluppo (ad esempio app_dev.php). Probabilmente
non si avrà mai bisogno di modificare, visualizzare o preoccuparsi del controllore principale dell’applicazione.
Un semplice controllore
Mentre un controllore può essere un qualsiasi callable PHP (una funzione, un metodo di un oggetto, o una
Closure), in Symfony2, un controllore di solito è un unico metodo all’interno di un oggetto controllore. I
controllori sono anche chiamati azioni.
58
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
1
// src/Acme/HelloBundle/Controller/HelloController.php
2
3
4
namespace Acme\HelloBundle\Controller;
use Symfony\Component\HttpFoundation\Response;
5
6
7
8
9
10
11
12
class HelloController
{
public function indexAction($name)
{
return new Response(’<html><body>Ciao ’.$name.’!</body></html>’);
}
}
Tip: Si noti che il controllore è il metodo indexAction, che si trova all’interno di una classe controllore
(HelloController). Non bisogna confondersi con i nomi: una classe controllore è semplicemente un modo
comodo per raggruppare insieme vari controllori/azioni. Tipicamente, la classe controllore ospiterà diversi controllori/azioni (ad esempio updateAction, deleteAction, ecc).
Questo controllore è piuttosto semplice, ma vediamo di analizzarlo:
• linea 3: Symfony2 sfrutta la funzionalità degli spazi dei nomi di PHP 5.3 per utilizzarla nell’intera classe
dei controllori. La parola chiave use importa la classe Response, che il controllore deve restituire.
• linea 6: Il nome della classe è la concatenazione di un nome per la classe controllore (ad esempio Hello)
e la parola Controller. Questa è una convenzione che fornisce coerenza ai controllori e permette loro di
essere referenziati solo dalla prima parte del nome (ad esempio Hello) nella configurazione delle rotte.
• linea 8: A ogni azione in una classe controllore viene aggiunto il suffisso Action mentre nella configurazione delle rotte viene utilizzato come riferimento il solo nome dell’azione (index). Nella sezione
successiva, verrà creata una rotta che mappa un URI in questa azione. Si imparerà come i segnaposto delle
rotte ({name}) diventano parametri del metodo dell’azione ($name).
• linea 10: Il controllore crea e restituisce un oggetto Response.
Mappare un URL in un controllore
Il nuovo controllore restituisce una semplice pagina HTML. Per visualizzare questa pagina nel browser, è necessario creare una rotta che mappa uno specifico schema URL nel controllore:
• YAML
# app/config/routing.yml
hello:
pattern:
/hello/{name}
defaults:
{ _controller: AcmeHelloBundle:Hello:index }
• XML
<!-- app/config/routing.xml -->
<route id="hello" pattern="/hello/{name}">
<default key="_controller">AcmeHelloBundle:Hello:index</default>
</route>
• PHP
// app/config/routing.php
$collection->add(’hello’, new Route(’/hello/{name}’, array(
’_controller’ => ’AcmeHelloBundle:Hello:index’,
)));
2.1. Libro
59
Symfony2 documentation Documentation, Release 2
Andando in /hello/ryan ora viene eseguito il controllore HelloController::indexAction() e viene
passato ryan nella variabile $name. Creare una “pagina” significa semplicemente creare un metodo controllore
e associargli una rotta.
Si noti la sintassi utilizzata per fare riferimento al controllore: AcmeHelloBundle:Hello:index. Symfony2 utilizza una notazione flessibile per le stringhe per fare riferimento a diversi controllori. Questa è la sintassi
più comune e dice a Symfony2 di cercare una classe controllore chiamata HelloController dentro un bundle
chiamato AcmeHelloBundle. Il metodo indexAction() viene quindi eseguito.
Per maggiori dettagli sul formato stringa utilizzato per fare riferimento ai diversi controllori, vedere Schema per
il nome dei controllori.
Note: Questo esempio pone la configurazione delle rotte direttamente nella cartella app/config/. Un modo
migliore per organizzare le proprie rotte è quello di posizionare ogni rotta nel bundle a cui appartiene. Per ulteriori
informazioni, si veda Includere risorse esterne per le rotte.
Tip: Si può imparare molto di più sul sistema delle rotte leggendo il capitolo sulle rotte.
I parametri delle rotte come parametri del controllore
Si è già appreso che il parametro AcmeHelloBundle:Hello:index di _controller fa riferimento a un
metodo HelloController::indexAction() che si trova all’interno di un bundle AcmeHelloBundle.
La cosa più interessante è che i parametri vengono passati a tale metodo:
<?php
// src/Acme/HelloBundle/Controller/HelloController.php
namespace Acme\HelloBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class HelloController extends Controller
{
public function indexAction($name)
{
// ...
}
}
Il controllore ha un solo parametro, $name, che corrisponde al parametro {name} della rotta corrispondente
(ryan nel nostro esempio). Infatti, quando viene eseguito il controllore, Symfony2 verifica ogni parametro del
controllore con un parametro della rotta abbinata. Vedere il seguente esempio:
• YAML
# app/config/routing.yml
hello:
pattern:
/hello/{first_name}/{last_name}
defaults:
{ _controller: AcmeHelloBundle:Hello:index, color: green }
• XML
<!-- app/config/routing.xml -->
<route id="hello" pattern="/hello/{first_name}/{last_name}">
<default key="_controller">AcmeHelloBundle:Hello:index</default>
<default key="color">green</default>
</route>
• PHP
60
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
// app/config/routing.php
$collection->add(’hello’, new Route(’/hello/{first_name}/{last_name}’, array(
’_controller’ => ’AcmeHelloBundle:Hello:index’,
’color’
=> ’green’,
)));
Per questo il controllore può richiedere diversi parametri:
public function indexAction($first_name, $last_name, $color)
{
// ...
}
Si noti che entrambe le variabili segnaposto ({first_name}, {last_name}), così come la variabile predefinita color, sono disponibili come parametri nel controllore. Quando una rotta viene abbinata, le variabili
segnaposto vengono unite con le impostazioni predefinite per creare un array che è disponibile al controllore.
La mappatura dei parametri delle rotte nei parametri del controllore è semplice e flessibile. Tenere in mente le
seguenti linee guida mentre si sviluppa.
• L’ordine dei parametri del controllore non ha importanza
Symfony è in grado di abbinare i nomi dei parametri delle rotte e i nomi delle variabili dei
metodi dei controllori. In altre parole, vuol dire che il parametro {last_name} corrisponde
al parametro $last_name. I parametri del controllore possono essere totalmente riordinati e
continuare a funzionare perfettamente:
public function indexAction($last_name, $color, $first_name)
{
// ..
}
• Ogni parametro richiesto del controllore, deve corrispondere a uno dei parametri della rotta
Il codice seguente genererebbe un RuntimeException, perché non c’è nessun parametro
foo definito nella rotta:
public function indexAction($first_name, $last_name, $color, $foo)
{
// ..
}
Rendere l’parametro facoltativo metterebbe le cose a posto. Il seguente esempio non lancerebbe
un’eccezione:
public function indexAction($first_name, $last_name, $color, $foo = ’bar’)
{
// ..
}
• Non tutti i parametri delle rotte devono essere parametri del controllore
Se, per esempio, last_name non è importante per il controllore, si può ometterlo del tutto:
public function indexAction($first_name, $color)
{
// ..
}
Tip: Ogni rotta ha anche un parametro speciale _route, che è equivalente al nome della rotta che è stata
abbinata (ad esempio hello). Anche se di solito non è utile, questa è ugualmente disponibile come parametro
del controllore.
2.1. Libro
61
Symfony2 documentation Documentation, Release 2
La Request come parametro del controllore
Per comodità, è anche possibile far passare a Symfony l’oggetto Request come parametro al controllore. È
particolarmente utile quando si lavora con i form, ad esempio:
use Symfony\Component\HttpFoundation\Request;
public function updateAction(Request $request)
{
$form = $this->createForm(...);
$form->bindRequest($request);
// ...
}
La classe base del controllore
Per comodità, Symfony2 ha una classe base Controller che aiuta nelle attività più comuni del controllore e
dà alla classe controllore l’accesso a qualsiasi risorsa che potrebbe essere necessaria. Estendendo questa classe
Controller, è possibile usufruire di numerosi metodi helper.
Aggiungere la dichiarazione use sopra alla classe Controller e modificare HelloController per estenderla:
// src/Acme/HelloBundle/Controller/HelloController.php
namespace Acme\HelloBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Response;
class HelloController extends Controller
{
public function indexAction($name)
{
return new Response(’<html><body>Hello ’.$name.’!</body></html>’);
}
}
Questo in realtà non cambia nulla su come lavora il controllore. Nella prossima sezione, si imparerà a
conoscere i metodi helper che rende disponibili la classe base del controllore. Questi metodi sono solo scorciatoie per usare funzionalità del nucleo di Symfony2 che sono a disposizione con o senza la classe base di
Controller. Un ottimo modo per vedere le funzionalità del nucleo in azione è quello di guardare nella classe
Symfony\Bundle\FrameworkBundle\Controller\Controller stessa.
Tip: Estendere la classe base è opzionale in Symfony; essa contiene utili scorciatoie ma niente di obbligatorio. È inoltre possibile estendere Symfony\Component\DependencyInjection\ContainerAware.
L’oggetto service container sarà quindi accessibile tramite la proprietà container.
Note: È inoltre possibile definire i Controllori come servizi.
Attività comuni del controllore
Anche se un controllore può fare praticamente qualsiasi cosa, la maggior parte dei controllori eseguiranno gli
stessi compiti di base più volte. Questi compiti, come il rinvio, l’inoltro, il rendere i template e l’accesso ai servizi
del nucleo, sono molto semplici da gestire con Symfony2.
62
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
Rinvio
Se si vuole rinviare l’utente a un’altra pagina, usare il metodo redirect():
public function indexAction()
{
return $this->redirect($this->generateUrl(’homepage’));
}
Il metodo generateUrl() è solo una funzione di supporto che genera l’URL per una determinata rotta. Per
maggiori informazioni, vedere il capitolo Rotte.
Per impostazione predefinita, il metodo redirect() esegue un rinvio 302 (temporaneo). Per eseguire un rinvio
301 (permanente), modificare il secondo parametro:
public function indexAction()
{
return $this->redirect($this->generateUrl(’homepage’), 301);
}
Tip: Il metodo redirect() è semplicemente una scorciatoia che crea un oggetto Response specializzato
nel rinviare l’utente. È equivalente a:
use Symfony\Component\HttpFoundation\RedirectResponse;
return new RedirectResponse($this->generateUrl(’homepage’));
Inoltro
Si può anche facilmente inoltrare internamente a un altro controllore con il metodo forward(). Invece di
redirigere il browser dell’utente, fa una sotto richiesta interna e chiama il controllore specificato. Il metodo
forward() restituisce l’oggetto Response che è tornato da quel controllore:
public function
{
$response =
’name’
’color’
));
indexAction($name)
$this->forward(’AcmeHelloBundle:Hello:fancy’, array(
=> $name,
=> ’green’
// modifica ulteriormente la risposta o ritorna direttamente
return $response;
}
Si noti che il metodo forward() utilizza la stessa rappresentazione stringa del controllore utilizzato nella configurazione delle rotte. In questo caso, l’obiettivo della classe del controllore sarà HelloController all’interno
di un qualche AcmeHelloBundle. L’array passato al metodo diventa un insieme di parametri sul controllore
risultante. La stessa interfaccia viene utilizzata quando si incorporano controllori nei template (vedere Inserire
controllori). L’obiettivo del metodo controllore dovrebbe essere simile al seguente:
public function fancyAction($name, $color)
{
// ... creare e restituire un oggetto Response
}
E proprio come quando si crea un controllore per una rotta, l’ordine dei parametri di fancyAction non è
importante. Symfony2 controlla i nomi degli indici chiave (ad esempio name) con i nomi dei parametri del
metodo (ad esempio $name). Se si modifica l’ordine dei parametri, Symfony2 continuerà a passare il corretto
valore di ogni variabile.
2.1. Libro
63
Symfony2 documentation Documentation, Release 2
Tip: Come per gli altri metodi base di Controller, il metodo forward è solo una scorciatoia per funzionalità
del nucleo di Symfony2. Un inoltro può essere eseguito direttamente attraverso il servizio http_kernel. Un
inoltro restituisce un oggetto Response:
$httpKernel
$response =
’name’
’color’
));
= $this->container->get(’http_kernel’);
$httpKernel->forward(’AcmeHelloBundle:Hello:fancy’, array(
=> $name,
=> ’green’,
Rendere i template
Sebbene non sia un requisito, la maggior parte dei controllori alla fine rendono un template che è responsabile di
generare il codice HTML (o un altro formato) per il controllore. Il metodo renderView() rende un template e
restituisce il suo contenuto. Il contenuto di un template può essere usato per creare un oggetto Response:
$content = $this->renderView(’AcmeHelloBundle:Hello:index.html.twig’, array(’name’ => $name));
return new Response($content);
Questo può anche essere fatto in un solo passaggio con il metodo render(), che restituisce un oggetto
Response contenente il contenuto di un template:
return $this->render(’AcmeHelloBundle:Hello:index.html.twig’, array(’name’ => $name));
In entrambi i casi, verrà reso il template Resources/views/Hello/index.html.twig presente
all’interno di AcmeHelloBundle.
Il motore per i template di Symfony è spiegato in dettaglio nel capitolo Template.
Tip: Il metodo renderView è una scorciatoia per utilizzare direttamente il servizio templating. Il servizio
templating può anche essere utilizzato in modo diretto:
$templating = $this->get(’templating’);
$content = $templating->render(’AcmeHelloBundle:Hello:index.html.twig’, array(’name’ => $name));
Accesso ad altri servizi
Quando si estende la classe base del controllore, è possibile accedere a qualsiasi servizio di Symfony2 attraverso
il metodo get(). Di seguito si elencano alcuni servizi comuni che potrebbero essere utili:
$request = $this->getRequest();
$templating = $this->get(’templating’);
$router = $this->get(’router’);
$mailer = $this->get(’mailer’);
Ci sono innumerevoli altri servizi disponibili e si incoraggia a definirne di propri. Per elencare tutti i servizi
disponibili, utilizzare il comando di console container:debug:
php app/console container:debug
Per maggiori informazioni, vedere il capitolo Contenitore di servizi.
64
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
Gestire gli errori e le pagine 404
Quando qualcosa non si trova, si dovrebbe utilizzare bene il protocollo HTTP e restituire una risposta 404. Per
fare questo, si lancia uno speciale tipo di eccezione. Se si sta estendendo la classe base del controllore, procedere
come segue:
public function indexAction()
{
$product = // recuperare l’oggetto dal database
if (!$product) {
throw $this->createNotFoundException(’Il prodotto non esiste’);
}
return $this->render(...);
}
Il metodo createNotFoundException() crea uno speciale oggetto NotFoundHttpException, che in
ultima analisi innesca una risposta HTTP 404 all’interno di Symfony.
Naturalmente si è liberi di lanciare qualunque classe Exception nel controllore - Symfony2 ritornerà automaticamente un codice di risposta HTTP 500.
throw new \Exception(’Qualcosa è andato storto!’);
In ogni caso, all’utente finale viene mostrata una pagina di errore predefinita e allo sviluppatore viene mostrata
una pagina di errore completa di debug (quando si visualizza la pagina in modalità debug). Entrambe le pagine
di errore possono essere personalizzate. Per ulteriori informazioni, leggere nel ricettario “Come personalizzare le
pagine di errore”.
Gestione della sessione
Symfony2 fornisce un oggetto sessione che si può utilizzare per memorizzare le informazioni sull’utente (che sia
una persona reale che utilizza un browser, un bot, o un servizio web) attraverso le richieste. Per impostazione
predefinita, Symfony2 memorizza gli attributi in un cookie utilizzando le sessioni PHP native.
Memorizzare e recuperare informazioni dalla sessione può essere fatto da qualsiasi controllore:
$session = $this->getRequest()->getSession();
// memorizza un attributo per riutilizzarlo durante una successiva richiesta dell’utente
$session->set(’foo’, ’bar’);
// in un altro controllore per un’altra richiesta
$foo = $session->get(’foo’);
// imposta il locale dell’utente
$session->setLocale(’fr’);
Questi attributi rimarranno sull’utente per il resto della sessione utente.
Messaggi flash
È anche possibile memorizzare messaggi di piccole dimensioni che vengono memorizzati sulla sessione utente
solo per una richiesta successiva. Ciò è utile quando si elabora un form: si desidera rinviare e avere un messaggio
speciale mostrato sulla richiesta successiva. Questo tipo di messaggi sono chiamati messaggi “flash”.
Per esempio, immaginiamo che si stia elaborando un form inviato:
public function updateAction()
{
$form = $this->createForm(...);
2.1. Libro
65
Symfony2 documentation Documentation, Release 2
$form->bindRequest($this->getRequest());
if ($form->isValid()) {
// fare una qualche elaborazione
$this->get(’session’)->setFlash(’notice’, ’Le modifiche sono state salvate!’);
return $this->redirect($this->generateUrl(...));
}
return $this->render(...);
}
Dopo l’elaborazione della richiesta, il controllore imposta un messaggio flash notice e poi rinvia. Il nome
(notice) non è significativo, è solo quello che si utilizza per identificare il tipo del messaggio.
Nel template dell’azione successiva, il seguente codice può essere utilizzato per rendere il messaggio notice:
• Twig
{% if app.session.hasFlash(’notice’) %}
<div class="flash-notice">
{{ app.session.flash(’notice’) }}
</div>
{% endif %}
• PHP
<?php if ($view[’session’]->hasFlash(’notice’)): ?>
<div class="flash-notice">
<?php echo $view[’session’]->getFlash(’notice’) ?>
</div>
<?php endif; ?>
Per come sono stati progettati, i messaggi flash sono destinati a vivere esattamente per una richiesta (hanno la
“durata di un flash”). Sono progettati per essere utilizzati in redirect esattamente come è stato fatto in questo
esempio.
L’oggetto Response
L’unico requisito per un controllore è restituire un oggetto Response.
La classe
Symfony\Component\HttpFoundation\Response è una astrazione PHP sulla risposta HTTP - il
messaggio testuale che contiene gli header HTTP e il contenuto che viene inviato al client:
// crea una semplice risposta con un codice di stato 200 (il predefinito)
$response = new Response(’Hello ’.$name, 200);
// crea una risposta JSON con un codice di stato 200
$response = new Response(json_encode(array(’name’ => $name)));
$response->headers->set(’Content-Type’, ’application/json’);
Tip: La proprietà headers è un oggetto Symfony\Component\HttpFoundation\HeaderBag con
alcuni utili metodi per leggere e modificare gli header Response. I nomi degli header sono normalizzati in
modo che l’utilizzo di Content-Type sia equivalente a content-type o anche a content_type.
L’oggetto Request
Oltre ai valori dei segnaposto delle rotte, il controllore ha anche accesso all’oggetto Request quando si estende
la classe base Controller:
66
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
$request = $this->getRequest();
$request->isXmlHttpRequest(); // è una richiesta Ajax?
$request->getPreferredLanguage(array(’en’, ’fr’));
$request->query->get(’page’); // recupera un parametro $_GET
$request->request->get(’page’); // recupera un parametro $_POST
Come l’oggetto Response, le intestazioni della richiesta sono memorizzate in un oggetto HeaderBag e sono
facilmente accessibili.
Considerazioni finali
Ogni volta che si crea una pagina, è necessario scrivere del codice che contiene la logica per quella pagina. In
Symfony, questo codice si chiama controllore, ed è una funzione PHP che può fare qualsiasi cosa di cui ha bisogno
per tornare l’oggetto finale Response che verrà restituito all’utente.
Per rendere la vita più facile, si può scegliere di estendere una classe base Controller, che contiene metodi
scorciatoia per molti compiti comuni del controllore. Per esempio, dal momento che non si vuole mettere il
codice HTML nel controllore, è possibile utilizzare il metodo render() per rendere e restituire il contenuto da
un template.
In altri capitoli, si vedrà come il controllore può essere usato per persistere e recuperare oggetti da un database,
processare i form inviati, gestire la cache e altro ancora.
Imparare di più dal ricettario
• Come personalizzare le pagine di errore
• Definire i controllori come servizi
2.1.6 Le rotte
URL ben realizzati sono una cosa assolutamente da avere per qualsiasi applicazione web seria. Questo
significa lasciarsi alle spalle URL del tipo index.php?article_id=57 in favore di qualcosa come
/read/intro-to-symfony.
Avere flessibilità è ancora più importante. Che cosa succede se è necessario modificare l’URL di una pagina da
/blog a /news? Quanti collegamenti bisogna cercare e aggiornare per realizzare la modifica? Se si stanno
utilizzando le rotte di Symfony la modifica è semplice.
Le rotte di Symfony2 consentono di definire URL creativi che possono essere mappati in differenti aree
dell’applicazione. Entro la fine del capitolo, si sarà in grado di:
• Creare rotte complesse che mappano i controllori
• Generare URL all’interno di template e controllori
• Caricare le risorse delle rotte dai bundle (o da altre parti)
• Eseguire il debug delle rotte
Le rotte in azione
Una rotta è una mappatura tra uno schema di URL e un controllore. Per esempio, supponiamo che si voglia
gestire un qualsiasi URL tipo /blog/my-post o /blog/all-about-symfony e inviarlo a un controllore
che cerchi e visualizzi quel post del blog. La rotta è semplice:
2.1. Libro
67
Symfony2 documentation Documentation, Release 2
• YAML
# app/config/routing.yml
blog_show:
pattern:
/blog/{slug}
defaults: { _controller: AcmeBlogBundle:Blog:show }
• XML
<!-- app/config/routing.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/r
<route id="blog_show" pattern="/blog/{slug}">
<default key="_controller">AcmeBlogBundle:Blog:show</default>
</route>
</routes>
• PHP
// app/config/routing.php
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;
$collection = new RouteCollection();
$collection->add(’blog_show’, new Route(’/blog/{slug}’, array(
’_controller’ => ’AcmeBlogBundle:Blog:show’,
)));
return $collection;
Lo schema definito dalla rotta blog_show si comporta come /blog/*, dove al carattere jolly viene dato il
nome slug. Per l’URL /blog/my-blog-post, la variabile slug ottiene il valore my-blog-post, che è
disponibile per l’utilizzo nel controllore (proseguire nella lettura).
Il parametro _controller è una chiave speciale che dice a Symfony quale controllore dovrebbe essere eseguito
quando un URL corrisponde a questa rotta. La stringa _controller è detta nome logico. Segue un pattern che
punta a un specifico classe e metodo PHP:
// src/Acme/BlogBundle/Controller/BlogController.php
namespace Acme\BlogBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class BlogController extends Controller
{
public function showAction($slug)
{
$blog = // usare la variabile $slug per interrogare il database
return $this->render(’AcmeBlogBundle:Blog:show.html.twig’, array(
’blog’ => $blog,
));
}
}
Congratulazioni! Si è appena creata la prima rotta, collegandola ad un controllore. Ora, quando si visita
/blog/my-post, verrà eseguito il controllore showAction e la variabile $slug avrà valore my-post.
Questo è l’obiettivo delle rotte di Symfony2: mappare l’URL di una richiesta in un controllore. Lungo la strada,
si impareranno tutti i trucchi per mappare facilmente anche gli URL più complessi.
68
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
Le rotte: funzionamento interno
Quando all’applicazione viene fatta una richiesta, questa contiene un indirizzo alla esatta “risorsa” che il client
sta richiedendo. Questo indirizzo è chiamato URL, (o URI) e potrebbe essere /contact, /blog/read-me, o
qualunque altra cosa. Prendere ad esempio la seguente richiesta HTTP:
GET /blog/my-blog-post
L’obiettivo del sistema delle rotte di Symfony2 è quello di analizzare questo URL e determinare quale controller
dovrebbe essere eseguito. L’intero processo è il seguente:
1. La richiesta è gestita dal front controller di Symfony2 (ad esempio app.php);
2. Il nucleo di Symfony2 (ad es. il kernel) chiede al router di ispezionare la richiesta;
3. Il router verifica la corrispondenza dell’URL in arrivo con una specifica rotta e restituisce informazioni sulla
rotta, tra le quali il controllore che deve essere eseguito;
4. Il kernel di Symfony2 esegue il controllore, che alla fine restituisce un oggetto Response.
Figure 2.2: Lo strato delle rotte è uno strumento che traduce l’URL in ingresso in uno specifico controllore da
eseguire.
Creazione delle rotte
Symfony carica tutte le rotte per l’applicazione da un singolo file con la configurazione delle rotte. Il file generalmente è app/config/routing.yml, ma può essere configurato per essere qualunque cosa (compreso un file
XML o PHP) tramite il file di configurazione dell’applicazione:
• YAML
# app/config/config.yml
framework:
# ...
router:
{ resource: "%kernel.root_dir%/config/routing.yml" }
• XML
<!-- app/config/config.xml -->
<framework:config ...>
<!-- ... -->
<framework:router resource="%kernel.root_dir%/config/routing.xml" />
</framework:config>
• PHP
2.1. Libro
69
Symfony2 documentation Documentation, Release 2
// app/config/config.php
$container->loadFromExtension(’framework’, array(
// ...
’router’
=> array(’resource’ => ’%kernel.root_dir%/config/routing.php’),
));
Tip: Anche se tutte le rotte sono caricate da un singolo file, è una pratica comune includere ulteriori risorse di
rotte all’interno del file. Vedere la sezione Includere risorse esterne per le rotte per maggiori informazioni.
Configurazione di base delle rotte
Definire una rotta è semplice e una tipica applicazione avrà molte rotte. Una rotta di base è costituita da due parti:
il pattern da confrontare e un array defaults:
• YAML
_welcome:
pattern:
defaults:
/
{ _controller: AcmeDemoBundle:Main:homepage }
• XML
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/r
<route id="_welcome" pattern="/">
<default key="_controller">AcmeDemoBundle:Main:homepage</default>
</route>
</routes>
• PHP
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;
$collection = new RouteCollection();
$collection->add(’_welcome’, new Route(’/’, array(
’_controller’ => ’AcmeDemoBundle:Main:homepage’,
)));
return $collection;
Questa rotta corrisponde alla homepage (/) e la mappa nel controllore AcmeDemoBundle:Main:homepage.
La stringa _controller è tradotta da Symfony2 in una funzione PHP effettiva, ed eseguita. Questo processo
verrà spiegato a breve nella sezione Schema per il nome dei controllori.
Rotte con segnaposti
Naturalmente il sistema delle rotte supporta rotte molto più interessanti. Molte rotte conterranno uno o più segnaposto “jolly”:
• YAML
blog_show:
pattern:
defaults:
70
/blog/{slug}
{ _controller: AcmeBlogBundle:Blog:show }
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
• XML
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/r
<route id="blog_show" pattern="/blog/{slug}">
<default key="_controller">AcmeBlogBundle:Blog:show</default>
</route>
</routes>
• PHP
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;
$collection = new RouteCollection();
$collection->add(’blog_show’, new Route(’/blog/{slug}’, array(
’_controller’ => ’AcmeBlogBundle:Blog:show’,
)));
return $collection;
Lo schema verrà soddisfatto da qualsiasi cosa del tipo /blog/*. Meglio ancora, il valore corrispondente il segnaposto {slug} sarà disponibile all’interno del controllore. In altre parole, se l’URL è /blog/hello-world,
una variabile $slug, con un valore hello-world, sarà disponibile nel controllore. Questo può essere usato,
ad esempio, per caricare il post sul blog che verifica questa stringa.
Tuttavia lo schema non deve corrispondere semplicemente a /blog. Questo perché, per impostazione predefinita,
tutti i segnaposto sono obbligatori. Questo comportamento può essere cambiato aggiungendo un valore segnaposto
all’array defaults.
Segnaposto obbligatori e opzionali
Per rendere le cose più eccitanti, aggiungere una nuova rotta che visualizza un elenco di tutti i post disponibili del
blog per questa applicazione immaginaria di blog:
• YAML
blog:
pattern:
defaults:
/blog
{ _controller: AcmeBlogBundle:Blog:index }
• XML
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/r
<route id="blog" pattern="/blog">
<default key="_controller">AcmeBlogBundle:Blog:index</default>
</route>
</routes>
• PHP
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;
$collection = new RouteCollection();
2.1. Libro
71
Symfony2 documentation Documentation, Release 2
$collection->add(’blog’, new Route(’/blog’, array(
’_controller’ => ’AcmeBlogBundle:Blog:index’,
)));
return $collection;
Finora, questa rotta è il più semplice possibile: non contiene segnaposto e corrisponde solo all’esatto URL /blog.
Ma cosa succede se si ha bisogno di questa rotta per supportare l’impaginazione, dove /blog/2 visualizza la
seconda pagina dell’elenco post del blog? Bisogna aggiornare la rotta per avere un nuovo segnaposto {page}:
• YAML
blog:
pattern:
defaults:
/blog/{page}
{ _controller: AcmeBlogBundle:Blog:index }
• XML
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/r
<route id="blog" pattern="/blog/{page}">
<default key="_controller">AcmeBlogBundle:Blog:index</default>
</route>
</routes>
• PHP
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;
$collection = new RouteCollection();
$collection->add(’blog’, new Route(’/blog/{page}’, array(
’_controller’ => ’AcmeBlogBundle:Blog:index’,
)));
return $collection;
Come il precedente segnaposto {slug}, il valore che verifica {page} sarà disponibile all’interno del controllore. Il suo valore può essere usato per determinare quale insieme di post del blog devono essere visualizzati per
una data pagina.
Un attimo però! Dal momento che i segnaposto per impostazione predefinita sono obbligatori, questa rotta non
avrà più corrispondenza con il semplice /blog. Invece, per vedere la pagina 1 del blog, si avrà bisogno di
utilizzare l’URL /blog/1! Dal momento che non c’è soluzione per una complessa applicazione web, modificare
la rotta per rendere il parametro {page} opzionale. Questo si fa includendolo nella collezione defaults:
• YAML
blog:
pattern:
defaults:
/blog/{page}
{ _controller: AcmeBlogBundle:Blog:index, page: 1 }
• XML
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/r
<route id="blog" pattern="/blog/{page}">
72
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
<default key="_controller">AcmeBlogBundle:Blog:index</default>
<default key="page">1</default>
</route>
</routes>
• PHP
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;
$collection = new RouteCollection();
$collection->add(’blog’, new Route(’/blog/{page}’, array(
’_controller’ => ’AcmeBlogBundle:Blog:index’,
’page’ => 1,
)));
return $collection;
Aggiungendo page alla chiave defaults, il segnaposto {page} non è più obbligatorio. L’URL /blog
corrisponderà a questa rotta e il valore del parametro page verrà impostato a 1. Anche l’URL /blog/2 avrà
corrispondenza, dando al parametro page il valore 2. Perfetto.
/blog
/blog/1
/blog/2
{page} = 1
{page} = 1
{page} = 2
Aggiungere requisiti
Si dia uno sguardo veloce alle rotte che sono state create finora:
• YAML
blog:
pattern:
defaults:
/blog/{page}
{ _controller: AcmeBlogBundle:Blog:index, page: 1 }
blog_show:
pattern:
defaults:
/blog/{slug}
{ _controller: AcmeBlogBundle:Blog:show }
• XML
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/r
<route id="blog" pattern="/blog/{page}">
<default key="_controller">AcmeBlogBundle:Blog:index</default>
<default key="page">1</default>
</route>
<route id="blog_show" pattern="/blog/{slug}">
<default key="_controller">AcmeBlogBundle:Blog:show</default>
</route>
</routes>
• PHP
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;
2.1. Libro
73
Symfony2 documentation Documentation, Release 2
$collection = new RouteCollection();
$collection->add(’blog’, new Route(’/blog/{page}’, array(
’_controller’ => ’AcmeBlogBundle:Blog:index’,
’page’ => 1,
)));
$collection->add(’blog_show’, new Route(’/blog/{show}’, array(
’_controller’ => ’AcmeBlogBundle:Blog:show’,
)));
return $collection;
Si riesce a individuare il problema? Notare che entrambe le rotte hanno schemi che verificano URL del tipo
/blog/*. Il router di Symfony sceglie sempre la prima rotta corrispondente che trova. In altre parole, la rotta
blog_show non sarà mai trovata. Invece, un URL del tipo /blog/my-blog-post verrà abbinato alla prima
rotta (blog) restituendo il valore senza senso my-blog-post per il parametro {page}.
URL
/blog/2
/blog/my-blog-post
rotta
blog
blog
paramettri
{page} = 2
{page} = my-blog-post
La risposta al problema è aggiungere rotte obbligatorie. Le rotte in questo esempio potrebbero funzionare perfettamente se lo schema /blog/{page} fosse verificato solo per gli URL dove {page} fosse un numero intero.
Fortunatamente, i requisiti possono essere scritti tramite espressioni regolari e aggiunti per ogni parametro. Per
esempio:
• YAML
blog:
pattern:
/blog/{page}
defaults: { _controller: AcmeBlogBundle:Blog:index, page: 1 }
requirements:
page: \d+
• XML
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/r
<route id="blog" pattern="/blog/{page}">
<default key="_controller">AcmeBlogBundle:Blog:index</default>
<default key="page">1</default>
<requirement key="page">\d+</requirement>
</route>
</routes>
• PHP
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;
$collection = new RouteCollection();
$collection->add(’blog’, new Route(’/blog/{page}’, array(
’_controller’ => ’AcmeBlogBundle:Blog:index’,
’page’ => 1,
), array(
’page’ => ’\d+’,
)));
return $collection;
74
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
Il requisito \d+ è una espressione regolare che dice che il valore del parametro {page} deve essere una cifra
(cioè un numero). La rotta blog sarà comunque abbinata a un URL del tipo /blog/2 (perché 2 è un numero),
ma non sarà più abbinata a un URL tipo /blog/my-blog-post (perché my-blog-post non è un numero).
Come risultato, un URL tipo /blog/my-blog-post ora verrà correttamente abbinato alla rotta blog_show.
URL
/blog/2
/blog/my-blog-post
rotta
blog
blog_show
paramettri
{page} = 2
{slug} = my-blog-post
Vincono sempre le rotte che compaiono prima
Il significato di tutto questo è che l’ordine delle rotte è molto importante. Se la rotta blog_show fosse
stata collocata sopra la rotta blog, l’URL /blog/2 sarebbe stato abbinato a blog_show invece di blog
perché il parametro {slug} di blog_show non ha requisiti. Utilizzando l’ordinamento appropriato e dei
requisiti intelligenti, si può realizzare qualsiasi cosa.
Poiché i requisiti dei parametri sono espressioni regolari, la complessità e la flessibilità di ogni requisito dipende
da come li si scrive. Si supponga che la pagina iniziale dell’applicazione sia disponibile in due diverse lingue, in
base all’URL:
• YAML
homepage:
pattern:
/{culture}
defaults: { _controller: AcmeDemoBundle:Main:homepage, culture: en }
requirements:
culture: en|fr
• XML
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/r
<route id="homepage" pattern="/{culture}">
<default key="_controller">AcmeDemoBundle:Main:homepage</default>
<default key="culture">en</default>
<requirement key="culture">en|fr</requirement>
</route>
</routes>
• PHP
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;
$collection = new RouteCollection();
$collection->add(’homepage’, new Route(’/{culture}’, array(
’_controller’ => ’AcmeDemoBundle:Main:homepage’,
’culture’ => ’en’,
), array(
’culture’ => ’en|fr’,
)));
return $collection;
Per le richieste in entrata, la porzione {culture} dell’URL viene controllata tramite l’espressione regolare
(en|fr).
2.1. Libro
75
Symfony2 documentation Documentation, Release 2
/
/en
/fr
/es
{culture} = en
{culture} = en
{culture} = fr
non si abbina a questa rotta
Aggiungere requisiti al metodo HTTP
In aggiunta agli URL, si può anche verificare il metodo della richiesta entrante (ad esempio GET, HEAD, POST,
PUT, DELETE). Si supponga di avere un form contatti con due controllori: uno per visualizzare il form (su una
richiesta GET) e uno per l’elaborazione del form dopo che è stato inviato (su una richiesta POST). Questo può
essere realizzato con la seguente configurazione per le rotte:
• YAML
contact:
pattern: /contact
defaults: { _controller: AcmeDemoBundle:Main:contact }
requirements:
_method: GET
contact_process:
pattern: /contact
defaults: { _controller: AcmeDemoBundle:Main:contactProcess }
requirements:
_method: POST
• XML
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/r
<route id="contact" pattern="/contact">
<default key="_controller">AcmeDemoBundle:Main:contact</default>
<requirement key="_method">GET</requirement>
</route>
<route id="contact_process" pattern="/contact">
<default key="_controller">AcmeDemoBundle:Main:contactProcess</default>
<requirement key="_method">POST</requirement>
</route>
</routes>
• PHP
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;
$collection = new RouteCollection();
$collection->add(’contact’, new Route(’/contact’, array(
’_controller’ => ’AcmeDemoBundle:Main:contact’,
), array(
’_method’ => ’GET’,
)));
$collection->add(’contact_process’, new Route(’/contact’, array(
’_controller’ => ’AcmeDemoBundle:Main:contactProcess’,
), array(
’_method’ => ’POST’,
)));
76
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
return $collection;
Nonostante il fatto che queste due rotte abbiano schemi identici (/contact), la prima rotta corrisponderà solo a
richieste GET e la seconda rotta corrisponderà solo a richieste POST. Questo significa che è possibile visualizzare
il form e invia e inviarlo utilizzando lo stesso URL ma controllori distinti per le due azioni.
Note: Se non viene specificato nessun requisito _method, la rotta verrà abbinata con tutti i metodi.
Come avviene per gli altri requisiti, il requisito _method viene analizzato come una espressione regolare. Per
abbinare le richieste GET o POST, si può utilizzare GET|POST.
Esempio di rotte avanzate
A questo punto, si ha tutto il necessario per creare una complessa struttura di rotte in Symfony. Quello che segue
è un esempio di quanto flessibile può essere il sistema delle rotte:
• YAML
article_show:
pattern: /articles/{culture}/{year}/{title}.{_format}
defaults: { _controller: AcmeDemoBundle:Article:show, _format: html }
requirements:
culture: en|fr
_format: html|rss
year:
\d+
• XML
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/r
<route id="article_show" pattern="/articles/{culture}/{year}/{title}.{_format}">
<default key="_controller">AcmeDemoBundle:Article:show</default>
<default key="_format">html</default>
<requirement key="culture">en|fr</requirement>
<requirement key="_format">html|rss</requirement>
<requirement key="year">\d+</requirement>
</route>
</routes>
• PHP
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;
$collection = new RouteCollection();
$collection->add(’homepage’, new Route(’/articles/{culture}/{year}/{title}.{_format}’, array(
’_controller’ => ’AcmeDemoBundle:Article:show’,
’_format’ => ’html’,
), array(
’culture’ => ’en|fr’,
’_format’ => ’html|rss’,
’year’ => ’\d+’,
)));
return $collection;
2.1. Libro
77
Symfony2 documentation Documentation, Release 2
Come si sarà visto, questa rotta verrà soddisfatta solo quando la porzione {culture} dell’URL è en o fr e se
{year} è un numero. Questa rotta mostra anche come sia possibile utilizzare un punto tra i segnaposto al posto
di una barra. Gli URL corrispondenti a questa rotta potrebbero essere del tipo:
• /articles/en/2010/my-post
• /articles/fr/2010/my-post.rss
Il parametro speciale _format per le rotte
Questo esempio mette in evidenza lo speciale parametro per le rotte _format. Quando si utilizza questo
parametro, il valore cercato diventa il “formato della richiesta” dell’oggetto Request. In definitiva, il
formato della richiesta è usato per cose tipo impostare il Content-Type della risposta (per esempio una
richiesta di formato json si traduce in un Content-Type con valore application/json). Può
essere utilizzato anche nel controllore per rendere un template diverso per ciascun valore di _format. Il
parametro _format è un modo molto potente per rendere lo stesso contenuto in formati diversi.
Parametri speciali per le rotte
Come si è visto, ogni parametro della rotta o valore predefinito è disponibile come parametro nel metodo del controllore. Inoltre, ci sono tre parametri speciali: ciascuno aggiunge una funzionalità all’interno dell’applicazione:
• _controller: Come si è visto, questo parametro viene utilizzato per determinare quale controllore viene
eseguito quando viene trovata la rotta;
• _format: Utilizzato per impostare il formato della richiesta (per saperne di più);
• _locale: Utilizzato per impostare il locale sulla sessione (per saperne di più);
Tip: Se si usa il parametro _locale in una rotta, il valore sarà memorizzato nella sessione, in modo che le
richieste successive lo mantengano.
Schema per il nome dei controllori
Ogni rotta deve avere un parametro _controller, che determina quale controllore dovrebbe essere eseguito
quando si accoppia la rotta. Questo parametro utilizza un semplice schema stringa, chiamato nome logico del
controllore, che Symfony mappa in uno specifico metodo PHP di una certa classe. Lo schema ha tre parti, ciascuna
separata da due punti:
bundle:controllore:azione
Per esempio, se _controller ha valore AcmeBlogBundle:Blog:show significa:
Bundle
AcmeBlogBundle
Classe del controllore
BlogController
Nome del metodo
showAction
Il controllore potrebbe essere simile a questo:
// src/Acme/BlogBundle/Controller/BlogController.php
namespace Acme\BlogBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class BlogController extends Controller
{
public function showAction($slug)
{
// ...
}
}
78
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
Si noti che Symfony aggiunge la stringa Controller al nome della classe (Blog => BlogController) e
Action al nome del metodo (show => showAction).
Si potrebbe anche fare riferimento a questo controllore con il nome completo di classe e metodo:
Acme\BlogBundle\Controller\BlogController::showAction. Ma seguendo alcune semplici
convenzioni, il nome logico è più conciso e permette una maggiore flessibilità.
Note: Oltre all’utilizzo del nome logico o il nome completo della classe, Symfony supporta un terzo modo
per fare riferimento a un controllore. Questo metodo utilizza solo un separatore due punti (ad esempio
nome_servizio:indexAction) e fa riferimento al controllore come un servizio (vedere Definire i controllori come servizi).
Parametri delle rotte e parametri del controllore
I parametri delle rotte (ad esempio {slug}) sono particolarmente importanti perché ciascuno è reso disponibile
come parametro al metodo del controllore:
public function showAction($slug)
{
// ...
}
In realtà, l’intera collezione defaults viene unita con i valori del parametro per formare un singolo array. Ogni
chiave di questo array è disponibile come parametro sul controllore.
In altre parole, per ogni parametro del metodo del controllore, Symfony cerca per un parametro della rotta con
quel nome e assegna il suo valore a tale parametro. Nell’esempio avanzato di cui sopra, qualsiasi combinazioni (in
qualsiasi ordine) delle seguenti variabili potrebbe essere usati come parametri per il metodo showAction():
• $culture
• $year
• $title
• $_format
• $_controller
Dal momento che il segnaposto e la collezione defaults vengono uniti insieme, è disponibile anche la variabile $_controller. Per una trattazione più dettagliata, vedere I parametri delle rotte come parametri del
controllore.
Tip: È inoltre possibile utilizzare una variabile speciale $_route, che è impostata sul nome della rotta che è
stata abbinata.
Includere risorse esterne per le rotte
Tutte le rotte vengono caricate attraverso un singolo file di configurazione, generalmente
app/config/routing.yml (vedere Creazione delle rotte sopra). In genere, però, si desidera caricare
le rotte da altri posti, come un file di rotte presente all’interno di un bundle. Questo può essere fatto “importando”
il file:
• YAML
# app/config/routing.yml
acme_hello:
resource: "@AcmeHelloBundle/Resources/config/routing.yml"
• XML
2.1. Libro
79
Symfony2 documentation Documentation, Release 2
<!-- app/config/routing.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/r
<import resource="@AcmeHelloBundle/Resources/config/routing.xml" />
</routes>
• PHP
// app/config/routing.php
use Symfony\Component\Routing\RouteCollection;
$collection = new RouteCollection();
$collection->addCollection($loader->import("@AcmeHelloBundle/Resources/config/routing.php"));
return $collection;
Note: Quando si importano le risorse in formato YAML, la chiave (ad esempio acme_hello) non ha senso.
Basta essere sicuri che sia unica, in modo che nessun’altra linea la sovrascriva.
La chiave resource carica la data risorsa di rotte. In questo esempio la risorsa è il percorso completo di un
file, dove la sintassi scorciatoia @AcmeHelloBundle viene risolta con il percorso del bundle. Il file importato
potrebbe essere tipo questo:
• YAML
# src/Acme/HelloBundle/Resources/config/routing.yml
acme_hello:
pattern: /hello/{name}
defaults: { _controller: AcmeHelloBundle:Hello:index }
• XML
<!-- src/Acme/HelloBundle/Resources/config/routing.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/r
<route id="acme_hello" pattern="/hello/{name}">
<default key="_controller">AcmeHelloBundle:Hello:index</default>
</route>
</routes>
• PHP
// src/Acme/HelloBundle/Resources/config/routing.php
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;
$collection = new RouteCollection();
$collection->add(’acme_hello’, new Route(’/hello/{name}’, array(
’_controller’ => ’AcmeHelloBundle:Hello:index’,
)));
return $collection;
Le rotte di questo file sono analizzate e caricate nello stesso modo del file principale delle rotte.
80
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
Prefissare le rotte importate
Si può anche scegliere di fornire un “prefisso” per le rotte importate. Per esempio, si supponga di volere che la
rotta acme_hello abbia uno schema finale con /admin/hello/{name} invece di /hello/{name}:
• YAML
# app/config/routing.yml
acme_hello:
resource: "@AcmeHelloBundle/Resources/config/routing.yml"
prefix:
/admin
• XML
<!-- app/config/routing.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/r
<import resource="@AcmeHelloBundle/Resources/config/routing.xml" prefix="/admin" />
</routes>
• PHP
// app/config/routing.php
use Symfony\Component\Routing\RouteCollection;
$collection = new RouteCollection();
$collection->addCollection($loader->import("@AcmeHelloBundle/Resources/config/routing.php"),
return $collection;
La stringa /admin ora verrà preposta allo schema di ogni rotta caricata dalla nuova risorsa delle rotte.
Visualizzare e fare il debug delle rotte
L’aggiunta e la personalizzazione di rotte è utile, ma lo è anche essere in grado di visualizzare e recuperare informazioni dettagliate sulle rotte. Il modo migliore per vedere tutte le rotte dell’applicazione è tramite il comando di
console router:debug. Eseguire il comando scrivendo il codice seguente dalla cartella radice del progetto
php app/console router:debug
Il comando visualizzerà un utile elenco di tutte le rotte configurate nell’applicazione:
homepage
contact
contact_process
article_show
blog
blog_show
ANY
GET
POST
ANY
ANY
ANY
/
/contact
/contact
/articles/{culture}/{year}/{title}.{_format}
/blog/{page}
/blog/{slug}
Inoltre è possibile ottenere informazioni molto specifiche su una singola rotta mettendo il nome della rotta dopo il
comando:
php app/console router:debug article_show
Generazione degli URL
Il sistema delle rotte dovrebbe anche essere usato per generare gli URL. In realtà, il routing è un sistema bidirezionale:
mappa l’URL in un controllore + parametri e una rotta +
2.1. Libro
81
Symfony2 documentation Documentation, Release 2
parametri di nuovo in un URL. I metodi :method:‘Symfony\\Component\\Routing\\Router::match‘ e
:method:‘Symfony\\Component\\Routing\\Router::generate‘ formano questo sistema bidirezionale. Si prenda
la rotta dell’esempio precedente blog_show:
$params = $router->match(’/blog/my-blog-post’);
// array(’slug’ => ’my-blog-post’, ’_controller’ => ’AcmeBlogBundle:Blog:show’)
$uri = $router->generate(’blog_show’, array(’slug’ => ’my-blog-post’));
// /blog/my-blog-post
Per generare un URL, è necessario specificare il nome della rotta (ad esempio blog_show) ed eventuali caratteri
jolly (ad esempio slug = my-blog-post) usati nello schema per questa rotta. Con queste informazioni,
qualsiasi URL può essere generata facilmente:
class MainController extends Controller
{
public function showAction($slug)
{
// ...
$url = $this->get(’router’)->generate(’blog_show’, array(’slug’ => ’my-blog-post’));
}
}
In una sezione successiva, si imparerà a generare URL da tempalte interni.
Tip: Se la propria applicazione usa richieste AJAX, si potrebbe voler generare URL in JavaScript, che siano
basate sulla propria configurazione delle rotte. Usando FOSJsRoutingBundle, lo si può fare:
var url = Routing.generate(’blog_show’, { "slug": ’my-blog-post});
Per ultetiori informazioni, vedere la documentazione del bundle.
Generare URL assoluti
Per impostazione predefinita, il router genera URL relativi (ad esempio /blog). Per generare un URL assoluto,
è sufficiente passare true come terzo parametro del metodo generate():
$router->generate(’blog_show’, array(’slug’ => ’my-blog-post’), true);
// http://www.example.com/blog/my-blog-post
Note: L’host che viene usato quando si genera un URL assoluto è l’host dell’oggetto Request corrente. Questo
viene rilevato automaticamente in base alle informazioni sul server fornite da PHP. Quando si generano URL
assolute per script che devono essere eseguiti da riga di comando, sarà necessario impostare manualmente l’host
desiderato sull’oggetto Request:
$request->headers->set(’HOST’, ’www.example.com’);
Generare URL con query string
Il metodo generate accetta un array di valori jolly per generare l’URI. Ma se si passano quelli extra, saranno
aggiunti all’URI come query string:
$router->generate(’blog’, array(’page’ => 2, ’category’ => ’Symfony’));
// /blog/2?category=Symfony
82
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
Generare URL da un template
Il luogo più comune per generare un URL è all’interno di un template quando si creano i collegamenti tra le varie
pagine dell’applicazione. Questo viene fatto esattamente come prima, ma utilizzando una funzione helper per i
template:
• Twig
<a href="{{ path(’blog_show’, { ’slug’: ’my-blog-post’ }) }}">
Read this blog post.
</a>
• PHP
<a href="<?php echo $view[’router’]->generate(’blog_show’, array(’slug’ => ’my-blog-post’)) ?
Read this blog post.
</a>
Possono anche essere generati gli URL assoluti.
• Twig
<a href="{{ url(’blog_show’, { ’slug’: ’my-blog-post’ }) }}">
Read this blog post.
</a>
• PHP
<a href="<?php echo $view[’router’]->generate(’blog_show’, array(’slug’ => ’my-blog-post’), t
Read this blog post.
</a>
Riassunto
Il routing è un sistema per mappare l’URL delle richieste in arrivo in una funzione controllore che dovrebbe essere
chiamata a processare la richiesta. Il tutto permette sia di creare URL “belle” che di mantenere la funzionalità
dell’applicazione disaccoppiata da questi URL. Il routing è un meccanismo bidirezionale, nel senso che dovrebbe
anche essere utilizzato per generare gli URL.
Imparare di più dal ricettario
• Come forzare le rotte per utilizzare sempre HTTPS
2.1.7 Creare e usare i template
Come noto, il controllore è responsabile della gestione di ogni richiesta che arriva a un’applicazione Symfony2.
In realtà, il controllore delega la maggior parte del lavoro pesante ad altri punti, in modo che il codice possa essere
testato e riusato. Quando un controllore ha bisogno di generare HTML, CSS o ogni altro contenuto, passa il lavoro
al motore dei template. In questo capitolo si imparerà come scrivere potenti template, che possano essere riusati
per restituire del contenuto all’utente, popolare corpi di email e altro. Si impareranno scorciatoie, modi intelligenti
di estendere template e come riusare il codice di un template
Template
Un template è un semplice file testuale che può generare qualsiasi formato basato sul testo (HTML, XML, CSV,
LaTeX ...). Il tipo più familiare di template è un template PHP, un file testuale analizzato da PHP che contiene un
misto di testo e codice PHP:
2.1. Libro
83
Symfony2 documentation Documentation, Release 2
<!DOCTYPE html>
<html>
<head>
<title>Benvenuto in Symfony!</title>
</head>
<body>
<h1><?php echo $page_title ?></h1>
<ul id="navigation">
<?php foreach ($navigation as $item): ?>
<li>
<a href="<?php echo $item->getHref() ?>">
<?php echo $item->getCaption() ?>
</a>
</li>
<?php endforeach; ?>
</ul>
</body>
</html>
Ma Symfony2 possiede un linguaggio di template ancora più potente, chiamato Twig. Twig consente di scrivere
template concisi e leggibili, più amichevoli per i grafici e, in molti modi, più potenti dei template PHP:
<!DOCTYPE html>
<html>
<head>
<title>Benvenuto in Symfony!</title>
</head>
<body>
<h1>{{ page_title }}</h1>
<ul id="navigation">
{% for item in navigation %}
<li><a href="{{ item.href }}">{{ item.caption }}</a></li>
{% endfor %}
</ul>
</body>
</html>
Twig definisce due tipi di sintassi speciali:
• {{ ...
}}: “Dice qualcosa”: stampa una variabile o il risultato di un’espressione nel template;
• {% ... %}: “Fa qualcosa”: un tag che controlla la logica del template; è usato per eseguire istruzioni,
come il ciclo for dell’esempio.
Note: C’è una terza sintassi usata per creare commenti: {# questo è un commento #}. Questa sintassi
può essere usata su righe multiple, come il suo equivalente PHP /* commento */.
Twig contiene anche dei filtri, che modificano il contenuto prima che sia reso. L’esempio seguente rende la
variabile title tutta maiuscola, prima di renderla:
{{ title | upper }}
Twig ha una lunga lista di tag e filtri, disponibili in maniera predefinita. Si possono anche aggiungere le proprie
estensioni a Twig, se necessario.
Tip: È facile registrare un’estensione di Twig basta creare un nuovo servizio e taggarlo con twig.extension
tag.
Come vedremo nella documentazione, Twig supporta anche le funzioni e si possono aggiungere facilmente nuove
funzioni. Per esempio, di seguito viene usato un tag standard for e la funzione cycle per stampare dieci tag
84
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
div, con classi alternate odd e even:
{% for i in 0..10 %}
<div class="{{ cycle([’odd’, ’even’], i) }}">
<!-- un po’ di codice HTML -->
</div>
{% endfor %}
In questo capitolo, gli esempi dei template saranno mostrati sia in Twig che in PHP.
Perché Twig?
I template di Twig sono pensati per essere semplici e non considerano i tag PHP. Questo è intenzionale: il
sistema di template di Twig è fatto per esprimere una presentazione, non logica di programmazione. Più si
usa Twig, più se ne può apprezzare benefici e distinzione. E, ovviamente, essere amati da tutti i grafici del
mondo.
Twig può anche far cose che PHP non può fare, come una vera ereditarietà dei template (i template Twig
compilano in classi PHP che ereditano tra di loro), controllo degli spazi vuoti, sandbox e inclusione di
funzioni e filtri personalizzati, che hanno effetti solo sui template. Twig possiede poche caratteristiche che
rendono la scrittura di template più facile e concisa. Si prenda il seguente esempio, che combina un ciclo
con un’istruzione logica if:
<ul>
{% for user in users %}
<li>{{ user.username }}</li>
{% else %}
<li>Nessun utente trovato</li>
{% endfor %}
</ul>
Cache di template Twig
Twig è veloce. Ogni template Twig è compilato in una classe nativa PHP, che viene resa a runtime. Le classi compilate sono situate nella cartella app/cache/{environment}/twig (dove {environment} è l’ambiente,
come dev o prod) e in alcuni casi possono essere utili durante il debug. Vedere Ambienti per maggiori informazioni sugli ambienti.
Quando si abilita la modalità di debug (tipicamente in ambiente dev), un template Twig viene automaticamente
ricompilato a ogni modifica subita. Questo vuol dire che durante lo sviluppo si possono tranquillamente effettuare
cambiamenti a un template Twig e vedere immediatamente le modifiche, senza doversi preoccupare di pulire la
cache.
Quando la modalità di debug è disabilitata (tipicamente in ambiente prod), tuttavia, occorre pulire la cache di
Twig, in modo che i template Twig siano rigenerati. Si ricordi di farlo al deploy della propria applicazione.
Ereditarietà dei template e layout
Molto spesso, i template di un progetto condividono elementi comuni, come la testata, il piè di pagina, una barra
laterale e altro. In Symfony2, ci piace pensare a questo problema in modo differente: un template può essere
decorato da un altro template. Funziona esattamente come per le classi PHP: l’ereditarietà dei template consente
di costruire un template “layout” di base, che contiene tutti gli elementi comuni del proprio sito, definiti come
blocchi (li si pensi come “classi PHP con metodi base”). Un template figlio può estendere un layout di base e
sovrascrivere uno qualsiasi dei suoi blocchi (li si pensi come “sottoclassi PHP che sovrascrivono alcuni metodi
della classe genitrice”).
Primo, costruire un file per il layout di base:
• Twig
2.1. Libro
85
Symfony2 documentation Documentation, Release 2
{# app/Resources/views/base.html.twig #}
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>{% block title %}Applicazione di test{% endblock %}</title>
</head>
<body>
<div id="sidebar">
{% block sidebar %}
<ul>
<li><a href="/">Home</a></li>
<li><a href="/blog">Blog</a></li>
</ul>
{% endblock %}
</div>
<div id="content">
{% block body %}{% endblock %}
</div>
</body>
</html>
• PHP
<!-- app/Resources/views/base.html.php -->
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title><?php $view[’slots’]->output(’title’, ’Applicazione di test’) ?></title>
</head>
<body>
<div id="sidebar">
<?php if ($view[’slots’]->has(’sidebar’): ?>
<?php $view[’slots’]->output(’sidebar’) ?>
<?php else: ?>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/blog">Blog</a></li>
</ul>
<?php endif; ?>
</div>
<div id="content">
<?php $view[’slots’]->output(’body’) ?>
</div>
</body>
</html>
Note: Sebbene la discussione sull’ereditarietà dei template sia relativa a Twig, la filosofia è condivisa tra template
Twig e template PHP.
Questo template definisce lo scheletro del documento HTML di base di una semplice pagina a due colonne. In
questo esempio, tre aree {% block %} sono definite (title, sidebar e body). Ciascun blocco può essere
sovrascritto da un template figlio o lasciato alla sua implementazione predefinita. Questo template potrebbe anche
essere reso direttamente. In questo caso, i blocchi title, sidebar e body manterrebbero semplicemente i
valori predefiniti usati in questo template.
Un template figlio potrebbe assomigliare a questo:
• Twig
86
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
{# src/Acme/BlogBundle/Resources/views/Blog/index.html.twig #}
{% extends ’::base.html.twig’ %}
{% block title %}I post fighi del mio blog{% endblock %}
{% block body %}
{% for entry in blog_entries %}
<h2>{{ entry.title }}</h2>
<p>{{ entry.body }}</p>
{% endfor %}
{% endblock %}
• PHP
<!-- src/Acme/BlogBundle/Resources/views/Blog/index.html.php -->
<?php $view->extend(’::base.html.php’) ?>
<?php $view[’slots’]->set(’title’, ’I post fighi del mio blog’) ?>
<?php $view[’slots’]->start(’body’) ?>
<?php foreach ($blog_entries as $entry): ?>
<h2><?php echo $entry->getTitle() ?></h2>
<p><?php echo $entry->getBody() ?></p>
<?php endforeach; ?>
<?php $view[’slots’]->stop() ?>
Note: Il template padre è identificato da una speciale sintassi di stringa (::base.html.twig) che indica che
il template si trova nella cartella app/Resources/views del progetto. Questa convenzione di nomi è spiegata
nel dettaglio in Nomi e posizioni dei template.
La chiave dell’ereditarietà dei template è il tag {% extends %}. Questo dice al motore dei template di valutare
prima il template base, che imposta il layout e definisce i vari blocchi. Quindi viene reso il template figlio e i blocchi title e body del padre vengono rimpiazzati da quelli del figlio. A seconda del valore di blog_entries,
l’output potrebbe assomigliare a questo:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>I post fighi del mio blog</title>
</head>
<body>
<div id="sidebar">
<ul>
<li><a href="/">Home</a></li>
<li><a href="/blog">Blog</a></li>
</ul>
</div>
<div id="content">
<h2>Il mio primo post</h2>
<p>Il testo del primo post.</p>
<h2>Un altro post</h2>
<p>Il testo del secondo post.</p>
</div>
</body>
</html>
Si noti che, siccome il template figlio non definisce un blocco sidebar, viene usato al suo posto il valore
del template padre. Il contenuto di un tag {% block %} in un template padre è sempre usato come valore
predefinito.
2.1. Libro
87
Symfony2 documentation Documentation, Release 2
Si possono usare tanti livelli di ereditarietà quanti se ne desiderano. Nella prossima sezione, sarà spiegato un
modello comuni a tre livelli di ereditarietà, insieme al modo in cui i template sono organizzati in un progetto
Symfony2.
Quando si lavora con l’ereditarietà dei template, ci sono alcuni concetti da tenere a mente:
• se si usa {% extends %} in un template, deve essere il primo tag di quel template.
• Più tag {% block %} si hanno in un template, meglio è. Si ricordi che i template figli non devono
definire tutti i blocchi del padre, quindi si possono creare molti blocchi nei template base e dar loro dei
valori predefiniti adeguati. Più blocchi si hanno in un template base, più sarà flessibile il layout.
• Se ci si trova ad aver duplicato del contenuto in un certo numero di template, vuol dire che probabilmente si
dovrebbe spostare tale contenuto in un {% block %} di un template padre. In alcuni casi, una soluzione
migliore potrebbe essere spostare il contenuto in un nuovo template e usare include (vedere Includere
altri template).
• Se occorre prendere il contenuto di un blocco da un template padre, si può usare la funzione {{ parent()
}}. È utile quando si vuole aggiungere il contenuto di un template padre, invece di sovrascriverlo completamente:
{% block sidebar %}
<h3>Sommario</h3>
...
{{ parent() }}
{% endblock %}
Nomi e posizioni dei template
Per impostazione predefinita, i template possono stare in una di queste posizioni:
• app/Resources/views/: La cartella views di un’applicazione può contenere template di base a
livello di applicazione (p.e. i layout dell’applicazione), ma anche template che sovrascrivono template di
bundle (vedere
Sovrascrivere template dei bundle);
• percorso/bundle/Resources/views/: Ogni bundle ha i suoi template, nella sua cartella
Resources/views (e nelle sotto-cartelle). La maggior parte dei template è dentro a un bundle.
Symfony2 usa una sintassi stringa bundle:controllore:template per i template. Questo consente diversi tipi di
template, ciascuno in un posto specifico:
• AcmeBlogBundle:Blog:index.html.twig: Questa sintassi è usata per specificare un template
per una determinata pagina. Le tre parti della stringa, ognuna separata da due-punti (:), hanno il seguente
significato:
– AcmeBlogBundle:
(bundle)
src/Acme/BlogBundle);
il
template
è
dentro
AcmeBlogBundle
(p.e.
– Blog: (controllore) indica che il template è nella sotto-cartella Blog di Resources/views;
– index.html.twig: (template) il nome del file è index.html.twig.
Ipotizzando che AcmeBlogBundle sia dentro src/Acme/BlogBundle, il percorso finale del layout
sarebbe src/Acme/BlogBundle/Resources/views/Blog/index.html.twig.
• AcmeBlogBundle::layout.html.twig: Questa sintassi si riferisce a un template di base specifico di AcmeBlogBundle. Poiché la parte centrale, “controllore”, manca, (p.e. Blog), il template è
Resources/views/layout.html.twig dentro AcmeBlogBundle.
• ::base.html.twig: Questa sintassi si riferisce a un template di base o a un layout di applicazione. Si
noti che la stringa inizia con un doppio due-punti (::), il che vuol dire che mancano sia la parte del bundle
che quella del controllore. Questo significa che il template non è in alcun bundle, ma invece nella cartella
radice app/Resources/views/.
88
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
Nella sezione Sovrascrivere template dei bundle si potrà trovare come ogni template dentro AcmeBlogBundle,
per esempio, possa essere sovrascritto mettendo un template con lo stesso nome nella cartella
app/Resources/AcmeBlogBundle/views/. Questo dà la possibilità di sovrascrivere template di qualsiasi bundle.
Tip: Si spera che la sintassi dei nomi risulti familiare: è la stessa convenzione di nomi usata per Schema per il
nome dei controllori.
Suffissi dei template
Il formato bundle:controllore:template di ogni template specifica dove il file del template si trova. Ogni nome di
template ha anche due estensioni, che specificano il formato e il motore per quel template.
• AcmeBlogBundle:Blog:index.html.twig - formato HTML, motore Twig
• AcmeBlogBundle:Blog:index.html.php - formato HTML, motore PHP
• AcmeBlogBundle:Blog:index.css.twig - formato CSS, motore Twig
Per impostazione predefinita, ogni template Symfony2 può essere scritto in Twig o in PHP, e l’ultima parte
dell’estensione (p.e. .twig o .php) specifica quale di questi due motori va usata. La prima parte dell’estensione,
(p.e. .html, .css, ecc.) è il formato finale che il template genererà. Diversamente dal motore, che determina il
modo in cui Symfony2 analizza il template, si tratta di una tattica organizzativa usata nel caso in cui alcune risorse
debbano essere rese come HTML (index.html.twig), XML (index.xml.twig) o in altri formati. Per
maggiori informazioni, leggere la sezione Debug.
Note: I “motori” disponibili possono essere configurati e se ne possono aggiungere di nuovi. Vedere Configurazione dei template per maggiori dettagli.
Tag e helper
Dopo aver parlato delle basi dei template, di che nomi abbiano e di come si possa usare l’ereditarietà, la parte più
difficile è passata. In questa sezione, si potranno conoscere un gran numero di strumenti disponibili per aiutare a
compiere i compiti più comuni sui template, come includere altri template, collegare pagine e inserire immagini.
Symfony2 dispone di molti tag di Twig specializzati e di molte funzioni, che facilitano il lavoro del progettista di
template. In PHP, il sistema di template fornisce un sistema estensibile di helper, che fornisce utili caratteristiche
nel contesto dei template.
Abbiamo già visto i tag predefiniti ({% block %} e {% extends %}), così come un esempio di helper PHP
($view[’slots’]). Vediamone alcuni altri.
Includere altri template
Spesso si vorranno includere lo stesso template o lo stesso pezzo di codice in pagine diverse. Per esempio, in
un’applicazione con “nuovi articoli”, il codice del template che mostra un articolo potrebbe essere usato sulla
pagina dei dettagli dell’articolo, un una pagina che mostra gli articoli più popolari o in una lista degli articoli più
recenti.
Quando occorre riusare un pezzo di codice PHP, tipicamente si posta il codice in una nuova classe o funzione
PHP. Lo stesso vale per i template. Spostando il codice del template da riusare in un template a parte, può essere
incluso in qualsiasi altro template. Primo, creare il template che occorrerà riusare.
• Twig
2.1. Libro
89
Symfony2 documentation Documentation, Release 2
{# src/Acme/ArticleBundle/Resources/views/Article/articleDetails.html.twig #}
<h2>{{ article.title }}</h2>
<h3 class="byline">by {{ article.authorName }}</h3>
<p>
{{ article.body }}
</p>
• PHP
<!-- src/Acme/ArticleBundle/Resources/views/Article/articleDetails.html.php -->
<h2><?php echo $article->getTitle() ?></h2>
<h3 class="byline">by <?php echo $article->getAuthorName() ?></h3>
<p>
<?php echo $article->getBody() ?>
</p>
Includere questo template da un altro template è semplice:
• Twig
{# src/Acme/ArticleBundle/Resources/Article/list.html.twig #}
{% extends ’AcmeArticleBundle::layout.html.twig’ %}
{% block body %}
<h1>Articoli recenti<h1>
{% for article in articles %}
{% include ’AcmeArticleBundle:Article:articleDetails.html.twig’ with {’article’: arti
{% endfor %}
{% endblock %}
• PHP
<!-- src/Acme/ArticleBundle/Resources/Article/list.html.php -->
<?php $view->extend(’AcmeArticleBundle::layout.html.php’) ?>
<?php $view[’slots’]->start(’body’) ?>
<h1>Articoli recenti</h1>
<?php foreach ($articles as $article): ?>
<?php echo $view->render(’AcmeArticleBundle:Article:articleDetails.html.php’, array(’
<?php endforeach; ?>
<?php $view[’slots’]->stop() ?>
Il template è incluso usando il tag {% include %}. Si noti che il nome del template segue le stesse tipiche
convenzioni. Il template articleDetails.html.twig usa una variabile article. Questa viene passata
nel template list.html.twig usando il comando with.
Tip: La sintassi {’article’: article} è la sintassi standard di Twig per gli array associativi (con chiavi
non numeriche). Se avessimo avuto bisogno di passare più elementi, sarebbe stato così: {’pippo’: pippo,
’pluto’: pluto}.
Inserire controllori
A volte occorre fare di più che includere semplici template. Si supponga di avere nel proprio layout una barra
laterale, che contiene i tre articoli più recenti. Recuperare i tre articoli potrebbe implicare una query al database,
o l’esecuzione di altra logica, che non si può fare dentro a un template.
90
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
La soluzione è semplicemente l’inserimento del risultato di un intero controllore dal proprio template. Primo,
creare un controllore che rende un certo numero di articoli recenti:
// src/Acme/ArticleBundle/Controller/ArticleController.php
class ArticleController extends Controller
{
public function recentArticlesAction($max = 3)
{
// chiamare il database o altra logica per ottenere "$max" articoli recenti
$articles = ...;
return $this->render(’AcmeArticleBundle:Article:recentList.html.twig’, array(’articles’ =>
}
}
Il template recentList è molto semplice:
• Twig
{# src/Acme/ArticleBundle/Resources/views/Article/recentList.html.twig #}
{% for article in articles %}
<a href="/article/{{ article.slug }}">
{{ article.title }}
</a>
{% endfor %}
• PHP
<!-- src/Acme/ArticleBundle/Resources/views/Article/recentList.html.php -->
<?php foreach ($articles in $article): ?>
<a href="/article/<?php echo $article->getSlug() ?>">
<?php echo $article->getTitle() ?>
</a>
<?php endforeach; ?>
Note:
Si noti che abbiamo barato e inserito a mano l’URL dell’articolo in questo esempio (p.e.
/article/*slug*). Questa non è una buona pratica. Nella prossima sezione, vedremo come farlo correttamente.
Per includere il controllore, occorrerà fare riferimento a esso usando la sintassi standard per i controllori (cioè
bundle:controllore:azione):
• Twig
{# app/Resources/views/base.html.twig #}
...
<div id="sidebar">
{% render "AcmeArticleBundle:Article:recentArticles" with {’max’: 3} %}
</div>
• PHP
<!-- app/Resources/views/base.html.php -->
...
<div id="sidebar">
<?php echo $view[’actions’]->render(’AcmeArticleBundle:Article:recentArticles’, array(’ma
</div>
Ogni volta che ci si trova ad aver bisogno di una variabile o di un pezzo di inforamzione a cui non si ha accesso
in un template, considerare di rendere un controllore. I controllori sono veloci da eseguire e promuovono buona
organizzazione e riuso del codice.
2.1. Libro
91
Symfony2 documentation Documentation, Release 2
Collegare le pagine
Creare collegamenti alle altre pagine nella propria applicazioni è uno dei lavori più comuni per un template. Invece
di inserire a mano URL nei template, usare la funzione path di Twig (o l’helper router in PHP) per generare
URL basati sulla configurazione delle rotte. Più avanti, se si vuole modificare l’URL di una particolare pagina,
tutto ciò di cui si avrà bisogno è cambiare la configurazione delle rotte: i template genereranno automaticamente
il nuovo URL.
Primo, collegare la pagina “_welcome”, accessibile tramite la seguente configurazione delle rotte:
• YAML
_welcome:
pattern: /
defaults: { _controller: AcmeDemoBundle:Welcome:index }
• XML
<route id="_welcome" pattern="/">
<default key="_controller">AcmeDemoBundle:Welcome:index</default>
</route>
• PHP
$collection = new RouteCollection();
$collection->add(’_welcome’, new Route(’/’, array(
’_controller’ => ’AcmeDemoBundle:Welcome:index’,
)));
return $collection;
Per collegare la pagina, usare la funzione path di Twig e riferirsi alla rotta:
• Twig
<a href="{{ path(’_welcome’) }}">Home</a>
• PHP
<a href="<?php echo $view[’router’]->generate(’_welcome’) ?>">Home</a>
Come ci si aspettava, questo genererà l’URL /. Vediamo come funziona con una rotta più complessa:
• YAML
article_show:
pattern: /article/{slug}
defaults: { _controller: AcmeArticleBundle:Article:show }
• XML
<route id="article_show" pattern="/article/{slug}">
<default key="_controller">AcmeArticleBundle:Article:show</default>
</route>
• PHP
$collection = new RouteCollection();
$collection->add(’article_show’, new Route(’/article/{slug}’, array(
’_controller’ => ’AcmeArticleBundle:Article:show’,
)));
return $collection;
In questo caso, occorre specificare sia il nome della rotta (article_show) che il valore del parametro {slug}.
Usando questa rotta, rivisitiamo il template recentList della sezione precedente e colleghiamo correttamente
gli articoli:
92
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
• Twig
{# src/Acme/ArticleBundle/Resources/views/Article/recentList.html.twig #}
{% for article in articles %}
<a href="{{ path(’article_show’, { ’slug’: article.slug }) }}">
{{ article.title }}
</a>
{% endfor %}
• PHP
<!-- src/Acme/ArticleBundle/Resources/views/Article/recentList.html.php -->
<?php foreach ($articles in $article): ?>
<a href="<?php echo $view[’router’]->generate(’article_show’, array(’slug’ => $article->g
<?php echo $article->getTitle() ?>
</a>
<?php endforeach; ?>
Tip: Si può anche generare un URL assoluto, usando la funzione url di Twig:
<a href="{{ url(’_welcome’) }}">Home</a>
Lo stesso si può fare nei template PH, passando un terzo parametro al metodo generate():
<a href="<?php echo $view[’router’]->generate(’_welcome’, array(), true) ?>">Home</a>
Collegare le risorse
I template solitamente hanno anche riferimenti a immagini, Javascript, fogli di stile e altre risorse. Certamente,
si potrebbe inserire manualmente il percorso a tali risorse (p.e. /images/logo.png), ma Symfony2 fornisce
un’opzione più dinamica, tramite la funzione assets di Twig:
• Twig
<img src="{{ asset(’images/logo.png’) }}" alt="Symfony!" />
<link href="{{ asset(’css/blog.css’) }}" rel="stylesheet" type="text/css" />
• PHP
<img src="<?php echo $view[’assets’]->getUrl(’images/logo.png’) ?>" alt="Symfony!" />
<link href="<?php echo $view[’assets’]->getUrl(’css/blog.css’) ?>" rel="stylesheet" type="tex
Lo scopo principale della funzione asset è rendere più portabile la propria applicazione.
Se la
propria applicazione si trova nella radice del proprio host (p.e.
http://example.com), i percorsi resi
dovrebbero essere del tipo /images/logo.png. Se invece la propria applicazione si trova in una
sotto-cartella (p.e. http://example.com/my_app), ogni percorso dovrebbe includere la sotto-cartella (p.e.
/my_app/images/logo.png). La funzione asset si prende cura di questi aspetti, determinando in che
modo è usata la propria applicazione e generando i percorsi adeguati.
Inoltre, se si usa la funzione asset, Symfony può aggiungere automaticamente un parametro all’URL
della risorsa, per garantire che le risorse statiche aggiornate non siano messe in cache. Per esempio,
/images/logo.png potrebbe comparire come /images/logo.png?v2. Per ulteriori informazioni, vedere
l’opzione di configurazione assets_version.
Includere fogli di stile e Javascript in Twig
Nessun sito sarebbe completo senza l’inclusione di file Javascript e fogli di stile. In Symfony, l’inclusione di tali
risorse è gestita elegantemente sfruttando l’ereditarietà dei template.
2.1. Libro
93
Symfony2 documentation Documentation, Release 2
Tip: Questa sezione insegnerà la filosofia che sta dietro l’inclusione di fogli di stile e Javascript in Symfony.
Symfony dispone di un’altra libreria, chiamata Assetic, che segue la stessa filosofia, ma consente di fare cose
molto più interessanti con queste risorse. Per maggiori informazioni sull’uso di Assetic, vedere Come usare
Assetic per la gestione delle risorse.
Iniziamo aggiungendo due blocchi al template di base, che conterranno le risorse: uno chiamato stylesheets,
dentro al tag head, e l’altro chiamato javascripts, appena prima della chiusura del tag body. Questi blocchi
conterranno tutti i fogli di stile e i Javascript che occorrerano al sito:
{# ’app/Resources/views/base.html.twig’ #}
<html>
<head>
{# ... #}
{% block stylesheets %}
<link href="{{ asset(’/css/main.css’) }}" type="text/css" rel="stylesheet" />
{% endblock %}
</head>
<body>
{# ... #}
{% block javascripts %}
<script src="{{ asset(’/js/main.js’) }}" type="text/javascript"></script>
{% endblock %}
</body>
</html>
È così facile! Ma che succede quando si ha bisogno di includere un foglio di stile o un Javascript aggiuntivo in un
template figlio? Per esempio, supponiamo di avere una pagina di contatti e che occorra includere un foglio di stile
contact.css solo su tale pagina. Da dentro il template della pagina di contatti, fare come segue:
{# src/Acme/DemoBundle/Resources/views/Contact/contact.html.twig #}
{# extends ’::base.html.twig’ #}
{% block stylesheets %}
{{ parent() }}
<link href="{{ asset(’/css/contact.css’) }}" type="text/css" rel="stylesheet" />
{% endblock %}
{# ... #}
Nel template figlio, basta sovrascrivere il blocco stylesheets ed inserire il nuovo tag del foglio di stile nel
blocco stesso. Ovviamente, poiché vogliamo aggiungere contenuto al blocco padre (e non sostituirlo), occorre
usare la funzione parent() di Twig, per includere tutto ciò che sta nel blocco stylesheets del template di
base.
Si possono anche includere risorse dalla cartella Resources/public del proprio bundle. Occorre poi eseguire
il comando php app/console assets:install target [--symlink], che copia (o collega) i file
nella posizione corretta (la posizione predefinita è sotto la cartella “web”).
<link href="{{ asset(’bundles/acmedemo/css/contact.css’) }}" type="text/css" rel="stylesheet" />
Il risultato finale è una pagina che include i fogli di stile main.css e contact.css.
Global Template Variables
During
each
request,
both Twig and PHP
94
Symfony2
will
template engines
set
by
a
global
default.
template
variable
app
The app variable is
in
a
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
Symfony\Bundle\FrameworkBundle\Templating\GlobalVariables
give you access to some application specific variables automatically:
instance
which
will
• app.security - The security context.
• app.user - The current user object.
• app.request - The request object.
• app.session - The session object.
• app.environment - The current environment (dev, prod, etc).
• app.debug - True if in debug mode. False otherwise.
• Twig
<p>Username: {{ app.user.username }}</p>
{% if app.debug %}
<p>Request method: {{ app.request.method }}</p>
<p>Application Environment: {{ app.environment }}</p>
{% endif %}
• PHP
<p>Username: <?php echo $app->getUser()->getUsername() ?></p>
<?php if ($app->getDebug()): ?>
<p>Request method: <?php echo $app->getRequest()->getMethod() ?></p>
<p>Application Environment: <?php echo $app->getEnvironment() ?></p>
<?php endif; ?>
Tip: You can add your own global template variables. See the cookbook example on Global Variables.
Configurare e usare il servizio templating
Il cuore del sistema dei template di Symfony2 è il motore dei template. L’oggetto speciale Engine è responsabile
della resa dei template e della restituzione del loro contenuto. Quando si rende un template in un controllore, per
esempio, si sta in realtà usando il servizio del motore dei template. Per esempio:
return $this->render(’AcmeArticleBundle:Article:index.html.twig’);
equivale a
$engine = $this->container->get(’templating’);
$content = $engine->render(’AcmeArticleBundle:Article:index.html.twig’);
return $response = new Response($content);
Il motore (o “servizio”) dei template è pre-configurato per funzionare automaticamente dentro a Symfony2. Può
anche essere ulteriormente configurato nel file di configurazione dell’applicazione:
• YAML
# app/config/config.yml
framework:
# ...
templating: { engines: [’twig’] }
• XML
<!-- app/config/config.xml -->
<framework:templating>
<framework:engine id="twig" />
</framework:templating>
2.1. Libro
95
Symfony2 documentation Documentation, Release 2
• PHP
// app/config/config.php
$container->loadFromExtension(’framework’, array(
// ...
’templating’
=> array(
’engines’ => array(’twig’),
),
));
Sono disponibili diverse opzioni di configurazione, coperte nell’Appendice: configurazione.
Note: Il motore twig è obbligatorio per poter usare il webprofiler (così come molti altri bundle di terze parti).
Sovrascrivere template dei bundle
La comunità di Symfony2 si vanta di creare e mantenere bundle di alta qualità (vedere KnpBundles.com) per un
gran numero di diverse caratteristiche. Quando si usa un bundle di terze parti, probabilmente occorrerà sovrascrivere e personalizzare uno o più dei suoi template.
Supponiamo di aver incluso l’immaginario bundle AcmeBlogBundle nel nostro progetto (p.e. nella cartella
src/Acme/BlogBundle). Pur essendo soddisfatti, vogliamo sovrascrivere la pagina “list” del blog, per
personalizzare il codice e renderlo specifico per la nostra applicazione. Analizzando il controllore Blog di
AcmeBlogBundle, troviamo:
public function indexAction()
{
$blogs = // logica per recuperare i blog
$this->render(’AcmeBlogBundle:Blog:index.html.twig’, array(’blogs’ => $blogs));
}
Quando viene reso AcmeBlogBundle:Blog:index.html.twig, Symfony2 cerca il template in due diversi posti:
1. app/Resources/AcmeBlogBundle/views/Blog/index.html.twig
2. src/Acme/BlogBundle/Resources/views/Blog/index.html.twig
Per sovrascrivere il template del bundle, basta copiare il file index.html.twig dal bundle
a
app/Resources/AcmeBlogBundle/views/Blog/index.html.twig
(la
cartella
app/Resources/AcmeBlogBundle non esiste ancora, quindi occorre crearla). Ora si può personalizzare il template.
Questa logica si applica anche ai template base dei bundle. Supponiamo che ogni template in AcmeBlogBundle
erediti da un template base chiamato AcmeBlogBundle::layout.html.twig. Esattamente come prima,
Symfony2 cercherà il template i questi due posti:
1. app/Resources/AcmeBlogBundle/views/layout.html.twig
2. src/Acme/BlogBundle/Resources/views/layout.html.twig
Anche
qui,
per
sovrascrivere
il
template,
basta
copiarlo
dal
bundle
app/Resources/AcmeBlogBundle/views/layout.html.twig. Ora lo si può personalizzare.
a
Facendo un passo indietro, si vedrà che Symfony2 inizia sempre a cercare un template nella cartella
app/Resources/{NOME_BUNDLE}/views/. Se il template non c’è, continua verificando nella cartella
Resources/views del bundle stesso. Questo vuol dire che ogni template di bundle può essere sovrascritto,
inserendolo nella sotto-cartella app/Resources appropriata.
96
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
Sovrascrivere template del nucleo
Essendo il framework Symfony2 esso stesso un bundle, i template del nucleo possono essere sovrascritti allo
stesso modo. Per esempio, TwigBundle contiene diversi template “exception” ed “error”, che possono essere
sovrascritti, copiandoli dalla cartella Resources/views/Exception di TwigBundle a, come si può immaginare, la cartella app/Resources/TwigBundle/views/Exception.
Ereditarietà a tre livelli
Un modo comune per usare l’ereditarietà è l’approccio a tre livelli. Questo metodo funziona perfettamente con i
tre diversi tipi di template di cui abbiamo appena parlato:
• Creare un file app/Resources/views/base.html.twig che contenga il layout principael per
la propria applicazione (come nell’esempio precedente). Internamente, questo template si chiama
::base.html.twig;
• Creare un template per ogni “sezione” del proprio sito. Per esempio, AcmeBlogBundle avrebbe un
template di nome AcmeBlogBundle::layout.html.twig, che contiene solo elementi specifici alla
sezione blog;
{# src/Acme/BlogBundle/Resources/views/layout.html.twig #}
{% extends ’::base.html.twig’ %}
{% block body %}
<h1>Applicazione blog</h1>
{% block content %}{% endblock %}
{% endblock %}
• Creare i singoli template per ogni pagina, facendo estendere il template della sezione appropriata. Per
esempio, la pagina “index” avrebbe un nome come AcmeBlogBundle:Blog:index.html.twig e
mostrerebbe la lista dei post del blog.
{# src/Acme/BlogBundle/Resources/views/Blog/index.html.twig #}
{% extends ’AcmeBlogBundle::layout.html.twig’ %}
{% block content %}
{% for entry in blog_entries %}
<h2>{{ entry.title }}</h2>
<p>{{ entry.body }}</p>
{% endfor %}
{% endblock %}
Si noti che questo template estende il template di sezione (AcmeBlogBundle::layout.html.twig), che
a sua volte estende il layout base dell’applicazione (::base.html.twig). Questo è il modello di ereditarietà
a tre livelli.
Durante la costruzione della propria applicazione, si può scegliere di seguire questo metodo oppure semplicemente far estendere direttamente a ogni template di pagina il template base dell’applicazione (p.e. {% extends
’::base.html.twig’ %}). Il modello a tre template è una best practice usata dai bundle dei venditori, in
modo che il template base di un bundle possa essere facilmente sovrascritto per estendere correttamente il layout
base della propria applicazione.
Escape dell’output
Quando si genera HTML da un template, c’è sempre il rischio che una variabile possa mostrare HTML indesiderato o codice pericoloso lato client. Il risultato è che il contenuto dinamico potrebbe rompere il codice HTML
della pagina risultante o consentire a un utente malintenzionato di eseguire un attacco Cross Site Scripting (XSS).
Consideriamo questo classico esempio:
• Twig
2.1. Libro
97
Symfony2 documentation Documentation, Release 2
Ciao {{ name }}
• PHP
Ciao <?php echo $name ?>
Si immagini che l’utente inserisca nel suo nome il seguente codice:
<script>alert(’ciao!’)</script>
Senza alcun escape dell’output, il template risultante causerebbe la comparsa di una finestra di alert Javascript:
Ciao <script>alert(’ciao!’)</script>
Sebbene possa sembrare innocuo, se un utente arriva a tal punto, lo stesso utente sarebbe in grado di scrivere
Javascript che esegua azioni dannose all’interno dell’area di un utente legittimo e ignaro.
La risposta a questo problema è l’escape dell’output. Con l’escape attivo, lo stesso template verrebbe reso in modo
innocuo e scriverebbe alla lettera il tag script su schermo:
Ciao &lt;script&gt;alert(&#39;ciao!&#39;)&lt;/script&gt;
L’approccio dei sistemi di template Twig e PHP a questo problema sono diversi. Se si usa Twig, l’escape è attivo
in modo predefinito e si è al sicuro. In PHP, l’escape dell’output non è automatico, il che vuol dire che occorre
applicarlo a mano, dove necessario.
Escape dell’output in Twig
Se si usano i template Twig, l’escape dell’output è attivo in modo predefinito. Questo vuol dire che si è protetti dalle conseguenze non intenzionali del codice inviato dall’utente. Per impostazione predefinita, l’escape
dell’output assume che il contenuto sia sotto escape per l’output HTML.
In alcuni casi, si avrà bisogno di disabilitare l’escape dell’output, quando si avrà bisogno di rendere una variabile affidabile che contiene markup. Supponiamo che gli utenti amministratori siano abilitati a scrivere articoli
che contengano codice HTML. Per impostazione predefinita, Twig mostrerà l’articolo con escape. Per renderlo
normalmente, aggiungere il filtro raw: {{ article.body | raw }}.
Si può anche disabilitare l’escape dell’output dentro a un {% block %} o per un intero template. Per maggiori
informazioni, vedere Escape dell’output nella documentazione di Twig.
Escape dell’output in PHP
L’escape dell’output non è automatico, se si usano i template PHP. Questo vuol dire che, a meno che non scelga
esplicitamente di passare una variabile sotto escape, non si è protetti. Per usare l’escape, usare il metodo speciale
escape():
Ciao <?php echo $view->escape($name) ?>
Per impostazione predefinita, il metodo escape() assume che la variabile sia resa in un contesto HTML (quindi
l’escape renderà la variabile sicura per HTML). Il secondo parametro consente di cambiare contesto. Per esempio
per mostrare qualcosa in una stringa Javascript, usare il contesto js:
var myMsg = ’Ciao <?php echo $view->escape($name, ’js’) ?>’;
Debug
New in version 2.0.9: Questa caratteristica è disponibile da Twig 1.5.x, che è stato aggiunto in Symfony 2.0.9.
Quando si usa PHP, si può ricorrere a var_dump(), se occorre trovare rapidamente il valore di una variabile
passata. Può essere utile, per esempio, nel proprio controllore. Si può ottenere lo stesso risultato con Twig,
usando l’estensione debug. Occorre abilitarla nella configurazione:
98
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
• YAML
# app/config/config.yml
services:
acme_hello.twig.extension.debug:
class:
Twig_Extension_Debug
tags:
- { name: ’twig.extension’ }
• XML
<!-- app/config/config.xml -->
<services>
<service id="acme_hello.twig.extension.debug" class="Twig_Extension_Debug">
<tag name="twig.extension" />
</service>
</services>
• PHP
// app/config/config.php
use Symfony\Component\DependencyInjection\Definition;
$definition = new Definition(’Twig_Extension_Debug’);
$definition->addTag(’twig.extension’);
$container->setDefinition(’acme_hello.twig.extension.debug’, $definition);
Si può quindi fare un dump dei parametri nei template, usando la funzione dump:
{# src/Acme/ArticleBundle/Resources/views/Article/recentList.html.twig #}
{{ dump(articles) }}
{% for article in articles %}
<a href="/article/{{ article.slug }}">
{{ article.title }}
</a>
{% endfor %}
Il dump delle variabili avverrà solo se l’impostazione debug (in config.yml) è true. Questo vuol dire che,
per impostazione predefinita, il dump avverrà in ambiente dev, ma non in prod.
Formati di template
I template sono un modo generico per rendere contenuti in qualsiasi formato. Pur usando nella maggior parte
dei casi i template per rendere contenuti HTML, un template può generare altrettanto facilmente Javascript, CSS,
XML o qualsiasi altro formato desiderato.
Per esempio, la stessa “risorsa” spesso è resa in molti formati diversi. Per rendere una pagina in XML, basta
includere il formato nel nome del template:
• nome del template XML: AcmeArticleBundle:Article:index.xml.twig
• nome del file del template XML: index.xml.twig
In realtà, questo non è niente più che una convenzione sui nomi e il template non è effettivamente resto in modo
diverso in base al suo formato.
In molti casi, si potrebbe voler consentire a un singolo controllore di rendere formati diversi, in base al “formato
di richiesta”. Per questa ragione, una soluzione comune è fare come segue:
public function indexAction()
{
$format = $this->getRequest()->getRequestFormat();
2.1. Libro
99
Symfony2 documentation Documentation, Release 2
return $this->render(’AcmeBlogBundle:Blog:index.’.$format.’.twig’);
}
Il metodo getRequestFormat dell’oggetto Request ha come valore predefinito html, ma può restituire
qualsiasi altro formato, in base al formato richiesto dall’utente. Il formato di richiesta è spesso gestito dalle
rotte, quando una rotta è configurata in modo che /contact imposti il formato di richiesta a html, mentre
/contact.xml lo imposti a xml. Per maggiori informazioni, vedere Esempi avanzati nel capitolo delle rotte.
Per creare collegamenti che includano il formato, usare la chiave _format come parametro:
• Twig
<a href="{{ path(’article_show’, {’id’: 123, ’_format’: ’pdf’}) }}">
versione PDF
</a>
• PHP
<a href="<?php echo $view[’router’]->generate(’article_show’, array(’id’ => 123, ’_format’ =>
versione PDF
</a>
Considerazioni finali
Il motore dei template in Symfony è un potente strumento, che può essere usato ogni volta che occorre generare
contenuto relativo alla presentazione in HTML, XML o altri formati. Sebbene i template siano un modo comune
per generare contenuti in un controllore, i loro utilizzo non è obbligatorio. L’oggetto Response restituito da un
controllore può essere creato con o senza l’uso di un template:
// crea un oggetto Response il cui contenuto è il template reso
$response = $this->render(’AcmeArticleBundle:Article:index.html.twig’);
// crea un oggetto Response il cui contenuto è semplice testo
$response = new Response(’contenuto della risposta’);
Il motore dei template di Symfony è molto flessibile e mette a disposizione due sistemi di template: i tradizionali
template PHP e i potenti e raffinati template Twig. Entrambi supportano una gerarchia di template e sono distribuiti
con un ricco insieme di funzioni helper, capaci di eseguire i compiti più comuni.
Complessivamente, l’argomento template dovrebbe essere considerato come un potente strumento a disposizione.
In alcuni casi, si potrebbe non aver bisogno di rendere un template , in Symfony2, questo non è assolutamente un
problema.
Imparare di più con il ricettario
• Come usare PHP al posto di Twig nei template
• Come personalizzare le pagine di errore
2.1.8 Database e Doctrine (“Il modello”)
Ammettiamolo, uno dei compiti più comuni e impegnativi per qualsiasi applicazione implica la persistenza e la
lettura di informazioni da un database. Fortunatamente, Symfony è integrato con Doctrine, una libreria il cui unico
scopo è quello di fornire potenti strumenti per facilitare tali compiti. In questo capitolo, si imparerà la filosofia
alla base di Doctrine e si vedrà quanto possa essere facile lavorare con un database.
Note: Doctrine è totalmente disaccoppiato da Symfony e il suo utilizzo è facoltativo. Questo capitolo è tutto su
Doctrine, che si prefigge lo scopo di consentire una mappatura tra oggetti un database relazionale (come MySQL,
PostgreSQL o Microsoft SQL). Se si preferisce l’uso di query grezze, lo si può fare facilmente, come spiegato nella
ricetta “Come usare il livello DBAL di Doctrine”.
100
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
Si possono anche persistere dati su MongoDB usando la libreria ORM Doctrine. Per ulteriori informazioni, leggere
la documentazione “DoctrineMongoDBBundle”.
Un semplice esempio: un prodotto
Il modo più facile per capire come funziona Doctrine è quello di vederlo in azione. In questa sezione, configureremo un database, creeremo un oggetto Product, lo persisteremo nel database e lo recupereremo da esso.
Codice con l’esempio
Se si vuole seguire l’esempio in questo capitolo, creare un bundle AcmeStoreBundle tramite:
php app/console generate:bundle --namespace=Acme/StoreBundle
Configurazione del database
Prima di iniziare, occorre configurare le informazioni sulla connessione al database. Per convenzione, questa
informazione solitamente è configurata in un file app/config/parameters.yml:
# app/config/parameters.yml
parameters:
database_driver:
pdo_mysql
database_host:
localhost
database_name:
test_project
database_user:
root
database_password: password
Note: La definizione della configurazione tramite parameters.yml è solo una convenzione. I parametri
definiti in tale file sono riferiti dal file di configurazione principale durante le impostazioni iniziali di Doctrine:
doctrine:
dbal:
driver:
host:
dbname:
user:
password:
%database_driver%
%database_host%
%database_name%
%database_user%
%database_password%
Separando le informazioni sul database in un file a parte, si possono mantenere facilmente diverse versioni del file
su ogni server. Si possono anche facilmente memorizzare configurazioni di database (o altre informazioni sensibili) fuori dal proprio progetto, come per esempio dentro la configurazione di Apache. Per ulteriori informazioni,
vedere Configurare parametri esterni nel contenitore dei servizi.
Ora che Doctrine ha informazioni sul proprio database, si può fare in modo che crei il database al posto nostro:
php app/console doctrine:database:create
Creare una classe entità
Supponiamo di star costruendo un’applicazione in cui i prodotti devono essere mostrati. Senza nemmeno pensare
a Doctrine o ai database, già sappiamo di aver bisogno di un oggetto Product che rappresenti questi prodotti.
Creare questa classe dentro la cartella Entity del proprio AcmeStoreBundle:
// src/Acme/StoreBundle/Entity/Product.php
namespace Acme\StoreBundle\Entity;
2.1. Libro
101
Symfony2 documentation Documentation, Release 2
class Product
{
protected $name;
protected $price;
protected $description;
}
La classe, spesso chiamata “entità” (che vuol dire una classe di base che contiene dati), è semplice e aiuta a
soddisfare i requisiti di business di necessità di prodotti della propria applicazione. Questa classe non può ancora
essere persistita in un database, è solo una semplice classe PHP.
Tip: Una volta imparati i concetti dietro a Doctrine, si può fare in modo che Doctrine crei questa classe entità al
posto nostro:
php app/console doctrine:generate:entity --entity="AcmeStoreBundle:Product" --fields="name:string(
Aggiungere informazioni di mappatura
Doctrine consente di lavorare con i database in un modo molto più interessante rispetto al semplice recupero di
righe da tabelle basate su colonne in un array. Invece, Doctrine consente di persistere interi oggetti sul database
e di recuperare interi oggetti dal database. Funziona mappando una classe PHP su una tabella di database e le
proprietà della classe PHP sulle colonne della tabella:
Per fare in modo che Doctrine possa fare ciò, occorre solo creare dei “meta-dati”, ovvero la configurazione che
dice esattamente a Doctrine come la classe Product e le sue proprietà debbano essere mappate sul database.
Questi meta-dati possono essere specificati in diversi formati, inclusi YAML, XML o direttamente dentro la classe
Product, tramite annotazioni:
Note: Un bundle può accettare un solo formato di definizione dei meta-dati. Per esempio, non è possibile
mischiare definizioni di meta-dati in YAML con definizioni tramite annotazioni.
• Annotations
// src/Acme/StoreBundle/Entity/Product.php
namespace Acme\StoreBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\Table(name="product")
*/
102
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
class Product
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* @ORM\Column(type="string", length=100)
*/
protected $name;
/**
* @ORM\Column(type="decimal", scale=2)
*/
protected $price;
/**
* @ORM\Column(type="text")
*/
protected $description;
}
• YAML
# src/Acme/StoreBundle/Resources/config/doctrine/Product.orm.yml
Acme\StoreBundle\Entity\Product:
type: entity
table: product
id:
id:
type: integer
generator: { strategy: AUTO }
fields:
name:
type: string
length: 100
price:
type: decimal
scale: 2
description:
type: text
• XML
<!-- src/Acme/StoreBundle/Resources/config/doctrine/Product.orm.xml -->
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
<entity name="Acme\StoreBundle\Entity\Product" table="product">
<id name="id" type="integer" column="id">
<generator strategy="AUTO" />
</id>
<field name="name" column="name" type="string" length="100" />
<field name="price" column="price" type="decimal" scale="2" />
<field name="description" column="description" type="text" />
</entity>
</doctrine-mapping>
2.1. Libro
103
Symfony2 documentation Documentation, Release 2
Tip: Il nome della tabella è facoltativo e, se omesso, sarà determinato automaticamente in base al nome della
classe entità.
Doctrine consente di scegliere tra una grande varietà di tipi di campo, ognuno con le sue opzioni Per informazioni
sui tipi disponibili, vedere la sezione book-doctrine-field-types.
See Also:
Si può anche consultare la Documentazione di base sulla mappatura di Doctrine per tutti i dettagli sulla
mappatura. Se si usano le annotazioni, occorrerà aggiungere a ogni annotazione il prefisso ORM\ (p.e.
ORM\Column(..)), che non è mostrato nella documentazione di Doctrine. Occorrerà anche includere
l’istruzione use Doctrine\ORM\Mapping as ORM;, che importa il prefisso ORM delle annotazioni.
Caution: Si faccia attenzione che il nome della classe e delle proprietà scelti non siano mappati a delle parole
riservate di SQL (come group o user). Per esempio, se il proprio nome di classe entità è Group, allora il
nome predefinito della tabella sarà group, che causerà un errore SQL in alcuni sistemi di database. Vedere la
Documentazione sulle parole riservate di SQL per sapere come fare correttamente escape di tali nomi.
Note: Se si usa un’altra libreria o programma che utilizza le annotazioni (come Doxygen), si dovrebbe inserire
l’annotazione @IgnoreAnnotation nella classe, per indicare a Symfony quali annotazioni ignorare.
Per esempio, per evitare che l’annotazione @fn sollevi un’eccezione, aggiungere il seguente:
/**
* @IgnoreAnnotation("fn")
*/
class Product
Generare getter e setter
Sebbene ora Doctrine sappia come persistere un oggetto Product nel database, la classe stessa non è molto utile.
Poiché Product è solo una normale classe PHP, occorre creare dei metodi getter e setter (p.e. getName(),
setName()) per poter accedere alle sue proprietà (essendo le proprietà protette). Fortunatamente, Doctrine può
farlo al posto nostro, basta eseguire:
php app/console doctrine:generate:entities Acme/StoreBundle/Entity/Product
Il comando si assicura che i getter e i setter siano generati per la classe Product. È un comando sicuro, lo si
può eseguire diverse volte: genererà i getter e i setter solamente se non esistono (ovvero non sostituirà eventuali
metodi già presenti).
Di più su doctrine:generate:entities
Con il comando doctrine:generate:entities si può:
• generare getter e setter,
• generare classi repository configurate con l’annotazione @ORM\Entity(repositoryClass="..."),
• generare il costruttore appropriato per relazioni 1:n e n:m.
Il comando doctrine:generate:entities salva una copia di backup del file originale
Product.php, chiamata Product.php~. In alcuni casi, la presenza di questo file può causare un errore
“Cannot redeclare class”. Il file può essere rimosso senza problemi.
Si noti che non è necessario usare questo comando. Doctrine non si appoggia alla generazione di codice.
Come con le normali classi PHP, occorre solo assicurarsi che le proprietà protected/private abbiano metodi
getter e setter. Questo comando è stato creato perché è una cosa comune da fare quando si usa Doctrine.
Si possono anche generare tutte le entità note (cioè ogni classe PHP con informazioni di mappatura di Doctrine)
di un bundle o di un intero spazio dei nomi:
104
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
php app/console doctrine:generate:entities AcmeStoreBundle
php app/console doctrine:generate:entities Acme
Note: Doctrine non si cura se le proprietà sono protected o private, o se siano o meno presenti getter o
setter per una proprietà. I getter e i setter sono generati qui solo perché necessari per interagire col proprio oggetto
PHP.
Creare tabelle e schema del database
Ora si ha una classe Product usabile, con informazioni di mappatura che consentono a Doctrine di sapere
esattamente come persisterla. Ovviamente, non si ha ancora la corrispondente tabella product nel proprio
database. Fortunatamente, Doctrine può creare automaticamente tutte le tabelle del database necessarie a ogni
entità nota nella propria applicazione. Per farlo, eseguire:
php app/console doctrine:schema:update --force
Tip: Questo comando è incredibilmente potente. Confronta ciò che il proprio database dovrebbe essere (basandosi sulle informazioni di mappatura delle entità) con ciò che effettivamente è, quindi genera le istruzioni SQL
necessarie per aggiornare il database e portarlo a ciò che dovrebbe essere. In altre parole, se si aggiunge una
nuova proprietà con meta-dati di mappatura a Product e si esegue nuovamente il task, esso genererà l’istruzione
“alter table” necessaria per aggiungere questa nuova colonna alla tabella product esistente.
Un modo ancora migliore per trarre vantaggio da questa funzionalità è tramite le migrazioni, che consentono di
generare queste istruzioni SQL e di memorizzarle in classi di migrazione, che possono essere eseguite sistematicamente sul proprio server di produzione, per poter tracciare e migrare il proprio schema di database in modo sicuro
e affidabile.
Il proprio database ora ha una tabella product pienamente funzionante, con le colonne corrispondenti ai metadati specificati.
Persistere gli oggetti nel database
Ora che l’entità Product è stata mappata alla corrispondente tabella product, si è pronti per persistere i dati
nel database. Da dentro un controllore, è molto facile. Aggiungere il seguente metodo a DefaultController
del bundle:
1
2
3
4
// src/Acme/StoreBundle/Controller/DefaultController.php
use Acme\StoreBundle\Entity\Product;
use Symfony\Component\HttpFoundation\Response;
// ...
5
6
7
8
9
10
11
public function createAction()
{
$product = new Product();
$product->setName(’Pippo Pluto’);
$product->setPrice(’19.99’);
$product->setDescription(’Lorem ipsum dolor’);
12
$em = $this->getDoctrine()->getEntityManager();
$em->persist($product);
$em->flush();
13
14
15
16
return new Response(’Creato prodotto con id ’.$product->getId());
17
18
}
2.1. Libro
105
Symfony2 documentation Documentation, Release 2
Note: Se si sta seguendo questo esempio, occorrerà creare una rotta che punti a questa azione, per poterla vedere
in azione.
Analizziamo questo esempio:
• righe 8-11 In questa sezione, si istanzia e si lavora con l’oggetto $product, come qualsiasi altro normale
oggetto PHP;
• riga 13 Questa riga recupera l’oggetto gestore di entità di Doctrine, responsabile della gestione del processo
di persistenza e del recupero di oggetti dal database;
• riga 14 Il metodo persist() dice a Doctrine di “gestire” l’oggetto $product. Questo non fa (ancora)
eseguire una query sul database.
• riga 15 Quando il metodo flush() è richiamato, Doctrine cerca tutti gli oggetti che sta gestendo, per
vedere se hanno bisogno di essere persistiti sul database. In questo esempio, l’oggetto $product non è
stato ancora persistito, quindi il gestore di entità esegue una query INSERT e crea una riga nella tabella
product.
Note: Di fatto, essendo Doctrine consapevole di tutte le proprie entità gestite, quando si chiama il metodo
flush(), esso calcola un insieme globale di modifiche ed esegue le query più efficienti possibili. Per esempio,
se si persiste un totale di 100 oggetti Product e quindi si richiama flush(), Doctrine creerà una singola
istruzione e la riuserà per ogni inserimento. Questo pattern si chiama Unit of Work ed è utilizzato in virtù della
sua velocità ed efficienza.
Quando si creano o aggiornano oggetti, il flusso è sempre lo stesso. Nella prossima sezione, si vedrà come
Doctrine sia abbastanza intelligente da usare una query UPDATE se il record è già esistente nel database.
Tip: Doctrine fornisce una libreria che consente di caricare dati di test nel proprio progetto (le cosiddette “fixture”). Per informazioni, vedere DoctrineFixturesBundle.
Recuperare oggetti dal database
Recuperare un oggetto dal database è ancora più facile. Per esempio, supponiamo di aver configurato una rotta
per mostrare uno specifico Product, in base al valore del suo id:
public function showAction($id)
{
$product = $this->getDoctrine()
->getRepository(’AcmeStoreBundle:Product’)
->find($id);
if (!$product) {
throw $this->createNotFoundException(’Nessun prodotto trovato per l\’id ’.$id);
}
// fare qualcosa, come passare l’oggetto $product a un template
}
Quando si cerca un particolare tipo di oggetto, si usa sempre quello che è noto come il suo “repository”. Si può
pensare a un repository come a una classe PHP il cui unico compito è quello di aiutare nel recuperare entità di una
certa classe. Si può accedere all’oggetto repository per una classe entità tramite:
$repository = $this->getDoctrine()
->getRepository(’AcmeStoreBundle:Product’);
Note: La stringa AcmeStoreBundle:Product è una scorciatoia utilizzabile ovunque in Doctrine al posto
del nome intero della classe dell’entità (cioè Acme\StoreBundle\Entity\Product). Questo funzionerà
finché le proprie entità rimarranno sotto lo spazio dei nomi Entity del proprio bundle.
106
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
Una volta ottenuto il proprio repository, si avrà accesso a tanti metodi utili:
// cerca per chiave primaria (di solito "id")
$product = $repository->find($id);
// nomi di metodi dinamici per cercare in base al valore di una colonna
$product = $repository->findOneById($id);
$product = $repository->findOneByName(’pippo’);
// trova *tutti* i prodotti
$products = $repository->findAll();
// trova un gruppo di prodotti in base a un valore arbitrario di una colonna
$products = $repository->findByPrice(19.99);
Note: Si possono ovviamente fare anche query complesse, su cui si può avere maggiori informazioni nella
sezione book-doctrine-queries.
Si possono anche usare gli utili metodi findBy e findOneBy per recuperare facilmente oggetti in base a
condizioni multiple:
// cerca un prodotto in base a nome e prezzo
$product = $repository->findOneBy(array(’name’ => ’pippo’, ’price’ => 19.99));
// cerca tutti i prodotti in base al nome, ordinati per prezzo
$product = $repository->findBy(
array(’name’ => ’pippo’),
array(’price’ => ’ASC’)
);
Tip: Quando si rende una pagina, si può vedere il numero di query eseguite nell’angolo inferiore destro della
barra di debug del web.
Cliccando sull’icona, si aprirà il profiler, che mostrerà il numero esatto di query eseguite.
Aggiornare un oggetto
Una volta che Doctrine ha recuperato un oggetto, il suo aggiornamento è facile. Supponiamo di avere una rotta
che mappi un id di prodotto a un’azione di aggiornamento in un controllore:
public function updateAction($id)
{
$em = $this->getDoctrine()->getEntityManager();
$product = $em->getRepository(’AcmeStoreBundle:Product’)->find($id);
2.1. Libro
107
Symfony2 documentation Documentation, Release 2
if (!$product) {
throw $this->createNotFoundException(’Nessun prodotto trovato per l\’id ’.$id);
}
$product->setName(’Nome del nuovo prodotto!’);
$em->flush();
return $this->redirect($this->generateUrl(’homepage’));
}
L’aggiornamento di un oggetto si svolge in tre passi:
1. recuperare l’oggetto da Doctrine;
2. modificare l’oggetto;
3. richiamare flush() sul gestore di entità
Si noti che non è necessario richiamare $em->persist($product). Ricordiamo che questo metodo dice
semplicemente a Doctrine di gestire o “osservare” l’oggetto $product. In questo caso, poiché l’oggetto
$product è stato recuperato da Doctrine, è già gestito.
Cancellare un oggetto
La cancellazione di un oggetto è molto simile, ma richiede una chiamata al metodo remove() del gestore delle
entità:
$em->remove($product);
$em->flush();
Come ci si potrebbe aspettare, il metodo remove() rende noto a Doctrine che si vorrebbe rimuovere la data
entità dal database. Tuttavia, la query DELETE non viene realmente eseguita finché non si richiama il metodo
flush().
Cercare gli oggetti
Abbiamo già visto come l’oggetto repository consenta di eseguire query di base senza alcuno sforzo:
$repository->find($id);
$repository->findOneByName(’Pippo’);
Ovviamente, Doctrine consente anche di scrivere query più complesse, usando Doctrine Query Language (DQL).
DQL è simile a SQL, tranne per il fatto che bisognerebbe immaginare di stare cercando uno o più oggetti di una
classe entità (p.e. Product) e non le righe di una tabella (p.e. product).
Durante una ricerca in Doctrine, si hanno due opzioni: scrivere direttamente query Doctrine, oppure usare il Query
Builder di Doctrine.
Cercare oggetti con DQL
Si immagini di voler cercare dei prodotti, ma solo quelli che costino più di 19.99, ordinati dal più economico al
più caro. Da dentro un controllore, fare come segue:
$em = $this->getDoctrine()->getEntityManager();
$query = $em->createQuery(
’SELECT p FROM AcmeStoreBundle:Product p WHERE p.price > :price ORDER BY p.price ASC’
)->setParameter(’price’, ’19.99’);
$products = $query->getResult();
108
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
Se ci si trova a proprio agio con SQL, DQL dovrebbe sembrare molto naturale. La maggiore differenze è
che occorre pensare in termini di “oggetti” invece che di righe di database. Per questa ragione, si cerca da
AcmeStoreBundle:Product e poi si usa p come suo alias.
Il metodo getResult() restituisce un array di risultati. Se si cerca un solo oggetto, si può usare invece il
metodo getSingleResult():
$product = $query->getSingleResult();
Caution:
Il
metodo
getSingleResult()
solleva
un’eccezione
Doctrine\ORM\NoResultException
se
non
ci
sono
risultati
e
una
Doctrine\ORM\NonUniqueResultException se c’è più di un risultato. Se si usa questo metodo, si
potrebbe voler inserirlo in un blocco try-catch, per assicurarsi che sia restituito un solo risultato (nel caso in
cui sia possibile che siano restituiti più risultati):
$query = $em->createQuery(’SELECT ....’)
->setMaxResults(1);
try {
$product = $query->getSingleResult();
} catch (\Doctrine\Orm\NoResultException $e) {
$product = null;
}
// ...
La sintassi DQL è incredibilmente potente e consente di fare join tra entità (l’argomento relazioni sarà affrontato
successivamente), raggruppare, ecc. Per maggiori informazioni, vedere la documentazione ufficiale di Doctrine
Doctrine Query Language.
Impostare i parametri
Si prenda nota del metodo setParameter(). Lavorando con Doctrine, è sempre una buona idea impostare ogni valore esterno come “segnaposto”, come è stato fatto nella query precedente:
... WHERE p.price > :price ...
Si può quindi impostare il valore del segnaposto price, richiamando il metodo setParameter():
->setParameter(’price’, ’19.99’)
L’uso di parametri al posto dei valori diretti nella stringa della query serve a prevenire attacchi di tipo SQL
injection e andrebbe fatto sempre. Se si usano più parametri, si possono impostare i loro valori in una volta
sola, usando il metodo setParameters():
->setParameters(array(
’price’ => ’19.99’,
’name’ => ’Pippo’,
))
Usare query builder di Doctrine
Invece di scrivere direttamente le query, si può invece usare QueryBuilder, per fare lo stesso lavoro usando un’interfaccia elegante e orientata agli oggetti. Se si usa un IDE, si può anche trarre vantaggio dall’autocompletamento durante la scrittura dei nomi dei metodi. Da dentro un controllore:
$repository = $this->getDoctrine()
->getRepository(’AcmeStoreBundle:Product’);
$query = $repository->createQueryBuilder(’p’)
->where(’p.price > :price’)
2.1. Libro
109
Symfony2 documentation Documentation, Release 2
->setParameter(’price’, ’19.99’)
->orderBy(’p.price’, ’ASC’)
->getQuery();
$products = $query->getResult();
L’oggetto QueryBuilder contiene tutti i metodi necessari per costruire la propria query. Richiamando il metodo
getQuery(), query builder restituisce un normale oggetto Query, che è lo stesso oggetto costruito direttamente
nella sezione precedente.
Per maggiori informazioni su query builder, consultare la documentazione di Doctrine Query Builder.
Classi repository personalizzate
Nelle sezioni precedenti, si è iniziato costruendo e usando query più complesse da dentro un controllore. Per
isolare, testare e riusare queste query, è una buona idea creare una classe repository personalizzata per la propria
entità e aggiungere metodi, come la propria logica di query, al suo interno.
Per farlo, aggiungere il nome della classe del repository alla propria definizione di mappatura.
• Annotations
// src/Acme/StoreBundle/Entity/Product.php
namespace Acme\StoreBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity(repositoryClass="Acme\StoreBundle\Repository\ProductRepository")
*/
class Product
{
//...
}
• YAML
# src/Acme/StoreBundle/Resources/config/doctrine/Product.orm.yml
Acme\StoreBundle\Entity\Product:
type: entity
repositoryClass: Acme\StoreBundle\Repository\ProductRepository
# ...
• XML
<!-- src/Acme/StoreBundle/Resources/config/doctrine/Product.orm.xml -->
<!-- ... -->
<doctrine-mapping>
<entity name="Acme\StoreBundle\Entity\Product"
repository-class="Acme\StoreBundle\Repository\ProductRepository">
<!-- ... -->
</entity>
</doctrine-mapping>
Doctrine può generare la classe repository per noi, eseguendo lo stesso comando usato precedentemente per generare i metodi getter e setter mancanti:
php app/console doctrine:generate:entities Acme
Quindi, aggiungere un nuovo metodo, chiamato findAllOrderedByName(), alla classe repository appena
generata. Questo metodo cercherà tutte le entità Product, ordinate alfabeticamente.
110
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
// src/Acme/StoreBundle/Repository/ProductRepository.php
namespace Acme\StoreBundle\Repository;
use Doctrine\ORM\EntityRepository;
class ProductRepository extends EntityRepository
{
public function findAllOrderedByName()
{
return $this->getEntityManager()
->createQuery(’SELECT p FROM AcmeStoreBundle:Product p ORDER BY p.name ASC’)
->getResult();
}
}
Tip: Si può accedere al gestore di entità tramite $this->getEntityManager() da dentro il repository.
Si può usare il metodo appena creato proprio come i metodi predefiniti del repository:
$em = $this->getDoctrine()->getEntityManager();
$products = $em->getRepository(’AcmeStoreBundle:Product’)
->findAllOrderedByName();
Note: Quando si usa una classe repository personalizzata, si ha ancora accesso ai metodi predefiniti di ricerca,
come find() e findAll().
Relazioni e associazioni tra entità
Supponiamo che i prodotti nella propria applicazione appartengano tutti a una “categoria”. In questo caso, occorrerà un oggetto Category e un modo per per mettere in relazione un oggetto Product con un oggetto
Category. Iniziamo creando l’entità Category. Sapendo che probabilmente occorrerà persistere la classe
tramite Doctrine, lasciamo che sia Doctrine stesso a creare la classe.
php app/console doctrine:generate:entity --entity="AcmeStoreBundle:Category" --fields="name:string
Questo task genera l’entità Category, con un campo id, un campo name e le relative funzioni getter e setter.
Meta-dati di mappatura delle relazioni
Per correlare le entità Category e Product, iniziamo creando una proprietà products nella classe
Category:
• Annotations
// src/Acme/StoreBundle/Entity/Category.php
// ...
use Doctrine\Common\Collections\ArrayCollection;
class Category
{
// ...
/**
* @ORM\OneToMany(targetEntity="Product", mappedBy="category")
*/
protected $products;
public function __construct()
2.1. Libro
111
Symfony2 documentation Documentation, Release 2
{
$this->products = new ArrayCollection();
}
}
• YAML
# src/Acme/StoreBundle/Resources/config/doctrine/Category.orm.yml
Acme\StoreBundle\Entity\Category:
type: entity
# ...
oneToMany:
products:
targetEntity: Product
mappedBy: category
# non dimenticare di inizializzare la collection nel metodo __construct() dell’entità
Primo, poiché un oggetto Category sarà collegato a diversi oggetti Product, va aggiutna una proprietà array
products, per contenere questi oggetti Product. Di nuovo, non va fatto perché Doctrine ne abbia bisogno,
ma perché ha senso nell’applicazione che ogni Category contenga un array di oggetti Product.
Note: Il codice nel metodo __construct() è importante, perché Doctrine esige che la proprietà $products
sia un oggetto ArrayCollection. Questo oggetto sembra e si comporta quasi esattamente come un array, ma
ha un po’ di flessibilità in più. Se non sembra confortevole, niente paura. Si immagini solamente che sia un
array.
Tip: Il valore targetEntity, usato in precedenza sul decoratore, può riferirsi a qualsiasi entità con uno spazio
dei nomi valido, non solo a entità definite nella stessa classe. Per riferirsi a entità definite in classi diverse, inserire
uno spazio dei nomi completo come targetEntity.
Poi, poiché ogni classe Product può essere in relazione esattamente con un oggetto Category, si deve aggiungere una proprietà $category alla classe Product:
• Annotations
// src/Acme/StoreBundle/Entity/Product.php
// ...
class Product
{
// ...
/**
* @ORM\ManyToOne(targetEntity="Category", inversedBy="products")
* @ORM\JoinColumn(name="category_id", referencedColumnName="id")
*/
protected $category;
}
• YAML
# src/Acme/StoreBundle/Resources/config/doctrine/Product.orm.yml
Acme\StoreBundle\Entity\Product:
type: entity
# ...
manyToOne:
category:
targetEntity: Category
inversedBy: products
joinColumn:
112
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
name: category_id
referencedColumnName: id
Infine, dopo aver aggiunto una nuova proprietà sia alla classe Category che a quella Product, dire a Doctrine
di generare i metodi mancanti getter e setter:
php app/console doctrine:generate:entities Acme
Ignoriamo per un momento i meta-dati di Doctrine. Abbiamo ora due classi, Category e Product, con
una relazione naturale uno-a-molti. La classe Category contiene un array di oggetti Product e l’oggetto
Product può contenere un oggetto Category. In altre parole, la classe è stata costruita in un modo che ha
senso per le proprie necessità. Il fatto che i dati necessitino di essere persistiti su un database è sempre secondario.
Diamo ora uno sguardo ai meta-dati nella proprietà $category della classe Product. Qui le informazioni
dicono a Doctrine che la classe correlata è Category e che dovrebbe memorizzare il valore id della categoria
in un campo category_id della tabella product. In altre parole, l’oggetto Category correlato sarà memorizzato nella proprietà $category, ma dietro le quinte Doctrine persisterà questa relazione memorizzando il
valore dell’id della categoria in una colonna category_id della tabella product.
I meta-dati della proprietà $products dell’oggetto Category è meno importante e dicono semplicemente a
Doctrine di cercare la proprietà Product.category per sapere come mappare la relazione.
2.1. Libro
113
Symfony2 documentation Documentation, Release 2
Prima di continuare, accertarsi di dire a Doctrine di aggiungere la nuova tabella category la nuova colonna
product.category_id e la nuova chiave esterna:
php app/console doctrine:schema:update --force
Note: Questo task andrebbe usato solo durante lo sviluppo. Per un metodo più robusto di aggiornamento sistematico del proprio database di produzione, leggere Migrazioni doctrine.
Salvare le entità correlate
Vediamo ora il codice in azione. Immaginiamo di essere dentro un controllore:
// ...
use Acme\StoreBundle\Entity\Category;
use Acme\StoreBundle\Entity\Product;
use Symfony\Component\HttpFoundation\Response;
// ...
class DefaultController extends Controller
{
public function createProductAction()
{
$category = new Category();
$category->setName(’Prodotti principali’);
$product = new Product();
$product->setName(’Pippo’);
$product->setPrice(19.99);
// correlare questo prodotto alla categoria
$product->setCategory($category);
$em = $this->getDoctrine()->getEntityManager();
$em->persist($category);
$em->persist($product);
$em->flush();
return new Response(
’Creati prodotto con id: ’.$product->getId().’ e categoria con id: ’.$category->getId(
);
}
}
Una riga è stata aggiunta alle tabelle category e product. La colonna product.category_id del nuovo
prodotto è impostata allo stesso valore di id della nuova categoria. Doctrine gestisce la persistenza di tale relazione per noi.
Recuperare gli oggetti correlati
Quando occorre recuperare gli oggetti correlati, il flusso è del tutto simile a quello precedente. Recuperare prima
un oggetto $product e poi accedere alla sua Category correlata:
public function showAction($id)
{
$product = $this->getDoctrine()
->getRepository(’AcmeStoreBundle:Product’)
->find($id);
$categoryName = $product->getCategory()->getName();
114
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
// ...
}
In questo esempio, prima di cerca un oggetto Product in base al suo id.
Questo implica una
query solo per i dati del prodotto e idrata l’oggetto $product con tali dati.
Poi, quando si
richiama $product->getCategory()->getName(), Doctrine effettua una seconda query, per trovare la
Category correlata con il Product. Prepara l’oggetto $category e lo restituisce.
Quello che è importante è il fatto che si ha facile accesso al prodotto correlato con la categoria, ma i dati della
categoria non sono recuperati finché la categoria non viene richiesta (processo noto come “lazy load”).
Si può anche cercare nella direzione opposta:
public function showProductAction($id)
{
$category = $this->getDoctrine()
->getRepository(’AcmeStoreBundle:Category’)
->find($id);
$products = $category->getProducts();
// ...
}
In questo caso succedono le stesse cose: prima si cerca un singolo oggetto Category, poi Doctrine esegue
una seconda query per recuperare l’oggetto Product correlato, ma solo quando/se richiesto (cioè al richiamo di
->getProducts()). La variabile $products è un array di tutti gli oggetti Product correlati con il dato
oggetto Category tramite il loro valore category_id.
2.1. Libro
115
Symfony2 documentation Documentation, Release 2
Relazioni e classi proxy
Questo “lazy load” è possibile perché, quando necessario, Doctrine restituisce un oggetto “proxy” al posto
del vero oggetto. Guardiamo di nuovo l’esempio precedente:
$product = $this->getDoctrine()
->getRepository(’AcmeStoreBundle:Product’)
->find($id);
$category = $product->getCategory();
// mostra "Proxies\AcmeStoreBundleEntityCategoryProxy"
echo get_class($category);
Questo oggetto proxy estende il vero oggetto Category e sembra e si comporta esattamente nello
stesso modo. La differenza è che, usando un oggetto proxy, Doctrine può rimandare la query per
i dati effettivi di Category fino a che non sia effettivamente necessario (cioè fino alla chiamata di
$category->getName()).
Le classy proxy sono generate da Doctrine e memorizzate in cache. Sebbene probabilmente non si noterà
mai che il proprio oggetto $category sia in realtà un oggetto proxy, è importante tenerlo a mente.
Nella prossima sezione, quando si recuperano i dati di prodotto e categoria in una volta sola (tramite una
join), Doctrine restituirà il vero oggetto Category, poiché non serve alcun lazy load.
Join di record correlati
Negli esempi precedenti, sono state eseguite due query: una per l’oggetto originale (p.e. una Category) e una
per gli oggetti correlati (p.e. gli oggetti Product).
Tip: Si ricordi che è possibile vedere tutte le query eseguite durante una richiesta, tramite la barra di web debug.
Ovviamente, se si sa in anticipo di aver bisogno di accedere a entrambi gli oggetti, si può evitare la seconda query,
usando una join nella query originale. Aggiungere il seguente metodo alla classe ProductRepository:
// src/Acme/StoreBundle/Repository/ProductRepository.php
public function findOneByIdJoinedToCategory($id)
{
$query = $this->getEntityManager()
->createQuery(’
SELECT p, c FROM AcmeStoreBundle:Product p
JOIN p.category c
WHERE p.id = :id’
)->setParameter(’id’, $id);
try {
return $query->getSingleResult();
} catch (\Doctrine\ORM\NoResultException $e) {
return null;
}
}
Ora si può usare questo metodo nel proprio controllore per cercare un oggetto Product e la relativa Category
con una sola query:
public function showAction($id)
{
$product = $this->getDoctrine()
->getRepository(’AcmeStoreBundle:Product’)
->findOneByIdJoinedToCategory($id);
116
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
$category = $product->getCategory();
// ...
}
Ulteriori informazioni sulle associazioni
Questa sezione è stata un’introduzione a un tipo comune di relazione tra entità, la relazione uno-a-molti. Per
dettagli ed esempi più avanzati su come usare altri tipi di relazioni (p.e. uno-a-uno, molti-a-molti), vedere la
Documentazione sulla mappatura delle associazioni.
Note:
Se si usano le annotazioni, occorrerà aggiungere a tutte le annotazioni il prefisso ORM\ (p.e.
ORM\OneToMany), che non si trova nella documentazione di Doctrine. Occorrerà anche includere l’istruzione
use Doctrine\ORM\Mapping as ORM;, che importa il prefisso delle annotazioni ORM.
Configurazione
Doctrine è altamente configurabile, sebbene probabilmente non si avrà nemmeno bisogno di preoccuparsi di gran
parte delle sue opzioni. Per saperne di più sulla configurazione di Doctrine, vedere la sezione Doctrine del manuale
di riferimento.
Callback del ciclo di vita
A volte, occorre eseguire un’azione subito prima o subito dopo che un entità sia inserita, aggiornata o cancellata.
Questi tipi di azioni sono noti come callback del “ciclo di vita”, perché sono metodi callback che occorre eseguire
durante i diversi stadi del ciclo di vita di un’entità (p.e. l’entità è inserita, aggiornata, cancellata, eccetera).
Se si usano le annotazioni per i meta-dati, iniziare abilitando i callback del ciclo di vita. Questo non è necessario
se si usa YAML o XML per la mappatura:
/**
* @ORM\Entity()
* @ORM\HasLifecycleCallbacks()
*/
class Product
{
// ...
}
Si può ora dire a Doctrine di eseguire un metodo su uno degli eventi disponibili del ciclo di vita. Per esempio,
supponiamo di voler impostare una colonna di data created alla data attuale, solo quando l’entità è persistita la
prima volta (cioè è inserita):
• Annotations
/**
* @ORM\prePersist
*/
public function setCreatedValue()
{
$this->created = new \DateTime();
}
• YAML
# src/Acme/StoreBundle/Resources/config/doctrine/Product.orm.yml
Acme\StoreBundle\Entity\Product:
type: entity
2.1. Libro
117
Symfony2 documentation Documentation, Release 2
# ...
lifecycleCallbacks:
prePersist: [ setCreatedValue ]
• XML
<!-- src/Acme/StoreBundle/Resources/config/doctrine/Product.orm.xml -->
<!-- ... -->
<doctrine-mapping>
<entity name="Acme\StoreBundle\Entity\Product">
<!-- ... -->
<lifecycle-callbacks>
<lifecycle-callback type="prePersist" method="setCreatedValue" />
</lifecycle-callbacks>
</entity>
</doctrine-mapping>
Note: L’esempio precedente presume che sia stata creata e mappata una proprietà created (non mostrata qui).
Ora, appena prima che l’entità sia persistita per la prima volta, Doctrine richiamerà automaticamente questo
metodo e il campo created sarà valorizzato con la data attuale.
Si può ripetere questa operazione per ogni altro evento del ciclo di vita:
• preRemove
• postRemove
• prePersist
• postPersist
• preUpdate
• postUpdate
• postLoad
• loadClassMetadata
Per maggiori informazioni sul significato di questi eventi del ciclo di vita e in generale sui callback del ciclo di
vita, vedere la Documentazione sugli eventi del ciclo di vita
Callback del ciclo di vita e ascoltatori di eventi
Si noti che il metodo setCreatedValue() non riceve parametri. Questo è sempre il caso di callback del
ciclo di vita ed è intenzionale: i callback del ciclo di vita dovrebbero essere metodi semplici, riguardanti la
trasformazione interna di dati nell’entità (p.e. impostare un campo di creazione/aggiornamento, generare un
valore per uno slug).
Se occorre un lavoro più pesante, come eseguire un log o inviare una email, si dovrebbe registrare una classe
esterna come ascoltatore di eventi e darle accesso a qualsiasi risorsa necessaria. Per maggiori informazioni,
vedere Registrare ascoltatori e sottoscrittori di eventi.
Estensioni di Doctrine: Timestampable, Sluggable, ecc.
Doctrine è alquanto flessibile e diverse estensioni di terze parti sono disponibili, consentendo di eseguire facilmente compiti comuni e ripetitivi sulle proprie entità. Sono inclusi Sluggable, Timestampable, Loggable, Translatable e Tree.
Per maggiori informazioni su come trovare e usare tali estensioni, vedere la ricetta usare le estensioni comuni di
Doctrine.
118
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
Riferimento sui tipi di campo di Doctrine
Doctrine ha un gran numero di tipi di campo a disposizione. Ognuno di questi mappa un tipo di dato PHP su un
tipo specifico di colonna in qualsiasi database si utilizzi. I seguenti tipi sono supportati in Doctrine:
• Stringhe
– string (per stringhe più corte)
– text (per stringhe più lunghe)
• Numeri
– integer
– smallint
– bigint
– decimal
– float
• Date e ore (usare un oggetto DateTime per questi campi in PHP)
– date
– time
– datetime
• Altri tipi
– boolean
– object (serializzato e memorizzato in un campo CLOB)
– array (serializzato e memorizzato in un campo CLOB)
Per maggiori informazioni, vedere Documentazione sulla mappatura dei tipi.
Opzioni dei campi
Ogni campo può avere un insieme di opzioni da applicare. Le opzioni disponibili includono type (predefinito
string), name, length, unique e nullable. Vediamo alcuni esempi con le annotazioni:
• Annotations
/**
* Un campo stringa con lunghezza 255 che non può essere nullo
* (riflette i valori predefiniti per le opzioni "type", "length" e *nullable*)
*
* @ORM\Column()
*/
protected $name;
/**
* Un campo stringa con lunghezza 150 che persiste su una colonna "email_address"
* e ha un vincolo di unicità.
*
* @ORM\Column(name="email_address", unique="true", length="150")
*/
protected $email;
• YAML
2.1. Libro
119
Symfony2 documentation Documentation, Release 2
fields:
# Un campo stringa con lunghezza 255 che non può essere nullo
# (riflette i valori predefiniti per le opzioni "type", "length" e *nullable*)
# l’attributo type è necessario nelle definizioni yaml
name:
type: string
# Un campo stringa con lunghezza 150 che persiste su una colonna "email_address"
# e ha un vincolo di unicità.
email:
type: string
column: email_address
length: 150
unique: true
Note: Ci sono alcune altre opzioni, non elencate qui. Per maggiori dettagli, vedere la Documentazione sulla
mappatura delle proprietà
Comandi da console
L’integrazione con l’ORM Doctrine2 offre diversi comandi da console, sotto lo spazio dei nomi doctrine. Per
vedere la lista dei comandi, si può eseguire la console senza parametri:
php app/console
Verrà mostrata una lista dei comandi disponibili, molti dei quali iniziano col prefisso doctrine:. Si possono trovare maggiori informazioni eseguendo il comando help. Per esempio, per ottenere dettagli sul task
doctrine:database:create, eseguire:
php app/console help doctrine:database:create
Alcuni task interessanti sono:
• doctrine:ensure-production-settings - verifica se l’ambiente attuale sia configurato efficientemente per la produzione. Dovrebbe essere sempre eseguito nell’ambiente prod:
php app/console doctrine:ensure-production-settings --env=prod
• doctrine:mapping:import - consente a Doctrine l’introspezione di un database esistente e di creare
quindi le informazioni di mappatura. Per ulteriori informazioni, vedere Come generare entità da una base
dati esistente.
• doctrine:mapping:info - elenca tutte le entità di cui Doctrine è a conoscenza e se ci sono o meno
errori di base con la mappatura.
• doctrine:query:dql e doctrine:query:sql - consente l’esecuzione di query DQL o SQL direttamente dalla linea di comando.
Note: Per poter caricare fixture nel proprio database, occorrerà avere il bundle DoctrineFixturesBundle
installato. Per sapere come farlo, leggere la voce “DoctrineFixturesBundle” della documentazione.
Riepilogo
Con Doctrine, ci si può concentrare sui propri oggetti e su come siano utili nella propria applicazione e preoccuparsi della persistenza su database in un secondo momento. Questo perché Doctrine consente di usare qualsiasi
oggetto PHP per tenere i propri dati e si appoggia su meta-dati di mappatura per mappare i dati di un oggetto su
una particolare tabella di database.
120
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
Sebbene Doctrine giri intorno a un semplice concetto, è incredibilmente potente, consentendo di creare query
complesse e sottoscrivere eventi che consentono di intraprendere diverse azioni, mentre gli oggetti viaggiano
lungo il loro ciclo di vita della persistenza.
Per maggiori informazioni su Doctrine, vedere la sezione Doctrine del ricettario, che include i seguenti articoli:
• DoctrineFixturesBundle
• Estensioni di Doctrine: Timestampable: Sluggable, Translatable, ecc.
2.1.9 Test
Ogni volta che si scrive una nuova riga di codice, si aggiungono potenzialmente nuovi bug. Per costruire applicazioni migliori e più affidabili, si dovrebbe sempre testare il proprio codice, usando sia i test funzionali che quelli
unitari.
Il framework dei test PHPUnit
Symfony2 si integra con una libreria indipendente, chiamata PHPUnit, per fornire un ricco framework per i test.
Questo capitolo non approfondisce PHPUnit stesso, che ha comunque un’eccellente documentazione.
Note: Symfony2 funziona con PHPUnit 3.5.11 o successivi, ma per testare il codice del nucleo di Symfony
occorre la versione 3.6.4.
Ogni test, sia esso unitario o funzionale, è una classe PHP, che dovrebbe trovarsi in una sotto-cartella Tests/ del
proprio bundle. Seguendo questa regola, si possono eseguire tutti i test della propria applicazione con il seguente
comando:
# specifica la cartella di configurazione nella linea di comando
$ phpunit -c app/
L’opzione -c dice a PHPUnit di cercare nella cartella app/ un file di configurazione. Chi fosee curioso di
conoscere le opzioni di PHPUnit, può dare uno sguardo al file app/phpunit.xml.dist.
Tip: Si può generare la copertura del codice, con l’opzione --coverage-html.
Test unitari
Un test unitario è solitamente un test di una specifica classe PHP. Se si vuole testare il comportamento generale
della propria applicazione, vedere la sezione dei Test funzionali.
La scrittura di test unitari in Symfony2 non è diversa dalla scrittura standard di test unitari in PHPUnit. Si
supponga, per esempio, di avere una classe incredibilmente semplice, chiamata Calculator, nella cartella
Utility/ del proprio bundle:
// src/Acme/DemoBundle/Utility/Calculator.php
namespace Acme\DemoBundle\Utility;
class Calculator
{
public function add($a, $b)
{
return $a + $b;
}
}
Per testarla, creare un file CalculatorTest nella cartella Tests/Utility del proprio bundle:
2.1. Libro
121
Symfony2 documentation Documentation, Release 2
// src/Acme/DemoBundle/Tests/Utility/CalculatorTest.php
namespace Acme\DemoBundle\Tests\Utility;
use Acme\DemoBundle\Utility\Calculator;
class CalculatorTest extends \PHPUnit_Framework_TestCase
{
public function testAdd()
{
$calc = new Calculator();
$result = $calc->add(30, 12);
// asserisce che il calcolatore aggiunga correttamente i numeri!
$this->assertEquals(42, $result);
}
}
Note: Per convenzione, si raccomanda di replicare la struttura di cartella di un bundle nella sua sotto-cartella
Tests/. Quindi, se si testa una classe nella cartella Utility/ del proprio bundle, mettere il test nella cartella
Tests/Utility/.
Proprio come per l’applicazione reale, l’autoloading è abilitato automaticamente tramite il file
bootstrap.php.cache (come configurato in modo predefinito nel file phpunit.xml.dist).
Anche eseguire i test per un dato file o una data cartella è molto facile:
# eseguire tutti i test nella cartella Utility
$ phpunit -c app src/Acme/DemoBundle/Tests/Utility/
# eseguire i test per la classe Calculator
$ phpunit -c app src/Acme/DemoBundle/Tests/Utility/CalculatorTest.php
# eseguire tutti i test per l’intero bundle
$ phpunit -c app src/Acme/DemoBundle/
Test funzionali
I test funzionali verificano l’integrazione dei diversi livelli di un’applicazione (dalle rotte alle viste). Non differiscono dai test unitari per quello che riguarda PHPUnit, ma hanno un flusso di lavoro molto specifico:
• Fare una richiesta;
• Testare la risposta;
• Cliccare su un collegamento o inviare un form;
• Testare la risposta;
• Ripetere.
Un primo test funzionale
I test funzionali sono semplici file PHP, che tipicamente risiedono nella cartella Tests/Controller del proprio bundle. Se si vogliono testare le pagine gestite dalla propria classe DemoController, si inizi creando un
file DemoControllerTest.php, che estende una classe speciale WebTestCase.
Per esempio, l’edizione standard di Symfony2 fornisce un semplice test funzionale per il suo DemoController
(DemoControllerTest), fatto in questo modo:
122
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
// src/Acme/DemoBundle/Tests/Controller/DemoControllerTest.php
namespace Acme\DemoBundle\Tests\Controller;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
class DemoControllerTest extends WebTestCase
{
public function testIndex()
{
$client = static::createClient();
$crawler = $client->request(’GET’, ’/demo/hello/Fabien’);
$this->assertTrue($crawler->filter(’html:contains("Hello Fabien")’)->count() > 0);
}
}
Tip: Per eseguire i test funzionali, la classe WebTestCase inizializza il kernel dell’applicazione. Nella maggior
parte dei casi, questo avviene in modo automatico. Tuttavia, se il proprio kenerl si trova in una cartella non
standard, occorre modificare il file phpunit.xml.dist e impostare nella variabile d’ambiente KERNEL_DIR
la cartella del proprio kernel:
<phpunit
<!-- ... -->
<php>
<server name="KERNEL_DIR" value="/percorso/della/propria/applicazione/" />
</php>
<!-- ... -->
</phpunit>
Il metodo createClient() restituisce un client, che è come un browser da usare per visitare il proprio sito:
$crawler = $client->request(’GET’, ’/demo/hello/Fabien’);
Il metodo request() (vedere di più sil metodo della richiesta) restituisce un oggetto
Symfony\Component\DomCrawler\Crawler, che può essere usato per selezionare elementi nella
risposta, per cliccare su collegamenti e per inviare form.
Tip: Il crawler può essere usato solo se il contenuto della risposta è un documento XML o HTML. Per altri tipi
di contenuto, richiamare $client->getResponse()->getContent().
Cliccare su un collegamento, seleziondolo prima con il Crawler, usando o un’espressione XPath o un selettore
CSS, quindi usando il Client per cliccarlo. Per esempio, il codice seguente trova tutti i collegamenti con il testo
Greet, quindi sceglie il secondo e infine lo clicca:
$link = $crawler->filter(’a:contains("Greet")’)->eq(1)->link();
$crawler = $client->click($link);
Inviare un form è molto simile: selezionare il bottone di un form, eventualmente sovrascrivere alcuni valori del
form e inviare il form corrispondente:
$form = $crawler->selectButton(’submit’)->form();
// impostare alcuni valori
$form[’name’] = ’Lucas’;
$form[’form_name[subject]’] = ’Bella per te!’;
// inviare il form
$crawler = $client->submit($form);
2.1. Libro
123
Symfony2 documentation Documentation, Release 2
Tip: Il form può anche gestire caricamenti di file e contiene metodi utili per riempire diversi tipi di campi (p.e.
select() e tick()). Per maggiori dettagli, vedere la sezione Form più avanti.
Ora che si è in grado di navigare facilmente nell’applicazione, usare le asserzioni per testare che faccia effettivamente quello che ci si aspetta. Usare il Crawler per fare asserzioni sul DOM:
// Asserisce che la risposta corrisponda a un dato selettore CSS.
$this->assertTrue($crawler->filter(’h1’)->count() > 0);
Oppure, testare direttamente il contenuto della risposta, se si vuole solo asserire che il contenuto debba contenere
del testo o se la risposta non è un documento XML/HTML:
$this->assertRegExp(’/Hello Fabien/’, $client->getResponse()->getContent());
Di più sul metodo request():
La firma completa del metodo request() è:
request(
$method,
$uri,
array $parameters = array(),
array $files = array(),
array $server = array(),
$content = null,
$changeHistory = true
)
L’array server contiene i valori grezzi che ci si aspetta di trovare normalmente nell’array superglobale
$_SERVER di PHP. Per esempio, per impostare gli header HTTP Content-Type e Referer, passare i seguenti:
$client->request(
’GET’,
’/demo/hello/Fabien’,
array(),
array(),
array(
’CONTENT_TYPE’ => ’application/json’,
’HTTP_REFERER’ => ’/foo/bar’,
)
);
Lavorare con il client dei test
Il client dei test emula un client HTTP, come un browser, ed effettua richieste all’applicazione Symfony2:
$crawler = $client->request(’GET’, ’/hello/Fabien’);
Il metodo request() accetta come parametri il metodo HTTP e un URL e restituisce un’istanza di Crawler.
Usare il crawler per cercare elementi del DOM nella risposta. Questi elementi possono poi essere usati per cliccare
su collegamenti e inviare form:
$link = $crawler->selectLink(’Vai da qualche parte...’)->link();
$crawler = $client->click($link);
$form = $crawler->selectButton(’validare’)->form();
$crawler = $client->submit($form, array(’name’ => ’Fabien’));
124
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
I metodi click() e submit() restituiscono entrambi un oggetto Crawler. Questi metodi sono il modo
migliore per navigare un’applicazione, perché si occupano di diversi dettagli, come il metodo HTTP di un form e
il fornire un’utile API per caricare file.
Tip: Gli oggetti Link e Form nel crawler saranno approfonditi nella sezione Crawler, più avanti.
Il metodo request() può anche essere usto per simulare direttamente l’invio di form o per eseguire richieste
più complesse:
// Invio diretto di form
$client->request(’POST’, ’/submit’, array(’name’ => ’Fabien’));
// Invio di form di con caricamento di file
use Symfony\Component\HttpFoundation\File\UploadedFile;
$photo = new UploadedFile(
’/path/to/photo.jpg’,
’photo.jpg’,
’image/jpeg’,
123
);
// oppure
$photo = array(
’tmp_name’ => ’/path/to/photo.jpg’,
’name’ => ’photo.jpg’,
’type’ => ’image/jpeg’,
’size’ => 123,
’error’ => UPLOAD_ERR_OK
);
$client->request(
’POST’,
’/submit’,
array(’name’ => ’Fabien’),
array(’photo’ => $photo)
);
// Eseguire richieste DELETE requests e passare header HTTP
$client->request(
’DELETE’,
’/post/12’,
array(),
array(),
array(’PHP_AUTH_USER’ => ’username’, ’PHP_AUTH_PW’ => ’pa$$word’)
);
Infine, ma non meno importante, si può forzare l’esecuzione di ogni richiesta nel suo processo PHP, per evitare
effetti collaterali quando si lavora con molti client nello stess script:
$client->insulate();
Browser
Il client supporta molte operazioni eseguibili in un browser reale:
$client->back();
$client->forward();
$client->reload();
// Pulisce tutti i cookie e la cronologia
$client->restart();
2.1. Libro
125
Symfony2 documentation Documentation, Release 2
Accesso agli oggetti interni
Se si usa il client per testare la propria applicazione, si potrebbe voler accedere agli oggetti interni del client:
$history
= $client->getHistory();
$cookieJar = $client->getCookieJar();
I possono anche ottenere gli oggetti relativi all’ultima richiesta:
$request = $client->getRequest();
$response = $client->getResponse();
$crawler = $client->getCrawler();
Se le richieste non sono isolate, si può accedere agli oggetti Container e Kernel:
$container = $client->getContainer();
$kernel
= $client->getKernel();
Accesso al contenitore
È caldamente raccomandato che un test funzionale testi solo la risposta. Ma sotto alcune rare circostanze, si
potrebbe voler accedere ad alcuni oggetti interni, per scrivere asserzioni. In questi casi, si può accedere al dependency injection container:
$container = $client->getContainer();
Attenzione, perché questo non funziona se si isola il client o se si usa un livello HTTP. Per un elenco di servizi
disponibili nell’applicazione, usare il comando container:debug.
Tip: Se l’informazione che occorre verificare è disponibile nel profiler, si usi invece quest’ultimo.
Accedere ai dati del profilatore
A ogni richiesta, il profiler di Symfony raccoglie e memorizza molti dati, che riguardano la gestione interna della
richiesta stessa. Per esempio, il profilatore può essere usato per verificare che una data pagina esegua meno di un
certo numero di query alla base dati.
Si può ottenere il profilatore dell’ultima richiesta in questo modo:
$profile = $client->getProfile();
Per dettagli specifici sull’uso del profilatore in un test, vedere la ricetta Come usare il profilatore nei test funzionali.
Rinvii
Quando una richiesta restituisce una risposta di rinvio, il client la segue automaticamente. Se si vuole esaminare la
rispostsa prima del rinvio, si può forzare il client a non seguire i rinvii, usando il metodo followRedirect():
$crawler = $client->followRedirect(false);
Quando il client non segue i rinvvi, lo si può forzare con il metodo followRedirects():
$client->followRedirects();
126
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
Il crawler
Un’istanza del crawler è creata automaticamente quando si esegue una richiesta con un client. Consente di attraversare i documenti HTML, selezionare nodi, trovare collegamenti e form.
Attraversamento
Come jQuery, il crawler dispone di metodi per attraversare il DOM di documenti HTML/XML. Per esempio, per
estrarre tutti gli elementi input[type=submit], trovarne l’ultimo e quindi selezionare il suo genitore:
$newCrawler = $crawler->filter(’input[type=submit]’)
->last()
->parents()
->first()
;
Ci sono molti altri metodi a disposizione:
Metodo
filter(’h1.title’)
filterXpath(’h1’)
eq(1)
first()
last()
siblings()
nextAll()
previousAll()
parents()
children()
reduce($lambda)
Descrizione
Nodi corrispondenti al selettore CSS
Nodi corrispondenti all’espressione XPath
Nodi per l’indice specificato
Primo nodo
Ultimo nodo
Fratelli
Tutti i fratelli successivi
Tutti i fratelli precedenti
Genitori
Figli
Nodi per cui la funzione non restituisce false
Si può iterativamente restringere la selezione del nodo, concatenando le chiamate ai metodi, perché ogni metodo
restituisce una nuova istanza di Crawler per i nodi corrispondenti:
$crawler
->filter(’h1’)
->reduce(function ($node, $i)
{
if (!$node->getAttribute(’class’)) {
return false;
}
})
->first();
Tip:
Usare la funzione count() per ottenere il numero di nodi memorizzati in un crawler:
count($crawler)
Estrarre informazioni
Il crawler può estrarre informazioni dai nodi:
// Restituisce il valore dell’attributo del primo nodo
$crawler->attr(’class’);
// Restituisce il valore del nodo del primo nodo
$crawler->text();
// Estrae un array di attributi per tutti i nodi (_text restituisce il valore del nodo)
2.1. Libro
127
Symfony2 documentation Documentation, Release 2
$crawler->extract(array(’_text’, ’href’));
// Esegue una funzione lambda per ogni nodo e restituisce un array di risultati
$data = $crawler->each(function ($node, $i)
{
return $node->getAttribute(’href’);
});
Collegamenti
Si possono selezionare collegamenti coi metodi di attraversamento, ma la scorciatoia selectLink() è spesso
più conveniente:
$crawler->selectLink(’Clicca qui’);
Seleziona i collegamenti che contengono il testo dato, oppure le immagini cliccabili per cui l’attributi alt contiene
il testo dato. Come gli altri metodi filtro, restituisce un altro oggetto Crawler.
Una volta selezionato un collegamento, si ha accesso a uno speciale oggetto Link, che ha utili metodi specifici per
i collegamenti (come getMethod() e getUri()). Per cliccare sul collegamento, usare il metodo click()
di Client e passargli un oggetto Link:
$link = $crawler->selectLink(’Click here’)->link();
$client->click($link);
Form
Come per i collegamenti, si possono selezionare i form col metodo selectButton():
$buttonCrawlerNode = $crawler->selectButton(’submit’);
Note: Si noti che si selezionano i bottoni dei form e non i form stessi, perché un form può avere più bottoni; se
si usa l’API di attraversamento, si tenga a mente che si deve cercare un bottone.
Il metodo selectButton() può selezionare i tag button e i tag input con attributo “submit”. Ha diverse
euristiche per trovarli:
• Il valore dell’attributo value;
• Il valore dell’attributo id o alt per le immagini;
• Il valore dell’attributo id o name per i tag button.
Quando si a un nodo che rappresenta un bottone, richiamare il metodo form() per ottenere un’istanza Form per
il form, che contiene il nodo bottone.
$form = $buttonCrawlerNode->form();
Quando si richiama il metodo form(), si può anche passare un array di valori di campi, che sovrascrivano quelli
predefiniti:
$form = $buttonCrawlerNode->form(array(
’name’
=> ’Fabien’,
’my_form[subject]’ => ’Symfony spacca!’,
));
Se si vuole emulare uno specifico metodo HTTP per il form, passarlo come secondo parametro:
$form = $buttonCrawlerNode->form(array(), ’DELETE’);
128
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
Il client puoi inviare istanze di Form:
$client->submit($form);
Si possono anche passare i valori dei campi come secondo parametro del metodo submit():
$client->submit($form, array(
’name’
=> ’Fabien’,
’my_form[subject]’ => ’Symfony spacca!’,
));
Per situazioni più complesse, usare l’istanza di Form come un array, per impostare ogni valore di campo individualmente:
// Cambiare il valore di un campo
$form[’name’] = ’Fabien’;
$form[’my_form[subject]’] = ’Symfony spacca!’;
C’è anche un’utile API per manipolare i valori dei campi, a seconda del tipo:
// Selezionare un’opzione o un radio
$form[’country’]->select(’France’);
// Spuntare un checkbox
$form[’like_symfony’]->tick();
// Caricare un file
$form[’photo’]->upload(’/path/to/lucas.jpg’);
Tip: Si possono ottenere i valori che saranno inviati, richiamando il metodo getValues(). I file caricati
sono disponibili in un array separato, restituito dal metodo getFiles(). Anche i metodi getPhpValues()
e getPhpFiles() restituiscono i valori inviati, ma nel formato di PHP (convertendo le chiavi con parentesi
quadre, p.e. my_form[subject] da, nella notazione degli array di PHP).
Configurazione dei test
Il client usato dai test funzionali crea un kernel che gira in uno speciale ambiente test. Sicomme Symfonu carica
app/config/config_test.yml in ambiente test, si possono modificare le impostazioni della propria
applicazione sperificatamente per i test.
Per esempio, swiftmailer è configurato in modo predefinito per non inviare le email in ambiente test. Lo si può
vedere sotto l’opzione di configurazione swiftmailer:
• YAML
# app/config/config_test.yml
# ...
swiftmailer:
disable_delivery: true
• XML
<!-- app/config/config_test.xml -->
<container>
<!-- ... -->
<swiftmailer:config disable-delivery="true" />
</container>
• PHP
2.1. Libro
129
Symfony2 documentation Documentation, Release 2
// app/config/config_test.php
// ...
$container->loadFromExtension(’swiftmailer’, array(
’disable_delivery’ => true
));
Si può anche cambiare l’ambiente predefinito (test) e sovrascrivere la modalità predefinita di debug (true)
passandoli come opzioni al metodo createClient():
$client = static::createClient(array(
’environment’ => ’my_test_env’,
’debug’
=> false,
));
Se la propria applicazione necessita di alcuni header HTTP, passarli come secondo parametro di
createClient():
$client = static::createClient(array(), array(
’HTTP_HOST’
=> ’en.example.com’,
’HTTP_USER_AGENT’ => ’MySuperBrowser/1.0’,
));
Si possono anche sovrascrivere gli header HTTP a ogni richiesta:
$client->request(’GET’, ’/’, array(), array(), array(
’HTTP_HOST’
=> ’en.example.com’,
’HTTP_USER_AGENT’ => ’MySuperBrowser/1.0’,
));
Tip: Il client dei test è disponibile come servizio nel contenitore, in ambiente test (o dovunque sia abilitata
l’opzione framework.test). Questo vuol dire che si può ridefinire completamente il servizio, qualora se ne avesse
la necessità.
Configurazione di PHPUnit
Ogni applicazione ha la sua configurazione di PHPUnit, memorizzata nel file phpunit.xml.dist. Si può
modificare tale file per cambiare i default, oppure creare un file phpunit.xml per aggiustare la configurazione
per la propria macchina locale.
Tip: Inserire il file phpunit.xml.dist nel proprio repository e ignorare il file phpunit.xml.
Per impostazione predefinita, solo i test memorizzati nei bundle “standard” sono eseguiti dal comando phpunit
(per “standard” si intendono i test sotto gli spazi dei nomi Vendor\*Bundle\Tests). Ma si possono facilmente
aggiungere altri spazi dei nomi. Per esempio, la configurazione seguente aggiunge i test per i bundle installati di
terze parti:
<!-- hello/phpunit.xml.dist -->
<testsuites>
<testsuite name="Project Test Suite">
<directory>../src/*/*Bundle/Tests</directory>
<directory>../src/Acme/Bundle/*Bundle/Tests</directory>
</testsuite>
</testsuites>
Per includere altri spazi dei nomi nella copertura del codice, modificare anche la sezione <filter>:
<filter>
<whitelist>
130
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
<directory>../src</directory>
<exclude>
<directory>../src/*/*Bundle/Resources</directory>
<directory>../src/*/*Bundle/Tests</directory>
<directory>../src/Acme/Bundle/*Bundle/Resources</directory>
<directory>../src/Acme/Bundle/*Bundle/Tests</directory>
</exclude>
</whitelist>
</filter>
Imparare di più con le ricette
• Come simulare un’autenticazione HTTP in un test funzionale
• Come testare l’interazione con diversi client
• Come usare il profilatore nei test funzionali
2.1.10 Validazione
La validazione è un compito molto comune nella applicazioni web. I dati inseriti nei form hanno bisogno di essere
validati. I dati hanno bisogno di essere validati anche prima di essere inseriti in una base dati o passati a un servizio
web.
Symfony2 ha un componente Validator , che rende questo compito facile e trasparente. Questo componente è
bastato sulle specifiche di validazione JSR303 Bean. Cosa? Specifiche Java in PHP? Proprio così, ma non è così
male come potrebbe sembrare. Vediamo come usarle in PHP.
Le basi della validazione
Il modo migliore per capire la validazione è quello di vederla in azione. Per iniziare, supponiamo di aver creato
un classico oggetto PHP, da usare in qualche parte della propria applicazione:
// src/Acme/BlogBundle/Entity/Author.php
namespace Acme\BlogBundle\Entity;
class Author
{
public $name;
}
Finora, questa è solo una normale classe, che ha una qualche utilità all’interno della propria applicazione. Lo
scopo della validazione è dire se i dati di un oggetto siano validi o meno. Per poterlo fare, occorre configurare una
lisa di regole (chiamate vincoli) che l’oggetto deve seguire per poter essere valido. Queste regole possono essere
specificate tramite diversi formati (YAML, XML, annotazioni o PHP).
Per esempio, per garantire che la proprietà $name non sia vuota, aggiungere il seguente:
• YAML
# src/Acme/BlogBundle/Resources/config/validation.yml
Acme\BlogBundle\Entity\Author:
properties:
name:
- NotBlank: ~
• Annotations
// src/Acme/BlogBundle/Entity/Author.php
use Symfony\Component\Validator\Constraints as Assert;
2.1. Libro
131
Symfony2 documentation Documentation, Release 2
class Author
{
/**
* @Assert\NotBlank()
*/
public $name;
}
• XML
<!-- src/Acme/BlogBundle/Resources/config/validation.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/s
<class name="Acme\BlogBundle\Entity\Author">
<property name="name">
<constraint name="NotBlank" />
</property>
</class>
</constraint-mapping>
• PHP
// src/Acme/BlogBundle/Entity/Author.php
use Symfony\Component\Validator\Mapping\ClassMetadata;
use Symfony\Component\Validator\Constraints\NotBlank;
class Author
{
public $name;
public static function loadValidatorMetadata(ClassMetadata $metadata)
{
$metadata->addPropertyConstraint(’name’, new NotBlank());
}
}
Tip: Anche le proprietà private e protette possono essere validati, così come i metodi “getter” (vedere validatorconstraint-targets).
Usare il servizio validator
Successivamente, per validare veramente un oggetto Author, usare il metodo validate sul servizio
validator (classe Symfony\Component\Validator\Validator). Il compito di validator è semplice: leggere i vincoli (cioè le regole) di una classe e verificare se i dati dell’oggetto soddisfi o no tali vincoli. Se
la validazione fallisce, viene restituito un array di errori. Prendiamo questo semplice esempio dall’interno di un
controllore:
use Symfony\Component\HttpFoundation\Response;
use Acme\BlogBundle\Entity\Author;
// ...
public function indexAction()
{
$author = new Author();
// ... fare qualcosa con l’oggetto $author
132
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
$validator = $this->get(’validator’);
$errors = $validator->validate($author);
if (count($errors) > 0) {
return new Response(print_r($errors, true));
} else {
return new Response(’L\’autore è valido! Sì!’);
}
}
Se la proprietà $name è vuota, si vedrà il seguente messaggio di errore:
Acme\BlogBundle\Author.name:
This value should not be blank
Se si inserisce un valore per la proprietà $name, apparirà il messaggio di successo.
Tip: La maggior parte delle volte, non si interagirà direttamente con il servizio validator, né ci si dovrà
occupare di stampare gli errori. La maggior parte delle volte, si userà indirettamente la validazione, durante la
gestione di dati inviati tramite form. Per maggiori informazioni, vedere Validazione e form.
Si può anche passare un insieme di errori in un template.
if (count($errors) > 0) {
return $this->render(’AcmeBlogBundle:Author:validate.html.twig’, array(
’errors’ => $errors,
));
} else {
// ...
}
Dentro al template, si può stampare la lista di errori, come necessario:
• Twig
{# src/Acme/BlogBundle/Resources/views/Author/validate.html.twig #}
<h3>L’autore ha i seguenti errori</h3>
<ul>
{% for error in errors %}
<li>{{ error.message }}</li>
{% endfor %}
</ul>
• PHP
<!-- src/Acme/BlogBundle/Resources/views/Author/validate.html.php -->
<h3>L’autore ha i seguenti errori</h3>
<ul>
<?php foreach ($errors as $error): ?>
<li><?php echo $error->getMessage() ?></li>
<?php endforeach; ?>
</ul>
Note:
Ogni errore di validazione (chiamato “violazione di vincolo”) è rappresentato da un oggetto
Symfony\Component\Validator\ConstraintViolation.
2.1. Libro
133
Symfony2 documentation Documentation, Release 2
Validazione e form
Il servizio validator può essere usato per validare qualsiasi oggetto. In realtà, tuttavia, solitamente si lavorerà con validator indirettamente, lavorando con i form. La libreria dei form di Symfony usa internamente il
servizio validator, per validare l’oggetto sottostante dopo che i valori sono stati inviati e collegati. Le violazioni dei vincoli sull’oggetto sono convertite in oggetti FieldError, che possono essere facilmente mostrati
con il proprio form. Il tipico flusso dell’invio di un form assomiglia al seguente, all’interno di un controllore:
use Acme\BlogBundle\Entity\Author;
use Acme\BlogBundle\Form\AuthorType;
use Symfony\Component\HttpFoundation\Request;
// ...
public function updateAction(Request $request)
{
$author = new Acme\BlogBundle\Entity\Author();
$form = $this->createForm(new AuthorType(), $author);
if ($request->getMethod() == ’POST’) {
$form->bindRequest($request);
if ($form->isValid()) {
// validazionoe passata, fare qualcosa con l’oggetto $author
return $this->redirect($this->generateUrl(’...’));
}
}
return $this->render(’BlogBundle:Author:form.html.twig’, array(
’form’ => $form->createView(),
));
}
Note: Questo esempio usa una classe AuthorType, non mostrata qui.
Per maggiori informazioni, vedere il capitolo sui Form.
Configurazione
La validazione in Symfony2 è abilitata per configurazione predefinita, ma si devono abilitare esplicitamente le
annotazioni, se le si usano per specificare i vincoli:
• YAML
# app/config/config.yml
framework:
validation: { enable_annotations: true }
• XML
<!-- app/config/config.xml -->
<framework:config>
<framework:validation enable_annotations="true" />
</framework:config>
• PHP
// app/config/config.php
$container->loadFromExtension(’framework’, array(’validation’ => array(
’enable_annotations’ => true,
)));
134
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
Vincoli
Il servizio validator è progettato per validare oggetti in base a vincoli (cioè regole). Per poter validare un
oggetto, basta mappare uno o più vincoli alle rispettive classi e quindi passarli al servizio validator.
Dietro le quinte, un vincolo è semplicemente un oggetto PHP che esegue un’istruzione assertiva. Nella vita reale,
un vincolo potrebbe essere “la torta non deve essere bruciata”. In Symfony2, i vincoli sono simili: sono asserzioni
sulla verità di una condizione. Dato un valore, un vincolo dirà se tale valore sia aderente o meno alle regole del
vincolo.
Vincoli supportati
Symfony2 dispone di un gran numero dei vincoli più comunemente necessari:
Vincoli di base
Questi sono i vincoli di base: usarli per asserire cose molto basilari sul valore delle proprietà o sui valori restituiti
dai metodi del proprio oggetto.
• NotBlank
• Blank
• NotNull
• Null
• True
• False
• Type
Vincoli stringhe
• Email
• MinLength
• MaxLength
• Url
• Regex
• Ip
Vincoli numerici
• Max
• Min
Vincoli date
• Date
• DateTime
• Time
2.1. Libro
135
Symfony2 documentation Documentation, Release 2
Vincoli di insiemi
• Choice
• Collection
• UniqueEntity
• Language
• Locale
• Country
Vincoli di file
• File
• Image
Altri vincoli
• Callback
• All
• UserPassword
• Valid
Si possono anche creare i propri vincoli personalizzati. L’argomento è coperto nell’articolo “Come creare vincoli
di validazione personalizzati” del ricettario.
Configurazione dei vincoli
Alcuni vincoli, come NotBlank, sono semplici, mentre altri, come Choice, hanno diverse opzioni di configurazione
disponibili. Supponiamo che la classe Author abbia un’altra proprietà, gender, che possa valore solo “M”
oppure “F”:
• YAML
# src/Acme/BlogBundle/Resources/config/validation.yml
Acme\BlogBundle\Entity\Author:
properties:
gender:
- Choice: { choices: [M, F], message: Scegliere un genere valido. }
• Annotations
// src/Acme/BlogBundle/Entity/Author.php
use Symfony\Component\Validator\Constraints as Assert;
class Author
{
/**
* @Assert\Choice(
choices = { "M", "F" },
*
message = "Scegliere un genere valido."
*
* )
*/
public $gender;
}
136
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
• XML
<!-- src/Acme/BlogBundle/Resources/config/validation.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/s
<class name="Acme\BlogBundle\Entity\Author">
<property name="gender">
<constraint name="Choice">
<option name="choices">
<value>M</value>
<value>F</value>
</option>
<option name="message">Scegliere un genere valido.</option>
</constraint>
</property>
</class>
</constraint-mapping>
• PHP
// src/Acme/BlogBundle/Entity/Author.php
use Symfony\Component\Validator\Mapping\ClassMetadata;
use Symfony\Component\Validator\Constraints\NotBlank;
class Author
{
public $gender;
public static function loadValidatorMetadata(ClassMetadata $metadata)
{
$metadata->addPropertyConstraint(’gender’, new Choice(array(
’choices’ => array(’M’, ’F’),
’message’ => ’Scegliere un genere valido.’,
)));
}
}
Le opzioni di un vincolo possono sempre essere passate come array. Alcuni vincoli, tuttavia, consentono anche
di passare il valore di una sola opzione, predefinita, al posto dell’array. Nel caso del vincolo Choice, l’opzione
choices può essere specificata in tal modo.
• YAML
# src/Acme/BlogBundle/Resources/config/validation.yml
Acme\BlogBundle\Entity\Author:
properties:
gender:
- Choice: [M, F]
• Annotations
// src/Acme/BlogBundle/Entity/Author.php
use Symfony\Component\Validator\Constraints as Assert;
class Author
{
/**
* @Assert\Choice({"M", "F"})
*/
protected $gender;
}
2.1. Libro
137
Symfony2 documentation Documentation, Release 2
• XML
<!-- src/Acme/BlogBundle/Resources/config/validation.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/s
<class name="Acme\BlogBundle\Entity\Author">
<property name="gender">
<constraint name="Choice">
<value>M</value>
<value>F</value>
</constraint>
</property>
</class>
</constraint-mapping>
• PHP
// src/Acme/BlogBundle/Entity/Author.php
use Symfony\Component\Validator\Mapping\ClassMetadata;
use Symfony\Component\Validator\Constraints\Choice;
class Author
{
protected $gender;
public static function loadValidatorMetadata(ClassMetadata $metadata)
{
$metadata->addPropertyConstraint(’gender’, new Choice(array(’M’, ’F’)));
}
}
Questo ha il solo scopo di rendere la configurazione delle opzioni più comuni di un vincolo più breve e rapida.
Se non si è sicuri di come specificare un’opzione, verificare la documentazione delle API per il vincolo relativo,
oppure andare sul sicuro passando sempre un array di opzioni (il primo metodo mostrato sopra).
Obiettivi dei vincoli
I vincoli possono essere applicati alle proprietà di una classe (p.e. $name) oppure a un metodo getter pubblico
(p.e. getFullName). Il primo è il modo più comune e facile, ma il secondo consente di specificare regole di
validazione più complesse.
Proprietà
La validazione delle proprietà di una classe è la tecnica di base. Symfony2 consente di validare proprietà private, protette o pubbliche. L’elenco seguente mostra come configurare la proprietà $firstName di una classe
Author, per avere almeno 3 caratteri.
• YAML
# src/Acme/BlogBundle/Resources/config/validation.yml
Acme\BlogBundle\Entity\Author:
properties:
firstName:
- NotBlank: ~
- MinLength: 3
• Annotations
138
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
// Acme/BlogBundle/Entity/Author.php
use Symfony\Component\Validator\Constraints as Assert;
class Author
{
/**
* @Assert\NotBlank()
* @Assert\MinLength(3)
*/
private $firstName;
}
• XML
<!-- src/Acme/BlogBundle/Resources/config/validation.xml -->
<class name="Acme\BlogBundle\Entity\Author">
<property name="firstName">
<constraint name="NotBlank" />
<constraint name="MinLength">3</constraint>
</property>
</class>
• PHP
// src/Acme/BlogBundle/Entity/Author.php
use Symfony\Component\Validator\Mapping\ClassMetadata;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\MinLength;
class Author
{
private $firstName;
public static function loadValidatorMetadata(ClassMetadata $metadata)
{
$metadata->addPropertyConstraint(’firstName’, new NotBlank());
$metadata->addPropertyConstraint(’firstName’, new MinLength(3));
}
}
Getter
I vincoli si possono anche applicare ai valori restituiti da un metodo. Symfony2 consente di aggiungere un vincolo
a qualsiasi metodo il cui nome inizi per “get” o “is”. In questa guida, si fa riferimento a questi due tipi di metodi
come “getter”.
Il vantaggio di questa tecnica è che consente di validare i proprio oggetti dinamicamente. Per esempio, supponiamo che ci si voglia assicurare che un campo password non corrisponda al nome dell’utente (per motivi di
sicurezza). Lo si può fare creando un metodo isPasswordLegal e asserendo che tale metodo debba restituire
true:
• YAML
# src/Acme/BlogBundle/Resources/config/validation.yml
Acme\BlogBundle\Entity\Author:
getters:
passwordLegal:
- "True": { message: "La password non può essere uguale al nome" }
• Annotations
2.1. Libro
139
Symfony2 documentation Documentation, Release 2
// src/Acme/BlogBundle/Entity/Author.php
use Symfony\Component\Validator\Constraints as Assert;
class Author
{
/**
* @Assert\True(message = "La password non può essere uguale al nome")
*/
public function isPasswordLegal()
{
// return true or false
}
}
• XML
<!-- src/Acme/BlogBundle/Resources/config/validation.xml -->
<class name="Acme\BlogBundle\Entity\Author">
<getter property="passwordLegal">
<constraint name="True">
<option name="message">La password non può essere uguale al nome</option>
</constraint>
</getter>
</class>
• PHP
// src/Acme/BlogBundle/Entity/Author.php
use Symfony\Component\Validator\Mapping\ClassMetadata;
use Symfony\Component\Validator\Constraints\True;
class Author
{
public static function loadValidatorMetadata(ClassMetadata $metadata)
{
$metadata->addGetterConstraint(’passwordLegal’, new True(array(
’message’ => ’La password non può essere uguale al nome’,
)));
}
}
Creare ora il metodo isPasswordLegal() e includervi la logica necessaria:
public function isPasswordLegal()
{
return ($this->firstName != $this->password);
}
Note: I lettori più attenti avranno notato che il prefisso del getter (“get” o “is”) viene omesso nella mappatura.
Questo consente di spostare il vincolo su una proprietà con lo stesso nome, in un secondo momento (o viceversa),
senza dover cambiare la logica di validazione.
Classi
Alcuni vincoli si applicano all’intera classe da validare. Per esempio, il vincolo Callback è un vincolo generico,
che si applica alla classe stessa. Quano tale classe viene validata, i metodi specifici di questo vincolo vengono
semplicemente eseguiti, in modo che ognuno possa fornire una validazione personalizzata.
140
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
Gruppi di validazione
Finora, si è stati in grado di aggiungere vincoli a una classe e chiedere se tale classe passasse o meno tutti i vincoli
definiti. In alcuni casi, tuttavia, occorre validare un oggetto solo per alcuni vincoli della sua classe. Per poterlo
fare, si può organizzare ogni vincolo in uno o più “gruppi di validazione” e quindi applicare la validazione solo su
un gruppo di vincoli.
Per esempio, si supponga di avere una classe User, usata sia quando un utente si registra che quando aggiorna
successivamente le sue informazioni:
• YAML
# src/Acme/BlogBundle/Resources/config/validation.yml
Acme\BlogBundle\Entity\User:
properties:
email:
- Email: { groups: [registration] }
password:
- NotBlank: { groups: [registration] }
- MinLength: { limit: 7, groups: [registration] }
city:
- MinLength: 2
• Annotations
// src/Acme/BlogBundle/Entity/User.php
namespace Acme\BlogBundle\Entity;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Validator\Constraints as Assert;
class User implements UserInterface
{
/**
* @Assert\Email(groups={"registration"})
*/
private $email;
/**
* @Assert\NotBlank(groups={"registration"})
* @Assert\MinLength(limit=7, groups={"registration"})
*/
private $password;
/**
* @Assert\MinLength(2)
*/
private $city;
}
• XML
<!-- src/Acme/BlogBundle/Resources/config/validation.xml -->
<class name="Acme\BlogBundle\Entity\User">
<property name="email">
<constraint name="Email">
<option name="groups">
<value>registration</value>
</option>
</constraint>
</property>
<property name="password">
<constraint name="NotBlank">
<option name="groups">
2.1. Libro
141
Symfony2 documentation Documentation, Release 2
<value>registration</value>
</option>
</constraint>
<constraint name="MinLength">
<option name="limit">7</option>
<option name="groups">
<value>registration</value>
</option>
</constraint>
</property>
<property name="city">
<constraint name="MinLength">7</constraint>
</property>
</class>
• PHP
// src/Acme/BlogBundle/Entity/User.php
namespace Acme\BlogBundle\Entity;
use
use
use
use
Symfony\Component\Validator\Mapping\ClassMetadata;
Symfony\Component\Validator\Constraints\Email;
Symfony\Component\Validator\Constraints\NotBlank;
Symfony\Component\Validator\Constraints\MinLength;
class User
{
public static function loadValidatorMetadata(ClassMetadata $metadata)
{
$metadata->addPropertyConstraint(’email’, new Email(array(
’groups’ => array(’registration’)
)));
$metadata->addPropertyConstraint(’password’, new NotBlank(array(
’groups’ => array(’registration’)
)));
$metadata->addPropertyConstraint(’password’, new MinLength(array(
’limit’ => 7,
’groups’ => array(’registration’)
)));
$metadata->addPropertyConstraint(’city’, new MinLength(3));
}
}
Con questa configurazione, ci sono due gruppi di validazione:
• Default - contiene i vincoli non assegnati ad altri gruppi;
• registration - contiene solo i vincoli sui campi email e password.
Per dire al validatore di usare uno specifico gruppo, passare uno o più nomi di gruppo come secondo parametro
del metodo validate():
$errors = $validator->validate($author, array(’registration’));
Ovviamente, di solito si lavorerà con la validazione in modo indiretto, tramite la libreria dei form. Per informazioni
su come usare i gruppi di validazione dentro ai form, vedere Gruppi di validatori.
Validare valori e array
Finora abbiamo visto come si possono validare oggetti interi. Ma a volte si vuole validare solo un semplice valore,
come verificare che una stringa sia un indirizzo email valido. Lo si può fare molto facilmente. Da dentro a un
142
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
controllore, assomiglia a questo:
// aggiungere questa riga in cima alla propria classe
use Symfony\Component\Validator\Constraints\Email;
public function addEmailAction($email)
{
$emailConstraint = new Email();
// tutte le opzioni sui vincoli possono essere impostate in questo modo
$emailConstraint->message = ’Invalid email address’;
// usa il validatore per validare il valore
$errorList = $this->get(’validator’)->validateValue($email, $emailConstraint);
if (count($errorList) == 0) {
// è un indirizzo email valido, fare qualcosa
} else {
// *non* è un indirizzo email valido
$errorMessage = $errorList[0]->getMessage()
// fare qualcosa con l’errore
}
// ...
}
Richiamando validateValue sul validatore, si può passare un valore grezzo e l’oggetto vincolo su cui si vuole
validare tale valore. Una lista completa di vincoli disponibili, così come i nomi completi delle classi per ciascun
vincolo, è disponibile nella sezione riferimento sui vincoli.
Il metodo validateValule restituisce un oggetto Symfony\Component\Validator\ConstraintViolationList,
che si comporta come un array di errori.
Ciascun errore della lista è un oggetto
Symfony\Component\Validator\ConstraintViolation, che contiene il messaggio di errore
nel suo metodo getMessage.
Considerazioni finali
validator di Symfony2 è uno strumento potente, che può essere sfruttato per garantire che i dati di qualsiasi
oggetto siano validi. La potenza dietro alla validazione risiede nei “vincoli”, che sono regole da applicare alle
proprietà o ai metodi getter del proprio oggetto. Sebbene la maggior parte delle volte si userà il framework della
validazione indirettamente, usando i form, si ricordi che può essere usato ovunque, per validare qualsiasi oggetto.
Imparare di più con le ricette
• Come creare vincoli di validazione personalizzati
2.1.11 Form
L’utilizzo dei form HTML è uno degli utilizzi più comuni e stimolanti per uno sviluppatore web. Symfony2 integra
un componente Form che permette di gestire facilmente i form. Con l’aiuto di questo capitolo si potrà creare da
zero un form complesso, e imparare le caratteristiche più importanti della libreria dei form.
Note: Il componente form di Symfony è una libreria autonoma che può essere usata al di fuori dei progetti
Symfony2. Per maggiori informazioni, vedere il Componente Form di Symfony2 su Github.
2.1. Libro
143
Symfony2 documentation Documentation, Release 2
Creazione di un form semplice
Supponiamo che si stia costruendo un semplice applicazione “elenco delle cose da fare” che dovrà visualizzare
le “attività”. Poiché gli utenti avranno bisogno di modificare e creare attività, sarà necessario costruire un form.
Ma prima di iniziare, si andrà a vedere la generica classe Task che rappresenta e memorizza i dati di una singola
attività:
// src/Acme/TaskBundle/Entity/Task.php
namespace Acme\TaskBundle\Entity;
class Task
{
protected $task;
protected $dueDate;
public function getTask()
{
return $this->task;
}
public function setTask($task)
{
$this->task = $task;
}
public function getDueDate()
{
return $this->dueDate;
}
public function setDueDate(\DateTime $dueDate = null)
{
$this->dueDate = $dueDate;
}
}
Note: Se si sta provando a digitare questo esempio, bisogna prima creare AcmeTaskBundle lanciando il
seguente comando (e accettando tutte le opzioni predefinite):
php app/console generate:bundle --namespace=Acme/TaskBundle
Questa classe è un “vecchio-semplice-oggetto-PHP”, perché finora non ha nulla a che fare con Symfony o qualsiasi
altra libreria. È semplicemente un normale oggetto PHP, che risolve un problema direttamente dentro la propria
applicazione (cioè la necessità di rappresentare un task nella propria applicazione). Naturalmente, alla fine di
questo capitolo, si sarà in grado di inviare dati all’istanza di un Task (tramite un form HTML), validare i suoi
dati e persisterli nella base dati.
Costruire il Form
Ora che la classe Task è stata creata, il prossimo passo è creare e visualizzare il form HTML. In Symfony2, lo
si fa costruendo un oggetto form e poi visualizzandolo in un template. Per ora, lo si può fare all’interno di un
controllore:
// src/Acme/TaskBundle/Controller/DefaultController.php
namespace Acme\TaskBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Acme\TaskBundle\Entity\Task;
use Symfony\Component\HttpFoundation\Request;
class DefaultController extends Controller
144
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
{
public function newAction(Request $request)
{
// crea un task fornendo alcuni dati fittizi per questo esempio
$task = new Task();
$task->setTask(’Write a blog post’);
$task->setDueDate(new \DateTime(’tomorrow’));
$form = $this->createFormBuilder($task)
->add(’task’, ’text’)
->add(’dueDate’, ’date’)
->getForm();
return $this->render(’AcmeTaskBundle:Default:new.html.twig’, array(
’form’ => $form->createView(),
));
}
}
Tip: Questo esempio mostra come costruire il form direttamente nel controllore. Più tardi, nella sezione “Creare
classi per i form”, si imparerà come costruire il form in una classe autonoma, metodo consigliato perché in questo
modo il form diventa riutilizzabile.
La creazione di un form richiede relativamente poco codice, perché gli oggetti form di Symfony2 sono costruiti
con un “costruttore di form”. Lo scopo del costruttore di form è quello di consentire di scrivere una semplice
“ricetta” per il form e fargli fare tutto il lavoro pesante della costruzione del form.
In questo esempio sono stati aggiunti due campi al form, task e dueDate, corrispondenti alle proprietà task
e dueDate della classe Task. È stato anche assegnato un “tipo” ciascuno (ad esempio text, date), che, tra le
altre cose, determina quale tag form HTML viene utilizzato per tale campo.
Symfony2 ha molti tipi predefiniti che verranno trattati a breve (see Tipi di campo predefiniti).
Visualizzare il Form
Ora che il modulo è stato creato, il passo successivo è quello di visualizzarlo. Questo viene fatto passando uno speciale oggetto form “view” al template (notare il $form->createView() nel controllore sopra) e utilizzando
una serie di funzioni helper per i form:
• Twig
{# src/Acme/TaskBundle/Resources/views/Default/new.html.twig #}
<form action="{{ path(’task_new’) }}" method="post" {{ form_enctype(form) }}>
{{ form_widget(form) }}
<input type="submit" />
</form>
• PHP
<!-- src/Acme/TaskBundle/Resources/views/Default/new.html.php -->
<form action="<?php echo $view[’router’]->generate(’task_new’) ?>" method="post" <?php echo $
<?php echo $view[’form’]->widget($form) ?>
<input type="submit" />
</form>
2.1. Libro
145
Symfony2 documentation Documentation, Release 2
Note: Questo esempio presuppone che sia stata creata una rotta chiamata task_new che punta al controllore
AcmeTaskBundle:Default:new che era stato creato precedentemente.
Questo è tutto! Scrivendo form_widget(form), ciascun campo del form viene reso, insieme a un’etichetta e
a un messaggio di errore (se presente). Per quanto semplice, questo metodo non è molto flessibile (ancora). Di
solito, si ha bisogno di rendere individualmente ciascun campo in modo da poter controllare la visualizzazione del
form. Si imparerà a farlo nella sezione “Rendere un form in un template”.
Prima di andare avanti, notare come il campo input task reso ha il value della proprietà task dall’oggetto
$task (ad esempio “Scrivi un post sul blog”). Questo è il primo compito di un form: prendere i dati da un
oggetto e tradurli in un formato adatto a essere reso in un form HTML.
Tip: Il sistema dei form è abbastanza intelligente da accedere al valore della proprietà protetta task attraverso
i metodi getTask() e setTask() della classe Task. A meno che una proprietà non sia pubblica, deve avere
un metodo “getter” e “setter” in modo che il componente form possa ottenere e mettere dati nella proprietà. Per
una proprietà booleana, è possibile utilizzare un metodo “isser” (ad esempio isPublished()) invece di un
getter ad esempio getPublished()).
Gestione dell’invio del form
Il secondo compito di un form è quello di tradurre i dati inviati dall’utente alle proprietà di un oggetto. Affinché
ciò avvenga, i dati inviati dall’utente devono essere associati al form. Aggiungere le seguenti funzionalità al
controllore:
// ...
public function newAction(Request $request)
{
// crea un nuovo oggetto $task (rimuove i dati fittizi)
$task = new Task();
$form = $this->createFormBuilder($task)
->add(’task’, ’text’)
->add(’dueDate’, ’date’)
->getForm();
if ($request->getMethod() == ’POST’) {
$form->bindRequest($request);
if ($form->isValid()) {
// esegue alcune azioni, come ad esempio salvare il task nel database
return $this->redirect($this->generateUrl(’task_success’));
}
}
146
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
// ...
}
Ora, quando si invia il form, il controllore associa i dati inviati al form, che traduce nuovamente i dati alle proprietà
task e dueDate dell’oggetto $task. Tutto questo avviene attraverso il metodo bindRequest().
Note: Appena viene chiamata bindRequest(), i dati inviati vengono immediatamente trasferiti all’oggetto
sottostante. Questo avviene indipendentemente dal fatto che i dati sottostanti siano validi o meno.
Questo controllore segue uno schema comune per gestire i form e ha tre possibili percorsi:
1. Quando in un browser inizia il caricamento di una pagina, il metodo request è GET e il form è semplicemente
creato e reso;
2. Quando l’utente invia il form (cioè il metodo è POST) con dati non validi (la validazione è trattata nella
sezione successiva), il form è associato e poi reso, questa volta mostrando tutti gli errori di validazione;
3. Quando l’utente invia il form con dati validi, il form viene associato e si ha la possibilità di eseguire alcune
azioni usando l’oggetto $task (ad esempio persistendo i dati nel database) prima di rinviare l’utente a
un’altra pagina (ad esempio una pagina “thank you” o “success”).
Note: Reindirizzare un utente dopo aver inviato con successo un form impedisce l’utente di essere in grado di
premere il tasto “aggiorna” e re-inviare i dati.
Validare un form
Nella sezione precedente, si è appreso come un form può essere inviato con dati validi o invalidi. In Symfony2,
la validazione viene applicata all’oggetto sottostante (per esempio Task). In altre parole, la questione non è se il
“form” è valido, ma se l’oggetto $task è valido o meno dopo che al form sono stati applicati i dati inviati. La
chiamata di $form->isValid() è una scorciatoia che chiede all’oggetto $task se ha dati validi o meno.
La validazione è fatta aggiungendo di una serie di regole (chiamate vincoli) a una classe. Per vederla in azione,
verranno aggiunti vincoli di validazione in modo che il campo task non possa essere vuoto e il campo dueDate
non possa essere vuoto e debba essere un oggetto DateTime valido.
• YAML
# Acme/TaskBundle/Resources/config/validation.yml
Acme\TaskBundle\Entity\Task:
properties:
task:
- NotBlank: ~
dueDate:
- NotBlank: ~
- Type: \DateTime
• Annotations
// Acme/TaskBundle/Entity/Task.php
use Symfony\Component\Validator\Constraints as Assert;
class Task
{
/**
* @Assert\NotBlank()
*/
public $task;
/**
* @Assert\NotBlank()
2.1. Libro
147
Symfony2 documentation Documentation, Release 2
* @Assert\Type("\DateTime")
*/
protected $dueDate;
}
• XML
<!-- Acme/TaskBundle/Resources/config/validation.xml -->
<class name="Acme\TaskBundle\Entity\Task">
<property name="task">
<constraint name="NotBlank" />
</property>
<property name="dueDate">
<constraint name="NotBlank" />
<constraint name="Type">
<value>\DateTime</value>
</constraint>
</property>
</class>
• PHP
// Acme/TaskBundle/Entity/Task.php
use Symfony\Component\Validator\Mapping\ClassMetadata;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\Type;
class Task
{
// ...
public static function loadValidatorMetadata(ClassMetadata $metadata)
{
$metadata->addPropertyConstraint(’task’, new NotBlank());
$metadata->addPropertyConstraint(’dueDate’, new NotBlank());
$metadata->addPropertyConstraint(’dueDate’, new Type(’\DateTime’));
}
}
Questo è tutto! Se si re-invia il form con i dati non validi, si vedranno i rispettivi errori visualizzati nel form.
Validazione HTML5
Dall’HTML5, molti browser possono nativamente imporre alcuni vincoli di validazione sul lato client. La
validazione più comune è attivata con la resa di un attributo required sui campi che sono obbligatori.
Per i browser che supportano HTML5, questo si tradurrà in un messaggio nativo del browser che verrà
visualizzato se l’utente tenta di inviare il form con quel campo vuoto.
I form generati traggono il massimo vantaggio di questa nuova funzionalità con l’aggiunta di appropriati
attributi HTML che verifichino la convalida. La convalida lato client, tuttavia, può essere disabilitata aggiungendo l’attributo novalidate al tag form o formnovalidate al tag submit. Ciò è particolarmente
utile quando si desidera testare i propri vincoli di convalida lato server, ma viene impedito dal browser, per
esempio, inviando campi vuoti.
La validazione è una caratteristica molto potente di Symfony2 e dispone di un proprio capitolo dedicato.
Gruppi di validatori
Tip: Se non si usano i gruppi di validatori, è possibile saltare questa sezione.
148
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
Se il proprio oggetto si avvale dei gruppi di validatori, si avrà bisogno di specificare quelle/i gruppi di convalida
deve usare il form:
$form = $this->createFormBuilder($users, array(
’validation_groups’ => array(’registration’),
))->add(...)
;
Se si stanno creando classi per i form (una buona pratica), allora si avrà bisogno di aggiungere quanto segue al
metodo getDefaultOptions():
public function getDefaultOptions(array $options)
{
return array(
’validation_groups’ => array(’registration’)
);
}
In entrambi i casi, solo il gruppo di validazione registration verrà utilizzato per validare l’oggetto sottostante.
Gruppi basati su dati inseriti
New in version 2.1: La possibilità di specificare un callback o una Closure in validation_groups è stata
aggiunta nella versione 2.1 Se si ha bisogno di una logica avanzata per determinare i gruppi di validazione (p.e.
basandosi sui dati inseriti), si può impostare l’opzione validation_groups a un callback o a una Closure:
public function getDefaultOptions(array $options)
{
return array(
’validation_groups’ => array(’Acme\\AcmeBundle\\Entity\\Client’, ’determineValidationGroup
);
}
Questo richiamerà il metodo statico determineValidationGroups() della classe Client, dopo il bind
del form ma prima dell’esecuzione della validazione. L’oggetto Form è passato come parametro del metodo
(vedere l’esempio successivo). Si può anche definire l’intera logica con una Closure:
public function getDefaultOptions(array $options)
{
return array(
’validation_groups’ => function(FormInterface $form) {
$data = $form->getData();
if (Entity\Client::TYPE_PERSON == $data->getType()) {
return array(’person’)
} else {
return array(’company’);
}
},
);
}
Tipi di campo predefiniti
Symfony dispone di un folto gruppo di tipi di campi che coprono tutti i campi più comuni e i tipi di dati di cui
necessitano i form:
Campi testo
• text
2.1. Libro
149
Symfony2 documentation Documentation, Release 2
• textarea
• email
• integer
• money
• number
• password
• percent
• search
• url
Campi di scelta
• choice
• entity
• country
• language
• locale
• timezone
Campi data e ora
• date
• datetime
• time
• birthday
Altri campi
• checkbox
• file
• radio
Gruppi di campi
• collection
• repeated
Campi nascosti
• hidden
• csrf
150
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
Campi di base
• field
• form
È anche possibile creare dei tipi di campi personalizzati. Questo argomento è trattato nell’articolo “Come creare
un tipo di campo personalizzato di un form” del ricettario.
Opzioni dei tipi di campo
Ogni tipo di campo ha un numero di opzioni che può essere utilizzato per la configurazione. Ad esempio, il campo
dueDate è attualmente reso con 3 menu select. Tuttavia, il campo data può essere configurato per essere reso
come una singola casella di testo (in cui l’utente deve inserire la data nella casella come una stringa):
->add(’dueDate’, ’date’, array(’widget’ => ’single_text’))
Ogni tipo di campo ha un numero di opzioni differente che possono essere passate a esso. Molte di queste sono
specifiche per il tipo di campo e i dettagli possono essere trovati nella documentazione di ciascun tipo.
L’opzione required
L’opzione più comune è l’opzione required, che può essere applicata a qualsiasi campo. Per impostazione
predefinita, l’opzione required è impostata a true e questo significa che i browser che interpretano
l’HTML5 applicheranno la validazione lato client se il campo viene lasciato vuoto. Se non si desidera
questo comportamento, impostare l’opzione required del campo a false o disabilitare la validazione
HTML5.
Si noti inoltre che l’impostazione dell’opzione required a true non farà applicare la validazione lato
server. In altre parole, se un utente invia un valore vuoto per il campo (sia con un browser vecchio o un
servizio web, per esempio), sarà accettata come valore valido a meno che si utilizzi il vincolo di validazione
NotBlank o NotNull.
In altre parole, l’opzione required è “bella”, ma la vera validazione lato server dovrebbe sempre essere
utilizzata.
L’opzione label
La label per il campo del form può essere impostata con l’opzione label, applicabile a qualsiasi campo:
->add(’dueDate’, ’date’, array(
’widget’ => ’single_text’,
’label’ => ’Due Date’,
))
La label per un campo può anche essere impostata nel template che rende il form, vedere sotto.
Indovinare il tipo di campo
Ora che sono stati aggiunti i metadati di validazione alla classe Task, Symfony sa già un po’ dei campi. Se lo si
vuole permettere, Symfony può “indovinare” il tipo del campo e impostarlo al posto vostro. In questo esempio,
Symfony può indovinare dalle regole di validazione che il campo task è un normale campo text e che il campo
dueDate è un campo date:
2.1. Libro
151
Symfony2 documentation Documentation, Release 2
public function newAction()
{
$task = new Task();
$form = $this->createFormBuilder($task)
->add(’task’)
->add(’dueDate’, null, array(’widget’ => ’single_text’))
->getForm();
}
Questa funzionalità si attiva quando si omette il secondo parametro del metodo add() (o se si passa null a
esso). Se si passa un array di opzioni come terzo parametro (fatto sopra per dueDate), queste opzioni vengono
applicate al campo indovinato.
Caution: Se il form utilizza un gruppo specifico di validazione, la funzionalità che indovina il tipo di campo
prenderà ancora in considerazione tutti i vincoli di validazione quando andrà a indovinare i tipi di campi
(compresi i vincoli che non fanno parte del processo di convalida dei gruppi in uso).
Indovinare le opzioni dei tipi di campo
Oltre a indovinare il “tipo” di un campo, Symfony può anche provare a indovinare i valori corretti di una serie di
opzioni del campo.
Tip: Quando queste opzioni vengono impostate, il campo sarà reso con speciali attributi HTML che forniscono la validazione HTML5 lato client. Tuttavia, non genera i vincoli equivalenti lato server (ad esempio
Assert\MaxLength). E anche se si ha bisogno di aggiungere manualmente la validazione lato server, queste
opzioni dei tipi di campo possono essere ricavate da queste informazioni.
• required: L’opzione required può essere indovinata in base alle regole di validazione (cioè se il
campo è NotBlank o NotNull) o dai metadati di Doctrine (vale a dire se il campo è nullable).
Questo è molto utile, perché la validazione lato client corrisponderà automaticamente alle vostre regole di
validazione.
• min_length: Se il campo è un qualche tipo di campo di testo, allora l’opzione min_length può essere
indovinata dai vincoli di validazione (se viene utilizzato MinLength o Min) o dai metadati Doctrine
(tramite la lunghezza del campo).
• max_length: Similmente a min_length, può anche essere indovinata la lunghezza massima.
Note: Queste opzioni di campi vengono indovinate solo se si sta usando Symfony per ricavare il tipo di campo
(ovvero omettendo o passando null nel secondo parametro di add()).
Se si desidera modificare uno dei valori indovinati, è possibile sovrascriverlo passando l’opzione nell’array di
opzioni del campo:
->add(’task’, null, array(’min_length’ => 4))
Rendere un form in un template
Finora si è visto come un intero form può essere reso con una sola linea di codice. Naturalmente, solitamente si
ha bisogno di molta più flessibilità:
• Twig
{# src/Acme/TaskBundle/Resources/views/Default/new.html.twig #}
<form action="{{ path(’task_new’) }}" method="post" {{ form_enctype(form) }}>
152
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
{{ form_errors(form) }}
{{ form_row(form.task) }}
{{ form_row(form.dueDate) }}
{{ form_rest(form) }}
<input type="submit" />
</form>
• PHP
<!-- // src/Acme/TaskBundle/Resources/views/Default/newAction.html.php -->
<form action="<?php echo $view[’router’]->generate(’task_new’) ?>" method="post" <?php echo $
<?php echo $view[’form’]->errors($form) ?>
<?php echo $view[’form’]->row($form[’task’]) ?>
<?php echo $view[’form’]->row($form[’dueDate’]) ?>
<?php echo $view[’form’]->rest($form) ?>
<input type="submit" />
</form>
Diamo uno sguardo a ogni parte:
• form_enctype(form) - Se almeno un campo è un campo di upload di file, questo inserisce
l’obbligatorio enctype="multipart/form-data";
• form_errors(form) - Rende eventuali errori globali per l’intero modulo (gli errori specifici dei campi
vengono visualizzati accanto a ciascun campo);
• form_row(form.dueDate) - Rende l’etichetta, eventuali errori e il widget HTML del form per il dato
campo (ad esempio dueDate) all’interno, per impostazione predefinita, di un elemento div;
• form_rest(form) - Rende tutti i campi che non sono ancora stati resi. Di solito è una buona idea
mettere una chiamata a questo helper in fondo a ogni form (nel caso in cui ci si è dimenticati di mostrare un
campo o non ci si voglia annoiare a inserire manualmente i campi nascosti). Questo helper è utile anche per
utilizzare automaticamente i vantaggi della protezione CSRF.
La maggior parte del lavoro viene fatto dall’helper form_row, che rende l’etichetta, gli errori e i widget HTML
del form di ogni campo all’interno di un tag div per impostazione predefinita. Nella sezione Temi con i form, si
apprenderà come l’output di form_row possa essere personalizzato su diversi levelli.
Tip: Si può accedere ai dati attuali del form tramite form.vars.value:
• Twig
{{ form.vars.value.task }}
• PHP
<?php echo $view[’form’]->get(’value’)->getTask() ?>
Rendere manualmente ciascun campo
L’helper form_row è utile perché si può rendere ciascun campo del form molto facilmente (e il markup utilizzato
per la “riga” può essere personalizzato come si vuole). Ma poiché la vita non è sempre così semplice, è anche
possibile rendere ogni campo interamente a mano. Il risultato finale del codice che segue è lo stesso di quando si
è utilizzato l’helper form_row:
2.1. Libro
153
Symfony2 documentation Documentation, Release 2
• Twig
{{ form_errors(form) }}
<div>
{{ form_label(form.task) }}
{{ form_errors(form.task) }}
{{ form_widget(form.task) }}
</div>
<div>
{{ form_label(form.dueDate) }}
{{ form_errors(form.dueDate) }}
{{ form_widget(form.dueDate) }}
</div>
{{ form_rest(form) }}
• PHP
<?php echo $view[’form’]->errors($form) ?>
<div>
<?php echo $view[’form’]->label($form[’task’]) ?>
<?php echo $view[’form’]->errors($form[’task’]) ?>
<?php echo $view[’form’]->widget($form[’task’]) ?>
</div>
<div>
<?php echo $view[’form’]->label($form[’dueDate’]) ?>
<?php echo $view[’form’]->errors($form[’dueDate’]) ?>
<?php echo $view[’form’]->widget($form[’dueDate’]) ?>
</div>
<?php echo $view[’form’]->rest($form) ?>
Se l’etichetta auto-generata di un campo non è giusta, si può specificarla esplicitamente:
• Twig
{{ form_label(form.task, ’Task Description’) }}
• PHP
<?php echo $view[’form’]->label($form[’task’], ’Task Description’) ?>
Alcuni tipi di campi hanno opzioni di resa aggiuntive che possono essere passate al widget. Queste opzioni sono
documentate con ogni tipo, ma un’opzione comune è attr, che permette di modificare gli attributi dell’elemento
form. Di seguito viene aggiunta la classe task_field al resa del campo casella di testo:
• Twig
{{ form_widget(form.task, { ’attr’: {’class’: ’task_field’} }) }}
• PHP
<?php echo $view[’form’]->widget($form[’task’], array(
’attr’ => array(’class’ => ’task_field’),
)) ?>
Se occorre rendere dei campi “a mano”, si può accedere ai singoli valori dei campi, come id, name e label.
Per esempio, per ottenere id:
• Twig
154
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
{{ form.task.vars.id }}
• PHP
<?php echo $form[’task’]->get(’id’) ?>
Per ottenere il valore usato per l’attributo nome dei campi del form, occorre usare il valore full_name:
• Twig
{{ form.task.vars.full_name }}
• PHP
<?php echo $form[’task’]->get(’full_name’) ?>
Riferimento alle funzioni del template Twig
Se si utilizza Twig, un riferimento completo alle funzioni di resa è disponibile nel manuale di riferimento. Leggendolo si può sapere tutto sugli helper disponibili e le opzioni che possono essere usate con ciascuno di essi.
Creare classi per i form
Come si è visto, un form può essere creato e utilizzato direttamente in un controllore. Tuttavia, una pratica
migliore è quella di costruire il form in una apposita classe PHP, che può essere riutilizzata in qualsiasi punto
dell’applicazione. Creare una nuova classe che ospiterà la logica per la costruzione del form task:
// src/Acme/TaskBundle/Form/Type/TaskType.php
namespace Acme\TaskBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
class TaskType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add(’task’);
$builder->add(’dueDate’, null, array(’widget’ => ’single_text’));
}
public function getName()
{
return ’task’;
}
}
Questa nuova classe contiene tutte le indicazioni necessarie per creare il form task (notare che il metodo
getName() dovrebbe restituire un identificatore univoco per questo “tipo” di form). Può essere usato per costruire rapidamente un oggetto form nel controllore:
// src/Acme/TaskBundle/Controller/DefaultController.php
// add this new use statement at the top of the class
use Acme\TaskBundle\Form\Type\TaskType;
public function newAction()
{
$task = // ...
$form = $this->createForm(new TaskType(), $task);
2.1. Libro
155
Symfony2 documentation Documentation, Release 2
// ...
}
Porre la logica del form in una propria classe significa che il form può essere facilmente riutilizzato in altre parti
del progetto. Questo è il modo migliore per creare form, ma la scelta in ultima analisi, spetta a voi.
Impostare data_class
Ogni form ha bisogno di sapere il nome della classe che detiene i dati sottostanti (ad esempio
Acme\TaskBundle\Entity\Task). Di solito, questo viene indovinato in base all’oggetto passato
al secondo parametro di createForm (vale a dire $task). Dopo, quando si inizia a incorporare i form,
questo non sarà più sufficiente. Così, anche se non sempre necessario, è in genere una buona idea specificare
esplicitamente l’opzione data_class aggiungendo il codice seguente alla classe del tipo di form:
public function getDefaultOptions(array $options)
{
return array(
’data_class’ => ’Acme\TaskBundle\Entity\Task’,
);
}
Tip: Quando si mappano form su oggetti, tutti i campi vengono mappati. Ogni campo nel form che non esiste
nell’oggetto mappato causerà il lancio di un’eccezione.
Nel caso in cui servano campi extra nel form (per esempio, un checkbox “accetto i termini”), che non saranno
mappati nell’oggetto sottostante, occorre impostare l’opzione property_path a false:
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add(’task’);
$builder->add(’dueDate’, null, array(’property_path’ => false));
}
Inoltre, se ci sono campi nel form che non sono inclusi nei dati inviati, tali campi saranno impostati esplicitamente
a null.
I form e Doctrine
L’obiettivo di un form è quello di tradurre i dati da un oggetto (ad esempio Task) a un form HTML e quindi
tradurre i dati inviati dall’utente indietro all’oggetto originale. Come tale, il tema della persistenza dell’oggetto
Task nel database è interamente non correlato al tema dei form. Ma, se la classe Task è stata configurata per
essere salvata attraverso Doctrine (vale a dire che per farlo si è aggiunta la mappatura dei meta-dati), allora si può
salvare dopo l’invio di un form, quando il form stesso è valido:
if ($form->isValid()) {
$em = $this->getDoctrine()->getEntityManager();
$em->persist($task);
$em->flush();
return $this->redirect($this->generateUrl(’task_success’));
}
Se, per qualche motivo, non si ha accesso all’oggetto originale $task, è possibile recuperarlo dal form:
$task = $form->getData();
Per maggiori informazioni, vedere il capitolo ORM Doctrine.
156
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
La cosa fondamentale da capire è che quando il form viene riempito, i dati inviati vengono trasferiti immediatamente all’oggetto sottostante. Se si vuole persistere i dati, è sufficiente persistere l’oggetto stesso (che già contiene
i dati inviati).
Incorporare form
Spesso, si vuole costruire form che includono campi provenienti da oggetti diversi. Ad esempio, un form di registrazione può contenere dati appartenenti a un oggetto User così come a molti oggetti Address. Fortunatamente,
questo è semplice e naturale con il componente per i form.
Incorporare un oggetto singolo
Supponiamo che ogni Task appartenga a un semplice oggetto Category. Si parte, naturalmente, con la
creazione di un oggetto Category:
// src/Acme/TaskBundle/Entity/Category.php
namespace Acme\TaskBundle\Entity;
use Symfony\Component\Validator\Constraints as Assert;
class Category
{
/**
* @Assert\NotBlank()
*/
public $name;
}
Poi, aggiungere una nuova proprietà category alla classe Task:
// ...
class Task
{
// ...
/**
* @Assert\Type(type="Acme\TaskBundle\Entity\Category")
*/
protected $category;
// ...
public function getCategory()
{
return $this->category;
}
public function setCategory(Category $category = null)
{
$this->category = $category;
}
}
Ora che l’applicazione è stata aggiornata per riflettere le nuove esigenze, creare una classe di form in modo che
l’oggetto Category possa essere modificato dall’utente:
// src/Acme/TaskBundle/Form/Type/CategoryType.php
namespace Acme\TaskBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
2.1. Libro
157
Symfony2 documentation Documentation, Release 2
use Symfony\Component\Form\FormBuilder;
class CategoryType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add(’name’);
}
public function getDefaultOptions(array $options)
{
return array(
’data_class’ => ’Acme\TaskBundle\Entity\Category’,
);
}
public function getName()
{
return ’category’;
}
}
L’obiettivo finale è quello di far si che la Category di un Task possa essere correttamente modificata all’interno
dello stesso form task. Per farlo, aggiungere il campo category all’oggetto TaskType, il cui tipo è un’istanza
della nuova classe CategoryType:
public function buildForm(FormBuilder $builder, array $options)
{
// ...
$builder->add(’category’, new CategoryType());
}
I campi di CategoryType ora possono essere resi accanto a quelli della classe TaskType. Rendere i campi di
Category allo stesso modo dei campi Task originali:
• Twig
{# ... #}
<h3>Category</h3>
<div class="category">
{{ form_row(form.category.name) }}
</div>
{{ form_rest(form) }}
{# ... #}
• PHP
<!-- ... -->
<h3>Category</h3>
<div class="category">
<?php echo $view[’form’]->row($form[’category’][’name’]) ?>
</div>
<?php echo $view[’form’]->rest($form) ?>
<!-- ... -->
Quando l’utente invia il form, i dati inviati con i campi Category sono utilizzati per costruire un’istanza di
Category, che viene poi impostata sul campo category dell’istanza Task.
L’istanza Category è accessibile naturalmente attraverso $task->getCategory() e può essere memorizzata nel database o utilizzata quando serve.
158
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
Incorporare un insieme di form
È anche possibile incorporare un insieme di form in un form (si immagini un form Category con tanti sotto-form
Product. Lo si può fare utilizzando il tipo di campo collection.
Per maggiori informazioni, vedere la ricetta “Come unire una collezione di form” e il riferimento al tipo collection.
Temi con i form
Ogni parte nel modo in cui un form viene reso può essere personalizzata. Si è liberi di cambiare come ogni “riga”
del form viene resa, modificare il markup utilizzato per rendere gli errori, o anche personalizzare la modalità con
cui un tag textarea dovrebbe essere rappresentato. Nulla è off-limits, e personalizzazioni differenti possono
essere utilizzate in posti diversi.
Symfony utilizza i template per rendere ogni singola parte di un form, come ad esempio i tag label, i tag input,
i messaggi di errore e ogni altra cosa.
In Twig, ogni “frammento” di form è rappresentato da un blocco Twig. Per personalizzare una qualunque parte di
come un form è reso, basta sovrascrivere il blocco appropriato.
In PHP, ogni “frammento” è reso tramite un file template individuale. Per personalizzare una qualunque parte del
modo in cui un form viene reso, basta sovrascrivere il template esistente creandone uno nuovo.
Per capire come funziona, cerchiamo di personalizzare il frammento form_row e aggiungere un attributo class
all’elemento div che circonda ogni riga. Per farlo, creare un nuovo file template per salvare il nuovo codice:
• Twig
{# src/Acme/TaskBundle/Resources/views/Form/fields.html.twig #}
{% block field_row %}
{% spaceless %}
<div class="form_row">
{{ form_label(form) }}
{{ form_errors(form) }}
{{ form_widget(form) }}
</div>
{% endspaceless %}
{% endblock field_row %}
• PHP
<!-- src/Acme/TaskBundle/Resources/views/Form/field_row.html.php -->
<div class="form_row">
<?php echo $view[’form’]->label($form, $label) ?>
<?php echo $view[’form’]->errors($form) ?>
<?php echo $view[’form’]->widget($form, $parameters) ?>
</div>
Il frammento di form field_row è utilizzato per rendere la maggior parte dei campi attraverso la funzione
form_row. Per dire al componente form di utilizzare il nuovo frammento field_row definito sopra, aggiungere il codice seguente all’inizio del template che rende il form:
• Twig
{# src/Acme/TaskBundle/Resources/views/Default/new.html.twig #}
{% form_theme form ’AcmeTaskBundle:Form:fields.html.twig’ %}
<form ...>
• PHP
2.1. Libro
159
Symfony2 documentation Documentation, Release 2
<!-- src/Acme/TaskBundle/Resources/views/Default/new.html.php -->
<?php $view[’form’]->setTheme($form, array(’AcmeTaskBundle:Form’)) ?>
<form ...>
Il tag form_theme (in Twig) “importa” i frammenti definiti nel dato template e li usa quando deve rendere il
form. In altre parole, quando la funzione form_row è successivamente chiamata in questo template, utilizzerà il
blocco field_row dal tema personalizzato (al posto del blocco predefinito field_row fornito con Symfony).
Per personalizzare una qualsiasi parte di un form, basta sovrascrivere il frammento appropriato. Sapere esattamente qual è il blocco o il file da sovrascrivere è l’oggetto della sezione successiva.
Per una trattazione più ampia, vedere Come personalizzare la resa dei form.
Nomi per i frammenti di form
In Symfony, ogni parte di un form che viene reso (elementi HTML del form, errori, etichette, ecc.) è definito in
un tema base, che in Twig è una raccolta di blocchi e in PHP una collezione di file template.
In Twig, ogni blocco necessario è definito in un singolo file template (form_div_layout.html.twig) che vive
all’interno di Twig Bridge. Dentro questo file, è possibile ogni blocco necessario alla resa del form e ogni tipo
predefinito di campo.
In PHP, i frammenti sono file template individuali. Per impostazione predefinita sono posizionati nella cartella
Resources/views/Form del bundle framework (vedere su GitHub).
Ogni nome di frammento segue lo stesso schema di base ed è suddiviso in due pezzi, separati da un singolo
carattere di sottolineatura (_). Alcuni esempi sono:
• field_row - usato da form_row per rendere la maggior parte dei campi;
• textarea_widget - usato da form_widget per rendere un campo di tipo textarea;
• field_errors - usato da form_errors per rendere gli errori di un campo;
Ogni frammento segue lo stesso schema di base: type_part. La parte type corrisponde al campo type che
viene reso (es. textarea, checkbox, date, ecc) mentre la parte part corrisponde a cosa si sta rendendo (es.
label, widget, errors, ecc). Per impostazione predefinita, ci sono 4 possibili parti di un form che possono
essere rese:
label
widget
errors
row
(es.
(es.
(es.
(es.
field_label)
field_widget)
field_errors)
field_row)
rende l’etichetta dei campi
rende la rappresentazione HTML dei campi
rende gli errori dei campi
rende l’intera riga del campo (etichetta, widget ed errori)
Note: In realtà ci sono altre 3 parti (rows, rest e enctype), ma raramente c’è la necessità di sovrascriverle.
Conoscendo il tipo di campo (ad esempio textarea) e che parte si vuole personalizzare (ad esempio widget),
si può costruire il nome del frammento che deve essere sovrascritto (esempio textarea_widget).
Ereditarietà dei frammenti di template
In alcuni casi, il frammento che si vuole personalizzare sembrerà mancare. Ad esempio, non c’è nessun frammento
textarea_errors nei temi predefiniti forniti con Symfony. Quindi dove sono gli errori di un campo textarea
che deve essere reso?
La risposta è: nel frammento field_errors. Quando Symfony rende gli errori per un tipo textarea, prima
cerca un frammento textarea_errors, poi cerca un frammento field_errors. Ogni tipo di campo ha un
tipo genitore (il tipo genitore di textarea è field) e Symfony utilizza il frammento per il tipo del genitore se
il frammento di base non esiste.
160
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
Quindi, per ignorare gli errori dei soli campi textarea, copiare il frammento field_errors, rinominarlo in
textarea_errors e personalizzrlo. Per sovrascrivere la resa degli errori predefiniti di tutti i campi, copiare e
personalizzare direttamente il frammento field_errors.
Tip: Il tipo “genitore” di ogni tipo di campo è disponibile per ogni tipo di campo in form type reference
Temi globali per i form
Nell’esempio sopra, è stato utilizzato l’helper form_theme (in Twig) per “importare” i frammenti personalizzati
solo in quel form. Si può anche dire a Symfony di importare personalizzazioni del form nell’intero progetto.
Twig Per includere automaticamente i blocchi personalizzati del template fields.html.twig creato in
precedenza, in tutti i template, modificare il file della configurazione dell’applicazione:
• YAML
# app/config/config.yml
twig:
form:
resources:
- ’AcmeTaskBundle:Form:fields.html.twig’
# ...
• XML
<!-- app/config/config.xml -->
<twig:config ...>
<twig:form>
<resource>AcmeTaskBundle:Form:fields.html.twig</resource>
</twig:form>
<!-- ... -->
</twig:config>
• PHP
// app/config/config.php
$container->loadFromExtension(’twig’, array(
’form’ => array(’resources’ => array(
’AcmeTaskBundle:Form:fields.html.twig’,
))
// ...
));
Tutti i blocchi all’interno del template fields.html.twig vengono ora utilizzati a livello globale per definire
l’output del form.
2.1. Libro
161
Symfony2 documentation Documentation, Release 2
Personalizzare tutti gli output del form in un singolo file con Twig
Con Twig, si può anche personalizzare il blocco di un form all’interno del template in cui questa personalizzazione è necessaria:
{% extends ’::base.html.twig’ %}
{# import "_self" as the form theme #}
{% form_theme form _self %}
{# make the form fragment customization #}
{% block field_row %}
{# custom field row output #}
{% endblock field_row %}
{% block content %}
{# ... #}
{{ form_row(form.task) }}
{% endblock %}
Il tag {% form_theme form _self %} ai blocchi del form di essere personalizzati direttamente
all’interno del template che utilizzerà tali personalizzazioni. Utilizzare questo metodo per creare velocemente personalizzazioni del form che saranno utilizzate solo in un singolo template.
PHP Per
includere
automaticamente
i
template
personalizzati
dalla
cartella
Acme/TaskBundle/Resources/views/Form creata in precedenza in tutti i template, modificare il
file con la configurazione dell’applicazione:
• YAML
# app/config/config.yml
framework:
templating:
form:
resources:
- ’AcmeTaskBundle:Form’
# ...
• XML
<!-- app/config/config.xml -->
<framework:config ...>
<framework:templating>
<framework:form>
<resource>AcmeTaskBundle:Form</resource>
</framework:form>
</framework:templating>
<!-- ... -->
</framework:config>
• PHP
// app/config/config.php
$container->loadFromExtension(’framework’, array(
’templating’ => array(’form’ =>
array(’resources’ => array(
’AcmeTaskBundle:Form’,
)))
162
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
// ...
));
Ogni frammento all’interno della cartella Acme/TaskBundle/Resources/views/Form è ora usato globalmente per definire l’output del form.
Protezione da CSRF
CSRF, o Cross-site request forgery, è un metodo mediante il quale un utente malintenzionato cerca di fare inviare
inconsapevolmente agli utenti legittimi dati che non intendono far conoscere. Fortunatamente, gli attacchi CSRF
possono essere prevenuti utilizzando un token CSRF all’interno dei form.
La buona notizia è che, per impostazione predefinita, Symfony integra e convalida i token CSRF automaticamente.
Questo significa che è possibile usufruire della protezione CSRF senza far nulla. Infatti, ogni form di questo
capitolo sfrutta la protezione CSRF!
La protezione CSRF funziona con l’aggiunta al form di un campo nascosto, chiamato per impostazione predefinita
_token, che contiene un valore che solo sviluppatore e utente conoscono. Questo garantisce che proprio l’utente,
non qualcun altro, stia inviando i dati. Symfony valida automaticamente la presenza e l’esattezza di questo token.
Il campo _token è un campo nascosto e sarà reso automaticamente se si include la funzione form_rest() nel
template, perché questa assicura che tutti i campi non resi vengano visualizzati.
Il token CSRF può essere personalizzato specificatamente per ciascun form. Ad esempio:
class TaskType extends AbstractType
{
// ...
public function getDefaultOptions(array $options)
{
return array(
’data_class’
=> ’Acme\TaskBundle\Entity\Task’,
’csrf_protection’ => true,
’csrf_field_name’ => ’_token’,
// una chiave univoca per generare il token
’intention’
=> ’task_item’,
);
}
// ...
}
Per disabilitare la protezione CSRF, impostare l’opzione csrf_protection a false. Le personalizzazioni
possono essere fatte anche a livello globale nel progetto. Per ulteriori informazioni, vedere la sezione riferimento
della configurazione dei form.
Note: L’opzione intention è opzionale, ma migliora notevolmente la sicurezza del token generato, rendendolo
diverso per ogni modulo.
Usare un form senza una classe
Nella maggior parte dei casi, un form è legato a un oggetto e i campi del form prendono i loro dati dalle proprietà
di tale oggetto. Questo è quanto visto finora in questo capitolo, con la classe Task.
A volte, però, si vuole solo usare un form senza classi, per ottenere un array di dati inseriti. Lo si può fare in modo
molto facile:
// assicurarsi di aver importato lo spazio dei nomi Request all’inizio della classe
use Symfony\Component\HttpFoundation\Request
2.1. Libro
163
Symfony2 documentation Documentation, Release 2
// ...
public function contactAction(Request $request)
{
$defaultData = array(’message’ => ’Type your message here’);
$form = $this->createFormBuilder($defaultData)
->add(’name’, ’text’)
->add(’email’, ’email’)
->add(’message’, ’textarea’)
->getForm();
if ($request->getMethod() == ’POST’) {
$form->bindRequest($request);
// data is an array with "name", "email", and "message" keys
$data = $form->getData();
}
// ... render the form
}
Per impostazione predefinita, un form ipotizza che si voglia lavorare con array di dati, invece che con oggetti. Ci
sono due modi per modificare questo comportamento e legare un form a un oggetto:
1. Passare un oggetto alla creazione del form (come primo parametro di createFormBuilder o come
secondo parametro di createForm);
2. Dichiarare l’opzione data_class nel form.
Se non si fa nessuna di queste due cose, il form restituirà i dati come array. In questo esempio, poiché
$defaultData non è un oggetto (e l’opzione data_class è omessa), $form->getData() restituirà un
array.
Tip: Si può anche accedere ai valori POST (“name”, in questo caso) direttamente tramite l’oggetto Request, in
questo modo:
$this->get(’request’)->request->get(’name’);
Tuttavia, si faccia attenzione che in molti casi l’uso del metodo getData() è preferibile, poiché restituisce i dati
(solitamente un oggetto) dopo che sono stati manipolati dal sistema dei form.
Aggiungere la validazione
L’ultima parte mancante è la validazione. Solitamente, quando si richiama $form->isValid(), l’oggetto
viene validato dalla lettura dei vincoli applicati alla classe. Ma senza una classe, come si possono aggiungere
vincoli ai dati del form?
La risposta è: impostare i vincoli in modo autonomo e passarli al proprio form. L’approccio generale è spiegato
meglio nel capitolo sulla validazione, ma ecco un breve esempio:
// importare gli spazi dei nomi all’inizio della classe
use Symfony\Component\Validator\Constraints\Email;
use Symfony\Component\Validator\Constraints\MinLength;
use Symfony\Component\Validator\Constraints\Collection;
$collectionConstraint = new Collection(array(
’name’ => new MinLength(5),
’email’ => new Email(array(’message’ => ’Invalid email address’)),
));
// creare un form, senza valori predefiniti, e passarlo all’opzione constraint
164
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
$form = $this->createFormBuilder(null, array(
’validation_constraint’ => $collectionConstraint,
))->add(’email’, ’email’)
// ...
;
Ora, richiamando $form->isValid(), i vincoli impostati sono eseguiti sui dati del form. Se si usa una classe form,
sovrascrivere il metodo getDefaultOptions per specificare l’opzione:
namespace Acme\TaskBundle\Form\Type;
use
use
use
use
use
Symfony\Component\Form\AbstractType;
Symfony\Component\Form\FormBuilder;
Symfony\Component\Validator\Constraints\Email;
Symfony\Component\Validator\Constraints\MinLength;
Symfony\Component\Validator\Constraints\Collection;
class ContactType extends AbstractType
{
// ...
public function getDefaultOptions(array $options)
{
$collectionConstraint = new Collection(array(
’name’ => new MinLength(5),
’email’ => new Email(array(’message’ => ’Invalid email address’)),
));
return array(’validation_constraint’ => $collectionConstraint);
}
}
Si possiede ora la flessibilità di creare form, con validazione, che restituiscano array di dati, invece di oggetti. In
molti casi, è meglio (e sicuramente più robusto) legare il form a un oggetto. Ma questo è un bell’approccio per
form più semplici.
Considerazioni finali
Ora si è a conoscenza di tutti i mattoni necessari per costruire form complessi e funzionali per la propria applicazione. Quando si costruiscono form, bisogna tenere presente che il primo gol di un form è quello di tradurre i
dati da un oggetto (Task) a un form HTML in modo che l’utente possa modificare i dati. Il secondo obiettivo di
un form è quello di prendere i dati inviati dall’utente e ri-applicarli all’oggetto.
Ci sono altre cose da imparare sul potente mondo dei form, ad esempio come gestire il caricamento di file con
Doctrine o come creare un form dove un numero dinamico di sub-form possono essere aggiunti (ad esempio
una todo list in cui è possibile continuare ad aggiungere più campi tramite Javascript prima di inviare). Vedere
il ricettario per questi argomenti. Inoltre, assicurarsi di basarsi sulla documentazione di riferimento sui tipi di
campo, che comprende esempi di come usare ogni tipo di campo e le relative opzioni.
Saperne di più con il ricettario
• Come gestire il caricamento di file con Doctrine
• Riferimento del tipo di campo file
• Creare tipi di campo personalizzati
• Come personalizzare la resa dei form
• Come generare dinamicamente form usando gli eventi form
• Utilizzare i data transformer
2.1. Libro
165
Symfony2 documentation Documentation, Release 2
2.1.12 Sicurezza
La sicurezza è una procedura che avviene in due fasi, il cui obiettivo è quello di impedire a un utente di accedere
a una risorsa a cui non dovrebbe avere accesso.
Nella prima fase del processo, il sistema di sicurezza identifica chi è l’utente, chiedendogli di presentare una sorta
di identificazione. Quest’ultima è chiamata autenticazione e significa che il sistema sta cercando di scoprire chi
sei.
Una volta che il sistema sa chi sei, il passo successivo è quello di determinare se dovresti avere accesso a una
determinata risorsa. Questa parte del processo è chiamato autorizzazione e significa che il sistema verifica se
disponi dei privilegi per eseguire una certa azione.
Il modo migliore per imparare è quello di vedere un esempio, vediamolo subito.
Note: Il componente della sicurezza di Symfony è disponibile come libreria PHP a sé stante, per l’utilizzo
all’interno di qualsiasi progetto PHP.
Esempio di base: l’autenticazione HTTP
Il componente della sicurezza può essere configurato attraverso la configurazione dell’applicazione. In realtà, per
molte configurazioni standard di sicurezza basta solo usare la giusta configurazione. La seguente configurazione
dice a Symfony di proteggere qualunque URL corrispondente a /admin/* e chiedere le credenziali all’utente
utilizzando l’autenticazione base HTTP (cioè il classico vecchio box nome utente/password):
• YAML
# app/config/security.yml
security:
firewalls:
secured_area:
pattern:
^/
anonymous: ~
http_basic:
realm: "Area demo protetta"
access_control:
166
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
- { path: ^/admin, roles: ROLE_ADMIN }
providers:
in_memory:
memory:
users:
ryan: { password: ryanpass, roles: ’ROLE_USER’ }
admin: { password: kitten, roles: ’ROLE_ADMIN’ }
encoders:
Symfony\Component\Security\Core\User\User: plaintext
• XML
<!-- app/config/security.xml -->
<srv:container xmlns="http://symfony.com/schema/dic/security"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:srv="http://symfony.com/schema/dic/services"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/
<config>
<firewall name="secured_area" pattern="^/">
<anonymous />
<http-basic realm="Area demo protetta" />
</firewall>
<access-control>
<rule path="^/admin" role="ROLE_ADMIN" />
</access-control>
<provider name="in_memory">
<memory>
<user name="ryan" password="ryanpass" roles="ROLE_USER" />
<user name="admin" password="kitten" roles="ROLE_ADMIN" />
</memory>
</provider>
<encoder class="Symfony\Component\Security\Core\User\User" algorithm="plaintext" />
</config>
</srv:container>
• PHP
// app/config/security.php
$container->loadFromExtension(’security’, array(
’firewalls’ => array(
’secured_area’ => array(
’pattern’ => ’^/’,
’anonymous’ => array(),
’http_basic’ => array(
’realm’ => ’Area demo protetta’,
),
),
),
’access_control’ => array(
array(’path’ => ’^/admin’, ’role’ => ’ROLE_ADMIN’),
),
’providers’ => array(
’in_memory’ => array(
’memory’ => array(
’users’ => array(
’ryan’ => array(’password’ => ’ryanpass’, ’roles’ => ’ROLE_USER’),
’admin’ => array(’password’ => ’kitten’, ’roles’ => ’ROLE_ADMIN’),
2.1. Libro
167
Symfony2 documentation Documentation, Release 2
),
),
),
),
’encoders’ => array(
’Symfony\Component\Security\Core\User\User’ => ’plaintext’,
),
));
Tip: Una distribuzione standard di Symfony pone la configurazione di sicurezza in un file separato (ad esempio
app/config/security.yml). Se non si ha un file di sicurezza separato, è possibile inserire la configurazione
direttamente nel file di configurazione principale (ad esempio app/config/config.yml).
Il risultato finale di questa configurazione è un sistema di sicurezza pienamente funzionale, simile al seguente:
• Ci sono due utenti nel sistema (ryan e admin);
• Gli utenti si autenticano tramite autenticazione HTTP;
• Qualsiasi URL corrispondente a /admin/* è protetto e solo l’utente admin può accedervi;
• Tutti gli URL che non corrispondono ad /admin/* sono accessibili da tutti gli utenti (e all’utente non
viene chiesto il login).
Di seguito si vedrà brevemente come funziona la sicurezza e come ogni parte della configurazione entra in gioco.
Come funziona la sicurezza: autenticazione e autorizzazione
Il sistema di sicurezza di Symfony funziona determinando l’identità di un utente (autenticazione) e poi controllando se l’utente deve avere accesso a una risorsa specifica o URL.
Firewall (autenticazione)
Quando un utente effettua una richiesta a un URL che è protetta da un firewall, viene attivato il sistema di sicurezza.
Il compito del firewall è quello di determinare se l’utente deve o non deve essere autenticato e se deve autenticarsi,
rimandare una risposta all’utente, avviando il processo di autenticazione.
Un firewall viene attivato quando l’URL di una richiesta in arrivo corrisponde al valore pattern dell’espressione
regolare del firewall configurato. In questo esempio, pattern (^/) corrisponderà a ogni richiesta in arrivo. Il
fatto che il firewall venga attivato non significa tuttavia che venga visualizzato il box di autenticazione con nome
utente e password per ogni URL. Per esempio, qualunque utente può accedere a /foo senza che venga richiesto
di autenticarsi.
168
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
Questo funziona in primo luogo perché il firewall consente utenti anonimi, attraverso il parametro di configurazione anonymous. In altre parole, il firewall non richiede all’utente di fare immediatamente un’autenticazione.
E poiché non è necessario nessun ruolo speciale per accedere a /foo (sotto la sezione access_control),
la richiesta può essere soddisfatta senza mai chiedere all’utente di autenticarsi.
Se si rimuove la chiave anonymous, il firewall chiederà sempre l’autenticazione all’utente.
Controlli sull’accesso (autorizzazione)
Se un utente richiede /admin/foo, il processo ha un diverso comportamento. Questo perché la sezione di
configurazione access_control dice che qualsiasi URL che corrispondono allo schema dell’espressione regolare ^/admin (cioè /admin o qualunque URL del tipo /admin/*) richiede il ruolo ROLE_ADMIN. I ruoli
sono la base per la maggior parte delle autorizzazioni: un utente può accedere /admin/foo solo se ha il ruolo
ROLE_ADMIN.
2.1. Libro
169
Symfony2 documentation Documentation, Release 2
Come prima, quando l’utente effettua inizialmente la richiesta, il firewall non chiede nessuna identificazione.
Tuttavia, non appena il livello di controllo di accesso nega l’accesso all’utente (perché l’utente anonimo non ha il
ruolo ROLE_ADMIN), il firewall entra in azione e avvia il processo di autenticazione. Il processo di autenticazione
dipende dal meccanismo di autenticazione in uso. Per esempio, se si sta utilizzando il metodo di autenticazione
tramite form di login, l’utente verrà rinviato alla pagina di login. Se si utilizza l’autenticazione HTTP, all’utente
sarà inviata una risposta HTTP 401 e verrà visualizzato una finestra del browser con nome utente e password.
Ora l’utente ha la possibilità di inviare le credenziali all’applicazione. Se le credenziali sono valide, può essere
riprovata la richiesta originale.
170
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
In questo esempio, l’utente ryan viene autenticato con successo con il firewall. Ma poiché ryan non ha il ruolo
ROLE_ADMIN, viene ancora negato l’accesso a /admin/foo. In definitiva, questo significa che l’utente vedrà
un qualche messaggio che indica che l’accesso è stato negato.
Tip: Quando Symfony nega l’accesso all’utente, l’utente vedrà una schermata di errore e riceverà un codice di
stato HTTP 403 (Forbidden). È possibile personalizzare la schermata di errore di accesso negato seguendo le
istruzioni sulle pagine di errore presenti nel ricettario per personalizzare la pagina di errore 403.
Infine, se l’utente admin richiede /admin/foo, avviene un processo simile, solo che adesso, dopo essere stato
autenticato, il livello di controllo di accesso lascerà passare la richiesta:
2.1. Libro
171
Symfony2 documentation Documentation, Release 2
Il flusso di richiesta quando un utente richiede una risorsa protetta è semplice, ma incredibilmente flessibile. Come
si vedrà in seguito, l’autenticazione può essere gestita in molti modi, come un form di login, un certificato X.509,
o da un’autenticazione dell’utente tramite Twitter. Indipendentemente dal metodo di autenticazione, il flusso di
richiesta è sempre lo stesso:
1. Un utente accede a una risorsa protetta;
2. L’applicazione rinvia l’utente al form di login;
3. L’utente invia le proprie credenziali (ad esempio nome utente / password);
4. Il firewall autentica l’utente;
5. L’utente autenticato riprova la richiesta originale.
Note: L’esatto processo in realtà dipende un po’ da quale meccanismo di autenticazione si sta usando. Per
esempio, quando si utilizza il form di login, l’utente invia le sue credenziali a un URL che elabora il form (ad
esempio /login_check) e poi viene rinviato all’URL originariamente richiesto (ad esempio /admin/foo).
Ma con l’autenticazione HTTP, l’utente invia le proprie credenziali direttamente all’URL originale (ad esempio
/admin/foo) e poi la pagina viene restituita all’utente nella stessa richiesta (cioè senza rinvio).
Questo tipo di idiosincrasie non dovrebbe causare alcun problema, ma è bene tenerle a mente.
Tip: Più avanti si imparerà che in Symfony2 qualunque cosa può essere protetto, tra cui controllori specifici,
oggetti, o anche metodi PHP.
Utilizzo di un form di login tradizionale
Finora, si è visto come proteggere l’applicazione con un firewall e poi proteggere l’accesso a determinate aree
tramite i ruoli. Utilizzando l’autenticazione HTTP, si può sfruttare senza fatica il box nativo nome utente/password
172
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
offerti da tutti i browser. Tuttavia, Symfony supporta nativamente molti meccanismi di autenticazione. Per i
dettagli su ciascuno di essi, vedere il Riferimento sulla configurazione di sicurezza.
In questa sezione, si potrà proseguire l’apprendimento, consentendo all’utente di autenticarsi attraverso un
tradizionale form di login HTML.
In primo luogo, abilitare il form di login sotto il firewall:
• YAML
# app/config/security.yml
security:
firewalls:
secured_area:
pattern:
^/
anonymous: ~
form_login:
login_path:
check_path:
/login
/login_check
• XML
<!-- app/config/security.xml -->
<srv:container xmlns="http://symfony.com/schema/dic/security"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:srv="http://symfony.com/schema/dic/services"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/
<config>
<firewall name="secured_area" pattern="^/">
<anonymous />
<form-login login_path="/login" check_path="/login_check" />
</firewall>
</config>
</srv:container>
• PHP
// app/config/security.php
$container->loadFromExtension(’security’, array(
’firewalls’ => array(
’secured_area’ => array(
’pattern’ => ’^/’,
’anonymous’ => array(),
’form_login’ => array(
’login_path’ => ’/login’,
’check_path’ => ’/login_check’,
),
),
),
));
Tip: Se non è necessario personalizzare i valori login_path o check_path (i valori usati qui sono i valori
predefiniti), è possibile accorciare la configurazione:
• YAML
form_login: ~
• XML
<form-login />
• PHP
2.1. Libro
173
Symfony2 documentation Documentation, Release 2
’form_login’ => array(),
Ora, quando il sistema di sicurezza inizia il processo di autenticazione, rinvierà l’utente al form di login (/login
per impostazione predefinita). Implementare visivamente il form di login è compito dello sviluppatore. In primo
luogo, bisogna creare due rotte: una che visualizzerà il form di login (cioè /login) e un’altra che gestirà l’invio
del form di login (ad esempio /login_check):
• YAML
# app/config/routing.yml
login:
pattern:
/login
defaults: { _controller: AcmeSecurityBundle:Security:login }
login_check:
pattern:
/login_check
• XML
<!-- app/config/routing.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/r
<route id="login" pattern="/login">
<default key="_controller">AcmeSecurityBundle:Security:login</default>
</route>
<route id="login_check" pattern="/login_check" />
</routes>
• PHP
// app/config/routing.php
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;
$collection = new RouteCollection();
$collection->add(’login’, new Route(’/login’, array(
’_controller’ => ’AcmeDemoBundle:Security:login’,
)));
$collection->add(’login_check’, new Route(’/login_check’, array()));
return $collection;
Note: Non è necessario implementare un controllore per l’URL /login_check perché il firewall catturerà ed
elaborerà qualunque form inviato a questo URL.
New in version 2.1: A partire da Symfony 2.1, si devono avere rotte configurate per i propri URL login_path
(p.e. /login) e check_path (p.e. /login_check). Notare che il nome della rotta login non è importante.
Quello che è importante è che l’URL della rotta (/login) corrisponda al valore di configurazione login_path,
in quanto è lì che il sistema di sicurezza rinvierà gli utenti che necessitano di effettuare il login.
Successivamente, creare il controllore che visualizzerà il form di login:
// src/Acme/SecurityBundle/Controller/Main;
namespace Acme\SecurityBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\Security\Core\SecurityContext;
174
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
class SecurityController extends Controller
{
public function loginAction()
{
$request = $this->getRequest();
$session = $request->getSession();
// verifica di eventuali errori
if ($request->attributes->has(SecurityContext::AUTHENTICATION_ERROR)) {
$error = $request->attributes->get(SecurityContext::AUTHENTICATION_ERROR);
} else {
$error = $session->get(SecurityContext::AUTHENTICATION_ERROR);
}
return $this->render(’AcmeSecurityBundle:Security:login.html.twig’, array(
// ultimo nome utente inserito
’last_username’ => $session->get(SecurityContext::LAST_USERNAME),
’error’
=> $error,
));
}
}
Non bisogna farsi confondere da questo controllore. Come si vedrà a momenti, quando l’utente compila il form,
il sistema di sicurezza lo gestisce automaticamente. Se l’utente ha inviato un nome utente o una password non
validi, questo controllore legge l’errore di invio del form dal sistema di sicurezza, in modo che possano essere
visualizzati all’utente.
In altre parole, il vostro compito è quello di visualizzare il form di login e gli eventuali errori di login che potrebbero essersi verificati, ma è il sistema di sicurezza stesso che si prende cura di verificare il nome utente e la
password inviati e di autenticare l’utente.
Infine, creare il template corrispondente:
• Twig
{# src/Acme/SecurityBundle/Resources/views/Security/login.html.twig #}
{% if error %}
<div>{{ error.message }}</div>
{% endif %}
<form action="{{ path(’login_check’) }}" method="post">
<label for="username">Username:</label>
<input type="text" id="username" name="_username" value="{{ last_username }}" />
<label for="password">Password:</label>
<input type="password" id="password" name="_password" />
{#
Se si desidera controllare l’URL a cui l’utente viene rinviato in caso di successo (m
<input type="hidden" name="_target_path" value="/account" />
#}
<input type="submit" name="login" />
</form>
• PHP
<?php // src/Acme/SecurityBundle/Resources/views/Security/login.html.php ?>
<?php if ($error): ?>
<div><?php echo $error->getMessage() ?></div>
<?php endif; ?>
<form action="<?php echo $view[’router’]->generate(’login_check’) ?>" method="post">
<label for="username">Username:</label>
2.1. Libro
175
Symfony2 documentation Documentation, Release 2
<input type="text" id="username" name="_username" value="<?php echo $last_username ?>" />
<label for="password">Password:</label>
<input type="password" id="password" name="_password" />
<!-Se si desidera controllare l’URL a cui l’utente viene rinviato in caso di successo (m
<input type="hidden" name="_target_path" value="/account" />
-->
<input type="submit" name="login" />
</form>
Tip:
La
variabile
error
passata
nel
template
è
un’istanza
di
Symfony\Component\Security\Core\Exception\AuthenticationException.
Potrebbe
contenere informazioni, anche sensibili, sull’errore di autenticazione: va quindi usata con cautela.
Il form ha pochi requisiti. In primo luogo, inviando il form a /login_check (tramite la rotta login_check),
il sistema di sicurezza intercetterà l’invio del form e lo processerà automaticamente. In secondo luogo, il sistema
di sicurezza si aspetta che i campi inviati siano chiamati _username e _password (questi nomi di campi
possono essere configurati).
E questo è tutto! Quando si invia il form, il sistema di sicurezza controllerà automaticamente le credenziali
dell’utente e autenticherà l’utente o rimanderà l’utente al form di login, dove sono visualizzati gli errori.
Rivediamo l’intero processo:
1. L’utente prova ad accedere a una risorsa protetta;
2. Il firewall avvia il processo di autenticazione rinviando l’utente al form di login (/login);
3. La pagina /login rende il form di login, attraverso la rotta e il controllore creato in questo esempio;
4. L’utente invia il form di login /login_check;
5. Il sistema di sicurezza intercetta la richiesta, verifica le credenziali inviate dall’utente, autentica l’utente se
sono corrette e, se non lo sono, lo rinvia al form di login.
Per impostazione predefinita, se le credenziali inviate sono corrette, l’utente verrà rinviato alla pagina originale
che è stata richiesta (ad esempio /admin/foo). Se l’utente originariamente è andato direttamente alla pagina
di login, sarà rinviato alla pagina iniziale. Questo comportamento può essere personalizzato, consentendo, ad
esempio, di rinviare l’utente a un URL specifico.
Per maggiori dettagli su questo e su come personalizzare in generale il processo di login con il form, vedere Come
personalizzare il form di login.
176
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
Come evitare gli errori più comuni
Quando si imposta il proprio form di login, bisogna fare attenzione a non incorrere in alcuni errori comuni.
1. Creare le rotte giuste
In primo luogo, essere sicuri di aver definito correttamente le rotte /login e /login_check e che
corrispondano ai valori di configurazione login_path e check_path. Un errore di configurazione qui
può significare che si viene rinviati a una pagina 404 invece che nella pagina di login, o che inviando il form
di login non succede nulla (continuando a vedere sempre il form di login).
2. Assicurarsi che la pagina di login non sia protetta
Inoltre, bisogna assicurarsi che la pagina di login non richieda nessun ruolo per essere visualizzata. Per
esempio, la seguente configurazione, che richiede il ruolo ROLE_ADMIN per tutti gli URL (includendo
l’URL /login), causerà un loop di redirect:
• YAML
access_control:
- { path: ^/, roles: ROLE_ADMIN }
• XML
<access-control>
<rule path="^/" role="ROLE_ADMIN" />
</access-control>
• PHP
’access_control’ => array(
array(’path’ => ’^/’, ’role’ => ’ROLE_ADMIN’),
),
Rimuovendo il controllo degli accessi sull’URL /login il problema si risolve:
• YAML
access_control:
- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/, roles: ROLE_ADMIN }
• XML
<access-control>
<rule path="^/login" role="IS_AUTHENTICATED_ANONYMOUSLY" />
<rule path="^/" role="ROLE_ADMIN" />
</access-control>
• PHP
’access_control’ => array(
array(’path’ => ’^/login’, ’role’ => ’IS_AUTHENTICATED_ANONYMOUSLY’),
array(’path’ => ’^/’, ’role’ => ’ROLE_ADMIN’),
),
Inoltre, se il firewall non consente utenti anonimi, sarà necessario creare un firewall speciale, che consenta
agli utenti anonimi la pagina di login:
• YAML
firewalls:
login_firewall:
pattern:
anonymous:
secured_area:
pattern:
form_login:
^/login$
~
^/
~
• XML
<firewall name="login_firewall" pattern="^/login$">
<anonymous />
</firewall>
<firewall name="secured_area" pattern="^/">
<form_login />
</firewall>
PHP
2.1. •Libro
’firewalls’ => array(
’login_firewall’ => array(
’pattern’ => ’^/login$’,
177
Symfony2 documentation Documentation, Release 2
Autorizzazione
Il primo passo per la sicurezza è sempre l’autenticazione: il processo di verificare l’identità dell’utente. Con
Symfony, l’autenticazione può essere fatta in qualunque modo, attraverso un form di login, autenticazione HTTP
o anche tramite Facebook.
Una volta che l’utente è stato autenticato, l’autorizzazione ha inizio. L’autorizzazione fornisce un metodo standard e potente per decidere se un utente può accedere a una qualche risorsa (un URL, un oggetto del modello,
una chiamata a metodo, ...). Questo funziona tramite l’assegnazione di specifici ruoli a ciascun utente e quindi
richiedendo ruoli diversi per differenti risorse.
Il processo di autorizzazione ha due diversi lati:
1. L’utente ha un insieme specifico di ruoli;
2. Una risorsa richiede un ruolo specifico per poter accedervi.
In questa sezione, ci si concentrerà su come proteggere risorse diverse (ad esempio gli URL, le chiamate a metodi,
ecc) con ruoli diversi. Più avanti, si imparerà di più su come i ruoli sono creati e assegnati agli utenti.
Protezione di specifici schemi di URL
Il modo più semplice per proteggere parte dell’applicazione è quello di proteggere un intero schema di URL. Si è
già visto questo nel primo esempio di questo capitolo, dove tutto ciò a cui corrisponde lo schema di espressione
regolare ^/admin richiede il ruolo ROLE_ADMIN.
È possibile definire tanti schemi di URL quanti ne occorrono, ciascuno è un’espressione regolare.
• YAML
# app/config/security.yml
security:
# ...
access_control:
- { path: ^/admin/users, roles: ROLE_SUPER_ADMIN }
- { path: ^/admin, roles: ROLE_ADMIN }
• XML
<!-- app/config/security.xml -->
<config>
<!-- ... -->
<rule path="^/admin/users" role="ROLE_SUPER_ADMIN" />
<rule path="^/admin" role="ROLE_ADMIN" />
</config>
• PHP
// app/config/security.php
$container->loadFromExtension(’security’, array(
// ...
’access_control’ => array(
array(’path’ => ’^/admin/users’, ’role’ => ’ROLE_SUPER_ADMIN’),
array(’path’ => ’^/admin’, ’role’ => ’ROLE_ADMIN’),
),
));
Tip: Anteporre il percorso con il simbolo ^ assicura che corrispondano solo gli URL che iniziano con lo schema.
Per esempio, un semplice percorso /admin (senza simbolo ^) corrisponderebbe correttamente a /admin/foo,
ma corrisponderebbe anche a URL come /foo/admin.
Per ogni richiesta in arrivo, Symfony2 cerca di trovare una regola per il controllo dell’accesso che corrisponde (la
prima vince). Se l’utente non è ancora autenticato, viene avviato il processo di autenticazione (cioè viene data
178
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
all’utente la possibilità di fare login). Tuttavia, se l’utente è autenticato ma non ha il ruolo richiesto, viene lanciata
un’eccezione Symfony\Component\Security\Core\Exception\AccessDeniedException, che
è possibile gestire e trasformare in una simpatica pagina di errore “accesso negato” per l’utente. Vedere Come
personalizzare le pagine di errore per maggiori informazioni.
Poiché Symfony utilizza la prima regola di controllo accesso trovata, un URL del tipo /admin/users/new
corrisponderà alla prima regola e richiederà solo il ruolo ROLE_SUPER_ADMIN. Qualunque URL tipo
/admin/blog corrisponderà alla seconda regola e richiederà ROLE_ADMIN.
Protezione tramite IP
In certe situazioni può succedere di limitare l’accesso a una data rotta basata su IP. Questo è particolarmente
rilevante nel caso di Edge Side Includes (ESI), per esempio, che utilizzano una rotta chiamata “_internal”. Quando
viene utilizzato ESI, è richiesta la rotta interna dal gateway della cache per abilitare diverse opzioni di cache per le
sottosezioni all’interno di una determinata pagina. Queste rotte fornite con il prefisso ^/_internal per impostazione
predefinita nell’edizione standard di Symfony (assumendo di aver scommentato queste linee dal file delle rotte).
Ecco un esempio di come si possa garantire questa rotta da intrusioni esterne:
• YAML
# app/config/security.yml
security:
# ...
access_control:
- { path: ^/_internal, roles: IS_AUTHENTICATED_ANONYMOUSLY, ip: 127.0.0.1 }
• XML
<access-control>
<rule path="^/_internal" role="IS_AUTHENTICATED_ANONYMOUSLY" ip="127.0.0.1" />
</access-control>
• PHP
’access_control’ => array(
array(’path’ => ’^/_internal’, ’role’ => ’IS_AUTHENTICATED_ANONYMOUSLY’, ’ip’ => ’127.0.0
),
Protezione tramite canale
Molto simile alla sicurezza basata su IP, richiedere l’uso di SSL è semplice, basta aggiungere la voce
access_control:
• YAML
# app/config/security.yml
security:
# ...
access_control:
- { path: ^/cart/checkout, roles: IS_AUTHENTICATED_ANONYMOUSLY, requires_channel: htt
• XML
<access-control>
<rule path="^/cart/checkout" role="IS_AUTHENTICATED_ANONYMOUSLY" requires_channel="https"
</access-control>
• PHP
’access_control’ => array(
array(’path’ => ’^/cart/checkout’, ’role’ => ’IS_AUTHENTICATED_ANONYMOUSLY’, ’requires_ch
),
2.1. Libro
179
Symfony2 documentation Documentation, Release 2
Proteggere un controllore
Proteggere l’applicazione basandosi su schemi di URL è semplice, ma in alcuni casi può non essere abbastanza
granulare. Quando necessario, si può facilmente forzare l’autorizzazione dall’interno di un controllore:
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
// ...
public function helloAction($name)
{
if (false === $this->get(’security.context’)->isGranted(’ROLE_ADMIN’)) {
throw new AccessDeniedException();
}
// ...
}
È anche possibile scegliere di installare e utilizzare l’opzionale JMSSecurityExtraBundle, che può proteggere il controllore utilizzando le annotazioni:
use JMS\SecurityExtraBundle\Annotation\Secure;
/**
* @Secure(roles="ROLE_ADMIN")
*/
public function helloAction($name)
{
// ...
}
Per maggiori informazioni, vedere la documentazione di JMSSecurityExtraBundle. Se si sta utilizzando la distribuzione standard di Symfony, questo bundle è disponibile per impostazione predefinita. In caso contrario, si
può facilmente scaricare e installare.
Protezione degli altri servizi
In realtà, con Symfony si può proteggere qualunque cosa, utilizzando una strategia simile a quella vista nella
sezione precedente. Per esempio, si supponga di avere un servizio (ovvero una classe PHP) il cui compito è
quello di inviare email da un utente all’altro. È possibile limitare l’uso di questa classe, non importa dove è stata
utilizzata, per gli utenti che hanno un ruolo specifico.
Per ulteriori informazioni su come utilizzare il componente della sicurezza per proteggere servizi e metodi diversi
nell’applicazione, vedere Proteggere servizi e metodi di un’applicazione.
Access Control List (ACL): protezione dei singoli oggetti del database
Si immagini di progettare un sistema di blog, in cui gli utenti possono commentare i messaggi. Si vuole che un
utente possa modificare i propri commenti, ma non quelli degli altri. Inoltre, come utente admin, si vuole essere
in grado di modificare tutti i commenti.
Il componente della sicurezza viene fornito con un sistema opzionale di access control list (ACL), che è possibile
utilizzare quando è necessario controllare l’accesso alle singole istanze di un oggetto nel sistema. Senza ACL,
è possibile proteggere il sistema in modo che solo certi utenti possono modificare i commenti sui blog. Ma con
ACL, si può limitare o consentire l’accesso commento per commento.
Per maggiori informazioni, vedere l’articolo del ricettario: Access Control List (ACL).
180
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
Utenti
Nelle sezioni precedenti, si è appreso come sia possibile proteggere diverse risorse, richiedendo una serie di ruoli
per una risorsa. In questa sezione, esploreremo l’altro lato delle autorizzazioni: gli utenti.
Da dove provengono utenti? (User Provider)
Durante l’autenticazione, l’utente invia un insieme di credenziali (di solito un nome utente e una password). Il
compito del sistema di autenticazione è quello di soddisfare queste credenziali con l’insieme degli utenti. Quindi
da dove proviene questa lista di utenti?
In Symfony2, gli utenti possono arrivare da qualsiasi parte: un file di configurazione, una tabella di un database,
un servizio web o qualsiasi altra cosa si può pensare. Qualsiasi cosa che prevede uno o più utenti nel sistema di
autenticazione è noto come “fornitore di utenti”. Symfony2 viene fornito con i due fornitori utenti più diffusi; uno
che carica gli utenti da un file di configurazione e uno che carica gli utenti da una tabella di un database.
Definizione degli utenti in un file di configurazione Il modo più semplice per specificare gli utenti è direttamente in un file di configurazione. In effetti, questo si è già aver visto nell’esempio di questo capitolo.
• YAML
# app/config/security.yml
security:
# ...
providers:
default_provider:
memory:
users:
ryan: { password: ryanpass, roles: ’ROLE_USER’ }
admin: { password: kitten, roles: ’ROLE_ADMIN’ }
• XML
<!-- app/config/security.xml -->
<config>
<!-- ... -->
<provider name="default_provider">
<memory>
<user name="ryan" password="ryanpass" roles="ROLE_USER" />
<user name="admin" password="kitten" roles="ROLE_ADMIN" />
</memory>
</provider>
</config>
• PHP
// app/config/security.php
$container->loadFromExtension(’security’, array(
// ...
’providers’ => array(
’default_provider’ => array(
’memory’ => array(
’users’ => array(
’ryan’ => array(’password’ => ’ryanpass’, ’roles’ => ’ROLE_USER’),
’admin’ => array(’password’ => ’kitten’, ’roles’ => ’ROLE_ADMIN’),
),
),
),
),
));
2.1. Libro
181
Symfony2 documentation Documentation, Release 2
Questo fornitore utenti è chiamato “in-memory” , dal momento che gli utenti non sono memorizzati in un database.
L’oggetto utente effettivo è fornito da Symfony (Symfony\Component\Security\Core\User\User).
Tip: Qualsiasi fornitore utenti può caricare gli utenti direttamente dalla configurazione, specificando il parametro
di configurazione users ed elencando gli utenti sotto di esso.
Caution: Se il nome utente è completamente numerico (ad esempio 77) o contiene un trattino (ad esempio
user-name), è consigliabile utilizzare la seguente sintassi alternativa quando si specificano utenti in YAML:
users:
- { name: 77, password: pass, roles: ’ROLE_USER’ }
- { name: user-name, password: pass, roles: ’ROLE_USER’ }
Per i siti più piccoli, questo metodo è semplice e veloce da configurare. Per sistemi più complessi, si consiglia di
caricare gli utenti dal database.
Caricare gli utenti da un database Se si vuole caricare gli utenti tramite l’ORM Doctrine, si può farlo facilmente attraverso la creazione di una classe User e configurando il fornitore entity.
Con questo approccio, bisogna prima creare la propria classe User, che sarà memorizzata nel database.
// src/Acme/UserBundle/Entity/User.php
namespace Acme\UserBundle\Entity;
use Symfony\Component\Security\Core\User\UserInterface;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
*/
class User implements UserInterface
{
/**
* @ORM\Column(type="string", length="255")
*/
protected $username;
// ...
}
Per come è stato pensato il sistema di sicurezza, l’unico requisito per la classe utente personalizzata è che implementi l’interfaccia Symfony\Component\Security\Core\User\UserInterface. Questo significa
che il concetto di “utente” può essere qualsiasi cosa, purché implementi questa interfaccia. New in version 2.1.
Note: L’oggetto utente verrà serializzato e salvato nella sessione durante le richieste, quindi si consiglia di
implementare l’interfaccia Serializable nel proprio oggetto utente. Ciò è particolarmente importante se la classe
User ha una classe genitore con proprietà private.
Quindi, configurare un fornitore utenti entity e farlo puntare alla classe User:
• YAML
# app/config/security.yml
security:
providers:
main:
entity: { class: Acme\UserBundle\Entity\User, property: username }
• XML
182
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
<!-- app/config/security.xml -->
<config>
<provider name="main">
<entity class="Acme\UserBundle\Entity\User" property="username" />
</provider>
</config>
• PHP
// app/config/security.php
$container->loadFromExtension(’security’, array(
’providers’ => array(
’main’ => array(
’entity’ => array(’class’ => ’Acme\UserBundle\Entity\User’, ’property’ => ’userna
),
),
));
Con l’introduzione di questo nuovo fornitore, il sistema di autenticazione tenterà di caricare un oggetto User dal
database, utilizzando il campo username di questa classe.
Note: Questo esempio ha come unico scopo quello di mostrare l’idea di base dietro al fornitore entity. Per un
esempio completamente funzionante, vedere Come caricare gli utenti dal database (il fornitore di entità).
Per ulteriori informazioni sulla creazione di un proprio fornitore personalizzato (ad esempio se è necessario caricare gli utenti tramite un servizio web), vedere Come creare un fornitore utenti personalizzato.
Codificare la password dell’utente
Finora, per semplicità, tutti gli esempi hanno memorizzato le password dell’utente in formato testo (se tali utenti
sono memorizzati in un file di configurazione o in un qualche database). Naturalmente, in un’applicazione reale,
si consiglia per ragioni di sicurezza, di codificare le password degli utenti. Questo è facilmente realizzabile
mappando la classe User in uno dei numerosi built-in “encoder”. Per esempio, per memorizzare gli utenti in
memoria, ma oscurare le lori password tramite sha1, fare come segue:
• YAML
# app/config/security.yml
security:
# ...
providers:
in_memory:
memory:
users:
ryan: { password: bb87a29949f3a1ee0559f8a57357487151281386, roles: ’ROLE
admin: { password: 74913f5cd5f61ec0bcfdb775414c2fb3d161b620, roles: ’ROLE
encoders:
Symfony\Component\Security\Core\User\User:
algorithm:
sha1
iterations: 1
encode_as_base64: false
• XML
<!-- app/config/security.xml -->
<config>
<!-- ... -->
<provider name="in_memory">
<memory>
<user name="ryan" password="bb87a29949f3a1ee0559f8a57357487151281386" roles="ROLE
2.1. Libro
183
Symfony2 documentation Documentation, Release 2
<user name="admin" password="74913f5cd5f61ec0bcfdb775414c2fb3d161b620" roles="ROL
</memory>
</provider>
<encoder class="Symfony\Component\Security\Core\User\User" algorithm="sha1" iterations="1
</config>
• PHP
// app/config/security.php
$container->loadFromExtension(’security’, array(
// ...
’providers’ => array(
’in_memory’ => array(
’memory’ => array(
’users’ => array(
’ryan’ => array(’password’ => ’bb87a29949f3a1ee0559f8a57357487151281386’,
’admin’ => array(’password’ => ’74913f5cd5f61ec0bcfdb775414c2fb3d161b620’
),
),
),
),
’encoders’ => array(
’Symfony\Component\Security\Core\User\User’ => array(
’algorithm’
=> ’sha1’,
’iterations’
=> 1,
’encode_as_base64’ => false,
),
),
));
Impostando le iterazioni a 1 e il encode_as_base64 a false, è sufficiente eseguire una sola volta
l’algoritmo sha1 sulla password e senza alcuna codifica supplementare. È ora possibile calcolare l’hash della
password a livello di codice (ad esempio hash(’sha1’, ’ryanpass’)) o tramite qualche strumento online
come functions-online.com
Se si sta creando i propri utenti in modo dinamico (e memorizzarli in un database), è possibile utilizzare algoritmi
di hash ancora più complessi e poi contare su un oggetto encoder oggetto per aiutarti a codificare le password.
Per esempio, supponiamo che l’oggetto User sia Acme\UserBundle\Entity\User (come nell’esempio
precedente). In primo, configurare l’encoder per questo utente:
• YAML
# app/config/security.yml
security:
# ...
encoders:
Acme\UserBundle\Entity\User: sha512
• XML
<!-- app/config/security.xml -->
<config>
<!-- ... -->
<encoder class="Acme\UserBundle\Entity\User" algorithm="sha512" />
</config>
• PHP
// app/config/security.php
$container->loadFromExtension(’security’, array(
// ...
184
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
’encoders’ => array(
’Acme\UserBundle\Entity\User’ => ’sha512’,
),
));
In questo caso, si utilizza il più forte algoritmo sha512. Inoltre, poiché si è semplicemente specificato l’algoritmo
(sha512) come stringa, il sistema per impostazione predefinita farà l’hash 5000 volte in un riga e poi la codificherà in base64. In altre parole, la password è stata notevolmente offuscata in modo che l’hash della password
non può essere decodificato (cioè non è possibile determinare la password dall’hash della password).
Se si ha una qualche form di registrazione per gli utenti, è necessario essere in grado di determinare la password
con hash, in modo che sia possibile impostarla per l’utente. Indipendentemente dall’algoritmo configurato per
l’oggetto User, la password con hash può essere determinata nel seguente modo da un controllore:
$factory = $this->get(’security.encoder_factory’);
$user = new Acme\UserBundle\Entity\User();
$encoder = $factory->getEncoder($user);
$password = $encoder->encodePassword(’ryanpass’, $user->getSalt());
$user->setPassword($password);
Recuperare l’oggetto User
Dopo l’autenticazione, si può accedere all’oggetto User per l ‘utente corrente tramite il servizio
security.context. Da dentro un controllore, assomiglierà a questo:
public function indexAction()
{
$user = $this->get(’security.context’)->getToken()->getUser();
}
In un controllore, si può usare una scorciatoia:
public function indexAction()
{
$user = $this->getUser();
}
Note: Gli utenti anonimi sono tecnicamente autenticati, nel senso che il metodo isAuthenticated()
dell’oggetto di un utente anonimo restituirà true. Per controllare se l’utente sia effettivamente autenticato,
verificare il ruolo IS_AUTHENTICATED_FULLY.
In un template Twig, si può accedere a questo oggetto tramite la chiave app.user, che richiama il metodo
:method:‘GlobalVariables::getUser()<Symfony\\Bundle\\FrameworkBundle\\Templating\\GlobalVariables::getUser>‘:
• Twig
<p>Nome utente: {{ app.user.username }}</p>
Utilizzare fornitori utenti multipli
Ogni meccanismo di autenticazione (ad esempio l’autenticazione HTTP, il form di login, ecc.) utilizza esattamente
un fornitore utenti e, per impostazione predefinita, userà il primo fornitore dichiarato. Ma cosa succede se si
desidera specificare alcuni utenti tramite configurazione e il resto degli utenti nel database? Questo è possibile
attraverso la creazione di un nuovo fornitore, che li unisca:
• YAML
2.1. Libro
185
Symfony2 documentation Documentation, Release 2
# app/config/security.yml
security:
providers:
chain_provider:
chain:
providers: [in_memory, user_db]
in_memory:
users:
foo: { password: test }
user_db:
entity: { class: Acme\UserBundle\Entity\User, property: username }
• XML
<!-- app/config/security.xml -->
<config>
<provider name="chain_provider">
<chain>
<provider>in_memory</provider>
<provider>user_db</provider>
</chain>
</provider>
<provider name="in_memory">
<user name="foo" password="test" />
</provider>
<provider name="user_db">
<entity class="Acme\UserBundle\Entity\User" property="username" />
</provider>
</config>
• PHP
// app/config/security.php
$container->loadFromExtension(’security’, array(
’providers’ => array(
’chain_provider’ => array(
’chain’ => array(
’providers’ => array(’in_memory’, ’user_db’),
),
),
’in_memory’ => array(
’users’ => array(
’foo’ => array(’password’ => ’test’),
),
),
’user_db’ => array(
’entity’ => array(’class’ => ’Acme\UserBundle\Entity\User’, ’property’ => ’userna
),
),
));
Ora, tutti i meccanismi di autenticazione utilizzeranno il chain_provider, dal momento che è il primo specificato. Il chain_provider, a sua volta, tenta di caricare l’utente da entrambi i fornitori in_memory e
user_db.
Tip: Se no ci sono ragioni per separare gli utenti in_memory dagli utenti user_db, è possibile ottenere ancora
più facilmente questo risultato combinando le due sorgenti in un unico fornitore:
• YAML
# app/config/security.yml
security:
providers:
186
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
main_provider:
memory:
users:
foo: { password: test }
entity:
class: Acme\UserBundle\Entity\User,
property: username
• XML
<!-- app/config/security.xml -->
<config>
<provider name=="main_provider">
<memory>
<user name="foo" password="test" />
</memory>
<entity class="Acme\UserBundle\Entity\User" property="username" />
</provider>
</config>
• PHP
// app/config/security.php
$container->loadFromExtension(’security’, array(
’providers’ => array(
’main_provider’ => array(
’memory’ => array(
’users’ => array(
’foo’ => array(’password’ => ’test’),
),
),
’entity’ => array(’class’ => ’Acme\UserBundle\Entity\User’, ’property’ => ’userna
),
),
));
È anche possibile configurare il firewall o meccanismi di autenticazione individuali per utilizzare un provider
specifico. Ancora una volta, a meno che un provider sia specificato esplicitamente, viene sempre utilizzato il
primo fornitore:
• YAML
# app/config/security.yml
security:
firewalls:
secured_area:
# ...
provider: user_db
http_basic:
realm: "Demo area sicura"
provider: in_memory
form_login: ~
• XML
<!-- app/config/security.xml -->
<config>
<firewall name="secured_area" pattern="^/" provider="user_db">
<!-- ... -->
<http-basic realm="Demo area sicura" provider="in_memory" />
<form-login />
</firewall>
</config>
2.1. Libro
187
Symfony2 documentation Documentation, Release 2
• PHP
// app/config/security.php
$container->loadFromExtension(’security’, array(
’firewalls’ => array(
’secured_area’ => array(
// ...
’provider’ => ’user_db’,
’http_basic’ => array(
// ...
’provider’ => ’in_memory’,
),
’form_login’ => array(),
),
),
));
In questo esempio, se un utente cerca di accedere tramite autenticazione HTTP, il sistema di autenticazione utilizzerà il fornitore utenti in_memory. Ma se l’utente tenta di accedere tramite il form di login, sarà usato il
fornitore user_db (in quanto è l’impostazione predefinita per il firewall).
Per ulteriori informazioni su fornitori utenti e configurazione del firewall, vedere il Riferimento configurazione
sicurezza.
Ruoli
L’idea di un “ruolo” è la chiave per il processo di autorizzazione. A ogni utente viene assegnato un insieme di
ruoli e quindi ogni risorsa richiede uno o più ruoli. Se l’utente ha i ruoli richiesti, l’accesso è concesso. In caso
contrario, l’accesso è negato.
I ruoli sono abbastanza semplici e sono fondamentalmente stringhe che si possono inventare e utilizzare secondo
necessità (anche se i ruoli internamente sono oggetti). Per esempio, se è necessario limitare l’accesso alla sezione
admin del sito web del blog , si potrebbe proteggere quella parte con un ruolo ROLE_BLOG_ADMIN. Questo ruolo
non ha bisogno di essere definito ovunque, è sufficiente iniziare a usarlo.
Note: Tutti i ruoli devono iniziare con il prefisso ROLE_ per poter essere gestiti da Symfony2. Se si definiscono
i propri ruoli con una classe Role dedicata (caratteristica avanzata), non bisogna usare il prefisso ROLE_.
I ruoli gerarchici
Invece di associare molti ruoli agli utenti, è possibile definire regole di ereditarietà dei ruoli creando una gerarchia
di ruoli:
• YAML
# app/config/security.yml
security:
role_hierarchy:
ROLE_ADMIN:
ROLE_USER
ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]
• XML
<!-- app/config/security.xml -->
<config>
<role id="ROLE_ADMIN">ROLE_USER</role>
<role id="ROLE_SUPER_ADMIN">ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH</role>
</config>
• PHP
188
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
// app/config/security.php
$container->loadFromExtension(’security’, array(
’role_hierarchy’ => array(
’ROLE_ADMIN’
=> ’ROLE_USER’,
’ROLE_SUPER_ADMIN’ => array(’ROLE_ADMIN’, ’ROLE_ALLOWED_TO_SWITCH’),
),
));
Nella configurazione sopra, gli utenti con ruolo ROLE_ADMIN avranno anche il ruolo ROLE_USER. Il
ruolo ROLE_SUPER_ADMIN ha ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH e ROLE_USER (ereditati da
ROLE_ADMIN).
Logout
Generalmente, si vuole che gli utenti possano disconnettersi tramite logout. Fortunatamente, il firewall può gestire
automaticamente questo caso quando si attiva il parametro di configurazione logout:
• YAML
# app/config/security.yml
security:
firewalls:
secured_area:
# ...
logout:
path:
/logout
target: /
# ...
• XML
<!-- app/config/security.xml -->
<config>
<firewall name="secured_area" pattern="^/">
<!-- ... -->
<logout path="/logout" target="/" />
</firewall>
<!-- ... -->
</config>
• PHP
// app/config/security.php
$container->loadFromExtension(’security’, array(
’firewalls’ => array(
’secured_area’ => array(
// ...
’logout’ => array(’path’ => ’logout’, ’target’ => ’/’),
),
),
// ...
));
Una volta che questo viene configurato sotto il firewall, l’invio di un utente in /logout (o qualunque debba essere
il percorso) farà disconnettere l’utente corrente. L’utente sarà quindi inviato alla pagina iniziale (il valore definito
dal parametro target). Entrambi i parametri di configurazione path e target assumono come impostazione
predefinita ciò che è specificato qui. In altre parole, se non è necessario personalizzarli, è possibile ometterli
completamente e accorciare la configurazione:
• YAML
logout: ~
2.1. Libro
189
Symfony2 documentation Documentation, Release 2
• XML
<logout />
• PHP
’logout’ => array(),
Si noti che non è necessario implementare un controllore per l’URL /logout, perché il firewall si occupa di
tutto. Si può, tuttavia, creare una rotta da poter utilizzare per generare l’URL:
• YAML
# app/config/routing.yml
logout:
pattern:
/logout
• XML
<!-- app/config/routing.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/r
<route id="logout" pattern="/logout" />
</routes>
• PHP
// app/config/routing.php
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;
$collection = new RouteCollection();
$collection->add(’logout’, new Route(’/logout’, array()));
return $collection;
Una volta che l’utente è stato disconnesso, viene rinviato al percorso definito dal parametro target sopra (ad
esempio, la homepage). Per ulteriori informazioni sulla configurazione di logout, vedere il Riferimento della
configurazione di sicurezza.
Controllare l’accesso nei template
Nel caso si voglia controllare all’interno di un template se l’utente corrente ha un ruolo, usare la funzione helper:
• Twig
{% if is_granted(’ROLE_ADMIN’) %}
<a href="...">Delete</a>
{% endif %}
• PHP
<?php if ($view[’security’]->isGranted(’ROLE_ADMIN’)): ?>
<a href="...">Delete</a>
<?php endif; ?>
Note: Se si utilizza questa funzione e non si è in un URL dove c’è un firewall attivo, viene lanciata un’eccezione.
Anche in questo caso, è quasi sempre una buona idea avere un firewall principale che copra tutti gli URL (come
si è visto in questo capitolo).
190
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
Verifica dell’accesso nei controllori
Quando si vuole verificare se l’utente corrente abbia un ruolo nel controllore, usare il metodo isGranted del
contesto di sicurezza:
public function indexAction()
{
// mostrare contenuti diversi agli utenti admin
if($this->get(’security.context’)->isGranted(’ADMIN’)) {
// caricare qui contenuti di amministrazione
}
// caricare qui altri contenuti normali
}
Note: Un firewall deve essere attivo o verrà lanciata un’eccezione quando viene chiamato il metodo isGranted.
Vedere la nota precedente sui template per maggiori dettagli.
Impersonare un utente
A volte, è utile essere in grado di passare da un utente all’altro senza dover uscire e rientrare tutte le volte (per
esempio quando si esegue il debug o si cerca di capire un bug che un utente vede ma che non si riesce a riprodurre).
Lo si può fare facilmente, attivando l’ascoltatore switch_user del firewall:
• YAML
# app/config/security.yml
security:
firewalls:
main:
# ...
switch_user: true
• XML
<!-- app/config/security.xml -->
<config>
<firewall>
<!-- ... -->
<switch-user />
</firewall>
</config>
• PHP
// app/config/security.php
$container->loadFromExtension(’security’, array(
’firewalls’ => array(
’main’=> array(
// ...
’switch_user’ => true
),
),
));
Per passare a un altro utente, basta aggiungere una stringa query all’URL corrente, con il parametro
_switch_user e il nome utente come valore :
http://example.com/indirizzo?_switch_user=thomas
Per tornare indietro all’utente originale, usare il nome utente speciale _exit:
2.1. Libro
191
Symfony2 documentation Documentation, Release 2
http://example.com/indirizzo?_switch_user=_exit
Naturalmente, questa funzionalità deve essere messa a disposizione di un piccolo gruppo di utenti. Per impostazione predefinita, l’accesso è limitato agli utenti che hanno il ruolo ROLE_ALLOWED_TO_SWITCH. Il nome
di questo ruolo può essere modificato tramite l’impostazione role. Per maggiore sicurezza, è anche possibile
modificare il nome del parametro della query tramite l’impostazione parameter:
• YAML
# app/config/security.yml
security:
firewalls:
main:
// ...
switch_user: { role: ROLE_ADMIN, parameter: _want_to_be_this_user }
• XML
<!-- app/config/security.xml -->
<config>
<firewall>
<!-- ... -->
<switch-user role="ROLE_ADMIN" parameter="_want_to_be_this_user" />
</firewall>
</config>
• PHP
// app/config/security.php
$container->loadFromExtension(’security’, array(
’firewalls’ => array(
’main’=> array(
// ...
’switch_user’ => array(’role’ => ’ROLE_ADMIN’, ’parameter’ => ’_want_to_be_this_u
),
),
));
Autenticazione senza stato
Per impostazione predefinita, Symfony2 si basa su un cookie (Session) per persistere il contesto di sicurezza
dell’utente. Ma se si utilizzano certificati o l’autenticazione HTTP, per esempio, la persistenza non è necessaria,
in quanto le credenziali sono disponibili a ogni richiesta. In questo caso e se non è necessario memorizzare
nient’altro tra le richieste, è possibile attivare l’autenticazione senza stato (il che significa Symfony non creerà
alcun cookie):
• YAML
# app/config/security.yml
security:
firewalls:
main:
http_basic: ~
stateless: true
• XML
<!-- app/config/security.xml -->
<config>
<firewall stateless="true">
<http-basic />
</firewall>
</config>
192
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
• PHP
// app/config/security.php
$container->loadFromExtension(’security’, array(
’firewalls’ => array(
’main’ => array(’http_basic’ => array(), ’stateless’ => true),
),
));
Note: Se si usa un form di login, Symfony2 creerà un cookie anche se si imposta stateless a true.
Considerazioni finali
La sicurezza può essere un problema profondo e complesso nell’applicazione da risolvere in modo corretto. Per
fortuna, il componente della sicurezza di Symfony segue un ben collaudato modello di sicurezza basato su autenticazione e autorizzazione. L’autenticazione, che avviene sempre per prima, è gestita da un firewall il cui compito
è quello di determinare l’identità degli utenti attraverso diversi metodi (ad esempio l’autenticazione HTTP, il form
di login, ecc.). Nel ricettario, si trovano esempi di altri metodi per la gestione dell’autenticazione, includendo
quello che tratta l’implementazione della funzionalità cookie “Ricorda i dati”.
Una volta che un utente è autenticato, lo strato di autorizzazione può stabilire se l’utente debba o meno avere
accesso a una specifica risorsa. Più frequentemente, i ruoli sono applicati a URL, classi o metodi e se l’utente
corrente non ha quel ruolo, l’accesso è negato. Lo strato di autorizzazione, però, è molto più profondo e segue un
sistema di “voto”, in modo che tutte le parti possono determinare se l’utente corrente dovrebbe avere accesso a
una data risorsa. Ulteriori informazioni su questo e altri argomenti nel ricettario.
Saperne di più con il ricettario
• Forzare HTTP/HTTPS
• Blacklist di utenti per indirizzo IP
• Access Control List (ACL)
• Come aggiungere la funzionalità “ricordami” al login
2.1.13 Cache HTTP
Le applicazioni web sono dinamiche. Non importa quanto efficiente possa essere la propria applicazione, ogni
richiesta conterrà sempre overhead rispetto a quando si serve un file statico.
Per la maggior parte delle applicazione, questo non è un problema. Symfony2 è molto veloce e, a meno che non si
stia facendo qualcosa di veramente molto pesante, ogni richiesta sarà gestita rapidamente, senza stressare troppo
il server.
Man mano che il proprio sito cresce, però, quell’overhead può diventare un problema. Il processo normalmente
seguito a ogni richiesta andrebbe fatto una volta sola. Questo è proprio lo scopo che si prefigge la cache.
La cache sulle spalle dei giganti
Il modo più efficace per migliorare le prestazioni di un’applicazione è mettere in cache l’intero output di una
pagina e quindi aggirare interamente l’applicazione a ogni richiesta successiva. Ovviamente, questo non è sempre
possibile per siti altamente dinamici, oppure sì? In questo capitolo, mostreremo come funziona il sistema di cache
di Symfony2 e perché pensiamo che sia il miglior approccio possibile.
Il sistema di cache di Symfony2 è diverso, perché si appoggia sulla semplicità e sulla potenza della cache HTTP,
definita nelle specifiche HTTP. Invence di inventare un altro metodo di cache, Symfony2 abbraccia lo standard che
2.1. Libro
193
Symfony2 documentation Documentation, Release 2
definisce la comunicazione di base sul web. Una volta capiti i fondamenti dei modelli di validazione e scadenza
della cache HTTP, si sarà in grado di padroneggiare il sistema di cache di Symfony2.
Per poter imparare come funziona la cache in Symfony2, procederemo in quattro passi:
• Passo 1: Un gateway cache, o reverse proxy, è un livello indipendente che si situa davanti alla propria
applicazione. Il reverse proxy mette in cache le risposte non appena sono restituite dalla propria applicazione
e risponde alle richieste con risposte in cache, prima che arrivino alla propria applicazione. Symfony2
fornisce il suo reverse proxy, ma se ne può usare uno qualsiasi.
• Passo 2: Gli header di cache HTTP sono usati per comunicare col gateway cache e con ogni altra cache
tra la propria applicazione e il client. Symfony2 fornisce impostazioni predefinite appropriate e una potente
interfaccia per interagire con gli header di cache.
• Passo 3: La scadenza e validazione HTTP sono due modelli usati per determinare se il contenuto in cache
è fresco (può essere riusato dalla cache) o vecchio (andrebbe rigenerato dall’applicazione):
• Passo 4: Gli Edge Side Include (ESI) consentono alla cache HTTP di essere usata per mettere in cache
frammenti di pagine (anche frammenti annidati) in modo indipendente. Con ESI, si può anche mettere in
cache una pagina intera per 60 minuti, ma una barra laterale interna per soli 5 minuti.
Poiché la cache con HTTP non è esclusiva di Symfony2, esistono già molti articoli a riguardo. Se si è nuovi
con la cache HTTP, raccomandiamo caldamente l’articolo di Ryan Tomayko Things Caches Do. Un’altra risorsa
importante è il Cache Tutorial di Mark Nottingham.
Cache con gateway cache
Quando si usa la cache con HTTP, la cache è completamente separata dalla propria applicazione e risiede in mezzo
tra la propria applicazione e il client che effettua la richiesta.
Il compito della cache è accettare le richieste dal client e passarle alla propria applicazione. La cache riceverà
anche risposte dalla propria applicazione e le girerà al client. La cache è un “uomo in mezzo” nella comunicazione
richiesta-risposta tra il client e la propria applicazione.
Lungo la via, la cache memorizzerà ogni risposta ritenuta “cacheable” (vedere Introduzione alla cache HTTP). Se
la stessa risorsa viene richiesta nuovamente, la cache invia la risposta in cache al client, ignorando completamente
la propria applicazione.
Questo tipo di cache è nota come HTTP gateway cache e ne esistono diverse, come Varnish, Squid in modalità
reverse proxy e il reverse proxy di Symfony2.
Tipi di cache
Ma il gateway cache non è l’unico tipo di cache. Infatti, gli header HTTP di cache inviati dalla propria applicazioni
sono analizzati e interpretati da tre diversi tipi di cache:
• Cache del browser: Ogni browser ha la sua cache locale, usata principalmente quando si clicca sul pulsante
“indietro” per immagini e altre risorse. La cache del browser è una cache privata, perché le risorse in cache
non sono condivise con nessun altro.
• Proxy cache: Un proxy è una cache condivisa, perché molte persone possono stare dietro a un singolo proxy.
Solitamente si trova nelle grandi aziende e negli ISP, per ridurre la latenza e il traffico di rete.
• Gateway cache: Come il proxy, anche questa è una cache condivisa, ma dalla parte del server. Installata dai
sistemisti di rete, rende i siti più scalabili, affidabili e performanti.
Tip: Le gateway cache sono a volte chiamate reverse proxy cache, cache surrogate o anche acceleratori HTTP.
Note: I significati di cache privata e condivisa saranno più chiari quando si parlerà di mettere in cache risposte
che contengono contenuti specifici per un singolo utente (p.e. informazioni sull’account).
194
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
Ogni risposta dalla propria applicazione probabilmente attraverserà una o più cache dei primi due tipi. Queste
cache sono fuori dal nostro controllo, ma seguono le indicazioni di cache HTTP impostate nella risposta.
Il reverse proxy di Symfony2
Symfony2 ha un suo reverse proxy (detto anche gateway cache) scritto in PHP. Abilitandolo, le risposte in cache
dalla propria applicazione inizieranno a essere messe in cache. L’installazione è altrettanto facile. Ogni una
applicazione Symfony2 ha la cache già configurata in AppCache, che estende AppKernel. Il kernel della
cache è il reverse proxy.
Per abilitare la cache, modificare il codice di un front controller, per usare il kernel della cache:
// web/app.php
require_once __DIR__.’/../app/bootstrap.php.cache’;
require_once __DIR__.’/../app/AppKernel.php’;
require_once __DIR__.’/../app/AppCache.php’;
use Symfony\Component\HttpFoundation\Request;
$kernel = new AppKernel(’prod’, false);
$kernel->loadClassCache();
// wrap the default AppKernel with the AppCache one
$kernel = new AppCache($kernel);
$kernel->handle(Request::createFromGlobals())->send();
Il kernel della cache agirà immediatamente da reverse proxy, mettendo in cache le risposte della propria applicazione e restituendole al client.
Tip: Il kernel della cache ha uno speciale metodo getLog(), che restituisce una rappresentazione in stringa
di ciò che avviene a livello di cache. Nell’ambiente di sviluppo, lo si può usare per il debug e la verifica della
strategia di cache:
error_log($kernel->getLog());
L’oggetto AppCache una una configurazione predefinita adeguata, ma può essere regolato tramite un insieme di
opzioni impostabili sovrascrivendo il metodo getOptions():
// app/AppCache.php
use Symfony\Bundle\FrameworkBundle\HttpCache\HttpCache;
class AppCache extends HttpCache
{
protected function getOptions()
{
return array(
’debug’
’default_ttl’
’private_headers’
’allow_reload’
’allow_revalidate’
’stale_while_revalidate’
’stale_if_error’
);
}
}
=>
=>
=>
=>
=>
=>
=>
false,
0,
array(’Authorization’, ’Cookie’),
false,
false,
2,
60,
Tip: A meno che non sia sovrascritta in getOptions(), l’opzione debug sarà impostata automaticamente al
valore di debug di AppKernel circostante.
2.1. Libro
195
Symfony2 documentation Documentation, Release 2
Ecco una lista delle opzioni principali:
• default_ttl: Il numero di secondi per cui un elemento in cache va considerato fresco, quando nessuna
informazione esplicita sulla freschezza viene fornita in una risposta. Header espliciti Cache-Control o
Expires sovrascrivono questo valore (predefinito: 0);
• private_headers: Insieme di header di richiesta che fanno scattare il comportamento “privato”
Cache-Control sulle risposte che non stabiliscono esplicitamente il loro stato di public o private,
tramite una direttiva Cache-Control. (predefinito: Authorization e Cookie);
• allow_reload: Specifica se il client possa forzare un ricaricamento della cache includendo una direttiva
Cache-Control “no-cache” nella richiesta. Impostare a true per aderire alla RFC 2616 (predefinito:
false);
• allow_revalidate: Specifica se il client possa forzare una rivalidazione della cache includendo una
direttiva Cache-Control “max-age=0” nella richiesta. Impostare a true per aderire alla RFC 2616
(predefinito: false);
• stale_while_revalidate: Specifica il numero predefinito di secondi (la granularità è il secondo,
perché la precisione del TTL della risposta è un secondo) durante il quale la cache può restituire immediatamente una risposta vecchia mentre si rivalida in background (predefinito: 2); questa impostazione
è sovrascritta dall’estensione stale-while-revalidate Cache-Control di HTTP (vedere RFC
5861);
• stale_if_error: Specifica il numero predefinito di secondi (la granularità è il secondo) durante il
quale la cache può servire una risposta vecchia quando si incontra un errore (predefinito: 60). Questa
impostazione è sovrascritta dall’estensione stale-if-error Cache-Control di HTTP (vedere RFC
5861).
Se debug è true, Symfony2 aggiunge automaticamente un header X-Symfony-Cache alla risposta, con
dentro informazioni utili su hit e miss della cache.
Cambiare da un reverse proxy a un altro
Il reverse proxy di Symfony2 è un grande strumento da usare durante lo sviluppo del proprio sito oppure
quando il deploy del proprio sito è su un host condiviso, dove non si può installare altro che codice PHP.
Ma essendo scritto in PHP, non può essere veloce quando un proxy scritto in C. Per questo raccomandiamo
caldamente di usare Varnish o Squid sul proprio server di produzione, se possibile. La buona notizia è che il
cambio da un proxy a un altro è facile e trasparente, non implicando alcuna modifica al codice della propria
applicazione. Si può iniziare semplicemente con il reverse proxy di Symfony2 e aggiornare successivamente
a Varnish, quando il traffico aumenta.
Per maggiori informazioni sull’uso di Varnish con Symfony2, vedere la ricetta Usare Varnish.
Note: Le prestazioni del reverse proxy di Symfony2 non dipendono dalla complessità dell’applicazione. Questo
perché il kernel dell’applicazione parte solo quando ha una richiesta a cui deve essere rigirato.
Introduzione alla cache HTTP
Per sfruttare i livelli di cache disponibili, la propria applicazione deve poter comunicare quale risposta può essere
messa in cache e le regole che stabiliscono quando e come tale cache debba essere considerata vecchia. Lo si può
fare impostando gli header di cache HTTP nella risposta.
Tip: Si tenga a mente che “HTTP” non è altro che il linguaggio (un semplice linguaggio testuale) usato dai client
web (p.e. i browser) e i server web per comunicare tra loro. Quando parliamo di cache HTTP, parliamo della parte
di tale linguaggio che consente a client e server di scambiarsi informazioni riguardo alla cache.
HTTP specifica quattro header di cache per la risposta di cui ci occupiamo:
196
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
• Cache-Control
• Expires
• ETag
• Last-Modified
L’header più importante e versatile è l’header Cache-Control, che in realtà è un insieme di varie informazioni
sulla cache.
Note: Ciascun header sarà spiegato in dettaglio nella sezione Scadenza e validazione HTTP.
L’header Cache-Control
L’header Cache-Control è unico, perché non contiene una, ma vari pezzi di informazione sulla possibilità di
una risposta di essere messa in cache. Ogni pezzo di informazione è separato da una virgola:
Cache-Control: private, max-age=0, must-revalidate
Cache-Control: max-age=3600, must-revalidate
Symfony fornisce un’astrazione sull’header Cache-Control, per rendere la sua creazione più gestibile:
$response = new Response();
// segna la risposta come pubblica o privata
$response->setPublic();
$response->setPrivate();
// imposta max age privata o condivisa
$response->setMaxAge(600);
$response->setSharedMaxAge(600);
// imposta una direttiva personalizzata Cache-Control
$response->headers->addCacheControlDirective(’must-revalidate’, true);
Risposte pubbliche e risposte private
Sia la gateway cache che la proxy cache sono considerate cache “condivise”, perché il contenuto della cache è
condiviso da più di un utente. Se una risposta specifica per un utente venisse per errore inserita in una cache
condivisa, potrebbe successivamente essere restituita a diversi altri utenti. Si immagini se delle informazioni su un
account venissero messe in cache e poi restituite a ogni utente successivo che richiede la sua pagina dell’account!
Per gestire questa situazione, ogni risposta può essere impostata a pubblica o privata:
• pubblica: Indica che la risposta può essere messa in cache sia da che private che da cache condivise;
• privata: Indica che tutta la risposta, o una sua parte, è per un singolo utente e quindi non deve essere messa
in una cache condivisa.
Symfony è conservativo e ha come predefinita una risposta privata. Per sfruttare le cache condivise (come il
reverse proxy di Symfony2), la risposta deve essere impostata esplicitamente come pubblica.
Metodi sicuri
La cache HTTP funziona solo per metodi HTTP “sicuri” (come GET e HEAD). Essere sicuri vuol dire che lo
stato dell’applicazione sul server non cambia mai quando si serve la richiesta (si può, certamente, memorizzare
un’informazione sui log, mettere in cache dati, eccetera). Questo ha due conseguenze molto ragionevoli:
2.1. Libro
197
Symfony2 documentation Documentation, Release 2
• Non si dovrebbe mai cambiare lo stato della propria applicazione quando si risponde a una richiesta GET
o HEAD. Anche se non si usa una gateway cache, la presenza di proxy cache vuol dire che ogni richiesta
GET o HEAD potrebbe arrivare al proprio server, ma potrebbe anche non arrivare.
• Non aspettarsi la cache dei metodi PUT, POST o DELETE. Questi metodi sono fatti per essere usati quando
si cambia lo stato della propria applicazione (p.e. si cancella un post di un blog). Metterli in cache impedirebbe ad alcune richieste di arrivare alla propria applicazione o di modificarla.
Regole e valori predefiniti della cache
HTTP 1.1 consente per impostazione predefinita la cache di tutto, a meno che non ci sia un header esplicito
Cache-Control. In pratica, la maggior parte delle cache non fanno nulla quando la richiesta ha un cookie, un
header di autorizzazione, usa un metodo non sicuro (PUT, POST, DELETE) o quando la risposta ha un codice di
stato di rinvio.
Symfony2 imposta automaticamente un header Cache-Control conservativo, quando nessun header è impostato dallo sviluppatore, seguendo queste regole:
• Se non è deinito nessun header di cache (Cache-Control, Expires, ETag o Last-Modified),
Cache-Control è impostato a no-cache, il che vuol dire che la risposta non sarà messa in cache;
• Se Cache-Control è vuoto (ma uno degli altri header di cache è presente), il suo valore è impostato a
private, must-revalidate;
• Se invece almeno una direttiva Cache-Control è impostata e nessuna direttiva public o private è
stata aggiunta esplicitamente, Symfony2 aggiunge automaticamente la direttiva private (tranne quando
è impostato s-maxage).
Scadenza e validazione HTTP
Le specifiche HTTP definiscono due modelli di cache:
• Con il modello a scadenza, si specifica semplicemente quanto a lungo una risposta debba essere considerata
“fresca”, includendo un header Cache-Control e/o uno Expires. Le cache che capiscono la scadenza
non faranno di nuovo la stessa richiesta finché la versione in cache non raggiunge la sua scadenza e diventa
“vecchia”.
• Quando le pagine sono molto dinamiche (cioè quando la loro rappresentazione varia spesso), il modello a
validazione è spesso necessario. Con questo modello, la cache memorizza la risposta, ma chiede al serve a
ogni richiesta se la risposta in cache sia ancora valida o meno. L’applicazione usa un identificatore univoco
per la risposta (l’header Etag) e/o un timestamp (come l’header Last-Modified) per verificare se la
pagina sia cambiata da quanto è stata messa in cache.
Lo scopo di entrambi i modelli è quello di non generare mai la stessa risposta due volte, appoggiandosi a una
cache per memorizzare e restituire risposte “fresche”.
Leggere le specifiche HTTP
Le specifiche HTTP definiscono un linguaggio semplice, ma potente, in cui client e server possono comunicare. Come sviluppatori web, il modello richiesta-risposta delle specifiche domina il nostro lavoro.
Sfortunatamente, il documento delle specifiche, la RFC 2616, può risultare di difficile lettura.
C’è uno sforzo in atto (HTTP Bis) per riscrivere la RFC 2616. Non descrive una nuova versione di HTTP, ma
per lo più chiarisce le specifiche HTTP originali. Anche l’organizzazione è migliore, essendo le specifiche
separate in sette parti; tutto ciò che riguarda la cache HTTP si trova in due parti dedicate (P4 - Richieste
condizionali e P6 - Cache: Browser e cache intermedie).
Come sviluppatori web, dovremmo leggere tutti le specifiche. Possiedono un chiarezza e una potenza, anche
dopo oltre dieci anni dalla creazione, inestimabili. Non ci si spaventi dalle apparenze delle specifiche, il
contenuto è molto più bello della copertina.
198
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
Scadenza
Il modello a scadenza è il più efficiente e il più chiaro dei due modelli di cache e andrebbe usato ogni volta che è
possibile. Quando una risposta è messa in cache con una scadenza, la cache memorizzerà la risposta e la restituirà
direttamente, senza arrivare all’applicazione, finché non scade.
Il modello a scadenza può essere implementato con l’uso di due header HTTP, quasi identici: Expires o
Cache-Control.
Scadenza con l’header Expires
Secondo le specifiche HTTP, “l’header Expires dà la data e l’ora dopo la quale la risposta è considerata vecchia”.
L’header Expires può essere impostato con il metodo setExpires() di Response. Accetta un’istanza di
DateTime come parametro:
$date = new DateTime();
$date->modify(’+600 seconds’);
$response->setExpires($date);
Il risultante header HTTP sarà simile a questo:
Expires: Thu, 01 Mar 2011 16:00:00 GMT
Note: Il metodo setExpires() converte automaticamente la data al fuso orario GMT, come richiesto dalle
specifiche.
Si noti che, nelle versioni di HTTP precedenti alla 1.1, non era richiesto al server di origine di inviare l’header
Date. Di conseguenza, la cache (p.e. il browser) potrebbe aver bisogno di appoggiarsi all’orologio locale per valuare l’header Expires, rendendo il calcolo del ciclo di vita vulnerabile a difformità di ore. L’header Expires
soffre di un’altra limitazione: le specifiche stabiliscono che “i server HTTP/1.1 non dovrebbero inviare header
Expires oltre un anno nel futuro.”
Scadenza con l’header Cache-Control
A causa dei limiti dell’header Expires, la maggior parte delle volte si userà al suo posto l’header
Cache-Control. Si ricordi che l’header Cache-Control è usato per specificare molte differenti direttive di
cache. Per la scadenza, ci sono due direttive, max-age e s-maxage. La prima è usata da tutte le cache, mentre
la seconda viene considerata solo dalla cache condivise:
// Imposta il numero di secondi dopo cui la risposta
// non dovrebbe più essere considerata fresca
$response->setMaxAge(600);
// Come sopra, ma solo per cache condivise
$response->setSharedMaxAge(600);
L’header Cache-Control avrebbe il seguente formato (potrebbe contenere direttive aggiuntive):
Cache-Control: max-age=600, s-maxage=600
Validazione
Quando una risorsa ha bisogno di essere aggiornata non appena i dati sottostanti subiscono una modifica, il modello a scadenza non raggiunge lo scopo. Con il modello a scadenza, all’applicazione non sarà chiesto di restituire
la risposta aggiornata, finché la cache non diventa vecchia.
2.1. Libro
199
Symfony2 documentation Documentation, Release 2
Il modello a validazione si occupa di questo problema. Con questo modello, la cache continua a memorizzare
risposte. La differenza è che, per ogni richiesta, la cache chiede all’applicazione se la risposta in cache è ancora
valida. Se la cache è ancora valida, la propria applicazione dovrebbe restituire un codice di stato 304 e nessun
contenuto. Questo dice alla cache che è va bene restituire la risposta in cache.
Con questo modello, principalmente si risparmia banda, perché la rappresentazione non è inviata due volte allo
stesso client (invece è inviata una risposta 304). Ma se si progetta attentamente la propria applicazione, si potrebbe
essere in grado di prendere il minimo dei dati necessari per inviare una risposta 304 e risparmiare anche CPU
(vedere sotto per un esempio di implementazione).
Tip: Il codice di stato 304 significa “non modificato”. È importante, perché questo codice di stato non contiene
il vero contenuto richiesto. La risposta è invece un semplice e leggero insieme di istruzioni che dicono alla cache
che dovrebbe usare la sua versione memorizzata.
Come per la scadenza, ci sono due diversi header HTTP che possono essere usati per implementare il modello a
validazione: ETag e Last-Modified.
Validazione con header ETag
L’header ETag è un header stringa (chiamato “tag entità”) che identifica univocamente una rappresentazione
della risorsa in questione. È interamente generato e impostato dalla propria applicazione, quindi si può dire, per
esempio, se la risorsa /about che è in cache sia aggiornata con ciò che la propria applicazione restituirebbe. Un
ETag è come un’impronta digitale ed è usato per confrontare rapidamente se due diverse versioni di una risorsa
siano equivalenti. Come le impronte digitali, ogni ETag deve essere univoco tra tutte le rappresentazioni della
stessa risorsa.
Vediamo una semplice implementazione, che genera l’ETag come un md5 del contenuto:
public function indexAction()
{
$response = $this->render(’MyBundle:Main:index.html.twig’);
$response->setETag(md5($response->getContent()));
$response->isNotModified($this->getRequest());
return $response;
}
Il metodo Response::isNotModified() confronta l’ETag inviato con la Request con quello impostato
nella Response. Se i due combaciano, il metodo imposta automaticamente il codice di stato della Response a
304.
Questo algoritmo è abbastanza semplice e molto generico, ma occorre creare l’intera Response prima di poter
calcolare l’ETag, che non è ottimale. In altre parole, fa risparmiare banda, ma non cicli di CPU.
Nella sezione Ottimizzare il codice con la validazione, mostreremo come si possa usare la validazione in modo
più intelligente, per determinare la validità di una cache senza dover fare tanto lavoro.
Tip:
Symfony2 supporta anche gli ETag deboli, passando true come secondo parametro del metodo
:method:‘Symfony\\Component\\HttpFoundation\\Response::setETag‘.
Validazione col metodo Last-Modified
L’header Last-Modified è la seconda forma di validazione. Secondo le specifiche HTTP, “l’header
Last-Modified indica la data e l’ora in cui il server di origine crede che la rappresentazione sia stata modificata l’ultima volta”. In altre parole, l’applicazione decide se il contenuto in cache sia stato modificato o meno, in
base al fatto se sia stato aggiornato o meno da quando la risposta è stata messa in cache.
200
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
Per esempio, si può usare la data di ultimo aggiornamento per tutti gli oggetti necessari per calcolare la rappresentazione della risorsa come valore dell’header Last-Modified:
public function showAction($articleSlug)
{
// ...
$articleDate = new \DateTime($article->getUpdatedAt());
$authorDate = new \DateTime($author->getUpdatedAt());
$date = $authorDate > $articleDate ? $authorDate : $articleDate;
$response->setLastModified($date);
$response->isNotModified($this->getRequest());
return $response;
}
Il metodo Response::isNotModified() confronta l’header If-Modified-Since inviato dalla richiesta con l’header Last-Modified impostato nella risposta. Se sono equivalenti, la Response sarà impostata
a un codice di stato 304.
Note: L’header della richiesta If-Modified-Since equivale all’header Last-Modified dell’ultima
risposta inviata al client per una determinata risorsa. In questo modo client e server comunicano l’uno con l’altro
e decidono se la risorsa sia stata aggiornata o meno da quando è stata messa in cache.
Ottimizzare il codice con la validazione
Lo scopo principale di ogni strategia di cache è alleggerire il carico dell’applicazione. In altre parole, meno la
propria applicazione fa per restituire una risposta 304, meglio è. Il metodo Response::isNotModified()
fa esattamente questo, esponendo uno schema semplice ed efficiente:
public function showAction($articleSlug)
{
// Prende l’informazione minima per calcolare
// l’ETag o o il valore di Last-Modified
// (in base alla Request, i dati sono recuperati da un
// database o da una memoria chiave-valore, per esempio)
$article = // ...
// crea una Response con un ETag e/o un header Last-Modified
$response = new Response();
$response->setETag($article->computeETag());
$response->setLastModified($article->getPublishedAt());
// Verifica che la Response non sia modificata per la Request data
if ($response->isNotModified($this->getRequest())) {
// restituisce subito la Response 304
return $response;
} else {
// qui fa più lavoro, come recuperare altri dati
$comments = // ...
// o rende un template con la $response già iniziata
return $this->render(
’MyBundle:MyController:article.html.twig’,
array(’article’ => $article, ’comments’ => $comments),
$response
);
}
2.1. Libro
201
Symfony2 documentation Documentation, Release 2
}
Quando la Response non è stata modificata, isNotModified() imposta automaticamente il codice di stato
della risposta a 304, rimuove il contenuto e rimuove alcuni header che no devono essere presenti in una risposta
304 (vedere :method:‘Symfony\\Component\\HttpFoundation\\Response::setNotModified‘).
Variare la risposta
Finora abbiamo ipotizzato che ogni URI avesse esattamente una singola rappresentazione della risorsa interessata.
Per impostazione predefinita, la cache HTTP usa l’URI della risorsa come chiave. Se due persone richiedono lo
stesso URI di una risorsa che si può mettere in cache, la seconda persona riceverà la versione in cache.
A volte questo non basta e diverse versioni dello stesso URI hanno bisogno di stare in cache in base a uno più
header di richiesta. Per esempio, se si comprimono le pagine per i client che supportano per la compressione,
ogni URI ha due rappresentazioni: una per i client col supporto e l’altra per i client senza supporto. Questo viene
determinato dal valore dell’header di richiesta Accept-Encoding.
In questo caso, occorre mettere in cache sia una versione compressa che una non compressa della risposta di un
particolare URI e restituirle in base al valore Accept-Encoding della richiesta. Lo si può fare usando l’header
di risposta Vary, che è una lista separata da virgole dei diversi header i cui valori causano rappresentazioni diverse
della risorsa richiesta:
Vary: Accept-Encoding, User-Agent
Tip: Questo particolare header Vary fa mettere in cache versioni diverse di ogni risorsa in base all’URI, al valore
di Accept-Encoding e all’header di richiesta User-Agent.
L’oggetto Response offre un’interfaccia pulita per la gestione dell’header Vary:
// imposta un header Vary
$response->setVary(’Accept-Encoding’);
// imposta diversi header Vary
$response->setVary(array(’Accept-Encoding’, ’User-Agent’));
Il metodo setVary() accetta un nome di header o un array di nomi di header per i quali la risposta varia.
Scadenza e validazione
Si può ovviamente usare sia la validazione che la scadenza nella stessa Response. Poiché la scadenza vince sulla
validazione, si può beneficiare dei vantaggi di entrambe. In altre parole, usando sia la scadenza che la validazione,
si può istruire la cache per servire il contenuto in cache, controllando ogni tanto (la scadenza) per verificare che il
contenuto sia ancora valido.
Altri metodi della risposta
La classe Response fornisce molti altri metodi per la cache. Ecco alcuni dei più utili:
// Segna la risposta come vecchia
$response->expire();
// Forza la risposta a restituire un 304 senza contenuti
$response->setNotModified();
Inoltre, la maggior parte degli header HTTP relativi alla cache può essere impostata tramite il singolo metodo
setCache():
202
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
// Imposta le opzioni della cache in una sola chiamata
$response->setCache(array(
’etag’
=> $etag,
’last_modified’ => $date,
’max_age’
=> 10,
’s_maxage’
=> 10,
’public’
=> true,
// ’private’
=> true,
));
Usare Edge Side Includes
Le gateway cache sono un grande modo per rendere il proprio sito più prestante. Ma hanno una limitazione:
possono mettere in cache solo pagine intere. Se non si possono mettere in cache pagine intere o se le pagine
hanno più parti dinamiche, non vanno bene. Fortunatamente, Symfony2 fornisce una soluzione a questi casi,
basata su una tecnologia chiamata ESI, o Edge Side Includes. Akamaï ha scritto le specifiche quasi dieci anni fa,
consentendo a determinate parti di una pagina di avere differenti strategie di cache rispetto alla pagina principale.
Le specifiche ESI descrivono dei tag che si possono inserire nelle proprie pagine, per comunicare col gateway
cache. L’unico tag implementato in Symfony2 è include, poiché è l’unico utile nel contesto di Akamaï:
<html>
<body>
Del contenuto
<!-- Inserisce qui il contenuto di un’altra pagina -->
<esi:include src="http://..." />
Dell’altro contenuto
</body>
</html>
Note: Si noti nell’esempio che ogni tag ESI ha un URL pienamente qualificato. Un tag ESI rappresenta un
frammento di pagina che può essere recuperato tramite l’URL fornito.
Quando gestisce una richiesta, il gateway cache recupera l’intera pagina dalla sua cache oppure la richiede
dall’applicazione di backend. Se la risposta contiene uno o più tag ESI, questi vengono processati nello stesso
modo. In altre parole, la gateway cache o recupera il frammento della pagina inclusa dalla sua cache oppure
richiede il frammento di pagina all’applicazione di backend. Quando tutti i tag ESI sono stati risolti, il gateway
cache li fonde nella pagina principale e invia il contenuto finale al client.
Tutto questo avviene in modo trasparente a livello di gateway cache (quindi fuori dalla propria applicazione).
Come vedremo, se si scegli di avvalersi dei tag ESI, Symfony2 rende quasi senza sforzo il processo di inclusione.
Usare ESI in Symfony2
Per usare ESI, assicurarsi prima di tutto di abilitarlo nella configurazione dell’applicazione:
• YAML
# app/config/config.yml
framework:
# ...
esi: { enabled: true }
• XML
<!-- app/config/config.xml -->
<framework:config ...>
<!-- ... -->
2.1. Libro
203
Symfony2 documentation Documentation, Release 2
<framework:esi enabled="true" />
</framework:config>
• PHP
// app/config/config.php
$container->loadFromExtension(’framework’, array(
// ...
’esi’
=> array(’enabled’ => true),
));
Supponiamo ora di avere una pagina relativamente statica, tranne per un elenco di news in fondo al contenuto.
Con ESI, si può mettere in cache l’elenco di news indipendentemente dal resto della pagina.
public function indexAction()
{
$response = $this->render(’MyBundle:MyController:index.html.twig’);
$response->setSharedMaxAge(600);
return $response;
}
In questo esempio, abbiamo dato alla cache della pagina intera un tempo di vita di dieci minuti. Successivamente,
includiamo l’elenco di news nel template, includendolo in un’azione. Possiamo farlo grazie all’helper render
(vedere Inserire controllori per maggiori dettagli).
Poiché il contenuto incluso proviene da un’altra pagina (o da un altro controllore), Symfony2 usa l’helper render
per configurare i tag ESI:
• Twig
{% render ’...:news’ with {}, {’standalone’: true} %}
• PHP
<?php echo $view[’actions’]->render(’...:news’, array(), array(’standalone’ => true)) ?>
Impostando standalone a true, si dice a Symfony2 che l’azione andrebbe resa come tag ESI. Ci si potrebbe
chiedere perché usare un helper invece di usare direttamente il tag ESI. Il motivo è che l’uso di un helper consente
all’applicazione di funzionare anche se non c’è nessun gateway cache installato. Vediamo come funziona.
Quando standalone è false (il valore predefinito), Symfony2 fonde il contenuto della pagina in quella principale, prima di inviare la risposta al client. Ma quando standalone è true e se Symfony2 individua che sta
parlando a una gateway cache che supporti ESI, genera un tag include di ESI. Se invece non c’è una gateway cache
con supporto a ESI, Symfony2 fonde direttamente il contenuto della pagina inclusa dentro la pagina principale,
come se standalone fosse stato impostato a false.
Note: Symfony2 individua se una gateway cache supporta ESI tramite un’altra specifica di Akamaï, che è
supportata nativamente dal reverse proxy di Symfony2.
L’azione inclusa ora può specificare le sue regole di cache, del tutto indipendentemente dalla pagina principale.
public function newsAction()
{
// ...
$response->setSharedMaxAge(60);
}
Con ESI, la cache dell’intera pagina sarà valida per 600 secondi, mentre il componente delle news avrà una cache
che dura per soli 60 secondi.
Un requisito di ESI, tuttavia, è che l’azione inclusa sia accessibile tramite un URL, in modo che il gateway cache
possa recuperarla indipendentemente dal resto della pagina. Ovviamente, un URL non può essere accessibile se
204
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
non ha una rotta che punti a esso. Symfony2 si occupa di questo tramite una rotta e un controllore generici. Per
poter far funzionare i tag include di ESI, occorre definire la rotta _internal:
• YAML
# app/config/routing.yml
_internal:
resource: "@FrameworkBundle/Resources/config/routing/internal.xml"
prefix:
/_internal
• XML
<!-- app/config/routing.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/r
<import resource="@FrameworkBundle/Resources/config/routing/internal.xml" prefix="/_inter
</routes>
• PHP
// app/config/routing.php
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;
$collection->addCollection($loader->import(’@FrameworkBundle/Resources/config/routing/interna
return $collection;
Tip: Poiché questa rotta consente l’accesso a tutte le azioni tramite URL, si potrebbe volerla proteggere usando il
firewall di Symfony2 (consentendo l’accesso al range di IP del proprio reverse proxy). Vedere la sezione Sicurezza
tramite IP del Capitolo sulla sicurezza per maggiori informazioni.
Un grosso vantaggio di questa strategia di cache è che si può rendere la propria applicazione tanto dinamica quanto
necessario e, allo stesso tempo, mantenere gli accessi al minimo.
Note: Una volta iniziato a usare ESI, si ricordi di usare sempre la direttiva s-maxage al posto di max-age.
Poiché il browser riceve la risorsa aggregata, non ha visibilità sui sotto-componenti, quindi obbedirà alla direttiva
max-age e metterà in cache l’intera pagina. E questo non è quello che vogliamo.
L’helper render supporta due utili opzioni:
• alt: usato come attributo alt nel tag ESI, che consente di specificare un URL alternativo da usare, nel
caso in cui src non venga trovato;
• ignore_errors: se impostato a true, un attributo onerror sarà aggiunto a ESI con il valore di
continue, a indicare che, in caso di fallimento, la gateway cache semplicemente rimuoverà il tag ESI
senza produrre errori.
Invalidazione della cache
“Ci sono solo due cose difficili in informatica: invalidazione della cache e nomi delle cose.” Phil
Karlton
Non si dovrebbe mai aver bisogno di invalidare i dati in cache, perché dell’invalidazione si occupano già nativamente i modelli di cache HTTP. Se si usa la validazione, non si avrà mai bisogno di invalidare nulla, per
definizione; se si usa la scadenza e si ha l’esigenza di invalidare una risorsa, vuol dire che si è impostata una data
di scadenza troppo in là nel futuro.
2.1. Libro
205
Symfony2 documentation Documentation, Release 2
Note:
Essendo l’invalidazione un argomento specifico di ogni reverse proxy, se non ci si preoccupa
dell’invalidazione, si può cambiare reverse proxy senza cambiare alcuna parte del codice della propria applicazione.
In realtà, ogni reverse proxy fornisce dei modi per pulire i dati in cache, ma andrebbero evitati, per quanto possibile. Il modo più standard è pulire la cache per un dato URL richiedendolo con il metodo speciale HTTP PURGE.
Ecco come si può configurare il reverse proxy di Symfony2 per supportare il metodo HTTP PURGE:
// app/AppCache.php
class AppCache extends Cache
{
protected function invalidate(Request $request)
{
if (’PURGE’ !== $request->getMethod()) {
return parent::invalidate($request);
}
$response = new Response();
if (!$this->getStore()->purge($request->getUri())) {
$response->setStatusCode(404, ’Not purged’);
} else {
$response->setStatusCode(200, ’Purged’);
}
return $response;
}
}
Caution: Occorre proteggere in qualche modo il metodo HTTP PURGE, per evitare che qualcuno pulisca
casualmente i dati in cache.
Riepilogo
Symfony2 è stato progettato per seguire le regole sperimentate della strada: HTTP. La cache non fa eccezione.
Padroneggiare il sistema della cache di Symfony2 vuol dire acquisire familiarità con i modelli di cache HTTP e
usarli in modo efficace. Vuol dire anche che, invece di basarsi solo su documentazione ed esempi di Symfony2, si
ha accesso al mondo della conoscenza relativo alla cache HTTP e a gateway cache come Varnish.
Imparare di più con le ricette
• Come usare Varnish per accelerare il proprio sito
2.1.14 Traduzioni
Il termine “internazionalizzazione” si riferisce al processo di astrazione delle stringhe e altri pezzi specifici
dell’applicazione che variano in base al locale, in uno strato dove possono essere tradotti e convertiti in base
alle impostazioni internazionali dell’utente (ad esempio lingua e paese). Per il testo, questo significa che ognuno
viene avvolto con una funzione capace di tradurre il testo (o “messaggio”) nella lingua dell’utente:
// il testo verrà *sempre* stampato in inglese
echo ’Hello World’;
// il testo può essere tradotto nella lingua dell’utente finale o per impostazione predefinita in
echo $translator->trans(’Hello World’);
206
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
Note: Il termine locale si riferisce all’incirca al linguaggio dell’utente e al paese. Può essere qualsiasi stringa che
l’applicazione utilizza poi per gestire le traduzioni e altre differenze di formati (ad esempio il formato di valuta).
Si consiglia di utilizzare il codice di lingua ISO639-1, un carattere di sottolineatura (_), poi il codice di paese
ISO3166 (per esempio fr_FR per francese / Francia).
In questo capitolo, si imparerà a preparare un’applicazione per supportare più locale e poi a creare le traduzioni
per più locale. Nel complesso, il processo ha diverse fasi comuni:
1. Abilitare e configurare il componente Translation di Symfony;
2. Astrarre le stringhe (i. “messaggi”) avvolgendoli nelle chiamate al Translator;
3. Creare risorse di traduzione per ogni lingua supportata che traducano tutti i messaggio dell’applicazione;
4. Determinare, impostare e gestire le impostazioni locali dell’utente per la richiesta e, facoltativamente,
sull’intera sessione.
Configurazione
Le traduzioni sono gestire da un servizio Translator, che utilizza i locale dell’utente per cercare e restituire i
messaggi tradotti. Prima di utilizzarlo, abilitare il Translator nella configurazione:
• YAML
# app/config/config.yml
framework:
translator: { fallback: en }
• XML
<!-- app/config/config.xml -->
<framework:config>
<framework:translator fallback="en" />
</framework:config>
• PHP
// app/config/config.php
$container->loadFromExtension(’framework’, array(
’translator’ => array(’fallback’ => ’en’),
));
L’opzione fallback definisce il locale da utilizzare quando una traduzione non esiste nel locale dell’utente.
Tip: Quando una traduzione non esiste per un locale, il traduttore prima prova a trovare la traduzione per la
lingua (ad esempio fr se il locale è fr_FR). Se non c’è, cerca una traduzione utilizzando il locale di ripiego.
Il locale usato nelle traduzioni è quello memorizzato nella richiesta. Tipicamente, è impostato tramite un attributo
_locale in una rotta (vedere Il locale e gli URL).
Traduzione di base
La
traduzione
del
testo
è
fatta
attraverso
il
servizio
translator
(Symfony\Component\Translation\Translator).
Per tradurre un blocco di testo (chiamato
messaggio), usare il metodo :method:‘Symfony\\Component\\Translation\\Translator::trans‘. Supponiamo,
ad esempio, che stiamo traducendo un semplice messaggio all’interno del controllore:
public function indexAction()
{
$t = $this->get(’translator’)->trans(’Symfony2 is great’);
2.1. Libro
207
Symfony2 documentation Documentation, Release 2
return new Response($t);
}
Quando questo codice viene eseguito, Symfony2 tenterà di tradurre il messaggio “Symfony2 is great” basandosi
sul locale dell’utente. Perché questo funzioni, bisogna dire a Symfony2 come tradurre il messaggio tramite una
“risorsa di traduzione”, che è una raccolta di traduzioni dei messaggi per un dato locale. Questo “dizionario” delle
traduzioni può essere creato in diversi formati, ma XLIFF è il formato raccomandato:
• XML
<!-- messages.fr.xliff -->
<?xml version="1.0"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" datatype="plaintext" original="file.ext">
<body>
<trans-unit id="1">
<source>Symfony2 is great</source>
<target>J’aime Symfony2</target>
</trans-unit>
</body>
</file>
</xliff>
• PHP
// messages.fr.php
return array(
’Symfony2 is great’ => ’J\’aime Symfony2’,
);
• YAML
# messages.fr.yml
Symfony2 is great: J’aime Symfony2
Ora, se la lingua del locale dell’utente è il francese (per esempio fr_FR o fr_BE), il messaggio sarà tradotto in
J’aime Symfony2.
Il processo di traduzione
Per tradurre il messaggio, Symfony2 utilizza un semplice processo:
• Viene determinato il locale dell’utente corrente, che è memorizzato nella richiesta (o nella sessione, come
_locale);
• Un catalogo di messaggi tradotti viene caricato dalle risorse di traduzione definite per il locale (ad es.
fr_FR). Vengono anche caricati i messaggi dal locale predefinito e aggiunti al catalogo, se non esistono
già. Il risultato finale è un grande “dizionario” di traduzioni. Vedere i Cataloghi di messaggi per maggiori
dettagli;
• Se il messaggio si trova nel catalogo, viene restituita la traduzione. Se no, il traduttore restituisce il messaggio originale.
Quando si usa il metodo trans(), Symfony2 cerca la stringa esatta all’interno del catalogo dei messaggi e la
restituisce (se esiste).
Segnaposto per i messaggi
A volte, un messaggio contiene una variabile deve essere tradotta:
208
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
public function indexAction($name)
{
$t = $this->get(’translator’)->trans(’Hello ’.$name);
return new Response($t);
}
Tuttavia, la creazione di una traduzione per questa stringa è impossibile, poiché il traduttore proverà a cercare
il messaggio esatto, includendo le parti con le variabili (per esempio “Ciao Ryan” o “Ciao Fabien”). Invece di
scrivere una traduzione per ogni possibile iterazione della variabile $name, si può sostituire la variabile con un
“segnaposto”:
public function indexAction($name)
{
$t = $this->get(’translator’)->trans(’Hello %name%’, array(’%name%’ => $name));
new Response($t);
}
Symfony2 cercherà ora una traduzione del messaggio raw (Hello %name%) e poi sostituirà i segnaposto con i
loro valori. La creazione di una traduzione è fatta esattamente come prima:
• XML
<!-- messages.fr.xliff -->
<?xml version="1.0"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" datatype="plaintext" original="file.ext">
<body>
<trans-unit id="1">
<source>Hello %name%</source>
<target>Bonjour %name%</target>
</trans-unit>
</body>
</file>
</xliff>
• PHP
// messages.fr.php
return array(
’Hello %name%’ => ’Bonjour %name%’,
);
• YAML
# messages.fr.yml
’Hello %name%’: Hello %name%
Note: Il segnaposto può assumere qualsiasi forma visto che il messaggio è ricostruito utilizzando la funzione
strtr di PHP. Tuttavia, la notazione %var% è richiesta quando si traduce utilizzando i template Twig e in generale
è una convenzione che è consigliato seguire.
Come si è visto, la creazione di una traduzione è un processo in due fasi:
1. Astrarre il messaggio che si deve tradurre, processandolo tramite il Translator.
2. Creare una traduzione per il messaggio in ogni locale che si desideri supportare.
Il secondo passo si esegue creando cataloghi di messaggi, che definiscono le traduzioni per ogni diverso locale.
2.1. Libro
209
Symfony2 documentation Documentation, Release 2
Cataloghi di messaggi
Quando un messaggio è tradotto, Symfony2 compila un catalogo di messaggi per il locale dell’utente e guarda in
esso per cercare la traduzione di un messaggio. Un catalogo di messaggi è come un dizionario di traduzioni per
uno specifico locale. Ad esempio, il catalogo per il locale fr_FR potrebbe contenere la seguente traduzione:
Symfony2 is Great => J’aime Symfony2
È compito dello sviluppatore (o traduttore) di una applicazione internazionalizzata creare queste traduzioni. Le
traduzioni sono memorizzate sul filesystem e vengono trovate da Symfony grazie ad alcune convenzioni.
Tip: Ogni volta che si crea una nuova risorsa di traduzione (o si installa un pacchetto che include una risorsa di
traduzione), assicurarsi di cancellare la cache in modo che Symfony possa scoprire la nuova risorsa di traduzione:
php app/console cache:clear
Sedi per le traduzioni e convenzioni sui nomi
Symfony2 cerca i file dei messaggi (ad esempio le traduzioni) in due sedi:
• Per i messaggi trovati in un bundle, i corrispondenti file con i messaggi dovrebbero trovarsi nella cartella
Resources/translations/ del bundle;
• Per sovrascrivere eventuali traduzioni del bundle, posizionare i file con i messaggi nella cartella
app/Resources/translations.
È importante anche il nome del file con le traduzioni, perché Symfony2 utilizza una convenzione per determinare i dettagli sulle traduzioni. Ogni file con i messaggi deve essere nominato secondo il seguente schema:
dominio.locale.caricatore:
• dominio: Un modo opzionale per organizzare i messaggi in gruppi (ad esempio admin, navigation o
il predefinito messages) - vedere Uso dei domini per i messaggi;
• locale: Il locale per cui sono state scritte le traduzioni (ad esempio en_GB, en, ecc.);
• caricatore: Come Symfony2 dovrebbe caricare e analizzare il file (ad esempio xliff, php o yml).
Il caricatore può essere il nome di un qualunque caricatore registrato. Per impostazione predefinita, Symfony
fornisce i seguenti caricatori:
• xliff: file XLIFF;
• php: file PHP;
• yml: file YAML.
La scelta di quali caricatori utilizzare è interamente a carico dello sviluppatore ed è una questione di gusti.
Note:
È anche possibile memorizzare le traduzioni in una base dati o in qualsiasi
altro
mezzo,
fornendo
una
classe
personalizzata
che
implementa
l’interfaccia
Symfony\Component\Translation\Loader\LoaderInterface.
Vedere Caricatori per
le traduzioni personalizzati di seguito per imparare a registrare caricatori personalizzati.
Creazione delle traduzioni
La creazione di file di traduzione è una parte importante della “localizzazione” (spesso abbreviata in L10n). Ogni
file è costituito da una serie di coppie id-traduzione per il dato dominio e locale. L’id è l’identificativo di una
traduzione individuale e può essere il messaggio nel locale principale (ad es. “Symfony is great”) dell’applicazione
o un identificatore univoci (ad es. “symfony2.great” - vedere la barra laterale di seguito):
• XML
210
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
<!-- src/Acme/DemoBundle/Resources/translations/messages.fr.xliff -->
<?xml version="1.0"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" datatype="plaintext" original="file.ext">
<body>
<trans-unit id="1">
<source>Symfony2 is great</source>
<target>J’aime Symfony2</target>
</trans-unit>
<trans-unit id="2">
<source>symfony2.great</source>
<target>J’aime Symfony2</target>
</trans-unit>
</body>
</file>
</xliff>
• PHP
// src/Acme/DemoBundle/Resources/translations/messages.fr.php
return array(
’Symfony2 is great’ => ’J\’aime Symfony2’,
’symfony2.great’
=> ’J\’aime Symfony2’,
);
• YAML
# src/Acme/DemoBundle/Resources/translations/messages.fr.yml
Symfony2 is great: J’aime Symfony2
symfony2.great:
J’aime Symfony2
Symfony2 troverà questi file e li utilizzerà quando dovrà tradurre “Symfony2 is great” o “symfony2.great” in un
locale di lingua francese (ad es. fr_FR o fr_BE).
2.1. Libro
211
Symfony2 documentation Documentation, Release 2
Utilizzare messaggi reali o parole chiave
Questo esempio mostra le due diverse filosofie nella creazione di messaggi che dovranno essere tradotti:
$t = $translator->trans(’Symfony2 is great’);
$t = $translator->trans(’symfony2.great’);
Nel primo metodo, i messaggi vengono scritti nella lingua del locale predefinito (in inglese in questo caso).
Questo messaggio viene quindi utilizzato come “id” durante la creazione delle traduzioni.
Nel secondo metodo, i messaggi sono in realtà “parole chiave” che trasmettono l’idea del messaggio.Il
messaggio chiave è quindi utilizzato come “id” per eventuali traduzioni. In questo caso, deve essere fatta
anche la traduzione per il locale predefinito (ad esempio per tradurre symfony2.great in Symfony2
is great).
Il secondo metodo è utile perché non sarà necessario cambiare la chiave del messaggio in ogni file di
traduzione se decidiamo che il messaggio debba essere modificato in “Symfony2 is really great” nel locale
predefinito.
La scelta del metodo da utilizzare è personale, ma il formato “chiave” è spesso raccomandato.
Inoltre, i formati di file php e yaml supportano gli id nidificati, per evitare di ripetersi se si utilizzano parole
chiave al posto di testo reale per gli id:
• YAML
symfony2:
is:
great: Symfony2 is great
amazing: Symfony2 is amazing
has:
bundles: Symfony2 has bundles
user:
login: Login
• PHP
return array(
’symfony2’ => array(
’is’ => array(
’great’ => ’Symfony2 is great’,
’amazing’ => ’Symfony2 is amazing’,
),
’has’ => array(
’bundles’ => ’Symfony2 has bundles’,
),
),
’user’ => array(
’login’ => ’Login’,
),
);
I livelli multipli vengono appiattiti in singole coppie id/traduzione tramite l’aggiunta di un punto (.) tra ogni
livello, quindi gli esempi di cui sopra sono equivalenti al seguente:
• YAML
symfony2.is.great: Symfony2 is great
symfony2.is.amazing: Symfony2 is amazing
symfony2.has.bundles: Symfony2 has bundles
user.login: Login
• PHP
return array(
’symfony2.is.great’ => ’Symfony2 is great’,
’symfony2.is.amazing’ => ’Symfony2 is amazing’,
’symfony2.has.bundles’ => ’Symfony2 has bundles’,
’user.login’ => ’Login’,
);
212
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
Uso dei domini per i messaggi
Come abbiamo visto, i file dei messaggi sono organizzati nei diversi locale che vanno a tradurre. I file dei messaggi
possono anche essere organizzati in “domini”. Quando si creano i file dei messaggi, il dominio è la prima parte del
nome del file. Il dominio predefinito è messages. Per esempio, supponiamo che, per organizzarle al meglio, le
traduzioni siano state divise in tre diversi domini: messages, admin e navigation. La traduzione francese
avrebbe i seguenti file per i messaggi:
• messages.fr.xliff
• admin.fr.xliff
• navigation.fr.xliff
Quando si traducono stringhe che non sono nel dominio predefinito (messages), è necessario specificare il
dominio come terzo parametro di trans():
$this->get(’translator’)->trans(’Symfony2 is great’, array(), ’admin’);
Symfony2 cercherà ora il messaggio del locale dell’utente nel dominio admin.
Gestione del locale dell’utente
Il locale dell’utente corrente è memorizzato nella richiesta ed è accessibile tramite l’oggetto request:
// accesso all’oggetto requesta in un controllore
$request = $this->getRequest();
$locale = $request->getLocale();
$request->setLocale(’en_US’);
È anche possibile memorizzare il locale in sessione, invece che in ogni richiesta. Se lo si fa, ogni richiesta
successiva avrà lo stesso locale.
$this->get(’session’)->set(’_locale’, ’en_US’);
Vedere la sezione .. _book-translation-locale-url: sotto, sull’impostazione del locale tramite rotte.
Fallback e locale predefinito
Se il locale non è stato impostato in modo esplicito nella sessione, sarà utilizzato dal Translator il parametro
di configurazione fallback_locale. Il valore predefinito del parametro è en (vedere Configurazione).
In alternativa, è possibile garantire che un locale è impostato sulla sessione dell’utente definendo un
default_locale per il servizio di sessione:
• YAML
# app/config/config.yml
framework:
default_locale: en
• XML
<!-- app/config/config.xml -->
<framework:config>
<framework:default-locale>en</framework:default-locale>
</framework:config>
• PHP
2.1. Libro
213
Symfony2 documentation Documentation, Release 2
// app/config/config.php
$container->loadFromExtension(’framework’, array(
’default_locale’ => ’en’,
));
New in version 2.1.
Il locale e gli URL
Dal momento che il locale dell’utente è memorizzato nella sessione, si può essere tentati di utilizzare
lo stesso URL per visualizzare una risorsa in più lingue in base al locale dell’utente. Per esempio,
http://www.example.com/contact può mostrare contenuti in inglese per un utente e in francese per
un altro. Purtroppo questo viola una fondamentale regola del web: un particolare URL deve restituire la stessa
risorsa indipendentemente dall’utente. Inoltre, quale versione del contenuto dovrebbe essere indicizzata dai motori
di ricerca?
Una politica migliore è quella di includere il locale nell’URL. Questo è completamente dal sistema delle rotte
utilizzando il parametro speciale _locale:
• YAML
contact:
pattern:
/{_locale}/contact
defaults: { _controller: AcmeDemoBundle:Contact:index, _locale: en }
requirements:
_locale: en|fr|de
• XML
<route id="contact" pattern="/{_locale}/contact">
<default key="_controller">AcmeDemoBundle:Contact:index</default>
<default key="_locale">en</default>
<requirement key="_locale">en|fr|de</requirement>
</route>
• PHP
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;
$collection = new RouteCollection();
$collection->add(’contact’, new Route(’/{_locale}/contact’, array(
’_controller’ => ’AcmeDemoBundle:Contact:index’,
’_locale’
=> ’en’,
), array(
’_locale’
=> ’en|fr|de’
)));
return $collection;
Quando si utilizza il parametro speciale _locale in una rotta, il locale corrispondente verrà automaticamente
impostato sulla sessione dell’utente. In altre parole, se un utente visita l’URI /fr/contact, il locale fr viene
impostato automaticamente come locale per la sessione dell’utente.
È ora possibile utilizzare il locale dell’utente per creare rotte ad altre pagine tradotte nell’applicazione.
Pluralizzazione
La pluralizzazione dei messaggi è un argomento un po’ difficile, perché le regole possono essere complesse. Per
esempio, questa è la rappresentazione matematica delle regole di pluralizzazione russe:
214
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
(($number % 10 == 1) && ($number % 100 != 11)) ? 0 : ((($number % 10 >= 2) && ($number % 10 <= 4)
Come si può vedere, in russo si possono avere tre diverse forme plurali, ciascuna dato un indice di 0, 1 o 2. Per
ciascuna forma il plurale è diverso e quindi anche la traduzione è diversa.
Quando una traduzione ha forme diverse a causa della pluralizzazione, è possibile fornire tutte le forme come una
stringa separata da un pipe (|):
’There is one apple|There are %count% apples’
Per tradurre i messaggi pluralizzati, utilizzare il metodo :method:‘Symfony\\Component\\Translation\\Translator::transChoice‘:
$t = $this->get(’translator’)->transChoice(
’There is one apple|There are %count% apples’,
10,
array(’%count%’ => 10)
);
Il secondo parametro (10 in questo esempio), è il numero di oggetti che vengono descritti ed è usato per determinare quale traduzione è da usare e anche per popolare il segnaposto %count%.
In base al numero dato, il traduttore sceglie la giusta forma plurale. In inglese, la maggior parte delle parole
hanno una forma singolare quando c’è esattamente un oggetto e una forma plurale per tutti gli altri numeri (0, 2,
3...). Quindi, se count è 1, il traduttore utilizzerà la prima stringa (There is one apple) come traduzione.
Altrimenti userà There are %count% apples.
Ecco la traduzione francese:
’Il y a %count% pomme|Il y a %count% pommes’
Anche se la stringa è simile (è fatta di due sotto-stringhe separate da un carattere pipe), le regole francesi sono
differenti: la prima forma (non plurale) viene utilizzata quando count è 0 o 1. Così, il traduttore utilizzerà
automaticamente la prima stringa (Il y a %count% pomme) quando count è 0 o 1.
Ogni locale ha una propria serie di regole, con alcuni che hanno ben sei differenti forme plurali con regole complesse che descrivono quali numeri mappano le forme plurali. Le regole sono abbastanza semplici per l’inglese e il
francese, ma per il russo, si potrebbe aver bisogno di un aiuto per sapere quali regole corrispondono alle stringhe.
Per aiutare i traduttori, è possibile opzionalmente “etichettare” ogni stringa:
’one: There is one apple|some: There are %count% apples’
’none_or_one: Il y a %count% pomme|some: Il y a %count% pommes’
Le etichette sono solo aiuti per i traduttori e non influenzano la logica usata per determinare quale plurale è da
usare. Le etichette possono essere una qualunque stringa che termina con due punti(:). Le etichette inoltre non
hanno bisogno di essere le stesse nel messaggio originale e in quello tradotto.
Intervallo di pluralizzazione esplicito
Il modo più semplice per pluralizzare un messaggio è quello di lasciare che Symfony2 utilizzi la sua logica interna
per scegliere quale stringa utilizzare sulla base di un dato numero. A volte c’è bisogno di più controllo o si vuole
una traduzione diversa per casi specifici (per 0, o quando il conteggio è negativo, ad esempio). In tali casi, è
possibile utilizzare espliciti intervalli matematici:
’{0} There is no apples|{1} There is one apple|]1,19] There are %count% apples|[20,Inf] There are
Gli intervalli seguono la notazione ISO 31-11. La suddetta stringa specifica quattro diversi intervalli: esattamente
0, esattamente 1, 2-19 e 20 e superiori.
È inoltre possibile combinare le regole matematiche e le regole standard. In questo caso, se il numero non corrisponde ad un intervallo specifico, le regole standard hanno effetto dopo aver rimosso le regole esplicite:
2.1. Libro
215
Symfony2 documentation Documentation, Release 2
’{0} There is no apples|[20,Inf] There are many apples|There is one apple|a_few: There are %count%
Ad esempio, per 1 mela, verrà usata la regola standard C’è una mela. Per 2-19 mele, verrà utilizzata la
seconda regola standard Ci sono %count% mele.
Symfony\Component\Translation\Interval può rappresentare un insieme finito di numeri:
{1,2,3,4}
O numeri tra due numeri:
[1, +Inf[
]-1,2[
Il delimitatore di sinistra può essere [ (incluso) o ] (escluso). Il delimitatore di destra può essere [ (escluso) o ]
(incluso). Oltre ai numeri, si può usare -Inf e +Inf per l’infinito.
Traduzioni nei template
La maggior parte delle volte, la traduzione avviene nei template. Symfony2 fornisce un supporto nativo sia per i
template Twig che per i template PHP.
Template Twig
Symfony2 fornisce dei tag specifici per Twig (trans e transchoice) per aiutare nella traduzione di messaggi
con blocchi statici di testo:
{% trans %}Hello %name%{% endtrans %}
{% transchoice count %}
{0} There is no apples|{1} There is one apple|]1,Inf] There are %count% apples
{% endtranschoice %}
Il tag transchoice ottiene automaticamente la variabile %count% dal contesto corrente e la passa al traduttore.
Questo meccanismo funziona solo quando si utilizza un segnaposto che segue lo schema %var%.
Tip: Se in una stringa è necessario usare il carattere percentuale (%), escapizzarlo raddoppiandolo: {% trans
%}Percent: %percent%%%{% endtrans %}
È inoltre possibile specificare il dominio del messaggio e passare alcune variabili aggiuntive:
{% trans with {’%name%’: ’Fabien’} from "app" %}Hello %name%{% endtrans %}
{% trans with {’%name%’: ’Fabien’} from "app" into "fr" %}Hello %name%{% endtrans %}
{% transchoice count with {’%name%’: ’Fabien’} from "app" %}
{0} There is no apples|{1} There is one apple|]1,Inf] There are %count% apples
{% endtranschoice %}
I filtri trans e transchoice possono essere usati per tradurre variabili di testo ed espressioni complesse:
{{ message|trans }}
{{ message|transchoice(5) }}
{{ message|trans({’%name%’: ’Fabien’}, "app") }}
{{ message|transchoice(5, {’%name%’: ’Fabien’}, ’app’) }}
216
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
Tip: Utilizzare i tag di traduzione o i filtri ha lo stesso effetto, ma con una sottile differenza: l’escape automatico
dell’output è applicato solo alle variabili tradotte utilizzando un filtro. In altre parole, se è necessario essere sicuri
che la variabile tradotta non venga escapizzata, è necessario applicare il filtro raw dopo il filtro di traduzione:
{# il testo tradotto tra i tag non è mai sotto escape #}
{% trans %}
<h3>foo</h3>
{% endtrans %}
{% set message = ’<h3>foo</h3>’ %}
{# una variabile tradotta tramite filtro è sotto escape per impostazione predefinita #}
{{ message|trans|raw }}
{# le stringhe statiche non sono mai sotto escape #}
{{ ’<h3>foo</h3>’|trans }}
Template PHP
Il servizio di traduzione è accessibile nei template PHP attraverso l’helper translator:
<?php echo $view[’translator’]->trans(’Symfony2 is great’) ?>
<?php echo $view[’translator’]->transChoice(
’{0} There is no apples|{1} There is one apple|]1,Inf[ There are %count% apples’,
10,
array(’%count%’ => 10)
) ?>
Forzare il locale della traduzione
Quando si traduce un messaggio, Symfony2 utilizza il lodale della sessione utente o il locale fallback se
necessario. È anche possibile specificare manualmente il locale da usare per la traduzione:
$this->get(’translator’)->trans(
’Symfony2 is great’,
array(),
’messages’,
’fr_FR’,
);
$this->get(’translator’)->trans(
’{0} There are no apples|{1} There is one apple|]1,Inf[ There are %count% apples’,
10,
array(’%count%’ => 10),
’messages’,
’fr_FR’,
);
Tradurre contenuti da un database
La traduzione del contenuto di un database dovrebbero essere gestite da Doctrine attraverso l’Estensione Translatable. Per maggiori informazioni, vedere la documentazione di questa libreria.
2.1. Libro
217
Symfony2 documentation Documentation, Release 2
Riassunto
Con il componente Translation di Symfony2, la creazione e l’internazionalizzazione di applicazioni non è più un
processo doloroso e si riduce solo a pochi semplici passi:
• Astrarre
i
messaggi
dell’applicazione
avvolgendoli
utilizzando
i
metodi
:method:‘Symfony\\Component\\Translation\\Translator::trans‘ o :method:‘Symfony\\Component\\Translation\\Trans
• Tradurre ogni messaggio in più locale creando dei file con i messaggi per la traduzione. Symfony2 scopre
ed elabora ogni file perché i suoi nomi seguono una specifica convenzione;
• Gestire il locale dell’utente, che è memorizzato nella richiesta, ma può anche essere memorizzato nella
sessione.
2.1.15 Contenitore di servizi
Una moderna applicazione PHP è piena di oggetti. Un oggetto può facilitare la consegna dei messaggi di posta
elettronica, mentre un altro può consentire di salvare le informazioni in un database. Nell’applicazione, è possibile
creare un oggetto che gestisce l’inventario dei prodotti, o un altro oggetto che elabora i dati da un’API di terze
parti. Il punto è che una moderna applicazione fa molte cose ed è organizzata in molti oggetti che gestiscono ogni
attività.
In questo capitolo si parlerà di un oggetto speciale PHP presente in Symfony2 che aiuta a istanziare, organizzare
e recuperare i tanti oggetti della propria applicazione. Questo oggetto, chiamato contenitore di servizi, permetterà
di standardizzare e centralizzare il modo in cui sono costruiti gli oggetti nell’applicazione. Il contenitore rende
la vita più facile, è super veloce ed evidenzia un’architettura che promuove codice riusabile e disaccoppiato. E
poiché tutte le classi del nucleo di Symfony2 utilizzano il contenitore, si apprenderà come estendere, configurare e
usare qualsiasi oggetto in Symfony2. In gran parte, il contenitore dei servizi è il più grande contributore riguardo
la velocità e l’estensibilità di Symfony2.
Infine, la configurazione e l’utilizzo del contenitore di servizi è semplice. Entro la fine di questo capitolo, si sarà
in grado di creare i propri oggetti attraverso il contenitore e personalizzare gli oggetti da un bundle di terze parti.
Si inizierà a scrivere codice che è più riutilizzabile, testabile e disaccoppiato, semplicemente perché il contenitore
di servizi consente di scrivere facilmente del buon codice.
Cos’è un servizio?
In parole povere, un servizio è un qualsiasi oggetto PHP che esegue una sorta di compito “globale”. È un nome
volutamente generico utilizzato in informatica per descrivere un oggetto che è stato creato per uno scopo specifico
(ad esempio spedire email). Ogni servizio è utilizzato in tutta l’applicazione ogni volta che si ha bisogno delle
funzionalità specifiche che fornisce. Non bisogna fare nulla di speciale per creare un servizio: è sufficiente scrivere
una classe PHP con del codice che realizza un compito specifico. Congratulazioni, si è appena creato un servizio!
Note: Come regola generale, un oggetto PHP è u nservizio se viene utilizzato a livello globale nell’applicazione.
Un singolo servizio Mailer è usato globalmente per inviare messaggi email mentre i molti oggetti Message
che spedisce non sono servizi. Allo stesso modo, un oggetto Product non è un servizio, ma un oggetto che
persiste oggetti Product su un database è un servizio.
Qual’è il discorso allora? Il vantaggio dei “servizi” è che si comincia a pensare di semparare ogni “pezzo di
funzionalità” dell’applicazione in una serie di servizi. Dal momento che ogni servizio fa solo un lavoro, si può
facilmente accedere a ogni servizio e utilizzare le sue funzionalità ovunque ce ne sia bisogno. Ogni servizio
può anche essere più facilmente testato e configurato essendo separato dalle altre funzionalità dell’applicazione.
Questa idea si chiama architettura orientata ai servizi e non riguarda solo Symfony2 o il PHP. Strutturare la propria
applicazione con una serie di indipendenti classi di servizi è una nota best-practice della programmazione a oggetti.
Queste conoscenze sono fondamentali per essere un buon sviluppatore in quasi tutti i linguaggi.
218
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
Cos’è un contenitore di servizi?
Un contenitore di servizi (o contenitore di dependency injection) è semplicemente un oggetto PHP che gestisce
l’istanza di servizi (cioè gli oggetti). Per esempio, supponiamo di avere una semplice classe PHP che spedisce
messaggi email. Senza un contenitore di servizi, bisogna creare manualmente l’oggetto ogni volta che se ne ha
bisogno:
use Acme\HelloBundle\Mailer;
$mailer = new Mailer(’sendmail’);
$mailer->send(’[email protected]’, ... );
Questo è abbastanza facile. La classe immaginaria Mailer permette di configurare il metodo utilizzato per
inviare i messaggi email (per esempio sendmail, smtp, ecc). Ma cosa succederebbe se volessimo utilizzare il
servizio mailer da qualche altra parte? Certamente non si vorrebbe ripetere la configurazione del mailer ogni volta
che si ha bisogno dell’oggetto Mailer. Cosa succederebbe se avessimo bisogno di cambiare il transport
da sendmail a smtp in ogni punto dell’applicazione? Avremo bisogno di cercare ogni posto in cui si crea un
servizio Mailer e cambiarlo.
Creare/Configurare servizi nel contenitore
Una soluzione migliore è quella di lasciare che il contenitore di servizi crei l’oggetto Mailer per noi. Affinché
questo funzioni, bisogna insegnare al contenitore come creare il servizio Mailer. Questo viene fatto tramite la
configurazione, che può essere specificata in YAML, XML o PHP:
• YAML
# app/config/config.yml
services:
my_mailer:
class:
Acme\HelloBundle\Mailer
arguments:
[sendmail]
• XML
<!-- app/config/config.xml -->
<services>
<service id="my_mailer" class="Acme\HelloBundle\Mailer">
<argument>sendmail</argument>
</service>
</services>
• PHP
// app/config/config.php
use Symfony\Component\DependencyInjection\Definition;
$container->setDefinition(’my_mailer’, new Definition(
’Acme\HelloBundle\Mailer’,
array(’sendmail’)
));
Note: Durante l’inizializzazione di Symfony2, viene costruito il contenitore di servizi utilizzando la configurazione dell’applicazione (per impostazione predefinita app/config/config.yml). Il file esatto che
viene caricato è indicato dal metodo AppKernel::registerContainerConfiguration(), che carica un file di configurazione specifico per l’ambiente (ad esempio config_dev.yml per l’ambiente dev o
config_prod.yml per prod).
Un’istanza dell’oggetto Acme\HelloBundle\Mailer è ora disponibile tramite il contenitore di servizio. Il
contenitore è disponibile in qualsiasi normale controllore di Symfony2 in cui è possibile accedere ai servizi del
contenitore attraverso il metodo scorciatoia get():
2.1. Libro
219
Symfony2 documentation Documentation, Release 2
class HelloController extends Controller
{
// ...
public function sendEmailAction()
{
// ...
$mailer = $this->get(’my_mailer’);
$mailer->send(’[email protected]’, ... );
}
}
Quando si chiede il servizio my_mailer del contenitore, il contenitore costruisce l’oggetto e lo restituisce.
Questo è un altro grande vantaggio che si ha utilizzando il contenitore di servizi. Questo significa che un servizio
non è mai costruito fino a che non ce n’è bisogno. Se si definisce un servizio e non lo si usa mai su una richiesta,
il servizio non verrà mai creato. Ciò consente di risparmiare memoria e aumentare la velocità dell’applicazione.
Questo significa anche che c’è un calo di prestazioni basso o inesistente quando si definiscono molti servizi. I
servizi che non vengono mai utilizzati non sono mai costruite.
Come bonus aggiuntivo, il servizio Mailer è creato una sola volta e ogni volta che si chiede per il servizio viene
restituita la stessa istanza. Questo è quasi sempre il comportamento di cui si ha bisogno (è più flessibile e potente),
ma si imparerà più avanti come configurare un servizio che ha istanze multiple.
I parametri del servizio
La creazione di nuovi servizi (cioè oggetti) attraverso il contenitore è abbastanza semplice. Con i parametri si
possono definire servizi più organizzati e flessibili:
• YAML
# app/config/config.yml
parameters:
my_mailer.class:
my_mailer.transport:
services:
my_mailer:
class:
arguments:
Acme\HelloBundle\Mailer
sendmail
%my_mailer.class%
[%my_mailer.transport%]
• XML
<!-- app/config/config.xml -->
<parameters>
<parameter key="my_mailer.class">Acme\HelloBundle\Mailer</parameter>
<parameter key="my_mailer.transport">sendmail</parameter>
</parameters>
<services>
<service id="my_mailer" class="%my_mailer.class%">
<argument>%my_mailer.transport%</argument>
</service>
</services>
• PHP
// app/config/config.php
use Symfony\Component\DependencyInjection\Definition;
$container->setParameter(’my_mailer.class’, ’Acme\HelloBundle\Mailer’);
$container->setParameter(’my_mailer.transport’, ’sendmail’);
$container->setDefinition(’my_mailer’, new Definition(
220
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
’%my_mailer.class%’,
array(’%my_mailer.transport%’)
));
Il risultato finale è esattamente lo stesso di prima, la differenza è solo nel come è stato definito il servizio. Circondando le stringhe my_mailer.class e my_mailer.transport con il segno di percentuale (%), il contenitore sa di dover cercare per parametri con questi nomi. Quando il contenitore è costruito, cerca il valore di ogni
parametro e lo usa nella definizione del servizio.
Lo scopo dei parametri è quello di inserire informazioni dei servizi. Naturalmente non c’è nulla di sbagliato a
definire il servizio senza l’uso di parametri. I parametri, tuttavia, hanno diversi vantaggi:
• separazione e organizzazione di tutte le “opzioni” del servizio sotto un’unica chiave parameters;
• i valori dei parametri possono essere utilizzati in molteplici definizioni di servizi;
• la creazione di un servizio in un bundle (lo mostreremo a breve), usando i parametri consente al servizio di
essere facilmente personalizzabile nell’applicazione..
La scelta di usare o non usare i parametri è personale. I bundle di alta qualità di terze parti utilizzeranno sempre
perché rendono i servizi memorizzati nel contenitore più configurabili. Per i servizi della propria applicazione,
tuttavia, potrebbe non essere necessaria la flessibilità dei parametri.
Parametri array
I parametri non devono necessariamente essere semplici stringhe, possono anche essere array. Per il formato
YAML, occorre usare l’attributo type=”collection” per tutti i parametri che sono array.
• YAML
# app/config/config.yml
parameters:
my_mailer.gateways:
- mail1
- mail2
- mail3
my_multilang.language_fallback:
en:
- en
- fr
fr:
- fr
- en
• XML
<!-- app/config/config.xml -->
<parameters>
<parameter key="my_mailer.gateways" type="collection">
<parameter>mail1</parameter>
<parameter>mail2</parameter>
<parameter>mail3</parameter>
</parameter>
<parameter key="my_multilang.language_fallback" type="collection">
<parameter key="en" type="collection">
<parameter>en</parameter>
<parameter>fr</parameter>
</parameter>
<parameter key="fr" type="collection">
<parameter>fr</parameter>
<parameter>en</parameter>
</parameter>
</parameter>
</parameters>
2.1. Libro
221
Symfony2 documentation Documentation, Release 2
• PHP
// app/config/config.php
use Symfony\Component\DependencyInjection\Definition;
$container->setParameter(’my_mailer.gateways’, array(’mail1’, ’mail2’, ’mail3’));
$container->setParameter(’my_multilang.language_fallback’,
array(’en’ => array(’en’, ’fr’),
’fr’ => array(’fr’, ’en’),
));
Importare altre risorse di configurazione del contenitore
Tip: In questa sezione, si farà riferimento ai file di configurazione del servizio come risorse. Questo per sottolineare il fatto che, mentre la maggior parte delle risorse di configurazione saranno file (ad esempio YAML, XML,
PHP), Symfony2 è così flessibile che la configurazione potrebbe essere caricata da qualunque parte (per esempio
in una base dati o tramite un servizio web esterno).
Il contenitore dei servizi è costruito utilizzando una singola risorsa di configurazione (per impostazione predefinita
app/config/config.yml). Tutte le altre configurazioni di servizi (comprese le configurazioni del nucleo
di Symfony2 e dei bundle di terze parti) devono essere importate da dentro questo file in un modo o nell’altro.
Questo dà una assoluta flessibilità sui servizi dell’applicazione.
La configurazione esterna di servizi può essere importata in due modi differenti. Il primo, è quello che verrà
utilizzato nelle applicazioni: la direttiva imports. Nella sezione seguente, si introdurrà il secondo metodo, che
è il metodo più flessibile e privilegiato per importare la configurazione di servizi in bundle di terze parti.
Importare la configurazione con imports
Finora, si è messo la definizione di contenitore del servizio my_mailer direttamente nel file di configurazione
dell’applicazione (ad esempio app/config/config.yml). Naturalmente, poiché la classe Mailer stessa
vive all’interno di AcmeHelloBundle, ha più senso mettere la definizione my_mailer del contenitore dentro
il bundle stesso.
In primo luogo, spostare la definizione my_mailer del contenitore, in un nuovo file risorse del contenitore in
AcmeHelloBundle. Se le cartelle Resources o Resources/config non esistono, crearle.
• YAML
# src/Acme/HelloBundle/Resources/config/services.yml
parameters:
my_mailer.class:
Acme\HelloBundle\Mailer
my_mailer.transport: sendmail
services:
my_mailer:
class:
arguments:
%my_mailer.class%
[%my_mailer.transport%]
• XML
<!-- src/Acme/HelloBundle/Resources/config/services.xml -->
<parameters>
<parameter key="my_mailer.class">Acme\HelloBundle\Mailer</parameter>
<parameter key="my_mailer.transport">sendmail</parameter>
</parameters>
<services>
<service id="my_mailer" class="%my_mailer.class%">
<argument>%my_mailer.transport%</argument>
222
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
</service>
</services>
• PHP
// src/Acme/HelloBundle/Resources/config/services.php
use Symfony\Component\DependencyInjection\Definition;
$container->setParameter(’my_mailer.class’, ’Acme\HelloBundle\Mailer’);
$container->setParameter(’my_mailer.transport’, ’sendmail’);
$container->setDefinition(’my_mailer’, new Definition(
’%my_mailer.class%’,
array(’%my_mailer.transport%’)
));
Non è cambiata la definizione, solo la sua posizione. Naturalmente il servizio contenitore non conosce il nuovo
file di risorse. Fortunatamente, si può facilmente importare il file risorse utilizzando la chiave imports nella
configurazione dell’applicazione.
• YAML
# app/config/config.yml
imports:
- { resource: @AcmeHelloBundle/Resources/config/services.yml }
• XML
<!-- app/config/config.xml -->
<imports>
<import resource="@AcmeHelloBundle/Resources/config/services.xml"/>
</imports>
• PHP
// app/config/config.php
$this->import(’@AcmeHelloBundle/Resources/config/services.php’);
La direttiva imports consente all’applicazione di includere risorse di configurazione per il contenitore di servizi
da qualsiasi altro posto (in genere da bundle). La locazione resource, per i file, è il percorso assoluto al file
risorse. La speciale sintassi @AcmeHello risolve il percorso della cartella del bundle AcmeHelloBundle.
Questo aiuta a specificare il percorso alla risorsa senza preoccuparsi in seguito, se si sposta AcmeHelloBundle
in una cartella diversa.
Importare la configurazione attraverso estensioni del contenitore
Quando si sviluppa in Symfony2, si usa spesso la direttiva imports per importare la configurazione del contenitore dai bundle che sono stati creati appositamente per l’applicazione. Le configurazioni dei contenitori di bundle
di terze parti, includendo i servizi del nucleo di Symfony2, di solito sono caricati utilizzando un altro metodo che
è più flessibile e facile da configurare nell’applicazione.
Ecco come funziona. Internamente, ogni bundle definisce i propri servizi in modo molto simile a come si è visto
finora. Un bundle utilizza uno o più file di configurazione delle risorse (di solito XML) per specificare i parametri
e i servizi del bundle. Tuttavia, invece di importare ciascuna di queste risorse direttamente dalla configurazione
dell’applicazione utilizzando la direttiva imports, si può semplicemente richiamare una estensione del contenitore di servizi all’interno del bundle che fa il lavoro per noi. Un’estensione del contenitore dei servizi è una classe
PHP creata dall’autore del bundle con lo scopo di realizzare due cose:
• importare tutte le risorse del contenitore dei servizi necessarie per configurare i servizi per il bundle;
• fornire una semplice configurazione semantica in modo che il bundle possa essere configurato senza interagire con i parametri “piatti” della configurazione del contenitore dei servizi del bundle.
2.1. Libro
223
Symfony2 documentation Documentation, Release 2
In altre parole, una estensione dei contenitore dei servizi configura i servizi per il bundle per voi. E, come si vedrà
tra poco, l’estensione fornisce una interfaccia sensibile e ad alto livello per configurare il bundle.
Si prenda il FrameworkBundle, il bundle del nucleo del framework Symfony2, come esempio. La presenza del
seguente codice nella configurazione dell’applicazione invoca l’estensione del contenitore dei servizi all’interno
del FrameworkBundle:
• YAML
# app/config/config.yml
framework:
secret:
xxxxxxxxxx
charset:
UTF-8
form:
true
csrf_protection: true
router:
{ resource: "%kernel.root_dir%/config/routing.yml" }
# ...
• XML
<!-- app/config/config.xml -->
<framework:config charset="UTF-8" secret="xxxxxxxxxx">
<framework:form />
<framework:csrf-protection />
<framework:router resource="%kernel.root_dir%/config/routing.xml" />
<!-- ... -->
</framework>
• PHP
// app/config/config.php
$container->loadFromExtension(’framework’, array(
’secret’
=> ’xxxxxxxxxx’,
’charset’
=> ’UTF-8’,
’form’
=> array(),
’csrf-protection’ => array(),
’router’
=> array(’resource’ => ’%kernel.root_dir%/config/routing.php’),
// ...
));
Quando viene analizzata la configurazione, il contenitore cerca un’estensione che sia in grado di gestire la direttiva
di configurazione framework. L’estensione in questione, che vive nel FrameworkBundle, viene invocata
e la configurazione del servizio per il FrameworkBundle viene caricata. Se si rimuove del tutto la chiave
framework dal file di configurazione dell’applicazione, i servizi del nucleo di Symfony2 non vengono caricati.
Il punto è che è tutto sotto controllo: il framework Symfony2 non contiene nessuna magia e non esegue nessuna
azione su cui non si abbia il controllo.
Naturalmente è possibile fare molto di più della semplice “attivazione” dell’estensione del contenitore dei servizi
del FrameworkBundle. Ogni estensione consente facilmente di personalizzare il bundle, senza preoccuparsi
di come i servizi interni siano definiti.
In questo caso, l’estensione consente di personalizzare la configurazione di charset, error_handler,
csrf_protection, router e di molte altre. Internamente, il FrameworkBundle usa le opzioni qui
specificate per definire e configurare i servizi a esso specifici. Il bundle si occupa di creare tutte i necessari
parameters e services per il contenitore dei servizi, pur consentendo di personalizzare facilmente gran
parte della configurazione. Come bonus aggiuntivo, la maggior parte delle estensioni dei contenitori di servizi
sono anche sufficientemente intelligenti da eseguire la validazione - notificando le opzioni mancanti o con un tipo
di dato sbagliato.
Durante l’installazione o la configurazione di un bundle, consultare la documentazione del bundle per per vedere
come devono essere installati e configurati i servizi per il bundle. Le opzioni disponibili per i bundle del nucleo si
possono trovare all’interno della Guida di riferimento.
224
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
Note: Nativamente, il contenitore dei servizi riconosce solo le direttive parameters, services e imports.
Ogni altra direttiva è gestita dall’estensione del contenitore dei servizi.
Referenziare (iniettare) servizi
Finora, il servizio my_mailer è semplice: accetta un solo parametro nel suo costruttore, che è facilmente configurabile. Come si vedrà, la potenza reale del contenitore viene fuori quando è necessario creare un servizio che
dipende da uno o più altri servizi nel contenitore.
Cominciamo con un esempio. Supponiamo di avere un nuovo servizio, NewsletterManager, che aiuta a
gestire la preparazione e la spedizione di un messaggio email a un insieme di indirizzi. Naturalmente il servizio
my_mailer è già capace a inviare messaggi email, quindi verrà usato all’interno di NewsletterManager
per gestire la spedizione effettiva dei messaggi. Questa classe potrebbe essere qualcosa del genere:
namespace Acme\HelloBundle\Newsletter;
use Acme\HelloBundle\Mailer;
class NewsletterManager
{
protected $mailer;
public function __construct(Mailer $mailer)
{
$this->mailer = $mailer;
}
// ...
}
Senza utilizzare il contenitore di servizi, si può creare abbastanza facilmente un nuovo NewsletterManager
dentro a un controllore:
public function sendNewsletterAction()
{
$mailer = $this->get(’my_mailer’);
$newsletter = new Acme\HelloBundle\Newsletter\NewsletterManager($mailer);
// ...
}
Questo approccio va bene, ma cosa succede se più avanti si decide che la classe NewsletterManager ha
bisogno di un secondo o terzo parametro nel costruttore? Che cosa succede se si decide di rifattorizzare il
codice e rinominare la classe? In entrambi i casi si avrà bisogno di cercare ogni posto in cui viene istanziata
NewsletterManager e fare le modifiche. Naturalmente, il contenitore dei servizi fornisce una soluzione
molto migliore:
• YAML
# src/Acme/HelloBundle/Resources/config/services.yml
parameters:
# ...
newsletter_manager.class: Acme\HelloBundle\Newsletter\NewsletterManager
services:
my_mailer:
# ...
newsletter_manager:
class:
%newsletter_manager.class%
arguments: [@my_mailer]
• XML
2.1. Libro
225
Symfony2 documentation Documentation, Release 2
<!-- src/Acme/HelloBundle/Resources/config/services.xml -->
<parameters>
<!-- ... -->
<parameter key="newsletter_manager.class">Acme\HelloBundle\Newsletter\NewsletterManager</
</parameters>
<services>
<service id="my_mailer" ... >
<!-- ... -->
</service>
<service id="newsletter_manager" class="%newsletter_manager.class%">
<argument type="service" id="my_mailer"/>
</service>
</services>
• PHP
// src/Acme/HelloBundle/Resources/config/services.php
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
// ...
$container->setParameter(’newsletter_manager.class’, ’Acme\HelloBundle\Newsletter\NewsletterM
$container->setDefinition(’my_mailer’, ... );
$container->setDefinition(’newsletter_manager’, new Definition(
’%newsletter_manager.class%’,
array(new Reference(’my_mailer’))
));
In YAML, la sintassi speciale @my_mailer dice al contenitore di cercare un servizio chiamato my_mailer e
di passare l’oggetto nel costruttore di NewsletterManager. In questo caso, tuttavia, il servizio specificato
my_mailer deve esistere. In caso contrario, verrà lanciata un’eccezione. È possibile contrassegnare le proprie
dipendenze come opzionali (sarà discusso nella prossima sezione).
L’utilizzo di riferimenti è uno strumento molto potente che permette di creare classi di servizi indipendenti
con dipendenze ben definite. In questo esempio, il servizio newsletter_manager ha bisogno del servizio
my_mailer per poter funzionare. Quando si definisce questa dipendenza nel contenitore dei servizi, il contenitore si prende cura di tutto il lavoro di istanziare degli oggetti.
Dipendenze opzionali: iniettare i setter
Iniettare dipendenze nel costruttore è un eccellente modo per essere sicuri che la dipendenza sia disponibile per
l’uso. Se per una classe si hanno dipendenze opzionali, allora l“‘iniezione dei setter” può essere una scelta
migliore. Significa iniettare la dipendenza utilizzando una chiamata di metodo al posto del costruttore. La classe
sarà simile a questa:
namespace Acme\HelloBundle\Newsletter;
use Acme\HelloBundle\Mailer;
class NewsletterManager
{
protected $mailer;
public function setMailer(Mailer $mailer)
{
$this->mailer = $mailer;
}
// ...
}
226
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
Iniettare la dipendenza con il metodo setter, necessita solo di un cambio di sintassi:
• YAML
# src/Acme/HelloBundle/Resources/config/services.yml
parameters:
# ...
newsletter_manager.class: Acme\HelloBundle\Newsletter\NewsletterManager
services:
my_mailer:
# ...
newsletter_manager:
class:
%newsletter_manager.class%
calls:
- [ setMailer, [ @my_mailer ] ]
• XML
<!-- src/Acme/HelloBundle/Resources/config/services.xml -->
<parameters>
<!-- ... -->
<parameter key="newsletter_manager.class">Acme\HelloBundle\Newsletter\NewsletterManager</
</parameters>
<services>
<service id="my_mailer" ... >
<!-- ... -->
</service>
<service id="newsletter_manager" class="%newsletter_manager.class%">
<call method="setMailer">
<argument type="service" id="my_mailer" />
</call>
</service>
</services>
• PHP
// src/Acme/HelloBundle/Resources/config/services.php
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
// ...
$container->setParameter(’newsletter_manager.class’, ’Acme\HelloBundle\Newsletter\NewsletterM
$container->setDefinition(’my_mailer’, ... );
$container->setDefinition(’newsletter_manager’, new Definition(
’%newsletter_manager.class%’
))->addMethodCall(’setMailer’, array(
new Reference(’my_mailer’)
));
Note: Gli approcci presentati in questa sezione sono chiamati “iniezione del costruttore” e “iniezione del setter”.
Il contenitore dei servizi di Symfony2 supporta anche “iniezione di proprietà”.
Rendere opzionali i riferimenti
A volte, uno dei servizi può avere una dipendenza opzionale, il che significa che la dipendenza non è richiesta
al fine di fare funzionare correttamente il servizio. Nell’esempio precedente, il servizio my_mailer deve esistere, altrimenti verrà lanciata un’eccezione. Modificando la definizione del servizio newsletter_manager,
è possibile rendere questo riferimento opzionale. Il contenitore inietterà se esiste e in caso contrario non farà nulla:
2.1. Libro
227
Symfony2 documentation Documentation, Release 2
• YAML
# src/Acme/HelloBundle/Resources/config/services.yml
parameters:
# ...
services:
newsletter_manager:
class:
%newsletter_manager.class%
arguments: [@?my_mailer]
• XML
<!-- src/Acme/HelloBundle/Resources/config/services.xml -->
<services>
<service id="my_mailer" ... >
<!-- ... -->
</service>
<service id="newsletter_manager" class="%newsletter_manager.class%">
<argument type="service" id="my_mailer" on-invalid="ignore" />
</service>
</services>
• PHP
// src/Acme/HelloBundle/Resources/config/services.php
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\ContainerInterface;
// ...
$container->setParameter(’newsletter_manager.class’, ’Acme\HelloBundle\Newsletter\NewsletterM
$container->setDefinition(’my_mailer’, ... );
$container->setDefinition(’newsletter_manager’, new Definition(
’%newsletter_manager.class%’,
array(new Reference(’my_mailer’, ContainerInterface::IGNORE_ON_INVALID_REFERENCE))
));
In YAML, la speciale sintassi @? dice al contenitore dei servizi che la dipendenza è opzionale. Naturalmente,
NewsletterManager deve essere scritto per consentire una dipendenza opzionale:
public function __construct(Mailer $mailer = null)
{
// ...
}
Servizi del nucleo di Symfony e di terze parti
Dal momento che Symfony2 e tutti i bundle di terze parti configurano e recuperano i loro servizi attraverso il
contenitore, si possono accedere facilmente o addirittura usarli nei propri servizi. Per mantenere le cose semplici, Symfony2 per impostazione predefinita non richiede che i controllori siano definiti come servizi. Inoltre
Symfony2 inietta l’intero contenitore dei servizi nel controllore. Ad esempio, per gestire la memorizzazione delle
informazioni su una sessione utente, Symfony2 fornisce un servizio session, a cui è possibile accedere dentro
a un controllore standard, come segue:
public function indexAction($bar)
{
$session = $this->get(’session’);
$session->set(’foo’, $bar);
228
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
// ...
}
In Symfony2, si potranno sempre utilizzare i servizi forniti dal nucleo di Symfony o dai bundle di terze parti
per eseguire funzionalità come la resa di template (templating), l’invio di email (mailer), o l’accesso a
informazioni sulla richiesta (request).
Questo possiamo considerarlo come un ulteriore passo in avanti con l’utilizzo di questi servizi all’interno di servizi
che si è creato per l’applicazione. Andiamo a modificare NewsletterManager per usare il reale servizio
mailer di Symfony2 (al posto del finto my_mailer). Si andrà anche a far passare il servizio con il motore dei
template al NewsletterManager in modo che possa generare il contenuto dell’email tramite un template:
namespace Acme\HelloBundle\Newsletter;
use Symfony\Component\Templating\EngineInterface;
class NewsletterManager
{
protected $mailer;
protected $templating;
public function __construct(\Swift_Mailer $mailer, EngineInterface $templating)
{
$this->mailer = $mailer;
$this->templating = $templating;
}
// ...
}
La configurazione del contenitore dei servizi è semplice:
• YAML
services:
newsletter_manager:
class:
%newsletter_manager.class%
arguments: [@mailer, @templating]
• XML
<service id="newsletter_manager" class="%newsletter_manager.class%">
<argument type="service" id="mailer"/>
<argument type="service" id="templating"/>
</service>
• PHP
$container->setDefinition(’newsletter_manager’, new Definition(
’%newsletter_manager.class%’,
array(
new Reference(’mailer’),
new Reference(’templating’)
)
));
Il servizio newsletter_manager ora ha accesso ai servizi del nucleo mailer e templating. Questo è
un modo comune per creare servizi specifici all’applicazione, in grado di sfruttare la potenza di numerosi servizi
presenti nel framework.
Tip: Assicurarsi che la voce swiftmailer appaia nella configurazione dell’applicazione. Come è stato accennato in Importare la configurazione attraverso estensioni del contenitore, la chiave swiftmailer invoca
l’estensione del servizio da SwiftmailerBundle, il quale registra il servizio mailer.
2.1. Libro
229
Symfony2 documentation Documentation, Release 2
Configurazioni avanzate del contenitore
Come si è visto, definire servizi all’interno del contenitore è semplice, in genere si ha bisogno della chiave di
configurazione service e di alcuni parametri. Tuttavia, il contenitore ha diversi altri strumenti disponibili che
aiutano ad aggiungere servizi per funzionalità specifiche, creare servizi più complessi ed eseguire operazioni dopo
che il contenitore è stato costruito.
Contrassegnare i servizi come pubblici / privati
Quando si definiscono i servizi, solitamente si vuole essere in grado di accedere a queste definizioni all’interno del
codice dell’applicazione. Questi servizi sono chiamati public. Per esempio, il servizio doctrine registrato
con il contenitore quando si utilizza DoctrineBundle è un servizio pubblico dal momento che è possibile accedervi
tramite:
$doctrine = $container->get(’doctrine’);
Tuttavia, ci sono casi d’uso in cui non si vuole che un servizio sia pubblico. Questo capita quando un servizio è
definito solamente perché potrebbe essere usato come parametro per un altro servizio.
Note:
Se si utilizza un servizio privato come parametro per più di un altro servizio, questo si tradurrà
nell’utilizzo di due istanze diverse perché l’istanza di un servizio privato è fatta in linea (ad esempio new
PrivateFooBar()).
In poche parole: Un servizio dovrà essere privato quando non si desidera accedervi direttamente dal codice.
Ecco un esempio:
• YAML
services:
foo:
class: Acme\HelloBundle\Foo
public: false
• XML
<service id="foo" class="Acme\HelloBundle\Foo" public="false" />
• PHP
$definition = new Definition(’Acme\HelloBundle\Foo’);
$definition->setPublic(false);
$container->setDefinition(’foo’, $definition);
Ora che il servizio è privato, non si può chiamare:
$container->get(’foo’);
Tuttavia, se un servizio è stato contrassegnato come privato, si può ancora farne l’alias (vedere sotto) per accedere
a questo servizio (attraverso l’alias).
Note: I servizi per impostazione predefinita sono pubblici.
230
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
Alias
Quando nella propria applicazione si utilizzano bundle del nucleo o bundle di terze parti, si possono utilizzare
scorciatoie per accedere ad alcuni servizi. Si può farlo mettendo un alias e, inoltre, si può mettere l’alias anche su
servizi non pubblici.
• YAML
services:
foo:
class: Acme\HelloBundle\Foo
bar:
alias: foo
• XML
<service id="foo" class="Acme\HelloBundle\Foo"/>
<service id="bar" alias="foo" />
• PHP
$definition = new Definition(’Acme\HelloBundle\Foo’);
$container->setDefinition(’foo’, $definition);
$containerBuilder->setAlias(’bar’, ’foo’);
Questo significa che quando si utilizza il contenitore direttamente, è possibile accedere al servizio foo richiedendo
il servizio bar in questo modo:
$container->get(’bar’); // Restituirà il servizio foo
Richiedere file
Ci potrebbero essere casi d’uso in cui è necessario includere un altro file subito prima che il servizio stesso venga
caricato. Per farlo, è possibile utilizzare la direttiva file.
• YAML
services:
foo:
class: Acme\HelloBundle\Foo\Bar
file: %kernel.root_dir%/src/path/to/file/foo.php
• XML
<service id="foo" class="Acme\HelloBundle\Foo\Bar">
<file>%kernel.root_dir%/src/path/to/file/foo.php</file>
</service>
• PHP
$definition = new Definition(’Acme\HelloBundle\Foo\Bar’);
$definition->setFile(’%kernel.root_dir%/src/path/to/file/foo.php’);
$container->setDefinition(’foo’, $definition);
Notare che symfony chiamerà internamente la funzione PHP require_once il che significa che il file verrà incluso
una sola volta per ogni richiesta.
I tag (tags)
Allo stesso modo con cui il post di un blog su web viene etichettato con cose tipo “Symfony” o “PHP”, i servizi
configurati nel contenitore possono anche loro essere etichettati. Nel contenitore dei servizi, un tag implica che si
2.1. Libro
231
Symfony2 documentation Documentation, Release 2
intende utilizzare il servizio per uno scopo specifico. Si prenda il seguente esempio:
• YAML
services:
foo.twig.extension:
class: Acme\HelloBundle\Extension\FooExtension
tags:
- { name: twig.extension }
• XML
<service id="foo.twig.extension" class="Acme\HelloBundle\Extension\FooExtension">
<tag name="twig.extension" />
</service>
• PHP
$definition = new Definition(’Acme\HelloBundle\Extension\FooExtension’);
$definition->addTag(’twig.extension’);
$container->setDefinition(’foo.twig.extension’, $definition);
Il tag twig.extension è un tag speciale che TwigBundle utilizza durante la configurazione. Dando al
servizio il tag twig.extension, il bundle sa che il servizio foo.twig.extension dovrebbe essere registrato come estensione Twig con Twig. In altre parole, Twig cerca tutti i servizi etichettati con twig.extension
e li registra automaticamente come estensioni.
I tag, quindi, sono un modo per dire a Symfony2 o a un altro bundle di terze parti che il servizio dovrebbe essere
registrato o utilizzato in un qualche modo speciale dal bundle.
Quello che segue è un elenco dei tag disponibili con i bundle del nucleo di Symfony2. Ognuno di essi ha un
differente effetto sul servizio e molti tag richiedono parametri aggiuntivi (oltre al solo name del parametro).
• assetic.filter
• assetic.templating.php
• data_collector
• form.field_factory.guesser
• kernel.cache_warmer
• kernel.event_listener
• monolog.logger
• routing.loader
• security.listener.factory
• security.voter
• templating.helper
• twig.extension
• translation.loader
• validator.constraint_validator
Imparare di più dal ricettario
• Usare il factory per creare servizi
• Gestire le dipendenza comuni con i servizi padre
• Definire i controllori come servizi
232
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
2.1.16 Prestazioni
Symfony2 è veloce, senza alcuna modifica. Ovviamente, se occorre maggiore velocità, ci sono molti modi per
rendere Symfony2 ancora più veloce. In questo capitolo, saranno esplorati molti dei modi più comuni e potenti
per rendere la propria applicazione Symfony più veloce.
Usare una cache bytecode (p.e. APC)
Una delle cose migliori (e più facili) che si possono fare per migliorare le prestazioni è quella di usare una cache
bytecode. L’idea di una cache bytecode è di rimuove l’esigenza di dover ricompilare ogni volta il codice sorgente
PHP. Ci sono numerose cache bytecode disponibili, alcune delle quali open source. La più usata è probabilmente
APC.
Usare una cache bytecode non ha alcun effetto negativo, e Symfony2 è stato progettato per avere prestazioni
veramente buone in questo tipo di ambiente.
Ulteriori ottimizzazioni
Le cache bytecode solitamente monitorano i cambiamenti dei file sorgente. Questo assicura che, se la sorgente del
file cambia, il bytecode sia ricompilato automaticamente. Questo è molto conveniente, ma ovviamente aggiunge
un overhead.
Per questa ragione, alcune cache bytecode offrono un’opzione per disabilitare questi controlli. Ovviamente,
quando si disabilitano i controlli, sarà compito dell’amministratore del server assicurarsi che la cache sia svuotata
a ogni modifica dei file sorgente. Altrimenti, gli aggiornamenti eseguiti non saranno mostrati.
Per esempio, per disabilitare questi controlli in APC, aggiungere semplicemente apc.stat=0 al proprio file di
configurazione php.ini.
Usare un autoloader con caches (p.e. ApcUniversalClassLoader)
Per impostazione predefinita, Symfony2 standard edition usa UniversalClassLoader nel file autoloader.php. Questo autoloader è facile da usare, perché troverà automaticamente ogni nuova classe inserita
nelle cartella registrate.
Sfortunatamente, questo ha un costo, perché il caricatore itera tutti gli spazi dei nomi configurati per trovare un
particolare file, richiamando file_exists finché non trova il file cercato.
La soluzione più semplice è mettere in cache la posizione di ogni classe, dopo che è stata trovata per la
prima volta. Symfony dispone di una classe di caricamento, ApcUniversalClassLoader, che estende
UniversalClassLoader e memorizza le posizioni delle classi in APC.
Per usare questo caricatore, basta adattare il file autoloader.php come segue:
// app/autoload.php
require __DIR__.’/../vendor/symfony/src/Symfony/Component/ClassLoader/ApcUniversalClassLoader.php’
use Symfony\Component\ClassLoader\ApcUniversalClassLoader;
$loader = new ApcUniversalClassLoader(’some caching unique prefix’);
// ...
Note: Quando si usa l’autoloader APC, se si aggiungono nuove classi, saranno trovate automaticamente e tutto
funzionerà come prima (cioè senza motivi per “pulire” la cache). Tuttavia, se si cambia la posizione di un particolare spazio dei nomi o prefisso, occorrerà pulire la cache di APC. Altrimenti, l’autoloader cercherà ancora la
classe nella vecchia posizione per tutte le classi in quello spazio dei nomi.
2.1. Libro
233
Symfony2 documentation Documentation, Release 2
Usare i file di avvio
Per assicurare massima flessibilità e riutilizzo del codice, le applicazioni Symfony2 sfruttano una varietà di classi
e componenti di terze parti. Ma il caricamento di tutte queste classi da diversi file a ogni richiesta può risultate in
un overhead. Per ridurre tale overhead, Symfony2 Standard Edition fornisce uno script per generare i cosiddetti
file di avvio, che consistono in definizioni di molte classi in un singolo file. Includendo questo file (che contiene
una copia di molte classi del nucleo), Symfony non avrà più bisogno di includere alcuno dei file sorgente contenuti
nelle classi stesse. Questo riduce un po’ la lettura/scrittura su disco.
Se si usa Symfony2 Standard Edition, probabilmente si usa già un file di avvio. Per assicurarsene, aprire il proprio
front controller (solitamente app.php) e verificare che sia presente la seguente riga:
require_once __DIR__.’/../app/bootstrap.php.cache’;
Si noti che ci sono due svantaggi nell’uso di un file di avvio:
• il file deve essere rigenerato ogni volta che cambia una delle sorgenti originali (p.e. quando si aggiorna il
sorgente di Symfony2 o le librerie dei venditori);
• durante il debug, occorre inserire i breakpoint nel file di avvio.
Se si usa Symfony2 Standard Edition, il file di avvio è ricostruito automaticamente dopo l’aggiornamento delle
librerie dei venditori, tramite il comando php bin/vendors install.
File di avvio e cache bytecode
Anche usando una cache bytecode, le prestazioni aumenteranno con l’uso di un file di avvio, perché ci saranno
meno file da monitorare per i cambiamenti. Certamente, se questa caratteristica è disabilitata nella cache bytecode
(p.e. con apc.stat=0 in APC), non c’è più ragione di usare un file di avvio.
2.1.17 Interno
Se si vuole capire come funziona Symfony2 ed estenderlo, in questa sezione si potranno trovare spiegazioni
approfondite dell’interno di Symfony2.
Note: La lettura di questa sezione è necessaria solo per capire come funziona Symfony2 dietro le quinte oppure
se si vuole estendere Symfony2.
Panoramica
Il codice di Symfony2 è composto da diversi livelli indipendenti. Ogni livello è costruito sulla base del precedente.
Tip:
L’auto-caricamento non viene gestito direttamente dal framework, ma indipendentemente,
con l’aiuto della classe Symfony\Component\ClassLoader\UniversalClassLoader e del file
src/autoload.php. Leggere il capitolo dedicato per maggiori informazioni.
Il componente HttpFoundation
Il livello più profondo è il componente :namespace:‘Symfony\\Component\\HttpFoundation‘. HttpFoundation
fornisce gli oggetti principali necessari per trattare con HTTP. È un’astrazione orientata gli oggetti di alcune
funzioni e variabili native di PHP:
• La classe Symfony\Component\HttpFoundation\Request astrae le variabili globali principali
di PHP, come $_GET, $_POST, $_COOKIE, $_FILES e $_SERVER;
234
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
• La classe Symfony\Component\HttpFoundation\Response astrae alcune funzioni PHP, come
header(), setcookie() ed echo;
• La
classe
Symfony\Component\HttpFoundation\Session
e
l’interfaccia
Symfony\Component\HttpFoundation\SessionStorage\SessionStorageInterface
astraggono le funzioni di gestione della sessione session_*().
Il componente HttpKernel
Sopra HttpFoundation c’è il componente :namespace:‘Symfony\\Component\\HttpKernel‘. HttpKernel
gestisce la parte dinamica di HTTP e incapsula in modo leggero le classi Request e Response, per standardizzare il modo in cui sono gestite le richieste. Fornisce anche dei punti di estensione e degli strumenti che lo
rendono il punto di partenza ideale per creare un framework web senza troppe sovrastrutture.
Opzionalmente, aggiunge anche configurabilità ed estensibilità, grazie al componente Dependency Injection e a
un potente sistema di plugin (bundle).
See Also:
Approfondimento sul componente HttpKernel. Approfondimento sul componente Dependency Injection e sui
Bundle.
Il bundle FrameworkBundle
Il bundle :namespace:‘Symfony\\Bundle\\FrameworkBundle‘ è il bundle che lega insieme i componenti e le
librerie principali, per fare un framework MVC leggero e veloce. Dispone in una configurazione predefinita
adeguata e di convenzioni che facilitano la curva di apprendimento.
Kernel
La classe Symfony\Component\HttpKernel\HttpKernel è la classe centrale di Symfony2 ed è responsabile della gestione delle richieste del client.
Il suo scopo principale è
“convertire” un oggetto Symfony\Component\HttpFoundation\Request in un oggetto
Symfony\Component\HttpFoundation\Response.
Ogni kernel di Symfony2 implementa Symfony\Component\HttpKernel\HttpKernelInterface:
function handle(Request $request, $type = self::MASTER_REQUEST, $catch = true)
Controllori
Per convertire una Request in una Response, il kernel si appoggia a un “controllore”. Un controllore può
essere qualsiasi funzione o metodo PHP valido.
Il kernel delega la scelta di quale controllore debba essere eseguito a un’implementazione di
Symfony\Component\HttpKernel\Controller\ControllerResolverInterface:
public function getController(Request $request);
public function getArguments(Request $request, $controller);
Il metodo :method:‘Symfony\\Component\\HttpKernel\\Controller\\ControllerResolverInterface::getController‘
restituisce il controllore (una funzione PHP) associato alla Request data. L’implementazionoe predefinita
(Symfony\Component\HttpKernel\Controller\ControllerResolver) cerca un attributo
_controller della richiesta, che rappresenta il nome del controllore (una stringa “classe::metodo”, come
Bundle\BlogBundle\PostController:indexAction).
2.1. Libro
235
Symfony2 documentation Documentation, Release 2
Tip: L’implementazione predefinita usa Symfony\Bundle\FrameworkBundle\EventListener\RouterListener
per definire l’attributo _controller della richista (vedere Evento kernel.request).
Il metodo :method:‘Symfony\\Component\\HttpKernel\\Controller\\ControllerResolverInterface::getArguments‘
restituisce un array di parametri da passare al controllore. L’implementazione predefinita risolve automaticamente
i parametri, basandosi sugli attributi di Request.
Parametri del controllore dai parametri della richiesta
Per ciascun parametro, Symfony2 prova a prendere il valore dell’attributo della richiesta che abbia lo stesso
nome. Se non definito, viene usato il valore del parametro predefinito, se specificato:
// Symfony2 cerca un attributo ’id’ (obbligatorio)
// e uno ’admin’ (facoltativo)
public function showAction($id, $admin = true)
{
// ...
}
Gestione delle richieste
Il metodo handle() prende una Request e restituisce sempre una Response. Per convertire Request,
handle() si appoggia su Resolver e su una catena ordinata di notifiche di eventi (vedere la prossima sezione
per maggiori informazioni sugli oggetti Event):
1. Prima di tutto, viene notificato l’evento kernel.request, se uno degli ascoltatori restituisce una
Response, salta direttamente al passo 8;
2. Viene chiamato Resolver, per decidere quale controllore eseguire;
3. Gli ascoltatori dell’evento kernel.controller possono ora manipolare il controllore, nel modo che
preferiscono (cambiarlo, avvolgerlo, ecc.);
4. Il kernel verifica che il controllore sia effettivamente un metodo valido;
5. Viene chiamato Resolver, per decidere i parametri da passare al controllore;
6. Il kernel richiama il controllore;
7. Se il controllore non restituisce una Response, gli ascoltatori dell’evento kernel.view possono convertire il valore restituito dal controllore in una Response;
8. Gli ascoltatori dell’evento kernel.response possono manipolare la Response (sia il contenuto che
gli header);
9. Viene restituita la risposta.
Se viene lanciata un’eccezione durante il processo, viene notificato l’evento kernel.exception e gli ascoltatori possono convertire l’eccezione in una risposta. Se funziona, viene notificato l’evento kernel.response,
altrimenti l’eccezione viene lanciata nuovamente.
Se non si vuole che le eccezioni siano catturate (per esempio per richieste incluse), disabilitare l’evento
kernel.exception, passando false come terzo parametro del metodo handle().
Richieste interne
In qualsiasi momento, durante la gestione della richiesta (quella “principale”), si può gestire una sotto-richiesta.
Si può passare il tipo di richiesta al metodo handle(), come secondo parametro:
• HttpKernelInterface::MASTER_REQUEST;
• HttpKernelInterface::SUB_REQUEST.
236
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
Il tipo è passato a tutti gli eventi e gli ascoltatori possono agire di conseguenza (alcuni processi possono avvenire
solo sulla richiesta principale).
Eventi
Ogni evento lanciato dal kernel è una sotto-classe di Symfony\Component\HttpKernel\Event\KernelEvent.
Questo vuol dire che ogni evento ha accesso alle stesse informazioni di base:
• getRequestType() - restituisce il tipo della richiesta (HttpKernelInterface::MASTER_REQUEST
o HttpKernelInterface::SUB_REQUEST);
• getKernel() - restituisce il kernel che gestisce la richiesta;
• getRequest() - restituisce la Request attualmente in gestione.
getRequestType() Il metodo getRequestType() consente di sapere il tipo di richiesta. Per esempio,
se un ascoltatore deve essere attivo solo per richieste principali, aggiungere il seguente codice all’inizio del proprio
metodo ascoltatore:
use Symfony\Component\HttpKernel\HttpKernelInterface;
if (HttpKernelInterface::MASTER_REQUEST !== $event->getRequestType()) {
// restituire immediatamente
return;
}
Tip: Se non si ha familiarità con il distributore di eventi di Symfony2, leggere prima la sezione Eventi.
Evento kernel.request Classe evento: Symfony\Component\HttpKernel\Event\GetResponseEvent
Lo scopo di questo evento e di restituire subito un oggetto Response oppure impostare delle variabili in modo
che il controllore sia richiamato dopo l’evento. Qualsiasi ascoltatore può restituire un oggetto Response, tramite
il metodo setResponse() sull’evento. In questo caso, tutti gli altri ascoltatori non saranno richiamati.
Questo evento è usato da FrameworkBundle per popolare l’attributo _controller della Request, tramite
Symfony\Bundle\FrameworkBundle\EventListener\RouterListener. RequestListener usa un
oggetto Symfony\Component\Routing\RouterInterface per corrispondere alla Request e determinare il nome del controllore (memorizzato nell’attributo _controller di Request).
Evento kernel.controller Classe evento: Symfony\Component\HttpKernel\Event\FilterControllerEve
Questo evento non è usato da FrameworkBundle, ma può essere un punto di ingresso usato per modificare il
controllore da eseguire:
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
public function onKernelController(FilterControllerEvent $event)
{
$controller = $event->getController();
// ...
// il controllore può essere cambiato da qualsiasi funzione PHP
$event->setController($controller);
}
2.1. Libro
237
Symfony2 documentation Documentation, Release 2
Evento kernel.view Classe evento: Symfony\Component\HttpKernel\Event\GetResponseForControllerR
Questo evento non è usato da FrameworkBundle, ma può essere usato per implementare un sotto-sistema di
viste. Questo evento è chiamato solo se il controllore non restituisce un oggetto Response. Lo scopo dell’evento
è di consentire a qualcun altro di restituire un valore da convertire in una Response.
Il valore restituito dal controllore è accessibile tramite il metodo getControllerResult:
use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;
use Symfony\Component\HttpFoundation\Response;
public function onKernelView(GetResponseForControllerResultEvent $event)
{
$val = $event->getReturnValue();
$response = new Response();
// personalizzare in qualche modo la risposta dal valore restituito
$event->setResponse($response);
}
Evento kernel.response Classe evento: Symfony\Component\HttpKernel\Event\FilterResponseEvent
Lo scopo di questo evento è di consentire ad altri sistemi di modificare o sostituire l’oggetto Response dopo la
sua creazione:
public function onKernelResponse(FilterResponseEvent $event)
{
$response = $event->getResponse();
// .. modificare l’oggetto Response
}
FrameworkBundle registra diversi ascoltatori:
• Symfony\Component\HttpKernel\EventListener\ProfilerListener: raccoglie dati per
la richiesta corrente;
• Symfony\Bundle\WebProfilerBundle\EventListener\WebDebugToolbarListener:
inserisce la barra di web debug;
• Symfony\Component\HttpKernel\EventListener\ResponseListener:
Content-Type della risposta, in base al formato della richiesta;
• Symfony\Component\HttpKernel\EventListener\EsiListener:
HTTP Surrogate-Control quando si deve cercare dei tag ESI nella risposta.
aggiusta
il
aggiunge un header
Evento kernel.exception Classe evento: Symfony\Component\HttpKernel\Event\GetResponseForExcept
FrameworkBundle registra un Symfony\Component\HttpKernel\EventListener\ExceptionListener,
che gira la Request a un controllore dato (il valore del parametro exception_listener.controller,
che deve essere nel formato classe::metodo).
Un ascoltatore di questo evento può creare e impostare un oggetto Response, creare e impostare un nuovo
oggetto Exception, oppure non fare nulla:
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpFoundation\Response;
public function onKernelException(GetResponseForExceptionEvent $event)
{
$exception = $event->getException();
$response = new Response();
// prepara l’oggetto Response in base all’eccezione catturata
$event->setResponse($response);
238
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
// in alternativa si può impostare una nuova eccezione
// $exception = new \Exception(’Una qualche ecccezione speciale’);
// $event->setException($exception);
}
Il distributore di eventi
Il codice orientato agli oggetti è riuscito ad assicurare l’estensibilità del codice. Creando classi con responsabilità
ben definite, il codice diventa più flessibile e lo sviluppatore può estendere le classi con delle sotto-classi, per
modificare il loro comportamento. Ma se si vogliono condividere le modifiche con altri sviluppatori che hanno
fatto a loro volta delle sotto-classi, l’ereditarietà inizia a diventare un problema.
Consideriamo un esempio dal mondo reale, in cui si vuole fornire un sistema di plugin per il proprio progetto. Un
plugin dovrebbe essere in grado di aggiungere metodi o di fare qualcosa prima o dopo che un altro metodo venga
eseguito, senza interferire con altri plugin. Questo non è un problema facile da risolvere con l’ereditarietà singola,
mentre l’ereditarietà multipla (ove possibile in PHP) ha i suoi difetti.
Il distributore di eventi di Symfony2 implementa il pattern Observer in un modo semplice ed efficace, per rendere
possibili tutte queste cose e per rendere i propri progetti veramente estensibili.
Prendiamo un semplice esempio dal componente HttpKernel di Symfony2. Una volta che un oggetto Response
è stato creato, potrebbe essere utile consentire ad altri elementi del sistema di modificarlo (p.e. aggiungere degli
header per la cache) prima che sia effettivamente usato. Per poterlo fare, il kernel di Symfony2 lancia un evento,
kernel.response. Ecco come funziona:
• Un ascoltatore (un oggetto PHP) dice all’oggetto distributore centrale che vuole ascoltare l’evento
kernel.response;
• A un certo punto, il kernel di Symfony2 dice all’oggetto distributore di distribuire l’evento
kernel.response, passando con esso un oggetto Event, che ha accesso all’oggetto Response;
• Il distributore notifica a (cioè chiamat un metodo su) tutti gli ascoltatori dell’evento kernel.response,
consentendo a ciascuno di essi di effettuare modifiche sull’oggetto Response.
Eventi
Quando un evento viene distribuito, è identificato da un nome univoco (p.e. kernel.response),
a cui un numero qualsiasi di ascoltatori può ascoltare.
Inoltre,
un’istanza di
Symfony\Component\EventDispatcher\Event viene creata e passata a tutti gli ascoltatori. Come
vedremo più avanti, l’oggetto stesso Event spesso contiene dati sull’evento distribuito.
Convenzioni sui nomi Il nome univoco di un evento può essere una stringa qualsiasi, ma segue facoltativamente
alcune piccole convenzioni sui nomi:
• usa solo lettere minuscole, numeri, punti (.) e sotto-linee (_);
• ha un prefisso con uno spazio dei nomi, seguito da un punto (p.e. kernel.);
• finisce con un verbo che indichi l’azione che sta per essere eseguita (p.e. request).
Ecco alcuni esempi di buoni nomi di eventi:
• kernel.response
• form.pre_set_data
Nomi di eventi e oggetti evento Quando il distributore notifica agli ascoltatori, passa un oggetto Event a questi
ultimi. La classe base Event è molto semplice: contiene un metodo per bloccare la propagazione degli eventi,
non molto di più.
2.1. Libro
239
Symfony2 documentation Documentation, Release 2
Spesso, occorre passare i dati su uno specifico evento insieme all’oggetto Event, in
modo che gli ascoltatori abbiano le informazioni necessarie.
Nel caso dell’evento
kernel.response, l’oggetto Event creato e passato a ciascun ascoltatore è in realtà di tipo
Symfony\Component\HttpKernel\Event\FilterResponseEvent, una sotto-classe dell’oggetto
base Event. Questa classe contiene metodi come getResponse e setResponse, che consentono agli
ascoltatori di ottenere o anche sostituire l’oggetto Response.
La morale della storia è questa: quando si crea un ascoltatore di un evento, l’oggetto Event passato all’ascoltatore
potrebbe essere una speciale sotto-classe, che possiede ulteriori metodi per recuperare informazioni dall’evento e
per rispondere a esso.
Il distributore
Il distributore è l’oggetto centrale del sistema di distribuzione degli eventi. In generale, viene creato un solo distributore di eventi, che mantiene un registro di ascoltatori. Quando un evento viene distribuito tramite il distributore,
esso notifica a tutti gli ascoltatori registrati con tale evento.
use Symfony\Component\EventDispatcher\EventDispatcher;
$dispatcher = new EventDispatcher();
Connettere gli ascoltatori
Per trarre vantaggio da un evento esistente, occorre connettere un ascoltatore al distributore, in modo che possa
essere notificato quando l’evento viene distribuito. Un chiamata al metodo addListener() del distributore
associa una funzione PHP a un evento:
$listener = new AcmeListener();
$dispatcher->addListener(’pippo.azione’, array($listener, ’allAzionePippo’));
Il metodo addListener() accetta fino a tre parametri:
• Il nome dell’evento (stringa) che questo ascoltatore vuole ascoltare;
• Una funzione PHP, che sarà notificata quando viene lanciato un evento che sta ascoltando;
• Un intero, opzionale, di priorità (più alto equivale a più importante), che determina quando un ascoltatore
viene avvisato, rispetto ad altri ascoltatori (il valore predefinito è 0). Se due ascoltatori hanno la stessa
priorità, sono eseguito nello stesso ordine con cui sono stati aggiunti al distributore.
Note: Una funzione PHP è una variabile PHP che può essere usata dalla funzione call_user_func() e
che restituisce true, se passata alla funzione is_callable(). Può essere un’istanza di una \Closure, una
stringa che rappresenta una funzione oppure un array che rappresenta un metodo di un oggetto o di una classe.
Finora, abbiamo visto come oggetti PHP possano essere registrati come ascoltatori. Si possono anche registrare
Closure PHP come ascoltatori di eventi:
use Symfony\Component\EventDispatcher\Event;
$dispatcher->addListener(’pippo.azione’, function (Event $event) {
// sarà eseguito quando l’evento pippo.azione viene distribuito
});
Una volta che un ascoltatore è registrato con il distributore, esso aspetta fino a che l’evento non è notificato.
Nell’esempio visto sopra, quando l’evento pippo.azione è distribuito, il distributore richiama il metodo
AcmeListener::allAzionePippo e passa l’oggetto Event come unico parametro:
use Symfony\Component\EventDispatcher\Event;
class AcmeListener
240
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
{
// ...
public function allAzionePippo(Event $event)
{
// fare qualcosa
}
}
Tip: Se si usa il framework MVC di Symfony2 MVC, gli ascoltatori possono essere registrati tramite la configurazione. Come bonus aggiuntivo, gli oggetti ascoltatori sono istanziati solo all’occorrenza.
In alcuni casi, una sotto-classe speciale Event, specifica dell’evento dato, viene passata
all’ascoltatore.
Questo dà accesso all’ascoltatore a informazioni speciali sull’evento.
Leggere la documentazione o l’implementazione di ogni evento per determinare l’esatta istanza di
Symfony\Component\EventDispatcher\Event passata. Per esempio, l’evento kernel.event
passa un’istanza di Symfony\Component\HttpKernel\Event\FilterResponseEvent:
use Symfony\Component\HttpKernel\Event\FilterResponseEvent
public function onKernelResponse(FilterResponseEvent $event)
{
$response = $event->getResponse();
$request = $event->getRequest();
// ...
}
Creare e distribuire un evento
Oltre a registrare ascoltatori su eventi esistenti, si possono creare e lanciare eventi propri. Questo è utile quando si
creano librerie di terze parti e anche quando si vogliono mantenere diversi componenti personalizzati nel proprio
sistema flessibili e disaccoppiati.
La classe statica Events Si supponga di voler creare un nuovo evento, chiamato negozio.ordine, distribuito ogni volta che un ordine viene creato dentro la propria applicazione. Per mantenere le cose organizzate,
iniziamo a creare una classe StoreEvents all’interno della propria applicazione, che serve a definire e documentare il proprio evento:
namespace Acme\StoreBundle;
final class StoreEvents
{
/**
* L’evento negozio.ordine è lanciato ogni volta che un ordine viene creato
* nel sistema.
*
* L’ascoltatore dell’evento riceve un’istanza di Acme\StoreBundle\Event\FilterOrderEvent.
*
*
* @var string
*/
const onStoreOrder = ’negozio.ordine’;
}
Si noti che la class in realtà non fa nulla. Lo scopo della classe StoreEvents è solo quello di essere un posto
in cui le informazioni sugli eventi comuni possano essere centralizzate. Si noti che anche che una classe speciale
FilterOrderEvent sarà passata a ogni ascoltatore di questo evento.
2.1. Libro
241
Symfony2 documentation Documentation, Release 2
Creare un oggetto evento Più avanti, quando si distribuirà questo nuovo evento, si creerà un’istanza di Event
e la si passerà al distributore. Il distributore quindi passa questa stessa istanza a ciascuno degli ascoltatori
dell’evento. Se non si ha bisogno di passare informazioni agli ascoltatori, si può usare la classe predefinita
Symfony\Component\EventDispatcher\Event. Tuttavia, la maggior parte delle volte, si avrà bisogno
di passare informazioni sull’evento a ogni ascoltatore. Per poterlo fare, si creerà una nuova classe, che estende
Symfony\Component\EventDispatcher\Event.
In questo esempio, ogni ascoltatore avrà bisogno di accedere a un qualche oggetto Order. Creare una classe
Event che lo renda possibile:
namespace Acme\StoreBundle\Event;
use Symfony\Component\EventDispatcher\Event;
use Acme\StoreBundle\Order;
class FilterOrderEvent extends Event
{
protected $order;
public function __construct(Order $order)
{
$this->order = $order;
}
public function getOrder()
{
return $this->order;
}
}
Ogni ascoltatore ora ha accesso all’oggetto Order, tramite il metodo getOrder.
Distribuire l’evento Il metodo :method:‘Symfony\\Component\\EventDispatcher\\EventDispatcher::dispatch‘
notifica a tutti gli ascoltatori l’evento dato. Accetta due parametri: il nome dell’evento da distribuire e l’istanza di
Event da passare a ogni ascoltatore di tale evento:
use Acme\StoreBundle\StoreEvents;
use Acme\StoreBundle\Order;
use Acme\StoreBundle\Event\FilterOrderEvent;
// l’ordine viene in qualche modo creato o recuperato
$order = new Order();
// ...
// creare FilterOrderEvent e distribuirlo
$event = new FilterOrderEvent($order);
$dispatcher->dispatch(StoreEvents::onStoreOrder, $event);
Si noti che l’oggetto speciale FilterOrderEvent è creato e passato al metodo dispatch. Ora ogni ascoltatore dell’evento negozio.ordino riceverà FilterOrderEvent e avrà accesso all’oggetto Order, tramite
il metodo getOrder:
// una qualche classe ascoltatore che è stata registrata per onStoreOrder
use Acme\StoreBundle\Event\FilterOrderEvent;
public function onStoreOrder(FilterOrderEvent $event)
{
$order = $event->getOrder();
// fare qualcosa con l’ordine
}
242
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
Passare l’oggetto distributore di eventi
Se si dà un’occhiata alla classe EventDispatcher, si noterà che non agisce come un singleton (non c’è un
metodo statico getInstance()). Questa cosa è voluta, perché si potrebbe avere necessità di diversi distributori
di eventi contemporanei in una singola richiesta PHP. Ma vuol dire anche che serve un modo per passare il
distributore agli oggetti che hanno bisogno di connettersi o notificare eventi.
Il modo migliore è iniettare l’oggetto distributore di eventi nei propri oggetti, quindi usare la dependency injection.
Si può usare una constructor injection:
class Foo
{
protected $dispatcher = null;
public function __construct(EventDispatcher $dispatcher)
{
$this->dispatcher = $dispatcher;
}
}
Oppure una setter injection:
class Foo
{
protected $dispatcher = null;
public function setEventDispatcher(EventDispatcher $dispatcher)
{
$this->dispatcher = $dispatcher;
}
}
La scelta tra i due alla fine è una questione di gusti. Alcuni preferiscono la constructor injection, perché gli oggetti
sono inizializzati in pieno al momento della costruzione. Ma, quando si ha una lunga lista di dipendenza, usare la
setter injection può essere il modo migliore, specialmente per le dipendenze opzionali.
Tip: Se si usa la dependency injection come negli esempi sopra, si può usare il componente Dependency Injection
di Symfony2 per gestire questi oggetti in modo elegante.
# src/Acme/HelloBundle/Resources/config/services.yml
services:
foo_service:
class: Acme/HelloBundle/Foo/FooService
arguments: [@event_dispatcher]
Usare i sottoscrittori
Il modo più comune per ascoltare un evento è registrare un ascoltatore con il distributore. Questo ascoltatore può
ascoltare uno o più eventi e viene notificato ogni volta che tali eventi sono distribuiti.
Un altro modo per ascoltare gli eventi è tramite un sottoscrittore. Un sottoscrittore di eventi è una classe
PHP che è in grado di dire al distributore esattamente quale evento dovrebbe sottoscrivere. Implementa
l’interfaccia Symfony\Component\EventDispatcher\EventSubscriberInterface, che richiede
un unico metodo statico, chiamato getSubscribedEvents. Si consideri il seguente esempio di un sottoscrittore, che sottoscrive gli eventi kernel.response e negozio.ordine:
namespace Acme\StoreBundle\Event;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
2.1. Libro
243
Symfony2 documentation Documentation, Release 2
class StoreSubscriber implements EventSubscriberInterface
{
static public function getSubscribedEvents()
{
return array(
’kernel.response’ => ’onKernelResponse’,
’negozio.ordine’ => ’onStoreOrder’,
);
}
public function onKernelResponse(FilterResponseEvent $event)
{
// ...
}
public function onStoreOrder(FilterOrderEvent $event)
{
// ...
}
}
È molto simile a una classe ascoltatore, tranne che la classe stessa può dire al distributore quali
eventi dovrebbe ascoltare.
Per registrare un sottoscrittore con il distributore, usare il metodo
:method:‘Symfony\\Component\\EventDispatcher\\EventDispatcher::addSubscriber‘ :
use Acme\StoreBundle\Event\StoreSubscriber;
$subscriber = new StoreSubscriber();
$dispatcher->addSubscriber($subscriber);
Il distributore registrerà automaticamente il sottoscrittore per ciascun evento restituito dal metodo
getSubscribedEvents. Questo metodo restituisce un array indicizzata per nomi di eventi e i cui valori
sono o i nomi dei metodi da chiamare o array composti dal nome del metodo e da una priorità.
Tip: Se si usa il framework MVC Symfony2, si possono registrare sottoscrittori tramite la propria configurazione.
Come bonus aggiuntivo, gli oggetti sottoscrittori sono istanziati solo quando servono.
Bloccare il flusso e la propagazione degli eventi
In alcuni casi, potrebbe aver senso che un ascoltatore prevenga il richiamo di qualsiasi altro ascoltatore. In altre
parole, l’ascoltatore deve poter essere in grado di dire al distributore di bloccare ogni propagazione dell’evento a
futuri ascoltatori (cioè di non notificare più altri ascoltatori). Lo si può fare da dentro un ascoltatore, tramite il
metodo :method:‘Symfony\\Component\\EventDispatcher\\Event::stopPropagation‘:
use Acme\StoreBundle\Event\FilterOrderEvent;
public function onStoreOrder(FilterOrderEvent $event)
{
// ...
$event->stopPropagation();
}
Ora, tutti gli ascoltatori di negozio.ordine che non sono ancora stati richiamati non saranno richiamati.
Profiler
Se abilitato, il profiler di Symfony2 raccoglie informazioni utili su ogni richiesta fatta alla propria applicazione e
le memorizza per analisi successive. L’uso del profiler in ambienti di sviluppo aiuta il debug del proprio codice e
244
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
a migliorare le prestazioni. Lo si può usare anche in ambienti di produzione, per approfondire i problemi che si
presentano.
Raramente si avrà a che fare direttamente con il profiler, visto che Symfony2 fornisce strumenti di visualizzazione,
come la barra di web debug e il profiler web. Se si usa Symfony2 Standard Edition, il profiler, la barra di web
debug e il profiler web sono già configurati con impostazioni appropriate.
Note: Il profiler raccoglie informazioni per tutte le richieste (richieste semplici, rinvii, eccezioni, richieste Ajax,
richieste ESI) e per tutti i metodi e formati HTTP. Questo vuol dire che per un singolo URL si possono avere
diversi dati di profile associati (uno per ogni coppia richiesta/risposta esterna).
Visualizzare i dati di profile
Usare la barra di web debug In ambiente di sviluppo, la barra di web debug è disponibile in fondo a ogni
pagina. Essa mostra un buon riassunto dei dati di profile, che danno accesso immediato a moltissime informazioni
utili, quando qualcosa non funziona come ci si aspetta.
Se il riassunto fornito dalla barra di web debug non basta, cliccare sul collegamento del token (una stringa di 13
caratteri casuali) per accedere al profiler web.
Note: Se il token non è cliccabile, vuol dire che le rotte del profiler non sono state registrate (vedere sotto per le
informazioni sulla configurazione).
Analizzare i dati di profile con il profiler web Il profiler web è uno strumento di visualizzazione per i dati
di profile, che può essere usato in sviluppo per il debug del codice e l’aumento delle prestazioni. Ma lo si può
anche usare per approfondire problemi occorsi in produzione. Espone tutte le informazioni raccolte dal profiler in
un’interfaccia web.
Accedere alle informazioni di profile Non occorre usare il visualizzatore predefinito per accedere alle informazioni di profile. Ma come si possono recuperare informazioni di profile per una specifica richiesta, dopo che
è accaduta? Quando il profiler memorizza i dati su una richiesta, vi associa anche un token. Questo token è
disponibile nell’header HTTP X-Debug-Token della risposta:
$profile = $container->get(’profiler’)->loadProfileFromResponse($response);
$profile = $container->get(’profiler’)->loadProfile($token);
Tip: Quando il profiler è abiliato, ma non lo è la barra di web debug, oppure quando si vuole il token di una
richiesta Ajax, usare uno strumento come Firebug per ottenere il valore dell’header HTTP X-Debug-Token.
Usare il metodo find() per accedere ai token, in base a determinati criteri:
// gli ultimi 10 token
$tokens = $container->get(’profiler’)->find(’’, ’’, 10);
// gli ultimi 10 token per URL che contengono /admin/
$tokens = $container->get(’profiler’)->find(’’, ’/admin/’, 10);
// gli ultimi 10 token per richieste locali
$tokens = $container->get(’profiler’)->find(’127.0.0.1’, ’’, 10);
Se si vogliono manipolare i dati di profile su macchine diverse da quella che ha generato le informazioni, usare i
metodi export() e import():
2.1. Libro
245
Symfony2 documentation Documentation, Release 2
// sulla macchina di produzione
$profile = $container->get(’profiler’)->loadProfile($token);
$data = $profiler->export($profile);
// sulla macchina di sviluppo
$profiler->import($data);
Configurazione La configurazione predefinita di Symfony2 ha delle impostazioni adeguate per il profiler, la
barra di web debug e il profiler web. Ecco per esempio la configurazione per l’ambiente di sviluppo:
• YAML
# load the profiler
framework:
profiler: { only_exceptions: false }
# enable the web profiler
web_profiler:
toolbar: true
intercept_redirects: true
verbose: true
• XML
<!-- xmlns:webprofiler="http://symfony.com/schema/dic/webprofiler" -->
<!-- xsi:schemaLocation="http://symfony.com/schema/dic/webprofiler http://symfony.com/schema/
<!-- load the profiler -->
<framework:config>
<framework:profiler only-exceptions="false" />
</framework:config>
<!-- enable the web profiler -->
<webprofiler:config
toolbar="true"
intercept-redirects="true"
verbose="true"
/>
• PHP
// carica il profiler
$container->loadFromExtension(’framework’, array(
’profiler’ => array(’only-exceptions’ => false),
));
// abilita il profiler web
$container->loadFromExtension(’web_profiler’, array(
’toolbar’ => true,
’intercept-redirects’ => true,
’verbose’ => true,
));
Quando only-exceptions è impostato a true, il profiler raccoglie dati solo quando l’applicazione solleva
un’eccezione.
Quando intercept-redirects è impostata true, il profiler web intercetta i rinvii e dà l’opportunità di
guardare i dati raccolti, prima di seguire il rinvio.
Quando verbose è impostato a true, la barra di web debug mostra diverse informazioni. L’impostazione
verbose a false nasconde alcune informazioni secondarie, per rendere la barra più corta.
Se si abilita il profiler web, occorre anche montare le rotte del profiler:
246
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
• YAML
_profiler:
resource: @WebProfilerBundle/Resources/config/routing/profiler.xml
prefix:
/_profiler
• XML
<import resource="@WebProfilerBundle/Resources/config/routing/profiler.xml" prefix="/_profile
• PHP
$collection->addCollection($loader->import("@WebProfilerBundle/Resources/config/routing/profi
Poiché il profiler aggiunge un po’ di sovraccarico, probabilmente lo si abiliterà solo in alcune circostanze in
ambiente di produzione. L’impostazione only-exceptions limita il profile alle pagine 500, ma che succede
se si vogliono più informazioni quando il client ha uno specifico indirizzo IP, oppure per una parte limitata del
sito? Si può usare un matcher della richiesta:
• YAML
# abilita il profiler solo per richieste provenienti dalla rete 192.168.0.0
framework:
profiler:
matcher: { ip: 192.168.0.0/24 }
# abilita il profiler solo per gli URL /admin
framework:
profiler:
matcher: { path: "^/admin/" }
# combina le regole
framework:
profiler:
matcher: { ip: 192.168.0.0/24, path: "^/admin/" }
# usa un matcher personalizzato, definito nel servizio "custom_matcher"
framework:
profiler:
matcher: { service: custom_matcher }
• XML
<!-- abilita il profiler solo per richieste provenienti dalla rete 192.168.0.0 -->
<framework:config>
<framework:profiler>
<framework:matcher ip="192.168.0.0/24" />
</framework:profiler>
</framework:config>
<!-- abilita il profiler solo per gli URL /admin -->
<framework:config>
<framework:profiler>
<framework:matcher path="^/admin/" />
</framework:profiler>
</framework:config>
<!-- combina le regole -->
<framework:config>
<framework:profiler>
<framework:matcher ip="192.168.0.0/24" path="^/admin/" />
</framework:profiler>
</framework:config>
<!-- usa un matcher personalizzato, definito nel servizio "custom_matcher" -->
2.1. Libro
247
Symfony2 documentation Documentation, Release 2
<framework:config>
<framework:profiler>
<framework:matcher service="custom_matcher" />
</framework:profiler>
</framework:config>
• PHP
// abilita il profiler solo per richieste provenienti dalla rete 192.168.0.0
$container->loadFromExtension(’framework’, array(
’profiler’ => array(
’matcher’ => array(’ip’ => ’192.168.0.0/24’),
),
));
// abilita il profiler solo per gli URL /admin
$container->loadFromExtension(’framework’, array(
’profiler’ => array(
’matcher’ => array(’path’ => ’^/admin/’),
),
));
// combina le regole
$container->loadFromExtension(’framework’, array(
’profiler’ => array(
’matcher’ => array(’ip’ => ’192.168.0.0/24’, ’path’ => ’^/admin/’),
),
));
# usa un matcher personalizzato, definito nel servizio "custom_matcher"
$container->loadFromExtension(’framework’, array(
’profiler’ => array(
’matcher’ => array(’service’ => ’custom_matcher’),
),
));
Imparare di più dal ricettario
• Come usare il profilatore nei test funzionali
• Come creare un raccoglitore di dati personalizzato
• Come estendere una classe senza usare l’ereditarietà
• Come personalizzare il comportamento di un metodo senza usare l’ereditarietà
2.1.18 L’API stabile di Symfony2
L’API stabile di Symfony2 è un sottoinsieme di tutti i metodi pubblici di Symfony2 (componenti e bundle del
nucleo) che condividono le seguenti proprietà:
• Lo spazio dei nomi e il nome della classe non cambieranno;
• Il nome del metodo non cambierà;
• La firma del metodo (i tipi dei parametri e del valore restituito) non cambierà;
• La semantica di quello che fa il metodo non cambierà;
Tuttavia potrebbe cambiare l’implementazione. L’unico caso valido per una modifica dell’API stabile è la soluzone
di una questione di sicurezza.
L’API stabile è basata su una lista, con il tag @api. Quindi, tutto ciò che non possiede esplicitamente il tag non fa
parte dell’API stabile.
248
Chapter 2. Libro
Symfony2 documentation Documentation, Release 2
Tip: Ogni bundle di terze parti dovrebbe a sua volta pubblicare la sua API stabile.
A partire da Symfony 2.0, i seguenti componenti hanno un tag API pubblico:
• BrowserKit
• ClassLoader
• Console
• CssSelector
• DependencyInjection
• DomCrawler
• EventDispatcher
• Finder
• HttpFoundation
• HttpKernel
• Locale
• Process
• Routing
• Templating
• Translation
• Validator
• Yaml
• Symfony2 e fondamenti di HTTP
• Symfony2 contro PHP puro
• Installare e configurare Symfony
• Creare pagine in Symfony2
• Il controllore
• Le rotte
• Creare e usare i template
• Database e Doctrine (“Il modello”)
• Test
• Validazione
• Form
• Sicurezza
• Cache HTTP
• Traduzioni
• Contenitore di servizi
• Prestazioni
• Interno
• L’API stabile di Symfony2
2.1. Libro
249
Symfony2 documentation Documentation, Release 2
• Symfony2 e fondamenti di HTTP
• Symfony2 contro PHP puro
• Installare e configurare Symfony
• Creare pagine in Symfony2
• Il controllore
• Le rotte
• Creare e usare i template
• Database e Doctrine (“Il modello”)
• Test
• Validazione
• Form
• Sicurezza
• Cache HTTP
• Traduzioni
• Contenitore di servizi
• Prestazioni
• Interno
• L’API stabile di Symfony2
250
Chapter 2. Libro
CHAPTER
THREE
RICETTARIO
3.1 Ricettario
3.1.1 Come creare e memorizzare un progetto Symfony2 in git
Tip: Sebbene questa guida riguardi nello specifico git, gli stessi principi valgono in generale se si memorizza un
progetto in Subversion.
Una volta letto Creare pagine in Symfony2 e preso familiarità con l’uso di Symfony, si vorrà certamente iniziare
un proprio progetto. In questa ricetta si imparerà il modo migliore per iniziare un nuovo progetto Symfony2,
memorizzato usando il sistema di controllo dei sorgenti git.
Preparazione del progetto
Per iniziare, occorre scaricare Symfony e inizializzare il repository locale:
1. Scaricare Symfony2 Standard Edition senza venditori.
2. Scompattare la distribuzione. Questo creerà una cartella chiamata “Symfony” con la struttura del nuovo
progetto, i file di configurazione, ecc. Si può rinominare la cartella a piacere.
3. Creare un nuovo file chiamato .gitignore nella radice del nuovo progetto (ovvero vicino al file deps)
e copiarvi le righe seguenti. I file corrispondenti a questi schemi saranno ignorati da git:
/web/bundles/
/app/bootstrap*
/app/cache/*
/app/logs/*
/vendor/
/app/config/parameters.yml
4. Copiare app/config/parameters.yml in app/config/parameters.yml.dist. Il file
parameters.yml è ignorato da git (vedi sopra), quindi le impostazioni specifiche della macchina, come
le password del database, non saranno inviate. Creando il file parameters.yml.dist, i nuovi sviluppatori potranno clonare rapidamente il progetto, copiando questo file in parameters.yml e personalizzandolo.
5. Inizializzare il proprio repository git:
$ git init
6. Aggiungere tutti i file in git:
$ git add .
7. Creare un commit iniziale con il nuovo progetto:
251
Symfony2 documentation Documentation, Release 2
$ git commit -m "Commit iniziale"
8. Infine, scaricare tutte le librerie dei venditori:
$ php bin/vendors install
A questo punto, si ha un progetto Symfony2 pienamente funzionante e correttamente copiato su git. Si può iniziare
subito a sviluppare, inviando i commit delle modifiche al proprio repository git.
Tip: Dopo aver eseguito il comando:
$ php bin/vendors install
il progetto conterrà la cronologia completa di tutt i bundle e le librerie definite nel file deps. Potrebbero essere
anche 100 MB! Si possono cancellare le cartelle della cronologia di git con il comando seguente:
$ find vendor -name .git -type d | xargs rm -rf
Il comando cancella tutte le cartelle .git contenute nella cartella vendor.
Se successivamente si vogliono aggiornare i bundle definiti nel file deps, occorrerà installarli nuovamente:
$ php bin/vendors install --reinstall
Si può continuare a seguire il capitolo Creare pagine in Symfony2 per imparare di più su come configurare e
sviluppare la propria applicazione.
Tip: Symfony2 Standard Edition è distribuito con alcuni esempi di funzionamento. Per rimuovere il codice di
esempio, seguire le istruzioni nel file Readme di Standard Edition.
Venditori e sotto-moduli
Invece di usare il sistema basato su deps e bin/vendors per gestire le librerie dei venditori, si potrebbe invece
voler usare i sotto-moduli di git. Non c’è nulla di sbagliato in questo approccio, ma il sistema deps è la via
ufficiale per risolvere questo problema e i sotto-moduli di git possono a volte creare delle difficoltà.
Memorizzare il progetto su un server remoto
Si è ora in possesso di un progetto Symfony2 pienamente funzionante e copiato in git. Tuttavia, spesso si vuole
memorizzare il proprio progetto un server remoto, sia per motivi di backup, sia per fare in modo che altri sviluppatori possano collaborare al progetto.
Il modo più facile per memorizzare il proprio progetto su un server remoto è l’utilizzo di GitHub. I repository
pubblici sono gratuiti, mentre per quelli privati è necessario pagare mensilmente.
In alternativa, si può ospitare un proprio repository git su un qualsiasi server, creando un repository privato e
usando quello. Una libreria che può aiutare in tal senso è Gitolite.
3.1.2 Come creare e memorizzare un progetto Symfony2 in Subversion
Tip: Questa voce è specifica per Subversion e si basa sui principi di Come creare e memorizzare un progetto
Symfony2 in git.
Una volta letto Creare pagine in Symfony2 e aver preso familiarità con l’uso di Symfony, si è senza dubbio pronti
per iniziare il proprio progetto. Il metodo preferito per gestire progetti Symfony2 è l’uso di git, ma qualcuno
252
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
preferisce usare Subversion, che va totalmente bene! In questa ricetta, vedremo come gestire il proprio progetto
usando svn, in modo simile a quanto si farebbe con git.
Tip: Questo è un metodo per memorizzare il proprio progetto Symfony2 in un repository Subversion. Ci sono
molti modi di farlo e questo è semplicemente uno che funziona.
Il repository Subversion
Per questa ricetta, supporremo che lo schema del repository segua la struttura standard, molto diffusa:
mio_progetto/
branches/
tags/
trunk/
Tip: La maggior parte degli host con subversion dovrebbero seguire questa pratica. Questo è lo schema raccomandato in Controllo di versione con Subversion e quello usato da quasi tutti gli host gratuiti (vedere Soluzioni di
hosting subversion).
Preparazione del progetto
Per iniziare, occorre scaricare Symfony2 e preparare Subversion:
1. Scaricare Symfony2 Standard Edition, con o senza venditori.
2. Scompattare la distribuzione. Questo creerà una cartella chiamata Symfony, con la struttura del nuovo
progetto, i file di configurazione, ecc. Rinominarla con il nome che si desidera.
3. Eseguire il checkout del repository Subversion che ospiterà questo progetto. Supponiamo che sia ospitato
su Google code e che si chiami mioprogetto:
$ svn checkout http://mioprogetto.googlecode.com/svn/trunk mioprogetto
4. Copiare i file del progetto Symfony2 nella cartella di subversion:
$ mv Symfony/* mioprogetto/
5. Impostiamo ora le regole di ignore. Non tutto andrebbe memorizzato nel repository subversion. Alcuni file
(come la cache) sono generati e altri (come la configurazione del database) devono essere personalizzati su
ciascuna macchina. Ciò implica l’uso della proprietà svn:ignore, che consente di ignorare specifici file.
$ cd mioprogetto/
$ svn add --depth=empty app app/cache app/logs app/config web
$
$
$
$
$
svn
svn
svn
svn
svn
propset
propset
propset
propset
propset
svn:ignore
svn:ignore
svn:ignore
svn:ignore
svn:ignore
"vendor" .
"bootstrap*" app/
"parameters.ini" app/config/
"*" app/cache/
"*" app/logs/
$ svn propset svn:ignore "bundles" web
$ svn ci -m "commit della lista di ignore di Symfony (vendor, app/bootstrap*, app/config/
6. Tutti gli altri file possono essere aggiunti al progetto:
$ svn add --force .
$ svn ci -m "aggiunta Symfony Standard 2.X.Y"
3.1. Ricettario
253
Symfony2 documentation Documentation, Release 2
7. Copiare app/config/parameters.ini su app/config/parameters.ini.dist. Il file
parameters.ini è ignorato da svn (vedere sopra) in modo che le impostazioni delle singole macchine,
come le password del database, non siano inserite. Creando il file parameters.ini.dist, i nuovi
sviluppatori possono prendere subito il progetto, copiare questo file in parameters.ini, personalizzarlo e iniziare a sviluppare.
8. Infine, scaricare tutte le librerie dei venditori:
$ php bin/vendors install
Tip: git deve essere installato per poter eseguire bin/vendors, essendo il protocollo usato per recuperare
le librerie. Questo vuol dire che git è usato solo come strumento per poter scaricare le librerie nella cartella
vendor/.
A questo punto, si ha un progetto Symfony2 pienamente funzionante, memorizzato nel proprio repository Subversion. Si può iniziare lo sviluppo, con i commit verso il repository.
Si può continuare a seguire il capitolo Creare pagine in Symfony2 per imparare di più su come configurare e
sviluppare la propria applicazione.
Tip: La Standard Edition di Symfony2 ha alcune funzionalità di esempio. Per rimuovere il codice di esempio,
seguire le istruzioni nel Readme della Standard Edition.
Gestire le librerie dei venditori con bin/vendors e deps
Ogni progetto Symfony usa un gruppo di librerie di “venditori”. In un modo o nell’altro, lo scopo è scaricare tali
file nella propria cartella vendor/ e, idealmente, avere un modo tranquillo per gestire l’esatta versione necessaria
per ciascuno.
Per impostazione predefinita, tali librerie sono scaricate eseguendo uno script “scaricatore” php bin/vendors
install. Questo script legge dal file deps nella radice del proprio progetto. Questo è uno script in formato ini,
che contiene una lista di ogni libreria necessaria, la cartella in cui ognuna va scaricata e (opzionalmente) la versione
da scaricare. Lo script bin/vendors usa git per scaricare, solamente perché queste librerie esterne solitamente
sono memorizzate tramite git. Lo script bin/vendors legge anche il file deps.lock, che consente di bloccare
ogni libreria a un preciso hash di commit.
È importante capire che queste librerie di venditori non sono in realtà parte del proprio repository. Sono invece dei semplici file non tracciati, che sono scaricati dallo script bin/vendors nella cartella vendor/. Ma,
poiché ogni informazione necessaria a scaricare tali file è nei file deps e deps.lock (che sono memorizzati nel
proprio repository), ogni altro sviluppatore può usare il progetto, eseguendo php bin/vendors install e
scaricando lo stesso preciso insieme di librerie di venditori. Questo vuol dire che si può controllare con precisione
ogni libreria di venditore, senza dover in realtà inserirle nel proprio repository.
Quindi, ogni volta che uno sviluppatore usa il progetto, deve eseguire lo script php bin/vendors install,
per assicurarsi di avere tutt le librerie necessarie.
Aggiornare Symfony
Poiché Symfony non è altro che un gruppo di librerie di terze parti e le librerie di terze parti sono interamente
controllate tramite deps e deps.lock, aggiornare Symfony vuol dire semplicemente aggiornare questi
due file, per far corrispondere il loro stato a quello dell’ultima Standard Edition di Symfony.
Ovviamente, se sono state aggiunte nuove voci a deps o deps.lock, assicurarsi di sostituire solo le parti
originali (cioè assicurarsi di non cancellare alcuna delle proprie voci).
254
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
Caution:
C’è anche un comando php bin/vendors update, ma non ha niente a che fare con
l’aggiornamento del progetto e solitamente non sarà necessario usarlo. Questo comando è usato per congelare
le versioni di tutte le librerie dei venditori, aggiornandole alle versioni specificate in deps e registrandole nel
file deps.lock.
Soluzioni di hosting subversion
La differenza maggiore tra git e svn è che Subversion necessita di un repository centrale per funzionare. Ci sono
diverse soluzioni:
• Hosting autonomo: creare il proprio repository e accedervi tramite filesystem o tramite rete. Per maggiori
informazioni, leggere Controllo di versione con Subversion.
• Hosting di terze parti: ci sono molte buone soluzioni di hosting gratuito a disposizione, come GitHub,
Google code, SourceForge o Gna. Alcune di queste offrono anche hosting git.
3.1.3 Come personalizzare le pagine di errore
Quando in Symfony2 viene lanciata una qualsiasi eccezione, l’eccezione viene catturata all’interno della classe
Kernel ed eventualmente inoltrata a un controllore speciale, TwigBundle:Exception:show per la gestione. Questo controllore, che vive all’interno del core TwigBundle, determina quale template di errore visualizzare e il codice di stato che dovrebbe essere impostato per la data eccezione.
Le pagine di errore possono essere personalizzate in due diversi modi, a seconda di quanto controllo si vuole
avere:
1. Personalizzare i template di errore delle diverse pagine di errore (spiegato qua sotto);
2. Sostituire il controllore predefinito delle eccezioni TwigBundle::Exception:show con il proprio
controllore e gestirlo come si vuole (vedere exception_controller nella guida di riferimento di Twig);
Tip: La personalizzazione della gestione delle eccezioni in realtà è molto più potente di quanto scritto qua.
Viene lanciato un evento interno, kernel.exception, che permette un controllo completo sulla gestione
delle eccezioni. Per maggiori informazioni, vedere Evento kernel.exception.
Tutti i template degli errori sono presenti all’interno di TwigBundle. Per sovrascrivere i template, si può semplicemente utilizzare il metodo standard per sovrascrivere i template che esistono all’interno di un bundle. Per
maggiori informazioni, vedere Sovrascrivere template dei bundle.
Ad esempio, per sovrascrivere il template di errore predefinito che mostrato all’utente finale, creare un nuovo
template posizionato in app/Resources/TwigBundle/views/Exception/error.html.twig:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Si è verificato un errore: {{ status_text }}</title>
</head>
<body>
<h1>Oops! Si è verificato un errore</h1>
<h2>Il server ha restituito un "{{ status_code }} {{ status_text }}".</h2>
</body>
</html>
Tip: Non bisogna preoccuparsi, se non hai familiarità con Twig. Twig è un semplice, potente e opzionale motore
per i template che si integra con Symfony2. Per maggiori informazioni su Twig vedere Creare e usare i template.
3.1. Ricettario
255
Symfony2 documentation Documentation, Release 2
In aggiunta alla pagina di errore standard HTML, Symfony fornisce una pagina di errore predefinita per molti
dei formati di risposta più comuni, tra cui JSON (error.json.twig), XML, (error.xml.twig) e anche
Javascript (error.js.twig), per citarne alcuni. Per sovrascrivere uno di questi template, basta creare un
nuovo file con lo stesso nome nella cartella app/Resources/TwigBundle/views/Exception. Questo
è il metodo standard per sovrascrivere qualunque template posizionato dentro a un bundle.
Personalizzazione della pagina 404 e di altre pagine di errore
È anche possibile personalizzare specializzare specifici template di errore in base al codice di stato. Per esempio, creare un template app/Resources/TwigBundle/views/Exception/error404.html.twig
per visualizzare una pagina speciale per gli errori 404 (pagina non trovata).
Symfony utilizza il seguente algoritmo per determinare quale template deve usare:
• Prima, cerca un template per il dato formato e codice di stato (tipo error404.json.twig);
• Se non esiste, cerca un per il dato formato (tipo error.json.twig);
• Se non esiste, si ricade nel template HTML (tipo error.html.twig).
Tip:
Per vedere l’elenco completo dei template di errore predefiniti, vedere la cartella
Resources/views/Exception del TwigBundle. In una installazione standard di Symfony2, il
TwigBundle può essere trovato in vendor/symfony/src/Symfony/Bundle/TwigBundle. Spesso,
il modo più semplice per personalizzare una pagina di errore è quello di copiarlo da TwigBundle in
app/Resources/TwigBundle/views/Exception e poi modificarlo.
Note: Le pagine “amichevoli” di debug delle eccezione mostrate allo sviluppatore possono anche loro essere
personalizzate nello stesso modo creando template come exception.html.twig per la pagina di eccezione
standard in HTML o exception.json.twig per la pagina di eccezione JSON.
3.1.4 Definire i controllori come servizi
Nel libro, abbiamo imparato quanto è facile usare un controllore quando estende la classe base
Symfony\Bundle\FrameworkBundle\Controller\Controller. Oltre a questo metodo, i controllori possono anche essere specificati come servizi.
Per fare rifermento a un controllore definito come servizio, usare la notazione con un solo “due punti” (:). Per
esempio, si supponga di aver definito un servizio chiamato mio controllore e che si voglia rimandare a un
metodo chiamato indexAction() all’interno di tale servizio:
$this->forward(’mio_controllore:indexAction’, array(’pippo’ => $pluto));
Occorre usare la stessa notazione, quando si definisce il valore _controller della rotta:
mio_controllore:
pattern:
/
defaults: { _controller: mio_controllore:indexAction }
Per usare un controllore in questo modo, deve essere definito nella configurazione del contenitore di servizi. Per
ulteriori informazioni, si veda il capitolo Contenitore di servizi.
Quando si usa un controllore definito come servizio, esso probabilmente non estenderà la classe base
Controller. Invece di appoggiarsi ai metodi scorciatoia di tale classe, si interagirà direttamente coi servizi
necessari. Fortunatamente, questo è di solito abbastanza facile e la classe Controller è una grande risorsa per
sapere come eseguire i compiti più comuni.
Note: Specificare un controllore come servizio richiede un po’ più di lavoro. Il vantaggio principale è che
l’intero controllore o qualsiasi servizio passato al controllore possono essere modificati tramite la configurazione
256
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
del contenitore di servizi. Questo è particolarmente utile quando si sviluppa un bundle open source o un bundle
che sarà usato in progetti diversi. Quindi, anche non specificando i propri controllori come servizi, probabilmente
si vedrà questo aspetto in diversi bundle open source di Symfony2.
3.1.5 Come forzare le rotte per utilizzare sempre HTTPS
A volte, si desidera proteggere alcune rotte ed essere sicuri che siano sempre accessibili solo tramite il protocollo
HTTPS. Il componente Routing consente di forzare lo schema HTTP attraverso il requisito _scheme:
• YAML
secure:
pattern: /secure
defaults: { _controller: AcmeDemoBundle:Main:secure }
requirements:
_scheme: https
• XML
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/r
<route id="secure" pattern="/secure">
<default key="_controller">AcmeDemoBundle:Main:secure</default>
<requirement key="_scheme">https</requirement>
</route>
</routes>
• PHP
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;
$collection = new RouteCollection();
$collection->add(’secure’, new Route(’/secure’, array(
’_controller’ => ’AcmeDemoBundle:Main:secure’,
), array(
’_scheme’ => ’https’,
)));
return $collection;
La configurazione sopra forza la rotta secure a utilizzare sempre HTTPS.
Quando si genera l’URL secure e se lo schema corrente è HTTP, Symfony genererà automaticamente un URL
assoluto con HTTPS come schema:
# Se lo schema corrente è HTTPS
{{ path(’secure’) }}
# generates /secure
# Se lo schema corrente è HTTP
{{ path(’secure’) }}
# generates https://example.com/secure
L’esigenza è anche quella di forzare le richieste in arrivo. Se si tenta di accedere al percorso /secure con HTTP,
si verrà automaticamente rinviati allo stesso URL, ma con lo schema HTTPS.
L’esempio precedente utilizza https per _scheme, ma si può anche forzare un URL per usare sempre http.
3.1. Ricettario
257
Symfony2 documentation Documentation, Release 2
Note: La componente di sicurezza fornisce un altro modo per forzare lo schema HTTP, tramite l’impostazione
requires_channel. Questo metodo alternativo è più adatto per proteggere un“‘area” del sito web (tutti gli
URL sotto /admin) o quando si vuole proteggere URL definiti in un bundle di terze parti.
3.1.6 Come permettere un carattere “/” in un parametro di rotta
A volte è necessario comporre URL con parametri che possono contenere una barra /. Per esempio, prendiamo
la classica rotta /hello/{name}. Per impostazione predefinita, /hello/Fabien corrisponderà a questa
rotta, ma non /hello/Fabien/Kris. Questo è dovuto al fatto che Symfony utilizza questo carattere come
separatore tra le parti delle rotte.
Questa guida spiega come modificare una rotta in modo che /hello/Fabien/Kris corrisponda alla rotta
/hello/{name}, dove {name} vale Fabien/Kris.
Configurare la rotta
Per impostazione predefinita, il componente delle rotte di symfony richiede che i parametri corrispondano alla
seguente espressione regolare: [^/]+. Questo significa che tutti i caratteri sono permessi eccetto /.
Bisogna consentire esplicitamente che il carattere / possa far parte del parametro specificando una espressione
regolare più permissiva.
• YAML
_hello:
pattern: /hello/{name}
defaults: { _controller: AcmeDemoBundle:Demo:hello }
requirements:
name: ".+"
• XML
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/r
<route id="_hello" pattern="/hello/{name}">
<default key="_controller">AcmeDemoBundle:Demo:hello</default>
<requirement key="name">.+</requirement>
</route>
</routes>
• PHP
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;
$collection = new RouteCollection();
$collection->add(’_hello’, new Route(’/hello/{name}’, array(
’_controller’ => ’AcmeDemoBundle:Demo:hello’,
), array(
’name’ => ’.+’,
)));
return $collection;
• Annotations
258
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
class DemoController
{
/**
* @Route("/hello/{name}", name="_hello", requirements={"name" = ".+"})
*/
public function helloAction($name)
{
// ...
}
}
Questo è tutto! Ora, il parametro {name} può contenere il carattere /.
3.1.7 Come usare Assetic per la gestione delle risorse
Assetic unisce due idee principali: risorse e filtri. Le risorse sono file come CSS, JavaScript e file di immagini. I
filtri sono cose che possono essere applicate a questi file prima di essere serviti al browser. Questo permette una
separazione tra i file delle risorse memorizzati nell’applicazione e i file effettivamente presentati all’utente.
Senza Assetic, basta servire direttamente i file che sono memorizzati nell’applicazione:
• Twig
<script src="{{ asset(’js/script.js’) }}" type="text/javascript" />
• PHP
<script src="<?php echo $view[’assets’]->getUrl(’js/script.js’) ?>"
type="text/javascript" />
Ma con Assetic, è possibile manipolare queste risorse nel modo che si preferisce (o caricarle da qualunque parte)
prima di servirli. Questo significa che si può:
• Minimizzare e combinare tutti i file CSS e JS
• Eseguire tutti (o solo alcuni) dei file CSS o JS attraverso una sorta di compilatore, come LESS, SASS o
CoffeeScript
• Eseguire ottimizzazioni delle immagini
Risorse
L’utilizzo di Assetic consente molti vantaggi rispetto a servire direttamente i file. I file non devono essere memorizzati dove vengono serviti e possono provenire da varie fonti come quelle all’interno di un bundle:
• Twig
{% javascripts
’@AcmeFooBundle/Resources/public/js/*’
%}
<script type="text/javascript" src="{{ asset_url }}"></script>
{% endjavascripts %}
• PHP
<?php foreach ($view[’assetic’]->javascripts(
array(’@AcmeFooBundle/Resources/public/js/*’)) as $url): ?>
<script type="text/javascript" src="<?php echo $view->escape($url) ?>"></script>
<?php endforeach; ?>
3.1. Ricettario
259
Symfony2 documentation Documentation, Release 2
Tip: Per i fogli di stile CSS, è possibile utilizzare le stesse metodologie viste in questo articolo, ma con il tag
stylesheets:
• Twig
{% stylesheets
’@AcmeFooBundle/Resources/public/css/*’
%}
<link rel="stylesheet" href="{{ asset_url }}" />
{% endstylesheets %}
• PHP
<?php foreach ($view[’assetic’]->stylesheets(
array(’@AcmeFooBundle/Resources/public/css/*’)) as $url): ?>
<link rel="stylesheet" href="<?php echo $view->escape($url) ?>" />
<?php endforeach; ?>
In questo esempio, tutti i file nella cartella Resources/public/js/ di AcmeFooBundle verranno caricati
e serviti da una posizione diversa. Il tag effettivamente reso potrebbe assomigliare a:
<script src="/app_dev.php/js/abcd123.js"></script>
Note: Questo è un punto fondamentale: una volta che si lascia gestire le risorse ad Assetic, i file vengono serviti
da una posizione diversa. Questo può causare problemi con i file CSS che fanno riferimento a immagini tramite il
loro percorso relativo. Comunque, il problema può essere risolto utilizzando il filtro cssrewrite, che aggiorna
i percorsi nei file CSS per riflettere la loro nuova posizione.
Combinare le risorse
È anche possibile combinare più file in uno. Questo aiuta a ridurre il numero delle richieste HTTP, una cosa molto
utile per le prestazioni front end. Permette anche di mantenere i file più facilmente, dividendoli in gruppi maggiormente gestibili. Questo può contribuire alla riusabilità in quanto si possono facilmente dividere file specifici
del progetto da quelli che possono essere utilizzati in altre applicazioni, ma servendoli ancora come un unico file:
• Twig
{% javascripts
’@AcmeFooBundle/Resources/public/js/*’
’@AcmeBarBundle/Resources/public/js/form.js’
’@AcmeBarBundle/Resources/public/js/calendar.js’
%}
<script src="{{ asset_url }}"></script>
{% endjavascripts %}
• PHP
<?php foreach ($view[’assetic’]->javascripts(
array(’@AcmeFooBundle/Resources/public/js/*’,
’@AcmeBarBundle/Resources/public/js/form.js’,
’@AcmeBarBundle/Resources/public/js/calendar.js’)) as $url): ?>
<script src="<?php echo $view->escape($url) ?>"></script>
<?php endforeach; ?>
Nell’ambiente dev, ciascun file è ancora servito individualmente, in modo che sia possibile eseguire il debug dei
problemi più facilmente. Tuttavia, nell’ambiente prod, questo verrà reso come un unico tag script.
Tip: Se si è nuovi con Assetic e si prova a utilizzare la propria applicazione nell’ambiente prod (utilizzando il
controllore app.php), probabilmente si vedrà che mancano tutti i CSS e JS. Non bisogna preoccuparsi! Accade
260
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
di proposito. Per informazioni dettagliate sull’utilizzo di Assetic in ambiente prod, vedere Copiare i file delle
risorse.
La combinazione dei file non si applica solo ai propri file. Si può anche utilizzare Assetic per combinare risorse
di terze parti (come jQuery) con i propri, in un singolo file:
• Twig
{% javascripts
’@AcmeFooBundle/Resources/public/js/thirdparty/jquery.js’
’@AcmeFooBundle/Resources/public/js/*’
%}
<script src="{{ asset_url }}"></script>
{% endjavascripts %}
• PHP
<?php foreach ($view[’assetic’]->javascripts(
array(’@AcmeFooBundle/Resources/public/js/thirdparty/jquery.js’,
’@AcmeFooBundle/Resources/public/js/*’)) as $url): ?>
<script src="<?php echo $view->escape($url) ?>"></script>
<?php endforeach; ?>
Filtri
Una volta che vengono gestite da Assetic, è possibile applicare i filtri alle proprie risorse prima che siano servite.
Questi includono filtri che comprimono l’output delle proprie risorse per ottenere file di dimensioni inferiori
(e migliore ottimizzazione nel frontend). Altri filtri possono compilare i file JavaScript da file CoffeeScript e
processare SASS in CSS. Assetic ha una lunga lista di filtri disponibili.
Molti filtri non fanno direttamente il lavoro, ma usano librerie di terze parti per fare il lavoro pesante. Questo
significa che spesso si avrà la necessità di installare una libreria di terze parti per usare un filtro. Il grande vantaggio
di usare Assetic per invocare queste librerie (invece di utilizzarle direttamente) è che invece di doverle eseguire
manualmente dopo aver lavorato sui file, sarà Assetic a prendersene cura, rimuovendo del tutto questo punto dal
processo di sviluppo e di pubblicazione.
Per usare un filtro, è necessario specificarlo nella configurazione di Assetic. L’aggiunta di un filtro qui non significa
che venga utilizzato: significa solo che è disponibile per l’uso.
Per esempio, per usare il compressore JavaScript YUI bisogna aggiungere la configurazione seguente:
• YAML
# app/config/config.yml
assetic:
filters:
yui_js:
jar: "%kernel.root_dir%/Resources/java/yuicompressor.jar"
• XML
<!-- app/config/config.xml -->
<assetic:config>
<assetic:filter
name="yui_js"
jar="%kernel.root_dir%/Resources/java/yuicompressor.jar" />
</assetic:config>
• PHP
// app/config/config.php
$container->loadFromExtension(’assetic’, array(
’filters’ => array(
3.1. Ricettario
261
Symfony2 documentation Documentation, Release 2
’yui_js’ => array(
’jar’ => ’%kernel.root_dir%/Resources/java/yuicompressor.jar’,
),
),
));
Ora, per utilizzare effettivamente il filtro su un gruppo di file JavaScript, bisogna aggiungerlo nel template:
• Twig
{% javascripts
’@AcmeFooBundle/Resources/public/js/*’
filter=’yui_js’
%}
<script src="{{ asset_url }}"></script>
{% endjavascripts %}
• PHP
<?php foreach ($view[’assetic’]->javascripts(
array(’@AcmeFooBundle/Resources/public/js/*’),
array(’yui_js’)) as $url): ?>
<script src="<?php echo $view->escape($url) ?>"></script>
<?php endforeach; ?>
Una guida più dettagliata sulla configurazione e l’utilizzo dei filtri di Assetic, oltre a dettagli della modalità di
debug di Assetic, si trova in Minimizzare i file JavaScript e i fogli di stile con YUI Compressor.
Controllare l’URL utilizzato
Se lo si desidera, è possibile controllare gli URL che produce Assetic. Questo è fatto dal template ed è relativo
alla radice del documento pubblico:
• Twig
{% javascripts
’@AcmeFooBundle/Resources/public/js/*’
output=’js/compiled/main.js’
%}
<script src="{{ asset_url }}"></script>
{% endjavascripts %}
• PHP
<?php foreach ($view[’assetic’]->javascripts(
array(’@AcmeFooBundle/Resources/public/js/*’),
array(),
array(’output’ => ’js/compiled/main.js’)
) as $url): ?>
<script src="<?php echo $view->escape($url) ?>"></script>
<?php endforeach; ?>
Note: Symfony contiene anche un metodo per accelerare la cache, in cui l’URL finale generato da Assetic
contiene un parametro di query che può essere incrementato tramite la configurazione di ogni pubblicazione. Per
ulteriori informazioni, vedere l’opzione di configurazione assets_version.
Copiare i file delle risorse
Nell’ambiente dev, Assetic genera persorsi a file CSS e JavaScript che non esistono fisicamente sul computer.
Ma vengono resi comunque perché un controllore interno di Symfony apre i file e restituisce indietro il contenuto
(dopo aver eseguito eventuali filtri).
262
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
Questo tipo di pubblicazione dinamica delle risorse che sono state elaborate, è ottima perché significa che si può
immediatamente vedere il nuovo stato di tutti i file delle risorse modificate. È anche un male, perché può essere
molto lento. Se si stanno usando molti filtri, potrebbe essere addirittura frustrante.
Fortunatamente, Assetic fornisce un modo per copiare le proprie risorse in file reali, anziché farli generare dinamicamente.
Copiare i file delle risorse nell’ambiente prod
Nell’ambiente prod, i file JS e CSS sono rappresentati da un unico tag. In altre parole, invece di vedere ogni file
JavaScript che che si sta includendo nei sorgenti, è probabile che si veda qualcosa di questo tipo:
<script src="/app_dev.php/js/abcd123.js"></script>
Questo file in realtà non esiste, né viene reso dinamicamente da Symfony (visto che i file di risorse sono
nell’ambiente dev). Lasciare generare a Symfony questi file dinamicamente in un ambiente di produzione sarebbe
troppo lento.
Invece, ogni volta che si utilizza l’applicazione nell’ambiente prod (e quindi, ogni volta che si fa un nuovo
rilascio), è necessario eseguire il seguente task:
php app/console assetic:dump --env=prod --no-debug
Questo genererà fisicamente e scriverà ogni file di cui si ha bisogno (ad esempio /js/abcd123.js). Se si
aggiorna una qualsiasi delle risorse, sarà necessario eseguirlo di nuovo per rigenerare il file.
Copiare i file delle risorse nell’ambiente dev
Per impostazione predefinita, ogni percorso generato della risorsa nell’ambiente dev è gestito dinamicamente
da Symfony. Questo non ha alcun svantaggio (è possibile visualizzare immediatamente le modifiche), salvo che
le risorse verranno caricate sensibilmente lente. Se si ritiene che le risorse vengano caricate troppo lentamente,
seguire questa guida.
In primo luogo, dire a Symfony di smettere di cercare di elaborare questi file in modo dinamico. Fare la seguente
modifica nel file config_dev.yml:
• YAML
# app/config/config_dev.yml
assetic:
use_controller: false
• XML
<!-- app/config/config_dev.xml -->
<assetic:config use-controller="false" />
• PHP
// app/config/config_dev.php
$container->loadFromExtension(’assetic’, array(
’use_controller’ => false,
));
Poi, dato che Symfony non generà più queste risorse dinamicamente, bisognerà copiarle manualmente. Per fare
ciò, eseguire il seguente comando:
php app/console assetic:dump
Questo scrive fisicamente tutti i file delle risorse necessari per l’ambiente dev. Il grande svantaggio è che è
necessario eseguire questa operazione ogni volta che si aggiorna una risorsa. Per fortuna, passando l’opzione
--watch, il comando rigenererà automaticamente le risorse che sono cambiate:
3.1. Ricettario
263
Symfony2 documentation Documentation, Release 2
php app/console assetic:dump --watch
Dal momento che l’esecuzione di questo comando nell’ambiente dev può generare molti file, di solito è una
buona idea far puntare i file con le risorse generate in una cartella separata (ad esempio /js/compiled), per
mantenere ordinate le cose:
• Twig
{% javascripts
’@AcmeFooBundle/Resources/public/js/*’
output=’js/compiled/main.js’
%}
<script src="{{ asset_url }}"></script>
{% endjavascripts %}
• PHP
<?php foreach ($view[’assetic’]->javascripts(
array(’@AcmeFooBundle/Resources/public/js/*’),
array(),
array(’output’ => ’js/compiled/main.js’)
) as $url): ?>
<script src="<?php echo $view->escape($url) ?>"></script>
<?php endforeach; ?>
3.1.8 Minimizzare i file JavaScript e i fogli di stile con YUI Compressor
Yahoo! mette a disposizione un eccellente strumento per minimizzare i file JavaScipt e i fogli di stile, che così
possono viaggiare più velocemente sulla rete: lo YUI Compressor. Grazie ad Assetic utilizzare questo strumento
è semplicissimo.
Scaricare il JAR di YUI Compressor
L’YUI Compressor è scritto in Java e viene distribuito in formato JAR. Si dovrà scaricare il file JAR e salvarlo in
app/Resources/java/yuicompressor.jar.
Configurare i filtri per YUI
È necessario configurare due filtri Assetic all’interno dell’applicazione. Uno per minimizzare i file JavaScript e
uno per minimizzare i fogli di stile con YUI Compressor:
• YAML
# app/config/config.yml
assetic:
filters:
yui_css:
jar: "%kernel.root_dir%/Resources/java/yuicompressor.jar"
yui_js:
jar: "%kernel.root_dir%/Resources/java/yuicompressor.jar"
• XML
<!-- app/config/config.xml -->
<assetic:config>
<assetic:filter
name="yui_css"
jar="%kernel.root_dir%/Resources/java/yuicompressor.jar" />
<assetic:filter
name="yui_js"
264
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
jar="%kernel.root_dir%/Resources/java/yuicompressor.jar" />
</assetic:config>
• PHP
// app/config/config.php
$container->loadFromExtension(’assetic’, array(
’filters’ => array(
’yui_css’ => array(
’jar’ => ’%kernel.root_dir%/Resources/java/yuicompressor.jar’,
),
’yui_js’ => array(
’jar’ => ’%kernel.root_dir%/Resources/java/yuicompressor.jar’,
),
),
));
Dall’applicazione si ha ora accesso a due nuovi filtri di Assetic: yui_css e yui_js. Questi filtri utilizzeranno
YUI Compressor per minimizzare, rispettivamente, i fogli di stile e i file JavaScript.
Minimizzare le risorse
YUI Compressor è stato configurato, ma, prima di poter vedere i risultati, è necessario applicare i filtri alle risorse.
Visto che le risorse fanno parte del livello della vista, questo lavoro dovrà essere svolto nei template:
• Twig
{% javascripts ’@AcmeFooBundle/Resources/public/js/*’ filter=’yui_js’ %}
<script src="{{ asset_url }}"></script>
{% endjavascripts %}
• PHP
<?php foreach ($view[’assetic’]->javascripts(
array(’@AcmeFooBundle/Resources/public/js/*’),
array(’yui_js’)) as $url): ?>
<script src="<?php echo $view->escape($url) ?>"></script>
<?php endforeach; ?>
Note: Il precedente esempio presuppone che ci sia un bundle chiamato AcmeFooBundle e che i file JavaScript
si trovino nella cartella Resources/public/js all’interno del bundle. È comunque possibile includere file
JavaScript che si trovino in posizioni differenti.
Con l’aggiunta del filtro yui_js dell’esempio precedente, i file minimizzati viaggeranno molto più velocemente
sulla rete. Lo stesso procedimento può essere ripetuto per minimizzare i fogli di stile.
• Twig
{% stylesheets ’@AcmeFooBundle/Resources/public/css/*’ filter=’yui_css’ %}
<link rel="stylesheet" type="text/css" media="screen" href="{{ asset_url }}" />
{% endstylesheets %}
• PHP
<?php foreach ($view[’assetic’]->stylesheets(
array(’@AcmeFooBundle/Resources/public/css/*’),
array(’yui_css’)) as $url): ?>
<link rel="stylesheet" type="text/css" media="screen" href="<?php echo $view->escape($url) ?>
<?php endforeach; ?>
3.1. Ricettario
265
Symfony2 documentation Documentation, Release 2
Disabilitare la minimizzazione in modalità debug
I file JavaScript e i fogli di stile minimizzati sono difficili da leggere e ancora più difficili da correggere. Per questo
motivo Assetic permette di disabilitare determinati filtri quando l’applicazione viene eseguita in modalità debug.
Mettendo il prefisso punto interrogativo ? al nome dei filtri, si chiede ad Assetic di applicarli solamente quando
la modalità debug è inattiva.
• Twig
{% javascripts ’@AcmeFooBundle/Resources/public/js/*’ filter=’?yui_js’ %}
<script src="{{ asset_url }}"></script>
{% endjavascripts %}
• PHP
<?php foreach ($view[’assetic’]->javascripts(
array(’@AcmeFooBundle/Resources/public/js/*’),
array(’?yui_js’)) as $url): ?>
<script src="<?php echo $view->escape($url) ?>"></script>
<?php endforeach; ?>
3.1.9 Usare Assetic per l’ottimizzazione delle immagini con le funzioni di Twig
Tra i vari filtri di Assetic, ve ne sono quattro che possono essere utilizzati per ottimizzare le immagini al volo.
Ciò permette di avere immagini di dimensioni inferiori, senza ricorrere a un editor grafico per ogni modifica. Il
risultato dei filtri può essere messo in cache e usato in fase di produzione, in modo da eliminare problemi di
prestazioni per l’utente finale.
Usare Jpegoptim
Jpegoptim è uno strumento per ottimizzare i file JPEG. Per poterlo usare, si aggiunge il seguente codice alla
configurazione di Assetic:
• YAML
# app/config/config.yml
assetic:
filters:
jpegoptim:
bin: percorso/per/jpegoptim
• XML
<!-- app/config/config.xml -->
<assetic:config>
<assetic:filter
name="jpegoptim"
bin="percorso/per/jpegoptim" />
</assetic:config>
• PHP
// app/config/config.php
$container->loadFromExtension(’assetic’, array(
’filters’ => array(
’jpegoptim’ => array(
’bin’ => ’percorso/per/jpegoptim’,
),
),
));
266
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
Note: Per poter utilizzare jpegoptim è necessario che sia già installato sul proprio computer. L’opzione bin
indica la posizione del programma eseguibile.
Sarà ora possibile usarlo nei propri template:
• Twig
{% image ’@AcmeFooBundle/Resources/public/images/esempio.jpg’
filter=’jpegoptim’ output=’/images/esempio.jpg’
%}
<img src="{{ asset_url }}" alt="Esempio"/>
{% endimage %}
• PHP
<?php foreach ($view[’assetic’]->images(
array(’@AcmeFooBundle/Resources/public/images/esempio.jpg’),
array(’jpegoptim’)) as $url): ?>
<img src="<?php echo $view->escape($url) ?>" alt="Esempio"/>
<?php endforeach; ?>
Rimozione dei dati EXIF
Senza ulteirori opzioni, questo filtro rimuove solamente le meta-informazioni contenute nel file. I dati EXIF e i
commenti non vengono eliminati: è comunque possibile rimuoverli usando l’opzione strip_all:
• YAML
# app/config/config.yml
assetic:
filters:
jpegoptim:
bin: percorso/per/jpegoptim
strip_all: true
• XML
<!-- app/config/config.xml -->
<assetic:config>
<assetic:filter
name="jpegoptim"
bin="percorso/per/jpegoptim"
strip_all="true" />
</assetic:config>
• PHP
// app/config/config.php
$container->loadFromExtension(’assetic’, array(
’filters’ => array(
’jpegoptim’ => array(
’bin’ => ’percorso/per/jpegoptim’,
’strip_all’ => ’true’,
),
),
));
3.1. Ricettario
267
Symfony2 documentation Documentation, Release 2
Diminuire la qualità massima
Senza ulteriori opzioni, la qualità dell’immagine JPEG non viene modificata. È però possibile ridurre ulteriormente la dimensione del file, configurando il livello di qualità massima per le immagini a un livello inferiore di
quello delle immagini stesse. Ovviamente, questo altererà la qualità dell’immagine:
• YAML
# app/config/config.yml
assetic:
filters:
jpegoptim:
bin: percorso/per/jpegoptim
max: 70
• XML
<!-- app/config/config.xml -->
<assetic:config>
<assetic:filter
name="jpegoptim"
bin="percorso/per/jpegoptim"
max="70" />
</assetic:config>
• PHP
// app/config/config.php
$container->loadFromExtension(’assetic’, array(
’filters’ => array(
’jpegoptim’ => array(
’bin’ => ’percorso/per/jpegoptim’,
’max’ => ’70’,
),
),
));
Abbreviare la sintassi: le funzioni di Twig
Se si utilizza Twig, è possibile inserire tutte queste opzioni con una sintassi più concisa, abilitando alcune speciali
funzioni di Twig. Si inizia modificando la configurazione, come di seguito:
• YAML
# app/config/config.yml
assetic:
filters:
jpegoptim:
bin: percorso/per/jpegoptim
twig:
functions:
jpegoptim: ~
• XML
<!-- app/config/config.xml -->
<assetic:config>
<assetic:filter
name="jpegoptim"
bin="percorso/per/jpegoptim" />
<assetic:twig>
<assetic:twig_function
name="jpegoptim" />
268
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
</assetic:twig>
</assetic:config>
• PHP
// app/config/config.php
$container->loadFromExtension(’assetic’, array(
’filters’ => array(
’jpegoptim’ => array(
’bin’ => ’percorso/per/jpegoptim’,
),
),
’twig’ => array(
’functions’ => array(’jpegoptim’),
),
),
));
A questo punto il template di Twig può essere modificato nel seguente modo:
<img src="{{ jpegoptim(’@AcmeFooBundle/Resources/public/images/esempio.jpg’) }}"
alt="Esempio"/>
È possibile specificare la cartella di output nel seguente modo:
• YAML
# app/config/config.yml
assetic:
filters:
jpegoptim:
bin: percorso/per/jpegoptim
twig:
functions:
jpegoptim: { output: images/*.jpg }
• XML
<!-- app/config/config.xml -->
<assetic:config>
<assetic:filter
name="jpegoptim"
bin="percorso/per/jpegoptim" />
<assetic:twig>
<assetic:twig_function
name="jpegoptim"
output="images/*.jpg" />
</assetic:twig>
</assetic:config>
• PHP
// app/config/config.php
$container->loadFromExtension(’assetic’, array(
’filters’ => array(
’jpegoptim’ => array(
’bin’ => ’percorso/per/jpegoptim’,
),
),
’twig’ => array(
’functions’ => array(
’jpegoptim’ => array(
output => ’images/*.jpg’
),
),
3.1. Ricettario
269
Symfony2 documentation Documentation, Release 2
),
));
3.1.10 Applicare i filtri di Assetic a file con specifiche estensioni
I filtri di Assetic possono essere applicati a singoli file, gruppi di file o anche, come vedremo, a file che hanno una
specifica estensione. Per mostrare l’utilizzo di ogni opzione, supponiamo di voler usare il filtro CoffeeScript di
Assetic che compila i file CoffeeScript in Javascript.
La configurazione prevede semplicemente di definire i percorsi per coffee e per node. I valori predefiniti sono
/usr/bin/coffee e /usr/bin/node:
• YAML
# app/config/config.yml
assetic:
filters:
coffee:
bin: /usr/bin/coffee
node: /usr/bin/node
• XML
<!-- app/config/config.xml -->
<assetic:config>
<assetic:filter
name="coffee"
bin="/usr/bin/coffee"
node="/usr/bin/node" />
</assetic:config>
• PHP
// app/config/config.php
$container->loadFromExtension(’assetic’, array(
’filters’ => array(
’coffee’ => array(
’bin’ => ’/usr/bin/coffee’,
’node’ => ’/usr/bin/node’,
),
),
));
Filtrare un singolo file
In questo modo sarà possibile inserire un singolo file CoffeScript nel template, come se fosse un normale
JavaScript:
• Twig
{% javascripts ’@AcmeFooBundle/Resources/public/js/esempio.coffee’
filter=’coffee’
%}
<script src="{{ asset_url }}" type="text/javascript"></script>
{% endjavascripts %}
• PHP
<?php foreach ($view[’assetic’]->javascripts(
array(’@AcmeFooBundle/Resources/public/js/esempio.coffee’),
array(’coffee’)) as $url): ?>
270
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
<script src="<?php echo $view->escape($url) ?>" type="text/javascript"></script>
<?php endforeach; ?>
Questo è tutto quel che serve per compilare il file CoffeeScript e restituirlo come un normale JavaScript.
Filtrare file multpili
È anche possibile combinare diversi file CoffeeScript in un singolo file:
• Twig
{% javascripts ’@AcmeFooBundle/Resources/public/js/esempio.coffee’
’@AcmeFooBundle/Resources/public/js/altro.coffee’
filter=’coffee’
%}
<script src="{{ asset_url }}" type="text/javascript"></script>
{% endjavascripts %}
• PHP
<?php foreach ($view[’assetic’]->javascripts(
array(’@AcmeFooBundle/Resources/public/js/esempio.coffee’,
’@AcmeFooBundle/Resources/public/js/altro.coffee’),
array(’coffee’)) as $url): ?>
<script src="<?php echo $view->escape($url) ?>" type="text/javascript"></script>
<?php endforeach; ?>
Tutti i file verranno restituiti e compilati in un unico, regolare file JavaScript.
Filtrare in base all’estensione del file
Uno dei grandi vantaggi nell’utilizzo di Assetic è quello di ridurre il numero di file di risorse, riducendo così
le richieste HTTP. Per massimizzarne i vantaggi, sarebbe utile combinare insieme tutti i file JavaScript e quelli
CoffeeScript in uno unico, visto che verranno tutti serviti come file JavaScript. Sfortunatamente non è possibile aggiungere semplicemente un file JavaScript ai file precedenti, per via del fatto che il file JavaScript non
supererebbe la compilazione di CoffeeScript.
Questo problema può essere ovviato utilizzando l’opzione apply_to nella configurazione, in modo da specificare che il filtro dovrà essere applicato solo ai file con una determinata estensione. In questo caso si dovrà
specificare che il filtro Coffee dovrà applicarsi a tutti e soli i file .coffee:
• YAML
# app/config/config.yml
assetic:
filters:
coffee:
bin: /usr/bin/coffee
node: /usr/bin/node
apply_to: "\.coffee$"
• XML
<!-- app/config/config.xml -->
<assetic:config>
<assetic:filter
name="coffee"
bin="/usr/bin/coffee"
node="/usr/bin/node"
apply_to="\.coffee$" />
</assetic:config>
3.1. Ricettario
271
Symfony2 documentation Documentation, Release 2
• PHP
// app/config/config.php
$container->loadFromExtension(’assetic’, array(
’filters’ => array(
’coffee’ => array(
’bin’ => ’/usr/bin/coffee’,
’node’ => ’/usr/bin/node’,
’apply_to’ => ’\.coffee$’,
),
),
));
In questo modo non è più necessario specificare il filtro coffee nel template. È anche possibile elencare i normali
file JavaScript, i quali verranno combinati e restituiti come un unico file JavaScript (e in modo tale che i soli file
.coffee venagano elaborati dal filtro CoffeeScript):
• Twig
{% javascripts ’@AcmeFooBundle/Resources/public/js/esempio.coffee’
’@AcmeFooBundle/Resources/public/js/altro.coffee’
’@AcmeFooBundle/Resources/public/js/regolare.js’
%}
<script src="{{ asset_url }}" type="text/javascript"></script>
{% endjavascripts %}
• PHP
<?php foreach ($view[’assetic’]->javascripts(
array(’@AcmeFooBundle/Resources/public/js/esempio.coffee’,
’@AcmeFooBundle/Resources/public/js/altro.coffee’,
’@AcmeFooBundle/Resources/public/js/regolare.js’),
as $url): ?>
<script src="<?php echo $view->escape($url) ?>" type="text/javascript"></script>
<?php endforeach; ?>
3.1.11 Come gestire il caricamento di file con Doctrine
La gestione del caricamento dei file tramite le entità di Doctrine non è diversa da qualsiasi altro tipo di caricamento.
In altre parole si è liberi di spostare il file nel controllore dopo aver gestito l’invio tramite una form. Per alcuni
esempi in merito fare riferimento alla pagina dedicata ai file type.
Volendo è anche possibile integrare il caricamento del file nel ciclo di vita di un’entità (creazione, modifica e
cancellazione). In questo caso, nel momento in cui l’entità viene creata, modificata, o cancellata da Doctrine,
il caricamento del file o il processo di rimozione verranno azionati automaticamente (senza dover fare nulla nel
controllore);
Per far funzionare tutto questo è necessario conoscere alcuni dettagli che verranno analizzati in questa sezione del
ricettario.
Preparazione
Innanzitutto creare una semplice classe entità di Doctrine, su cui lavorare:
// src/Acme/DemoBundle/Entity/Document.php
namespace Acme\DemoBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/**
* @ORM\Entity
272
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
*/
class Document
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
public $id;
/**
* @ORM\Column(type="string", length=255)
* @Assert\NotBlank
*/
public $name;
/**
* @ORM\Column(type="string", length=255, nullable=true)
*/
public $path;
public function getAbsolutePath()
{
return null === $this->path ? null : $this->getUploadRootDir().’/’.$this->path;
}
public function getWebPath()
{
return null === $this->path ? null : $this->getUploadDir().’/’.$this->path;
}
protected function getUploadRootDir()
{
// il percorso assoluto della cartella dove i documenti caricati verranno salvati
return __DIR__.’/../../../../web/’.$this->getUploadDir();
}
protected function getUploadDir()
{
// get rid of the __DIR__ so it doesn’t screw when displaying uploaded doc/image in the vi
return ’uploads/documents’;
}
}
L’entità Document ha un nome che viene associato al file. La proprietà path contiene il percorso relativo al file
e viene memorizzata sul database. Il metodo getAbsolutePath() è un metodo di supporto che restituisce il
percorso assoluto al file mentre il getWebPath() è un altro metodo di supporto che restituisce il percorso web
che può essere utilizzato nei template per collegare il file caricato.
Tip: Se non è già stato fatto, si consiglia la lettura della documentazione relativa ai file type per comprendere
meglio come funziona il caricamento di base.
Note: Se si stanno utilizzando le annotazioni per specificare le regole di validazione (come nell’esempio proposto), assicurarsi di abilitare la validazione tramite annotazioni (confrontare configurazione della validazione).
Per gestire il file attualmente caricato tramite il form utilizzare un campo file “virtuale”. Per esempio, se si sta
realizzando il form direttamente nel controller, potrebbe essere come il seguente:
public function uploadAction()
{
3.1. Ricettario
273
Symfony2 documentation Documentation, Release 2
// ...
$form = $this->createFormBuilder($document)
->add(’name’)
->add(’file’)
->getForm()
;
// ...
}
In seguito, creare la proprietà nella classe Document aggiungendo alcune regole di validazione:
// src/Acme/DemoBundle/Entity/Document.php
// ...
class Document
{
/**
* @Assert\File(maxSize="6000000")
*/
public $file;
// ...
}
Note: Grazie al fatto che si utilizza il vincolo File, Symfony2 ipotizzerà automaticamente che il campo del
form sia un file upload. È per questo motivo che non si rende necessario impostarlo esplicitamente al momento di
creazione del form precedente (->add(’file’)).
Il controllore seguente mostra come gestire l’intero processo:
use Acme\DemoBundle\Entity\Document;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
// ...
/**
* @Template()
*/
public function uploadAction()
{
$document = new Document();
$form = $this->createFormBuilder($document)
->add(’name’)
->add(’file’)
->getForm()
;
if ($this->getRequest()->getMethod() === ’POST’) {
$form->bindRequest($this->getRequest());
if ($form->isValid()) {
$em = $this->getDoctrine()->getEntityManager();
$em->persist($document);
$em->flush();
$this->redirect($this->generateUrl(’...’));
}
}
return array(’form’ => $form->createView());
}
274
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
Note: Realizzando il template non dimenticarsi di impostare l’attributo enctype:
<h1>Upload File</h1>
<form action="#" method="post" {{ form_enctype(form) }}>
{{ form_widget(form) }}
<input type="submit" value="Upload Document" />
</form>
Il controllore precedente memorizzerà automaticamente l’entità Document con il nome inviato, ma non farà
nulla relativamente al file e la proprietà path sarà vuota.
Un modo semplice per gestire il caricamento del file è quello si spostarlo appena prima che l’entità venga memorizzata, impostando la proprietà path in modo corretto. Iniziare invocando un nuovo metodo upload(), che si
creerà tra poco per gestire il caricamento del file, nella classe Document:
if ($form->isValid()) {
$em = $this->getDoctrine()->getEntityManager();
$document->upload();
$em->persist($document);
$em->flush();
$this->redirect(’...’);
}
Il metodo upload() sfrutterà l’oggetto Symfony\Component\HttpFoundation\File\UploadedFile
che è quanto viene restituito dopo l’invio di un campo di tipo file:
public function upload()
{
// la proprietà file può essere vuota se il campo non è obbligatorio
if (null === $this->file) {
return;
}
// si utilizza il nome originale del file ma è consigliabile
// un processo di sanitizzazione almeno per evitare problemi di sicurezza
// move accetta come parametri la cartella di destinazione e il nome del file di destinazione
$this->file->move($this->getUploadRootDir(), $this->file->getClientOriginalName());
// impostare la proprietà del percorso al nome del file dove è stato salvato il file
$this->path = $this->file->getClientOriginalName();
// impostare a null la proprietà file dato che non è più necessaria
$this->file = null;
}
Utilizzare i callback del ciclo di vita delle entità
Anche se l’implementazione funziona, essa presenta un grave difetto: cosa succede se si verifica un problema
mentre l’entità viene memorizzata? Il file potrebbe già essere stato spostato nella sua posizione finale anche se la
proprietà path dell’entità non fosse stata impostata correttamente.
Per evitare questo tipo di problemi, è necessario modificare l’implementazione in modo tale da rendere atomiche
le azioni del database e dello spostamento del file: se si verificasse un problema durante la memorizzazione
dell’entità, o se il file non potesse essere spostato, allora non dovrebbe succedere niente.
3.1. Ricettario
275
Symfony2 documentation Documentation, Release 2
Per fare questo, è necessario spostare il file nello stesso momento in cui Doctrine memorizza l’entità sul database.
Questo può essere fatto agganciandosi a un callback del ciclo di vita dell’entità:
/**
* @ORM\Entity
* @ORM\HasLifecycleCallbacks
*/
class Document
{
}
Quindi, rifattorizzare la classe Document, per sfruttare i vantaggi dei callback:
use Symfony\Component\HttpFoundation\File\UploadedFile;
/**
* @ORM\Entity
* @ORM\HasLifecycleCallbacks
*/
class Document
{
/**
* @ORM\PrePersist()
* @ORM\PreUpdate()
*/
public function preUpload()
{
if (null !== $this->file) {
// fare qualsiasi cosa si voglia per generare un nome univoco
$this->path = uniqid().’.’.$this->file->guessExtension();
}
}
/**
* @ORM\PostPersist()
* @ORM\PostUpdate()
*/
public function upload()
{
if (null === $this->file) {
return;
}
// se si verifica un errore mentre il file viene spostato viene
// lanciata automaticamente un’eccezione da move(). Questo eviterà
// la memorizzazione dell’entità su database in caso di errore
$this->file->move($this->getUploadRootDir(), $this->path);
unset($this->file);
}
/**
* @ORM\PostRemove()
*/
public function removeUpload()
{
if ($file = $this->getAbsolutePath()) {
unlink($file);
}
}
}
La classe ora ha tutto quello che serve: genera un nome di file univoco prima della memorizzazione, sposta il file
dopo la memorizzazione, rimuove il file se l’entità viene eliminata.
276
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
Note: Le callback @ORM\PrePersist() e @ORM\PostPersist() scattano prima e dopo la memorizzazione di un’entità sul database. Parallelamente le callback @ORM\PreUpdate() e @ORM\PostUpdate()
vengono invocate quanto l’entità viene modificata.
Caution: I callback PreUpdate e PostUpdate scattano solamente se c’è una modifica a uno dei campi
dell’entità memorizzata. Questo significa che, se si modifica solamente la proprietà $file, questi eventi non
verranno invocati, dato che la proprietà in questione non viene memorizzata direttamente tramite Doctrine.
Una soluzione potrebbe essere quella di utilizzare un campo updated memorizzato tramite Doctrine, da
modificare manualmente in caso di necessità per la sostituzione del file.
Usare id come nome del file
Volendo usare l’id come nome del file, l’implementazione è leggermente diversa, dato che sarebbe necessario
memorizzare l’estensione nella proprietà path, invece che nell’attuale nome del file:
use Symfony\Component\HttpFoundation\File\UploadedFile;
/**
* @ORM\Entity
* @ORM\HasLifecycleCallbacks
*/
class Document
{
/**
* @ORM\PrePersist()
* @ORM\PreUpdate()
*/
public function preUpload()
{
if (null !== $this->file) {
$this->path = $this->file->guessExtension();
}
}
/**
* @ORM\PostPersist()
* @ORM\PostUpdate()
*/
public function upload()
{
if (null === $this->file) {
return;
}
// qui si deve lanciare un’eccezione se il file non può essere spostato
// per fare in modo che l’entità non possa essere memorizzata a database
$this->file->move($this->getUploadRootDir(), $this->id.’.’.$this->file->guessExtension());
unset($this->file);
}
/**
* @ORM\PostRemove()
*/
public function removeUpload()
{
if ($file = $this->getAbsolutePath()) {
unlink($file);
3.1. Ricettario
277
Symfony2 documentation Documentation, Release 2
}
}
public function getAbsolutePath()
{
return null === $this->path ? null : $this->getUploadRootDir().’/’.$this->id.’.’.$this->pa
}
}
3.1.12 Estensioni di Doctrine: Timestampable: Sluggable, Translatable, ecc.
Doctrine2 è molto flessibile e la comunità ha già creato una serie di utili estensioni di Doctrine, per aiutare nei
compiti più comuni relativi alle entità.
In paricolare, il bundle DoctrineExtensionsBundle fornisce integrazione con una libreria di estensioni, che offre i
comportamenti Sluggable, Translatable, Timestampable, Loggable e Tree.
Si veda il bundle per maggiori dettagli.
3.1.13 Registrare ascoltatori e sottoscrittori di eventi
Doctrine include un ricco sistema di eventi, lanciati quasi ogni volta che accade qualcosa nel sistema. Per lo
sviluppatore, significa la possibilità di creare servizi arbitrari e dire a Doctrine di notificare questi oggetti ogni
volta che accade una certa azione (p.e. prePersist). Questo può essere utile, per esempio, per creare un indice
di ricerca indipendente ogni volta che un oggetto viene salvato nel database.
Doctrine defininsce due tipi di oggetti che possono ascoltare eventi: ascoltatori e sottoscrittori. Sono simili tra
loro, ma gli ascoltatori sono leggermente più semplificati. Per approfondimenti, vedere The Event System sul sito
di Doctrine.
Configurare ascoltatori e sottoscrittori
Per registrare un servizio come ascoltatore o sottoscrittore di eventi, basta assegnarli il tag appropriato. A seconda
del caso, si può agganciare un ascoltatore a ogni connessione DBAL o gestore di entità dell’ORM, oppure solo a
una specifica connessione DBAL e a tutti i gestori di entità che usano tale connessione.
• YAML
doctrine:
dbal:
default_connection: default
connections:
default:
driver: pdo_sqlite
memory: true
services:
my.listener:
class: Acme\SearchBundle\Listener\SearchIndexer
tags:
- { name: doctrine.event_listener, event: postPersist }
my.listener2:
class: Acme\SearchBundle\Listener\SearchIndexer2
tags:
- { name: doctrine.event_listener, event: postPersist, connection: default }
my.subscriber:
class: Acme\SearchBundle\Listener\SearchIndexerSubscriber
tags:
- { name: doctrine.event_subscriber, connection: default }
278
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
• XML
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:doctrine="http://symfony.com/schema/dic/doctrine">
<doctrine:config>
<doctrine:dbal default-connection="default">
<doctrine:connection driver="pdo_sqlite" memory="true" />
</doctrine:dbal>
</doctrine:config>
<services>
<service id="my.listener" class="Acme\SearchBundle\Listener\SearchIndexer">
<tag name="doctrine.event_listener" event="postPersist" />
</service>
<service id="my.listener2" class="Acme\SearchBundle\Listener\SearchIndexer2">
<tag name="doctrine.event_listener" event="postPersist" connection="default" />
</service>
<service id="my.subscriber" class="Acme\SearchBundle\Listener\SearchIndexerSubscriber
<tag name="doctrine.event_subscriber" connection="default" />
</service>
</services>
</container>
Creare la classe dell’ascoltatore
Nell’esempio precedente, è stato configurato un servizio my.listener come ascoltatore dell’evento
postPersist. La classe dietro al servizio deve avere un metodo postPersist, che sarà richiamato al lancio
dell’evento:
// src/Acme/SearchBundle/Listener/SearchIndexer.php
namespace Acme\SearchBundle\Listener;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Acme\StoreBundle\Entity\Product;
class SearchIndexer
{
public function postPersist(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
$entityManager = $args->getEntityManager();
// si potrebbe voler fare qualcosa su un’entità Product
if ($entity instanceof Product) {
// fare qualcosa con l’oggetto Product
}
}
}
In ciascun evento, si ha accesso all’oggetto LifecycleEventArgs, che rende disponibili sia l’oggetto entità
dell’evento che lo stesso gestore di entità.
Una cosa importante da notare è che un ascoltatore ascolterà tutte le entità della propria applicazione. Quindi, se si
vuole gestire solo un tipo specifico di entità (p.e. un’entità Product, ma non un’entità BlogPost), si dovrebbe
verificare il nome della classe dell’entità nel proprio metodo (come precedentemente mostrato).
3.1. Ricettario
279
Symfony2 documentation Documentation, Release 2
3.1.14 Come generare entità da una base dati esistente
Quando si inizia a lavorare su un nuovo progetto, che usa una base dati, si pongono due situazioni diverse. Nella
maggior parte dei casi, il modello della base dati è progettato e costruito da zero. A volte, tuttavia, si inizia con un
modello di base dati esistente e probabilmente non modificabile. Per fortuna, Doctrine dispone di molti strumenti
che aiutano a generare classi del modello da una base dati esistente.
Note: Come dice la documentazione sugli strumenti di Doctrine, il reverse engineering è un processo da eseguire
una sola volta su un progetto. Doctrine è in grado di convertire circa il 70-80% delle informazioni di mappatura
necessarie, in base a campi, indici e vincoli di integrità referenziale. Doctrine non può scoprire le associazioni
inverse, i tipi di ereditarietà, le entità con chiavi esterne come chiavi primarie, né operazioni semantiche sulle
associazioni, come le cascate o gli eventi del ciclo di vita. Sarà necessario un successivo lavoro manuale sulle
entità generate, perché tutto corrisponda alle specifiche del modello del proprio dominio.
Questa guida ipotizza che si stia usando una semplice applicazione blog, con le seguenti due tabelle: blog_post
e blog_comment. Una riga di un commento è collegata alla riga di un post tramite una chiave esterna.
CREATE TABLE ‘blog_post‘ (
‘id‘ bigint(20) NOT NULL AUTO_INCREMENT,
‘title‘ varchar(100) COLLATE utf8_unicode_ci NOT NULL,
‘content‘ longtext COLLATE utf8_unicode_ci NOT NULL,
‘created_at‘ datetime NOT NULL,
PRIMARY KEY (‘id‘),
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
CREATE TABLE ‘blog_comment‘ (
‘id‘ bigint(20) NOT NULL AUTO_INCREMENT,
‘post_id‘ bigint(20) NOT NULL,
‘author‘ varchar(20) COLLATE utf8_unicode_ci NOT NULL,
‘content‘ longtext COLLATE utf8_unicode_ci NOT NULL,
‘created_at‘ datetime NOT NULL,
PRIMARY KEY (‘id‘),
KEY ‘blog_comment_post_id_idx‘ (‘post_id‘),
CONSTRAINT ‘blog_post_id‘ FOREIGN KEY (‘post_id‘) REFERENCES ‘blog_post‘ (‘id‘) ON DELETE CASCAD
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
Prima di addentrarsi nella ricetta, ci si assicuri di aver configurato correttamente i propri parametri di connessione,
nel file app/config/parameters.yml (o in qualsiasi altro posto in cui la configurazione è memorizzata) e
di aver inizializzato un bundle che possa ospitare le future classi entità. In questa guida, si ipotizza che esista un
AcmeBlogBundle, posto nella cartella src/Acme/BlogBundle.
Il primo passo nella costruzione di classi entità da una base dati esistente è quello di chiedere a Doctrine
un’introspezione della base dati e una generazione dei file dei meta-dati corrispondenti. I file dei meta-dati descrivono le classi entità da generare in base ai campi delle tabelle.
php app/console doctrine:mapping:convert xml ./src/Acme/BlogBundle/Resources/config/doctrine/metad
Questo comando del terminale chiede a Doctrine l’introspezione della base dati e la generazione dei file di metadati XML sotto la cartella src/Acme/BlogBundle/Resources/config/doctrine/metadata/orm
del bundle.
Tip: Le classi dei meta-dati possono anche essere generate in YAML, modificando il primo parametro in yml.
Il file dei meta-dati BlogPost.dcm.xml assomiglia a questo:
<?xml version="1.0" encoding="utf-8"?>
<doctrine-mapping>
<entity name="BlogPost" table="blog_post">
<change-tracking-policy>DEFERRED_IMPLICIT</change-tracking-policy>
<id name="id" type="bigint" column="id">
<generator strategy="IDENTITY"/>
280
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
</id>
<field name="title" type="string" column="title" length="100"/>
<field name="content" type="text" column="content"/>
<field name="isPublished" type="boolean" column="is_published"/>
<field name="createdAt" type="datetime" column="created_at"/>
<field name="updatedAt" type="datetime" column="updated_at"/>
<field name="slug" type="string" column="slug" length="255"/>
<lifecycle-callbacks/>
</entity>
</doctrine-mapping>
Una volta generati i file dei meta-dati, si può chiedere a Doctrine di importare lo schema e costruire le relative
classi entità, eseguendo i seguenti comandi.
php app/console doctrine:mapping:import AcmeBlogBundle annotation
php app/console doctrine:generate:entities AcmeBlogBundle
Il primo comando genera le classi delle entità con annotazioni, ma ovviamente si può cambiare il parametro
annotation in xml o yml. La nuva classe entità BlogComment è simile a questa:
<?php
// src/Acme/BlogBundle/Entity/BlogComment.php
namespace Acme\BlogBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* Acme\BlogBundle\Entity\BlogComment
*
* @ORM\Table(name="blog_comment")
* @ORM\Entity
*/
class BlogComment
{
/**
* @var bigint $id
*
* @ORM\Column(name="id", type="bigint", nullable=false)
* @ORM\Id
* @ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* @var string $author
*
* @ORM\Column(name="author", type="string", length=100, nullable=false)
*/
private $author;
/**
* @var text $content
*
* @ORM\Column(name="content", type="text", nullable=false)
*/
private $content;
/**
* @var datetime $createdAt
*
* @ORM\Column(name="created_at", type="datetime", nullable=false)
*/
3.1. Ricettario
281
Symfony2 documentation Documentation, Release 2
private $createdAt;
/**
* @var BlogPost
*
* @ORM\ManyToOne(targetEntity="BlogPost")
* @ORM\JoinColumn(name="post_id", referencedColumnName="id")
*/
private $post;
}
Come si può vedere, Doctrine converte tutti i campi delle tabelle in proprietà della classe. La cosa più notevole è
che scopre anche la relazione con la classe entità BlogPost, basandosi sulla chiave esterna. Di conseguenza, si
può trovare una proprietà $post, mappata con l’entità BlogPost nella classe BlogComment.
Il secondo comando genera tutti i getter e i setter per le proprietà delle classi entità BlogPost e BlogComment.
Le entità generate sono ora pronte per essere usate.
3.1.15 Come usare il livello DBAL di Doctrine
Note: Questo articolo riguarda il livello DBAL di Doctrine. Di solito si lavora con il livello dell’ORM di
Doctrine, che è un livello più astratto e usa il DBAL dietro le quinte, per comunicare con il database. Per saperne
di più sull’ORM di Docrine, si veda “Database e Doctrine (“Il modello”)”.
Il livello di astrazione del database (Database Abstraction Layer o DBAL) di Doctrine è un livello posto sopra
PDO e offre un’API intuitiva e flessibile per comunicare con i database relazionali più diffusi. In altre parole, la
libreria DBAL facilita l’esecuzione delle query ed esegue altre azioni sul database.
Tip: Leggere la documentazione di Doctrine DBAL Documentation per conoscere tutti i dettagli e le capacità
della libreria DBAL di Doctrine.
Per iniziare, configurare i parametri di connessione al database:
• YAML
# app/config/config.yml
doctrine:
dbal:
driver:
pdo_mysql
dbname:
Symfony2
user:
root
password: null
charset: UTF8
• XML
// app/config/config.xml
<doctrine:config>
<doctrine:dbal
name="default"
dbname="Symfony2"
user="root"
password="null"
driver="pdo_mysql"
/>
</doctrine:config>
• PHP
282
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
// app/config/config.php
$container->loadFromExtension(’doctrine’, array(
’dbal’ => array(
’driver’
=> ’pdo_mysql’,
’dbname’
=> ’Symfony2’,
’user’
=> ’root’,
’password’ => null,
),
));
Per un elenco completo delle opzioni di configurazione, vedere Configurazione Doctrine DBAL.
Si può quindi accedere alla connessione del DBAL di Doctrine usando il servizio database_connection:
class UserController extends Controller
{
public function indexAction()
{
$conn = $this->get(’database_connection’);
$users = $conn->fetchAll(’SELECT * FROM users’);
// ...
}
}
Registrare tipi di mappatura personalizzati
Si possono registrare tipi di mappatura personalizzati attraverso la configurazione di Symfony. Saranno aggiunti
a tutte le configurazioni configurate. Per maggiori informazioni sui tipi di mappatura personalizzati, leggere la
sezione Custom Mapping Types della documentazione di Doctrine.
• YAML
# app/config/config.yml
doctrine:
dbal:
types:
custom_first: Acme\HelloBundle\Type\CustomFirst
custom_second: Acme\HelloBundle\Type\CustomSecond
• XML
<!-- app/config/config.xml -->
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:doctrine="http://symfony.com/schema/dic/doctrine"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/
http://symfony.com/schema/dic/doctrine http://symfony.com/schema/dic/
<doctrine:config>
<doctrine:dbal>
<doctrine:dbal default-connection="default">
<doctrine:connection>
<doctrine:mapping-type name="enum">string</doctrine:mapping-type>
</doctrine:connection>
</doctrine:dbal>
</doctrine:config>
</container>
• PHP
// app/config/config.php
$container->loadFromExtension(’doctrine’, array(
3.1. Ricettario
283
Symfony2 documentation Documentation, Release 2
’dbal’ => array(
’connections’ => array(
’default’ => array(
’mapping_types’ => array(
’enum’ => ’string’,
),
),
),
),
));
Registrare tipi di mappatura personalizzati in SchemaTool
SchemaTool è usato per ispezionare il database per confrontare lo schema. Per assolvere a questo compito, ha
bisogno di sapere quale tipo di mappatura deve essere usato per ogni tipo di database. Se ne possono registrare di
nuovi attraverso la configurazione.
Mappiamo il tipo ENUM (non supportato di base dal DBAL) sul tipo di mappatura string:
• YAML
# app/config/config.yml
doctrine:
dbal:
connection:
default:
// Other connections parameters
mapping_types:
enum: string
• XML
<!-- app/config/config.xml -->
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:doctrine="http://symfony.com/schema/dic/doctrine"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/
http://symfony.com/schema/dic/doctrine http://symfony.com/schema/dic/
<doctrine:config>
<doctrine:dbal>
<doctrine:type name="custom_first" class="Acme\HelloBundle\Type\CustomFirst" />
<doctrine:type name="custom_second" class="Acme\HelloBundle\Type\CustomSecond" />
</doctrine:dbal>
</doctrine:config>
</container>
• PHP
// app/config/config.php
$container->loadFromExtension(’doctrine’, array(
’dbal’ => array(
’types’ => array(
’custom_first’ => ’Acme\HelloBundle\Type\CustomFirst’,
’custom_second’ => ’Acme\HelloBundle\Type\CustomSecond’,
),
),
));
284
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
3.1.16 Come lavorare con gestori di entità multipli
Si possono usare gestori di entità multipli in un’applicazione Symfony2. Questo si rende necessario quando si
usano diversi database o addirittura venditori con insiemi di entità completamente differenti. In altre parole, un
gestore di entità che si connette a un database gestirà alcune entità, mentre un altro gestore di entità che si connette
a un altro database potrebbe gestire il resto.
Note: L’uso di molti gestori di entità è facile, ma più avanzato e solitamente non richiesto. Ci si assicuri di avere
effettivamente bisogno di gestori di entità multipli, prima di aggiungere un tale livello di complessità.
La configurazione seguente mostra come configurare due gestori di entità:
• YAML
doctrine:
orm:
default_entity_manager:
default
entity_managers:
default:
connection:
default
mappings:
AcmeDemoBundle: ~
AcmeStoreBundle: ~
customer:
connection:
customer
mappings:
AcmeCustomerBundle: ~
In questo caso, sono stati definiti due gestori di entità, chiamati default e customer. Il gestore di entità default gestisce le entità in AcmeDemoBundle e AcmeStoreBundle, mentre il gestore di entità
customer gestisce le entità in AcmeCustomerBundle.
Lavorando con gestori di entità multipli, occorre esplicitare quale gestore di entità si vuole usare. Se si omette
il nome del gestore di entità al momento della sua richiesta, verrà restituito il gestore di entità predefinito (cioè
default):
class UserController extends Controller
{
public function indexAction()
{
// entrambi restiuiscono "default"
$em = $this->get(’doctrine’)->getEntityManager();
$em = $this->get(’doctrine’)->getEntityManager(’default’);
$customerEm =
$this->get(’doctrine’)->getEntityManager(’customer’);
}
}
Si può ora usare Doctrine come prima, usando il gestore di entità default per persistere e recuperare le entità
da esso gestite e il gestore di entità customer per persistere e recuperare le sue entità.
3.1.17 Registrare funzioni DQL personalizzate
Doctrine consente di specificare funzioni DQL personalizzate. Per maggiori informazioni sull’argomento, leggere
la ricetta di Doctrine “DQL User Defined Functions”.
In Symfony, si possono registrare funzioni DQL personalizzate nel modo seguente:
• YAML
# app/config/config.yml
doctrine:
3.1. Ricettario
285
Symfony2 documentation Documentation, Release 2
orm:
# ...
entity_managers:
default:
# ...
dql:
string_functions:
test_string: Acme\HelloBundle\DQL\StringFunction
second_string: Acme\HelloBundle\DQL\SecondStringFunction
numeric_functions:
test_numeric: Acme\HelloBundle\DQL\NumericFunction
datetime_functions:
test_datetime: Acme\HelloBundle\DQL\DatetimeFunction
• XML
<!-- app/config/config.xml -->
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:doctrine="http://symfony.com/schema/dic/doctrine"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/
http://symfony.com/schema/dic/doctrine http://symfony.com/schema/dic/
<doctrine:config>
<doctrine:orm>
<!-- ... -->
<doctrine:entity-manager name="default">
<!-- ... -->
<doctrine:dql>
<doctrine:string-function name="test_string>Acme\HelloBundle\DQL\StringFu
<doctrine:string-function name="second_string>Acme\HelloBundle\DQL\Second
<doctrine:numeric-function name="test_numeric>Acme\HelloBundle\DQL\Numeri
<doctrine:datetime-function name="test_datetime>Acme\HelloBundle\DQL\Date
</doctrine:dql>
</doctrine:entity-manager>
</doctrine:orm>
</doctrine:config>
</container>
• PHP
// app/config/config.php
$container->loadFromExtension(’doctrine’, array(
’orm’ => array(
// ...
’entity_managers’ => array(
’default’ => array(
// ...
’dql’ => array(
’string_functions’ => array(
’test_string’
=> ’Acme\HelloBundle\DQL\StringFunction’,
’second_string’ => ’Acme\HelloBundle\DQL\SecondStringFunction’,
),
’numeric_functions’ => array(
’test_numeric’ => ’Acme\HelloBundle\DQL\NumericFunction’,
),
’datetime_functions’ => array(
’test_datetime’ => ’Acme\HelloBundle\DQL\DatetimeFunction’,
),
),
),
),
),
));
286
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
3.1.18 Come personalizzare la resa dei form
Symfony permette un’ampia varietà di modi per personalizzare la resa di un form. In questa guida, si apprenderà
come personalizzare ogni possibile parte del form con il minimo sforzo possibile se si utilizza Twig o PHP come
motore di template.
Le basi della resa dei form
Si ricordi che le label, gli errori e i widget HTML di un campo del form possono essere facilmente resi usando la
funzione di Twig form_row oppure il metodo dell’helper PHP row:
• Twig
{{ form_row(form.age) }}
• PHP
<?php echo $view[’form’]->row($form[’age’]) }} ?>
È possibile anche rendere individualmente ogni parte dell’albero del campo:
• Twig
<div>
{{ form_label(form.age) }}
{{ form_errors(form.age) }}
{{ form_widget(form.age) }}
</div>
• PHP
<div>
<?php echo $view[’form’]->label($form[’age’]) }} ?>
<?php echo $view[’form’]->errors($form[’age’]) }} ?>
<?php echo $view[’form’]->widget($form[’age’]) }} ?>
</div>
In entrambi i casi le label, gli errori e i widget HTML del form, sono resi utilizzando un set di markup che sono
standard con Symfony. Per esempio, entrambi i template sopra renderebbero:
<div>
<label for="form_age">Età</label>
<ul>
<li>Questo campo è obbligatorio</li>
</ul>
<input type="number" id="form_age" name="form[age]" />
</div>
Per prototipare velocemente e testare un form, è possibile rendere l’intero form semplicemente con una riga:
• Twig
{{ form_widget(form) }}
• PHP
<?php echo $view[’form’]->widget($form) }} ?>
Nella restante parte di questa ricetta, verrà mostrato come ogni parte del codice del form può essere modificato
a diversi livelli. Per maggiori informazioni sulla resa dei form in generale, è disponibile Rendere un form in un
template.
3.1. Ricettario
287
Symfony2 documentation Documentation, Release 2
Cosa sono i temi di un form?
Symfony usa frammenti di form, piccoli pezzi di template che rendono semplicemente alcune parti, per rendere
ogni parte di un form: la label del campo, gli errori, campi di testo input, tag select, ecc.
I frammenti sono definiti come dei blocchi in Twig e come dei template in PHP.
Un tema non è nient’altro che un insieme di frammenti che si vuole utilizzare quando si rende un form. In altre
parole, se si vuole personalizzare una parte della resa del form, è possibile importare un tema che contiene una
personalizzazione del frammento appropriato del form.
Symfony ha un tema predefinito (form_div_layout.html.twig in Twig e FrameworkBundle:Form in PHP),
che definisce tutti i frammenti necessari per rendere ogni parte di un form.
Nella prossima sezione si potrà vedere come personalizzare un tema, sovrascrivendo qualcuno o tutti i suoi frammenti.
Per esempio, quando è reso il widget di un campo integer, è generato un campo input number
• Twig
{{ form_widget(form.age) }}
• PHP
<?php echo $view[’form’]->widget($form[’age’]) ?>
rende:
<input type="number" id="form_age" name="form[age]" required="required" value="33" />
Internamente, Symfony utilizza il frammento integer_widget per rendere il campo. Questo perché il tipo di
campo è integer e si vuole rendere il widget (in contrapposizione alla sua label o ai suoi errors).
In Twig per impostazione predefinita il blocco integer_widget dal template form_div_layout.html.twig.
In
PHP
è
il
file
integer_widget.html.php
FrameworkBundle/Resources/views/Form.
posizionato
nella
cartella
L’implementazione del frammento integer_widget sarà simile a:
• Twig
{% block integer_widget %}
{% set type = type|default(’number’) %}
{{ block(’field_widget’) }}
{% endblock integer_widget %}
• PHP
<!-- integer_widget.html.php -->
<?php echo $view[’form’]->renderBlock(’field_widget’, array(’type’ => isset($type) ? $type :
Come è possibile vedere, questo frammento rende un altro frammento: field_widget:
• Twig
{% block field_widget %}
{% set type = type|default(’text’) %}
<input type="{{ type }}" {{ block(’widget_attributes’) }} value="{{ value }}" />
{% endblock field_widget %}
• PHP
<!-- FrameworkBundle/Resources/views/Form/field_widget.html.php -->
<input
288
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
type="<?php echo isset($type) ? $view->escape($type) : "text" ?>"
value="<?php echo $view->escape($value) ?>"
<?php echo $view[’form’]->renderBlock(’attributes’) ?>
/>
Il punto è che il frammento detta l’output HTML di ogni parte del form. Per personalizzare l’output del form,
è necessario soltanto identificare e sovrascrivere il frammento corretto. Un set di queste personalizzazioni di
frammenti è conosciuto come “tema” di un form. Quando viene reso un form, è possibile scegliere quale tema del
form si vuole applicare.
In Twig un tema è un singolo file di template e i frammente sono dei blocchi definiti in questo file.
In PHP un tema è una cartella e i frammenti sono singoli file di template in questa cartella.
Conoscere quale blocco personalizzare
In questo esempio, il nome del frammento personalizzato è integer_widget perché si vuole sovrascrivere l’HTML del widget per tutti i tipi di campo integer. Se si ha la necessità di personalizzare campi
textarea, si deve personalizzare il widget textarea_widget.
Come è possibile vedere, il nome del frammento è una combinazione del tipo di campo e ogni parte del
campo viene resa (es. widget, label, errors, row). Come tale, per personalizzare la resa degli errori
solo per il campo input text, bisogna personalizzare il frammento text_errors.
Più frequentemente, tuttavia, si vorrà personalizzare la visualizzazione degli errori attraverso tutti i campi.
È possibile fare questo personalizzando il frammento field_errors. Questo si avvale delle ereditarietà
del tipo di campo. Specificamente dato che il tipo text è esteso dal tipo field, il componente del form
guarderà per prima cosa al tipo-specifico di frammento (es. text_errors) prima di ricadere sul nome del
frammento del suo genitore, se non esiste (es. field_errors).
Per maggiori informazioni sull’argomento, si veda Nomi per i frammenti di form.
Temi del Form
Per vedere la potenza dei temi di un form, si supponga di voler impacchettare ogni campo di input number in un
tag div. La chiave per fare questo è personalizzare il frammento integer_widget.
Temi del form in Twig
Per personalizzare il blocco dei campi del form in Twig, si hanno due possibilità su dove il blocco del form
personalizzato può essere implementato:
Metodo
Nello stesso template del form
In un template separato
Pro
Veloce e facile
Riutilizzabile in più template
Contro
Non utilizzabile in altri template
Richiede la creazione di un template extra
Entrambi i metodi hanno lo stesso effetto ma sono consigliati per situazioni differenti.
Metodo 1: Nello stesso template del form
Il modo più facile di personalizzare il blocco integer_widget è personalizzarlo direttamente nel template che
è sta attualmente rendendo il form.
{% extends ’::base.html.twig’ %}
{% form_theme form _self %}
{% block integer_widget %}
<div class="integer_widget">
{% set type = type|default(’number’) %}
{{ block(’field_widget’) }}
3.1. Ricettario
289
Symfony2 documentation Documentation, Release 2
</div>
{% endblock %}
{% block content %}
{# render the form #}
{{ form_row(form.age) }}
{% endblock %}
Utilizzando il tag speciale {% form_theme form _self %}, Twig guarda nello stesso template per ogni
blocco di form sovrascritto. Assumendo che il campo form.age è un tipo di campo integer, quando il suo
widget è reso, verrà utilizzato il blocco personalizzato integer_widget.
Lo svantaggio di questo metodo è che il blocco del form personalizzato non può essere riutilizzato quando si rende
un altro form in altri template. In altre parole, questo metodo è molto utile quando si effettuano personalizzazioni
che sono specifiche per singoli form nell’applicazione. Se si vuole riutilizzare una personalizzazione attraverso
alcuni (o tutti) form nell’applicazione, si legga la prossima sezione.
Metodo 2: In un template separato
È possibile scegliere di mettere il blocco del form personalizzato integer_widget in un interamente in un
template separato. Il codice e il risultato finale sono gli stessi, ma ora è possibile riutilizzare la personalizzazione
del formi in diversi template:
{# src/Acme/DemoBundle/Resources/views/Form/fields.html.twig #}
{% block integer_widget %}
<div class="integer_widget">
{% set type = type|default(’number’) %}
{{ block(’field_widget’) }}
</div>
{% endblock %}
Ora che è stato creato il blocco del form personalizzato, si ha la necessità di dire a Symfony di utilizzarlo. Nel
template dove si sta rendendo il form, dire a Symfony di utilizzare il template attraverso il tag form_theme:
{% form_theme form ’AcmeDemoBundle:Form:fields.html.twig’ %}
{{ form_widget(form.age) }}
Quando il widget form.age è reso, Symfony utilizzerà il blocco integer_widget dal nuovo template e il
tag input sarà incorporato nel div specificato nel blocco personalizzato.
Temi del form in PHP
Quando si utilizza PHP come motore per i temi, l’unico metodo per personalizzare un frammento è creare un
nuovo file di tema, in modo simile al secondo metodo adottato per Twig.
Bisogna nominare il file del tema dopo il frammento. Bisogna creare il file integer_widget.html.php per
personalizzare il frammento integer_widget.
<!-- src/Acme/DemoBundle/Resources/views/Form/integer_widget.html.php -->
<div class="integer_widget">
<?php echo $view[’form’]->renderBlock(’field_widget’, array(’type’ => isset($type) ? $type : "
</div>
Ora che è stato creato il tema del form personalizzato, bisogna dire a Symfony di utilizzarlo. Nel template dove
viene attualmente reso il form, dire a Symfony di utilizzare il tema attraverso il metodo setTheme dell’helper:
290
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
<?php $view[’form’]->setTheme($form, array(’AcmeDemoBundle:Form’)) ;?>
<?php $view[’form’]->widget($form[’age’]) ?>
Quando il widget form.age viene reso,
Symfony utilizzerà il
integer_widget.html.php e il tag input sarà contenuto in un elemento div.
tema
personalizzato
Referenziare blocchi di form (specifico per Twig)
Finora, per sovrascrivere un particolare blocco del form, il metodo migliore è copiare il blocco di default da
form_div_layout.html.twig, incollarlo in un template differente, e personalizzarlo. In molti casi, è possibile evitare
di fare questo referenziando il blocco di base quando lo si personalizza.
Tutto ciò è semplice da fare, ma varia leggermente a seconda se le personalizzazioni del blocco di form sono nello
stesso template del form o in un template separato.
Referenziare blocchi dall’interno dello stesso template del form
Importare i blocchi aggiungendo un tag use nel template da dove si sta rendendo il form:
{% use ’form_div_layout.html.twig’ with integer_widget as base_integer_widget %}
Ora, quando sono importati i blocchi da form_div_layout.html.twig, il blocco integer_widget è chiamato
base_integer_widget. Questo significa che quando viene ridefinito il blocco integer_widget, è possibile referenziare il markup di default tramite base_integer_widget:
{% block integer_widget %}
<div class="integer_widget">
{{ block(’base_integer_widget’) }}
</div>
{% endblock %}
Referenziare blocchi base da un template esterno
Se la personalizzazione è stata fatta su un template esterno, è possibile referenziare il blocco base utilizzando la
funzione di Twig parent():
{# src/Acme/DemoBundle/Resources/views/Form/fields.html.twig #}
{% extends ’form_div_layout.html.twig’ %}
{% block integer_widget %}
<div class="integer_widget">
{{ parent() }}
</div>
{% endblock %}
Note: Non è possibile referenziare il blocco base quando si usa PHP come motore di template. Bisogna copiare
manualmente il contenuto del blocco base nel nuovo file di template.
Personalizzare lo strato applicativo
Se si vuole che una determinata personalizzazione del form sia globale nell’applicazione, è possibile realizzare ciò effettuando personalizzazioni del form in un template esterno e dopo importarlo nella configurazione
dell’applicazione:
3.1. Ricettario
291
Symfony2 documentation Documentation, Release 2
Twig
Utilizzando la seguente configurazione, ogni blocco di form personalizzato nel template
AcmeDemoBundle:Form:fields.html.twig verrà utilizzato globalmente quando un form verrà
reso.
• YAML
# app/config/config.yml
twig:
form:
resources:
- ’AcmeDemoBundle:Form:fields.html.twig’
# ...
• XML
<!-- app/config/config.xml -->
<twig:config ...>
<twig:form>
<resource>AcmeDemoBundle:Form:fields.html.twig</resource>
</twig:form>
<!-- ... -->
</twig:config>
• PHP
// app/config/config.php
$container->loadFromExtension(’twig’, array(
’form’ => array(’resources’ => array(
’AcmeDemoBundle:Form:fields.html.twig’,
))
// ...
));
Di default, Twig utilizza un layout a div quando rende i form. Qualcuno, tuttavia, potrebbe preferire rendere i
form in un layout a tabella. Utilizzare la risorsa form_table_layout.html.twig come layout:
• YAML
# app/config/config.yml
twig:
form:
resources: [’form_table_layout.html.twig’]
# ...
• XML
<!-- app/config/config.xml -->
<twig:config ...>
<twig:form>
<resource>form_table_layout.html.twig</resource>
</twig:form>
<!-- ... -->
</twig:config>
• PHP
// app/config/config.php
292
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
$container->loadFromExtension(’twig’, array(
’form’ => array(’resources’ => array(
’form_table_layout.html.twig’,
))
// ...
));
Se si vuole effettuare un cambiamento soltanto in un template, aggiungere la seguente riga al file di template
piuttosto che aggiungere un template come risorsa:
{% form_theme form ’form_table_layout.html.twig’ %}
Si osservi che la variabile form nel codice sottostante è la variabile della vista form che è stata passata al template.
PHP
Utilizzando la configurazione seguente, ogni frammento di form personalizzato nella cartella
src/Acme/DemoBundle/Resources/views/Form sarà utilizzato globalmente quando un form
viene reso.
• YAML
# app/config/config.yml
framework:
templating:
form:
resources:
- ’AcmeDemoBundle:Form’
# ...
• XML
<!-- app/config/config.xml -->
<framework:config ...>
<framework:templating>
<framework:form>
<resource>AcmeDemoBundle:Form</resource>
</framework:form>
</framework:templating>
<!-- ... -->
</framework:config>
• PHP
// app/config/config.php
// PHP
$container->loadFromExtension(’framework’, array(
’templating’ => array(’form’ =>
array(’resources’ => array(
’AcmeDemoBundle:Form’,
)))
// ...
));
Per impostazione predefinita, il motore PHP utilizza un layout a div quando rende i form. Qualcuno, tuttavia,
potrebbe preferire rendere i form in un layout a tabella. Utilizzare la risorsa FrameworkBundle:FormTable
per il layout:
• YAML
3.1. Ricettario
293
Symfony2 documentation Documentation, Release 2
# app/config/config.yml
framework:
templating:
form:
resources:
- ’FrameworkBundle:FormTable’
• XML
<!-- app/config/config.xml -->
<framework:config ...>
<framework:templating>
<framework:form>
<resource>FrameworkBundle:FormTable</resource>
</framework:form>
</framework:templating>
<!-- ... -->
</framework:config>
• PHP
// app/config/config.php
$container->loadFromExtension(’framework’, array(
’templating’ => array(’form’ =>
array(’resources’ => array(
’FrameworkBundle:FormTable’,
)))
// ...
));
Se si vuole effettuare un cambiamento soltanto in un template, aggiungere la seguente riga al file di template
piuttosto che aggiungere un template come risorsa:
<?php $view[’form’]->setTheme($form, array(’FrameworkBundle:FormTable’)); ?>
Si osservi che la variabile $form nel codice sottostante è la variabile della vista form che è stata passata al
template.
Personalizzare un singolo campo
Finora, sono stati mostrati i vari modi per personalizzare l’output di un widget di tutti i tipi di campo testuali.
Ma è anche possibile personalizzare singoli campi. Per esempio, si supponga di avere due campi di testo,
first_name e last_name, ma si vuole personalizzare solo uno dei campi. LO si può fare personalizzando
un frammento, in cui il nome è una combinazione dell’attributo id del campo e in cui parte del campo viene
personalizzato. Per esempio:
• Twig
{% form_theme form _self %}
{% block _product_name_widget %}
<div class="text_widget">
{{ block(’field_widget’) }}
</div>
{% endblock %}
{{ form_widget(form.name) }}
• PHP
294
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
<!-- Main template -->
<?php echo $view[’form’]->setTheme($form, array(’AcmeDemoBundle:Form’)); ?>
<?php echo $view[’form’]->widget($form[’name’]); ?>
<!-- src/Acme/DemoBundle/Resources/views/Form/_product_name_widget.html.php -->
<div class="text_widget">
echo $view[’form’]->renderBlock(’field_widget’) ?>
</div>
Qui, il frammento _product_name_widget definisce il template da utilizzare per il campo del quale l’id è
product_name (e il nome è product[name]).
Tip: La porzione del campo product è il nome del form, che può essere impostato manualmente o generato
automaticamente basandosi sul tipo di nome del form (es. ProductType equivale a product). Se non si è
sicuri di cosa sia il nome del form, basta semplicemente vedere il sorgente del form generato.
È possibile sovrascrivere il markup per un intera riga di campo utilizzando lo stesso metodo:
• Twig
{% form_theme form _self %}
{% block _product_name_row %}
<div class="name_row">
{{ form_label(form) }}
{{ form_errors(form) }}
{{ form_widget(form) }}
</div>
{% endblock %}
• PHP
<!-- _product_name_row.html.php -->
<div class="name_row">
<?php echo $view[’form’]->label($form) ?>
<?php echo $view[’form’]->errors($form) ?>
<?php echo $view[’form’]->widget($form) ?>
</div>
Altre personalizzazioni comuni
Finora, questa ricetta ha illustrato diversi modi per personalizzare la resa di un form. La chiave di tutto è personalizzare uno specifico frammento che corrisponde alla porzione del form che si vuole controllare (si veda nominare
i blocchi dei form).
Nella prossima sezone, si potrà vedere come è possibile effettuare diverse personalizzazioni comuni per il form.
Per applicare queste personalizzazioni, si utilizzi uno dei metodi descritti nella sezione Temi del Form.
Personalizzare l’output degli errori
Note: Il componente del form gestisce soltanto come gli errori di validazione vengono resi, e non gli attuali
messaggi di errore di validazione. I messaggi d’errore sono determinati dai vincoli di validazione applicati agli
oggetti. Per maggiori informazioni, si veda il capitolo validazione.
3.1. Ricettario
295
Symfony2 documentation Documentation, Release 2
Ci sono diversi modi di personalizzare come gli errori sono resi quando un form viene inviato con errori. I
messaggi di errore per un campo sono resi quando si utilizza l’helper form_errors:
• Twig
{{ form_errors(form.age) }}
• PHP
<?php echo $view[’form’]->errors($form[’age’]); ?>
Di default, gli errori sono resi dentro una lista non ordinata:
<ul>
<li>Questo campo è obbligatorio</li>
</ul>
Per sovrascrivere come gli errori sono resi per tutti i campi, basta semplicemente copiare, incollare e personalizzare
il frammento field_errors.
• Twig
{% block field_errors %}
{% spaceless %}
{% if errors|length > 0 %}
<ul class="error_list">
{% for error in errors %}
<li>{{ error.messageTemplate|trans(error.messageParameters, ’validators’) }}</li>
{% endfor %}
</ul>
{% endif %}
{% endspaceless %}
{% endblock field_errors %}
• PHP
<!-- fields_errors.html.php -->
<?php if ($errors): ?>
<ul class="error_list">
<?php foreach ($errors as $error): ?>
<li><?php echo $view[’translator’]->trans(
$error->getMessageTemplate(),
$error->getMessageParameters(),
’validators’
) ?></li>
<?php endforeach; ?>
</ul>
<?php endif ?>
Tip: Si veda Temi del Form per come applicare questa personalizzazione.
È anche possibile personalizzare l’output dell’errore per uno specifico tipo di campo. Per esempio, alcuni errori
che sono globali al form (es. non specifici a un singolo campo) sono resi separatamente, di solito all’inizio del
form:
• Twig
{{ form_errors(form) }}
• PHP
<?php echo $view[’form’]->render($form); ?>
296
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
Per personalizzare solo il markup utilizzato per questi errori, si segue la stesa strada de codice sopra ma verrà
chiamato il blocco form_errors (Twig) / il file form_errors.html.php (PHP). Ora, quando sono resi gli
errori per il form, i frammenti personalizzati verranno utilizzati al posto dei field_errors di default.
Personalizzare una “riga del form”
Quando è possibile modificarlo, la strada più facile per rendere il campo di un form è attraverso la funzione
form_row, che rende l’etichetta, gli errori e il widget HTML del campo. Per personalizzare il markup utilizzato
per rendere tutte le righe del campo di un form bisogna sovrascrivere il frammento field_row. Per esempio, si
supponga di voler aggiungere una classe all’elemento div per ogni riga:
• Twig
{% block field_row %}
<div class="form_row">
{{ form_label(form) }}
{{ form_errors(form) }}
{{ form_widget(form) }}
</div>
{% endblock field_row %}
• PHP
<!-- field_row.html.php -->
<div class="form_row">
<?php echo $view[’form’]->label($form) ?>
<?php echo $view[’form’]->errors($form) ?>
<?php echo $view[’form’]->widget($form) ?>
</div>
Tip: Si veda Temi del Form per conoscere come applicare questa personalizzazione.
Aggiungere un asterisco “obbligatorio” alle label del campo
È possibile denotare tutti i campi obbligatori con un asterisco (*), semplicemente personalizzando il frammento
field_label.
In Twig, se si sta personalizzando il form all’interno dello stesso template del form, basta modificare il tag use e
aggiungere le seguenti righe:
{% use ’form_div_layout.html.twig’ with field_label as base_field_label %}
{% block field_label %}
{{ block(’base_field_label’) }}
{% if required %}
<span class="required" title="This field is required">*</span>
{% endif %}
{% endblock %}
In Twig, se si sta personalizzando il form all’interno di un template separato, bisogna utilizzare le seguenti righe:
{% extends ’form_div_layout.html.twig’ %}
{% block field_label %}
{{ parent() }}
{% if required %}
<span class="required" title="Questo campo è obbligatorio">*</span>
3.1. Ricettario
297
Symfony2 documentation Documentation, Release 2
{% endif %}
{% endblock %}
Quando si usa PHP come motore di template bisogna copiare il contenuto del template originale:
<!-- field_label.html.php -->
<!-- original content -->
<label for="<?php echo $view->escape($id) ?>" <?php foreach($attr as $k => $v) { printf(’%s="%s" ’
<!-- personalizzazione -->
<?php if ($required) : ?>
<span class="required" title="Questo campo è obbligatorio">*</span>
<?php endif ?>
Tip: Si veda Temi del Form per sapere come effettuare questa personalizzazione.
Aggiungere messaggi di aiuto
È possibile personalizzare i widget del form per ottenere un messaggio di aiuto opzionale.
In Twig, se si sta personalizzando il form all’interno dello stesso template del form, basta modificare il tag use e
aggiungere le seguenti righe:
{% use ’form_div_layout.html.twig’ with field_widget as base_field_widget %}
{% block field_widget %}
{{ block(’base_field_widget’) }}
{% if help is defined %}
<span class="help">{{ help }}</span>
{% endif %}
{% endblock %}
In Twig, se si sta personalizzando il form all’interno di un template separato, bisogna utilizzare le seguenti righe:
{% extends ’form_div_layout.html.twig’ %}
{% block field_widget %}
{{ parent() }}
{% if help is defined %}
<span class="help">{{ help }}</span>
{% endif %}
{% endblock %}
Quando si usa PHP come motore di template bisogna copiare il contenuto del template originale:
<!-- field_widget.html.php -->
<!-- contenuto originale -->
<input
type="<?php echo isset($type) ? $view->escape($type) : "text" ?>"
value="<?php echo $view->escape($value) ?>"
<?php echo $view[’form’]->renderBlock(’attributes’) ?>
/>
<!-- Personalizzazione -->
<?php if (isset($help)) : ?>
<span class="help"><?php echo $view->escape($help) ?></span>
<?php endif ?>
298
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
Per rendere un messaggio di aiuto sotto al campo, passare nella variabile help:
• Twig
{{ form_widget(form.title, { ’help’: ’foobar’ }) }}
• PHP
<?php echo $view[’form’]->widget($form[’title’], array(’help’ => ’foobar’)) ?>
Tip: Si veda Temi del Form per sapere come applicare questa configurazione.
3.1.19 Utilizzare i data transformer
Spesso si avrà la necessità di trasformare i dati che l’utente ha immesso in un form in qualcosa di diverso da
utilizzare nel programma. Tutto questo si potrebbe fare manualmente nel controller ma nel caso in cui si volesse
utilizzare il form in posti diversi?
Supponiamo di avere una relazione uno-a-uno tra Task e Rilasci, per esempio un Task può avere un rilascio
associato. Avere una casella di riepilogo con la lista di tutti i rilasci può portare ad una casella di riepilogo molto
lunga nella quale risulterà impossibile cercare qualcosa. Si vorrebbe, piuttosto, aggiungere un campo di testo nel
quale l’utente può semplicemente inserire il numero del rilascio. Nel controller si può convertire questo numero
di rilascio in un task attuale ed eventualmente aggiungere errori al form se non è stato trovato ma questo non è il
modo migliore di procedere.
Sarebbe meglio se questo rilascio fosse cercato automaticamente e convertito in un oggetto rilascio, in modo da
poterlo utilizzare nell’azione. In questi casi entrano in gioco i data transformer.
Come prima cosa, bisogna creare un form che abbia un data transformer collegato che, dato un numero, ritorni
un oggetto Rilascio: il tipo selettore rilascio. Eventualmente sarà semplicemente un campo di testo, dato che la
configurazione dei campi che estendono è impostata come campo di testo, nel quale si potrà inserire il numero di
rilascio. Il campo di testo farà comparire un errore se verrà inserito un numero di rilascio che non esiste:
// src/Acme/TaskBundle/Form/IssueSelectorType.php
namespace Acme\TaskBundle\Form\Type;
use
use
use
use
Symfony\Component\Form\AbstractType;
Symfony\Component\Form\FormBuilder;
Acme\TaskBundle\Form\DataTransformer\IssueToNumberTransformer;
Doctrine\Common\Persistence\ObjectManager;
class IssueSelectorType extends AbstractType
{
private $om;
public function __construct(ObjectManager $om)
{
$this->om = $om;
}
public function buildForm(FormBuilder $builder, array $options)
{
$transformer = new IssueToNumberTransformer($this->om);
$builder->appendClientTransformer($transformer);
}
public function getDefaultOptions(array $options)
{
return array(
’invalid_message’=>’Il rilascio che cerchi non esiste.’
);
3.1. Ricettario
299
Symfony2 documentation Documentation, Release 2
}
public function getParent(array $options)
{
return ’text’;
}
public function getName()
{
return ’issue_selector’;
}
}
Tip: È possibile utilizzare i transformer senza necessariamente creare un nuovo form personalizzato invocando
la funzione appendClientTransformer su qualsiasi field builder:
use Acme\TaskBundle\Form\DataTransformer\IssueToNumberTransformer;
class TaskType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
// ...
// si assume che l’entity manager è stato passato come opzione
$entityManager = $options[’em’];
$transformer = new IssueToNumberTransformer($entityManager);
// utilizza un campo di testo ma trasforma il testo in un oggetto rilascio
$builder
->add(’issue’, ’text’)
->appendClientTransformer($transformer)
;
}
// ...
}
quindi, creiamo il data transformer che effettua la vera e propria conversione:
// src/Acme/TaskBundle/Form/DataTransformer/IssueToNumberTransformer.php
namespace Acme\TaskBundle\Form\DataTransformer;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Symfony\Component\Form\DataTransformerInterface;
use Doctrine\Common\Persistence\ObjectManager;
class IssueToNumberTransformer implements DataTransformerInterface
{
private $om;
public function __construct(ObjectManager $om)
{
$this->om = $om;
}
// trasforma l’oggetto Rilascio in una stringa
public function transform($val)
{
if (null === $val) {
return ’’;
}
300
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
return $val->getNumber();
}
// trasforma il numero rilascio in un oggetto rilascio
public function reverseTransform($val)
{
if (!$val) {
return null;
}
$issue = $this->om->getRepository(’AcmeTaskBundle:Issue’)->findOneBy(array(’number’ => $va
if (null === $issue) {
throw new TransformationFailedException(sprintf(’Un rilascio con numero %s non esiste’
}
return $issue;
}
}
Infine, poiché abbiamo deciso di creare un campo di testo personalizzato che utilizza il data transformer, bisogna
registrare il tipo nel service container, in modo che l’entity manager può essere automaticamente iniettato:
• YAML
services:
acme_demo.type.issue_selector:
class: Acme\TaskBundle\Form\IssueSelectorType
arguments: ["@doctrine.orm.entity_manager"]
tags:
- { name: form.type, alias: issue_selector }
• XML
<service id="acme_demo.type.issue_selector" class="Acme\TaskBundle\Form\IssueSelectorType">
<argument type="service" id="doctrine.orm.entity_manager"/>
<tag name="form.type" alias="issue_selector" />
</service>
Ora è possibile aggiungere il tipo al form dal suo alias come segue:
// src/Acme/TaskBundle/Form/Type/TaskType.php
namespace Acme\TaskBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
class TaskType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add(’task’);
$builder->add(’dueDate’, null, array(’widget’ => ’single_text’));
$builder->add(’issue’, ’issue_selector’);
}
public function getName()
{
return ’task’;
}
}
Ora sarà molto facile in qualsiasi punto dell’applicazione, usare questo tipo selettore per selezionare un rilascio
3.1. Ricettario
301
Symfony2 documentation Documentation, Release 2
da un numero. Tutto questo, senza aggiungere nessuna logica al controllore.
Se si vuole creare un nuovo rilascio quando viene inserito un numero di rilascio sconosciuto, è possibile istanziarlo
piuttosto che lanciare l’eccezione TransformationFailedException e inoltre persiste nel proprio entity manager se
il task non ha opzioni a cascata per il rilascio.
3.1.20 Come generare dinamicamente form usando gli eventi form
Prima di addentrarci nella generazione dinamica dei form, diamo un’occhiata veloce alla classe dei form:
//src/Acme/DemoBundle/Form/ProductType.php
namespace Acme\DemoBundle\Form
use Symfony\Component\Form\AbstractType
use Symfony\Component\Form\FormBuilder;
class ProductType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add(’nome’);
$builder->add(’prezzo’);
}
public function getName()
{
return ’prodotto’;
}
}
Si assuma per un momento che questo form utilizzi una classe immaginaria “prodotto” questa ha solo due attributi
rilevanti (“nome” e “prezzo”). Il form generato da questa classe avrà lo stesso aspetto, indipendentemente se un
nuovo prodotto sta per essere creato oppure se un prodotto esistente sta per essere modificato (es. un prodotto
ottenuto da database).
Si supponga ora, di non voler abilitare l’utente alla modifica del campo ‘nome’ una volta che l’oggetto è stato
creato. Per fare ciò si può dare un’occhiata al Event Dispatcher sistema che analizza l’oggetto e modifica il form
basato sull’ oggetto ‘prodotto’. In questa voce, si imparerà come aggiungere questo livello di flessibilità ai form.
Aggiungere un evento sottoscrittore alla classe di un form
Invece di aggiungere direttamente il widget “nome” tramite la classe dei form ProductType si deleghi la responsabilità di creare questo particolare campo ad un evento sottoscrittore:
//src/Acme/DemoBundle/Form/ProductType.php
namespace Acme\DemoBundle\Form
use Symfony\Component\Form\AbstractType
use Symfony\Component\Form\FormBuilder;
use Acme\DemoBundle\Form\EventListener\AddNameFieldSubscriber;
class ProductType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$subscriber = new AddNameFieldSubscriber($builder->getFormFactory());
$builder->addEventSubscriber($subscriber);
$builder->add(’price’);
}
public function getName()
302
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
{
return ’prodotto’;
}
}
L’evento sottoscrittore è passato dall’oggetto FormFactory nel suo costruttore, quindi il nuovo sottoscrittore è in
grado di creare il widget del form una volta che viene notificata dall’evento inviato durante la creazione del form.
Dentro la classe dell’evento sottoscrittore
L’obiettivo è di creare un campo “nome” solo se l’oggetto Prodotto sottostante è nuovo (es. non è stato persistito
nel database). Basandosi su questo, l’sottoscrittore potrebbe essere simile a questo:
// src/Acme/DemoBundle/Form/EventListener/AddNameFieldSubscriber.php
namespace Acme\DemoBundle\Form\EventListener;
use
use
use
use
Symfony\Component\Form\Event\DataEvent;
Symfony\Component\Form\FormFactoryInterface;
Symfony\Component\EventDispatcher\EventSubscriberInterface;
Symfony\Component\Form\FormEvents;
class AddNameFieldSubscriber implements EventSubscriberInterface
{
private $factory;
public function __construct(FormFactoryInterface $factory)
{
$this->factory = $factory;
}
public static function getSubscribedEvents()
{
// Indica al dispacher che si vuole ascoltare l’evento form.pre_set_data
// e che verrà invocato il metodo preSetData.
return array(FormEvents::PRE_SET_DATA => ’preSetData’);
}
public function preSetData(DataEvent $event)
{
$data = $event->getData();
$form = $event->getForm();
//
//
//
//
//
if
Dutante la creazione del form, setData è chiamata con parametri null
dal costruttore di FormBuilder. Si è interessati a quando
setData è invocato con l’oggetto Entity attuale (se è nuovo,
oppure recuperato con Doctrine). Bisognerà uscire dal metoro
se la condizione restituisce null.
(null === $data) {
return;
}
// controlla se l’oggetto Prodotto è nuovo
if (!$data->getId()) {
$form->add($this->factory->createNamed(’text’, ’name’));
}
}
}
Caution: È facile fraintendere lo scopo dell’istruzione if (null === $data) dell’evento sottoscrittore.
Per comprendere appieno il suo ruolo, bisogna dare uno sguardo alla classe Form e prestare attenzione a dove
setData() è invocato alla fine del costruttore, nonché al metodo setData() stesso.
3.1. Ricettario
303
Symfony2 documentation Documentation, Release 2
La riga FormEvents::PRE_SET_DATA viene attualmente risolta nella stringa form.pre_set_data. La
classe FormEvents ha uno scopo organizzativo. Ha una posizione centralizzata in quello che si può trovare tra i
diversi eventi dei form disponibili.
Anche se in questo esempio si potrebbe utilizzare l’evento form.set_data o anche l’evento
form.post_set_data, utilizzando form.pre_set_data si garantisce che i dati saranno ottenuti
dall’oggetto Event che non è stato modificato da nessun altro sottoscrittore o ascoltatore. Questo perché
form.pre_set_data passa all’oggetto DataEvent invece dell’oggetto FilterDataEvent passato dall’evento
form.set_data. DataEvent, a differenza del suo figlio FilterDataEvent, non ha il metodo setData().
Note: È possibile consultare la lista completa degli eventi del form tramite la classe FormEvents, nel bundle dei
form.
3.1.21 Come unire una collezione di form
Con questa ricetta si apprenderà come creare un form che unisce una collezione di altri form. Ciò può essere utile,
ad esempio, se si ha una classe Task e si vuole modificare/creare/cancellare oggetti Tag connessi a questo Task,
all’interno dello stesso form.
Note: Con questa ricetta, si assume di utilizzare Doctrine come ORM. Se non si utilizza Doctrine (es. Propel o
semplicemente una connessione a database), il tutto è pressapoco simile.
Se si utilizza Doctrine, si avrà la necessità di aggiungere meta-dati Doctrine, includendo una relazione
ManyToMany sulla colonna tags di Task.
Iniziamo: supponiamo che ogni Task appartiene a più oggetti Tags. Si crei una semplice classe Task:
// src/Acme/TaskBundle/Entity/Task.php
namespace Acme\TaskBundle\Entity;
use Doctrine\Common\Collections\ArrayCollection;
class Task
{
protected $description;
protected $tags;
public function __construct()
{
$this->tags = new ArrayCollection();
}
public function getDescription()
{
return $this->description;
}
public function setDescription($description)
{
$this->description = $description;
}
public function getTags()
{
return $this->tags;
}
public function setTags(ArrayCollection $tags)
304
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
{
$this->tags = $tags;
}
}
Note: ArrayCollection è specifica per Doctrine ed è fondamentalmente la stessa cosa di utilizzare un
array (ma deve essere un ArrayCollection) se si utilizza Doctrine.
Ora, si crei una classe Tag. Come è possibile verificare, un Task può avere più oggetti Tag:
// src/Acme/TaskBundle/Entity/Tag.php
namespace Acme\TaskBundle\Entity;
class Tag
{
public $name;
}
Tip: La proprietà name qui è pubblica, ma può essere facilmente protetta o privata (ma in questo caso si avrebbe
bisogno dei metodi getName e setName).
Si crei ora una classe di form cosicché un oggetto Tag può essere modificato dall’utente:
// src/Acme/TaskBundle/Form/Type/TagType.php
namespace Acme\TaskBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
class TagType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add(’name’);
}
public function getDefaultOptions(array $options)
{
return array(
’data_class’ => ’Acme\TaskBundle\Entity\Tag’,
);
}
public function getName()
{
return ’tag’;
}
}
Questo è sufficiente per rendere un form tag. Ma dal momento che l’obiettivo finale è permettere la modifica dei
tag di un task nello stesso form del task, bisogna creare un form per la classe Task.
Da notare che si unisce una collezione di form TagType utilizzando il tipo di campo collection:
// src/Acme/TaskBundle/Form/Type/TaskType.php
namespace Acme\TaskBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
class TaskType extends AbstractType
3.1. Ricettario
305
Symfony2 documentation Documentation, Release 2
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add(’description’);
$builder->add(’tags’, ’collection’, array(’type’ => new TagType()));
}
public function getDefaultOptions(array $options)
{
return array(
’data_class’ => ’Acme\TaskBundle\Entity\Task’,
);
}
public function getName()
{
return ’task’;
}
}
Nel controllore, è possibile inizializzare una nuova istanza di TaskType:
// src/Acme/TaskBundle/Controller/TaskController.php
namespace Acme\TaskBundle\Controller;
use
use
use
use
use
Acme\TaskBundle\Entity\Task;
Acme\TaskBundle\Entity\Tag;
Acme\TaskBundle\Form\TaskType;
Symfony\Component\HttpFoundation\Request;
Symfony\Bundle\FrameworkBundle\Controller\Controller;
class TaskController extends Controller
{
public function newAction(Request $request)
{
$task = new Task();
// codice fittizio: è qui solo perché il Task ha alcuni tag
// altrimenti, questo non è un esempio interessante
$tag1 = new Tag()
$tag1->name = ’tag1’;
$task->getTags()->add($tag1);
$tag2 = new Tag()
$tag2->name = ’tag2’;
$task->getTags()->add($tag2);
// fine del codice fittizio
$form = $this->createForm(new TaskType(), $task);
// fare qualche processo del form qui, in una richiesta POST
return $this->render(’AcmeTaskBundle:Task:new.html.twig’, array(
’form’ => $form->createView(),
));
}
}
Il template corrispondente ora è abilitato a rendere entrambi i campi description per il form dei task, oltre
tutti i form TagType che sono relazionati a questo Task. Nel controllore sottostante, viene aggiunto del codice
fittizio così da poterlo vedere in azione (dato che un Task non ha tags appena viene creato).
• Twig
306
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
{# src/Acme/TaskBundle/Resources/views/Task/new.html.twig #}
{# ... #}
{# rende solo il campo: description #}
{{ form_row(form.description) }}
<h3>Tags</h3>
<ul class="tags">
{# itera per ogni tag esistente e rende solo il campo: nome #}
{% for tag in form.tags %}
<li>{{ form_row(tag.name) }}</li>
{% endfor %}
</ul>
{{ form_rest(form) }}
{# ... #}
• PHP
<!-- src/Acme/TaskBundle/Resources/views/Task/new.html.php -->
<!-- ... -->
<h3>Tags</h3>
<ul class="tags">
<?php foreach($form[’tags’] as $tag): ?>
<li><?php echo $view[’form’]->row($tag[’name’]) ?></li>
<?php endforeach; ?>
</ul>
<?php echo $view[’form’]->rest($form) ?>
<!-- ... -->
Quando l’utente invia il form, i dati inviati per i campi di Tags sono utilizzato per costruire un ArrayCollection
di oggetti Tag,che viene poi impostato sul campo tag dell’istanza Task.
La collezione Tags‘‘è acessibile tramite ‘‘$task->getTags() e può essere persistita nel
database oppure utilizzata dove se ne ha bisogno.
Finora, tutto ciò funziona bene, ma questo non permette di aggiungere nuovi dinamicamente todo o eliminare todo
esistenti. Quindi, la modifica dei todo esistenti funziona bene ma ancora non si possono aggiungere nuovi todo.
Permettere “nuovi” todo con “prototipo”
Permettere all’utente di inserire dinamicamente nuovi todo significa che abbiamo la necessità di utilizzare
Javascript. Precedentemente sono stati aggiunti due tags al nostro form nel controllore. Ora si ha la necessità
che l’utente possa aggiungere diversi form di tag secondo le sue necessità direttamente dal browser. Questo può
essere fatto attraverso un po’ di Javascript.
La prima cosa di cui si ha bisogno è di far capire alla collezione di form che riceverà un numero indeterminato di
tag. Finora sono stati aggiunti due tag e il form si aspetta di riceverne esattamente due, altrimenti verrà lanciato
un errore: Questo form non può contenere campi extra. Per rendere flessibile il form, bisognerà
aggiungere l’opzione allow_add alla collezione di campi:
// ...
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add(’description’);
$builder->add(’tags’, ’collection’, array(
’type’ => new TagType(),
’allow_add’ => true,
’by_reference’ => false,
3.1. Ricettario
307
Symfony2 documentation Documentation, Release 2
));
}
Da notare che è stata aggiunto ’by_reference’ => false. Questo perché non si sta inviando una referenza
ad un tag esistente ma piuttosto si sta creando un nuovo tag quando si salva insieme il todo e i suoi tag.
L’opzione allow_add effettua anche un’altra cosa. Aggiunge la proprietà data-prototype al div che
contiene la collezione del tag. Questa proprietà contiene html da aggiungere all’elemento Tag nella pagina, come
il seguente esempio:
<div data-prototype="&lt;div&gt;&lt;label class=&quot; required&quot;&gt;$$name$$&lt;/label&gt;&lt
</div>
Sarà, quindi, possibile ottenere questa proprietà da Javascript ed utilizzarla per visualizzare U nuovo form di Tag.
Per rendere le cose semplici, verrà incorporato jQuery nella pagina dato che permette la manipolazione della
pagina in modalità cross-browser..
Come prima cosa, si aggiunga un nuovo form con la classe add_tag_link. Ogni volta che viene cliccato
dall’utente, verrà aggiunto un tag vuoto:
$(’.record_action’).append(’<li><a href="#" class="add_tag_link">Add a tag</a></li>’);
Inoltre, bisognerà includere un template che contenga il codice Javascript necessario per aggiungere gli elementi
del form quando il link verrà premuto..
Il codice può essere semplice:
function addTagForm() {
// Ottieni il div che detiene la collezione di tag
var collectionHolder = $(’#task_tags’);
// prendi il data-prototype
var prototype = collectionHolder.attr(’data-prototype’);
// Sostituisci ’$$name$$’ nell’html del prototype in the prototype’s HTML
// affiché sia un nummero basato sulla lunghezza corrente della collezione.
form = prototype.replace(/\$\$name\$\$/g, collectionHolder.children().length);
// Visualizza il form nella pagina
collectionHolder.append(form);
}
// Aggiungi il link per aggiungere ulteriori tag
$(’.record_action’).append(’<li><a href="#" class="add_tag_link">Aggiungi un tag</a></li>’);
// Quando il link viene premuto aggiunge un campo per immettere un nuovo tag
$(’a.jslink’).click(function(event){
addTagForm();
});
Ora, ogni volta che un utente clicca sul link Aggiungi un tag, apparirà un nuovo form nella pagina. Il form
lato server è consapevole di tutto e non si aspetterà nessuna specifica dimensione per la collezione Tag. Tutti i tag
verranno aggiunti creando un nuovo Todo salvandolo insieme a esso.
Per ulteriori dettagli, guarda collection form type reference.
Permettere la rimozione di todo
Questa sezione non è ancora stata scritta, ma lo sarà presto. Se si è interessati a scrivere questa sezione, si guardi
Contribuire alla documentazione.
3.1.22 Come creare un tipo di campo personalizzato di un form
Symfony è dotato di una serie di tipi di campi per la costruzione dei form. Tuttavia ci sono situazioni in cui
è necessario realizzare un campo personalizzato per uno scopo specifico. Questa ricetta ipotizza che si abbia
necessità di un capo personalizzato che contenga il genere di una persona, un nuovo campo basato su un campo di
308
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
tipo scelta. Questa sezione spiega come il campo è definito, come si può personalizzare il layout e, infine, come è
possibile registrarlo per utilizzarlo nell’applicazione.
Definizione del tipo di campo
Per creare il tipo di campo personalizzato, è necessario creare per prima la classe che rappresenta il campo.
Nell’esempio proposto la classe che realizza il tipo di campo sarà chiamata GenderType e il file sarà salvato nella
cartella default contenente i capi del form, che è <BundleName>\Form\Type. Assicurati che il campo estenda
Symfony\Component\Form\AbstractType:
# src/Acme/DemoBundle/Form/Type/GenderType.php
namespace Acme\DemoBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
class GenderType extends AbstractType
{
public function getDefaultOptions(array $options)
{
return array(
’choices’ => array(
’m’ => ’Male’,
’f’ => ’Female’,
)
);
}
public function getParent(array $options)
{
return ’choice’;
}
public function getName()
{
return ’gender’;
}
}
Tip: La cartella di memorizzazione di questo file non è importante: la cartella Form\Type è solo una convenzione.
Qui, il valore di ritorno del metodo getParent indica che che si sta estendendo il tipo di campo choice.
Questo significa che di default, sono ereditate tutte le logiche e la resa di queto tipo di campo. Per vedere alcune
logiche, controlla la classe ChoiceType. Ci sono tre metodi che sono particolarmente importanti:
• buildForm() - Ogni tipo di campo possiede un metodo buildForm, che permette di configurare e
creare ogni campo/campi. Notare che questo è lo stesso metodo che è utilizzato per la preparazione del
proprio form, e qui funziona allo stesso.
• buildView() - Questo metodo è utilizzato per impostare le altre variabili che sono necessarie per la resa
del campo nel template. Per esempio, nel tipo di campo ChoiceType, la variabile multiple è impostata
e utilizzata nel template per impostare (o non impostare) l’attributo multiple nel campo select. Si
faccia riferimento a ‘Creare un template per il campo‘_ per maggiori dettagli.
• getDefaultOptions() - Questo metodo definisce le opzioni per il tipo di form che possono essere
utilizzate in buildForm() e buildView(). Ci sono molte opzioni comuni a tutti i campi (vedere
FieldType), ma è possibile crearne altre, quante sono necessarie.
Tip: Se si sta creando un campo che consiste di molti campi, assicurarsi di impostare come “padre” un tipo come
3.1. Ricettario
309
Symfony2 documentation Documentation, Release 2
form o qualcos’altro che estenda form. Nello stesso modo, se occorre modificare la “vista” di ogni sottotipo che
estende il proprio tipo, utilizzare il metodo buildViewBottomUp().
Il metodo getName() restituisce un identificativo che dovrebbe essere unico all’interno dell’applicazione.
Questo è usato in vari posti, ad esempio nel momento in cui il tipo di form è reso.
L’obiettivo del nostro tipo di campo era di estendere il tipo choice per permettere la selezione del genere. Ciò si
ottiene impostando in maniera fissa le choices con la lista dei generi.
Creazione del template per il campo
Ogni campo è reso da un template, che è determinato in parte dal valore del metodo getName(). Per maggiori
informazioni, vedere Cosa sono i temi di un form?.
In questo caso, dato che il campo padre è choice, non è necessario fare altre attività e il tipo di campo creato
sarà automaticamente reso come tipo choice. Ma per avere un esempio più incisivo, supponiamo che il tipo
di campo creato sia “expanded” (ad es. radio button o checkbox, al posto di un campo select), vogliamo sempre
la resa del campo in un elemento ul. Nel template del proprio form (vedere il link sopra per maggiori dettagli),
creare un blocco gender_widget per gestire questo caso:
{# src/Acme/DemoBundle/Resources/views/Form/fields.html.twig #}
{% block gender_widget %}
{% spaceless %}
{% if expanded %}
<ul {{ block(’widget_container_attributes’) }}>
{% for child in form %}
<li>
{{ form_widget(child) }}
{{ form_label(child) }}
</li>
{% endfor %}
</ul>
{% else %}
{# far rendere il tag select al widget choice #}
{{ block(’choice_widget’) }}
{% endif %}
{% endspaceless %}
{% endblock %}
Note: Assicurarsu che il prefisso del widget utilizzato sia corretto. In questo esempio il nome dovrebbe essere
gender_widget, in base al valore restituito da getName. Inoltre, il file principale di configurazione dovrebbe
puntare al template personalizzato del form, in modo che sia utilizzato per la resa di tutti i form.
# app/config/config.yml
twig:
form:
resources:
- ’AcmeDemoBundle:Form:fields.html.twig’
Utilizzare il tipo di campo
Ora si può utilizzare il tipo di campo immediatamente, creando semplicemente una nuova istanza del tipo in un
form:
// src/Acme/DemoBundle/Form/Type/AuthorType.php
namespace Acme\DemoBundle\Form\Type;
310
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
class AuthorType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add(’gender_code’, new GenderType(), array(
’empty_value’ => ’Choose a gender’,
));
}
}
Questo funziona perché il GenderType() è veramente semplice. Cosa succede se i valori del genere sono stati
inseriti nella configurazione o nel database? La prossima sezione spiega come un tipo di campo più complesso
può risolvere questa situazione.
Creazione di un tipo di campo come servizio
Finora, questa spiegazione ha assunto che si ha un tipo di campo molto semplice. Ma se fosse necessario accedere
alla configurazione o al database o a qualche altro servizio, è necessario registrare il tipo di campo come servizio.
Per esempio, si supponga che i valori del genere siano memorizzati nella configurazione:
• YAML
# app/config/config.yml
parameters:
genders:
m: Male
f: Female
• XML
<!-- app/config/config.xml -->
<parameters>
<parameter key="genders" type="collection">
<parameter key="m">Male</parameter>
<parameter key="f">Female</parameter>
</parameter>
</parameters>
Per utilizzare i parametri, è necessario definire il tipo di campo come un servizio, iniettando i valori dei parametri
di genders come primo parametro del metodo __construct:
• YAML
# src/Acme/DemoBundle/Resources/config/services.yml
services:
form.type.gender:
class: Acme\DemoBundle\Form\Type\GenderType
arguments:
- "%genders%"
tags:
- { name: form.type, alias: gender }
• XML
<!-- src/Acme/DemoBundle/Resources/config/services.xml -->
<service id="form.type.gender" class="Acme\DemoBundle\Form\Type\GenderType">
<argument>%genders%</argument>
<tag name="form.type" alias="gender" />
</service>
3.1. Ricettario
311
Symfony2 documentation Documentation, Release 2
Tip: Assicurarsi che il file dei servizi sia importato. Leggere Importare la configurazione con imports per dettagli.
Assicurarsi che l’attributo alias di tags corrisponda al valore restituito dal metodo getName definito precedentemente. Si vedrà l’importanza di questo nel momento in cui si utilizzerà il tipo di campo. Ma prima, si aggiunga
al metodo __construct di GenderType un parametro, che riceverà la configurazione di gender:
# src/Acme/DemoBundle/Form/Type/GenderType.php
namespace Acme\DemoBundle\Form\Type;
// ...
class GenderType extends AbstractType
{
private $genderChoices;
public function __construct(array $genderChoices)
{
$this->genderChoices = $genderChoices;
}
public function getDefaultOptions(array $options)
{
return array(
’choices’ => $this->genderChoices,
);
}
// ...
}
Benissimo! Il tipo GenderType è ora caricato con i parametri di configurazione ed è registrato come servizio.
In quanto nella configurazione del servizio si utilizza nel form.type l’alias, utilizzare il campo risulta molto
semplice:
// src/Acme/DemoBundle/Form/Type/AuthorType.php
namespace Acme\DemoBundle\Form\Type;
// ...
class AuthorType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add(’gender_code’, ’gender’, array(
’empty_value’ => ’Choose a gender’,
));
}
}
Notare che al posto di creare l’istanza di una nuova istanza, ora è possibile riferirsi al tipo di campo tramite l’alias
utilizzato nella configurazione del servizio, gender.
3.1.23 Come creare vincoli di validazione personalizzati
È possibile creare vincoli personalizzati estendendo la classe base Symfony\Component\Validator\Constraint.
Le opzioni dei propri vincoli sono rappresentate come proprietà pubbliche della classe. Ad esempio, i vincoli di
Url includono le proprietà message (messaggio) e protocols (protocolli):
namespace Symfony\Component\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
312
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
/**
* @Annotation
*/
class Url extends Constraint
{
public $message = ’This value is not a valid URL’;
public $protocols = array(’http’, ’https’, ’ftp’, ’ftps’);
}
Note: In questo vincolo, l’annotazione @Annotation è necessaria per poterne rendere disponibile l’uso nelle
altre classi.
Come si può vedere, un vincolo è estremamente minimalistico. La validazione vera e propria è effettuata da
un’altra classe di “validazione dei vincoli”. La classe per la validazione dei vincoli è definita dal metodo del
vincolo validatedBy(), che usa una semplice logica predefinita:
// nella classe base Symfony\Component\Validator\Constraint
public function validatedBy()
{
return get_class($this).’Validator’;
}
In altre parole, se si crea un Constraint, ovvero un vincolo, personalizzato (come MioVincolo), Symfony2,
automaticamente, cercherà anche un’altra la classe, MioVincoloValidator per effettuare la validazione vera
e propria.
Anche la classe validatrice è semplice e richiede solo un metodo obbligatorio: isValid. Si prenda, ad esempio,
la classe NotBlankValidator:
class NotBlankValidator extends ConstraintValidator
{
public function isValid($value, Constraint $constraint)
{
if (null === $value || ’’ === $value) {
$this->setMessage($constraint->message);
return false;
}
return true;
}
}
Validatori di vincoli con dipendenze
Se il proprio vincolo ha delle dipendenze, come una connessione alla base dati, sarà necessario configurarlo come servizio nel contenitore delle dipendenze.
Questo servizio dovrà includere il tag
validator.constraint_validator e l’attributo alias:
• YAML
services:
validator.unique.nome_proprio_validatore:
class: Nome\Pienamente\Qualificato\Della\Classe\Validatore
tags:
- { name: validator.constraint_validator, alias: nome_alias }
• XML
<service id="validator.unique.nome_proprio_validatore" class="Nome\Pienamente\Qualificato\Del
<argument type="service" id="doctrine.orm.default_entity_manager" />
3.1. Ricettario
313
Symfony2 documentation Documentation, Release 2
<tag name="validator.constraint_validator" alias="nome_alias" />
</service>
• PHP
$container
->register(’validator.unique.nome_proprio_validatore’, ’Nome\Pienamente\Qualificato\Della
->addTag(’validator.constraint_validator’, array(’alias’ => ’nome_alias’))
;
La classe del vincolo dovrà utilizzare l’alias appena definito per riferirsi al validatore corretto:
public function validatedBy()
{
return ’nome_alias’;
}
Come già detto, Symfony2 cercherà automaticamente una classe il cui nome sia uguale a quello del vincolo ma
con il suffisso Validator. Se il proprio validatore di vincoli è definito come servizio, è importante che si
faccia l’override del metodo validatedBy() in modo tale che restituisca l’alias utilizzato nella definizione del
servizio altrimenti Symfony2 non utilizzerà il servizio di validazione dei vincoli e istanzierà la classe senza che le
dipendenze vengano iniettate.
3.1.24 Come padroneggiare e creare nuovi ambienti
Ogni applicazione è la combinazione di codice e di un insieme di configurazioni che determinano come il codice
dovrà lavorare. La configurazione può definire il database da utilizzare, cosa dovrà essere messo in cache e cosa
non, o quanto esaustivi dovranno essere i log. In Symfony2, l’idea di ambiente è quella di eseguire il codice,
utilizzando differenti configurazioni. Per esempio, l’ambiente dev dovrebbe usare una configurazione che renda
lo sviluppo semplice e ricco di informazioni, mentre l’ambiente prod dovrebbe usare un insieme di configurazioni
che ottimizzino la velocità.
Ambienti differenti, differenti file di configurazione
Una tipica applicazione Symfony2 inizia con tre ambienti: dev, prod e test. Come si è già detto, ogni
“ambiente ” rappresenta un modo in cui eseguire l’intero codice con differenti configurazioni. Non dovrebbe
destare sorpresa il fatto che ogni ambiente carichi i suoi propri file di configurazione. Se si utilizza il formato di
configurazione YAML, verranno utilizzati i seguenti file:
• per l’ambiente dev: app/config/config_dev.yml
• per l’ambiente prod: app/config/config_prod.yml
• per l’ambiente test: app/config/config_test.yml
Il funzionamento si basa su di un semplice comportamento predefinito all’interno della classe AppKernel:
// app/AppKernel.php
// ...
class AppKernel extends Kernel
{
// ...
public function registerContainerConfiguration(LoaderInterface $loader)
{
$loader->load(__DIR__.’/config/config_’.$this->getEnvironment().’.yml’);
}
}
Come si può vedere, quando Symfony2 viene caricato, utilizza l’ambiente per determinare quale file di configurazione caricare. Questo permette di avere ambienti differenti in modo elegante, efficace e trasparente.
314
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
Ovviamente, in realtà, ogni ambiente differisce solo per alcuni aspetti dagli altri. Generalmente, gli ambienti
condividono gran parte della loro configurazione. Aprendo il file di configurazione di dev, si può vedere come
questo venga ottenuto facilmente e in modo trasparente:
• YAML
imports:
- { resource: config.yml }
# ...
• XML
<imports>
<import resource="config.xml" />
</imports>
<!-- ... -->
• PHP
$loader->import(’config.php’);
// ...
Per condividere una configurazione comune, i file di configurazione di ogni ambiente importano, per iniziare, un
file di configurazione comune (config.yml). Il resto del file potrà deviare dalla configurazione predefinita,
sovrascrivendo i singoli parametri. Ad esempio, nell’ambiente dev, la barra delle applicazioni viene attivata
modificando, nel file di configurazione di dev, il relativo parametro predefinito:
• YAML
# app/config/config_dev.yml
imports:
- { resource: config.yml }
web_profiler:
toolbar: true
# ...
• XML
<!-- app/config/config_dev.xml -->
<imports>
<import resource="config.xml" />
</imports>
<webprofiler:config
toolbar="true"
# ...
/>
• PHP
// app/config/config_dev.php
$loader->import(’config.php’);
$container->loadFromExtension(’web_profiler’, array(
’toolbar’ => true,
// ..
));
3.1. Ricettario
315
Symfony2 documentation Documentation, Release 2
Eseguire un’applicazione in ambienti differenti
Per eseguire l’applicazione in ogni ambiente, sarà necessario caricarla utilizzando il front controller app.php
(per l’ambiente prod) o utilizzando il front controller app_dev.php (per l’ambiente dev):
http://localhost/app.php
http://localhost/app_dev.php
-> ambiente *prod*
-> ambiente *dev*
Note: Le precedenti URL presuppongono che il server web sia configurato in modo da usare la cartella web/
dell’applicazione, come radice. Per approfondire, si legga Installare Symfony2.
Guardando il contenuto di questi file, si vede come l’ambiente utilizzato da entrambi, sia definito in modo esplicito:
1
<?php
2
3
4
require_once __DIR__.’/../app/bootstrap_cache.php’;
require_once __DIR__.’/../app/AppCache.php’;
5
6
use Symfony\Component\HttpFoundation\Request;
7
8
9
$kernel = new AppCache(new AppKernel(’prod’, false));
$kernel->handle(Request::createFromGlobals())->send();
Si può vedere come la chiave prod specifica che l’ambiente di esecuzione sarà l’ambiente prod. Un’applicazione
Symfony2 può essere esguita in qualsiasi ambiente utilizzando lo stesso codice, cambiando la sola stringa relativa
all’ambiente.
Note: L’ambiente test è utilizzato quando si scrivono i test funzionali e non è perciò accessibile direttamente
dal browser tramite un front controller. In altre parole, diversamente dagli altri ambienti, non c’è alcun file, per il
front controller, del tipo app_test.php.
316
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
Modalità debug
Importante, ma non collegato all’argomento ambienti, è il valore false in riga 8 del precedente
front controller. Questo valore specifica se l’applicazione dovrà essere eseguità in “modalità debug” o meno. Indipendentemente dall’ambiente, un’applicazione Symfony2 può essere eseguita con
la modalità debug configurata a true o a false. Questo modifica diversi aspetti
dell’applicazione, come il fatto che gli errori vengano mostrati o se
la cache debba essere ricreata dinamicamente a ogni richiesta. Sebbene
non sia obbligatorio, la modalità debug è sempre configurata a ‘‘true
negli ambienti dev e test e a false per l’ambiente prod.
Internamente il valore della modalità debug diventa il parametro kernel.debug utilizzato all’interno
del contenitore di servizi. Dando uno sguardo al file di configurazione dell’applicazione, si vede come il
parametro venga utilizzato, ad esempio, per avviare o interrompere il logging quando si utilizza il DBAL di
Doctrine:
• YAML
doctrine:
dbal:
logging:
# ...
%kernel.debug%
• XML
<doctrine:dbal logging="%kernel.debug%" ... />
• PHP
$container->loadFromExtension(’doctrine’, array(
’dbal’ => array(
’logging’ => ’%kernel.debug%’,
// ...
),
// ...
));
Creare un nuovo ambiente
Un’applicazione Symfony2 viene generata con tre ambienti preconfigurati per gestire la maggior parte dei casi.
Ovviamente, visto che un ambiente non è nient’altro che una stringa che corrisponde ad un insieme di configurazioni, creare un nuovo ambiente è abbastanza semplice.
Supponiamo, per esempio, di voler misurare le prestazioni dell’applicazione prima del suo invio in produzione.
Un modo è quello di usare una configurazione simile a quella del rilascio ma che utilizzasse il web_profiler di
Symfony2. Queso permetterebbe a Symfony2 di registrare le informazioni dell’applicazione mentre se ne misura
le prestazioni.
Il modo migliore per ottenere tutto ciò è tramite un ambiente che si chiami, per esempio, benchmark. Si parte
creando un nuovo file di configurazione:
• YAML
# app/config/config_benchmark.yml
imports:
- { resource: config_prod.yml }
framework:
profiler: { only_exceptions: false }
• XML
<!-- app/config/config_benchmark.xml -->
3.1. Ricettario
317
Symfony2 documentation Documentation, Release 2
<imports>
<import resource="config_prod.xml" />
</imports>
<framework:config>
<framework:profiler only-exceptions="false" />
</framework:config>
• PHP
// app/config/config_benchmark.php
$loader->import(’config_prod.php’)
$container->loadFromExtension(’framework’, array(
’profiler’ => array(’only-exceptions’ => false),
));
Con queste poche e semplici modifiche, l’applicazione supporta un nuovo ambiente chiamato benchmark.
Questa nuova configurazione importa la configurazione dell’ambiente prod e la modifica. Così si garantice che
l’ambiente sia identico a quello prod eccetto per le modifiche espressamente inserite in configurazione.
Siccome sarà necessario che l’ambiente sia accessibile tramite browser, sarà necessario creare un apposito front
controller. Basterà copiare il file web/app.php nel file web/app_benchmark.php e modificare l’ambiente
in modo che punti a benchmark:
<?php
require_once __DIR__.’/../app/bootstrap.php’;
require_once __DIR__.’/../app/AppKernel.php’;
use Symfony\Component\HttpFoundation\Request;
$kernel = new AppKernel(’benchmark’, false);
$kernel->handle(Request::createFromGlobals())->send();
Il nuovo ambiente sarà accessibile tramite:
http://localhost/app_benchmark.php
Note: Alcuni ambienti, come il dev, non dovrebbero mai essere accessibile su di un server pubblico di produzione. Questo perché alcuni ambienti, per facilitarne il debug, potrebbero fornire troppe informazioni relative
all’infrastruttura sottostante l’applicazione. Per essere sicuri che questi ambienti non siano accessibili, il front
controller è solitamente protetto dall’accesso da parte di indirizzi IP esterni tramite il seguente codice, posto in
cima al controllore:
if (!in_array(@$_SERVER[’REMOTE_ADDR’], array(’127.0.0.1’, ’::1’))) {
die(’You are not allowed to access this file. Check ’.basename(__FILE__).’ for more infor
}
Gli ambienti e la cartella della cache
Symfony2 sfrutta la cache in diversi modi: la configurazione dell’applicazione, la configurazione delle rotte, i
template di Twig vengono tutti immagazzinati in oggetti PHP e salvati su file nella cartella della cache.
Normalmente questi file sono conservati principalmente nella cartella app/cache. Comunque ogni ambiente
usa il suo proprio insieme di file della cache:
app/cache/dev
app/cache/prod
318
- cartella per la cache dell’ambiente *dev*
- cartella per la cache dell’ambiente *prod*
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
Alcune volte, durante il debug, può essere utile poter controllare i file salvati in cache, per capire come le cose
stiano funzionando. In questi casi bisogna ricordarsi di guardare nella cartella dell’ambiente che si sta utilizzando (solitamente, in fase di sviluppo e debug, il dev). Sebbene possa variare, il contenuto della cartella
app/cache/dev includerà i seguenti file:
• appDevDebugProjectContainer.php - il “contenitore di servizi” salvato in cache che rappresenta
la configurazione dell’applicazione;
• appdevUrlGenerator.php - la classe PHP generata a partire dalla configurazione delle rotte e usata
nella generazione degli URL;
• appdevUrlMatcher.php - la classe PHP utilizzata per ricercare le rotte: qui è possibile vedere le
espressioni regolari utilizzate per associare gli URL in ingresso con le rotte disponibili;
• twig/ - questa cartella contiene la cache dei template di Twig.
Approfondimenti
Si legga l’articolo Configurare parametri esterni nel contenitore dei servizi.
3.1.25 Configurare parametri esterni nel contenitore dei servizi
Nel capitolo Come padroneggiare e creare nuovi ambienti, si è visto come gestire la configurazione
dell’applicazione. Alle volte potrebbe essere utile, per l’applicazione, salvare alcune credenziali al di fuori del
codice del progetto. Ad esempio la configurazione dell’accesso alla base dati. La flessibilità del contenitore dei
servizi di symfony permette di farlo in modo agevole.
Variabili d’ambiente
Symfony recupera qualsiasi variabile d’ambiente, il cui prefisso sia SYMFONY__ e la usa come un parametro
all’interno del contenitore dei servizi. Il doppio trattino basso viene sostituito da un punto, dato che il punto non
è un carattere valido per i nomi delle variabili d’ambiente.
Ad esempio, se si usa l’ambiente Apache, le variabili d’ambiente possono essere configurate utilizzando la
seguente configurazione del VirtualHost:
<VirtualHost *:80>
ServerName
DocumentRoot
DirectoryIndex
SetEnv
SetEnv
Symfony2
"/percorso/applicazione/symfony_2/web"
index.php index.html
SYMFONY__UTENTE__DATABASE utente
SYMFONY__PASSWORD__DATABASE segreto
<Directory "/percorso/applicazione/symfony_2/web">
AllowOverride All
Allow from All
</Directory>
</VirtualHost>
Note: Il precedente esempio è relativo alla configurazione di Apache e utilizza la direttiva SetEnv. Comunque,
lo stesso concetto si applica a qualsiasi server web che supporti la configurazione delle variabili d’ambiente.
Inoltre, per far si che possa funzionare anche per la riga di comando (che non utilizza Apache), sarà necessario
esportare i parametri come variabili di shell. Su di un sistema Unix, lo si può fare con il seguente comando:
export SYMFONY__UTENTE__DATABASE=utente
export SYMFONY__PASSWORD__DATABASE=segreto
3.1. Ricettario
319
Symfony2 documentation Documentation, Release 2
Una volta dichiarate, le variabili saranno disponibili all’interno della variabile globale $_SERVER di PHP. Symfony si occuperà di trasformare tutte le variabili di $_SERVER, con prefisso SYMFONY__, in parametri per il
contenitore dei servizi.
A questo punto, sarà possibile richiamare questi parametri ovunque sia necessario.
• YAML
doctrine:
dbal:
driver
dbname:
user:
password:
pdo_mysql
symfony2_project
%utente.database%
%password.database%
• XML
<!-- xmlns:doctrine="http://symfony.com/schema/dic/doctrine" -->
<!-- xsi:schemaLocation="http://symfony.com/schema/dic/doctrine http://symfony.com/schema/dic
<doctrine:config>
<doctrine:dbal
driver="pdo_mysql"
dbname="progetto_symfony2"
user="%utente.database%"
password="%password.database%"
/>
</doctrine:config>
• PHP
$container->loadFromExtension(’doctrine’, array(’dbal’ => array(
’driver’
=> ’pdo_mysql’,
’dbname’
=> ’progetto_symfony2’,
’user’
=> ’%utente.database%’,
’password’ => ’%password.database%’,
));
Costanti
Il contenitore permette di usare anche le costanti PHP come parametri. Per poter usare questa funzionalità, si
dovrà associare la costante alla chiave del parametro e definirne il tipo come constant.
<?xml version="1.0" encoding="UTF-8"?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
>
<parameters>
<parameter key="valore.costante.globale" type="constant">COSTANTE_GLOBALE</parameter>
<parameter key="mia_classe.valore.constante" type="constant">Mia_Classe::NOME_COSTANT
</parameters>
</container>
Note: Per funzionare è necessario che la configurazione usi l’XML. Se non si sta usando l’XML, per sfruttare
questa funzionalità, basta importarne uno:
// app/config/config.yml
imports:
- { resource: parametri.xml }
320
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
Configurazioni varie
La direttiva import può essere usata per importare parametri conservati in qualsiasi parte. Importare un file PHP
permette di avere la flessibilità di aggiungere qualsiasi cosa sia necessaria al contenitore. Il seguente esempio
importa un file di nome parametri.php.
• YAML
# app/config/config.yml
imports:
- { resource: parametri.php }
• XML
<!-- app/config/config.xml -->
<imports>
<import resource="parametri.php" />
</imports>
• PHP
// app/config/config.php
$loader->import(’parametri.php’);
Note: Un file di risorse può essere espresso in diversi formati. PHP, XML, YAML, INI e risorse di closure, sono
tutti supportati dalla direttiva imports.
parametri.php conterrà i parametri che si vuole che il contenitore dei servizi configuri. Questo è specialmente
utile nel caso si voglia importare una configurazione con formato non standard. Il seguente esempio importa la
configurazione di una base dati per Drupal in un contenitore di servizi symfony.
// app/config/parameters.php
include_once(’/percorso/al/sito/drupal/default/settings.php’);
$container->setParameter(’url.database.drupal’, $db_url);
3.1.26 Usare il factory per creare servizi
Il contenitore di servizi di Symfony2 mette a disposizione potenti strumenti per la creazione di oggetti, permettendo di specificare sia i parametri da passare al costruttore, sia i metodi di chiamata, che i parametri di configurazione. Alle volte, però, questo non è sufficiente a soddisfare tutti i requisiti per la creazione dei propri oggetti.
In questi casi, è possibile usare un factory per la creazione di oggetti e fare in modo che il contenitore di servizi
chiami uno specifico metodo nel factory, invece che inizializzare direttamente l’oggetto.
Supponiamo di avere un factory che configura e restituisce un oggetto GestoreNewsletter:
namespace Acme\HelloBundle\Newsletter;
class NewsletterFactory
{
public function get()
{
$gestoreNewsletter = new GestoreNewsletter();
// ...
return $gestoreNewsletter;
}
}
3.1. Ricettario
321
Symfony2 documentation Documentation, Release 2
Per rendere disponibile, in forma di servizio, l’oggetto GestoreNewsletter, è possibile configurare un contenitore di servizi in modo che usi la classe factory NewsletterFactory:
• YAML
# src/Acme/HelloBundle/Resources/config/services.yml
parameters:
# ...
gestore_newsletter.class: Acme\HelloBundle\Newsletter\GestoreNewsletter
newsletter_factory.class: Acme\HelloBundle\Newsletter\NewsletterFactory
services:
gestore_newsletter:
class:
%gestore_newsletter.class%
factory_class: %newsletter_factory.class%
factory_method: get
• XML
<!-- src/Acme/HelloBundle/Resources/config/services.xml -->
<parameters>
<!-- ... -->
<parameter key="gestore_newsletter.class">Acme\HelloBundle\Newsletter\GestoreNewsletter</
<parameter key="newsletter_factory.class">Acme\HelloBundle\Newsletter\NewsletterFactory</
</parameters>
<services>
<service id="gestore_newsletter"
class="%gestore_newsletter.class%"
factory-class="%newsletter_factory.class%"
factory-method="get"
/>
</services>
• PHP
// src/Acme/HelloBundle/Resources/config/services.php
use Symfony\Component\DependencyInjection\Definition;
// ...
$container->setParameter(’gestore_newsletter.class’, ’Acme\HelloBundle\Newsletter\GestoreNews
$container->setParameter(’newsletter_factory.class’, ’Acme\HelloBundle\Newsletter\NewsletterF
$container->setDefinition(’gestore_newsletter’, new Definition(
’%gestore_newsletter.class%’
))->setFactoryClass(
’%newsletter_factory.class%’
)->setFactoryMethod(
’get’
);
Quando si specifica la classe da utilizzare come factory (tramite factory_class) il metodo verrà chiamato
staticamente. Se il factory stesso dovesse essere istanziato e il relativo metodo dell’oggetto sia chiamato (come
nell’esempio), si dovrà configurare il factory come servizio:
• YAML
# src/Acme/HelloBundle/Resources/config/services.yml
parameters:
# ...
gestore_newsletter.class: Acme\HelloBundle\Newsletter\GestoreNewsletter
newsletter_factory.class: Acme\HelloBundle\Newsletter\NewsletterFactory
services:
newsletter_factory:
class:
%newsletter_factory.class%
gestore_newsletter:
322
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
class:
factory_service:
factory_method:
%gestore_newsletter.class%
newsletter_factory
get
• XML
<!-- src/Acme/HelloBundle/Resources/config/services.xml -->
<parameters>
<!-- ... -->
<parameter key="gestore_newsletter.class">Acme\HelloBundle\Newsletter\GestoreNewsletter</
<parameter key="newsletter_factory.class">Acme\HelloBundle\Newsletter\NewsletterFactory</
</parameters>
<services>
<service id="newsletter_factory" class="%newsletter_factory.class%"/>
<service id="gestore_newsletter"
class="%gestore_newsletter.class%"
factory-service="newsletter_factory"
factory-method="get"
/>
</services>
• PHP
// src/Acme/HelloBundle/Resources/config/services.php
use Symfony\Component\DependencyInjection\Definition;
// ...
$container->setParameter(’gestore_newsletter.class’, ’Acme\HelloBundle\Newsletter\GestoreNews
$container->setParameter(’newsletter_factory.class’, ’Acme\HelloBundle\Newsletter\NewsletterF
$container->setDefinition(’newsletter_factory’, new Definition(
’%newsletter_factory.class%’
))
$container->setDefinition(’gestore_newsletter’, new Definition(
’%gestore_newsletter.class%’
))->setFactoryService(
’newsletter_factory’
)->setFactoryMethod(
’get’
);
Note: Il servizio del factory viene specificato tramite il suo nome id e non come un riferimento al servizio stesso.
Perciò non è necessario usare la sintassi con @.
Passaggio di argomenti al metodo del factory
Per poter passare argomenti al metodo del factory, si può utilizzare l’opzione arguments all’interno del contenitore di servizi. Si supponga, ad esempio, che il metodo get, del precedente esempio, accetti il servizio
templating come argomento:
• YAML
# src/Acme/HelloBundle/Resources/config/services.yml
parameters:
# ...
gestore_newsletter.class: Acme\HelloBundle\Newsletter\GestoreNewsletter
newsletter_factory.class: Acme\HelloBundle\Newsletter\NewsletterFactory
services:
newsletter_factory:
class:
%newsletter_factory.class%
3.1. Ricettario
323
Symfony2 documentation Documentation, Release 2
gestore_newsletter:
class:
factory_service:
factory_method:
arguments:
-
%gestore_newsletter.class%
newsletter_factory
get
@templating
• XML
<!-- src/Acme/HelloBundle/Resources/config/services.xml -->
<parameters>
<!-- ... -->
<parameter key="gestore_newsletter.class">Acme\HelloBundle\Newsletter\GestoreNewsletter</
<parameter key="newsletter_factory.class">Acme\HelloBundle\Newsletter\NewsletterFactory</
</parameters>
<services>
<service id="newsletter_factory" class="%newsletter_factory.class%"/>
<service id="gestore_newsletter"
class="%gestore_newsletter.class%"
factory-service="newsletter_factory"
factory-method="get"
>
<argument type="service" id="templating" />
</service>
</services>
• PHP
// src/Acme/HelloBundle/Resources/config/services.php
use Symfony\Component\DependencyInjection\Definition;
// ...
$container->setParameter(’gestore_newsletter.class’, ’Acme\HelloBundle\Newsletter\GestoreNews
$container->setParameter(’newsletter_factory.class’, ’Acme\HelloBundle\Newsletter\NewsletterF
$container->setDefinition(’newsletter_factory’, new Definition(
’%newsletter_factory.class%’
))
$container->setDefinition(’gestore_newsletter’, new Definition(
’%gestore_newsletter.class%’,
array(new Reference(’templating’))
))->setFactoryService(
’newsletter_factory’
)->setFactoryMethod(
’get’
);
3.1.27 Gestire le dipendenza comuni con i servizi padre
Aggiungendo funzionalità alla propria applicazione, si può arrivare ad un punto in cui classi tra loro collegate
condividano alcune dipendenze. Si potrebbe avere, ad esempio, un Gestore Newsletter che usa una setter injection
per configurare le proprie dipendenze:
namespace Acme\HelloBundle\Mail;
use Acme\HelloBundle\Mailer;
use Acme\HelloBundle\FormattatoreMail;
class GestoreNewsletter
{
protected $mailer;
324
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
protected $formattatoreMail;
public function setMailer(Mailer $mailer)
{
$this->mailer = $mailer;
}
public function setFormattatoreMail(FormattatoreMail $formattatoreMail)
{
$this->formattatoreMail = $formattatoreMail;
}
// ...
}
ed una classe BigliettoAuguri che condivide le stesse dipendenze:
namespace Acme\HelloBundle\Mail;
use Acme\HelloBundle\Mailer;
use Acme\HelloBundle\FormattatoreMail;
class GestoreBigliettoAuguri
{
protected $mailer;
protected $formattatoreMail;
public function setMailer(Mailer $mailer)
{
$this->mailer = $mailer;
}
public function setFormattatoreMail(FormattatoreMail $formattatoreMail)
{
$this->formattatoreMail = $formattatoreMail;
}
// ...
}
La configurazione del servizio per queste classi sarà simile alla seguente:
• YAML
# src/Acme/HelloBundle/Resources/config/services.yml
parameters:
# ...
gestore_newsletter.class: Acme\HelloBundle\Mail\GestoreNewsletter
gestore_biglietto_auguri.class: Acme\HelloBundle\Mail\GestoreBigliettoAuguri
services:
mio_mailer:
# ...
mio_formattatore_mail:
# ...
gestore_newsletter:
class:
%gestore_newsletter.class%
calls:
- [ setMailer, [ @mio_mailer ] ]
- [ setFormattatoreMail, [ @mio_formattatore_mail] ]
gestore_biglietto_auguri:
class:
%gestore_biglietto_auguri.class%
calls:
- [ setMailer, [ @mio_mailer ] ]
- [ setFormattatoreMail, [ @mio_formattatore_mail] ]
3.1. Ricettario
325
Symfony2 documentation Documentation, Release 2
• XML
<!-- src/Acme/HelloBundle/Resources/config/services.xml -->
<parameters>
<!-- ... -->
<parameter key="gestore_newsletter.class">Acme\HelloBundle\Mail\GestoreNewsletter</parame
<parameter key="gestore_biglietto_auguri.class">Acme\HelloBundle\Mail\GestoreBigliettoAug
</parameters>
<services>
<service id="mio_mailer" ... >
<!-- ... -->
</service>
<service id="mio_formattatore_mail" ... >
<!-- ... -->
</service>
<service id="gestore_newsletter" class="%gestore_newsletter.class%">
<call method="setMailer">
<argument type="service" id="mio_mailer" />
</call>
<call method="setFormattatoreMail">
<argument type="service" id="mio_formattatore_mail" />
</call>
</service>
<service id="gestore_biglietto_auguri" class="%gestore_biglietto_auguri.class%">
<call method="setMailer">
<argument type="service" id="mio_mailer" />
</call>
<call method="setFormattatoreMail">
<argument type="service" id="mio_formattatore_mail" />
</call>
</service>
</services>
• PHP
// src/Acme/HelloBundle/Resources/config/services.php
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
// ...
$container->setParameter(’gestore_newsletter.class’, ’Acme\HelloBundle\Mail\GestoreNewsletter
$container->setParameter(’gestore_biglietto_auguri.class’, ’Acme\HelloBundle\Mail\GestoreBigl
$container->setDefinition(’mio_mailer’, ... );
$container->setDefinition(’mio_formattatore_mail’, ... );
$container->setDefinition(’gestore_newsletter’, new Definition(
’%gestore_newsletter.class%’
))->addMethodCall(’setMailer’, array(
new Reference(’mio_mailer’)
))->addMethodCall(’setFormattatoreMail’, array(
new Reference(’mio_formattatore_mail’)
));
$container->setDefinition(’gestore_biglietto_auguri’, new Definition(
’%gestore_biglietto_auguri.class%’
))->addMethodCall(’setMailer’, array(
new Reference(’mio_mailer’)
))->addMethodCall(’setFormattatoreMail’, array(
new Reference(’mio_formattatore_mail’)
));
Ci sono molte ripetizioni, sia nelle classi che nella configurazione. Questo vuol dire che se qualcosa viene cambiato, ad esempio le classi Mailer o FormattatoreMail che dovranno essere iniettate tramite il costruttore,
sarà necessario modificare la configurazione in due posti. Allo stesso modo, se si volesse modificare il metodo
326
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
setter, sarebbe necessario modificare entrambe le classi. Il tipico modo di gestire i metodi comuni di queste classi
sarebbe quello di far si che estendano una super classe comune:
namespace Acme\HelloBundle\Mail;
use Acme\HelloBundle\Mailer;
use Acme\HelloBundle\FormattatoreMail;
abstract class GestoreMail
{
protected $mailer;
protected $formattatoreMail;
public function setMailer(Mailer $mailer)
{
$this->mailer = $mailer;
}
public function setFormattatoreMail(EmailFormatter $formattatoreMail)
{
$this->formattatoreMail = $formattatoreMail;
}
// ...
}
Le classi GestoreNewsletter e GestoreBigliettoAuguri potranno estendere questa super classe:
namespace Acme\HelloBundle\Mail;
class GestoreNewsletter extends GestoreMail
{
// ...
}
e:
namespace Acme\HelloBundle\Mail;
class GestoreBigliettoAuguri extends GestoreMail
{
// ...
}
Allo stesso modo, il contenitore di servizi di Symfony2 supporta la possibilità di estendere i servizi nella configurazione in modo da poter ridurre le ripetizioni specificando un servizio padre.
• YAML
# src/Acme/HelloBundle/Resources/config/services.yml
parameters:
# ...
gestore_newsletter.class: Acme\HelloBundle\Mail\GestoreNewsletter
gestore_biglietto_auguri.class: Acme\HelloBundle\Mail\GestoreBigliettoAuguri
gestore_mail.class: Acme\HelloBundle\Mail\GestoreMail
services:
mio_mailer:
# ...
mio_formattatore_mail:
# ...
gestore_mail:
class:
%gestore_mail.class%
abstract: true
calls:
- [ setMailer, [ @mio_mailer ] ]
- [ setFormattatoreMail, [ @mio_formattatore_mail] ]
3.1. Ricettario
327
Symfony2 documentation Documentation, Release 2
gestore_newsletter:
class:
%gestore_newsletter.class%
parent: gestore_mail
gestore_biglietto_auguri:
class:
%gestore_biglietto_auguri.class%
parent: gestore_mail
• XML
<!-- src/Acme/HelloBundle/Resources/config/services.xml -->
<parameters>
<!-- ... -->
<parameter key="gestore_newsletter.class">Acme\HelloBundle\Mail\GestoreNewsletter</parame
<parameter key="gestore_biglietto_auguri.class">Acme\HelloBundle\Mail\GestoreBigliettoAug
<parameter key="gestore_mail.class">Acme\HelloBundle\Mail\GestoreMail</parameter>
</parameters>
<services>
<service id="mio_mailer" ... >
<!-- ... -->
</service>
<service id="mio_formattatore_mail" ... >
<!-- ... -->
</service>
<service id="gestore_mail" class="%gestore_mail.class%" abstract="true">
<call method="setMailer">
<argument type="service" id="mio_mailer" />
</call>
<call method="setFormattatoreMail">
<argument type="service" id="mio_formattatore_mail" />
</call>
</service>
<service id="gestore_newsletter" class="%gestore_newsletter.class%" parent="gestore_mail"
<service id="gestore_biglietto_auguri" class="%gestore_biglietto_auguri.class%" parent="g
</services>
• PHP
// src/Acme/HelloBundle/Resources/config/services.php
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
// ...
$container->setParameter(’gestore_newsletter.class’, ’Acme\HelloBundle\Mail\GestoreNewsletter
$container->setParameter(’gestore_biglietto_auguri.class’, ’Acme\HelloBundle\Mail\GestoreBigl
$container->setParameter(’gestore_mail.class’, ’Acme\HelloBundle\Mail\GestoreMail’);
$container->setDefinition(’mio_mailer’, ... );
$container->setDefinition(’mio_formattatore_mail’, ... );
$container->setDefinition(’gestore_mail’, new Definition(
’%gestore_mail.class%’
))->SetAbstract(
true
)->addMethodCall(’setMailer’, array(
new Reference(’mio_mailer’)
))->addMethodCall(’setFormattatoreMail’, array(
new Reference(’mio_formattatore_mail’)
));
$container->setDefinition(’gestore_newsletter’, new DefinitionDecorator(
’gestore_mail’
))->setClass(
’%gestore_newsletter.class%’
328
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
);
$container->setDefinition(’gestore_biglietto_auguri’, new DefinitionDecorator(
’gestore_mail’
))->setClass(
’%gestore_biglietto_auguri.class%’
);
In questo contesto, avere un servizio padre implica che gli argomenti e le chiamate dei metodi del servizio padre
dovrebbero essere utilizzati per i servizi figli. Nello specifico, i metodi setter definiti nel servizio padre verranno
chiamati quando i servizi figli saranno istanziati.
Note: Rimuovendo la chiave di configurazione parent i servizi verranno comunque istanziati e estenderanno
comunque la classe GestoreMail. La differenza è che, omettendo la chiave di configurazione parent, le
chiamate definite nel servizio gestore_mail non saranno eseguite quando i servizi figli saranno istanziati.
La classe padre è astratta e dovrebbe essere istanziata direttamente. Configurarla come astratta nel file di configurazione, così come è stato fatto precedentemente, implica che potrà essere usata come servizio padre e che non
potrà essere utilizzata direttamente come servizio da iniettare e che verrà rimossa in fase di compilazione. In altre
parole, esisterà semplicemente come un “template” che altri servizi potranno usare.
Override delle dipendenze della classe padre
Potrebbe succedere che sia preferibile fare l’override della classe passata come dipendenza di un servizio figlio.
Fortunatamente, aggiungendo la configurazione della chiamata al metodo per il servizio figlio, le dipendenze
configurate nella classe padre verranno sostituite. Perciò, nel caso si volesse passare una dipendenza diversa solo
per la classe GestoreNewsletter, la configurazione sarà simile alla seguente:
• YAML
# src/Acme/HelloBundle/Resources/config/services.yml
parameters:
# ...
gestore_newsletter.class: Acme\HelloBundle\Mail\GestoreNewsletter
gestore_biglietto_auguri.class: Acme\HelloBundle\Mail\GestoreBigliettoAuguri
gestore_mail.class: Acme\HelloBundle\Mail\GestoreMail
services:
mio_mailer:
# ...
mio_mailer_alternativo:
# ...
mio_formattatore_mail:
# ...
gestore_mail:
class:
%gestore_mail.class%
abstract: true
calls:
- [ setMailer, [ @mio_mailer ] ]
- [ setFormattatoreMail, [ @mio_formattatore_mail] ]
gestore_newsletter:
class:
%gestore_newsletter.class%
parent: gestore_mail
calls:
- [ setMailer, [ @mio_mailer_alternativo ] ]
gestore_biglietto_auguri:
class:
%gestore_biglietto_auguri.class%
parent: gestore_mail
• XML
3.1. Ricettario
329
Symfony2 documentation Documentation, Release 2
<!-- src/Acme/HelloBundle/Resources/config/services.xml -->
<parameters>
<!-- ... -->
<parameter key="gestore_newsletter.class">Acme\HelloBundle\Mail\GestoreNewsletter</parame
<parameter key="gestore_biglietto_auguri.class">Acme\HelloBundle\Mail\GestoreBigliettoAug
<parameter key="gestore_mail.class">Acme\HelloBundle\Mail\GestoreMail</parameter>
</parameters>
<services>
<service id="mio_mailer" ... >
<!-- ... -->
</service>
<service id="mio_mailer_alternativo" ... >
<!-- ... -->
</service>
<service id="mio_formattatore_mail" ... >
<!-- ... -->
</service>
<service id="gestore_mail" class="%gestore_mail.class%" abstract="true">
<call method="setMailer">
<argument type="service" id="mio_mailer" />
</call>
<call method="setFormattatoreMail">
<argument type="service" id="mio_formattatore_mail" />
</call>
</service>
<service id="gestore_newsletter" class="%gestore_newsletter.class%" parent="gestore_mail"
<call method="setMailer">
<argument type="service" id="mio_mailer_alternativo" />
</call>
</service>
<service id="gestore_biglietto_auguri" class="%gestore_biglietto_auguri.class%" parent="g
</services>
• PHP
// src/Acme/HelloBundle/Resources/config/services.php
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
// ...
$container->setParameter(’gestore_newsletter.class’, ’Acme\HelloBundle\Mail\GestoreNewsletter
$container->setParameter(’gestore_biglietto_auguri.class’, ’Acme\HelloBundle\Mail\GestoreBigl
$container->setParameter(’gestore_mail.class’, ’Acme\HelloBundle\Mail\GestoreMail’);
$container->setDefinition(’mio_mailer’, ... );
$container->setDefinition(’mio_mailer_alternativo’, ... );
$container->setDefinition(’mio_formattatore_mail’, ... );
$container->setDefinition(’gestore_mail’, new Definition(
’%gestore_mail.class%’
))->SetAbstract(
true
)->addMethodCall(’setMailer’, array(
new Reference(’mio_mailer’)
))->addMethodCall(’setFormattatoreMail’, array(
new Reference(’mio_formattatore_mail’)
));
$container->setDefinition(’gestore_newsletter’, new DefinitionDecorator(
’gestore_mail’
))->setClass(
’%gestore_newsletter.class%’
)->addMethodCall(’setMailer’, array(
new Reference(’mio_mailer_alternativo’)
330
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
));
$container->setDefinition(’gestore_newsletter’, new DefinitionDecorator(
’gestore_mail’
))->setClass(
’%gestore_biglietto_auguri.class%’
);
Il GestoreBigliettoAuguri riceverà le stesse dipendenze di prima mentre al GestoreNewsletter
verrà passato il mio_mailer_alternativo invece del servizio mio_mailer.
Collezioni di dipendenze
È da notare che il metodo setter di cui si è fatto l’override nel precedente esempio viene chiamato due volte: una
volta nella definizione del padre e una nella definizione del figlio. Nel precedente esempio la cosa va bene, visto
che la chiamata al secondo setMailer sostituisce l’oggetto mailer configurato nella prima chiamata.
In alcuni casi, però, questo potrebbe creare problemi. Ad esempio, nel caso in cui il metodo per cui si fa l’override
dovesse aggiungere qualcosa ad una collezione, si potrebbero aggiungere due oggetti alla collezione. Di seguito
se ne può vedere un esempio:
namespace Acme\HelloBundle\Mail;
use Acme\HelloBundle\Mailer;
use Acme\HelloBundle\FormattatoreMail;
abstract class GestoreMail
{
protected $filtri;
public function setFiltro($filtro)
{
$this->filtri[] = $filtro;
}
// ...
}
Ipotizziamo di avere la seguente configurazione:
• YAML
# src/Acme/HelloBundle/Resources/config/services.yml
parameters:
# ...
gestore_newsletter.class: Acme\HelloBundle\Mail\GestoreNewsletter
gestore_mail.class: Acme\HelloBundle\Mail\GestoreMail
services:
mio_filtro:
# ...
altro_filtro:
# ...
gestore_mail:
class:
%gestore_mail.class%
abstract: true
calls:
- [ setFiltro, [ @mio_filtro ] ]
gestore_newsletter:
class:
%gestore_newsletter.class%
parent: gestore_mail
calls:
- [ setFiltro, [ @altro_filtro ] ]
• XML
3.1. Ricettario
331
Symfony2 documentation Documentation, Release 2
<!-- src/Acme/HelloBundle/Resources/config/services.xml -->
<parameters>
<!-- ... -->
<parameter key="gestore_newsletter.class">Acme\HelloBundle\Mail\GestoreNewsletter</parame
<parameter key="gestore_mail.class">Acme\HelloBundle\Mail\GestoreMail</parameter>
</parameters>
<services>
<service id="mio_filtro" ... >
<!-- ... -->
</service>
<service id="altro_filtro" ... >
<!-- ... -->
</service>
<service id="gestore_mail" class="%gestore_mail.class%" abstract="true">
<call method="setFiltro">
<argument type="service" id="mio_filtro" />
</call>
</service>
<service id="gestore_newsletter" class="%gestore_newsletter.class%" parent="gestore_mail"
<call method="setFiltro">
<argument type="service" id="altro_filtro" />
</call>
</service>
</services>
• PHP
// src/Acme/HelloBundle/Resources/config/services.php
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
// ...
$container->setParameter(’gestore_newsletter.class’, ’Acme\HelloBundle\Mail\GestoreNewsletter
$container->setParameter(’gestore_mail.class’, ’Acme\HelloBundle\Mail\GestoreMail’);
$container->setDefinition(’mio_filtro’, ... );
$container->setDefinition(’altro_filtro’, ... );
$container->setDefinition(’gestore_mail’, new Definition(
’%gestore_mail.class%’
))->SetAbstract(
true
)->addMethodCall(’setFiltro’, array(
new Reference(’mio_filtro’)
));
$container->setDefinition(’gestore_newsletter’, new DefinitionDecorator(
’gestore_mail’
))->setClass(
’%gestore_newsletter.class%’
)->addMethodCall(’setFiltro’, array(
new Reference(’altro_filtro’)
));
In questo caso il metodo setFiltro del servizio gestore_newsletter verrebbe chiamato due volte
cosa che produrrà, come risultato che l’array $filtri conterrà sia l’oggetto mio_filtro che l’oggetto
altro_filtro. Il che va bene se l’obbiettivo è quello di avere più filtri nella sotto classe. Ma se si volesse sostituire il filtro passato alla sotto classe, la rimozione della configurazione della classe padre eviterà che la
classe base chiami il metodo setFiltro.
332
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
3.1.28 Come lavorare con gli scope
Questa ricetta parla di scope, un argomento alquanto avanzato, relativo al Contenitore di servizi. Se si ottiene un
errore che menziona gli “scopes” durante la creazione di servizi oppure se si ha l’esigenza di creare un servizio
che dipenda dal servizio request, questa è la ricetta giusta.
Capure gli scope
Lo scope di un servizio controlla quanto a lungo un’istanza di un servizio è usata dal contenitore. Il componente
Dependency Injection fornisce due scope generici:
• container (quello predefinito): la stessa istanza è usata ogni volta che la si richiede da questo contenitore.
• prototype: viene creata una nuova istanza, ogni volta che si richiede il servizio.
FrameworkBundle definisce anche un terzo scope: request. Questi scope sono legati alla richiesta, il che vuol dire
che viene creata una nuova istanza per ogni sotto-richiesta, non disponibile al di fuori della richiesta stessa (per
esempio nella CLI).
Gli scope aggiungono un vincolo sulle dipendenze di un servizio:
un servizio non
può dipendere da servizi con scope più stretti.
Per esempio, se si crea un generico servizio pippo, ma si prova a iniettare il componente request, si riceverà una
Symfony\Component\DependencyInjection\Exception\ScopeWideningInjectionException
alla compilazione del contenitore. Leggere la nota seguente sotto per maggiori dettagli.
Scope e dipendenze
Si immagini di aver configurato un servizio posta. Non è stato configurato lo scope del servizio, quindi ha
container. In altre parole, ogni volta che si chiede al contenitore il servizio posta, si ottiene lo stesso oggetto.
Solitamente, si vuole che un servizio funzioni in questo modo.
Si immagini, tuttavia, di aver bisogno del servizio request da dentro posta, magari perché si deve leggere
l’URL della richiesta corrente. Lo si aggiunge quindi come parametro del costruttore. Vediamo quali problemi si presentano:
• Alla richiesta di posta, viene creata un’istanza di posta (chiamiamola PostaA), a cui viene passato il
servizio request (chiamiamolo RequestA). Fin qui tutto bene.
• Si effettua ora una sotto-richiesta in Symfony, che è un modo carino per dire che è stata chiamata,
per esempio, la funzione {% render ... %} di Twig, che esegue un altro controllore. Internamente, il
vecchio servizio request (RequestA) viene effettivamente sostituito da una nuova istanza di richiesta
(RequestB). Questo avviene in background ed è del tutto normale.
• Nel proprio controllore incluso, si richiede nuovamente il servizio posta. Poiché il servizio è nello
scope container scope, viene riutilizzata la stessa istanza (PostaA). Ma ecco il problema: l’istanza
PostaA contiene ancora il vecchio oggetto RequestA, che non è ora l’oggetto di richiesta corretto
da avere (attualmente RequestB è il servizio request). La differenza è sottile, ma questa mancata
corrispondenza potrebbe causare grossi guai, per questo non è consentita.
Questa è dunque la ragione per cui esistono gli scope e come possono causare problemi. Vedremo più
avanti delle soluzioni comuni.
Note: Ovviamente, un servizio può dipendere senza alcun problema da un altro servizio che abbia uno scope più
ampio, .
Impostare lo scope nella definizione
Lo scope di un servizio è definito nella definizione del servizio stesso:
• YAML
3.1. Ricettario
333
Symfony2 documentation Documentation, Release 2
# src/Acme/HelloBundle/Resources/config/services.yml
services:
greeting_card_manager:
class: Acme\HelloBundle\Mail\GreetingCardManager
scope: request
• XML
<!-- src/Acme/HelloBundle/Resources/config/services.xml -->
<services>
<service id="greeting_card_manager" class="Acme\HelloBundle\Mail\GreetingCardManager" sco
</services>
• PHP
// src/Acme/HelloBundle/Resources/config/services.php
use Symfony\Component\DependencyInjection\Definition;
$container->setDefinition(
’greeting_card_manager’,
new Definition(’Acme\HelloBundle\Mail\GreetingCardManager’)
)->setScope(’request’);
Se non si specifica lo scope, viene usato container, che è quello che si desidera la maggior parte delle volte. A
meno che il proprio servizio non dipenda da un altro servizio con uno scope più stretto (solitamente, il servizio
request), probabilmente non si avrà bisogno di impostare lo scope.
Usare un servizio da uno scope più stretto
Se il proprio servizio dipende da un servizio con scope, la soluzione migliore è metterlo nello stesso scope (o in
uno pià stretto). Di solito, questo vuol dire mettere il proprio servizio nello scope request.
Ma questo non è sempre possibile (per esempio, un’estensione di Twig deve stare nello scope container, perché
l’ambiente di Twig ne ha bisogno per le sue dipendenze). In questi casi, si dovrebbe passare l’intero contenitore
dentro il proprio servizio e recuperare le proprie dipendenze dal contenitore ogni volta che servono, per assicurarsi
di avere l’istanza giusta:
namespace Acme\HelloBundle\Mail;
use Symfony\Component\DependencyInjection\ContainerInterface;
class Mailer
{
protected $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
public function sendEmail()
{
$request = $this->container->get(’request’);
// Fare qualcosa con la richiesta in questo punto
}
}
Caution: Si faccia attenzione a non memorizzare la richiesta in una proprietà dell’oggetto per una chiamata
futura del servizio, perché causerebbe lo stesso problema spiegato nella prima sezione (tranne per il fatto che
Symfony non è in grado di individuare l’errore).
La configurazione del servizio per questa classe assomiglia a questa:
334
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
• YAML
# src/Acme/HelloBundle/Resources/config/services.yml
parameters:
# ...
posta.class: Acme\HelloBundle\Mail\Mailer
services:
posta:
class:
%posta.class%
arguments:
- "@service_container"
# scope: container può essere omesso, perché è il predefinito
• XML
<!-- src/Acme/HelloBundle/Resources/config/services.xml -->
<parameters>
<!-- ... -->
<parameter key="posta.class">Acme\HelloBundle\Mail\Mailer</parameter>
</parameters>
<services>
<service id="posta" class="%posta.class%">
<argument type="service" id="service_container" />
</service>
</services>
• PHP
// src/Acme/HelloBundle/Resources/config/services.php
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
// ...
$container->setParameter(’posta.class’, ’Acme\HelloBundle\Mail\Mailer’);
$container->setDefinition(’posta’, new Definition(
’%posta.class%’,
array(new Reference(’service_container’))
));
Note: Iniettare l’intero contenitore in un servizio di solito non è una buona idea (iniettare solo ciò che serve). In
alcuni rari casi, è necessario quando si ha un servizio nello scope container che necessita di un servizio nello
scope request.
Se si definisce un controllore come servizio, si può ottenere l’oggetto Request senza iniettare il contenitore,
facendoselo passare come parametro nel metodo dell’azione. Vedere La Request come parametro del controllore
per maggiori dettagli.
3.1.29 Come far sì che i servizi usino le etichette
Molti dei servizi centrali di Symfony2 dipendono da etichette per capire quali servizi dovrebbero essere caricati, ricevere notifiche di eventi o per essere maneggiati in determinati modi. Ad esempio, Twig usa l’etichetta
twig.extension per caricare ulteriori estensioni.
Ma è possibile usare etichette anche nei propri bundle. Ad esempio nel caso in cui uno dei propri servizi gestisca
una collezione di un qualche genere o implementi una “lista” nella quale diverse strategie alternative vengono
provate fino a che una non risulti efficace. In questo articolo si userà come esempio una “lista di trasporto”
che è una collezione di classi che implementano \Swift_Transport. Usando questa lista il mailer di Swift
proverà diversi tipi di trasporto fino a che uno non abbia successo. Questo articolo si focalizza fondamentalmente
sull’argomento dell’iniezione di dipendenze.
3.1. Ricettario
335
Symfony2 documentation Documentation, Release 2
Per iniziare si definisce la classe della ListaDiTrasporto:
namespace Acme\MailerBundle;
class ListaDiTrasporto
{
private $trasporti;
public function __construct()
{
$this->trasporti = array();
}
public function aggiungiTrasporto(\Swift_Transport
{
$this->trasporti[] = $trasporto;
}
$trasporto)
}
Quindi si definisce la lista come servizio:
• YAML
# src/Acme/MailerBundle/Resources/config/services.yml
parameters:
acme_mailer.lista_trasporto.class: Acme\MailerBundle\ListaDiTrasporto
services:
acme_mailer.lista_trasporto:
class: %acme_mailer.lista_trasporto.class%
• XML
<!-- src/Acme/MailerBundle/Resources/config/services.xml -->
<parameters>
<parameter key="acme_mailer.lista_trasporto.class">Acme\MailerBundle\ListaDiTrasporto</pa
</parameters>
<services>
<service id="acme_mailer.lista_trasporto" class="%acme_mailer.lista_trasporto.class%" />
</services>
• PHP
// src/Acme/MailerBundle/Resources/config/services.php
use Symfony\Component\DependencyInjection\Definition;
$container->setParameter(’acme_mailer.lista_trasporto.class’, ’Acme\MailerBundle\ListaDiTrasp
$container->setDefinition(’acme_mailer.lista_trasporto’, new Definition(’%acme_mailer.lista_t
Definire un servizio con etichette personalizzate
A questo punto si vuole che diverse classi di \Swift_Transport vengano istanziate e automaticamente aggiunte alla lista, usando il metodo aggiungiTrasporto. Come esempio si possono aggiungere i seguenti
trasporti come servizi:
• YAML
# src/Acme/MailerBundle/Resources/config/services.yml
services:
acme_mailer.transport.smtp:
class: \Swift_SmtpTransport
336
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
arguments:
- %mailer_host%
tags:
- { name: acme_mailer.transport }
acme_mailer.transport.sendmail:
class: \Swift_SendmailTransport
tags:
- { name: acme_mailer.transport }
• XML
<!-- src/Acme/MailerBundle/Resources/config/services.xml -->
<service id="acme_mailer.transport.smtp" class="\Swift_SmtpTransport">
<argument>%mailer_host%</argument>
<tag name="acme_mailer.transport" />
</service>
<service id="acme_mailer.transport.sendmail" class="\Swift_SendmailTransport">
<tag name="acme_mailer.transport" />
</service>
• PHP
// src/Acme/MailerBundle/Resources/config/services.php
use Symfony\Component\DependencyInjection\Definition;
$definitionSmtp = new Definition(’\Swift_SmtpTransport’, array(’%mailer_host%’));
$definitionSmtp->addTag(’acme_mailer.transport’);
$container->setDefinition(’acme_mailer.transport.smtp’, $definitionSmtp);
$definitionSendmail = new Definition(’\Swift_SendmailTransport’);
$definitionSendmail->addTag(’acme_mailer.transport’);
$container->setDefinition(’acme_mailer.transport.sendmail’, $definitionSendmail);
Si noti l’etichetta “acme_mailer.transport”. Si vuole che il bundle riconosca questi trasporti e li aggiunga, autonomamente, alla lista. Per realizzare questo meccanismo è necessario definire un metodo build() nella classe
AcmeMailerBundle:
namespace Acme\MailerBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Acme\MailerBundle\DependencyInjection\Compiler\TransportCompilerPass;
class AcmeMailerBundle extends Bundle
{
public function build(ContainerBuilder $contenitore)
{
parent::build($contenitore);
$contenitore->addCompilerPass(new TransportCompilerPass());
}
}
Creazione del CompilerPass
Si può notare che il metodo fa riferimento alla non ancora esistente classe TransportCompilerPass.
Questa classe dovrà fare in modo che tutti i servizi etichettat come acme_mailer.transport vengano aggiunti alla classe ListaDiTrasporto tramite una chiamata al metodo aggiungiTrasporto(). La classe
TransportCompilerPass sarà simile alla seguente:
3.1. Ricettario
337
Symfony2 documentation Documentation, Release 2
namespace Acme\MailerBundle\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Reference;
class TransportCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $contenitore)
{
if (false === $contenitore->hasDefinition(’acme_mailer.lista_trasporto’)) {
return;
}
$definizione = $contenitore->getDefinition(’acme_mailer.lista_trasporto’);
foreach ($contenitore->findTaggedServiceIds(’acme_mailer.transport’) as $id => $attributi)
$definizione->addMethodCall(’aggiungiTrasporto’, array(new Reference($id)));
}
}
}
Il metodo process() controllo l’esistenza di un servizio acme_mailer.lista_trasporto, quindi
cerca tra tutti i servizi etichettati acme_mailer.transport.
Aggiunge alla definizione del
servizio acme_mailer.lista_trasporto una chiamata a aggiungiTrasporto() per ogni servizio
“acme_mailer.transport” trovato. Il primo argomento di ognuna di queste chiamate sarà lo stesso servizio di
trasporto della posta.
Note: Per convenzione, in nomi di etichette sono formati dal nome del bundle(minuscolo con il trattino basso
come separatore), seguito dal punto e, infine, dal nome “reale”: perciò, l’etichetta “transport” di AcmeMailerBundle sarà acme_mailer.transport.
Definizione dei servizi compilati
Aggiungere il passo della compilazione avrà come risultato la creazione, in automatico, delle seguenti righe
di codice nella versione compilata del contenitore di servizi. Nel caso si stia lavorando nell’ambiente
“dev”, si apra il file /cache/dev/appDevDebugProjectContainer.php e si cerchi il metodo
getTransportChainService(). Dovrebbe essere simile al seguente:
protected function getAcmeMailer_ListaTrasportoService()
{
$this->services[’acme_mailer.lista_trasporto’] = $instance = new \Acme\MailerBundle\ListaDiTra
$instance->aggiungiTrasporto($this->get(’acme_mailer.transport.smtp’));
$instance->aggiungiTrasporto($this->get(’acme_mailer.transport.sendmail’));
return $instance;
}
3.1.30 Usare PdoSessionStorage per salvare le sessioni nella base dati
Normalmente, nella gestione delle sessioni, Symfony2 salva le relative informazioni all’interno di file. Solitamente, i siti web di dimensioni medio grandi utilizzano la basi dati, invece dei file, per salvare i dati di sessione.
Questo perché le basi dati sono più semplici da utilizzare e sono più scalabili in ambienti multi-webserver.
Symfony2 ha, al suo interno, una soluzione per l’archiviazione delle sessioni su base dati, chiamata
Symfony\Component\HttpFoundation\SessionStorage\PdoSessionStorage. Per utilizzarla
è sufficiente cambiare alcuni parametri di config.yml (o del proprio formato di configurazione):
338
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
• YAML
# app/config/config.yml
framework:
session:
# ...
storage_id:
session.storage.pdo
parameters:
pdo.db_options:
db_table:
db_id_col:
db_data_col:
db_time_col:
sessione
sessione_id
sessione_value
sessione_time
services:
pdo:
class: PDO
arguments:
dsn:
"mysql:dbname=db_sessione"
user:
utente_db
password: password_db
session.storage.pdo:
class:
Symfony\Component\HttpFoundation\SessionStorage\PdoSessionStorage
arguments: [@pdo, %session.storage.options%, %pdo.db_options%]
• XML
<!-- app/config/config.xml -->
<framework:config>
<framework:session storage-id="session.storage.pdo" lifetime="3600" auto-start="true"/>
</framework:config>
<parameters>
<parameter key="pdo.db_options" type="collection">
<parameter key="db_table">sessione</parameter>
<parameter key="db_id_col">sessione_id</parameter>
<parameter key="db_data_col">sessione_value</parameter>
<parameter key="db_time_col">sessione_time</parameter>
</parameter>
</parameters>
<services>
<service id="pdo" class="PDO">
<argument>mysql:dbname=db_sessione</argument>
<argument>utente_db</argument>
<argument>password_db</argument>
</service>
<service id="session.storage.pdo" class="Symfony\Component\HttpFoundation\SessionStorage\
<argument type="service" id="pdo" />
<argument>%session.storage.options%</argument>
<argument>%pdo.db_options%</argument>
</service>
</services>
• PHP
// app/config/config.yml
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
$container->loadFromExtension(’framework’, array(
3.1. Ricettario
339
Symfony2 documentation Documentation, Release 2
// ...
’session’ => array(
// ...
’storage_id’ => ’session.storage.pdo’,
),
));
$container->setParameter(’pdo.db_options’, array(
’db_table’
=> ’sessione’,
’db_id_col’
=> ’sessione_id’,
’db_data_col’
=> ’sessione_value’,
’db_time_col’
=> ’sessione_time’,
));
$pdoDefinition = new Definition(’PDO’, array(
’mysql:dbname=db_sessione’,
’utente_db’,
’password_db’,
));
$container->setDefinition(’pdo’, $pdoDefinition);
$storageDefinition = new Definition(’Symfony\Component\HttpFoundation\SessionStorage\PdoSessi
new Reference(’pdo’),
’%session.storage.options%’,
’%pdo.db_options%’,
));
$container->setDefinition(’session.storage.pdo’, $storageDefinition);
• db_table: Nome della tabella, nella base dati, per le sessioni
• db_id_col: Nome della colonna id della tabella delle sessioni (VARCHAR(255) o maggiore)
• db_data_col: Nome della colonna dove salvare il valore della sessione (TEXT o CLOB)
• db_time_col: Nome della colonna per la registrazione del tempo della sessione (INTEGER)
Condividere le informazioni di connessione della base dati
Grazie a questa configurazione, i parametri della connessione alla base dati sono definiti solo per l’archiviazione
dei dati di sessione. La qual cosa è perfetta se si usa una base dati differente per i dati di sessione.
Ma se si preferisce salvare i dati di sessione nella stessa base dati in cui risiedono i rimanenti dati del progetto, è
possibile utilizzare i parametri di connessione di parameter.ini, richiamandone la configurazione della base dati:
• YAML
pdo:
class: PDO
arguments:
- "mysql:dbname=%database_name%"
- %database_user%
- %database_password%
• XML
<service id="pdo" class="PDO">
<argument>mysql:dbname=%database_name%</argument>
<argument>%database_user%</argument>
<argument>%database_password%</argument>
</service>
• XML
340
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
$pdoDefinition = new Definition(’PDO’, array(
’mysql:dbname=%database_name%’,
’%database_user%’,
’%database_password%’,
));
Esempi di dichiarazioni SQL
MySQL
La dichiarazione SQL per creare la necessaria tabella nella base dati potrebbe essere simile alla seguente
(MySQL):
CREATE TABLE ‘sessione‘ (
‘sessione_id‘ varchar(255) NOT NULL,
‘sessione_value‘ text NOT NULL,
‘sessione_time‘ int(11) NOT NULL,
PRIMARY KEY (‘session_id‘)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
PostgreSQL
Per PostgreSQL, la dichiarazione sarà simile alla seguente:
CREATE TABLE sessione (
sessione_id character varying(255) NOT NULL,
sessione_value text NOT NULL,
sessione_time integer NOT NULL,
CONSTRAINT session_pkey PRIMARY KEY (session_id),
);
3.1.31 Struttura del bundle e best practice
Un bundle è una cartella con una struttura ben definita, che può ospitare qualsiasi cosa, dalle classi ai controllori
e alle risorse web. Anche se i bundle sono molto flessibili, si devono seguire alcune best practice per distribuirne
uno.
Nome del bundle
Un bundle è anche uno spazio dei nomi di PHP. Lo spazio dei nomi deve seguire gli standard tecnici di interoperabilità di PHP 5.3 per gli spazi dei nomi e i nomi delle classi: inizia con un segmento del venditore, seguito da
zero o più segmenti di categoria e finisce con il nome breve dello spazio dei nomi, che deve terminare col suffisso
Bundle.
Uno spazio dei nomi diventa un bundle non appena vi si aggiunge una classe Bundle. La classe Bundle deve
seguire queste semplici regole:
• Usare solo caratteri alfanumerici e trattini bassi;
• Usare un nome in CamelCase;
• Usare un nome breve e descrittivo (non oltre 2 parole);
• Aggiungere un prefisso con il nome del venditore (e, opzionalmente, con gli spazi dei nomi della categoria);
• Aggiungere il suffisso Bundle.
3.1. Ricettario
341
Symfony2 documentation Documentation, Release 2
Ecco alcuni spazi dei nomi e nomi di classi Bundle validi:
Spazio dei nomi
Acme\Bundle\BlogBundle
Acme\Bundle\Social\BlogBundle
Acme\BlogBundle
Nome classe Bundle
AcmeBlogBundle
AcmeSocialBlogBundle
AcmeBlogBundle
Per convenzione, il metodo getName() della classe Bundle deve restituire il nome della classe.
Note: Se si condivide pubblicamente il bundle, si deve usare il nome della classe Bundle per il repository
(AcmeBlogBundle e non BlogBundle, per esempio).
Note: I bundle del nucleo di Symfony2 non hanno il prefisso Symfony e hanno sempre un sotto-spazio dei nomi
Bundle; per esempio: Symfony\Bundle\FrameworkBundle\FrameworkBundle.
Ogni bundle ha un alias, che è la versione breve in minuscolo del nome del bundle, con trattini bassi
(acme_hello per AcmeHelloBundle o acme_social_blog per Acme\Social\BlogBundle, per
esempio). Questo alias è usato per assicurare l’univocità di un bundle (vedere più avanti alcuni esempi d’utilizzo).
Struttura della cartella
La struttura di base delle cartella di un bundle HelloBundle deve essere come segue:
XXX/...
HelloBundle/
HelloBundle.php
Controller/
Resources/
meta/
LICENSE
config/
doc/
index.rst
translations/
views/
public/
Tests/
Le cartelle XXX riflettono la struttura dello spazio dei nomi del bundle.
I seguenti file sono obbligatori:
• HelloBundle.php;
• Resources/meta/LICENSE: La licenza completa del codice;
• Resources/doc/index.rst: Il file iniziale della documentazione del bundle.
Note: Queste convenzioni assicurano che strumenti automatici possano appoggiarsi a tale struttura predefinita
nel loro funzionamento.
La profondità delle sotto-cartelle va mantenuta al minimo per le classi e i file più usati (massimo 2 livelli). Ulteriori
livelli possono essere definiti per file meno usati e non strategici.
La cartella del bundle è in sola lettura. Se occorre scrivere file temporanei, memorizzarli sotto le cartelle cache/
o log/ dell’applicazione. Degli strumenti possono generare file nella cartella del bundle, ma solo se i file generati
devono far parte del repository.
Le seguenti classi e i seguenti file hanno postazioni specifiche:
342
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
Tipo
Comandi
Controllori
Estensioni del contenitore
Ascoltatori di eventi
Configurazione
Risorse Web
File di traduzione
Template
Test unitari e funzionali
Cartella
Command/
Controller/
DependencyInjection/
EventListener/
Resources/config/
Resources/public/
Resources/translations/
Resources/views/
Tests/
Classi
La struttura delle cartelle di un bundle è usata dalla gerarchia degli spazi dei nomi. Per esempio, un controllore HelloController è posto in Bundle/HelloBundle/Controller/HelloController.php e
il nome pienamente qualificato della classe è Bundle\HelloBundle\Controller\HelloController.
Tutte le classi e i file devono seguire gli standard di codice di Symfony2.
Alcune classi vanno viste solo come facciati e devono essere più corte possibile, come comandi, helper, ascoltatori
e controllori.
Le classi che si connettono al distributore di eventi devono avere come suffisso Listener.
Le classi eccezione devono essere poste nel sotto-spazio dei nomi Exception.
Venditori
Un bundle non deve includere librerie PHP di terze parti. Deve invece appoggiarsi all’auto-caricamento standard
di Symfony2.
Un bundle non dovrebbe includere librerie di terze parti scritte in JavaScript, CSS o altro linguaggio.
Test
Un bundle deve avere una suite di test scritta con PHPUnit e posta sotto la cartella Tests/. I test devono seguire
i seguenti principi:
• La suite di test deve essere eseguibile con un semplice comando phpunit, eseguito da un’applicazione di
esempio;
• I test funzionali vanno usati solo per testare la risposta e alcune informazioni di profile, se se ne hanno;
• La copertura del codice deve essere almeno del 95%.
Note:
Una suite di test non deve contenere script come AllTests.php, ma appoggiarsi a un file
phpunit.xml.dist.
Documentazione
Tutte le classi e le funzioni devono essere complete di PHPDoc.
Una documentazione estensiva andrebbe fornita in formato reStructuredText, sotto la cartella
Resources/doc/; il file Resources/doc/index.rst è l’unico file obbligatorio e deve essere il
punto di ingresso della documentazione.
3.1. Ricettario
343
Symfony2 documentation Documentation, Release 2
Controllori
Come best practice, i controllori di un bundle inteso per essere distribuito non devono estendere la
classe base Symfony\Bundle\FrameworkBundle\Controller\Controller.
Possono implementare Symfony\Component\DependencyInjection\ContainerAwareInterface oppure estendere Symfony\Component\DependencyInjection\ContainerAware .
Note: Se si dà uno sguardo ai metodi di Symfony\Bundle\FrameworkBundle\Controller\Controller,
si vedrà che sono solo delle scorciatoie utili per facilitare l’apprendimento.
Rotte
Se il bundle fornisce delle rotte, devono avere come prefisso l’alias del bundle. Per esempio, per AcmeBlogBundle,
tutte le rotte devono avere come prefisso acme_blog_.
Template
Se un bundle fornisce template, devono usare Twig. Un bundle non deve fornire un layout principale, tranne se
fornisce un’applicazione completa.
File di traduzione
Se un bundle fornisce messaggi di traduzione, devono essere definiti in formato XLIFF; il dominio deve avere il
nome del bundle (bundle.hello).
Un bundle non deve sovrascrivere messaggi esistenti in altri bundle.
Configurazione
Per fornire maggiore flessibilità, un bundle può fornire impostazioni configurabili, usando i meccanismi di Symfony2.
Per semplici impostazioni di configurazione, appoggiarsi alla voce predefinita parameters della configurazione
di Symfony2. I parametri di Symfony2 sono semplici coppie chiave/valore; un valore può essere un qualsiasi valore valido in PHP. Ogni nome di parametro dovrebbe iniziare con l’alias del bundle, anche se questo
è solo un suggerimento. Gli altri nomi di parametri useranno un punto (.) per separare le varie parti (p.e.
acme_hello.email.from).
L’utente finale può fornire valori in qualsiasi file di configurazione:
• YAML
# app/config/config.yml
parameters:
acme_hello.email.from: [email protected]
• XML
<!-- app/config/config.xml -->
<parameters>
<parameter key="acme_hello.email.from">[email protected]</parameter>
</parameters>
• PHP
// app/config/config.php
$container->setParameter(’acme_hello.email.from’, ’[email protected]’);
344
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
• INI
[parameters]
acme_hello.email.from = [email protected]
Recuperare i parametri di configurazione nel proprio codice dal contenitore:
$container->getParameter(’acme_hello.email.from’);
Pur essendo questo meccanismo abbastanza semplice, si consiglia caldamente l’uso della configurazione semantica, descritta nel ricettario.
Note: Se si definiscono servizi, deve avere anche essi come prefisso l’alias del bundle.
Imparare di più dal ricettario
• Come esporre una configurazione semantica per un bundle
3.1.32 Come usare l’ereditarietà per sovrascrivere parti di un bundle
Lavorando con bundle di terze parti, ci si troverà probabilmente in situazioni in cui si vuole sovrascrivere un file in
quel bundle con un file del proprio bundle. Symfony fornisce un modo molto conveniente per sovrascrivere cose
come controllori, template, traduzioni e altri file nella cartella Resources/ di un bundle.
Per esempio, si supponga di aver installato FOSUserBundle, ma di voler sovrascrivere il suo template
base layout.html.twig, così come uno dei suoi controllori. Si supponga anche di avere il proprio
AcmeUserBundle, in cui si vogliono mettere i file sovrascritti. Si inizi registrando FOSUserBundle come
“genitore” del proprio bundle:
// src/Acme/UserBundle/AcmeUserBundle.php
namespace Acme\UserBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class AcmeUserBundle extends Bundle
{
public function getParent()
{
return ’FOSUserBundle’;
}
}
Con questa semplice modifica, si possono ora sovrascrivere diverse parti di FOSUserBundle, semplicemente
creando un file con lo stesso nome.
Sovrascrivere i controllori
Si
supponga
di
voler
aggiungere
alcune
funzionalità
a
registerAction
di
un
RegistrationController, che sta dentro FOSUserBundle. Per poterlo fare, creare un proprio
RegistrationController.php, ridefinire i metodi originali del bundle e cambiarne le funzionalità:
// src/Acme/UserBundle/Controller/RegistrationController.php
namespace Acme\UserBundle\Controller;
use FOS\UserBundle\Controller\RegistrationController as BaseController;
class RegistrationController extends BaseController
{
public function registerAction()
3.1. Ricettario
345
Symfony2 documentation Documentation, Release 2
{
$response = parent::registerAction();
// do custom stuff
return $response;
}
}
Tip:
A seconda di quanto si vuole cambiare il comportamento, si potrebbe voler richiamare
parent::registerAction() oppure sostituirne completamente la logica con una propria.
Note: Sovrascrivere i controllori in questo modo funziona solo se il bundle fa riferimento al controllore tramite la
sintassi standard FOSUserBundle:Registration:register in rotte e template. Questa è la best practice.
Sovrascrivere risorse: template, rotte, traduzioni, validazione, ecc.
Anche molte risorse possono essere sovrascritte, semplicemente creando un file con lo stesso percorso del bundle
genitore.
Per esempio, è molto comune l’esigenza di sovrascrivere il template layout.html.twig di
FOSUserBundle, in modo che usi il layout di base della propria applicazione. Poiché il file si trova in
Resources/views/layout.html.twig di FOSUserBundle, si può creare il proprio file nello stesso
posto in AcmeUserBundle. Symfony ignorerà completamente il file dentro FOSUserBundle e userà il nuovo
file al suo posto.
Lo stesso vale per i file delle rotte, della configurazione della validazione e di altre risorse.
Note:
La sovrascrittura di risorse funziona solo se ci si riferisce alle risorse col metodo
@FosUserBundle/Resources/config/routing/security.xml. Se ci si riferisce alle risorse senza
usare la scorciatoia @NomeBundle, non possono essere sovrascritte in tal modo.
Caution: I file delle traduzioni non funzionano nel modo descritto sopra. Tutti i file di traduzione sono
raggruppati in un insieme di domini di “pool” (uno per ciascuno). Symfony carica i file delle traduzioni prima
dai bundle (nell’ordine in cui i bundle sono inizializzati) e poi dalla cartella app/Resources. Se la stessa
traduzione compare in più risorse, sarà applicata la traduzione della risorsa caricata per ultima.
3.1.33 Come sovrascrivere parti di un bundle
Questo articolo non è ancora stato scritto, ma lo sarà presto. Se qualcuno fosse interessato a scriverlo, veda
Contribuire alla documentazione.
Questo argomento dovrebbe mostrare come sovrascrivere ciascuna parte di un bundle, sia da un’applicazione che
da altri bundle. Questo potrebbe includere:
• Template
• Rotte
• Controllori
• Servizi & configurazione
• Entità & mappatura di entità
• Form
• Validazione
346
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
In alcuni casi, si potrebbe parlare di best practice che un bundle deve usare per fare in modo che certe parti
siano sovrascrivibili (o facilmente sovrascrivibili). Si potrebbe anche parlare di come alcune parti non siano
effettivamente sovrascrivibili, mostrando l’approccio migliore per risolvere il problema.
3.1.34 Come esporre una configurazione semantica per un bundle
Se si apre il file di configurazione della propria applicazione (di solito app/config/config.yml), si vedranno un certo numero di “spazi di nomi” di configurazioni, come framework, twig e doctrine. Ciasucno
di questi configura uno specifico bundle, consentendo di configurare cose ad alto livello e quindi lasciando al
bundle tutte le modifiche complesse e di basso livello.
Per esempio, il codice seguente dice a FrameworkBundle di abilitare l’integrazione con i form, che implica la
definizione di alcuni servizi, così come anche l’integrazione di altri componenti correlati:
• YAML
framework:
# ...
form:
true
• XML
<framework:config>
<framework:form />
</framework:config>
• PHP
$container->loadFromExtension(’framework’, array(
// ...
’form’
=> true,
// ...
));
Quando si crea un bundle, si hanno due scelte sulla gestione della configurazione:
1. Normale configurazione di servizi (facile):
Si possono specificare i propri servizi in un file di configurazione (p.e. services.yml) posto
nel proprio bundle e quindi importarlo dalla configurazione principale della propria applicazione.
Questo è molto facile, rapido ed efficace. Se si usano i parametri, si avrà ancora la flessibilità di
personalizzare il bundle dalla configurazione della propria applicazione. Vedere “Importare la
configurazione con imports” per ulteriori dettagli.
2. Esporre una configurazione semantica (avanzato):
Questo è il modo usato per la configurazione dei bundle del nucleo (come descritto sopra). L’idea
di base è che, invece di far sovrascrivere all’utente i singoli parametri, lasciare che ne configuri
alcune opzioni create specificatamente. Lo sviluppatore del bundle deve quindi analizzare tale
configurazione e caricare i servizi all’interno di una classe “Extension”. Con questo metodo, non
si avrà bisogno di importare alcuna risorsa di configurazione dall’appplicazione principale: la
classe Extension può gestire tutto.
La seconda opzione, di cui parleremo, è molto più flessibili, ma richiede anche più tempo di preparazione. Se si
ci sta chiedendo quale metodo scegliere, probabilmente è una buona idea partire col primo metodo, poi cambiare
al secondo, se se ne ha necessità.
Il secondo metodo ha diversi vantaggi:
• Molto più potente che definire semplici parametri: un valore specifico di un’opzione può scatenare la
creazioni di molte definizioni di servizi;
• Possibilità di avere una gerarchia di configurazioni
3.1. Ricettario
347
Symfony2 documentation Documentation, Release 2
• Fusione intelligente quando diversi file di configurazione (p.e. config_dev.yml e config.yml)
sovrascrivono le proprie configurazioni a vicenda;
• Validazione della configurazione (se si usa una classe di configurazione);
• auto-completamento nell’IDE quando si crea un XSD e lo sviluppatore usa XML.
Sovrascrivere parametri dei bundle
Se un bundle fornisce una classe Extension, in generale non si dovrebbe sovrascrivere alcun parametro del
contenitore di servizi per quel bundle. L’idea è che se è presente una classe Extension, ogni impostazione
configurabile sia presente nella configurazione messa a disposizione da tale classe. In altre parole, la classe
Extension definisce tutte le impostazioni supportate pubblicamente, per i quali sarà mantenuta una retrocompatibilità.
Creare una classe Extension
Se si sceglie di esporre una configurazione semantica per il proprio bundle, si avrà prima bisogno di creare una
nuova classe “Extension”, per gestire il processo. Tale classe va posta nella cartella DependencyInjection
del proprio bundle e il suo nome va costruito sostituendo il postfisso Bundle del nome della classe
del bundle con Extension. Per esempio, la classe Extension di AcmeHelloBundle si chiamerebbe
AcmeHelloExtension:
// Acme/HelloBundle/DependencyInjection/AcmeHelloExtension.php
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\ContainerBuilder;
class AcmeHelloExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container)
{
// qui sta tutta la logica
}
public function getXsdValidationBasePath()
{
return __DIR__.’/../Resources/config/’;
}
public function getNamespace()
{
return ’http://www.example.com/symfony/schema/’;
}
}
Note: I metodi getXsdValidationBasePath e getNamespace servono solo se il bundle fornisce degli
XSD facoltativi per la configurazione.
La presenza della classe precedente vuol dire che si può definire uno spazio dei nomi acme_hello in un qualsiasi file di configurazione. Lo spazio dei nomi acme_hello viene dal nome della classe Extension, a cui è
stata rimossa la parola Extension e posto in minuscolo e con trattini bassi il resto del nome. In altre parole„
AcmeHelloExtension diventa acme_hello.
Si può iniziare specificando la configurazione sotto questo spazio dei nomi:
• YAML
# app/config/config.yml
acme_hello: ~
348
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
• XML
<!-- app/config/config.xml -->
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:acme_hello="http://www.example.com/symfony/schema/"
xsi:schemaLocation="http://www.example.com/symfony/schema/ http://www.example.com/symfony
<acme_hello:config />
...
</container>
• PHP
// app/config/config.php
$container->loadFromExtension(’acme_hello’, array());
Tip: Seguendo le convenzioni di nomenclatura viste sopra, il metodo load() della propria estensione sarà
sempre richiamato, a patto che il proprio bundle sia registrato nel Kernel. In altre parole, anche se l’utente
non fornisce alcuna configurazione (cioè se la voce acme_hello non appare mai), il metodo load() sarà
richiamato, passandogli un array $configs vuoto. Si possono comunque fornire valori predefiniti adeguati per
il proprio bundle, se lo si desidera.
Analisi dell’array $configs
Ogni volta che un utente include lo spazio dei nomi acme_hello in un file di configurazione, la configurazione
sotto di esso viene aggiunta a un array di configurazioni e passata al metodo load() dell’estensione (Symfony2
converte automaticamente XML e YAML in array).
Si prenda la seguente configurazione:
• YAML
# app/config/config.yml
acme_hello:
foo: fooValue
bar: barValue
• XML
<!-- app/config/config.xml -->
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:acme_hello="http://www.example.com/symfony/schema/"
xsi:schemaLocation="http://www.example.com/symfony/schema/ http://www.example.com/symfony
<acme_hello:config foo="fooValue">
<acme_hello:bar>barValue</acme_hello:bar>
</acme_hello:config>
</container>
• PHP
// app/config/config.php
$container->loadFromExtension(’acme_hello’, array(
’foo’ => ’fooValue’,
3.1. Ricettario
349
Symfony2 documentation Documentation, Release 2
’bar’ => ’barValue’,
));
L’array passato al metodo load() sarà simile a questo:
array(
array(
’foo’ => ’fooValue’,
’bar’ => ’barValue’,
)
)
Si noti che si tratta di un array di array, non di un semplice array di valori di configurazione. È stato fatto intenzionalmente. Per esempio, se acme_hello appare in un altro file di configurazione, come config_dev.yml,
con valori diversi sotto di esso, l’array in uscita sarà simile a questo:
array(
array(
’foo’
’bar’
),
array(
’foo’
’baz’
),
)
=> ’fooValue’,
=> ’barValue’,
=> ’fooDevValue’,
=> ’newConfigEntry’,
L’ordine dei due array dipende da quale è stato definito prima. È compito di chi sviluppa il bundle, quindi, decidere
in che modo tali configurazioni vadano fuse insieme. Si potrebbe, per esempio, voler fare in modo che i valori
successivi sovrascrivano quelli precedenti, oppure fonderli in qualche modo.
Successivamente, nella sezione classe Configuration, si imparerà un modo robusto per gestirli. Per ora, ci si può
accontentare di fonderli a mano:
public function load(array $configs, ContainerBuilder $container)
{
$config = array();
foreach ($configs as $subConfig) {
$config = array_merge($config, $subConfig);
}
// usare ora l’array $config
}
Caution: Assicurarsi che la tecnica di fusione vista sopra abbia senso per il proprio bundle. Questo è solo un
esempio e andrebbe usato con la dovuta cautela.
Usare il metodo load()
Con load(), la variabile $container si riferisce a un contenitore che conosce solo la configurazione del
proprio spazio dei nomi (cioè non contiene informazioni su servizi caricati da altri bundle). Lo scopo del metodo
load() è quello di manipolare il contenitore, aggiungere e configurare ogni metodo o servizio necessario per il
proprio bundle.
Caricare risorse di configurazioni esterne
Una cosa che si fa di solito è caricare un file di configurazione esterno, che potrebbe contenere i servizi necessari al proprio bundle. Per esempio, si supponga di avere un file services.xml, che contiene molte delle
configurazioni di servizio del proprio bundle:
350
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
use Symfony\Component\Config\FileLocator;
public function load(array $configs, ContainerBuilder $container)
{
// prepara la propria variabile $config
$loader = new XmlFileLoader($container, new FileLocator(__DIR__.’/../Resources/config’));
$loader->load(’services.xml’);
}
Lo si potrebbe anche con una condizione, basata su uno dei valori di configurazione. Per esempio, si supponga di
voler caricare un insieme di servizi, ma solo se un’opzione enabled è impostata a true:
public function load(array $configs, ContainerBuilder $container)
{
// prepara la propria variabile $config
$loader = new XmlFileLoader($container, new FileLocator(__DIR__.’/../Resources/config’));
if (isset($config[’enabled’]) && $config[’enabled’]) {
$loader->load(’services.xml’);
}
}
Configurare servizi e impostare parametri
Una volta caricati alcune configurazioni di servizi, si potrebbe aver bisogno di modificare la configurazione in base
ad alcuni valori inseriti. Per esempio, si supponga di avere un servizio il cui primo parametro è una stringa “type”,
che sarà usata internamente. Si vorrebbe che fosse facilmente configurata dall’utente del bundle, quindi nella
proprio file di configurazione del servizio (services.xml), si definisce questo servizio e si usa un parametro
vuoto, come acme_hello.my_service_type, come primo parametro:
<!-- src/Acme/HelloBundle/Resources/config/services.xml -->
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/servi
<parameters>
<parameter key="acme_hello.my_service_type" />
</parameters>
<services>
<service id="acme_hello.my_service" class="Acme\HelloBundle\MyService">
<argument>%acme_hello.my_service_type%</argument>
</service>
</services>
</container>
Ma perché definire un parametro vuoto e poi passarlo al proprio servizio? La risposa è che si imposterà questo
parametro nella propria classe Extension, in base ai valori di configurazione in entrata. Si supponga, per esempio,
di voler consentire all’utente di definire questa opzione type sotto una chiave di nome my_type. Aggiungere al
metodo load() il codice seguente:
public function load(array $configs, ContainerBuilder $container)
{
// prepara la propria variabile $config
$loader = new XmlFileLoader($container, new FileLocator(__DIR__.’/../Resources/config’));
$loader->load(’services.xml’);
3.1. Ricettario
351
Symfony2 documentation Documentation, Release 2
if (!isset($config[’my_type’])) {
throw new \InvalidArgumentException(’The "my_type" option must be set’);
}
$container->setParameter(’acme_hello.my_service_type’, $config[’my_type’]);
}
L’utente ora è in grado di configurare effettivamente il servizio, specificando il valore di configurazione my_type:
• YAML
# app/config/config.yml
acme_hello:
my_type: foo
# ...
• XML
<!-- app/config/config.xml -->
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:acme_hello="http://www.example.com/symfony/schema/"
xsi:schemaLocation="http://www.example.com/symfony/schema/ http://www.example.com/symfony
<acme_hello:config my_type="foo">
<!-- ... -->
</acme_hello:config>
</container>
• PHP
// app/config/config.php
$container->loadFromExtension(’acme_hello’, array(
’my_type’ => ’foo’,
// ...
));
Parametri globali
Quando si configura il contenitore, si hanno a disposizione i seguenti parametri globali:
• kernel.name
• kernel.environment
• kernel.debug
• kernel.root_dir
• kernel.cache_dir
• kernel.logs_dir
• kernel.bundle_dirs
• kernel.bundles
• kernel.charset
Caution: Tutti i nomi di parametri e di servizi che iniziano con _ sono riservati al framework e non se ne
dovrebbero definire altri nei bundle.
352
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
Validazione e fusione con una classe Configuration
Finora, la fusione degli array di configurazione è stata fatta a mano, verificando la presenza di valori di configurazione con la funzione isset() di PHP. Un sistema opzionale Configuration è disponibile, per aiutare nella
fusione, nella validazione, con i valori predefiniti e per la normalizzazione dei formati.
Note: La normalizzazione dei formati riguarda alcuni formati, soprattutto XML, che offrono array di configurazione leggermente diversi, per cui tali array hanno bisgno di essere normalizzati, per corrispondere a tutti gli
altri.
Per sfruttare questo sistema, si creerà una classe Configuration e si costruirà un albero, che definisce la
propria configurazione in tale classe:
// src/Acme/HelloBundle/DependencyExtension/Configuration.php
namespace Acme\HelloBundle\DependencyInjection;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
class Configuration implements ConfigurationInterface
{
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder();
$rootNode = $treeBuilder->root(’acme_hello’);
$rootNode
->children()
->scalarNode(’my_type’)->defaultValue(’bar’)->end()
->end()
;
return $treeBuilder;
}
Questo è un esempio molto semplice, ma si può ora usare questa classe nel proprio metodo load(), per fondere
la propria configurazione e forzare la validazione. Se viene passata un’opzione che non sia my_type, l’utente
sarà avvisato con un’eccezione del passaggio di un’opzione non supportata:
use Symfony\Component\Config\Definition\Processor;
// ...
public function load(array $configs, ContainerBuilder $container)
{
$processor = new Processor();
$configuration = new Configuration();
$config = $processor->processConfiguration($configuration, $configs);
// ...
}
Il metodo processConfiguration() usa l’albero di configurazione definito nella classe Configuration
per validare, normalizzare e fondere tutti gli array di configurazione insieme.
La classe Configuration può essere molto più complicata di quanto mostrato qui, poiché supporta nodi array,
nodi “prototipo”, validazione avanzata, normalizzazione specifica di XML e fusione avanzata. Il modo migliore
per vederla in azione è guardare alcune classi Configuration del nucleo, come quella FrameworkBundle o di
TwigBundle.
3.1. Ricettario
353
Symfony2 documentation Documentation, Release 2
Esportare la configurazione predefinita
New in version 2.1: Il comando config:dump-reference è stato aggiunto in Symfony 2.1 Il comando
config:dump-reference consente di mostrare nella console, in formato YAML, la configurazione predefinita di un bundle.
Il comando funziona automaticamente solo se la configurazione del bundle si trova nella posizione standard (MioBundle\DependencyInjection\Configuration) e non ha un __constructor().
Se si ha qualcosa di diverso, la propria classe Extension dovrà sovrascrivere il metodo
Extension::getConfiguration() e restituire un’istanza di Configuration.
Si possono aggiungere commenti ed esempi alla configurazione, usando i metodi ->setInfo() e
->setExample():
// src/Acme/HelloBundle/DependencyExtension/Configuration.php
namespace Acme\HelloBundle\DependencyInjection;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
class Configuration implements ConfigurationInterface
{
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder();
$rootNode = $treeBuilder->root(’acme_hello’);
$rootNode
->children()
->scalarNode(’mio_tipo’)
->defaultValue(’bar’)
->setInfo(’cosa configura mio_tipo’)
->setExample(’impostazione di esempio’)
->end()
->end()
;
return $treeBuilder;
}
Il testo apparirà come commenti YAML nell’output del comando config:dump-reference.
Convenzioni per l’estensione
Quando si crea un’estensione, seguire queste semplici convenzioni:
• L’estensione deve trovarsi nel sotto-spazio dei nomi DependencyInjection;
• l’estensione deve avere lo stesso nome del bundle, ma con Extension (AcmeHelloExtension per
AcmeHelloBundle);
• L’estensione deve fornire uno schema XSD.
Se si seguono queste semplici convenzioni, la propria estensione sarà registrata automaticamente da Symfony2. In
caso contrario, sovrascrivere il metodo :method:‘Symfony\\Component\\HttpKernel\\Bundle\\Bundle::build‘
nel proprio bundle:
use Acme\HelloBundle\DependencyInjection\UnconventionalExtensionClass;
class AcmeHelloBundle extends Bundle
{
public function build(ContainerBuilder $container)
{
354
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
parent::build($container);
// registrare a mano estensioni che non seguono le convenzioni
$container->registerExtension(new UnconventionalExtensionClass());
}
}
In questo caso, la classe Extension deve implementare anche un metodo getAlias() e restituire un alias univoco, con nome che dipende dal bundle (p.e. acme_hello). Questo perché il nome della classe non segue le
convenzioni e non finisce per Extension.
Inoltre, il metodo load() dell’estensione sarà richiamato solo se l’utente specifica l’alias acme_hello in
almeno un file di configurazione. Ancora, questo perché la classe Extension non segue le convenzioni viste sopra,
quindi non succede nulla in modo automatico.
3.1.35 Come spedire un’email
Spedire le email è un delle azioni classiche di ogni applicazione web ma rappresenta anche l’origine di potenziali problemi e complicazioni. Invece di reinventare la ruota, una soluzione per l’invio di email è l’uso di
SwiftmailerBundle, il quale sfrutta la potenza della libreria Swiftmailer .
Note: Non dimenticatevi di abilitare il bundle all’interno del kernel prima di utilizzarlo:
public function registerBundles()
{
$bundles = array(
// ...
new Symfony\Bundle\SwiftmailerBundle\SwiftmailerBundle(),
);
// ...
}
Configurazione
Prima di utilizzare Swiftmailer, assicuratevi di includerne la configurazione. L’unico parametro obbligatorio della
configurazione è il parametro transport:
• YAML
# app/config/config.yml
swiftmailer:
transport: smtp
encryption: ssl
auth_mode: login
host:
smtp.gmail.com
username:
tuo_nome_utente
password:
tua_password
• XML
<!-- app/config/config.xml -->
<!-xmlns:swiftmailer="http://symfony.com/schema/dic/swiftmailer"
http://symfony.com/schema/dic/swiftmailer http://symfony.com/schema/dic/swiftmailer/swiftmail
-->
<swiftmailer:config
transport="smtp"
3.1. Ricettario
355
Symfony2 documentation Documentation, Release 2
encryption="ssl"
auth-mode="login"
host="smtp.gmail.com"
username="tuo_nome_utente"
password="tua_password" />
• PHP
// app/config/config.php
$container->loadFromExtension(’swiftmailer’, array(
’transport’ => "smtp",
’encryption’ => "ssl",
’auth_mode’ => "login",
’host’
=> "smtp.gmail.com",
’username’
=> "tuo_nome_utente",
’password’
=> "tua_password",
));
La maggior parte della configurazione di Swiftmailer è relativa al come i messaggi debbano essere inoltrati.
Sono disponibili i seguenti parametri di configurazione:
• transport (smtp, mail, sendmail, o gmail)
• username
• password
• host
• port
• encryption (tls, o ssl)
• auth_mode (plain, login, o cram-md5)
• spool
– type (come accodare i messaggi: attualmente solo l’opzione file è supportata)
– path (dove salvare i messaggi)
• delivery_address (indirizzo email dove spedire TUTTE le email)
• disable_delivery (impostare a true per disabilitare completamente l’invio)
L’invio delle email
Per lavorare con la libreria Swiftmailer dovrete creare, configurare e quindi spedire oggetti di tipo
Swift_Message. Il “mailer” è il vero responsabile dell’invio dei messaggi ed è accessibile tramite il servizio
mailer. In generale, spedire un’email è abbastanza intuitivo:
public function indexAction($name)
{
$messaggio = \Swift_Message::newInstance()
->setSubject(’Hello Email’)
->setFrom(’[email protected]’)
->setTo(’[email protected]’)
->setBody($this->renderView(’HelloBundle:Hello:email.txt.twig’, array(’nome’ => $nome)))
;
$this->get(’mailer’)->send($messaggio);
return $this->render(...);
}
Per tenere i vari aspetti separati, il corpo del messaggio è stato salvato in un template che viene poi restituito
tramite il metodo renderView().
356
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
L’oggetto $messaggio supporta molte altre opzioni, come l’aggiunta di allegati, l’inserimento di HTML e molto
altro. Fortunatamente la documentazione di Swiftmailer affronta questo argomento dettagliatamente nel capitolo
sulla Creazione di Messaggi .
Tip: Diversi altri articoli di questo ricettario spiegano come spedire le email grazie Symfony2:
• Come usare Gmail per l’invio delle email
• email/dev_environment
• email/spool
3.1.36 Come usare Gmail per l’invio delle email
In fase di sviluppo, invece di utilizzare un normale server SMTP per l’invio delle email, potrebbe essere più
semplice e pratico usare Gmail. Il bundle Swiftmailer ne rende facilissimo l’utilizzo.
Tip: Invece di usare un normale account di Gmail, sarebbe meglio crearne uno da usare appositamente per questo
scopo.
Nel file di configurazione dell’ambiente di sviluppo, si assegna al parametro transport l’ozione gmail e ai
parametri username e password le credenziali dell’account di Google:
• YAML
# app/config/config_dev.yml
swiftmailer:
transport: gmail
username: nome_utente_gmail
password: password_gmail
• XML
<!-- app/config/config_dev.xml -->
<!-xmlns:swiftmailer="http://symfony.com/schema/dic/swiftmailer"
http://symfony.com/schema/dic/swiftmailer http://symfony.com/schema/dic/swiftmailer/swiftmail
-->
<swiftmailer:config
transport="gmail"
username="nome_utente_gmail"
password="password_gmail" />
• PHP
// app/config/config_dev.php
$container->loadFromExtension(’swiftmailer’, array(
’transport’ => "gmail",
’username’ => "nome_utente_gmail",
’password’ => "password_gmail",
));
E il gioco è fatto!
Note: L’attributo di trasporto gmail è in realtà una scorciatoia che imposta a smtp il trasporto, e modifica
encryption, auth_mode e host in modo da poter comunicare con Gmail.
3.1. Ricettario
357
Symfony2 documentation Documentation, Release 2
3.1.37 Lavorare con le email durante lo sviluppo
Durante lo sviluppo di applicazioni che inviino email, non sempre è desiderabile che le email vengano inviate
all’effettivo destinatario del messaggio. Se si utilizza SwiftmailerBundle con Symfony2, è possibile evitarlo
semplicemente modificano i parametri di configurazione, senza modificare alcuna parte del codice. Ci sono due
possibili scelte quando si tratta di gestire le email in fase di sviluppo: (a) disabilitare del tutto l’invio delle email
o (b) inviare tutte le email a uno specifico indirizzo.
Disabilitare l’invio
È possibile disabilitare l’invio delle email, ponendo true nell’opzione disable_delivery. Questa è la
configurazione predefinita per l’ambiente test della distribuzione Standard. Facendo questa modifica nell’ambiente
test le email non verranno inviate durante l’esecuzione dei test ma continueranno a essere inviate negli ambienti
prod e dev:
• YAML
# app/config/config_test.yml
swiftmailer:
disable_delivery: true
• XML
<!-- app/config/config_test.xml -->
<!-xmlns:swiftmailer="http://symfony.com/schema/dic/swiftmailer"
http://symfony.com/schema/dic/swiftmailer http://symfony.com/schema/dic/swiftmailer/swiftmail
-->
<swiftmailer:config
disable-delivery="true" />
• PHP
// app/config/config_test.php
$container->loadFromExtension(’swiftmailer’, array(
’disable_delivery’ => "true",
));
Se si preferisce disabilitare l’invio anche nell’ambiente dev, basterà aggiungere la stessa configurazione nel file
config_dev.yml.
Invio a uno specifico indirizzo
È possibile anche scegliere di inviare le email a uno specifico indirizzo, invece che a quello effettivamente specificato nell’invio del messaggio. Ciò si può fare tramite l’opzione delivery_address:
• YAML
# app/config/config_dev.yml
swiftmailer:
delivery_address: [email protected]
• XML
<!-- app/config/config_dev.xml -->
<!-xmlns:swiftmailer="http://symfony.com/schema/dic/swiftmailer"
http://symfony.com/schema/dic/swiftmailer http://symfony.com/schema/dic/swiftmailer/swiftmail
-->
358
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
<swiftmailer:config
delivery-address="[email protected]" />
• PHP
// app/config/config_dev.php
$container->loadFromExtension(’swiftmailer’, array(
’delivery_address’ => "[email protected]",
));
Supponiamo di inviare un’email a [email protected].
public function indexAction($name)
{
$message = \Swift_Message::newInstance()
->setSubject(’Email di saluto’)
->setFrom(’[email protected]’)
->setTo(’[email protected]’)
->setBody($this->renderView(’HelloBundle:Hello:email.txt.twig’, array(’name’ => $name)))
;
$this->get(’mailer’)->send($message);
return $this->render(...);
}
Nell’ambiente dev, l’email verrà in realtà inviata a [email protected]. Swiftmailer aggiungerà un’ulteriore
intestazione nell’email, X-Swift-To, contenente l’indirizzo sostituito, così da poter vedere a chi sarebbe stata
inviata l’email in realtà.
Note: Oltre alle email inviate all’indirizzo to, questa configurazione blocca anche quelle inviate a qualsiasi indirizzo CC e BCC‘. Swiftmailer aggiungerà ulteriori intestazioni contenenti gli
indirizzi ignorati. Le intestazioni usate saranno ‘‘X-Swift-Cc e X-Swift-Bcc
rispettivamente per gli indirizzi in CC e per quelli in BCC.
Visualizzazione tramite Web Debug Toolbar
Utilizzando la Web Debug Toolbar è possibile visualizzare le email inviate durante la singola risposta
nell’ambiente dev. L’icona dell’email apparirà nella barra mostrando quante email sono state spedite. Cliccandoci sopra, un report mostrerà il dettaglio delle email inviate.
Se si invia un’email e immediatamente si esegue un redirect a un’altra pagina, la barra di debug del web non
mostrerà né l’icona delle email né alcun report nella pagina finale.
È però possibile, configurando a true l’opzione intercept_redirects nel file config_dev.yml, fermare il redirect in modo da permettere la visualizzazione del report con il dettaglio delle email inviate.
Tip: Alternativamente è possibile aprire il profiler in seguito al redirect e cercare l’URL utilizzato nella richiesta
precedente (p.e. /contatti/gestione). Questa funzionalità di ricerca del profiler permette di ottenere
informazioni relative a qualsiasi richiesta pregressa.
• YAML
# app/config/config_dev.yml
web_profiler:
intercept_redirects: true
• XML
3.1. Ricettario
359
Symfony2 documentation Documentation, Release 2
<!-- app/config/config_dev.xml -->
<!-- xmlns:webprofiler="http://symfony.com/schema/dic/webprofiler" -->
<!-- xsi:schemaLocation="http://symfony.com/schema/dic/webprofiler http://symfony.com/schema/
<webprofiler:config
intercept-redirects="true"
/>
• PHP
// app/config/config_dev.php
$container->loadFromExtension(’web_profiler’, array(
’intercept_redirects’ => ’true’,
));
3.1.38 Lo spool della posta
Quando si utilizza SwiftmailerBundle per l’invio delle email da un’applicazione Symfony2, queste vengono
inviate immediatamente. È però possibile evitare il rallentamento dovuto dalla comunicazione tra Swiftmailer
e il servizio di trasporto delle email che potrebbe mettere l’utente in attesa del caricamento della pagina durante
l’invio. Per fare questo basta scegliere di mettere le email in uno “spool” invece di spedirle direttamente. Questo
vuol dire che Swiftmailer non cerca di inviare le email ma invece salva i messaggi in qualche posto come, ad
esempio, in un file. Un’altro processo potrebbe poi leggere lo spool e prendersi l’incarico di inviare le email in
esso contenute. Attualmente Swiftmailer supporta solo lo spool tramite file.
Per usare lo spool, si usa la seguente configurazione:
• YAML
# app/config/config.yml
swiftmailer:
# ...
spool:
type: file
path: /percorso/file/di/spool
• XML
<!-- app/config/config.xml -->
<!-xmlns:swiftmailer="http://symfony.com/schema/dic/swiftmailer"
http://symfony.com/schema/dic/swiftmailer http://symfony.com/schema/dic/swiftmailer/swiftmail
-->
<swiftmailer:config>
<swiftmailer:spool
type="file"
path="/percorso/file/di/spool" />
</swiftmailer:config>
• PHP
// app/config/config.php
$container->loadFromExtension(’swiftmailer’, array(
// ...
’spool’ => array(
’type’ => ’file’,
’path’ => ’/percorso/file/di/spool’,
)
));
360
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
Tip: Per creare lo spool all’interno delle cartelle del progetto, è possibile usare il paramtreo %kernel.root_dir%
per indicare la cartella radice del progetto:
path: %kernel.root_dir%/spool
Fatto questo, quando un’applicazione invia un’email, questa non verrà inviata subito ma aggiunta allo spool.
L’invio delle email dallo spool viene fatto da un processo separato. Sarà un comando della console a inviare i
messaggi dallo spool:
php app/console swiftmailer:spool:send
È possibili limitare il numero di messaggi da inviare con un’apposita opzione:
php app/console swiftmailer:spool:send --message-limit=10
È anche possibile indicare un limite in secondi per l’invio:
php app/console swiftmailer:spool:send --time-limit=10
Ovviamente questo comando non dovrà essere eseguito manualmente. Il comando dovrebbe perciò essere eseguito, a intervalli regolari, come un lavoro di cron o come un’operazione pianificata.
3.1.39 Come simulare un’autenticazione HTTP in un test funzionale
Se la propria applicazione necessita di autenticazione HTTP, passare il nome utente e la password come variabili
di createClient():
$client = static::createClient(array(), array(
’PHP_AUTH_USER’ => ’nome_utente’,
’PHP_AUTH_PW’
=> ’pa$$word’,
));
Si possono anche sovrascrivere per ogni richiesta:
$client->request(’DELETE’, ’/post/12’, array(), array(
’PHP_AUTH_USER’ => ’nome_utente’,
’PHP_AUTH_PW’
=> ’pa$$word’,
));
3.1.40 Come testare l’interazione con diversi client
Se occorre simulare un’interazionoe tra diversi client (si pensi a una chat, per esempio), creare tanti client:
$harry = static::createClient();
$sally = static::createClient();
$harry->request(’POST’, ’/say/sally/Hello’);
$sally->request(’GET’, ’/messages’);
$this->assertEquals(201, $harry->getResponse()->getStatusCode());
$this->assertRegExp(’/Hello/’, $sally->getResponse()->getContent());
Questo non funziona se il proprio codice mantiene uno stato globale o se dipende da librerie di terze parti che
abbiano un qualche tipo di stato globale. In questo caso, si possono isolare i client:
$harry = static::createClient();
$sally = static::createClient();
$harry->insulate();
$sally->insulate();
3.1. Ricettario
361
Symfony2 documentation Documentation, Release 2
$harry->request(’POST’, ’/say/sally/Hello’);
$sally->request(’GET’, ’/messages’);
$this->assertEquals(201, $harry->getResponse()->getStatusCode());
$this->assertRegExp(’/Hello/’, $sally->getResponse()->getContent());
Client isolati possono eseguire trasparentemente le loro richieste in un processo PHP dedicato e pulito, evitando
quindi effetti collaterali.
Tip: Essendo un client isolato più lento, si può mantenere un client nel processo principale e isolare gli altri.
3.1.41 Come usare il profilatore nei test funzionali
È caldamente raccomandato che un test funzionale testi solo la risposta. Ma se si scrivono test funzionali che
monitorano i propri server di produzione, si potrebbe voler scrivere test sui dati di profilazione, che sono un
ottimo strumento per verificare varie cose e controllare alcune metriche.
Il profilatore di Symfony2 raccoglie diversi dati per ogni richiesta. Usare questi dati per verificare il numero
di chiamate al database, il tempo speso nel framework, eccetera. Ma prima di scrivere asserzioni, verificare sempre
che il profilatore sia effettivamente una variabile (è abilitato per impostazione predefinita in ambiente test):
class HelloControllerTest extends WebTestCase
{
public function testIndex()
{
$client = static::createClient();
$crawler = $client->request(’GET’, ’/hello/Fabien’);
// Scrivere asserzioni sulla risposta
// ...
// Check that the profiler is enabled
if ($profile = $client->getProfile()) {
// verificare il numero di richieste
$this->assertTrue($profile->getCollector(’db’)->getQueryCount() < 10);
// verifica il tempo speso nel framework
$this->assertTrue( $profile->getCollector(’timer’)->getTime() < 0.5);
}
}
}
Se un test fallisce a causa dei dati di profilazione (per esempio, troppe query al DB), si potrebbe voler usare il
profilatore web per analizzare la richiesta, dopo che i test sono finiti. È facile, basta inserire il token nel messaggio
di errore:
$this->assertTrue(
$profile->get(’db’)->getQueryCount() < 30,
sprintf(’Verifica che ci siano meno di 30 query (token %s)’, $profile->getToken())
);
Caution: I dati del profilatore possono essere differenti, a seconda dell’ambiente (specialmente se si usa
SQLite, che è configurato in modo predefinito).
Note: Le informazioni del profilatore sono disponibili anche se si isola client o se se si usa un livello HTTP per i
propri test.
362
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
Tip: Leggere le API dei raccoglitori di dati per saperne di più sulle loro interfacce.
3.1.42 Come testare i repository Doctrine
I test unitari dei repository Doctrine in un progetto Symfony non sono un compito facile. Infatti, per caricare un
repository occorre caricare le entità, un gestore di entità e un po’ di altre cose, come una connessione.
Per testare i propri repository, ci sono due opzioni diverse:
1. Test funzionali: includono l’uso di una vera connessione al database, con veri oggetti del database. Sono
facili da preparare e possono testare tutto, ma sono lenti da eseguire. Vedere Test funzionali.
2. Test unitari: i test unitari sono più veloci da eseguire e più precisi su cosa testare. Richiedono un po’
più di preparazione, come vedremo in questo documento. Possono testare solo metodi che, per esempio,
costruiscono query, non metodi che le eseguono effettivamente.
Test unitari
Poiché Symfony e Doctrine condividono lo stesso framework di test, è facile implementare test unitari nel proprio
progetto Symfony. L’ORM ha il suo insieme di strumenti, che facilitano i test unitari e i mock di ogni cosa di cui si
abbia bisogno, come una connessione, un gestore di entità, ecc. Usando i componenti dei test forniti da Dcotrine,
con un po’ di preparazione di base, si possono sfruttare gli strumenti di Doctrine per testare i propri repository.
Si tenga a mente che, se si vuole testare la reale esecuzione delle proprie query, occorrerà un test funzionale
(vedere Test funzionali). I test unitari consentono solo di tesare un metodo che costruisce una query.
Preparazione
Inannzitutto, occorre aggiungere lo spazio dei nomi Doctrine\Tests al proprio autoloader:
// app/autoload.php
$loader->registerNamespaces(array(
//...
’Doctrine\\Tests’
));
=> __DIR__.’/../vendor/doctrine/tests’,
Poi, occorrerà preparare un gestore di entità in ogni test, in modo che Doctrine possa caricare le entità e i repository.
Poiché Doctrine da solo non è in grado di caricare i meta-dati delle annotazioni dalle entità, occorrerà configurare
il lettore di annotazioni per poter analizzare e caricare le entità:
// src/Acme/ProductBundle/Tests/Entity/ProductRepositoryTest.php
namespace Acme\ProductBundle\Tests\Entity;
use
use
use
use
Doctrine\Tests\OrmTestCase;
Doctrine\Common\Annotations\AnnotationReader;
Doctrine\ORM\Mapping\Driver\DriverChain;
Doctrine\ORM\Mapping\Driver\AnnotationDriver;
class ProductRepositoryTest extends OrmTestCase
{
private $_em;
protected function setUp()
{
$reader = new AnnotationReader();
$reader->setIgnoreNotImportedAnnotations(true);
$reader->setEnableParsePhpImports(true);
$metadataDriver = new AnnotationDriver(
3.1. Ricettario
363
Symfony2 documentation Documentation, Release 2
$reader,
// fornisce lo spazio dei nomi delle entità che si vogliono testare
’Acme\\ProductBundle\\Entity’
);
$this->_em = $this->_getTestEntityManager();
$this->_em->getConfiguration()
->setMetadataDriverImpl($metadataDriver);
// consente di usare la sintassi AcmeProductBundle:Product
$this->_em->getConfiguration()->setEntityNamespaces(array(
’AcmeProductBundle’ => ’Acme\\ProductBundle\\Entity’
));
}
}
Guardando il codice, si può notare:
• Si estende da \Doctrine\Tests\OrmTestCase, che fornisce metodi utili per i test unitari;
• Occorre preparare AnnotationReader per poter analizzare e caricare le entità;
• Si crea il gestore di entità, richiamando _getTestEntityManager, che restituisce il mock di un gestore
di entità, con il mock di una connessione.
Ecco fatto! Si è pronti per scrivere test unitari per i repository Doctrine.
Scrivere i test unitari
Tenere a mente che i metodi dei repository Doctrine possono essere testati solo se costruiscono e restituiscono una
query (senza eseguirla). Si consideri il seguente esempio:
// src/Acme/StoreBundle/Entity/ProductRepository
namespace Acme\StoreBundle\Entity;
use Doctrine\ORM\EntityRepository;
class ProductRepository extends EntityRepository
{
public function createSearchByNameQueryBuilder($name)
{
return $this->createQueryBuilder(’p’)
->where(’p.name LIKE :name’)
->setParameter(’name’, $name);
}
}
In questo esempio, il metodo restituisce un’istanza di QueryBuilder. Si può testare il risultato di questo
metodo in molti modi:
class ProductRepositoryTest extends \Doctrine\Tests\OrmTestCase
{
/* ... */
public function testCreateSearchByNameQueryBuilder()
{
$queryBuilder = $this->_em->getRepository(’AcmeProductBundle:Product’)
->createSearchByNameQueryBuilder(’foo’);
$this->assertEquals(’p.name LIKE :name’, (string) $queryBuilder->getDqlPart(’where’));
$this->assertEquals(array(’name’ => ’foo’), $queryBuilder->getParameters());
364
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
}
}
In questo test, si disseziona l’oggetto QueryBuilder, cercando che ogni parte sia come ci si aspetta. Se si
aggiungessero altre cose al costruttore di query, si potrebbero verificare le parti DQL: select, from, join,
set, groupBy, having o orderBy.
Se si ha solo un oggetto Query grezzo o se si preferisce testare la vera query, si può testare direttamente la query
DQL:
public function testCreateSearchByNameQueryBuilder()
{
$queryBuilder = $this->_em->getRepository(’AcmeProductBundle:Product’)
->createSearchByNameQueryBuilder(’foo’);
$query = $queryBuilder->getQuery();
// testa la DQL
$this->assertEquals(
’SELECT p FROM Acme\ProductBundle\Entity\Product p WHERE p.name LIKE :name’,
$query->getDql()
);
}
Test funzionali
Se occorre eseguire effettivamente una query, occorrerò far partire il kernel, per ottenere una connessione valida.
In questo caso, si estenderà WebTestCase, che rende tutto alquanto facile:
// src/Acme/ProductBundle/Tests/Entity/ProductRepositoryFunctionalTest.php
namespace Acme\ProductBundle\Tests\Entity;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
class ProductRepositoryFunctionalTest extends WebTestCase
{
/**
* @var \Doctrine\ORM\EntityManager
*/
private $_em;
public function setUp()
{
$kernel = static::createKernel();
$kernel->boot();
$this->_em = $kernel->getContainer()
->get(’doctrine.orm.entity_manager’);
}
public function testProductByCategoryName()
{
$results = $this->_em->getRepository(’AcmeProductBundle:Product’)
->searchProductsByNameQuery(’foo’)
->getResult();
$this->assertEquals(count($results), 1);
}
}
3.1. Ricettario
365
Symfony2 documentation Documentation, Release 2
3.1.43 Come aggiungere la funzionalità “ricordami” al login
Una volta che l’utente è autenticato, le sue credenziali sono solitamente salvate nella sessione. Questo vuol dire
che quando la sessione finisce, l’utente sarà fuori dal sito e dovrà inserire nuovamente le sue informazioni di login,
la prossima volta che vorrà accedere all’applicazione. Si può consentire agli utenti di scegliere di rimanere dentro
più a lungo del tempo della sessione, usando un cookie con l’opzione remember_me del firewall. Il firewall ha
bisogno di una chiave segreta configurata, usata per codificare il contenuto del cookie. Ci sono anche molte altre
opzioni, con valori predefiniti mostrati di seguito:
• YAML
# app/config/security.yml
firewalls:
main:
remember_me:
key:
aSecretKey
lifetime: 3600
path:
/
domain:
~ # Defaults to the current domain from $_SERVER
• XML
<!-- app/config/security.xml -->
<config>
<firewall>
<remember-me
key="aSecretKey"
lifetime="3600"
path="/"
domain="" <!-- Defaults to the current domain from $_SERVER -->
/>
</firewall>
</config>
• PHP
// app/config/security.php
$container->loadFromExtension(’security’, array(
’firewalls’ => array(
’main’ => array(’remember_me’ => array(
’key’
=> ’/login_check’,
’lifetime’
=> 3600,
’path’
=> ’/’,
’domain’
=> ’’, // Defaults to the current domain from $_SERVER
)),
),
));
È una buona idea dare all’utente la possibilità di usare o non usare la funzionalità “ricordami”, perché non sempre
è appropriata. Il modo usuale per farlo è l’aggiunta di un checkbox al form di login. Dando al checkbox il
nome _remember_me, il cookie sarà automaticamente impostato quando il checkbox è spuntato e l’utente entra.
Quindi, il proprio form di login potrebbe alla fine assomigliare a questo:
• Twig
{# src/Acme/SecurityBundle/Resources/views/Security/login.html.twig #}
{% if error %}
<div>{{ error.message }}</div>
{% endif %}
<form action="{{ path(’login_check’) }}" method="post">
<label for="username">Nome utente:</label>
<input type="text" id="username" name="_username" value="{{ last_username }}" />
366
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
<label for="password">Password:</label>
<input type="password" id="password" name="_password" />
<input type="checkbox" id="remember_me" name="_remember_me" checked />
<label for="remember_me">Ricordami</label>
<input type="submit" name="login" />
</form>
• PHP
<?php // src/Acme/SecurityBundle/Resources/views/Security/login.html.php ?>
<?php if ($error): ?>
<div><?php echo $error->getMessage() ?></div>
<?php endif; ?>
<form action="<?php echo $view[’router’]->generate(’login_check’) ?>" method="post">
<label for="username">Nome utente:</label>
<input type="text" id="username"
name="_username" value="<?php echo $last_username ?>" />
<label for="password">Password:</label>
<input type="password" id="password" name="_password" />
<input type="checkbox" id="remember_me" name="_remember_me" checked />
<label for="remember_me">Ricordami</label>
<input type="submit" name="login" />
</form>
L’utente sarà quindi automaticamente autenticato nelle sue visite successive, finché il cookie resta valido.
Costringere l’utente ad autenticarsi di nuovo prima di accedere ad alcune risorse
Quando l’utente torna sul sito, viene autenticato automaticamente in base alle informazioni memorizzate nel
cookie “ricordami”. Ciò consente all’utente di accedere a risorse protette, come se si fosse effettivamente autenticato prima di entrare nel sito.
In alcuni casi, si potrebbe desiderare di costringere l’utente ad autenticarsi nuovamente, prima di accedere ad
alcune risorse. Per esempio, si potrebbe voler consentire un “ricordami” per vedere le informazioni di base di un
account, ma poi richiedere un’effettiva autenticazione prima di modificare le informazioni stesse.
Il componente della sicurezza fornisce un modo facile per poterlo fare. In aggiunta ai ruoli esplicitamente assegnati
loro, agli utenti viene dato automaticamente uno dei seguenti ruoli, a seconda di come si sono autenticati:
• IS_AUTHENTICATED_ANONYMOUSLY - assegnato automaticamente a un utente che si trova in una parte
del sito protetta dal firewall, ma che non si è effettivamente autenticato. Ciò è possibile solo se è consentito
l’accesso anonimo.
• IS_AUTHENTICATED_REMEMBERED - assegnato automaticamente a un utente che si è autenticato
tramite un cookie “ricordami”.
• IS_AUTHENTICATED_FULLY - assegnato automaticamente a un utente che ha fornito le sue informazioni
di autenticazione durante la sessione corrente.
Si possono usare questi ruoli, oltre a quelli espliciti, per controllare l’accesso.
Note:
Se si ha il ruolo IS_AUTHENTICATED_REMEMBERED, si ha anche il ruolo
IS_AUTHENTICATED_ANONYMOUSLY. Se si ha il ruolo IS_AUTHENTICATED_FULLY, si hanno anche gli
altri due ruoli. In altre parole, questi ruoli rappresentano tre livelli incrementali della “forza” dell’autenticazione.
3.1. Ricettario
367
Symfony2 documentation Documentation, Release 2
Si possono usare questi ruoli addizionali per affinare il controllo sugli accessi a parti di un sito. Per esempio, si
potrebbe desiderare che l’utente sia in grado di vedere il suo account in /account se autenticato con cookie,
ma che debba fornire le sue informazioni di accesso per poterlo modificare. Lo si può fare proteggendo specifiche
azioni del controllore, usando questi ruoli. L’azione di modifica del controllore potrebbe essere messa in sicurezza
usando il contesto del servizio.
Nel seguente esempio, l’azione è consentita solo se l’utente ha il ruolo IS_AUTHENTICATED_FULLY.
use Symfony\Component\Security\Core\Exception\AccessDeniedException
// ...
public function editAction()
{
if (false === $this->get(’security.context’)->isGranted(
’IS_AUTHENTICATED_FULLY’
)) {
throw new AccessDeniedException();
}
// ...
}
Si può anche installare opzionalmente JMSSecurityExtraBundle, che può mettere in sicurezza il controllore
tramite annotazioni:
use JMS\SecurityExtraBundle\Annotation\Secure;
/**
* @Secure(roles="IS_AUTHENTICATED_FULLY")
*/
public function editAction($name)
{
// ...
}
Tip: Se si avesse anche un controllo di accesso nella propria configurazione della sicurezza, che richiede
all’utente il ruolo ROLE_USER per poter accedere all’area dell’account, si avrebbe la seguente situazione:
• Se un utente non autenticato (o anonimo) tenta di accedere all’area dell’account, gli sarà chiesto di autenticarsi.
• Una volta inseriti nome utente e password, ipotizzando che l’utente riceva il ruolo ROLE_USER in base alla
configurazione, l’utente avrà il ruolo IS_AUTHENTICATED_FULLY e potrà accedere a qualsiasi pagina
della sezione account, incluso il controllore editAction.
• Se la sessione scade, quando l’utente torna sul sito, potrà accedere a ogni pagina della sezione account,
tranne per quella di modifica, senza doversi autenticare nuovamente. Tuttavia, quando proverà ad accedere
al controllore editAction, sarà costretto ad autenticarsi di nuovo, perché non è ancora pienamente autenticato.
Per maggiori informazioni sulla messa in sicurezza di servizi o metodi con questa tecnica, vedere Proteggere
servizi e metodi di un’applicazione.
3.1.44 Come implementare i propri votanti per una lista nera di indirizzi IP
Il componente della sicurezza di Symfony2 fornisce diversi livelli per autenticare gli utenti. Uno dei livelli
è chiamato voter. Un votante è una classe dedicata a verificare che l’utente abbia i diritti per connettersi
all’applicazione. Per esempio, Symfony2 fornisce un livello che verifica se l’utente è pienamente autenticato
oppure se ha dei ruoli.
A volte è utile creare un votante personalizzato, per gestire un caso specifico, non coperto dal framework. In
questa sezione, si imparerà come creare un votante che consenta di mettere gli utenti una lista nera, in base al loro
368
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
IP.
L’interfaccia Voter
Un votante personalizzato deve implementare Symfony\Component\Security\Core\Authorization\Voter\VoterI
che richiede i seguenti tre metodi:
interface VoterInterface
{
function supportsAttribute($attribute);
function supportsClass($class);
function vote(TokenInterface $token, $object, array $attributes);
}
Il metodo supportsAttribute() è usato per verificare che il votante supporti l’attributo utente dato (p.e.:
un ruolo, un’ACL, ecc.)
Il metodo supportsClass() è usato per verificare che il votante supporti l’attuale classe per il token
dell’utente.
Il metodo vote() deve implementare la logica di business che verifica se l’utente possa avere accesso o meno.
Questo metodo deve restituire uno dei seguenti valori:
• VoterInterface::ACCESS_GRANTED: L’utente può accedere all’applicazione
• VoterInterface::ACCESS_ABSTAIN: Il votante non può decidere se l’utente possa accedere o meno
• VoterInterface::ACCESS_DENIED: L’utente non può accedere all’applicazione
In questo esempio, verificheremo la corrispondenza dell’indirizzo IP dell’utente con una lista nera di indirizzi. Se
l’IP dell’utente è nella lista nera, restituiremo VoterInterface::ACCESS_DENIED, altrimenti restituiremo
VoterInterface::ACCESS_ABSTAIN, perché lo scopo del votante è solo quello di negare l’accesso, non
di consentirlo.
Creare un votante personalizzato
Per inserire un utente nella lista nera in base al suo IP, possiamo usare il servizio request e confrontare
l’indirizzo IP con un insieme di indirizzi IP in lista nera:
namespace Acme\DemoBundle\Security\Authorization\Voter;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
class ClientIpVoter implements VoterInterface
{
public function __construct(ContainerInterface $container, array $blacklistedIp = array())
{
$this->container
= $container;
$this->blacklistedIp = $blacklistedIp;
}
public function supportsAttribute($attribute)
{
// non verifichiamo l’attributo utente, quindi restituiamo true
return true;
}
public function supportsClass($class)
{
// il nostro votante supporta ogni tipo di classe token, quindi restituiamo true
return true;
3.1. Ricettario
369
Symfony2 documentation Documentation, Release 2
}
function vote(TokenInterface $token, $object, array $attributes)
{
$request = $this->container->get(’request’);
if (in_array($this->request->getClientIp(), $this->blacklistedIp)) {
return VoterInterface::ACCESS_DENIED;
}
return VoterInterface::ACCESS_ABSTAIN;
}
}
Ecco fatto! Il votante è pronto. Il prossimo passo consiste nell’iniettare il votante dentro al livello della sicurezza.
Lo si può fare facilmente tramite il contenitore di servizi.
Dichiarare il votante come servizio
Per iniettare il votante nel livello della sicurezza, dobbiamo dichiararlo come servizio e taggarlo come “security.voter”:
• YAML
# src/Acme/AcmeBundle/Resources/config/services.yml
services:
security.access.blacklist_voter:
class:
Acme\DemoBundle\Security\Authorization\Voter\ClientIpVoter
arguments: [@service_container, [123.123.123.123, 171.171.171.171]]
public:
false
tags:
{ name: security.voter }
• XML
<!-- src/Acme/AcmeBundle/Resources/config/services.xml -->
<service id="security.access.blacklist_voter"
class="Acme\DemoBundle\Security\Authorization\Voter\ClientIpVoter" public="false">
<argument type="service" id="service_container" strict="false" />
<argument type="collection">
<argument>123.123.123.123</argument>
<argument>171.171.171.171</argument>
</argument>
<tag name="security.voter" />
</service>
• PHP
// src/Acme/AcmeBundle/Resources/config/services.php
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
$definition = new Definition(
’Acme\DemoBundle\Security\Authorization\Voter\ClientIpVoter’,
array(
new Reference(’service_container’),
array(’123.123.123.123’, ’171.171.171.171’),
),
);
$definition->addTag(’security.voter’);
$definition->setPublic(false);
370
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
$container->setDefinition(’security.access.blacklist_voter’, $definition);
Tip: Assicurarsi di importare questo file di configurazione dal proprio file di configurazione principale (p.e.
app/config/config.yml). Per ulteriori informazioni, vedere Importare la configurazione con imports. Per
saperne di più sulla definizione di servizi in generale, vederre il capitolo Contenitore di servizi.
Cambiare la strategia decisionale per l’accesso
Per far sì che il votante abbia effetto, occorre modificare la strategia decisionale predefinita per l’accesso, che, per
impostazione predefinita, consente l’accesso se uno qualsiasi dei votanti consente l’accesso.
Nel nostro caso, sceglieremo la strategia unanimous. A differenza della strategia affirmative (quella
predefinita), con la strategia unanimous, l’accesso all’utente è negato se anche solo uno dei votanti (p.e.
ClientIpVoter) lo nega.
Per poterlo fare, sovrascrivere la sezione access_decision_manager del file di configurazione della propria
applicazione con il codice seguente.
• YAML
# app/config/security.yml
security:
access_decision_manager:
# Strategy can be: affirmative, unanimous or consensus
strategy: unanimous
Ecco fatto! Ora, nella decisione sull’accesso di un utente, il nuovo votante negherà l’accesso a ogni utente nella
lista nera degli IP.
3.1.45 Access Control List (ACL)
In applicazioni complesse, ci si trova spesso con il problema di decisioni di accesso che non possono essere basate
solo sulla persona che lo richiede (il cosiddetto Token), ma che coinvolgono anche un oggetto del dominio per
cui l’accesso viene richiesto. Qui entrano in gioco le ACL.
Si immagini di progettare un sistema di blog, in cui gli utenti possano commentare i post. Ora, si vuole che
un utente sia in grado di modificare i propri commenti, ma non quelli degli altri utenti; inoltre, si vuole che
l’amministratore possa modificare tutti i commenti. In questo scenario, Comment sarebbe l’oggetto del dominio
a cui si vuole restringere l’accesso. Si possono usare diversi approcci per ottenere questo scopo in Symfony2, due
approcci di base (non esaustivi) sono:
• Gestire la sicurezza nei metodi: Di base, questo vuol dire mantenere un riferimento in ogni oggetto
Comment a tutti gli utenti che vi hanno accesso, e quindi confrontare tali utenti con quello del Token.
• Gestire la sicurezza con i ruoli: In questo approccio, si aggiunge un ruolo per ogni oggetto Comment, p.e.
ROLE_COMMENT_1, ROLE_COMMENT_2, ecc.
Entrambi gli approcci sono perfettamente validi. Tuttavia, accoppiano la logica di autorizzazione al proprio codice,
il che li rende meno riutilizzabili altrove, e inoltre incrementano le difficoltà nei test unitari. D’altra parte, si
potrebbero avere problemi di prestazioni, se molti utenti avessero accesso a un singolo oggetto del dominio.
Per fortuna, c’è un modo migliore, di cui ora parleremo.
Preparazione
Prima di entrare in azione, ci occorre una breve preparazione. Innanzitutto, occorre configurare la connessione al
sistema ACL da usare:
• YAML
3.1. Ricettario
371
Symfony2 documentation Documentation, Release 2
# app/config/security.yml
security:
acl:
connection: default
• XML
<!-- app/config/security.xml -->
<acl>
<connection>default</connection>
</acl>
• PHP
// app/config/security.php
$container->loadFromExtension(’security’, ’acl’, array(
’connection’ => ’default’,
));
Note: Il sistema ACL richiede almeno una connessione di Doctrine configurata. Tuttavia, questo non significa
che si debba usare Doctrine per mappare i propri oggetti del dominio. Si può usare qualsiasi mapper si desideri
per i propri oggetti, sia esso l’ORM Doctrine, l’ODM Mongo, Propel o anche SQL puro, la scelta è lasciata allo
sviluppatore.
Dopo aver configurato la connessione, occorre importare la struttura del database. Fortunatamente, c’è un task per
farlo. Basta eseguire il comando seguente:
php app/console init:acl
Iniziare
Tornando al piccolo esempio iniziale, implementiamo una ACL.
Creare una ACL e aggiungere un ACE
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Acl\Domain\ObjectIdentity;
use Symfony\Component\Security\Acl\Domain\UserSecurityIdentity;
use Symfony\Component\Security\Acl\Permission\MaskBuilder;
// ...
// BlogController.php
public function addCommentAction(Post $post)
{
$comment = new Comment();
// preparazione di $form e collegamento dei dati
// ...
if ($form->isValid()) {
$entityManager = $this->get(’doctrine.orm.default_entity_manager’);
$entityManager->persist($comment);
$entityManager->flush();
// creazione dell’ACL
$aclProvider = $this->get(’security.acl.provider’);
$objectIdentity = ObjectIdentity::fromDomainObject($comment);
$acl = $aclProvider->createAcl($objectIdentity);
372
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
// recupero dell’identità di sicurezza dell’utente attuale
$securityContext = $this->get(’security.context’);
$user = $securityContext->getToken()->getUser();
$securityIdentity = UserSecurityIdentity::fromAccount($user);
// l’utente può accedere
$acl->insertObjectAce($securityIdentity, MaskBuilder::MASK_OWNER);
$aclProvider->updateAcl($acl);
}
}
In questo pezzo di codice ci sono alcune importanti decisioni implementative. Per ora, ne mettiamo in evidenza
solo due:
Prima di tutto, il metodo ->createAcl() non accetta direttamente oggetti del dominio, ma solo implementazioni di ObjectIdentityInterface. Questo passo aggiuntivo consente di lavorare con le ACL, anche
se non si hanno veri oggetti del dominio a portata di mano. Questo può essere molto utile quando si vogliono
verificare i permessi di un gran numero di oggetti, senza dover idratare gli oggetti stessi.
L’altra parte interessante è la chiamata a ->insertObjectAce(). Nel nostro esempio, stiamo consentendo
l’accesso come proprietario del commento all’utente corrente. La costante MaskBuilder::MASK_OWNER è un
intero predefinito; non ci si deve preoccupare, perché il costruttore di maschere astrae la maggior parte dei dettagli
tecnici, ma usando questa tecnica si possono memorizzare molti permessi diversi in una singola riga di database,
che fornisce un considerevole vantaggio in termini di prestazioni.
Tip: L’ordine in cui gli ACE sono verificati è significativo. Come regola generale, si dovrebbero mettere le voci
più specifiche all’inizio.
Verifica dell’accesso
// BlogController.php
public function editCommentAction(Comment $comment)
{
$securityContext = $this->get(’security.context’);
// verifica per l’accesso in modifica
if (false === $securityContext->isGranted(’EDIT’, $comment))
{
throw new AccessDeniedException();
}
// recuperare l’oggetto commento e fare le modifiche
// ...
}
In questo esempio, verifichiamo se l’utente abbia il permesso EDIT. Internamente, Symfony2 mappa i permessi a
diversi interi e verifica se l’uente possieda uno di essi.
Note: Si possono definire fino a 32 permessi base (a seconda del proprio sistema operativo, può variare tra 30 e
32). Inoltre, si possono anche definire dei permessi cumulativi.
Permessi cumulativi
Nel nostro primo esempio, abbiamo assegnato al’utente solo il permesso di base OWNER. Sebbene questo consenta
effettivamente all’utente di eseguire qualsiasi operazione sull’oggetto del dominio, come vedere, modificare, ecc.,
ci sono dei casi in cui si vuole assegnare tali permessi in modo esplicito.
MaskBuilder può essere usato per creare facilmente delle maschere, combinando diversi permessi di base:
3.1. Ricettario
373
Symfony2 documentation Documentation, Release 2
$builder = new MaskBuilder();
$builder
->add(’view’)
->add(’edit’)
->add(’delete’)
->add(’undelete’)
;
$mask = $builder->get(); // int(15)
Questa maschera può quindi essere usata per assegnare all’utente i permessi di base aggiunti in precedenza:
$acl->insertObjectAce(new UserSecurityIdentity(’johannes’), $mask);
Ora l’utente ha il permesso di vedere, modificare, cancellare e ripristinare gli oggetti.
3.1.46 Concetti avanzati su ACL
Lo scopo di questa ricetta è approfondire il sistema ACL, nonché spiegare alcune decisioni progettuali che ne
stanno alle spalle.
Concetti progettuali
Le capacità di sicurezza degli oggetti di Symfony2 sono basate sul concetto di Access Control List. Ogni istanza
di un oggetto del dominio ha la sua ACL. L’istanza ACL contiene una lista dettagliata di Access Control Entry
(ACE), usate per prendere decisioni sugli accessi. Il sistema ACL di Symfony2 si focalizza su due obiettivi
principali:
• fornire un modo per recuperare in modo efficiente grosse quantità di ACL/ACE per gli oggetti del dominio
e modificarli;
• fornire un modo per prendere facilmente decisioni sulla possibilità o meno che qualcuno esegua un’azione
su un oggetto del dominio.
Come indicato nel primo punto, una delle capacità principali del sistema ACL di Symfony2 è il modo altamente
performante con cui recupera ACL e ACE. Questo è molto importante, perché ogni ACL può avere tanti ACE,
nonché ereditare da altri ACL, come in un albero. Quindi, non ci appoggiamo specificatamente ad alcun ORM,
ma l’implementazione predefinita interagisce con la connessione usando direttamente il DBAL di Doctrine.
Identità degli oggetti
Il sistema ACL è interamente disaccoppiato dagli oggetti del dominio. Non devono nemmeno essere nello stesso
database o nello stesso server. Per ottenere tale disaccoppiamento, nel sistema ACL gli oggetti sono rappresentati
tramite oggetti identità di oggetti. Ogni volta che si vuole recuperare l’ACL per un oggetto del dominio, il sistema
ACL creerà prima un oggetto identità a partire dall’oggetto del dominio, quindi passerà tale oggetto identità al
fornitore ACL per ulteriori processi.
Identità di sicurezza
È analoga all’identità degli oggetti, ma rappresenta un utente o un ruolo nell’applicazione. Ogni ruolo, o utente,
ha la sua identità di sicurezza.
Struttura delle tabelle del database
L’implementazione predefinita usa cinque tabelle del database, elencate sotto. Le tabelle sono ordinate dalla più
piccola alla più grande, in una tipica applicazione:
374
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
• acl_security_identities: questa tabella registra tutte le identità di sicurezza (SID) che contengono
ACE. L’implementazione predefinita ha due identità di sicurezza, RoleSecurityIdentity e
UserSecurityIdentity
• acl_classes: questa tabella mappa i nomi delle classi con identificatori univoci, a cui possono fare riferimento le altre tabelle
• acl_object_identities: ogni riga in questa tabella rappresebta un’istanza di un singolo oggetto del dominio
• acl_object_identity_ancestors: questa tabella consente di determinare tutti gli antenati di un ACL in modo
molto efficiente
• acl_entries: questa tabella contiene tutti gli ACE. Questa è tipicamente la tabella con più righe. Può contenerne decine di milioni, senza impattare in modo significativo sulle prestazioni
Visibilità degli Access Control Entry
Gli ACE possono avere visibilità diverse in cui applicarsi. In Symfony2, ci sono di base due visibilità:
• di classe: questi ACE si applicano a tutti gli oggetti della stessa classe
• di oggetto: questa è l’unica visibilità usata nel capitolo precedente e si applica solo a uno specifico oggetto
A volte, si avrà bisogno di applicare un ACE solo a uno specifico campo dell’oggetto. Si supponga di volere
che l’ID sia visibile da un amministratore, ma non dal servizio clienti. Per risolvere questo problema comune,
abbiamo aggiunto altre due sotto-visibilità:
• di campo di classe: questi ACE si applicano a tutti gli oggetti della stessa classe, ma solo a un campo
specifico
• di campo di oggetto: questi ACE si applicano a uno specifico oggetto e solo a uno specifico campo di tale
oggetto
Decisioni pre-autorizzazione
Per decisioni pre-autorizzazione, che sono decisioni da prendere prima dell’invocazione di un metodo o di
un’azione sicura, ci si appoggia sul servizio AccessDecisionManager, usato anche per prendere decisioni di
autorizzazione sui ruoli. Proprio come i ruoli, il sistema ACL aggiunge molti nuovi attributi, che possono essere
usati per verificare diversi permessi.
3.1. Ricettario
375
Symfony2 documentation Documentation, Release 2
Mappa predefinita dei permessi
Attributo
VIEW
Significato inteso
Maschere di bit
Se è consentito vedere l’oggetto del dominio
EDIT
Se è consentito modificare l’oggetto del dominio
VIEW, EDIT,
OPERATOR, MASTER o
OWNER
EDIT, OPERATOR,
MASTER o OWNER
CREATE, OPERATOR,
MASTER o OWNER
DELETE, OPERATOR,
MASTER o OWNER
UNDELETE,
OPERATOR, MASTER o
OWNER
OPERATOR, MASTER o
OWNER
CRESe è consentito creare l’oggetto del dominio
ATE
DELETE Se è consentito eliminare l’oggetto del dominio
UNSe è consentito ripristinare un precedente oggetto del dominio
DELETE
OPERATOR
MASTER
OWNER
Se è consentito eseguire tutte le azioni precedenti
Se è consentito eseguire tutte le azioni precedenti e inoltre è
consentito concedere uno dei permessi precedenti ad altri
Se si possiede l’oggetto del dominio. Il proprietario può eseguire
tutte le azioni precedenti e concedere i permessi master e owner
MASTER o OWNER
OWNER
Attributi dei permessi o maschere di bit dei permessi
Gli attributi sono usati da AccessDecisionManager, così come i ruoli sono attributi usati da
AccessDecisionManager. Spesso, tali attributi rappresentano di f atto un aggregato di maschere di bit.
Le maschere di bit, d’altro canto, sono usate internamente dal sistema ACL per memorizzare in modo efficiente i
permessi degli utenti sul database e verificare gli accessi, usando operazioni di bit molto veloci.
Estensibilità
La mappa dei permessi vista sopra non è affatto statica e in teoria può essere sostituita totalmente. Tuttavia,
dovrebbe essere in grado di coprire la maggior parte dei problemi che si incontrano e, per interoperabilità con altri
bundle, si raccomanda di mantenere i significati che gli abbiamo attribuito.
Decisioni post-autorizzazione
Le decisioni post-autorizzazione sono eseguite dopo che un metodo sicuro è stato invocato e coinvolgono solitamente oggetti del dominio restituiti da tali metodi. Dopo l’invocazione, i fornitori consentono anche di modificare
o filtrare gli oggetti del dominio, prima che siano restituiti.
A causa di limitazioni del linguaggio PHP, non ci sono capacità di post-autorizzazione predefinite nel componente
della sicurezza. Tuttavia, c’è un bundle sperimentale, JMSSecurityExtraBundle, che aggiunge tali capacità. Si
veda la documentazione del bundle per maggiori informazioni sulla loro implementazione.
Processo di determinazione dell’autorizzazione
La classe ACL fornisce due metodi per determinare se un’identità di sicurezza abbia i bit richiesti, isGranted e
isFieldGranted. Quando l’ACL riceve una richiesta di autorizzazione tramite uno di questi metodi, delega la
richiesta a un’implementazione di PermissionGrantingStrategy. Questo consente di sostituire il modo
in cui sono prese le decisioni di accesso, senza dover modificare la classe ACL stessa.
376
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
PermissionGrantingStrategy verifica prima tutti gli ACE con visibilità di oggetto. Se nessuno è applicabile, verifica gli ACE con visibilità di classe. Se nessuno è applicabile, il processo viene ripetuto con gli ACE
dell’ACL genitore. Se non esiste alcun ACL genitore, viene sollevata un’eccezione.
3.1.47 Come forzare HTTPS o HTTP per URL diversi
Si possono forzare aree del proprio sito a usare il protocollo HTTPS nella configurazione della sicurezza. Lo
si può fare tramite le regole access_control, usando l’opzione requires_channel. Per esempio, se si
vogliono forzare tutti gli URL che iniziano per /secure a usare HTTPS, si può usare la seguente configurazione:
• YAML
access_control:
- path: ^/secure
roles: ROLE_ADMIN
requires_channel: https
• XML
<access-control>
<rule path="^/secure" role="ROLE_ADMIN" requires_channel="https" />
</access-control>
• PHP
’access_control’ => array(
array(’path’ => ’^/secure’,
’role’ => ’ROLE_ADMIN’,
’requires_channel’ => ’https’
),
),
Il form di login deve consentire l’accesso anonimo, altrimenti l’utente sarebbe impossibilitato ad autenticarsi. Per forzarlo a usare HTTPS, si possono usare ancora le regole access_control con il ruolo
IS_AUTHENTICATED_ANONYMOUSLY:
• YAML
access_control:
- path: ^/login
roles: IS_AUTHENTICATED_ANONYMOUSLY
requires_channel: https
• XML
<access-control>
<rule path="^/login"
role="IS_AUTHENTICATED_ANONYMOUSLY"
requires_channel="https" />
</access-control>
• PHP
’access_control’ => array(
array(’path’ => ’^/login’,
’role’ => ’IS_AUTHENTICATED_ANONYMOUSLY’,
’requires_channel’ => ’https’
),
),
È anche possibile specificare l’uso di HTTPS nella configurazione delle rotte, vedere Come forzare le rotte per
utilizzare sempre HTTPS per maggiori dettagli.
3.1. Ricettario
377
Symfony2 documentation Documentation, Release 2
3.1.48 Come personalizzare il form di login
L’uso di un form di login per l’autenticazione è un metodo comune e flessibile per gestire l’autenticazione in
Symfony2. Quasi ogni aspetto del form è personalizzabile. La configurazione predefinita e completa è mostrata
nella prossima sezione.
Riferimento di configurazione del form di login
• YAML
# app/config/security.yml
security:
firewalls:
main:
form_login:
# the user is redirected here when he/she needs to login
login_path:
/login
# if true, forward the user to the login form instead of redirecting
use_forward:
false
# submit the login form here
check_path:
/login_check
# by default, the login form *must* be a POST, not a GET
post_only:
true
# login success redirecting options (read further below)
always_use_default_target_path: false
default_target_path:
/
target_path_parameter:
_target_path
use_referer:
false
# login failure redirecting options (read further below)
failure_path:
null
failure_forward:
false
# field names for the username and password fields
username_parameter:
_username
password_parameter:
_password
# csrf token options
csrf_parameter:
intention:
_csrf_token
authenticate
• XML
<!-- app/config/security.xml -->
<config>
<firewall>
<form-login
check_path="/login_check"
login_path="/login"
use_forward="false"
always_use_default_target_path="false"
default_target_path="/"
target_path_parameter="_target_path"
use_referer="false"
failure_path="null"
failure_forward="false"
username_parameter="_username"
password_parameter="_password"
378
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
csrf_parameter="_csrf_token"
intention="authenticate"
post_only="true"
/>
</firewall>
</config>
• PHP
// app/config/security.php
$container->loadFromExtension(’security’, array(
’firewalls’ => array(
’main’ => array(’form_login’ => array(
’check_path’
=> ’/login_check’,
’login_path’
=> ’/login’,
’user_forward’
=> false,
’always_use_default_target_path’ => false,
’default_target_path’
=> ’/’,
’target_path_parameter’
=> _target_path,
’use_referer’
=> false,
’failure_path’
=> null,
’failure_forward’
=> false,
’username_parameter’
=> ’_username’,
’password_parameter’
=> ’_password’,
’csrf_parameter’
=> ’_csrf_token’,
’intention’
=> ’authenticate’,
’post_only’
=> true,
)),
),
));
Rinvio dopo il successo
Si può modificare il posto in cui il form di login rinvia dopo un login eseguito con successo, usando le varie opzioni di configurazione.
Per impostazione predefinita, il form rinvierà all’URL
richiesto dall’utente (cioè l’URL che ha portato al form di login).
Per esempio, se l’utente ha
richiesto http://www.example.com/admin/post/18/edit, sarà successivamente rimandato indietro
a http://www.example.com/admin/post/18/edit, dopo il login. Questo grazie alla memorizzazione
in sessione dell’URL richiesto. Se non c’è alcun URL in sessione (forse l’utente ha richiesto direttamente la pagina di login), l’utente è rinviato alla pagina predefinita, che è / (ovvero la homepage). Si può modificare questo
comportamento in diversi modi.
Note: Come accennato, l’utente viene rinviato alla pagina che ha precedentemente richiesto. A volte questo può
causare problemi, per esempio se una richiesta AJAX eseguita in background appare come ultimo URL visitato,
rinviando quindi l’utente in quell’URL. Per informazioni su come controllare questo comportamento, vedere Come
cambiare il comportamento predefinito del puntamento del percorso.
Cambiare la pagina predefinita
Prima di tutto, la pagina predefinita (la pagina a cui l’utente viene rinviato, se non ci sono pagine precedenti in
sessione) può essere impostata. Per impostarla a /admin, usare la seguente configurazione:
• YAML
# app/config/security.yml
security:
firewalls:
main:
3.1. Ricettario
379
Symfony2 documentation Documentation, Release 2
form_login:
# ...
default_target_path: /admin
• XML
<!-- app/config/security.xml -->
<config>
<firewall>
<form-login
default_target_path="/admin"
/>
</firewall>
</config>
• PHP
// app/config/security.php
$container->loadFromExtension(’security’, array(
’firewalls’ => array(
’main’ => array(’form_login’ => array(
// ...
’default_target_path’ => ’/admin’,
)),
),
));
Ora, se non ci sono URL in sessione, gli utenti saranno mandati su /admin.
Rinviare sempre alla pagina predefinita
Si può fare in modo che gli utenti siano sempre rinviati alla pagina predefinita, senza considerare l’URL richiesta
prima del login, impostando l’opzione always_use_default_target_path a true:
• YAML
# app/config/security.yml
security:
firewalls:
main:
form_login:
# ...
always_use_default_target_path: true
• XML
<!-- app/config/security.xml -->
<config>
<firewall>
<form-login
always_use_default_target_path="true"
/>
</firewall>
</config>
• PHP
// app/config/security.php
$container->loadFromExtension(’security’, array(
’firewalls’ => array(
’main’ => array(’form_login’ => array(
// ...
’always_use_default_target_path’ => true,
380
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
)),
),
));
Usare l’URL del referer
Se nessun URL è stato memorizzato in sessione, si potrebbe voler provare a usare HTTP_REFERER, che spesso
coincide. Lo si può fare impostando use_referer a true (il valore predefinito è false):
• YAML
# app/config/security.yml
security:
firewalls:
main:
form_login:
# ...
use_referer:
true
• XML
<!-- app/config/security.xml -->
<config>
<firewall>
<form-login
use_referer="true"
/>
</firewall>
</config>
• PHP
// app/config/security.php
$container->loadFromExtension(’security’, array(
’firewalls’ => array(
’main’ => array(’form_login’ => array(
// ...
’use_referer’ => true,
)),
),
));
New in version 2.1: Dalla 2.1, se il referer è uguale all’opzione login_path, l’utente sarà rinviato a
default_target_path.
Controllare l’URL di rinvio da dentro un form
Si può anche forzare la pagina di rinvio dell’utente nel form stesso, includendo un campo nascosto dal nome
_target_path. Per esempio, per rinviare all’URL definito in una rotta account, fare come segue:
• Twig
{# src/Acme/SecurityBundle/Resources/views/Security/login.html.twig #}
{% if error %}
<div>{{ error.message }}</div>
{% endif %}
<form action="{{ path(’login_check’) }}" method="post">
<label for="username">Username:</label>
<input type="text" id="username" name="_username" value="{{ last_username }}" />
3.1. Ricettario
381
Symfony2 documentation Documentation, Release 2
<label for="password">Password:</label>
<input type="password" id="password" name="_password" />
<input type="hidden" name="_target_path" value="account" />
<input type="submit" name="login" />
</form>
• PHP
<?php // src/Acme/SecurityBundle/Resources/views/Security/login.html.php ?>
<?php if ($error): ?>
<div><?php echo $error->getMessage() ?></div>
<?php endif; ?>
<form action="<?php echo $view[’router’]->generate(’login_check’) ?>" method="post">
<label for="username">Nome utente:</label>
<input type="text" id="username" name="_username" value="<?php echo $last_username ?>" />
<label for="password">Password:</label>
<input type="password" id="password" name="_password" />
<input type="hidden" name="_target_path" value="account" />
<input type="submit" name="login" />
</form>
L’utente sarà ora rinviato al valore del campo nascosto. Il valore può essere un percorso relativo, un URL
assoluto o un nome di rotta. Si può anche modificare il nome del campo nascosto, cambiando l’opzione
target_path_parameter con il valore desiderato.
• YAML
# app/config/security.yml
security:
firewalls:
main:
form_login:
target_path_parameter: redirect_url
• XML
<!-- app/config/security.xml -->
<config>
<firewall>
<form-login
target_path_parameter="redirect_url"
/>
</firewall>
</config>
• PHP
// app/config/security.php
$container->loadFromExtension(’security’, array(
’firewalls’ => array(
’main’ => array(’form_login’ => array(
’target_path_parameter’ => redirect_url,
)),
),
));
382
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
Rinvio al fallimento del login
Oltre a rinviare l’utente dopo un login eseguito con successo, si può anche impostare l’URL a cui l’utente va
rinviato dopo un login fallito (p.e. perché è stato inserito un nome utente o una password non validi). Per impostazione predefinita, l’utente viene rinviato al medesimo form di login. Si può impostare un URL diverso,
usando la configurazione seguente:
• YAML
# app/config/security.yml
security:
firewalls:
main:
form_login:
# ...
failure_path: /login_failure
• XML
<!-- app/config/security.xml -->
<config>
<firewall>
<form-login
failure_path="login_failure"
/>
</firewall>
</config>
• PHP
// app/config/security.php
$container->loadFromExtension(’security’, array(
’firewalls’ => array(
’main’ => array(’form_login’ => array(
// ...
’failure_path’ => login_failure,
)),
),
));
3.1.49 Proteggere servizi e metodi di un’applicazione
Nel capitolo sulla sicurezza, si può vedere come proteggere un controllore, richiedendo il servizio
security.context dal contenitore di servizi e verificando il ruolo dell’utente attuale:
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
// ...
public function helloAction($name)
{
if (false === $this->get(’security.context’)->isGranted(’ROLE_ADMIN’)) {
throw new AccessDeniedException();
}
// ...
}
Si può anche proteggere qualsiasi servizio in modo simile, iniettando in esso il servizio security.context.
Per un’introduzione generale all’iniezione di dipendenze nei servizi, vedere il capitolo Contenitore di servizi del
libro. Per esempio, si supponga di avere una classe NewsletterManager, che invia email, e di voler restringere
il suo utilizzo ai soli utenti con un ruolo ROLE_NEWSLETTER_ADMIN. Prima di aggiungere la sicurezza, la classe
assomiglia a qualcosa del genere:
3.1. Ricettario
383
Symfony2 documentation Documentation, Release 2
namespace Acme\HelloBundle\Newsletter;
class NewsletterManager
{
public function sendNewsletter()
{
// qui va la logica specifica
}
// ...
}
Lo scopo è verificare il ruolo dell’utente al richiamo del metodo sendNewsletter(). Il primo passo in questa
direzione è l’iniezione del servizio security.context nell’oggetto. Non avendo molto senso non eseguire un
controllo di sicurezza, questo è un candidato ideale per un’iniezione nel costruttore, che garantisce che l’oggetto
della sicurezza sia disponibile in tutta la classe NewsletterManager:
namespace Acme\HelloBundle\Newsletter;
use Symfony\Component\Security\Core\SecurityContextInterface;
class NewsletterManager
{
protected $securityContext;
public function __construct(SecurityContextInterface $securityContext)
{
$this->securityContext = $securityContext;
}
// ...
}
Quindi, nella configurazione dei servizi, si può iniettare il servizio:
• YAML
# src/Acme/HelloBundle/Resources/config/services.yml
parameters:
newsletter_manager.class: Acme\HelloBundle\Newsletter\NewsletterManager
services:
newsletter_manager:
class:
%newsletter_manager.class%
arguments: [@security.context]
• XML
<!-- src/Acme/HelloBundle/Resources/config/services.xml -->
<parameters>
<parameter key="newsletter_manager.class">Acme\HelloBundle\Newsletter\NewsletterManager</
</parameters>
<services>
<service id="newsletter_manager" class="%newsletter_manager.class%">
<argument type="service" id="security.context"/>
</service>
</services>
• PHP
// src/Acme/HelloBundle/Resources/config/services.php
use Symfony\Component\DependencyInjection\Definition;
384
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
use Symfony\Component\DependencyInjection\Reference;
$container->setParameter(’newsletter_manager.class’, ’Acme\HelloBundle\Newsletter\NewsletterM
$container->setDefinition(’newsletter_manager’, new Definition(
’%newsletter_manager.class%’,
array(new Reference(’security.context’))
));
Il servizio iniettato può quindi essere usato per eseguire il controllo di sicurezza, quando il metodo
sendNewsletter() viene richiamato:
namespace Acme\HelloBundle\Newsletter;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Core\SecurityContextInterface;
// ...
class NewsletterManager
{
protected $securityContext;
public function __construct(SecurityContextInterface $securityContext)
{
$this->securityContext = $securityContext;
}
public function sendNewsletter()
{
if (false === $this->securityContext->isGranted(’ROLE_NEWSLETTER_ADMIN’)) {
throw new AccessDeniedException();
}
//-}
// ...
}
Se l’utente attuale non ha il ruolo ROLE_NEWSLETTER_ADMIN, gli sarà richiesto di autenticarsi.
Mettere i sicurezza i metodi con le annotazioni
Si possono anche proteggere i metodi di un servizio tramite annotazioni, usando il bundle JMSSecurityExtraBundle. Questo bundle è incluso nella Standard Edition di Symfony2.
Per abilitare le annotazioni, taggare il servizio da proteggere con il tag security.secure_service (si può
anche abilitare automaticamente la funzionalità per tutti i servizi, vedere i dettagli più avanti):
• YAML
# src/Acme/HelloBundle/Resources/config/services.yml
# ...
services:
newsletter_manager:
# ...
tags:
- { name: security.secure_service }
• XML
3.1. Ricettario
385
Symfony2 documentation Documentation, Release 2
<!-- src/Acme/HelloBundle/Resources/config/services.xml -->
<!-- ... -->
<services>
<service id="newsletter_manager" class="%newsletter_manager.class%">
<!-- ... -->
<tag name="security.secure_service" />
</service>
</services>
• PHP
// src/Acme/HelloBundle/Resources/config/services.php
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
$definition = new Definition(
’%newsletter_manager.class%’,
array(new Reference(’security.context’))
));
$definition->addTag(’security.secure_service’);
$container->setDefinition(’newsletter_manager’, $definition);
Si possono ottenere gli stessi risultati usando le annotazioni:
namespace Acme\HelloBundle\Newsletter;
use JMS\SecurityExtraBundle\Annotation\Secure;
// ...
class NewsletterManager
{
/**
* @Secure(roles="ROLE_NEWSLETTER_ADMIN")
*/
public function sendNewsletter()
{
//-}
// ...
}
Note: Le annotazioni funzionano perché viene creata una classe proxy per la propria classe, che esegue i controlli
di sicurezza. Questo vuol dire che, sebbene si possano usare le annotazioni su metodi pubblici e protetti, non si
possono usare su metodi privati o su metodi finali.
Il bundle JMSSecurityExtraBundle consente anche di proteggere i parametri e i valori resituiti dai metodi.
Per maggiori informazioni vedere la documentazione di JMSSecurityExtraBundle.
386
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
Attivare le annotazioni per tutti i servizi
Quando si proteggono i metodi di un servizio (come mostrato precedentemente), si può taggare ogni
servizio individualmente oppure attivare la funzionalità per tutti i servizi. Per farlo, impostare l’opzione
secure_all_services a true:
• YAML
# app/config/config.yml
jms_security_extra:
# ...
secure_all_services: true
• XML
<!-- app/config/config.xml -->
<srv:container xmlns="http://symfony.com/schema/dic/security"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:srv="http://symfony.com/schema/dic/services"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic
<jms_security_extra secure_controllers="true" secure_all_services="true" />
</srv:container>
• PHP
// app/config/config.php
$container->loadFromExtension(’jms_security_extra’, array(
// ...
’secure_all_services’ => true,
));
Lo svantaggio di questo sistema è che, se attivato, il caricamento della pagina iniziale potrebbe essere molto
lento, a seconda di quanti servizi sono stati definiti.
3.1.50 Come caricare gli utenti dal database (il fornitore di entità)
Il livello della sicurezza è uno degli strumenti migliori di Symfony. Gestisce due aspetti: il processo di autenticazione e quello di autorizzazione. Sebbene possa sembrare difficile capirne il funzionamento interno, il sistema di
sicurezza è molto flessibile e consente di integrare la propria applicazione con qualsiasi backend di autenticazione,
come Active Directory, OAuth o un database.
Introduzione
Questo articolo mostra come autenticare gli utenti con una tabella di database, gestita da una
classe entità di Doctrine.
Il contenuto di questa ricetta è suddiviso in tre parti.
La prima
parte riguarda la progettazione di una classe entità User e il renderla usabile nel livello della sicurezza di Symfony. La seconda parte descrive come autenticare facilmente un utente con l’oggetto
Symfony\Bridge\Doctrine\Security\User\EntityUserProvider distribuito con il framework, oltre che con un po’ di configurazione.
Infine, la guida dimostrerà come creare una classe
Symfony\Bridge\Doctrine\Security\User\EntityUserProvider personalizzata, per recuperare utenti dal database con condizioni particolari.
Questa guida presume che ci sia un bundle Acme\UserBundle già pronto nell’applicazione.
Il modello dei dati
Ai fini di questa ricetta, il bundle AcmeUserBundle contiene una classe entità User, con i seguenti campi: id,
username, salt, password, email e isActive. Il campo isActive indica se l’utente è attivo o meno.
3.1. Ricettario
387
Symfony2 documentation Documentation, Release 2
Per sintetizzare, i metodi setter e getter per ogni campo sono stati rimossi, in modo da focalizzarsi sui metodi
più importanti, provenienti da Symfony\Component\Security\Core\User\UserInterface. New
in version 2.1.
// src/Acme/UserBundle/Entity/User.php
namespace Acme\UserBundle\Entity;
use Symfony\Component\Security\Core\User\UserInterface;
use Doctrine\ORM\Mapping as ORM;
/**
* Acme\UserBundle\Entity\User
*
* @ORM\Table(name="acme_users")
* @ORM\Entity(repositoryClass="Acme\UserBundle\Entity\UserRepository")
*/
class User implements UserInterface
{
/**
* @ORM\Column(name="id", type="integer")
* @ORM\Id()
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @ORM\Column(name="username", type="string", length=25, unique=true)
*/
private $username;
/**
* @ORM\Column(name="salt", type="string", length=40)
*/
private $salt;
/**
* @ORM\Column(name="password", type="string", length=40)
*/
private $password;
/**
* @ORM\Column(name="email", type="string", length=60, unique=true)
*/
private $email;
/**
* @ORM\Column(name="is_active", type="boolean")
*/
private $isActive;
public function __construct()
{
$this->isActive = true;
$this->salt = base_convert(sha1(uniqid(mt_rand(), true)), 16, 36);
}
public function getRoles()
{
return array(’ROLE_USER’);
}
public function eraseCredentials()
388
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
{
}
public function getUsername()
{
return $this->username;
}
public function getSalt()
{
return $this->salt;
}
public function getPassword()
{
return $this->password;
}
}
Per poter usare un’istanza della classe AcmeUserBundle:User nel livello della sicurezza di Symfony, la classe
entità deve implementare Symfony\Component\Security\Core\User\UserInterface. Questa interfaccia costringe la classe a implementare i seguenti cinque metodi: getRoles(), getPassword(),
getSalt(), getUsername(), eraseCredentials(). Per maggiori dettagli su tali metodi, vedere
Symfony\Component\Security\Core\User\UserInterface.
Di seguito è mostrata un’esportazione della tabella User in MySQL. Per dettagli sulla creazione delle righe degli
utenti e sulla codifica delle password, vedere Codificare la password dell’utente.
mysql> select * from user;
+----+----------+------------------------------------------+-------------------------------------| id | username | salt
| password
+----+----------+------------------------------------------+-------------------------------------| 1 | hhamon
| 7308e59b97f6957fb42d66f894793079c366d7c2 | 09610f61637408828a35d7debee5b38a8350e
| 2 | jsmith
| ce617a6cca9126bf4036ca0c02e82deea081e564 | 8390105917f3a3d533815250ed7c64b4594d7
| 3 | maxime
| cd01749bb995dc658fa56ed45458d807b523e4cf | 9764731e5f7fb944de5fd8efad4949b995b72
| 4 | donald
| 6683c2bfd90c0426088402930cadd0f84901f2f4 | 5c3bcec385f59edcc04490d1db95fdb8673bf
+----+----------+------------------------------------------+-------------------------------------4 rows in set (0.00 sec)
Il database ora contiene quattro utenti, con differenti nomi, email e status. Nella prossima parte, vedremo come
autenticare uno di questi utenti, grazie al fornitore di entità di Doctrine e a un paio di righe di configurazione.
Autenticazione con utenti sul database
L’autenticazione di un utente tramite database, usando il livello della sicurezza di Symfony, è un gioco da ragazzi.
Sta tutto nella configurazione SecurityBundle, memorizzata nel file app/config/security.yml.
Di seguito è mostrato un esempio di configurazione, in cui l’utente inserirà il suo nome e la sua password, tramite
autenticazione HTTP. Queste informazioni saranno poi verificate sulla nostra entità User, nel database:
• YAML
# app/config/security.yml
security:
encoders:
Acme\UserBundle\Entity\User:
algorithm: sha1
encode_as_base64: false
iterations: 1
providers:
administrators:
entity: { class: AcmeUserBundle:User, property: username }
3.1. Ricettario
389
Symfony2 documentation Documentation, Release 2
firewalls:
admin_area:
pattern:
^/admin
http_basic: ~
access_control:
- { path: ^/admin, roles: ROLE_ADMIN }
La sezione encoders associa il codificatore sha1 alla classe entità. Ciò vuol dire che Symfony si aspetta che
le password siano codificate nel database, tramite tale algoritmo. Per maggiori dettagli su come creare un nuovo
oggetto utente, vedere la sezione Codificare la password dell’utente del capitolo sulla sicurezza.
La sezione providers definsice un fornitore di utenti administrators. Un fornitore di utenti è una “sorgente” da cui gli utenti vengono caricati durante l’autenticazione. In questo caso, la chiave entity vuol dire
che Symfony userà il fornitore di entità di Doctrine per caricare gli oggetti User dal database, usando il campo
univoco username. In altre parole, dice a Symfony come recuperare gli utenti dal database, prima di verificare
la validità della password.
Questo codice e questa configurazione funzionano, ma non bastano per proteggere l’applicazione per gli utenti
attivi. Finora, possiamo ancora autenticarci con maxime. Nella prossima sezione, vedremo come inibire gli
utenti non attivi.
Inibire gli utenti inattivi
Il
modo
più
facile
per
escludere
gli
utenti
inattivi
è
implementare
l’interfaccia
Symfony\Component\Security\Core\User\AdvancedUserInterface, che si occupa di verificare lo stato degli utenti. L’interfaccia Symfony\Component\Security\Core\User\AdvancedUserInterface
estende Symfony\Component\Security\Core\User\UserInterface, quindi occorre solo modificare l’interfaccia nella classe AcmeUserBundle:User, per poter beneficiare di comportamenti semplici e
avanzati di autenticazione.
L’interfaccia Symfony\Component\Security\Core\User\AdvancedUserInterface aggiunge altri quattro metodi, per validare lo stato degli utenti:
• isAccountNonExpired() verifica se l’utente è scaduto,
• isAccountNonLocked() verifica se l’utente è bloccato,
• isCredentialsNonExpired() verifica se la password dell’utente è scaduta,
• isEnabled() verifica se l’utente è abilitato
Per questo esempio, i primi tre metodi restituiranno true, mentre il metodo isEnabled() restituire il valore
booleano del campo isActive.
// src/Acme/UserBundle/Entity/User.php
namespace Acme\Bundle\UserBundle\Entity;
// ...
use Symfony\Component\Security\Core\User\AdvancedUserInterface;
// ...
class User implements AdvancedUserInterface
{
// ...
public function isAccountNonExpired()
{
return true;
}
public function isAccountNonLocked()
{
390
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
return true;
}
public function isCredentialsNonExpired()
{
return true;
}
public function isEnabled()
{
return $this->isActive;
}
}
Se proviamo ora ad autenticare maxime, l’accesso sarà negato, perché questo utente non è stato abilitato. La
prossima parte analizzerà il modo in cui scrivere fornitori di utenti personalizzati, per autenticare un utente con il
suo nome oppure con la sua email.
Autenticazione con un fornitore entità personalizzato
Il passo successivo consisten nel consentire a un utente di autenticarsi con il suo nome o con il suo indirizzo email,
che sono entrambi unici nel database. Sfortunatamente, il fornitore di entità nativo è in grado di gestire una sola
proprietà per recuperare l’utente dal database.
Per poterlo fare, creare un fornitore di entità personalizzato, che cerchi un utente il cui
nome o la cui email corrisponda al nome utente inserito.
La buona notizia è che un
oggetto repository di Doctrine può agire da fornitore di entità, se implementa l’interfaccia
Symfony\Component\Security\Core\User\UserProviderInterface.
Questa
interfaccia
ha
tre
metodi
da
implementare:
loadUserByUsername($username),
refreshUser(UserInterface $user) e supportsClass($class). Per maggiori dettagli, si
veda Symfony\Component\Security\Core\User\UserProviderInterface.
Il codice successivo mostra l’implementazione di Symfony\Component\Security\Core\User\UserProviderInterf
nella classe UserRepository:
// src/Acme/UserBundle/Entity/UserRepository.php
namespace Acme\UserBundle\Entity;
use
use
use
use
use
use
Symfony\Component\Security\Core\User\UserInterface;
Symfony\Component\Security\Core\User\UserProviderInterface;
Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
Symfony\Component\Security\Core\Exception\UnsupportedUserException;
Doctrine\ORM\EntityRepository;
Doctrine\ORM\NoResultException;
class UserRepository extends EntityRepository implements UserProviderInterface
{
public function loadUserByUsername($username)
{
$q = $this
->createQueryBuilder(’u’)
->where(’u.username = :username OR u.email = :email’)
->setParameter(’username’, $username)
->setParameter(’email’, $username)
->getQuery()
;
try {
// The Query::getSingleResult() method throws an exception
// if there is no record matching the criteria.
$user = $q->getSingleResult();
3.1. Ricettario
391
Symfony2 documentation Documentation, Release 2
} catch (NoResultException $e) {
throw new UsernameNotFoundException(sprintf(’Impossibile trovare un oggetto AcmeUserBu
}
return $user;
}
public function refreshUser(UserInterface $user)
{
$class = get_class($user);
if (!$this->supportsClass($class)) {
throw new UnsupportedUserException(sprintf(’Istanze di "%s" non supportate.’, $class))
}
return $this->loadUserByUsername($user->getUsername());
}
public function supportsClass($class)
{
return $this->getEntityName() === $class || is_subclass_of($class, $this->getEntityName())
}
}
Per concludere l’implementazione, occorre modificare la configurazione del livello della sicurezza, per
dire a Symfony di usare il nuovo fornitore di entità personalizzato, al posto del fornitore di entità generico di Doctrine. Lo si può fare facilmente, rimuovendo il campo property nella sezione
security.providers.administrators.entity del file security.yml.
• YAML
# app/config/security.yml
security:
# ...
providers:
administrators:
entity: { class: AcmeUserBundle:User }
# ...
In questo modo, il livello della sicurezza userà un’istanza di UserRepository e richiamerà il suo metodo
loadUserByUsername() per recuperare un utente dal database, sia che abbia inserito il suo nome utente che
abbia inserito la sua email.
Gestire i ruoli nel database
L’ultima parte della guida spiega come memorizzare e recuperare una lista di ruoli dal database. Come già accennato, quando l’utente viene caricato, il metodo getRoles() restituisce un array di ruoli di sicurezza, che
gli andrebbero assegnati. Si possono caricare tali dati da qualsiasi posto, una lista predefinita usata per ogni
utente (p.e. array(’ROLE_USER’)), un array di Doctrine chiamato roles, oppure tramite una relazione di
Doctrine, come vedremo in questa sezione.
Caution:
In una configurazione tipica, si dovrebbe sempre restituire almeno un ruolo nel
metodo‘‘getRoles()‘‘. Per convenzione, solitamente si restituisce un ruolo chiamato ROLE_USER. Se non
si restituisce alcun ruolo, l’utente potrebbe apparire come non autenticato.
In questo esempio, la classe entità AcmeUserBundle:User definisce una relazione molti-a-molti con la classe
entità AcmeUserBundle:Group. Un utente può essere in relazione con molti gruppi e un gruppo può essere
composto da uno o più utenti. Poiché un gruppo è anche un ruolo, il precedente metodo getRoles() ora
restituisce l’elenco dei gruppi correlati:
// src/Acme/UserBundle/Entity/User.php
392
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
namespace Acme\Bundle\UserBundle\Entity;
use Doctrine\Common\Collections\ArrayCollection;
// ...
class User implements AdvancedUserInterface
{
/**
* @ORM\ManyToMany(targetEntity="Group", inversedBy="users")
*
*/
private $groups;
public function __construct()
{
$this->groups = new ArrayCollection();
}
// ...
public function getRoles()
{
return $this->groups->toArray();
}
}
La classe entità AcmeUserBundle:Group definisce tre campi di tabella (id, name e role). Il campo
univoco role contiene i nomi dei ruoli usati dal livello della sicurezza di Symfony per proteggere parti
dell’applicazione. La cosa più importante da notare è che la classe entità AcmeUserBundle:Group implementa Symfony\Component\Security\Core\Role\RoleInterface, che la obbliga ad avere un
metodo getRole():
namespace Acme\Bundle\UserBundle\Entity;
use Symfony\Component\Security\Core\Role\RoleInterface;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Table(name="acme_groups")
* @ORM\Entity()
*/
class Group implements RoleInterface
{
/**
* @ORM\Column(name="id", type="integer")
* @ORM\Id()
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/** @ORM\Column(name="name", type="string", length=30) */
private $name;
/** @ORM\Column(name="role", type="string", length=20, unique=true) */
private $role;
/** @ORM\ManyToMany(targetEntity="User", mappedBy="groups") */
private $users;
public function __construct()
{
$this->users = new ArrayCollection();
3.1. Ricettario
393
Symfony2 documentation Documentation, Release 2
}
// ... getter e setter per ogni proprietà
/** @see RoleInterface */
public function getRole()
{
return $this->role;
}
}
Per migliorare le prestazioni ed evitare il caricamento pigro dei gruppi al momento del recupero dell’utente
dal fornitore di utenti personalizzato, la soluzione migliore è fare un join dei gruppi correlati nel metodo
UserRepository::loadUserByUsername(). In tal modo, sarà recuperato l’utente e i suoi gruppi/ruoli
associati, con una sola query:
// src/Acme/UserBundle/Entity/UserRepository.php
namespace Acme\Bundle\UserBundle\Entity;
// ...
class UserRepository extends EntityRepository implements UserProviderInterface
{
public function loadUserByUsername($username)
{
$q = $this
->createQueryBuilder(’u’)
->select(’u, g’)
->leftJoin(’u.groups’, ’g’)
->where(’u.username = :username OR u.email = :email’)
->setParameter(’username’, $username)
->setParameter(’email’, $username)
->getQuery()
;
// ...
}
// ...
}
Il metodo QueryBuilder::leftJoin() recupera con un join i gruppi correlati dalla classe del modello
AcmeUserBundle:User, quando un utente viene recuperato con la sua email o con il suo nome.
3.1.51 Come creare un fornitore utenti personalizzato
Parte del processo standard di autenticazione di Symfony2 dipende dai “fornitori utenti”. Quando un utente invia
nome e password, il livello di autenticazione chiede al fornitore utenti configurato di restituire un oggetto utente
per un dato nome utente. Symfony quindi verifica che la password di tale utente sia corretta e genera un token di
sicurezza, in modo che l’utente resti autenticato per la sessione corrente. Symfony dispone di due fornitori utenti
predefiniti, “in_memory” e “entity”. In questa ricetta, vedremo come poter creare il poprio fornitore utenti, che
potrebbe essere utile se gli utenti accedono tramite un database personalizzato, un file, oppure (come mostrato in
questo esempio) tramite un servizio web.
Creare una classe utente
Prima di tutto, indipendentemente dalla provenienza dei dati utente, occorre creare una classe User, che rappresenti tali dati. La classe User, comunque, può essere fatta a piacere e contenere qualsiasi dato si desideri. L’unico
requisito è che implementi Symfony\Component\Security\Core\User\UserInterface. I metodi
394
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
in tale interfaccia vanno quindi deifniti nella classe utente personalizzata: getRoles(), getPassword(),
getSalt(), getUsername(), eraseCredentials(), equals().
Vediamola in azione:
// src/Acme/WebserviceUserBundle/Security/User.php
namespace Acme\WebserviceUserBundle\Security\User;
use Symfony\Component\Security\Core\User\UserInterface;
class WebserviceUser implements UserInterface
{
private $username;
private $password;
private $salt;
private $roles;
public function __construct($username, $password, $salt, array $roles)
{
$this->username = $username;
$this->password = $password;
$this->salt = $salt;
$this->roles = $roles;
}
public function getRoles()
{
return $this->roles;
}
public function getPassword()
{
return $this->password;
}
public function getSalt()
{
return $this->salt;
}
public function getUsername()
{
return $this->username;
}
public function eraseCredentials()
{
}
public function equals(UserInterface $user)
{
if (!$user instanceof WebserviceUser) {
return false;
}
if ($this->password !== $user->getPassword()) {
return false;
}
if ($this->getSalt() !== $user->getSalt()) {
return false;
}
if ($this->username !== $user->getUsername()) {
3.1. Ricettario
395
Symfony2 documentation Documentation, Release 2
return false;
}
return true;
}
}
Se si hanno maggiori informazioni sui propri utenti, come il nome di battesimo, si possono aggiungere campi per
memorizzare tali dati.
Per maggiori dettagli su ciascun metodo, vedere Symfony\Component\Security\Core\User\UserInterface.
Creare un fornitore utenti
Ora che abbiamo una classe User, creeremo un fornitore di utenti, che estrarrà informazioni da un servizio web,
creerà un oggetto WebserviceUser e lo popolerà con i dati.
Il
fornitore
utenti
è
semplicemente
una
classe
PHP
che
deve
implementare
Symfony\Component\Security\Core\User\UserProviderInterface,
la
quale
richiede
la
definizione
di
tre
metodi:
loadUserByUsername($username),
refreshUser(UserInterface $user) and supportsClass($class). Per maggiori dettagli,
vedere Symfony\Component\Security\Core\User\UserProviderInterface.
Ecco un esempio di come potrebbe essere:
// src/Acme/WebserviceUserBundle/Security/User/WebserviceUserProvider.php
namespace Acme\WebserviceUserBundle\Security\User;
use
use
use
use
Symfony\Component\Security\Core\User\UserProviderInterface;
Symfony\Component\Security\Core\User\UserInterface;
Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
Symfony\Component\Security\Core\Exception\UnsupportedUserException;
class WebserviceUserProvider implements UserProviderInterface
{
public function loadUserByUsername($username)
{
// fare qui una chiamata al servizio web
// $userData = ...
// supponiamo che restituisca un array, oppure false se non trova utenti
if ($userData) {
// $password = ’...’;
// ...
return new WebserviceUser($username, $password, $salt, $roles)
} else {
throw new UsernameNotFoundException(sprintf(’Nome utente "%s" non trovato.’, $username
}
}
public function refreshUser(UserInterface $user)
{
if (!$user instanceof WebserviceUser) {
throw new UnsupportedUserException(sprintf(’Istanza di "%s" non supportata.’, get_clas
}
return $this->loadUserByUsername($user->getUsername());
}
public function supportsClass($class)
{
396
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
return $class === ’Acme\WebserviceUserBundle\Security\User\WebserviceUser’;
}
}
Creare un servizio per il fornitore utenti
Ora renderemo il fornitore utenti disponibile come servizio.
• YAML
# src/Acme/MailerBundle/Resources/config/services.yml
parameters:
webservice_user_provider.class: Acme\WebserviceUserBundle\Security\User\WebserviceUserPro
services:
webservice_user_provider:
class: %webservice_user_provider.class%
• XML
<!-- src/Acme/WebserviceUserBundle/Resources/config/services.xml -->
<parameters>
<parameter key="webservice_user_provider.class">Acme\WebserviceUserBundle\Security\User\W
</parameters>
<services>
<service id="webservice_user_provider" class="%webservice_user_provider.class%"></service
</services>
• PHP
// src/Acme/WebserviceUserBundle/Resources/config/services.php
use Symfony\Component\DependencyInjection\Definition;
$container->setParameter(’webservice_user_provider.class’, ’Acme\WebserviceUserBundle\Securit
$container->setDefinition(’webservice_user_provider’, new Definition(’%webservice_user_provid
Tip: La vera implementazione del fornitore utenti avrà probabilmente alcune dipendenze da opzioni di configurazione o altri servizi. Aggiungerli come parametri nella definizione del servizio.
Note: Assicurarsi che il file dei servizi sia importato. Vedere Importare la configurazione con imports per
maggiori dettagli.
Modificare security.yml
È tutto in ‘‘/app/config/security.yml‘‘r. Aggiungere il fornitore di utenti alla lista di fornitori nella sezione “security”. Scegliere un nome per il fornitore di utenti (p.e. “webservice”) e menzionare l’id del servizio appena
definito.
security:
providers:
webservice:
id: webservice_user_provider
Symfony deve anche sapere come codificare le password fornite dagli utenti, per esempio quando
compilano il form di login.
Lo si può fare aggiungendo una riga alla sezione “encoders”, in
/app/config/security.yml.
3.1. Ricettario
397
Symfony2 documentation Documentation, Release 2
security:
encoders:
Acme\WebserviceUserBundle\Security\User\WebserviceUser: sha512
Il valore inserito deve corrispondere al modo in cui le password sono state codificate originariamente, alla
creazione degli uenti (in qualsiasi modo siano stati creati). Quando un utente inserisce la sua password, la password viene concatenata con il valore del sale e quindi codificata con questo algoritmo, prima di confrontarla con la
password restituita dal proprio metodo getPassword(). Inoltre, a seconda delle proprie opzioni, la password
può essere codificata più volte e poi codificata in base64.
Specifiche sulle codifiche delle password
Symfony usa un metodo specifico per concatenare il sale e codificare la password, prima di confrontarla
con la password memorizzata. Se getSalt() non restituisce nulla, la password inserita è semplicemente
codificata con l’algoritmo specificato in security.yml. Se invece il sale è fornito, il seguente valore
viene creato e poi codificato tramite l’algoritmo:
$password.’{’.$salt.’}’;
Se gli utenti esterni hanno password con sali diversi, occorre un po’ di lavoro in più per far sì che Symfony possa codificare correttamente la password. Questo va oltre lo scopo di questa ricetta, possiamo accennare che includerebbe la creazione di una sotto-classe di MessageDigestPasswordEncoder e la
sovrascrittura del metodo mergePasswordAndSalt.
Inoltre, per impostazione predefinita, l’hash è codificato più volte e poi codificato in base64. Per i dettagli,
si veda MessageDigestPasswordEncoder. Se lo si vuole evitare, configurarlo in security.yml:
security:
encoders:
Acme\WebserviceUserBundle\Security\User\WebserviceUser:
algorithm: sha512
encode_as_base64: false
iterations: 1
3.1.52 Come creare un fornitore di autenticazione personalizzato
Chi ha letto il capitolo sulla Sicurezza può capire la distinzione che fa Symfony2 tra autenticazione e autorizzazione, nell’implementazione della sicurezza. Questo capitolo discute le classi di base coinvolte nel processo di
autenticazione e come implementare un fornitore di autenticazione personalizzato. Poiché autenticazione e autorizzazione sono concetti separati, questa estensione sarà agnostica rispetto al fornitore di utenti e funzionerà con
il fornitore di utenti della propria applicazione, sia esso basato sulla memoria, su un database o su qualsiasi altro
supporto scelto.
WSSE
Il seguente capitolo mostra come creare un fornitore di autenticazione personalizzato per l’autenticazione WSSE.
Questo protocollo di sicurezza per WSSE fornisce diversi benefici:
1. Criptazione di nome utente e password
2. Protezione dagli attacchi di replay
3. Nessuna configurazione del server web necessaria
WSSE è molto utile per proteggere i servizi web, siano essi SOAP o REST.
C’è molta buona documentazione su WSSE, ma questo articolo non approfondirà il protocollo di sicurezza, quando
il modo in cui un protocollo personalizzato possa essere aggiunto alla propria applicazione Symfony2. La base di
WSSE è la verifica degli header di richiesta tramite credenziali criptate, con l’uso di un timestamp e di nonce, e la
verifica dell’utente richiesto tramite un digest di password.
398
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
Note: WSSE supporta anche la validazione di chiavi dell’applicazione, che è utile per i servizi web, ma è fuori
dallo scopo di questo capitolo.
Il token
Il ruolo del token nel contesto della sicurezza di Symfony2 è importante. Un token rappresenta i dati di autenticazione dell’utente presenti nella richiesta. Una volta autenticata la richiesta, il token mantiene i dati dell’utente
e fornisce tali data attraverso il contesto della sicurezza. Prima di tutto, creeremo la nostra classe per il token.
Questo consentirà il passaggio di tutte le informazioni rilevanti al nostro fornitore di autenticazione.
// src/Acme/DemoBundle/Security/Authentication/Token/WsseUserToken.php
namespace Acme\DemoBundle\Security\Authentication\Token;
use Symfony\Component\Security\Core\Authentication\Token\AbstractToken;
class WsseUserToken extends AbstractToken
{
public $created;
public $digest;
public $nonce;
public function getCredentials()
{
return ’’;
}
}
Note: La classe WsseUserToken estende la classe Symfony\Component\Security\Core\Authentication\Token
del componente della sicurezza, la quale fornisce funzionalità di base per il token. Si può implementare
Symfony\Component\Security\Core\Authentication\Token\TokenInterface su una
qualsiasi classe da usare come token.
L’ascoltatore
Ora occorre un ascoltatore, che ascolti nel contesto della sicurezza. L’ascoltatore è responsabile delle
richieste al firewall e di richiamare il fornitore di autenticazione. Un ascoltatore deve essere un’istanza
di Symfony\Component\Security\Http\Firewall\ListenerInterface. Un ascoltatore di sicurezza dovrebbe gestire l’evento Symfony\Component\HttpKernel\Event\GetResponseEvent e
impostare un token di autenticazione nel contesto della sicurezza, in caso positivo.
// src/Acme/DemoBundle/Security/Firewall/WsseListener.php
namespace Acme\DemoBundle\Security\Firewall;
use
use
use
use
use
use
use
use
Symfony\Component\HttpFoundation\Response;
Symfony\Component\HttpKernel\Event\GetResponseEvent;
Symfony\Component\Security\Http\Firewall\ListenerInterface;
Symfony\Component\Security\Core\Exception\AuthenticationException;
Symfony\Component\Security\Core\SecurityContextInterface;
Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface;
Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
Acme\DemoBundle\Security\Authentication\Token\WsseUserToken;
class WsseListener implements ListenerInterface
{
protected $securityContext;
protected $authenticationManager;
public function __construct(SecurityContextInterface $securityContext, AuthenticationManagerIn
3.1. Ricettario
399
Symfony2 documentation Documentation, Release 2
{
$this->securityContext = $securityContext;
$this->authenticationManager = $authenticationManager;
}
public function handle(GetResponseEvent $event)
{
$request = $event->getRequest();
if ($request->headers->has(’x-wsse’)) {
$wsseRegex = ’/UsernameToken Username="([^"]+)", PasswordDigest="([^"]+)", Nonce="([^"
if (preg_match($wsseRegex, $request->headers->get(’x-wsse’), $matches)) {
$token = new WsseUserToken();
$token->setUser($matches[1]);
$token->digest
$token->nonce
$token->created
= $matches[2];
= $matches[3];
= $matches[4];
try {
$returnValue = $this->authenticationManager->authenticate($token);
if ($returnValue instanceof TokenInterface) {
return $this->securityContext->setToken($returnValue);
} else if ($returnValue instanceof Response) {
return $event->setResponse($returnValue);
}
} catch (AuthenticationException $e) {
// si potrebbe loggare qualcosa in questo punto
}
}
}
$response = new Response();
$response->setStatusCode(403);
$event->setResponse($response);
}
}
Questo ascoltatore verifica che la richiesta contenga l’header X-WSSE, confronta il valore restituito con
l’informazione WSSE attesa, crea un token usando tale informazione e passa il token al gestore di autenticazione. Se non viene fornita un’informazione adeguata oppure se il gestore di autenticazione lancia una Symfony\Component\Security\Core\Exception\AuthenticationException, viene
restituita una risposta 403.
Note: Una classe non usata precedentemente, la classe Symfony\Component\Security\Http\Firewall\AbstractAu
è una classe base molto utile, che fornisce le funzionalità solitamente necessarie per le estensioni della sicurezza.
Ciò include il mantenimento del token in sessione, fornire gestori di successo/fallimento, login da URL, eccetera.
Poiché WSSE non richiede di mantenere sessioni di autenticazione né form di login, non sarà usata per questo
esempio.
Il fornitore di autenticazione
Il fornitore di autenticazione verificherà il token WsseUserToken. Questo vuol dire che il fornitore verificherà
che il valore dell’header Created sia valido entro cinque minuti, che il valore dell’header Nonce sia unico nei
cinque minuti e che il valore dell’header PasswordDigest corrisponda alla password dell’utente.
400
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
// src/Acme/DemoBundle/Security/Authentication/Provider/WsseProvider.php
namespace Acme\DemoBundle\Security\Authentication\Provider;
use
use
use
use
use
use
Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface;
Symfony\Component\Security\Core\User\UserProviderInterface;
Symfony\Component\Security\Core\Exception\AuthenticationException;
Symfony\Component\Security\Core\Exception\NonceExpiredException;
Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
Acme\DemoBundle\Security\Authentication\Token\WsseUserToken;
class WsseProvider implements AuthenticationProviderInterface
{
private $userProvider;
private $cacheDir;
public function __construct(UserProviderInterface $userProvider, $cacheDir)
{
$this->userProvider = $userProvider;
$this->cacheDir
= $cacheDir;
}
public function authenticate(TokenInterface $token)
{
$user = $this->userProvider->loadUserByUsername($token->getUsername());
if ($user && $this->validateDigest($token->digest, $token->nonce, $token->created, $user->
$authenticatedToken = new WsseUserToken($user->getRoles());
$authenticatedToken->setUser($user);
return $authenticatedToken;
}
throw new AuthenticationException(’The WSSE authentication failed.’);
}
protected function validateDigest($digest, $nonce, $created, $secret)
{
// Scade dopo 5 minuti
if (time() - strtotime($created) > 300) {
return false;
}
// Valida che nonce sia unico nei 5 minuti
if (file_exists($this->cacheDir.’/’.$nonce) && file_get_contents($this->cacheDir.’/’.$nonc
throw new NonceExpiredException(’Previously used nonce detected’);
}
file_put_contents($this->cacheDir.’/’.$nonce, time());
// Valida la parola segreta
$expected = base64_encode(sha1(base64_decode($nonce).$created.$secret, true));
return $digest === $expected;
}
public function supports(TokenInterface $token)
{
return $token instanceof WsseUserToken;
}
}
Note: L’interfaccia Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProv
richiede un metodo authenticate sul token dell’utente e un metodo supports, che dice al gestore di aut-
3.1. Ricettario
401
Symfony2 documentation Documentation, Release 2
enticazione se usare o meno questo fornitore per il token dato. In caso di più fornitori, il gestore di autenticazione
passerà al fornitore successivo della lista.
Il factory
Abbiamo creato un token personalizzato, un ascoltatore personalizzato e un fornitore personalizzato.
Ora dobbiamo legarli insieme.
Come rendere disponibile il fornitore alla configurazione della sicurezza?
La risposta è: usando un factory.
Un factory è quando ci si aggancia al componente della sicurezza, dicendogli il nome del proprio provider e qualsiasi opzione
di configurazione disponibile per esso.
Prima di tutto, occorre creare una classe che implementi
Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SecurityFactoryInterf
// src/Acme/DemoBundle/DependencyInjection/Security/Factory/WsseFactory.php
namespace Acme\DemoBundle\DependencyInjection\Security\Factory;
Symfony\Component\DependencyInjection\ContainerBuilder;
Symfony\Component\DependencyInjection\Reference;
Symfony\Component\DependencyInjection\DefinitionDecorator;
Symfony\Component\Config\Definition\Builder\NodeDefinition;
Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SecurityFactoryInterface;
use
use
use
use
use
class WsseFactory implements SecurityFactoryInterface
{
public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntry
{
$providerId = ’security.authentication.provider.wsse.’.$id;
$container
->setDefinition($providerId, new DefinitionDecorator(’wsse.security.authentication.pro
->replaceArgument(0, new Reference($userProvider))
;
$listenerId = ’security.authentication.listener.wsse.’.$id;
$listener = $container->setDefinition($listenerId, new DefinitionDecorator(’wsse.security.
return array($providerId, $listenerId, $defaultEntryPoint);
}
public function getPosition()
{
return ’pre_auth’;
}
public function getKey()
{
return ’wsse’;
}
public function addConfiguration(NodeDefinition $node)
{}
}
L’interfaccia Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SecurityFact
richiede i seguenti metodi:
• metodo create, che aggiunge l’ascoltatore e il fornitore di autenticazione provider al contenitore di dipendenze per il contesto della sicurezza appropriato;
• metodo getPosition, che deve essere del tipo pre_auth, form, http o remember_me e definisce
la posizione in cui il fornitore viene chiamato;
• metodo getKey, che definisce la chiave di configurazione usata per fare riferimento al fornitore;
402
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
• metodo addConfiguration, usato per definire le opzioni di configurazione sotto la chiave
configuration della configurazione della sciurezza. Le opzioni di configurazione sono spiegate più
avanti in questo capitolo.
Note: Una classe non usata in questo esempio, Symfony\Bundle\SecurityBundle\DependencyInjection\Securit
è una classe base molto utile, che fornisce funzionalità solitamente necessaria per i factory della sicurezza. Può
tornare utile quando si definisce un fornitore di autenticazione di tipo diverso.
Una volta creata la classe factory, la chiave wsse può essere usata con firewall nella configurazione della sicurezza.
Note: Ci si potrebbe chiedere il motivo per cui sia necessaria una speciale classe factory per aggiungere ascoltatori
e fornitori al contenitore di dipendenze. È una buona domanda. La ragione è che si può usare il proprio firewall più
volte, per proteggere diverse parti della propria applicazione. Per questo, ogni volta che si usa il proprio firewall,
il contenitore di dipendenze crea un nuovo servizio. Il factory serve a creare questi nuovi servizi.
Configurazione
È tempo di vedere in azione il nostro fornitore di autenticazione. Servono ancora alcune cose per farlo funzionare.
La prima cosa è aggiungere i servizi di cui sopra al contenitore di servizi. La classe factory vista prima fa
riferimento a degli id di servizi che non esistono ancora: wsse.security.authentication.provider
e wsse.security.authentication.listener. Ora definiremo questi servizi.
• YAML
# src/Acme/DemoBundle/Resources/config/services.yml
services:
wsse.security.authentication.provider:
class: Acme\DemoBundle\Security\Authentication\Provider\WsseProvider
arguments: [’’, %kernel.cache_dir%/security/nonces]
wsse.security.authentication.listener:
class: Acme\DemoBundle\Security\Firewall\WsseListener
arguments: [@security.context, @security.authentication.manager]
• XML
<!-- src/Acme/DemoBundle/Resources/config/services.xml -->
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/
<services>
<service id="wsse.security.authentication.provider"
class="Acme\DemoBundle\Security\Authentication\Provider\WsseProvider" public="false"
<argument /> <!-- User Provider -->
<argument>%kernel.cache_dir%/security/nonces</argument>
</service>
<service id="wsse.security.authentication.listener"
class="Acme\DemoBundle\Security\Firewall\WsseListener" public="false">
<argument type="service" id="security.context"/>
<argument type="service" id="security.authentication.manager" />
</service>
</services>
</container>
• PHP
3.1. Ricettario
403
Symfony2 documentation Documentation, Release 2
// src/Acme/DemoBundle/Resources/config/services.php
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
$container->setDefinition(’wsse.security.authentication.provider’,
new Definition(
’Acme\DemoBundle\Security\Authentication\Provider\WsseProvider’,
array(’’, ’%kernel.cache_dir%/security/nonces’)
));
$container->setDefinition(’wsse.security.authentication.listener’,
new Definition(
’Acme\DemoBundle\Security\Firewall\WsseListener’, array(
new Reference(’security.context’),
new Reference(’security.authentication.manager’))
));
Ora che i servizi sono stati definiti, diciamo al contesto della sicurezza del factory. I factory devono essere inclusi
in un singolo file di configurazione, mentre stiamo scrivendo. Quindi, iniziamo creando il file con il servizio
factory, con tag security.listener.factory:
• YAML
# src/Acme/DemoBundle/Resources/config/security_factories.yml
services:
security.authentication.factory.wsse:
class: Acme\DemoBundle\DependencyInjection\Security\Factory\WsseFactory
tags:
- { name: security.listener.factory }
• XML
<!-- src/Acme/DemoBundle/Resources/config/security_factories.xml -->
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/
<services>
<service id="security.authentication.factory.wsse"
class="Acme\DemoBundle\DependencyInjection\Security\Factory\WsseFactory" public="fa
<tag name="security.listener.factory" />
</service>
</services>
</container>
New in version 2.1: Prima della 2.1, il factory successivo veniva aggiunto tramite security.yml. Come ultimo
passo, aggiungere il factory all’estensione della sicurezza nella classe del bundle.
// src/Acme/DemoBundle/AcmeDemoBundle.php
namespace Acme\DemoBundle;
use Acme\DemoBundle\DependencyInjection\Security\Factory\WsseFactory;
use Symfony\Component\HttpKernel\Bundle\Bundle;
use Symfony\Component\DependencyInjection\ContainerBuilder;
class AcmeDemoBundle extends Bundle
{
public function build(ContainerBuilder $container)
{
parent::build($container);
$extension = $container->getExtension(’security’);
$extension->addSecurityListenerFactory(new WsseFactory());
}
404
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
}
Abbiamo finito! Ora si possono definire le parti dell’applicazione sotto protezione WSSE.
security:
firewalls:
wsse_secured:
pattern:
wsse:
/api/.*
true
Con questo abbiamo concluso la scrittura di un fornitore di autenticazione personalizzato.
Un piccolo extra
E se si volesse rendere il fornitore di autenticazione WSSE un po’ più eccitante? Le possibilità sono infinite.
Possiamo iniziare a renderlo ancora più brillante.
Configurazione
Si possono aggiungere opzioni personalizzate sotto la voce wsse nella configurazione della sicurezza. Per esempio, il tempo consentito predefinito prima della scadenza dell’header di creazione è di 5 minuti. Lo si può rendere
configurabile, in modo che firewall diversi possano avere lunghezze di scadenza diverse.
Occorre innanzitutto modificare WsseFactory e definire la nuova opzione nel metodo addConfiguration.
class WsseFactory implements SecurityFactoryInterface
{
# ...
public function addConfiguration(NodeDefinition $node)
{
$node
->children()
->scalarNode(’lifetime’)->defaultValue(300)
->end()
;
}
}
Ora, nel metodo create del factory, il parametro $config conterrà una chiave ‘lifetime’, impostata a 5 minuti
(300 secondi), a meno che non sia specificato diversamente nella configurazione. Per usarlo, occorre passarlo
come parametro al proprio fornitore di autenticazione.
class WsseFactory implements SecurityFactoryInterface
{
public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntry
{
$providerId = ’security.authentication.provider.wsse.’.$id;
$container
->setDefinition($providerId,
new DefinitionDecorator(’wsse.security.authentication.provider’))
->replaceArgument(0, new Reference($userProvider))
->replaceArgument(2, $config[’lifetime’])
;
// ...
}
// ...
}
Note:
Occorre aggiungere anche un terzo parametro alla configurazione del servizio
wsse.security.authentication.provider, che potrebbe essere vuoto, oppure contenente il
3.1. Ricettario
405
Symfony2 documentation Documentation, Release 2
tempo di scadenza nel factory. La classe WsseProvider dovrà anche accettare un terzo parametro nel
costruttore, il tempo, che dovrebbe usare al posto dei 300 secondi precedentemente fissati. Questi due passi non
sono mostrati.
Il tempo di scadenza di ogni richiesta WSSE è ora configurabile e può essere impostato con qualsiasi valore
desiderato per ogni firewall.
security:
firewalls:
wsse_secured:
pattern:
wsse:
/api/.*
{ lifetime: 30 }
Qualsiasi altra configurazione rilevante può essere definita nel factory e utilizzata o passata a altre classi nel
contenitore.
3.1.53 Come cambiare il comportamento predefinito del puntamento del percorso
Per impostazione predefinita, il componente della sicurezza mantiene le informazioni sull’URI dell’ultima richiesta in una variabile di sessione, chiamata _security.target_path. Dopo che un accesso viene eseguito,
l’utente viene rinviato a questo percorso, come aiuto per continuare dall’ultima pagina visitata.
In alcune occasioni, questo comportamento è inatteso. Per esempio, quando l’URI dell’ultima richiesta era un
POST HTTP su una rotta configurata per consentire solo il metodo POST, l’utente rinviato a tale rotta otterrebbe
solo un errore 404.
Per aggirare questo comportamento, occorre semplicemente estendere la classe ExceptionListener e
sovrascrivere il metodo chiamato setTargetPath().
Come prima cosa, sovrascrivere il parametro security.exception_listener.class nel proprio file
di configurazione. Lo si può fare dalla propria configurazione principale (in app/config) oppure in un file di
configurazione importato da un bundle:
• YAML
# src/Acme/HelloBundle/Resources/config/services.yml
parameters:
# ...
security.exception_listener.class: Acme\HelloBundle\Security\Firewall\ExceptionListener
• XML
<!-- src/Acme/HelloBundle/Resources/config/services.xml -->
<parameters>
<!-- ... -->
<parameter key="security.exception_listener.class">Acme\HelloBundle\Security\Firewall\Exc
</parameters>
• PHP
// src/Acme/HelloBundle/Resources/config/services.php
// ...
$container->setParameter(’security.exception_listener.class’, ’Acme\HelloBundle\Security\Fire
Quindi, crare il proprio ExceptionListener:
// src/Acme/HelloBundle/Security/Firewall/ExceptionListener.php
namespace Acme\HelloBundle\Security\Firewall;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Http\Firewall\ExceptionListener as BaseExceptionListener;
406
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
class ExceptionListener extends BaseExceptionListener
{
protected function setTargetPath(Request $request)
{
// Non salvare il percorso del puntamento per richieste XHR o diverse da GET
// Si può aggiungere altra logica, all’occorrenza
if ($request->isXmlHttpRequest() || ’GET’ !== $request->getMethod()) {
return;
}
$request->getSession()->set(’_security.target_path’, $request->getUri());
}
}
Si può aggiungere tutta la logica necessaria ai propri scopi!
3.1.54 Come usare Varnish per accelerare il proprio sito
Poiché la cache di Symfony2 usa gli header standard della cache HTTP, la Il reverse proxy di Symfony2 può essere
facilmente sostituita da qualsiasi altro reverse proxy. Varnish è un acceleratore HTTP potente e open source, che
è in grado di servire contenuti in cache in modo veloce e include il supporto per Edge Side Include.
Configurazione
Come visto in precedenza, Symfony2 è abbastanza intelligente da capire se sta parlando a un reverse proxy che
capisca ESI o meno. Funziona immediatamente, se si usa il reverse proxy di Symfony2, mentre occorre una
configurazione speciale per poter funzionare con Varnish. Fortunatamente, Symfony2 si appoggia a uno standard
scritto da Akamaï (Architettura Edge), quindi i suggerimenti di configurazione in questo capitolo possono essere
utili anche non usando Symfony2.
Note: Varnish supporta solo l’attributo src per i tag ESI (onerror e alt vengono ignorati).
Prima di tutto, configurare Varnish in modo che pubblicizzi il suo supporto a ESI, aggiungendo un header
Surrogate-Capability alle richieste girate all’applicazione sottostante:
sub vcl_recv {
set req.http.Surrogate-Capability = "abc=ESI/1.0";
}
Quindi, ottimizzare Varnish in modo che analizzi i contenuti della risposta solo quando ci sia almeno un tag ESI,
verificando l’header Surrogate-Control, che Symfony2 aggiunge automaticamente:
sub vcl_fetch {
if (beresp.http.Surrogate-Control ~ "ESI/1.0") {
unset beresp.http.Surrogate-Control;
// per Varnish >= 3.0
set beresp.do_esi = true;
// per Varnish < 3.0
// esi;
}
}
Caution: La compressione con ESI non era supportata in Varnish fino alle versione 3.0 (leggere GZIP e
Varnish). Se non si usa Varnish 3.0, inserire un server web davanti a Varnish per eseguire la compressione.
3.1. Ricettario
407
Symfony2 documentation Documentation, Release 2
Invalidare la cache
Non si dovrebbe aver mai bisogno di invalidare dati in cache, perché l’invalidazione è già gestita nativamente nei
modelli di cache HTTP (vedere Invalidazione della cache).
Tuttavia, Varnish può essere configurato per accettare un metodo HTTP speciale PURGE, che invalida la cache per
una data risorsa:
sub vcl_hit {
if (req.request == "PURGE") {
set obj.ttl = 0s;
error 200 "Purged";
}
}
sub vcl_miss {
if (req.request == "PURGE") {
error 404 "Not purged";
}
}
Caution: Bisogna proteggere il metodo HTTP PURGE in qualche modo, per evitare che qualcuno pulisca i
dati in cache in modo casuale.
3.1.55 Iniettare variabili in tutti i template (variabili globali)
A volte si vuole che una variabile sia accessibile in tutti i template usati. Lo si può fare, modificando il file
app/config/config.yml:
# app/config/config.yml
twig:
# ...
globals:
ga_tracking: UA-xxxxx-x
Ora, la variabile ga_tracking è disponibile in tutti i template Twig
<p>Il codice di tracciamento Google è: {{ ga_tracking }} </p>
È molto facile! Si può anche usare il sistema I parametri del servizio, che consente di isolare o riutilizzare il
valore:
; app/config/parameters.yml
[parameters]
ga_tracking: UA-xxxxx-x
# app/config/config.yml
twig:
globals:
ga_tracking: %ga_tracking%
La stessa variabile è disponibile esattamente come prima.
Variabili globali più complesse
Se la variabile globale che si vuole impostare è più complicata, per esempio un oggetto, non si potrà usare il
metodo precedente. Invece, occorrerà creare un’estensione Twig e restituire la variabile globale come una delle
voci del metodo getGlobals.
408
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
3.1.56 Come usare PHP al posto di Twig nei template
Anche se Twig è il motore predefinito di template in Symfony2, si può ancora usare PHP, se lo si preferisce. Entrambi i motori di template sono supportati in ugual modo in Symfony2. Symfony2 aggiunge alcune caratteristiche
interessanti sopra PHP, per rendere la scrittura dei template più potente.
Rendere i template PHP
Se si vuole usare il motore di template PHP, occorre prima di tutto assicurarsi di abilitarlo nel file di configurazione
della propria applicazione:
• YAML
# app/config/config.yml
framework:
# ...
templating:
{ engines: [’twig’, ’php’] }
• XML
<!-- app/config/config.xml -->
<framework:config ... >
<!-- ... -->
<framework:templating ... >
<framework:engine id="twig" />
<framework:engine id="php" />
</framework:templating>
</framework:config>
• PHP
$container->loadFromExtension(’framework’, array(
// ...
’templating’
=> array(
’engines’ => array(’twig’, ’php’),
),
));
Ora si può rendere un template PHP invece di uno Twig, semplicemente usando nel nome del template l’estensione
.php al posto di .twig. Il controllore sottostante rende il template index.html.php:
// src/Acme/HelloBundle/Controller/HelloController.php
public function indexAction($name)
{
return $this->render(’HelloBundle:Hello:index.html.php’, array(’name’ => $name));
}
Decorare i template
Spesso i template in un progetto condividono elementi comuni, come la testata e il pie’ di pagina. In Symfony2,
ci piace pensare a questo problema in modo diverso: un template può essere decorato da un altro template.
Il template index.html.php è decorato layout.html.php, grazie alla chiamata a extend():
<!-- src/Acme/HelloBundle/Resources/views/Hello/index.html.php -->
<?php $view->extend(’AcmeHelloBundle::layout.html.php’) ?>
Ciao <?php echo $name ?>!
3.1. Ricettario
409
Symfony2 documentation Documentation, Release 2
La notazione HelloBundle::layout.html.php suona familiare, non è vero? È la stessa notazione usata
per fare riferimento a un template. La parte :: vuol dire semplicemente che l’elemento controllore è vuoto,
quindi il file corrispondente è memorizzato direttamente sotto views/.
Diamo ora un’occhiata al file layout.html.php:
<!-- src/Acme/HelloBundle/Resources/views/layout.html.php -->
<?php $view->extend(’::base.html.php’) ?>
<h1>Applicazione Ciao</h1>
<?php $view[’slots’]->output(’_content’) ?>
Il layout stesso è decorato da un altro template (::base.html.php). Symfony2 supporta livelli molteplici di
decorazione: un layout può esso stesso essere decorato da un altro layout. Quando la parte bundle del nome del
template è vuota, le viste sono cercate nella cartella app/Resources/views/. Questa cartella contiene le
viste globali del proprio progetto:
<!-- app/Resources/views/base.html.php -->
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title><?php $view[’slots’]->output(’title’, ’Hello Application’) ?></title>
</head>
<body>
<?php $view[’slots’]->output(’_content’) ?>
</body>
</html>
Per entrambi i layout, l’espressione $view[’slots’]->output(’_content’) viene sostituita dal contenuto del template figlio, rispettivamente index.html.php e layout.html.php (approfondiremo gli slot
nella prossima sezione).
Come si può vedere, Symfony2 fornisce metodi su un misterioso oggetto $view. In un template, la variabile
$view è sempre disponibile e fa riferimento a uno speciale oggetto che fornisce un sacco di metodi, che mantengono snello il motore dei template.
Lavorare con gli slot
Uno slot è un pezzetto di codice, definito in un template e riutilizzabile in qualsiasi layout che decora il template.
Nel template index.html.php, definiamo uno slot title:
<!-- src/Acme/HelloBundle/Resources/views/Hello/index.html.php -->
<?php $view->extend(’AcmeHelloBundle::layout.html.php’) ?>
<?php $view[’slots’]->set(’title’, ’Applicazione Ciao mondo’) ?>
Ciao <?php echo $name ?>!
Il layout base ha già il codice per mostrare il titolo nella testata:
<!-- app/Resources/views/layout.html.php -->
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title><?php $view[’slots’]->output(’title’, ’Applicazione Ciao’) ?></title>
</head>
Il metodo output() inserisce il contenuto di uno slot e accetta un valore predefinito opzionale, se lo slot non è
definito. E _content è solo uno slot speciale che contiene la resa del template figlio.
Per slot più grandi, si può usare una sintassi estesa:
410
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
<?php $view[’slots’]->start(’title’) ?>
Un sacco di HTML
<?php $view[’slots’]->stop() ?>
Includere altri template
Il modo migliore di condividere un pezzo di codice di template è quello di definire un template che possa essere
incluso in altri template.
Creare un template hello.html.php:
<!-- src/Acme/HelloBundle/Resources/views/Hello/hello.html.php -->
Ciao <?php echo $name ?>!
E cambiare il template index.html.php per includerlo:
<!-- src/Acme/HelloBundle/Resources/views/Hello/index.html.php -->
<?php $view->extend(’AcmeHelloBundle::layout.html.php’) ?>
<?php echo $view->render(’AcmeHello:Hello:hello.html.php’, array(’name’ => $name)) ?>
Il metodo render() valuta e restituisce il contenuto di un altro template (questo è esattamente lo stesso metodo
usato nel controllore).
Inserire altri controllori
Cosa fare se si vuole inserire il risultato di un altro controllore in un template? Può essere molto utile lavorando
con Ajax, oppure quando il template inserito ha bisogno di variabili non disponibili nel template principale.
Se si crea un’azione fancy e la si vuole includere nel template index.html.php, basta usare il seguente
codice:
<!-- src/Acme/HelloBundle/Resources/views/Hello/index.html.php -->
<?php echo $view[’actions’]->render(’HelloBundle:Hello:fancy’, array(’name’ => $name, ’color’ => ’
Qui la stringa HelloBundle:Hello:fancy si riferisce all’azione fancy del controllore Hello:
// src/Acme/HelloBundle/Controller/HelloController.php
class HelloController extends Controller
{
public function fancyAction($name, $color)
{
// crear un oggettom basato sulla variabile $color
$object = ...;
return $this->render(’HelloBundle:Hello:fancy.html.php’, array(’name’ => $name, ’object’ =
}
// ...
}
Ma dove è definito $view[’actions’]? Come anche $view[’slots’], è chiamato helper di template e
sarà approfondito nella prossima sezione.
Usare gli helper di template
Il sistema di template di Symfony2 può essere facilmente esteso tramite gli helper. Gli helper sono oggetti PHP
che forniscono caratteristiche utili nel contesto di un template. actions e slots sono due degli helper già
disponibili in Symfony2.
3.1. Ricettario
411
Symfony2 documentation Documentation, Release 2
Creare collegamenti tra le pagine
Parlando di applicazioni web, non può mancare la creazione di collegamenti. Invece di inserire a mano gli URL
nei template, l’helper router sa come generare gli URL, in base alla configurazione delle rotte. In questo modo,
tutti gli URL possono essere facilmente cambiati, cambiando la configurazione:
<a href="<?php echo $view[’router’]->generate(’ciao’, array(’name’ => ’Thomas’)) ?>">
Saluti Thomas!
</a>
Il metodo generate() accetta come parametri il nome della rotta e un array di parametri. Il nome della rotta
è la chiave principale sotto cui le rotte sono referenziate e i parametri sono i valori dei segnaposto definiti nello
schema della rotta:
# src/Acme/HelloBundle/Resources/config/routing.yml
ciao: # Nome della rotta
pattern: /hello/{name}
defaults: { _controller: AcmeHelloBundle:Hello:index }
Usare le risorse: immagini, JavaScript e fogli di stile
Cosa sarebbe Internet senza immagini, JavaScript e fogli di stile? Symfony2 fornisce il tag assets per gestirli
facilmente:
<link href="<?php echo $view[’assets’]->getUrl(’css/blog.css’) ?>" rel="stylesheet" type="text/css
<img src="<?php echo $view[’assets’]->getUrl(’images/logo.png’) ?>" />
Lo scopo principale dell’helper assets è quello di rendere l’applicazione più portabile. Grazie a questo helper,
si può spostare la cartella radice dell’applicazione in qualsiasi punto sotto la propria cartella radice del web, senza
dover cambiare nulla nel codice dei template.
Escape dell’output
Quando si usano i template PHP, occorre fare escape delle variabili mostrate all’utente:
<?php echo $view->escape($var) ?>
Per impostazione predefinita, il metodo escape() assume che la variabili sia inviata in output in un contesto
HTML. Il secondo parametro consente di cambiare il contesto. Per esempio, per mandare in output qualcosa in
uno script JavaScript, usare il contesto js:
<?php echo $view->escape($var, ’js’) ?>
3.1.57 Come usare Monologo per scrivere log
Monolog è una libreria di log per PHP 5.3 usata da Symfony2. È ispirata dalla libreria LogBook di Python.
Utilizzo
In Monolog, ogni logger definisce un canale di log. Ogni canale ha una pila di gestori per scrivere i log (i gestori
possono essere condivisi).
Tip: Quando si inietta il logger in un servizio, si può usar un canale personalizzato per vedere facilmente quale
parte dell’applicazione ha loggato il messaggio.
412
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
Il gestore di base è StreamHandler, che scrive log in un flusso (per impostazione definita, in
app/logs/prod.log in ambiente di produzione e in app/logs/dev.log in quello di sviluppo).
Monolog dispone anche di un potente gestore per il log in ambiente di produzione:
FingersCrossedHandler. Esso consente di memorizzare i messaggi in un buffer e di loggarli solo
se un messaggio raggiunge il livello di azione (ERROR, nella configurazione fornita con la standard edition)
girando i messaggi a un altro gestore.
Per loggare un messaggio, basta prendere il servizio logger dal contenitore, nel proprio controllore:
$logger = $this->get(’logger’);
$logger->info(’Abbiamo preso il logger’);
$logger->err(’C’è stato un errore’);
Tip: Usare solo i metodi dell’interfaccia Symfony\Component\HttpKernel\Log\LoggerInterface
consente di cambiare l’implementazione del logger senza cambiare il proprio codice.
Usare diversi gestori
Il logger usa una pila di gestori, che sono richiamati in successione. Ciò consente di loggare facilmente i messaggi
in molti modi.
• YAML
monolog:
handlers:
syslog:
type: stream
path: /var/log/symfony.log
level: error
main:
type: fingers_crossed
action_level: warning
handler: file
file:
type: stream
level: debug
• XML
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:monolog="http://symfony.com/schema/dic/monolog"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/
http://symfony.com/schema/dic/monolog http://symfony.com/schema/dic/m
<monolog:config>
<monolog:handler
name="syslog"
type="stream"
path="/var/log/symfony.log"
level="error"
/>
<monolog:handler
name="main"
type="fingers_crossed"
action-level="warning"
handler="file"
/>
<monolog:handler
name="file"
3.1. Ricettario
413
Symfony2 documentation Documentation, Release 2
type="stream"
level="debug"
/>
</monolog:config>
</container>
La configurazione appena vista definisce una pila di gestori, che saranno richiamati nell’ordine in cui sono stati
definiti.
Tip: Il gestore chiamato “file” non sarà incluso nella pila, perché è usato come gestore annidato del gestore
fingers_crossed.
Note: Se si vuole cambiare la configurazione di MonologBundle con un altro file di configurazione, occorre
ridefinire l’intera pila. Non si possono fondere, perché l’ordine conta e una fusione non consente di controllare
l’ordine.
Cambiare il formattatore
Il gestore usa un Formatter per formattare un record, prima di loggarlo. Tutti i gestori di Monolog usano,
per impostazione predefinita, un’istanza di Monolog\Formatter\LineFormatter, ma la si può sostituire
facilmente. Il proprio formattatore deve implementare Monolog\Formatter\FormatterInterface.
• YAML
services:
my_formatter:
class: Monolog\Formatter\JsonFormatter
monolog:
handlers:
file:
type: stream
level: debug
formatter: my_formatter
• XML
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:monolog="http://symfony.com/schema/dic/monolog"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/
http://symfony.com/schema/dic/monolog http://symfony.com/schema/dic/m
<services>
<service id="my_formatter" class="Monolog\Formatter\JsonFormatter" />
</services>
<monolog:config>
<monolog:handler
name="file"
type="stream"
level="debug"
formatter="my_formatter"
/>
</monolog:config>
</container>
414
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
Aggiungere dati extra nei messaggi di log
Monolog consente di processare il record prima di loggarlo, per aggiungere alcuni dati extra. Un processore può
essere applicato all’intera pila dei gestori oppure solo a un gestore specifico.
Un processore è semplicemente una funzione che riceve il record come primo parametro.
I processori sono configurati con il tag monolog.processor del DIC. Vedere il riferimento.
Aggiungere un token di sessione/richiesta
A volte è difficile dire quali voci nel log appartengano a quale sessione e/o richiesta. L’esempio seguente aggiunge
un token univoco per ogni richiesta, usando un processore.
namespace Acme\MyBundle;
use Symfony\Component\HttpFoundation\Session;
class SessionRequestProcessor
{
private $session;
private $token;
public function __construct(Session $session)
{
$this->session = $session;
}
public function processRecord(array $record)
{
if (null === $this->token) {
try {
$this->token = substr($this->session->getId(), 0, 8);
} catch (\RuntimeException $e) {
$this->token = ’????????’;
}
$this->token .= ’-’ . substr(uniqid(), -8);
}
$record[’extra’][’token’] = $this->token;
return $record;
}
}
• YAML
services:
monolog.formatter.session_request:
class: Monolog\Formatter\LineFormatter
arguments:
- "[%%datetime%%] [%%extra.token%%] %%channel%%.%%level_name%%: %%message%%\n"
monolog.processor.session_request:
class: Acme\MyBundle\SessionRequestProcessor
arguments: [ @session ]
tags:
- { name: monolog.processor, method: processRecord }
monolog:
handlers:
main:
type: stream
path: %kernel.logs_dir%/%kernel.environment%.log
3.1. Ricettario
415
Symfony2 documentation Documentation, Release 2
level: debug
formatter: monolog.formatter.session_request
Note: Se si usano molti gestori, si può anche registrare il processore a livello di gestore, invece che globalmente.
3.1.58 Come configurare Monolog con errori per email
Monolog può essere configurato per inviare un’email quando accade un errore in un’applicazione. La configurazione per farlo richiede alcuni gestori annidati, per evitare di ricevere troppe email. Questa configurazione
appare complicata a prima vista, ma ogni gestore è abbastanza semplice, se visto singolarmente.
• YAML
# app/config/config.yml
monolog:
handlers:
mail:
type:
fingers_crossed
action_level: critical
handler:
buffered
buffered:
type:
buffer
handler: swift
swift:
type:
swift_mailer
from_email: [email protected]
to_email:
[email protected]
subject:
An Error Occurred!
level:
debug
• XML
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:monolog="http://symfony.com/schema/dic/monolog"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/
http://symfony.com/schema/dic/monolog http://symfony.com/schema/dic/m
<monolog:config>
<monolog:handler
name="mail"
type="fingers_crossed"
action-level="critical"
handler="buffered"
/>
<monolog:handler
name="buffered"
type="buffer"
handler="swift"
/>
<monolog:handler
name="swift"
from-email="[email protected]"
to-email="[email protected]"
subject="An Error Occurred!"
level="debug"
/>
</monolog:config>
</container>
416
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
Il gestore mail è un gestore fingers_crossed, che vuol dire che viene evocato solo quando si raggiunge il
livello di azione, in questo caso critical. Esso logga ogni cosa, inclusi i messaggi sotto il livello di azione. Il
livello critical viene raggiunto solo per codici di errore HTTP 5xx. L’impostazione handler vuol dire che
l’output è quindi passato nel gestore buffered.
Tip: Se si vuole che siano inviati per email sia gli errori 400 che i 500, impostare action_level a error,
invece che a critical.
Il gestore buffered mantiene tutti i messaggi per una richiesta e quindi li passa al gestore annidato in un colpo.
Se non si usa questo gestore, ogni messaggio sarà inviato separatamente. Viene quindi passato al gestore swift.
Questo gestore è quello che tratta effettivamente l’invio della email con gli errori. Le sue impostazioni sono
semplici: gli indirizzi di mittente e destinatario e l’oggetto.
Si possono combinare questi gestori con altri gestori, in modo che gli errori siano comunque loggati sul server,
oltre che inviati per email:
• YAML
# app/config/config.yml
monolog:
handlers:
main:
type:
fingers_crossed
action_level: critical
handler:
grouped
grouped:
type:
group
members: [streamed, buffered]
streamed:
type: stream
path: %kernel.logs_dir%/%kernel.environment%.log
level: debug
buffered:
type:
buffer
handler: swift
swift:
type:
swift_mailer
from_email: [email protected]
to_email:
[email protected]
subject:
An Error Occurred!
level:
debug
• XML
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:monolog="http://symfony.com/schema/dic/monolog"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/
http://symfony.com/schema/dic/monolog http://symfony.com/schema/dic/m
<monolog:config>
<monolog:handler
name="main"
type="fingers_crossed"
action_level="critical"
handler="grouped"
/>
<monolog:handler
name="grouped"
type="group"
>
<member type="stream"/>
<member type="buffered"/>
3.1. Ricettario
417
Symfony2 documentation Documentation, Release 2
</monolog:handler>
<monolog:handler
name="stream"
path="%kernel.logs_dir%/%kernel.environment%.log"
level="debug"
/>
<monolog:handler
name="buffered"
type="buffer"
handler="swift"
/>
<monolog:handler
name="swift"
from-email="[email protected]"
to-email="[email protected]"
subject="An Error Occurred!"
level="debug"
/>
</monolog:config>
</container>
Qui è stato usato il gestore group, per inviare i messaggi ai due membri del gruppo, il gestore buffered e il
gestore stream. I messaggi saranno ora sia scritti sul log che inviati per email.
3.1.59 Come ottimizzare l’ambiente di sviluppo per il debug
Lavorando a un progetto Symfony sulla propria macchina locale, si dovrebbe utilizzare l’ambiente dev (il front
controller app_dev.php). La configurazione di questo ambiente è ottimizzata per due scopi principali:
• Dare accurate informazioni al programmatore quando qualcosa non funziona (barra web di debug, chiare
pagine delle eccezzioni, misurazione delle prestazioni, ...);
• Essere più simile possibile all’ambiente di produzione per evitare spiacevoli sorprese nel momento del
rilascio del progetto.
Disabilitare il file di avvio e la cache delle classi
Per rendere l’ambiente di produzione il più veloce possibile, Symfony crea un unico file PHP, all’interno della
cache, che raccoglie tutte le classi PHP di cui ha bisogno il progetto. Un comportamento che potrebbe però
confondere l’IDE o il debugger. Questa ricetta mostrerà come modificare il meccanismo di gestione della cache
per rendere più agevole il debug del codice relativo alle classi di Symfony.
Il front controller app_dev.php contiene, nella sua versione predefinita, il seguente codice:
// ...
require_once __DIR__.’/../app/bootstrap.php.cache’;
require_once __DIR__.’/../app/AppKernel.php’;
use Symfony\Component\HttpFoundation\Request;
$kernel = new AppKernel(’dev’, true);
$kernel->loadClassCache();
$kernel->handle(Request::createFromGlobals())->send();
Per facilitare il lavoro del debugger, è possibile disabilitare la cache di tutte le classi PHP rimuovendo la chiamata
a loadClassCache() e sostituendo la dichirazione del require, nel seguente modo:
// ...
// require_once __DIR__.’/../app/bootstrap.php.cache’;
418
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
require_once __DIR__.’/../vendor/symfony/src/Symfony/Component/ClassLoader/UniversalClassLoader.ph
require_once __DIR__.’/../app/autoload.php’;
require_once __DIR__.’/../app/AppKernel.php’;
use Symfony\Component\HttpFoundation\Request;
$kernel = new AppKernel(’dev’, true);
// $kernel->loadClassCache();
$kernel->handle(Request::createFromGlobals())->send();
Tip: Una volta disabilitata la cache delle classi PHP, non bisogna dimenticare di riabilitarla alla fine della sessione
di debug.
Alcuni IDE non gradiscono il fatto che certe classi siano salvate in posti differenti. Per evitare problemi, è possibile
o configurare l’IDE per ignorare i file PHP della cache oppure modificare l’estensione che Symfony assegna a
questi file:
$kernel->loadClassCache(’classes’, ’.php.cache’);
3.1.60 Come estendere una classe senza usare l’ereditarietà
Per consentire a molte classi di aggiungere metodi a un’altra classe, si può definire il metodo magico __call()
nella classe che si vuole estendere, in questo modo:
class Pippo
{
// ...
public function __call($method, $arguments)
{
// crea un evento chiamato ’pippo.metodo_non_trovato’
$event = new HandleUndefinedMethodEvent($this, $method, $arguments);
$this->dispatcher->dispatch($this, ’pippo.metodo_non_trovato’, $event);
// nessun ascoltatore ha potuto processare l’evento? Il metodo non esiste
if (!$event->isProcessed()) {
throw new \Exception(sprintf(’Metodo non definito %s::%s.’, get_class($this), $method)
}
// restituisce all’ascoltatore il valore restituito
return $event->getReturnValue();
}
}
Qui viene usato una classe speciale HandleUndefinedMethodEvent, che va creata. È una classe generica
che potrebbe essere riusata ogni volta che si ha bisogno di questo tipo di estensione di classe:
use Symfony\Component\EventDispatcher\Event;
class HandleUndefinedMethodEvent extends Event
{
protected $subject;
protected $method;
protected $arguments;
protected $returnValue;
protected $isProcessed = false;
public function __construct($subject, $method, $arguments)
{
$this->subject = $subject;
3.1. Ricettario
419
Symfony2 documentation Documentation, Release 2
$this->method = $method;
$this->arguments = $arguments;
}
public function getSubject()
{
return $this->subject;
}
public function getMethod()
{
return $this->method;
}
public function getArguments()
{
return $this->arguments;
}
/**
* Imposta il valore da restituire e ferma le notifiche agli altri ascoltatori
*/
public function setReturnValue($val)
{
$this->returnValue = $val;
$this->isProcessed = true;
$this->stopPropagation();
}
public function getReturnValue($val)
{
return $this->returnValue;
}
public function isProcessed()
{
return $this->isProcessed;
}
}
Quindi, creare una classe che ascolterà l’evento pippo.metodo_non_trovato e aggiungere il metodo
pluto():
class Pluto
{
public function onPippoMethodIsNotFound(HandleUndefinedMethodEvent $event)
{
// vogliamo rispondere solo alle chiamate al metodo ’pluto’
if (’pluto’ != $event->getMethod()) {
// consente agli altri ascoltatori di prendersi cura di questo metodo sconosciuto
return;
}
// l’oggetto in questione (l’istanza di Pippo)
$pippo = $event->getSubject();
// i parametri del metodo ’pluto’
$arguments = $event->getArguments();
// fare qualcosa
// ...
// impostare il valore restituito
420
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
$event->setReturnValue($someValue);
}
}
Infine, aggiungere il nuovo metodo pluto alla classe Pippo, registrando un’istanza di Pluto con l’evento
pippo.metodo_non_trovato:
$pluto = new Pluto();
$dispatcher->addListener(’pippo.metodo_non_trovato’, $pluto);
3.1.61 Come personalizzare il comportamento di un metodo senza usare
l’ereditarietà
Fare qualcosa prima o dopo la chiamata a un metodo
Se si vuole fare qualcosa subito prima o subito dopo che un metodo sia chiamato, si può inviare un evento rispettivamente all’inizio o alla fine del metodo:
class Pippo
{
// ...
public function send($foo, $bar)
{
// fa qualcosa prima del metodo
$event = new FilterBeforeSendEvent($foo, $bar);
$this->dispatcher->dispatch(’foo.pre_send’, $event);
// prende $foo e $bar dall’evento, potrebbero essere stati modificati
$foo = $event->getFoo();
$bar = $event->getBar();
// la vera implementazione del metodo è qui
// $ret = ...;
// fa qualcosa dopo il metodo
$event = new FilterSendReturnValue($ret);
$this->dispatcher->dispatch(’foo.post_send’, $event);
return $event->getReturnValue();
}
}
In questo esempio, vengono lanciati due eventi: foo.pre_send, prima che il metodo sia eseguito, e
foo.post_send, dopo che il metodo è eseguito. Ciascuno usa una classe Event personalizzata per comunicare informazioni agli ascoltatori di questi due eventi. Queste classi evento andrebbero create dallo sviluppatore
e dovrebbero consentire, in questo esempio, alle variabili $foo, $bar e $ret di essere recuperate e impostate
dagli ascoltatori.
Per esempio, ipotizziamo che FilterSendReturnValue abbia un metodo setReturnValue. un ascoltatore potrebbe assomigliare a questo:
public function onFooPostSend(FilterSendReturnValue $event)
{
$ret = $event->getReturnValue();
// modifica il valore originario di $ret
$event->setReturnValue($ret);
}
3.1. Ricettario
421
Symfony2 documentation Documentation, Release 2
3.1.62 Registrare un nuovo formato di richiesta e un nuovo tipo mime
Ogni Richiesta ha a un “formato” (come html, json), che viene usato per determinare il tipo
di contenuto che dovrà essere restituito nell Risposta. Il formato della richiesta, accessibile tramite
:method:‘Symfony\\Component\\HttpFoundation\\Request::getRequestFormat‘, viene infatti utilizzato per
definire il tipo MIME dell’intestazione Content-Type dell’oggetto Risposta. Symfony contiene una
mappa dei formati più comuni (come html, json) e del corrispettivo tipo MIME (come text/html,
application/json). È comunque possibile aggiungere nuovi formati-MIME. In questo documento si vedrà come aggiungere un nuovo formato jsonp e il corrispondente tipo MIME.
Creazione di un ascoltatore per il kernel.request
La chiave per definire un nuovo tipo MIME è creare una classe che rimarrà in ascolto dell’evento
kernel.request emesso dal kernel di Symfony. L’evento kernel.request è emesso da Symfony nelle
primissime fasi della gestione della richiesta e permette di modificare l’ oggetto richiesta.
Si crea una classe simile alla seguente, sostituendo i percorsi in modo che puntino ad un bundle del proprio
progetto:
// src/Acme/DemoBundle/RequestListener.php
namespace Acme\DemoBundle;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
class RequestListener
{
public function onKernelRequest(GetResponseEvent $event)
{
$event->getRequest()->setFormat(’jsonp’, ’application/javascript’);
}
}
Registrazione dell’ascoltatore
Come per ogni ascoltatore, è necessario aggiungere anche questo nel file di configurazione e registrarlo come tale
aggiungendogli il tag kernel.event_listener:
• XML
<!-- app/config/config.xml -->
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/
<service id="acme.demobundle.listener.request" class="Acme\DemoBundle\RequestListener">
<tag name="kernel.event_listener" event="kernel.request" method="onKernelRequest" />
</service>
</container>
• YAML
# app/config/config.yml
services:
acme.demobundle.listener.request:
class: Acme\DemoBundle\RequestListener
tags:
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }
422
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
• PHP
# app/config/config.php
$definition = new Definition(’Acme\DemoBundle\RequestListener’);
$definition->addTag(’kernel.event_listener’, array(’event’ => ’kernel.request’, ’method’ => ’
$container->setDefinition(’acme.demobundle.listener.request’, $definition);
A questo punto, il servizio acme.demobundle.listener.request è stato configurato e verrà notificato
dell’avvenuta emissione, da parte del kernel Symfony, dell’evento kernel.request.
Tip: È possibile registrare l’ascoltatore anche in una classe di estensione della configurazione (si veda Importare
la configurazione attraverso estensioni del contenitore per ulteriori informazioni).
3.1.63 Come creare un raccoglitore di dati personalizzato
Il Profiler di Symfony delega la raccolta di dati ai raccoglitori di dati. Symfony2 dispone di un paio di
raccoglitori, ma se ne possono creare di personalizzati.
Creare un raccoglitore di dati personalizzato
Creare
un
raccoglitore
di
dati
personalizzato
è
semplice,
basta
implementare
Symfony\Component\HttpKernel\DataCollector\DataCollectorInterface:
interface DataCollectorInterface
{
/**
* Collects data for the given Request and Response.
*
$request
A Request instance
* @param Request
$response A Response instance
* @param Response
* @param \Exception $exception An Exception instance
*/
function collect(Request $request, Response $response, \Exception $exception = null);
/**
* Returns the name of the collector.
*
* @return string The collector name
*/
function getName();
}
Il metodo getName() deve restituire un nome univoco. Viene usato per accedere successivamente
all’informazione (vedere Come usare il profilatore nei test funzionali, per esempio).
Il metodo collect() è responsabile della memorizzazione dei dati, a cui vuole dare accesso, in proprietà locali.
Caution: Siccome il profilatore serializza istanze di raccoglitori di dati, non si dovrebbero memorizzare
oggetti che non possono essere serializzati (come gli oggetti PDO), altrimenti occorre fornire il proprio metodo
serialize().
La maggior parte delle volte, conviene estendere Symfony\Component\HttpKernel\DataCollector\DataCollector
e popolare la proprietà $this->data (che si occupa di serializzare la proprietà $this->data):
class MemoryDataCollector extends DataCollector
{
public function collect(Request $request, Response $response, \Exception $exception = null)
{
$this->data = array(
3.1. Ricettario
423
Symfony2 documentation Documentation, Release 2
’memory’ => memory_get_peak_usage(true),
);
}
public function getMemory()
{
return $this->data[’memory’];
}
public function getName()
{
return ’memory’;
}
}
Abilitare i raccoglitori di dati personalizzati
Per abilitare un raccoglitore di dati, aggiungerlo come servizio in una delle proprie configurazioni e assegnarli il
tag data_collector:
• YAML
services:
data_collector.your_collector_name:
class: Fully\Qualified\Collector\Class\Name
tags:
- { name: data_collector }
• XML
<service id="data_collector.your_collector_name" class="Fully\Qualified\Collector\Class\Name"
<tag name="data_collector" />
</service>
• PHP
$container
->register(’data_collector.your_collector_name’, ’Fully\Qualified\Collector\Class\Name’)
->addTag(’data_collector’)
;
Aggiungere template al profilatore web
Quando si vogliono mostrare i dati raccolti dal proprio raccoglitore di dati nella barra di debug del web, oppure
nel profilatore web, creare un template Twig, seguendo questo scheletro:
{% extends ’WebProfilerBundle:Profiler:layout.html.twig’ %}
{% block toolbar %}
{# contenuto della barra di debug del web #}
{% endblock %}
{% block head %}
{# se il profiltatore web ha bisogno di file JS o CSS #}
{% endblock %}
{% block menu %}
{# contenuto del menù #}
{% endblock %}
{% block panel %}
424
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
{# contenuto del pannello #}
{% endblock %}
I blocchi sono tutti facoltativi. Il blocco toolbar è usato per la barra di debug del web, mentre menu e panel
sono usati per aggiungere un pannello al profilatore web.
Tutti i blocchi hanno accesso all’oggetto collector.
Tip:
I
template
predefiniti
usano
immagini
codificate
in
base64
barra
(<img src="src="data:image/png;base64,...").
Si
può
lare facilmente il valore base64 di un’immagine con questo piccolo script:
base64_encode(file_get_contents($_SERVER[’argv’][1]));.
per
la
calcoecho
Per abilitare il template, aggiungere un attributo template al tag data_collector nella propria configurazione. Per esempio, ipotizzando che il template sia in un AcmeDebugBundle:
• YAML
services:
data_collector.your_collector_name:
class: Acme\DebugBundle\Collector\Class\Name
tags:
- { name: data_collector, template: "AcmeDebug:Collector:templatename", id: "your
• XML
<service id="data_collector.your_collector_name" class="Acme\DebugBundle\Collector\Class\Name
<tag name="data_collector" template="AcmeDebug:Collector:templatename" id="your_collector
</service>
• PHP
$container
->register(’data_collector.your_collector_name’, ’Acme\DebugBundle\Collector\Class\Name’)
->addTag(’data_collector’, array(’template’ => ’AcmeDebugBundle:Collector:templatename’,
;
3.1.64 Come creare un servizio web SOAP in un controllore di Symfony2
Impostare un controllore per agire da server SOAP è semplice, con un paio di strumenti. Occorre avere, ovviamente, l’estensione PHP SOAP installata. Poiché l’estensione PHP SOAP non può attualmente generare un
WSDL, se ne deve creare uno da zero, oppure usare un generatore di terze parti.
Note: Ci sono molte implementazioni di server SOAP disponibili per PHP. Zend SOAP e NuSOAP sono due
esempi. Anche se useremo l’estensione PHP SOAP nei nostri esempi, l’idea generale dovrebbe essere applicabile
ad altre implementazioni.
SOAP funziona espondendo i metodi di un oggetto PHP a un’entità esterna (alla persona che usa il servizio
SOAP). Per iniziare, creare una classe HelloService, che rappresenta la funzionalità che sarà esposta nel
servizio SOAP. In questo caso, il servizio SOAP consentirà al client di richiamare un metodo chiamto hello, che
invia un’email:
namespace Acme\SoapBundle;
class HelloService
{
private $mailer;
public function __construct(\Swift_Mailer $mailer)
{
3.1. Ricettario
425
Symfony2 documentation Documentation, Release 2
$this->mailer = $mailer;
}
public function hello($name)
{
$message = \Swift_Message::newInstance()
->setTo(’[email protected]’)
->setSubject(’Servizio Hello’)
->setBody($name . ’ dice ciao!’);
$this->mailer->send($message);
return ’Hello, ’ . $name;
}
}
Quindi, si può dire a Symfony di creare un’istanza di questa classe. Poiché la classe invia un’email, è stata
concepita per accettare un’istanza di Swift_Mailer. Usando il contenitore di servizi, possiamo configurare
Symfony per costruire un oggetto HelloService in modo appropriato:
• YAML
# app/config/config.yml
services:
hello_service:
class: Acme\DemoBundle\Services\HelloService
arguments: [mailer]
• XML
<!-- app/config/config.xml -->
<services>
<service id="hello_service" class="Acme\DemoBundle\Services\HelloService">
<argument>mailer</argument>
</service>
</services>
Di seguito un esempio di un controllore che è in grando di gestire una richiesta SOAP. Se indexAction() è
accessibile tramite la rotta /soap, il documento WSDL può essere recuperato tramite /soap?wsdl.
namespace Acme\SoapBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class HelloServiceController extends Controller
{
public function indexAction()
{
$server = new \SoapServer(’/path/to/hello.wsdl’);
$server->setObject($this->get(’hello_service’));
$response = new Response();
$response->headers->set(’Content-Type’, ’text/xml; charset=ISO-8859-1’);
ob_start();
$server->handle();
$response->setContent(ob_get_clean());
return $response;
}
}
426
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
Si notino le chiamate a ob_start() e ob_get_clean(). Qesti metodi controllano il buffer dell’output, che
consente di “intrappolare” l’output inviato da $server->handle(). Questo si rende necessario, in quanto
Symfony si aspetta che il controllore restituisca un oggetto Response, con l’output come contenuto. Si deve
anche ricordare di impostare l’header “Content-Type” a “text/xml”, che è quello che il client si aspetta. Quindi,
si usa ob_start() per iniziare il buffer di STDOUT e ob_get_clean() per inviare l’output nel contenuto
della risposta e per pulire il buffer. Infine, è tutto pronto per restituire l’oggetto Response.
Di seguito un esempio che richiama il servizio, usando un client NuSOAP. Questo esempio presume che
indexAction nel controllore visto sopra sia accessibile tramite la rotta /soap:
$client = new \soapclient(’http://example.com/app.php/soap?wsdl’, true);
$result = $client->call(’hello’, array(’name’ => ’Scott’));
Di seguito, un esempio di WSDL
<?xml version="1.0" encoding="ISO-8859-1"?>
<definitions xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/"
xmlns:tns="urn:arnleadservicewsdl"
xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
xmlns="http://schemas.xmlsoap.org/wsdl/"
targetNamespace="urn:helloservicewsdl">
<types>
<xsd:schema targetNamespace="urn:hellowsdl">
<xsd:import namespace="http://schemas.xmlsoap.org/soap/encoding/" />
<xsd:import namespace="http://schemas.xmlsoap.org/wsdl/" />
</xsd:schema>
</types>
<message name="helloRequest">
<part name="name" type="xsd:string" />
</message>
<message name="helloResponse">
<part name="return" type="xsd:string" />
</message>
<portType name="hellowsdlPortType">
<operation name="hello">
<documentation>Hello World</documentation>
<input message="tns:helloRequest"/>
<output message="tns:helloResponse"/>
</operation>
</portType>
<binding name="hellowsdlBinding" type="tns:hellowsdlPortType">
<soap:binding style="rpc" transport="http://schemas.xmlsoap.org/soap/http"/>
<operation name="hello">
<soap:operation soapAction="urn:arnleadservicewsdl#hello" style="rpc"/>
<input>
<soap:body use="encoded" namespace="urn:hellowsdl"
encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/>
</input>
<output>
<soap:body use="encoded" namespace="urn:hellowsdl"
encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/>
</output>
</operation>
</binding>
<service name="hellowsdl">
<port name="hellowsdlPort" binding="tns:hellowsdlBinding">
<soap:address location="http://example.com/app.php/soap" />
</port>
</service>
3.1. Ricettario
427
Symfony2 documentation Documentation, Release 2
</definitions>
3.1.65 Differenze tra Symfony2 e symfony1
Il framework Symfony2 rappresenta un’importante evoluzione rispetto alla sua versione precedente. Fortunatamente, con l’architettura MVC al suo interno, le abilità usate per padroneggiare un progetto symfony1 continuano
a essere molto importanti per lo sviluppo con Symfony2. Certo, non c’è più app.yml, ma le rotte, i controllori e
i template ci sono ancora tutti.
In questo capitolo analizzeremo le differenze tra symfony1 e Symfony2. Come vedremo, diverse cose sono implementate in modo un po’ diverso. Si imparerà ad apprezzare tali differenze, in quanto esse promuovono nella
propria applicazione Symfony2 un codice stabile, prevedibile, testabile e disaccoppiato.
Prendiamoci dunque un po’ di relax, per andare da “allora” ad “adesso”.
Struttura delle cartelle
Guardando a un progetto Symfony2, per esempio Symfony2 Standard, si noterà una struttura di cartelle molto
diversa rispetto a symfony1. Le differenze, tuttavia, sono in qualche modo superficiali.
La cartella app/
In symfony1, un progetto ha una o più applicazioni, ognuna delle quali risiede nella cartella apps/ (per esempio apps/frontend). La configurazione predefinita di Symfony2 è di avere un’unica applicazione, nella
cartella app/. Come in symfony1, la cartella app/ contiene una configurazione specifica per quell’applicazione.
Contiene inoltre cartelle di cache, log e template specifiche dell’applicazione, come anche una classe Kernel
(AppKernel), che è l’oggetto di base che rappresenta l’applicazione.
Diversamente da symfony1, c’è pochissimo codice PHP nella cartella app/. Questa cartella non è pensata per
ospitare moduli o file di librerie, come era in symfony1. Invece, è semplicemente il posto in cui risiedono la
configurazione e altre risorse (template, file di traduzione).
La cartella src/
Semplicemente, il proprio codice va messo qui. In Symfony, tutto il codice relativo alle applicazioni risiede in
un bundle (pressappoco equivalente a un plugin di symfony1) e ogni bundle risiede, per impostazione predefinita,
nella cartella src. In questo modo, la cartella src è un po’ come la cartella plugins di symfony1, ma molto
più flessibile. Inoltre, mentre i propri bundle risiedono nella cartella src, i bundle di terze parti possono risedere
nella cartella vendor/bundles/.
Per avere un quadro più completo della cartella src/, pensiamo a un’applicazione symfony1. Innanzitutto, parte
del proprio codice probabilmente risiede in una o più applicazioni. Solitamente questo include dei moduli, ma
potrebbe anche includere altre classi PHP inserite nella propria applicazione. Si potrebbe anche aver creato un
file schema.yml nella cartella config del progetto e costruito diversi file di modello. Infine, per aiutarsi con
alcune funzionalità comuni, si usano diversi plugin di terze parte, che stanno nella cartella plugins/. In altre
parole, il codice che guida la propria applicazione risiede in molti posti diversi.
In Symfony2, la vite è molto più semplice, perché tutto il codice di Symfony2 deve risiedere in un bundle. Nel
nostro ipotetico progetto symfony1, tutto il codice potrebbe essere spostato in uno o più plugin (che in effetti è una
buona pratica). Ipotizzando che tutti i moduli, le classi PHP, lo schema, la configurazione delle rotte, eccetera siano
spostate in un plugin, la cartella plugins/ di symfony1 sarebbe molto simile alla cartella src/ di Symfony2.
Detto semplicemente, la cartella src/ è il posto in cui risiedono il proprio codice, le risorse, i template e quasi
ogni altra cosa specifica del proprio progetto.
428
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
La cartella vendor/
La cartella vendor/ è essenzialmente equivalente alla cartella lib/vendor/ in symfony1, che era la cartella
convenzionale per tutte le librerie di terze parti. Per impostazione predefinita, si troveranno le librerie di Symfony2
in questa cartella, insieme a diverse altre librerie indipendenti, come Doctrine2, Twig e Swiftmailer. I bundle di
Symfony2 di terze parti solitamente risiedono in vendor/bundles/.
La cartella web/
Non è cambiato molto nella cartella web/. La differenza più notevole è l’assenza delle cartelle css/, js/ e
images/. La cosa è intenzionale. Come il proprio codice PHP, tutte le risorse dovrebbero risiedere all’interno
di un bundle. Con l’aiuto di un comando della console, la cartella Resources/public/ di ogni bundle viene
copiata o collegata alla cartella web/bundles/. Questo consente di mantenere le risorse organizzate nel proprio
bundle, ma ancora disponibili pubblicamente. Per assicurarsi che tutti i bundle siano disponibili, eseguire il
seguente comando:
php app/console assets:install web
Note: Questo comando di Symfony2 è l’equivalente del comando plugin:publish-assets di symfony1.
Auto-caricamento
Uno dei vantaggi dei framework moderni è il non doversi preoccupare di richiedere i file. Utilizzando un autoloader, si può fare riferimento a qualsiasi classe nel proprio progetto e fidarsi che essa sia disponibile. L’autocaricamento è cambiato in Symfony2, per essere più universale, più veloce e indipendente dalla pulizia della
cache.
In symfony1, l’auto-caricamento era effettuato cercando nell’intero progetto la presenza di file di classe PHP e
mettendo in cache tale informazione in un gigantesco array. Questo array diceva a symfony1 esattamente quale
file conteneva ciascuna classe. Nell’ambiente di produzione, questo causava la necessità di dover pulire la cache
quando una classe veniva aggiunta o spostata.
In Symfony2, una nuova classe, UniversalClassLoader gestisce questo processo. L’idea dietro
all’autoloader è semplice: il nome della propria classe (incluso lo spazio dei nomi) deve corrispondere al percorso del file che contiene tale classe. Si prenda come esempio FrameworkExtraBundle, nella Standard
Edition di Symfony2:
namespace Sensio\Bundle\FrameworkExtraBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
// ...
class SensioFrameworkExtraBundle extends Bundle
{
// ...
Il file stesso risiede in vendor/bundle/Sensio/Bundle/FrameworkExtraBundle/SensioFrameworkExtraBundl
Come si può vedere, la locazione del file segue lo spazio dei nomi della classe. Nello specifico, lo spazio
dei nomi Sensio\Bundle\FrameworkExtraBundle dice che la cartella in cui il file dovrebbe
risiedere (vendor/bundle/Sensio/Bundle/FrameworkExtraBundle). Per questo motivo, nel file
app/autoload.php, si dovrà configurare Symfony2 per cercare lo spazio dei nomi Sensio nella cartella
vendor/bundle:
// app/autoload.php
// ...
$loader->registerNamespaces(array(
// ...
3.1. Ricettario
429
Symfony2 documentation Documentation, Release 2
’Sensio’
=> __DIR__.’/../vendor/bundles’,
));
Se il file non risiede in questa esatta locazione,
si riceverà un errore Class
"Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle" does not
exist.. In Symfony2, un errore “class does not exist” vuol dire che lo spazio dei nomi della classe e la
locazione fisica del file non corrispondono. Fondamentalmente, Symfony2 cerca in una specifica locazione quella
classe, ma quella locazione non esiste (oppure contiene una classe diversa). Per poter auto-caricare una classe,
non è mai necessario pulire la cache in Symfony2.
Come già accennato, per poter far funzionare l’autoloader, esso deve sapere che lo spazio dei nomi Sensio
risiede nella cartella vendor/bundles e che, per esempio, lo spazio dei nomi Doctrine risiede nella cartella
vendor/doctrine/lib/. Questa mappatura è interamente controllata dallo sviluppatore, tramite il file
app/autoload.php.
Se si dà un’occhiata a HelloController nella Standard Edition di Symfony2, si vedrà che esso risiede nello
spazio dei nomi Acme\DemoBundle\Controller. Anche qui, lo spazio dei nomi Acme non è definito
in app/autoload.php. Non occorre configurare esplicitamente la locazione dei bundle che risiedono nella
cartella src/. UniversalClassLoader è configurato per usare come locazione di riserva la cartella src/,
usando il suo metodo registerNamespaceFallbacks:
// app/autoload.php
// ...
$loader->registerNamespaceFallbacks(array(
__DIR__.’/../src’,
));
Uso della console
In symfony1, la console è nella cartella radice del progetto ed è chiamata symfony:
php symfony
In Symfony2, la console è ora nella sotto-cartella app ed è chiamata console:
php app/console
Applicazioni
In un progetto basato su symfony 1, è frequente avere diverse applicazioni: per esempio, una per il frontend e una
per il backend.
In un progetto basato su Symfony2, occorre creare una sola applicazione (un’applicazione blog, un’applicazione
intranet, ...). La maggior parte delle volte, se si vuole creare una seconda applicazione, sarebbe meglio creare un
altro progetto e condividere alcuni bundle tra essi.
Se poi si ha bisogno di separare le caratteristiche di frontend e di backend di alcuni bundle, creare dei sottospazi per controller, delle sotto-cartelle per i template, configurazioni semantiche diverse, configurazioni di rotte
separate e così via.
Ovviamente non c’è nulla di sbagliato ad avere più di un’applicazione nel proprio progetto, questa scelta è lasciata
allo sviluppatore. Una seconda applicazione vorrebbe dire una nuova cartella, per esempio app2/, con la stessa
struttura di base della cartella app/.
Bundle e plugin
In un progetto symfony1, un plugin può contenere configurazioni, moduli, librerie PHP, risorse e qualsiasi altra
cosa relativa al proprio progetto. In Symfony2, l’idea di plugin è stata rimpiazzata con quella di “bundle”. Un
bundle è ancora più potente di un plugin, perché il nucleo stesso del framework Symfony2 è costituito da una
430
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
serie di bundle. In Symfony2, i bundle sono cittadini di prima classe e sono così flessibili che il nucleo stesso è un
bundle.
In symfony1, un plugin deve essere abilitato nella classe ProjectConfiguration:
// config/ProjectConfiguration.class.php
public function setup()
{
$this->enableAllPluginsExcept(array(/* nomi dei plugin */));
}
In Symfony2, i bundle sono attivati nel kernel dell’applicazione:
// app/AppKernel.php
public function registerBundles()
{
$bundles = array(
new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
new Symfony\Bundle\TwigBundle\TwigBundle(),
// ...
new Acme\DemoBundle\AcmeDemoBundle(),
);
return $bundles;
}
Rotte (routing.yml) e configurazione (config.yml)
In symfony1, i file di configurazione routing.yml e app.yml sono caricati automaticamente all’interno di un
plugin. In Symfony2, le rotte e le configurazioni dell’applicazioni all’interno di un bundle vanno incluse a mano.
Per esempio, per inmcludere le rotte di un bundle chiamato AcmeDemoBundle, si può fare nel seguente modo:
# app/config/routing.yml
_hello:
resource: "@AcmeDemoBundle/Resources/config/routing.yml"
Questo caricherà le rotte trovate nel file Resources/config/routing.yml di AcmeDemoBundle. Il
nome @AcmeDemoBundle è una sintassi abbreviata, risolta internamente con il percorso completo di quel bundle.
Si può usare la stessa strategia per portare una configurazione da un bundle:
# app/config/config.yml
imports:
- { resource: "@AcmeDemoBundle/Resources/config/config.yml" }
In Symfony2, la configurazione è un po’ come app.yml in symfony1, ma più sistematica. Con app.yml, si
poteva semplicemente creare le voci volute. Per impostazione predefinita, queste voci erano prive di significato ed
era lasciato allo sviluppatore il compito di usarle nella propria applicazione:
# un file app.yml da symfony1
all:
email:
from_address: [email protected]
In Symfony2, si possono ancora creare voci arbitrarie sotto la voce parameters della propria configurazione:
parameters:
email.from_address: [email protected]
Si può ora accedervi da un controllore, per esempio:
3.1. Ricettario
431
Symfony2 documentation Documentation, Release 2
public function helloAction($name)
{
$fromAddress = $this->container->getParameter(’email.from_address’);
}
In realtà, la configurazione di Symfony2 è molto più potente ed è usata principalmente per configurare oggetti da
usare. Per maggiori informazioni, vedere il capitolo intitolato “Contenitore di servizi”.
• Flusso di lavoro
– Come creare e memorizzare un progetto Symfony2 in git
– Come creare e memorizzare un progetto Symfony2 in Subversion
• Controllori
– Come personalizzare le pagine di errore
– Definire i controllori come servizi
• Rotte
– Come forzare le rotte per utilizzare sempre HTTPS
– Come permettere un carattere “/” in un parametro di rotta
• Gestione di JavaScript e CSS
– Come usare Assetic per la gestione delle risorse
– Minimizzare i file JavaScript e i fogli di stile con YUI Compressor
– Usare Assetic per l’ottimizzazione delle immagini con le funzioni di Twig
– Applicare i filtri di Assetic a file con specifiche estensioni
• Interazione col database (Doctrine)
– Come gestire il caricamento di file con Doctrine
– Estensioni di Doctrine: Timestampable: Sluggable, Translatable, ecc.
– Registrare ascoltatori e sottoscrittori di eventi
– Come usare il livello DBAL di Doctrine
– Come generare entità da una base dati esistente
– Come lavorare con gestori di entità multipli
– Registrare funzioni DQL personalizzate
• Form e validazione
– Come personalizzare la resa dei form
– Utilizzare i data transformer
– Come generare dinamicamente form usando gli eventi form
– Come unire una collezione di form
– Come creare un tipo di campo personalizzato di un form
– Come creare vincoli di validazione personalizzati
– (doctrine) Come gestire il caricamento di file con Doctrine
• Configurazione e contenitore di servizi
– Come padroneggiare e creare nuovi ambienti
– Configurare parametri esterni nel contenitore dei servizi
– Usare il factory per creare servizi
432
Chapter 3. Ricettario
Symfony2 documentation Documentation, Release 2
– Gestire le dipendenza comuni con i servizi padre
– Come lavorare con gli scope
– Come far sì che i servizi usino le etichette
– Usare PdoSessionStorage per salvare le sessioni nella base dati
• Bundle
– Struttura del bundle e best practice
– Come usare l’ereditarietà per sovrascrivere parti di un bundle
– Come sovrascrivere parti di un bundle
– Come esporre una configurazione semantica per un bundle
• Email
– Come spedire un’email
– Come usare Gmail per l’invio delle email
– Lavorare con le email durante lo sviluppo
– Lo spool della posta
• Test
– Come simulare un’autenticazione HTTP in un test funzionale
– Come testare l’interazione con diversi client
– Come usare il profilatore nei test funzionali
– Come testare i repository Doctrine
• Sicurezza
– Come caricare gli utenti dal database (il fornitore di entità)
– Come aggiungere la funzionalità “ricordami” al login
– Come implementare i propri votanti per una lista nera di indirizzi IP
– Access Control List (ACL)
– Concetti avanzati su ACL
– Come forzare HTTPS o HTTP per URL diversi
– Come personalizzare il form di login
– Proteggere servizi e metodi di un’applicazione
– Come creare un fornitore utenti personalizzato
– Come creare un fornitore di autenticazione personalizzato
– Come cambiare il comportamento predefinito del puntamento del percorso
• Cache
– Come usare Varnish per accelerare il proprio sito
• Template
– Iniettare variabili in tutti i template (variabili globali)
– Come usare PHP al posto di Twig nei template
• Strumenti, log e interni
– Come usare Monologo per scrivere log
– Come configurare Monolog con errori per email
3.1. Ricettario
433
Symfony2 documentation Documentation, Release 2
• Strumenti e interni
– Come ottimizzare l’ambiente di sviluppo per il debug
• Servizi web
– Come creare un servizio web SOAP in un controllore di Symfony2
• Estendere Symfony
– Come estendere una classe senza usare l’ereditarietà
– Come personalizzare il comportamento di un metodo senza usare l’ereditarietà
– Registrare un nuovo formato di richiesta e un nuovo tipo mime
– Come creare un raccoglitore di dati personalizzato
• Symfony2 per utenti di symfony1
– Differenze tra Symfony2 e symfony1
Leggere il ricettario.
434
Chapter 3. Ricettario
CHAPTER
FOUR
COMPONENTI
4.1 I componenti
4.1.1 Il componente ClassLoader
Il componente ClassLoader carica le classi di un progetto automaticamente, purché seguano alcune
convenzioni standard di PHP.
Ogni volta che si usa una classe non ancora definita, PHP utilizza il meccanismo di auto-caricamento per delegare
il caricamento di un file che definisca la classe. Symfony2 fornisce un autoloader “universale”, capace di caricare
classi da file che implementano una delle seguenti convenzioni:
• Gli standard tecnici di interoperabilità per i nomi di classi e gli spazi dei nomi di PHP 5.3;
• La convenzione dei nomi delle classi di PEAR.
Se le proprie classi e le librerie di terze parti usate per il proprio progetto seguono questi standard, l’autoloader di
Symfony2 è l’unico autoloader di cui si ha bisogno.
Installazione
Si può installare il componente in molti modi diversi:
• Usare il repository ufficiale su Git (https://github.com/symfony/ClassLoader);
• Installarlo via PEAR ( pear.symfony.com/ClassLoader);
• Installarlo via Composer (symfony/class-loader su Packagist).
Uso
New in version 2.1: Il metodo useIncludePath è stato aggiunto in Symfony 2.1. La registrazione di
Symfony\Component\ClassLoader\UniversalClassLoader è molto semplice:
require_once ’/percorso/src/Symfony/Component/ClassLoader/UniversalClassLoader.php’;
use Symfony\Component\ClassLoader\UniversalClassLoader;
$loader = new UniversalClassLoader();
// Si può cercare in include_path come ultima risorsa.
$loader->useIncludePath(true);
$loader->register();
Per un minimo guadagno di prestazioni, i percorsi delle classi possono essere memorizzati usando APC, registrando Symfony\Component\ClassLoader\ApcUniversalClassLoader:
435
Symfony2 documentation Documentation, Release 2
require_once ’/percorso/src/Symfony/Component/ClassLoader/UniversalClassLoader.php’;
require_once ’/percorso/src/Symfony/Component/ClassLoader/ApcUniversalClassLoader.php’;
use Symfony\Component\ClassLoader\ApcUniversalClassLoader;
$loader = new ApcUniversalClassLoader(’apc.prefix.’);
$loader->register();
L’autoloader è utile solo se si aggiungono delle librerie da auto-caricare.
Note:
L’autoloader è registrato automaticamente in ogni applicazione Symfony2 (si veda
app/autoload.php).
Se
le
classi
da
auto-caricare
usano
spazi
dei
nomi,
usare
:method:‘Symfony\\Component\\ClassLoader\\UniversalClassLoader::registerNamespace‘
:method:‘Symfony\\Component\\ClassLoader\\UniversalClassLoader::registerNamespaces‘:
i
metodi
o
$loader->registerNamespace(’Symfony’, __DIR__.’/vendor/symfony/src’);
$loader->registerNamespaces(array(
’Symfony’ => __DIR__.’/../vendor/symfony/src’,
’Monolog’ => __DIR__.’/../vendor/monolog/src’,
));
$loader->register();
Per
classi
che
seguono
la
convenzione
dei
nomi
di
PEAR,
usare
:method:‘Symfony\\Component\\ClassLoader\\UniversalClassLoader::registerPrefix‘
:method:‘Symfony\\Component\\ClassLoader\\UniversalClassLoader::registerPrefixes‘:
i
metodi
o
$loader->registerPrefix(’Twig_’, __DIR__.’/vendor/twig/lib’);
$loader->registerPrefixes(array(
’Swift_’ => __DIR__.’/vendor/swiftmailer/lib/classes’,
’Twig_’ => __DIR__.’/vendor/twig/lib’,
));
$loader->register();
Note: Alcune librerie richiedono anche che il loro percorso radice sia registrato nell’include_path di PHP
(set_include_path()).
Le classi di un sotto-spazio dei nomi o di una sotto-gerarchia di PEAR possono essere cercate in un elenco di
posizioni, per facilitare i venditori di un sotto-insieme di classi per grossi progetti:
$loader->registerNamespaces(array(
’Doctrine\\Common’
=>
’Doctrine\\DBAL\\Migrations’ =>
’Doctrine\\DBAL’
=>
’Doctrine’
=>
));
__DIR__.’/vendor/doctrine-common/lib’,
__DIR__.’/vendor/doctrine-migrations/lib’,
__DIR__.’/vendor/doctrine-dbal/lib’,
__DIR__.’/vendor/doctrine/lib’,
$loader->register();
In questo esempio, se si prova a usare una classe nello spazio dei nomi Doctrine\Common o uno dei suoi
figli, l’autoloader cercherà prima le classi sotto la cartella doctrine-common, quindi, se non le trova, cercherà
nella cartella Doctrine (l’ultima configurata), infine si arrenderà. In questo caso, l’ordine di registrazione è
significativo.
436
Chapter 4. Componenti
Symfony2 documentation Documentation, Release 2
4.1.2 Il componente Console
Il componente Console semplifica la creazione di eleganti e testabili comandi da terminale.
Symfony2 viene distribuito con un componente Console che permette di creare comandi da terminale. I comandi
da terminale possono essere utilizzati per qualsiasi lavoro ripetivo come i lavori di cron, le importazioni o lavori
batch.
Installazione
Il componente può essere installato in diversi modi:
• Utilizzando il repository Git ufficiale (https://github.com/symfony/Console);
• Installandolo via PEAR ( pear.symfony.com/Console);
• Installandolo via Composer (symfony/console in Packagist).
Creazione di comandi di base
Per avere automaticamente a disposizione, sotto Symfony2, un comando a terminale, si crea una cartella Command
all’interno del proprio bundle dentro la quale si inserirà un file, con il suffisso Command.php, per ogni comando
che si voglia realizzare. Ad esempio, per estendere l’AcmeDemoBundle (disponibile in Symfony Standard
Edition) con un programma che porga il saluto dal terminale, si dovrà creare il file SalutaCommand.php
contenente il seguente codice:
// src/Acme/DemoBundle/Command/GreetCommand.php
namespace Acme\DemoBundle\Command;
use
use
use
use
use
Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
Symfony\Component\Console\Input\InputArgument;
Symfony\Component\Console\Input\InputInterface;
Symfony\Component\Console\Input\InputOption;
Symfony\Component\Console\Output\OutputInterface;
class SalutaCommand extends ContainerAwareCommand
{
protected function configure()
{
$this
->setName(’demo:saluta’)
->setDescription(’Saluta qualcuno’)
->addArgument(’nome’, InputArgument::OPTIONAL, ’Chi vuoi salutare?’)
->addOption(’urla’, null, InputOption::VALUE_NONE, ’Se impostato, il saluto verrà urla
;
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$nome = $input->getArgument(’nome’);
if ($nome) {
$testo = ’Ciao ’.$nome;
} else {
$testo = ’Ciao’;
}
if ($input->getOption(’urla’)) {
$testo = strtoupper($testo);
}
$output->writeln($testo);
4.1. I componenti
437
Symfony2 documentation Documentation, Release 2
}
}
You also need to create the file to run at the command line which creates an Application and adds commands
to it:
È possibile provare il programma nel modo seguente
app/console demo:saluta Fabien
Il comando scriverà, nel terminale, quello che segue:
Ciao Fabien
È anche possibile usare l’opzione --urla per stampare il saluto in lettere maiuscole:
app/console demo:saluta Fabien --urla
Il cui risultato sarà:
CIAO FABIEN
Colorare l’output
È possibile inserire il testo da stampare, all’interno di speciali tag per colorare l’output. Ad esempio:
// testo verde
$output->writeln(’<info>pippo</info>’);
// testo giallo
$output->writeln(’<comment>pippo</comment>’);
// testo nero su sfondo ciano
$output->writeln(’<question>pippo</question>’);
// testo nero su sfondo rosso
$output->writeln(’<error>pippo</error>’);
Utilizzo degli argomenti nei comandi
La parte più interessante dei comandi è data dalla possibilità di mettere a disposizione parametri e argomenti.
Gli argomenti sono delle stringhe, separate da spazi, che seguono il nome stesso del comando. Devono essere
inseriti in un ordine preciso e possono essere opzionali o obbligatori. Ad esempio, per aggiungere un argomento
opzionale cognome al precedente comando e rendere l’argomento nome obbligatorio, si dovrà scrivere:
$this
// ...
->addArgument(’nome’, InputArgument::REQUIRED, ’Chi vuoi salutare?’)
->addArgument(’cognome’, InputArgument::OPTIONAL, ’Il tuo cognome?’)
// ...
A questo punto si può accedere all’argomento cognome dal proprio codice:
if ($cognome = $input->getArgument(’cognome’)) {
$testo .= ’ ’.$cognome;
}
Il comando potrà essere utilizzato in uno qualsiasi dei seguenti modi:
app/console demo:saluta Fabien
app/console demo:saluta Fabien Potencier
438
Chapter 4. Componenti
Symfony2 documentation Documentation, Release 2
Utilizzo delle opzioni nei comandi
Diversamente dagli argomenti, le opzioni non sono ordinate (cioè possono essere specificate in qualsiasi ordine)
e sono identificate dal doppio trattino (come in –urla; è anche possibile dichiarare una scorciatoia a singola lettera
preceduta da un solo trattino come in -u). Le opzioni sono sempre opzionali e possono accettare valori (come in
dir=src) o essere semplici indicatori booleani senza alcuna assegnazione (come in urla).
Tip: È anche possibile fare in modo che un’opzione possa opzionalmente accettare un valore (ad esempio si
potrebbe avere --urla o --urla=forte). Le opzioni possono anche essere configurate per accettare array di
valori.
Ad esempio, per specificare il numero di volte in cui il messaggio di saluto sarà stampato, si può aggiungere la
seguente opzione:
$this
// ...
->addOption(’ripetizioni’, null, InputOption::VALUE_REQUIRED, ’Quante volte dovrà essere stamp
Ora è possibile usare l’opzione per stampare più volte il messaggio:
for ($i = 0; $i < $input->getOption(’ripetizioni’); $i++) {
$output->writeln($testo);
}
In questo modo, quando si esegue il comando, sarà possibile specificare, opzionalmente, l’impostazione
--ripetizioni:
app/console demo:saluta Fabien
app/console demo:saluta Fabien --ripetizioni=5
Nel primo esempio, il saluto verrà stampata una sola volta, visto che ripetizioni è vuoto e il suo valore
predefinito è 1 (l’ultimo argomento di addOption). Nel secondo esempio, il saluto verrà stampato 5 volte.
Ricordiamo che le opzioni non devono essere specificate in un ordina predefinito. Perciò, entrambi i seguenti
esempi funzioneranno correttamente:
app/console demo:saluta Fabien --ripetizioni=5 --urla
app/console demo:saluta Fabien --urla --ripetizioni=5
Ci sono 4 possibili varianti per le opzioni:
Opzione
InputOption::VALUE_IS_ARRAY
InputOption::VALUE_NONE
InputOption::VALUE_REQUIRED
InputOption::VALUE_OPTIONAL
Valore
Questa opzione accetta valori multipli
Non accettare alcun valore per questa opzione (come in --urla)
Il valore è obbligatorio (come in ripetizioni=5)
Il valore è opzionale
È possibile combinare VALUE_IS_ARRAY con VALUE_REQUIRED o con VALUE_OPTIONAL nel seguente
modo:
$this
// ...
->addOption(’ripetizioni’, null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, ’Q
Richiedere informazioni all’utente
Nel creare comandi è possibile richiedere ulteriori informazioni dagli utenti rivolgendo loro domande. Ad esempio, si potrbbe richiedere la conferma prima di effettuare realmente una determinata azione. In questo caso si
dovrà aggiungere il seguente codice al comando:
4.1. I componenti
439
Symfony2 documentation Documentation, Release 2
$dialogo = $this->getHelperSet()->get(’dialog’);
if (!$dialogo->askConfirmation($output, ’<question>Vuoi proseguire con questa azione?</question>’,
return;
}
In questo modo, all’utente verrà chiesto se vuole “proseguire con questa azione” e, a meno che la risposta non sia
y, l’azione non verrà eseguita. Il terzo argomento di askConfirmation è il valore predefinito da restituire nel
caso in cui l’utente non fornisca alcun input.
È possibile rivolgere domande che prevedano risposte più complesse di un semplice si/no. Ad esempio, se volessimo conoscere il nome di qualcosa, potremmo fare nel seguente modo:
$dialogo = $this->getHelperSet()->get(’dialog’);
$nome = $dialogo->ask($output, ’Insersci il nome del widget’, ’pippo’);
Testare i comandi
Symfony2 mette a disposizione diversi strumenti a supporto del test dei comandi. Il più utile di questi è la
classe Symfony\Component\Console\Tester\CommandTester. Questa utilizza particolari classi per
la gestione dell’input/output che semplificano lo svolgimento di test senza una reale interazione da terminale:
use Symfony\Component\Console\Tester\CommandTester;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Acme\DemoBundle\Command\SalutaCommand;
class ListCommandTest extends \PHPUnit_Framework_TestCase
{
public function testExecute()
{
$application = new Application($kernel);
$application->add(new SalutaCommand());
$comando = $application->find(’demo:saluta’);
$testDelComando = new CommandTester($comando);
$testDelComando->execute(array(’command’ => $comando->getFullName()));
$this->assertRegExp(’/.../’, $testDelComando->getDisplay());
// ...
}
}
Il metodo :method:‘Symfony\\Component\\Console\\Tester\\CommandTester::getDisplay‘ restituisce ciò che
sarebbe stato mostrato durante una normale chiamata dal terminale.
Si può testare l’invio di argomenti e opzioni al comando, passandoli come array al metodo
:method:‘Symfony\\Component\\Console\\Tester\\CommandTester::getDisplay‘:
use Symfony\Component\Console\Tester\CommandTester;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Acme\DemoBundle\Command\GreetCommand;
class ListCommandTest extends \PHPUnit_Framework_TestCase
{
//-public function testNameIsOutput()
{
$application = new Application();
$application->add(new GreetCommand());
440
Chapter 4. Componenti
Symfony2 documentation Documentation, Release 2
$command = $application->find(’demo:saluta’);
$commandTester = new CommandTester($command);
$commandTester->execute(
array(’command’ => $command->getName(), ’name’ => ’Fabien’)
);
$this->assertRegExp(’/Fabien/’, $commandTester->getDisplay());
}
}
Tip:
È
possibile
testare
un’intera
applicazione
Symfony\Component\Console\Tester\ApplicationTester.
da
terminale
utilizzando
Richiamare un comando esistente
Se un comando dipende da un altro, da eseguire prima, invece di chiedere all’utente di ricordare l’ordine di
esecuzione, lo si può richiamare direttamente. Questo è utile anche quando si vuole creare un “meta” comando,
che esegue solo una serie di altri comandi (per esempio, tutti i comandi necessari quando il codice del progetto è
cambiato sui server di produzione: pulire la cache, genereare i proxy di Doctrine, esportare le risorse di Assetic,
...).
Richiamare un comando da un altro è molto semplice:
protected function execute(InputInterface $input, OutputInterface $output)
{
$comando = $this->getApplication()->find(’demo:saluta’);
$argomenti = array(
’command’ => ’demo:saluta’,
’nome’
=> ’Fabien’,
’--urla’ => true,
);
$input = new ArrayInput($argomenti);
$codiceDiRitorno = $comando->run($input, $output);
// ...
}
Innanzitutto si dovrà trovare (:method:‘Symfony\\Component\\Console\\Command\\Command::find‘) il comando da eseguire usandone il nome come parametro.
Quindi si dovrà creare un nuovo Symfony\Component\Console\Input\ArrayInput che contenga gli
argomenti e le opzioni da passare al comando.
Infine, la chiamata al metodo run() manderà effettivamente in esecuzione il comando e restituirà il codice di
ritorno del comando (0 se tutto è andato a buon fine, un qualsiasi altro intero negli altri altri casi).
Note: Nella maggior parte dei casi, non è una buona idea quella di eseguire un comando al di fuori del terminale.
Innanzitutto perché l’output del comando è ottimizzato per il terminale. Ma, anche più importante, un comando
è come un controllore: dovrebbe usare un modello per fare qualsiasi cosa e restituire informazioni all’utente.
Perciò, invece di eseguire un comando dal Web, sarebbe meglio provare a rifattorizzare il codice e spostare la
logica all’interno di una nuova classe.
4.1.3 Il componente CssSelector
Il componente CssSelector converte selettori CSS in espressioni XPath.
4.1. I componenti
441
Symfony2 documentation Documentation, Release 2
Installazione
Si può installare il componente in molti modi diversi:
• Usare il repository ufficiale su Git (https://github.com/symfony/CssSelector);
• Installarlo via PEAR ( pear.symfony.com/CssSelector);
• Installarlo via Composer (symfony/css-selector su Packagist).
Uso
Perché usare selettori CSS?
Quando si analizzano documenti HTML o XML, XPath è certamente il metodo più potente.
Le espressioni XPath sono incredibilmente flessibili, quindi c’è quasi sempre un’espressione XPath che troverò
l’elemento richiesto. Sfortunatamente, possono essere anche molto complicate e la curva di apprendimento è
ripida. Anche operazioni comuni (come trovare un elemento con una data classe) possono richiedere espressioni
lunghe e poco maneggevoli.
Molti sviluppatori, in particolare gli sviluppatori web, si trovano più a loro agio nel cercare elementi tramite
selettori CSS. Oltre a funzionare con i fogli di stile, i selettori CSS sono usati da Javascript con la funzione
querySelectorAll e da famose librerie Javascript, come jQuery, Prototype e MooTools.
I selettori CSS sono meno potenti di XPath, ma molto più facili da scrivere, leggere e capire. Essendo meno potenti, quasi tutti i selettori CSS possono essere convertiti in equivalenti XPath. Queste espressioni XPath possono
quindi essere usate con altre funzioni e classi che usano XPath per trovare elementi in un documento.
Il componente CssSelector
L’unico fine del componente è la conversione di selettori CSS nei loro equivalenti XPath:
use Symfony\Component\CssSelector\CssSelector;
print CssSelector::toXPath(’div.item > h4 > a’);
Questo fornisce il seguente output:
descendant-or-self::div[contains(concat(’ ’,normalize-space(@class), ’ ’), ’ item ’)]/h4/a
Si può usare questa espressione, per esempio, con :phpclass:‘DOMXPath‘ o :phpclass:‘SimpleXMLElement‘,
per trovare elementi in un documento.
Tip: Il metodo :method:‘Crawler::filter()<Symfony\\Component\\DomCrawler\\Crawler::filter>‘ usa il
componente CssSelector per trovare elementi in base a un selettore CSS. Si veda Il componente DomCrawler
per ulteriori dettagli.
Limiti del componente CssSelector
Non tutti i selettori CSS possono essere convertiti in equivalenti XPath.
Ci sono molti selettori CSS che hanno senso solo nel contesto di un browser.
• selettori dei collegamenti: :link, :visited, :target
• selettori basati su azioni dell’utente: :hover, :focus, :active
• selettori dello stato dell’interfaccia: :enabled, :disabled, :indeterminate (tuttavia, :checked
e :unchecked sono disponibili)
442
Chapter 4. Componenti
Symfony2 documentation Documentation, Release 2
Gli pseudo-elementi (:before, :after, :first-line, :first-letter) non sono supportati, perché
selezionano porzioni di testo, piuttosto che elementi.
Diverse pseudo-classi non sono ancora supportate:
• :lang(language)
• root
• *:first-of-type,
*:last-of-type,
*:nth-of-type,
*:only-of-type. (funzionano con il nome di un elemento (p.e.
non con *.
*:nth-last-of-type,
li:first-of-type) ma
4.1.4 Il componente DomCrawler
Il componente DomCrawler semplifica la navigazione nel DOM dei documenti HTML e XML.
Installazione
È possibile installare il componente in diversi modi:
• Utilizzando il repository ufficiale su Git (https://github.com/symfony/DomCrawler);
• Installandolo via PEAR ( pear.symfony.com/DomCrawler);
• Installandolo via Composer (symfony/dom-crawler su Packagist).
Utilizzo
La classe Symfony\Component\DomCrawler\Crawler mette a disposizione metodi per effettuare query
e manipolare i documenti HTML e XML.
Un’istanza di Crawler rappresenta un insieme (:phpclass:‘SplObjectStorage‘)
class:‘DOMElement‘, che sono, in pratica, nodi facilmente visitabili:
di
oggetti
:php-
use Symfony\Component\DomCrawler\Crawler;
$html = <<<’HTML’
<html>
<body>
<p class="messaggio">Ciao Mondo!</p>
<p>Ciao Crawler!</p>
</body>
</html>
HTML;
$crawler = new Crawler($html);
foreach ($crawler as $elementoDom) {
print $elementoDom->nodeName;
}
Le classi specializzate Symfony\Component\DomCrawler\Link e Symfony\Component\DomCrawler\Form
sono utili per interagire con collegamenti html e i form durante la visita dell’albero HTML.
Filtrare i nodi
È possibile usare facilmente le espressioni di XPath:
$crawler = $crawler->filterXPath(’descendant-or-self::body/p’);
4.1. I componenti
443
Symfony2 documentation Documentation, Release 2
Tip: internamente viene usato DOMXPath::query per eseguire le query XPath.
La ricerca è anche più semplice se si è installato il componente CssSelector. In questo modo è possibile usare
lo stile jQuery per l’attraversamento:
$crawler = $crawler->filter(’body > p’);
È possibile usare funzioni anonime per eseguire filtri complessi:
$crawler = $crawler->filter(’body > p’)->reduce(function ($node, $i) {
// filtra anche i nodi
return ($i % 2) == 0;
});
Per rimuovere i nodi, la funzione anonima dovrà restituire false.
Note:
Tutti
i
metodi
dei
filtri
restituiscono
una
Symfony\Component\DomCrawler\Crawler contenente gli elementi filtrati.
nuova
istanza
di
istanza
di
Attraversamento dei nodi
Accedere ai nodi tramite la loro posizione nella lista:
$crawler->filter(’body > p’)->eq(0);
Ottenere il primo o l’ultimo nodo della selezione:
$crawler->filter(’body > p’)->first();
$crawler->filter(’body > p’)->last();
Ottenere i nodi allo stesso livello della selezione attuale:
$crawler->filter(’body > p’)->siblings();
Ottenere i nodi, allo stesso livello, precedenti o successivi alla selezione attuale:
$crawler->filter(’body > p’)->nextAll();
$crawler->filter(’body > p’)->previousAll();
Ottenere tutti i nodi figlio o padre:
$crawler->filter(’body’)->children();
$crawler->filter(’body > p’)->parents();
Note:
Tutti i metodi di attraversamento
Symfony\Component\DomCrawler\Crawler.
restituiscono
un
nuova
Accedere ai nodi tramite il loro valore
Accedere al valore del primo nodo della selezione attuale:
$message = $crawler->filterXPath(’//body/p’)->text();
Accedere al valore dell’attributo del primo nodo della selezione attuale:
$class = $crawler->filterXPath(’//body/p’)->attr(’class’);
Estrarre l’attributo e/o il valore di un nodo da una lista di nodi:
444
Chapter 4. Componenti
Symfony2 documentation Documentation, Release 2
$attributes = $crawler->filterXpath(’//body/p’)->extract(array(’_text’, ’class’));
Note: L’attributo speciale _text rappresenta il valore di un nodo.
Chiamare una funzione anonima su ogni nodo della lista:
$nodeValues = $crawler->filter(’p’)->each(function ($nodo, $i) {
return $nodo->nodeValue;
});
La funzione anonima riceve la posizione e il nodo come argomenti. Il risultato è un array contenente i valori
restituiti dalle chiamate alla funzione anonima.
Aggiungere contenuti
Il crawler supporta diversi modi per aggiungere contenuti:
$crawler = new Crawler(’<html><body /></html>’);
$crawler->addHtmlContent(’<html><body /></html>’);
$crawler->addXmlContent(’<root><node /></root>’);
$crawler->addContent(’<html><body /></html>’);
$crawler->addContent(’<root><node /></root>’, ’text/xml’);
$crawler->add(’<html><body /></html>’);
$crawler->add(’<root><node /></root>’);
Essendo l’implementazione del Crawler basata sull’estensione di DOM, è anche possibile interagire con le classi
native :phpclass:‘DOMDocument‘, :phpclass:‘DOMNodeList‘ e :phpclass:‘DOMNode‘:
$documento = new \DOMDocument();
$documento->loadXml(’<root><node /><node /></root>’);
$listaNodi = $documento->getElementsByTagName(’node’);
$nodo = $documento->getElementsByTagName(’node’)->item(0);
$crawler->addDocument($documento);
$crawler->addNodeList($listaNodi);
$crawler->addNodes(array($nodo));
$crawler->addNode($nodo);
$crawler->add($documento);
Supporto per i collegamenti e per i form
Per i collegamenti e i form, contenuti nell’albero DOM, è riservato un trattamento speciale.
Collegamenti Per trovare un collegamento tramite il suo nome (o un’immagine cliccabile tramite il suo attributo alt) si usa il metodo selectLink in un crawler esistente. La chiamata restituisce un’istanza di
Crawler contenente il/i solo/i collegamento/i selezionato/i. La chiamata link() restituisce l’oggetto speciale
Symfony\Component\DomCrawler\Link:
$linksCrawler = $crawler->selectLink(’Vai altrove...’);
$link = $linksCrawler->link();
// oppure, in una sola riga
$link = $crawler->selectLink(’Vai altrove...’)->link();
4.1. I componenti
445
Symfony2 documentation Documentation, Release 2
L’oggetto Symfony\Component\DomCrawler\Link ha diversi utili metodi per avere ulteriori informazioni
relative al collegamento selezionato:
// restituisce il valore di href
$href = $link->getRawUri();
// restituisce la URI che può essere utilizzata per effettuare nuove richieste
$uri = $link->getUri();
Il metodo getUri() è specialmente utile perché pulisce il valore di href e lo trasforma nel modo in cui
dovrebbe realmente essere processato. Ad esempio, un collegamento del tipo href="#foo" restituirà la URI
completa della pagina corrente con il suffisso #foo. Il valore restituito da getUri() è sempre una URI completa
sulla quale è possibile eseguire lavori.
I Form Un trattamento speciale è riservato anche ai form. È disponibile, in Crawler, un metodo
selectButton() che restituisce un’altro Crawler relativo al pulsante (input[type=submit],
input[type=image], o button) con il testo dato. Questo metodo è specialmente utile perché può essere usato per restituire un oggetto Symfony\Component\DomCrawler\Form che rappresenta il form all’interno
del quale il pulsante è definito:
$form = $crawler->selectButton(’Valida’)->form();
// o "riempie" i campi del form con dati
$form = $crawler->selectButton(’Valida’)->form(array(
’nome’ => ’Ryan’,
));
L’oggetto Symfony\Component\DomCrawler\Form ha molti utilissimi metodi che permettono di lavorare
con i form:
$uri = $form->getUri();
$metodo = $form->getMethod();
Il metodo :method:‘Symfony\\Component\\DomCrawler\\Form::getUri‘ fa più che restituire il mero attributo action del form. Se il metodo del form è GET, allora, imitando il comportamento del borwser, restituirà
l’attributo dell’azione seguito da una stringa di tutti i valori del form.
È possibile impostare e leggere virtualmente i valori nel form:
// imposta, internamente, i valori del form
$form->setValues(array(
’registrazione[nomeutente]’ => ’fandisymfony’,
’registrazione[termini]’
=> 1,
));
// restituisce un array di valori in un array "semplice", come in precedenza
$values = $form->getValues();
// restituisce i valori come li vedrebbe PHP con "registrazione" come array
$values = $form->getPhpValues();
Per lavorare con i campi multi-dimensionali:
<form>
<input name="multi[]" />
<input name="multi[]" />
<input name="multi[dimensionale]" />
</form>
È necessario specificare il nome pienamente qualificato del campo:
// Imposta un singolo campo
$form->setValue(’multi[0]’, ’valore’);
446
Chapter 4. Componenti
Symfony2 documentation Documentation, Release 2
// Imposta molteplici campi in una sola volta
$form->setValue(’multi’, array(
1
=> ’valore’,
’dimensionale’ => ’un altro valore’
));
Se questo è fantastico, il resto è anche meglio! L’oggetto Form permette di interagire con il form come se si
usasse il borwser, selezionando i valori dei radio, spuntando i checkbox e caricando file:
$form[’registrazione[nomeutente]’]->setValue(’fandisymfony’);
// cambia segno di spunta ad un checkbox
$form[’registrazione[termini]’]->tick();
$form[’registrazione[termini]’]->untick();
// seleziona un’opzione
$form[’registrazione[data_nascita][anno]’]->select(1984);
// seleziona diverse opzioni da una lista di opzioni o da una serie di checkbox
$form[’registrazione[interessi]’]->select(array(’symfony’, ’biscotti’));
// può anche imitare l’upload di un file
$form[’registrazione[foto]’]->upload(’/percorso/al/file/lucas.jpg’);
A cosa serve tutto questo? Se si stanno eseguendo i test interni, è possibile recuperare informazioni da tutti i form
esattamente come se fossero stati inviati utilizzando i valori PHP:
$valori = $form->getPhpValues();
$files = $form->getPhpFiles();
Se si utilizza un client HTTP esterno, è possibile usare il form per recuperare tutte le informazioni necessarie per
create una richiesta POST dal form:
$uri = $form->getUri();
$metodo = $form->getMethod();
$valori = $form->getValues();
$files = $form->getFiles();
// a questo punto si usa un qualche client HTTP e si inviano le informazioni
Un ottimo esempio di sistema integrato che utilizza tutte queste funzioni è Goutte. Goutte usa a pieno gli oggetti
Symfony Crawler e, con essi, può inviare i form direttamente:
use Goutte\Client;
// crea una richiesta ad un sito esterno
$client = new Client();
$crawler = $client->request(’GET’, ’https://github.com/login’);
// seleziona il form e riempie alcuni valori
$form = $crawler->selectButton(’Log in’)->form();
$form[’login’] = ’fandisymfony’;
$form[’password’] = ’unapassword’;
// invia il form
$crawler = $client->submit($form);
4.1.5 Il componente Finder
Il componente Finder cerca file e cartelle tramite un’interfaccia intuitiva e “fluida”.
4.1. I componenti
447
Symfony2 documentation Documentation, Release 2
Installazione
È possibile installare il componente in diversi modi:
• Utilizzando il repository ufficiale su Git (https://github.com/symfony/Finder);
• Installandolo via PEAR ( pear.symfony.com/Finder);
• Installandolo tramite Composer (symfony/finder su Packagist).
Utilizzo
La classe Symfony\Component\Finder\Finder trova i file e/o le cartelle:
use Symfony\Component\Finder\Finder;
$finder = new Finder();
$finder->files()->in(__DIR__);
foreach ($finder as $file) {
// Stampa il percorso assoluto
print $file->getRealpath()."\n";
// Stampa il percorso relativo del file, omettendo il nome del file stesso
print $file->getRelativePath()."\n";
// Stampa il percorso relativo del file
print $file->getRelativePathname()."\n";
}
$file è un’istanza di Symfony\Component\Finder\SplFileInfo la quale estende :phpclass:‘SplFileInfo‘ che mette a disposizione i metodi per poter lavorare con i percorsi relativi.
Il precedente codice stampa, ricorsivamente, i nomi di tutti i file della cartella corrente. La classe Finder implementa il concetto di interfaccia fluida, perciò tutti i metodi restituiscono un’istanza di Finder.
Tip: Un Finder è un’istanza di un Iterator PHP. Perciò, invece di dover iterare attraverso Finder con un ciclo
foreach, è possibile convertirlo in un array, tramite il metodo :phpfunction:‘iterator_to_array‘, o ottenere il
numero di oggetto in esso contenuti con :phpfunction:‘iterator_count‘.
Criteri
Posizione
La posizione è l’unico parametro obbligatorio. Indica al finder la cartella da utilizzare come base per la ricerca:
$finder->in(__DIR__);
Per
cercare
in
diverse
posizioni,
è
possibile
:method:‘Symfony\\Component\\Finder\\Finder::in‘:
concatenare
diverse
chiamate
a
$finder->files()->in(__DIR__)->in(’/altraparte’);
È possibile escludere cartelle dalla ricerca tramite il metodo :method:‘Symfony\\Component\\Finder\\Finder::exclude‘:
$finder->in(__DIR__)->exclude(’ruby’);
Visto che Finder utilizza gli iteratori di PHP, è possibile passargli qualsiasi URL che sia supportata dal protocollo:
$finder->in(’ftp://example.com/pub/’);
Funziona anche con flussi definiti dall’utente:
448
Chapter 4. Componenti
Symfony2 documentation Documentation, Release 2
use Symfony\Component\Finder\Finder;
$s3 = new \Zend_Service_Amazon_S3($chiave, $segreto);
$s3->registerStreamWrapper("s3");
$finder = new Finder();
$finder->name(’photos*’)->size(’< 100K’)->date(’since 1 hour ago’);
foreach ($finder->in(’s3://bucket-name’) as $file) {
// fare qualcosa
print $file->getFilename()."\n";
}
Note: Per approfondire l’argomento su come creare flussi personalizzati, si legga la documentazione degli stream.
File o cartelle
Il
comportamento
predefinito
di
Finder
è
quello
di
restituire
file
e
cartelle,
ma
grazie
ai
metodi
:method:‘Symfony\\Component\\Finder\\Finder::files‘
e
:method:‘Symfony\\Component\\Finder\\Finder::directories‘, è possibile raffinare i risultati:
$finder->files();
$finder->directories();
Per seguire i collegamenti, è possibile utilizzare il metodo followLinks():
$finder->files()->followLinks();
Normalmente l’iteratore ignorerà i file dei VCS più diffusi. È possibile modificare questo comportamento, grazie
al metodo ignoreVCS():
$finder->ignoreVCS(false);
Ordinamento
È possibile ordinare i risultati per nome o per tipo (prima le cartelle e poi i file):
$finder->sortByName();
$finder->sortByType();
Note: Si noti che i metodi sort*, per poter funzionare, richiedono tutti gli elementi ricercati. In caso di iteratori
molto grandi, l’ordinamento potrebbe risultare lento.
È anche possibile definire algoritmi di ordinamento personalizzati, grazie al metodo sort():
$sort = function (\SplFileInfo $a, \SplFileInfo $b)
{
return strcmp($a->getRealpath(), $b->getRealpath());
};
$finder->sort($sort);
4.1. I componenti
449
Symfony2 documentation Documentation, Release 2
Nomi dei file
È
possibile
eseguire
filtri
sui
nomi
:method:‘Symfony\\Component\\Finder\\Finder::name‘:
dei
file,
utilizzando
il
metodo
$finder->files()->name(’*.php’);
Il metodo name() accetta, come parametri, glob, stringhe o espressioni regolari:
$finder->files()->name(’/\.php$/’);
Il metodo notNames() viene invece usato per escludere i file che corrispondono allo schema:
$finder->files()->notName(’*.rb’);
Dimensione dei file
Per filtrare i file in base alla dimensione, si usa il metodo :method:‘Symfony\\Component\\Finder\\Finder::size‘:
$finder->files()->size(’< 1.5K’);
Si possono filtrare i file di dimensione compresa tra due valori, concatenando le chiamate:
$finder->files()->size(’>= 1K’)->size(’<= 2K’);
È possibile utilizzare uno qualsiasi dei seguenti operatori di confronto: >, >=, <, ‘<=’, ‘==’.
La dimensione può essere indicata usando l’indicazione in kilobyte (k, ki), megabyte (m, mi) o in gigabyte (g,
gi). Gli indicatori che terminano con i utilizzano l’appropriata versione 2**n, in accordo allo standard IEC
Data dei file
È possibile filtrare i file in base alla data
:method:‘Symfony\\Component\\Finder\\Finder::date‘:
dell’ultima
modifica,
con
il
metodo
$finder->date(’since yesterday’);
È possibile utilizzare uno qualsiasi dei seguenti operatori di confronto: >, >=, <, ‘<=’, ‘==’. È anche possibile
usare i sostantivi since o after come degli alias di >, e until o before come alias di <.
Il valore usato può essere una data qualsiasi tra quelle supportate dalla funzione strtotime.
Profondità della ricerca
Normalmente, Finder attraversa ricorsivamente tutte le cartelle. Per restringere la profondità dell’attraversamento,
si usa il metodo :method:‘Symfony\\Component\\Finder\\Finder::depth‘:
$finder->depth(’== 0’);
$finder->depth(’< 3’);
Filtri personalizzati
È possibile definire filtri personalizzati, grazie al metodo :method:‘Symfony\\Component\\Finder\\Finder::filter‘:
$filtro_personalizzato = function (\SplFileInfo $file)
{
if (strlen($file) > 10) {
return false;
450
Chapter 4. Componenti
Symfony2 documentation Documentation, Release 2
}
};
$finder->files()->filter($filtro_personalizzato);
Il metodo filter() prende una Closure come argomento. Per ogni file che corrisponde ai criteri, la Closure
viene chiamata passandogli il file come un’istanza di Symfony\Component\Finder\SplFileInfo. Il file
sarà escluso dal risultato della ricerca nel caso in cui la Closure restituisca false.
4.1.6 Il componente HttpFoundation
Il componente HttpFoundation definisce un livello orientato agli oggetti per le specifiche HTTP.
In PHP, la richiesta è rappresentata da alcune variabili globali ($_GET, $_POST, $_FILE, $_COOKIE,
$_SESSION...) e la risposta è generata da alcune funzioni (echo, header, setcookie, ...).
Il componente HttpFoundation di Symfony2 sostituisce queste variabili globali e queste funzioni di PHP con un
livello orientato agli oggetti.
Installazione
Si può installare il componente in molti modi diversi:
• Usare il repository ufficiale su Git (https://github.com/symfony/HttpFoundation);
• Installarlo via PEAR ( pear.symfony.com/HttpFoundation);
• Installarlo via Composer (symfony/http-foundation su Packagist).
Richiesta
Il modo più comune per creare una richiesta è basarla sulle variabili attuali di PHP, con
:method:‘Symfony\\Component\\HttpFoundation\\Request::createFromGlobals‘:
use Symfony\Component\HttpFoundation\Request;
$request = Request::createFromGlobals();
che è quasi equivalente al più verboso, ma anche più flessibile, :method:‘Symfony\\Component\\HttpFoundation\\Request::__con
$request = new Request($_GET, $_POST, array(), $_COOKIE, $_FILES, $_SERVER);
Accedere ai dati della richiesta
Un oggetto richiesta contiene informazioni sulla richiesta del client. Si può accedere a queste informazioni tramite
varie proprietà pubbliche:
• request: equivalente di $_POST;
• query: equivalente di $_GET ($request->query->get(’name’));
• cookies: equivalente di $_COOKIE;
• attributes: non ha equivalenti, è usato dall’applicazione per memorizzare alrti dati (vedere sotto)
• files: equivalente di $_FILE;
• server: equivalente di $_SERVER;
• headers:
quasi
equivalente
di
un
($request->headers->get(’Content-Type’)).
4.1. I componenti
sottinsieme
di
$_SERVER
451
Symfony2 documentation Documentation, Release 2
Ogni proprietà è un’istanza di Symfony\Component\HttpFoundation\ParameterBag (o di una sua
sotto-classe), che è una classe contenitore:
• request: Symfony\Component\HttpFoundation\ParameterBag;
• query: Symfony\Component\HttpFoundation\ParameterBag;
• cookies: Symfony\Component\HttpFoundation\ParameterBag;
• attributes: Symfony\Component\HttpFoundation\ParameterBag;
• files: Symfony\Component\HttpFoundation\FileBag;
• server: Symfony\Component\HttpFoundation\ServerBag;
• headers: Symfony\Component\HttpFoundation\HeaderBag.
Tutte le istanze di Symfony\Component\HttpFoundation\ParameterBag hanno metodi per recuperare e aggiornare i propri dati:
• :method:‘Symfony\\Component\\HttpFoundation\\ParameterBag::all‘: Restituisce i parametri;
• :method:‘Symfony\\Component\\HttpFoundation\\ParameterBag::keys‘:
parametri;
Restituisce le chiavi dei
• :method:‘Symfony\\Component\\HttpFoundation\\ParameterBag::replace‘: Sostituisce i parametri attuali con dei nuovi;
• :method:‘Symfony\\Component\\HttpFoundation\\ParameterBag::add‘: Aggiunge parametri;
• :method:‘Symfony\\Component\\HttpFoundation\\ParameterBag::get‘: Restituisce un parametro per
nome;
• :method:‘Symfony\\Component\\HttpFoundation\\ParameterBag::set‘:
nome;
Imposta un parametro per
• :method:‘Symfony\\Component\\HttpFoundation\\ParameterBag::has‘:
parametro è definito;
Restituisce true se il
• :method:‘Symfony\\Component\\HttpFoundation\\ParameterBag::remove‘: Rimuove un parametro.
La classe Symfony\Component\HttpFoundation\ParameterBag ha anche alcuni metodi per filtrare i
valori in entrata:
• :method:‘Symfony\\Component\\HttpFoundation\\Request::getAlpha‘: Restituisce i caratteri alfabetici
nel valore del parametro;
• :method:‘Symfony\\Component\\HttpFoundation\\Request::getAlnum‘: Restituisce i caratteri alfabetici e i numeri nel valore del parametro;
• :method:‘Symfony\\Component\\HttpFoundation\\Request::getDigits‘: Restituisce i numeri nel valore
del parametro;
• :method:‘Symfony\\Component\\HttpFoundation\\Request::getInt‘: Restituisce il valore del parametro
convertito in intero;
• :method:‘Symfony\\Component\\HttpFoundation\\Request::filter‘: Filtra il parametro, usando la funzione PHP filter_var().
Tutti i getter accettano tre parametri: il primo è il nome del parametro e il secondo è il valore predefinito, da
restituire se il parametro non esiste:
// la query string è ’?foo=bar’
$request->query->get(’foo’);
// restituisce bar
$request->query->get(’bar’);
// restituisce null
452
Chapter 4. Componenti
Symfony2 documentation Documentation, Release 2
$request->query->get(’bar’, ’bar’);
// restituisce ’bar’
Quando PHP importa la query della richiesta, gestisce i parametri della richiesta, come foo[bar]=bar, in modo
speciale, creando un array. In questo modo, si può richiedere il parametro foo e ottenere un array con un elemento
bar. A volte, però, si potrebbe volere il valore del nome “originale” del parametro: foo[bar]. Ciò è possibile con tutti i getter di ParameterBag, come :method:‘Symfony\\Component\\HttpFoundation\\Request::get‘,
tramite il terzo parametro:
// la query string è ’?foo[bar]=bar’
$request->query->get(’foo’);
// restituisce array(’bar’ => ’bar’)
$request->query->get(’foo[bar]’);
// restituisce null
$request->query->get(’foo[bar]’, null, true);
// restituisce ’bar’
Infine, ma non meno importante, si possono anche memorizzare dati aggiuntivi nella
richiesta,
grazie alla proprietà pubblica attributes,
che è anche un’istanza di
Symfony\Component\HttpFoundation\ParameterBag. La si usa soprattutto per allegare informazioni che appartengono alla richiesta e a cui si deve accedere in diversi punti della propria applicazione. Per
informazioni su come viene usata nel framework Symfony2, vedere saperne di più.
Identificare una richiesta
Nella propria applicazione, serve un modo per identificare una richiesta.
La maggior parte delle
volte, lo si fa tramite il “path info” della richiesta, a cui si può accedere tramite il metodo
:method:‘Symfony\\Component\\HttpFoundation\\Request::getPathInfo‘:
// per una richiesta a http://example.com/blog/index.php/post/hello-world
// path info è "/post/hello-world"
$request->getPathInfo();
Simulare una richiesta
Invece di creare una richiesta basata sulle variabili di PHP, si può anche simulare una richiesta:
$request = Request::create(’/hello-world’, ’GET’, array(’name’ => ’Fabien’));
Il metodo :method:‘Symfony\\Component\\HttpFoundation\\Request::create‘ crea una richiesta in base a path
info, un metodo e alcuni parametri (i parametri della query o quelli della richiesta, a seconda del metodo HTTP) e,
ovviamente, si possono forzare anche tutte le altre variabili (Symfony crea dei valori predefiniti adeguati per ogni
variabile globale di PHP).
In base a tale richiesta,
si possono forzare le variabili globali
:method:‘Symfony\\Component\\HttpFoundation\\Request::overrideGlobals‘:
di
PHP
tramite
$request->overrideGlobals();
Tip: Si può anche duplicare una query esistente, tramite :method:‘Symfony\\Component\\HttpFoundation\\Request::duplicate‘,
o cambiare molti parametri con una singola chiamata a :method:‘Symfony\\Component\\HttpFoundation\\Request::initialize‘.
4.1. I componenti
453
Symfony2 documentation Documentation, Release 2
Accedere alla sessione
Se si ha una sessione allegata alla richiesta, vi si può accedere tramite il metodo
:method:‘Symfony\\Component\\HttpFoundation\\Request::getSession‘.
Il
metodo
:method:‘Symfony\\Component\\HttpFoundation\\Request::hasPreviousSession‘ dice se la richiesta
contiene una sessione, che sia stata fatta partire in una delle richieste precedenti.
Accedere ad altri dati
La classe Request ha molti altri metodi, che si possono usare per accedere alle informazioni della richiesta. Si dia
uno sguardo alle API per maggiori informazioni.
Risposta
Un oggetto Symfony\Component\HttpFoundation\Response contiene tutte le informazioni che devono essere rimandate al client, per una data richiesta. Il costruttore accetta fino a tre parametri: il contenuto della
risposta, il codice di stato e un array di header HTTP:
use Symfony\Component\HttpFoundation\Response;
$response = new Response(’Contenuto’, 200, array(’content-type’ => ’text/html’));
Queste informazioni possono anche essere manipolate dopo la creazione di Response:
$response->setContent(’Ciao mondo’);
// l’attributo pubblico headers è un ResponseHeaderBag
$response->headers->set(’Content-Type’, ’text/plain’);
$response->setStatusCode(404);
Quando si imposta il Content-Type di Response, si può impostare il charset, ma è meglio impostarlo tramite
il metodo :method:‘Symfony\\Component\\HttpFoundation\\Response::setCharset‘:
$response->setCharset(’ISO-8859-1’);
Si noti che Symfony presume che le risposte siano codificate in UTF-8.
Inviare la risposta
Prima di inviare la risposta, ci si può assicurare che rispetti le specifiche HTTP, richiamando il metodo
:method:‘Symfony\\Component\\HttpFoundation\\Response::prepare‘:
$response->prepare($request);
Inviare la risposta al client è quindi semplice, basta richiamare :method:‘Symfony\\Component\\HttpFoundation\\Response::send
$response->send();
Impostare cookie
Si possono manipolare i cookie della risposta attraverso l’attributo pubblico headers:
use Symfony\Component\HttpFoundation\Cookie;
$response->headers->setCookie(new Cookie(’pippo’, ’pluto’));
454
Chapter 4. Componenti
Symfony2 documentation Documentation, Release 2
Il metodo :method:‘Symfony\\Component\\HttpFoundation\\ResponseHeaderBag::setCookie‘
un’istanza di Symfony\Component\HttpFoundation\Cookie come parametro.
accetta
Si può pulire un cookie tramite il metodo :method:‘Symfony\\Component\\HttpFoundation\\Response::clearCookie‘.
Gestire la cache HTTP
La classe Symfony\Component\HttpFoundation\Response ha un corposo insieme di metodi per manipolare gli header HTTP relativi alla cache:
• :method:‘Symfony\\Component\\HttpFoundation\\Response::setPublic‘;
• :method:‘Symfony\\Component\\HttpFoundation\\Response::setPrivate‘;
• :method:‘Symfony\\Component\\HttpFoundation\\Response::expire‘;
• :method:‘Symfony\\Component\\HttpFoundation\\Response::setExpires‘;
• :method:‘Symfony\\Component\\HttpFoundation\\Response::setMaxAge‘;
• :method:‘Symfony\\Component\\HttpFoundation\\Response::setSharedMaxAge‘;
• :method:‘Symfony\\Component\\HttpFoundation\\Response::setTtl‘;
• :method:‘Symfony\\Component\\HttpFoundation\\Response::setClientTtl‘;
• :method:‘Symfony\\Component\\HttpFoundation\\Response::setLastModified‘;
• :method:‘Symfony\\Component\\HttpFoundation\\Response::setEtag‘;
• :method:‘Symfony\\Component\\HttpFoundation\\Response::setVary‘;
Il metodo :method:‘Symfony\\Component\\HttpFoundation\\Response::setCache‘ può essere usato per impostare le informazioni di cache più comuni, con un’unica chiamata:
$response->setCache(array(
’etag’
=> ’abcdef’,
’last_modified’ => new \DateTime(),
’max_age’
=> 600,
’s_maxage’
=> 600,
’private’
=> false,
’public’
=> true,
));
Per
verificare
che
i
validatori
della
risposta
(ETag,
Last-Modified)
corrispondano a un valore condizionale specificato nella richiesta del client,
usare il metodo
:method:‘Symfony\\Component\\HttpFoundation\\Response::isNotModified‘:
if ($response->isNotModified($request)) {
$response->send();
}
Se la risposta non è stata modificata, imposta il codice di stato a 304 e rimuove il contenuto effettivo della risposta.
Rinviare l’utente
Per rinviare il client a un altro URL, si può usare la classe Symfony\Component\HttpFoundation\RedirectResponse:
use Symfony\Component\HttpFoundation\RedirectResponse;
$response = new RedirectResponse(’http://example.com/’);
4.1. I componenti
455
Symfony2 documentation Documentation, Release 2
Flusso di risposta
New in version 2.1: Il supporto per i flussi di risposte è stato aggiunto in Symfony 2.1. La classe
Symfony\Component\HttpFoundation\StreamedResponse consente di inviare flussi di risposte al
client. Il contenuto della risposta viene rappresentato da un callable PHP, invece che da una stringa:
use Symfony\Component\HttpFoundation\StreamedResponse;
$response = new StreamedResponse();
$response->setCallback(function () {
echo ’Ciao mondo’;
flush();
sleep(2);
echo ’Ciao mondo’;
flush();
});
$response->send();
Scaricare file
New in version 2.1: Il metodo makeDisposition è stato aggiunto in Symfony 2.1. Quando si carica un
file, occorre aggiungere un header Content-Disposition alla risposta. Sebbene la creazione di questo
header per scaricamenti di base sia facile, l’uso di nomi di file non ASCII è più complesso. Il metodo
:method:‘:Symfony\\Component\\HttpFoundation\\Response:makeDisposition‘ astrae l’ingrato compito dietro una semplice API:
use Symfony\\Component\\HttpFoundation\\ResponseHeaderBag;
$d = $response->headers->makeDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, ’foo.pdf’);
$response->headers->set(’Content-Disposition’, $d);
Sessione
TBD – Questa parte non è ancora stata scritta, perché probabilmente sarà presto rifattorizzata in Symfony 2.1.
4.1.7 Il componente Locale
Il componente Locale fornisce codice per gestire casi in cui manchi l’estensione intl. Inoltre,
estende l’implementazione di una classe nativa :phpclass:‘Locale‘ con diversi metodi.
Viene fornito un rimpiazzo per le seguenti funzioni e classi:
• :phpfunction:‘intl_is_failure()‘
• :phpfunction:‘intl_get_error_code()‘
• :phpfunction:‘intl_get_error_message()‘
• :phpclass:‘Collator‘
• :phpclass:‘IntlDateFormatter‘
• :phpclass:‘Locale‘
• :phpclass:‘NumberFormatter‘
Note: L’implementazione di stub supporta solo il locale en.
456
Chapter 4. Componenti
Symfony2 documentation Documentation, Release 2
Installazione
Si può installare il componente in molti modi diversi:
• Usare il repository ufficiale su Git (https://github.com/symfony/Locale);
• Installarlo via PEAR ( pear.symfony.com/Locale);
• Installarlo via Composer (symfony/locale su Packagist).
Uso
Tra i vantaggi presenti, è inclusa la richiesta di funzioni stub e l’aggiunta di classi stub all’autoloader.
Quando si usa il componente ClassLoader, il codice seguente basta a fornire l’estensione intl mancante:
if (!function_exists(’intl_get_error_code’)) {
require __DIR__.’/percorso/src/Symfony/Component/Locale/Resources/stubs/functions.php’;
$loader->registerPrefixFallbacks(array(__DIR__.’/percorso/src/Symfony/Component/Locale/Resourc
}
La classe Symfony\Component\Locale\Locale arricchisce la classe nativa :phpclass:‘Locale‘ con caratteristiche aggiuntive:
use Symfony\Component\Locale\Locale;
// Nomi dei paesi per un locale, o tutti i codici dei paesi
$countries = Locale::getDisplayCountries(’pl’);
$countryCodes = Locale::getCountries();
// Nomi delle lingue per un locale, o tutti i codici delle lingue
$languages = Locale::getDisplayLanguages(’fr’);
$languageCodes = Locale::getLanguages();
// Nomi dei locale per un dato codice, o tutti i codici dei locale
$locales = Locale::getDisplayLocales(’en’);
$localeCodes = Locale::getLocales();
// Versioni ICU
$icuVersion = Locale::getIcuVersion();
$icuDataVersion = Locale::getIcuDataVersion();
4.1.8 Il componente Process
Il componente Process esegue i comandi nei sotto-processi.
Installazione
Si può installare il componente in molti modi diversi:
• Usare il repository ufficiale su Git (https://github.com/symfony/Process);
• Installarlo via PEAR ( pear.symfony.com/Process);
• Installarlo via Composer (symfony/process su Packagist).
Uso
La classe Symfony\Component\Process\Process consente di eseguire un comando in un sotto-processo:
4.1. I componenti
457
Symfony2 documentation Documentation, Release 2
use Symfony\Component\Process\Process;
$process = new Process(’ls -lsa’);
$process->setTimeout(3600);
$process->run();
if (!$process->isSuccessful()) {
throw new RuntimeException($process->getErrorOutput());
}
print $process->getOutput();
Il metodo :method:‘Symfony\\Component\\Process\\Process::run‘ si prende cura delle sottili differenze tra le
varie piattaforme, durante l’esecuzione del comando.
Quando si esegue un comando che gira a lungo (come la sincronizzazione di file con un server remoto), si può dare un feedback all’utente finale in tempo reale, passando una funzione anonima al metodo
:method:‘Symfony\\Component\\Process\\Process::run‘:
use Symfony\Component\Process\Process;
$process = new Process(’ls -lsa’);
$process->run(function ($type, $buffer) {
if (’err’ === $type) {
echo ’ERR > ’.$buffer;
} else {
echo ’OUT > ’.$buffer;
}
});
Se si vuole eseguire del codice PHP in isolamento, usare invece PhpProcess:
use Symfony\Component\Process\PhpProcess;
$process = new PhpProcess(<<<EOF
<?php echo ’Ciao mondo’; ?>
EOF);
$process->run();
New in version 2.1: La classe ProcessBuilder è stata aggiunta nella 2.1.
Per far funzionare meglio il proprio codice su tutte le piattaforme, potrebbe essere preferibile usare la classe
Symfony\Component\Process\ProcessBuilder:
use Symfony\Component\Process\ProcessBuilder;
$builder = new ProcessBuilder(array(’ls’, ’-lsa’));
$builder->getProcess()->run();
4.1.9 Il componente Routing
Il componente Routing confronta una richiesta HTTP con un insieme di variabili di configurazione.
Installazione
È possibile installare il componente in diversi modi:
• Utilizzando il repository ufficiale su Git (https://github.com/symfony/Routing);
• Installandolo via PEAR ( pear.symfony.com/Routing);
• Installandolo via Composer (symfony/routing su Packagist).
458
Chapter 4. Componenti
Symfony2 documentation Documentation, Release 2
Utilizzo
Per poter usare un sistema di rotte di base, sono necessari tre elementi:
• Una Symfony\Component\Routing\RouteCollection, che contiene la definizione delle rotte
(un’istanza della classe Symfony\Component\Routing\Route)
• Un Symfony\Component\Routing\RequestContext, che contiene informazioni relative alla
richiesta
• Un Symfony\Component\Routing\Matcher\UrlMatcher, che associa la richiesta ad una singola rotta
Il seguente esempio assume che l’autoloader sia già stato configurato in modo tale da caricare il componente
Routing:
use
use
use
use
Symfony\Component\Routing\Matcher\UrlMatcher;
Symfony\Component\Routing\RequestContext;
Symfony\Component\Routing\RouteCollection;
Symfony\Component\Routing\Route;
$rotte = new RouteCollection();
$rotte->add(’nome_rotta’, new Route(’/pippo’, array(’controller’ => ’MioControllore’)));
$contesto = new RequestContext($_SERVER[’REQUEST_URI’]);
$abbinatore = new UrlMatcher($rotte, $contesto);
$parametri = $abbinatore->match( ’/pippo’ );
// array(’controller’ => ’MioControllore’, ’_route’ => ’nome_rotta’)
Note: Particolare attenzione va data al’utilizzo di $_SERVER[’REQUEST_URI’], perché potrebbe contenere
qualsiasi parametro della richiesta inserito nel’URL creando problemi con l’abbinamento alla rotta. Un semplice
modo per risolvere il problema è usare il componente HTTPFoundation come spiegato in below.
È
possibile
aggiungere
un
numero
qualsiasi
Symfony\Component\Routing\RouteCollection.
di
rotte
ad
una
classe
Il
metodo
:method:‘RouteCollection::add()<Symfony\\Component\\Routing\\RouteCollection::add>‘
accetta due parametri.
Il primo è il nome della rotta, il secondo è un oggetto
Symfony\Component\Routing\Route, il cui costruttore si aspetta di ricevere un percorso URL e
un array di variabili personalizzate. L’array di variabili personalizzate può essere qualsiasi cosa che abbia senso
per l’applicazione e viene restituito quando la rotta viene abbinata.
Se
non
viene
trovato
alcun
abbinamento
con
la
rotta
verrà
Symfony\Component\Routing\Exception\ResourceNotFoundException.
lanciata
una
Oltre al’array di variabili personalizzate, viene aggiunta la chiave _route che conterrà il nome della rotta abbinata.
Definire le rotte
La definizione completa di una rotta può contenere fino a quattro parti:
1. Lo schema dell’URL della rotta. È questo il valore con il quale si confronta l’URL passato a RequestContext. Può contenere diversi segnaposto (per esempio {segnaposto}) che possono abbinarsi a parti dinamiche
dell’URL.
2. Un array di valori base. Contiene un array di valori arbitrari che verranno restituiti quando la richiesta viene
abbinata alla rotta.
3. Un array di requisiti. Definisce i requisiti per i valori dei segnaposto in forma di espressione regolare.
4.1. I componenti
459
Symfony2 documentation Documentation, Release 2
4. Un array di opzioni. Questo array contiene configurazioni interne per le rotte e, solitamente, sono la parte di
cui meno ci si interessa.
Si prenda la seguente rotta, che combina diversi dei concetti esposti:
$route = new Route(
’/archivio/{mese}’, // pattern per la rotta
array(’controller’ => ’mostraArchivio’), // valori predefiniti
array(’mese’ => ’[0-9]{4}-[0-9]{2}’), // requisiti
array() // opzioni
);
// ...
$parametri = $abbinatore->match(’/archivio/2012-01’);
// array(’controller’ => ’mostraArchivio’, ’mese’ => 2012-01’’, ’_route’ => ’...’)
$parametri = $abbinatore->match(’/archivio/pippo’);
// lancia una ResourceNotFoundException
In questo caso la rotta viene trovata con /archivio/2012/01, perché il segnaposto {mese} è associabile
alla espressione regolare definita. Invece, per /archivio/pippo, non verrà trovata nessuna corrispondenza
perché “pippo” non rispetta i requisiti del segnaposto.
Oltre ai requisiti definiti con le espressioni regolari, è possibile definire due requisiti speciali:
• _method richiede che il metodo HTTP utilizzato sia quello definito (HEAD, GET, POST, ...)
• _scheme richiede che lo schema HTTP utilizzato sia quello definito (http, https)
La rotta seguente, per esempio, accetterà le sole richieste a /pippo che siano eseguite con metodo POST e con
connessione sicura:
$rotta = new Route(’/pippo’, array(’_method’ => ’post’, ’_scheme’ => ’https’ ));
Tip: Per creare una corrispondenza che trovi tutte le url che inizino con un determinato percorso e terminino con
un suffisso arbitrario, è possibile usare la seguente definizione:
$rotta = new Route(’/inizio/{suffisso}’, array(’suffisso’ => ’’), array(’suffisso’ => ’.*’));
Usare i prefissi
È possibile aggiungere sia rotte che nuove istanze di Symfony\Component\Routing\RouteCollection
ad un’altra collezione. In questo modo si possono creare alberi di rotte. Inoltre è possibile definire dei prefissi,
requisiti predefiniti e opzioni predefinite per tutte le rotte di un sotto albero:
$radiceCollezione = new RouteCollection();
$subCollezione = new RouteCollection();
$subCollezione->add( /*...*/ );
$subCollezione->add( /*...*/ );
$radiceCollezione->addCollection($subCollezione, ’/prefisso’, array(’_scheme’ => ’https’));
Configurare i parametri della richiesta
Symfony\Component\Routing\RequestContext fornisce informazioni relative alla richiesta attuale.
Con questa classe, tramite il suo costruttore, è possibile definire tutti i parametri di una richiesta HTTP:
460
Chapter 4. Componenti
Symfony2 documentation Documentation, Release 2
public function __construct($baseUrl = ’’, $method = ’GET’, $host = ’localhost’, $scheme = ’http’,
È
possibile
passare
i
valori
della
variabile
$_SERVER
per
popolare
Symfony\Component\Routing\RequestContext. Ma se si utilizza il componente HttpFoundation, è possibile usarne la classe Symfony\Component\HttpFoundation\Request per riempire la
Symfony\Component\Routing\RequestContext con una scorciatoia:
use Symfony\Component\HttpFoundation\Request;
$context = new RequestContext();
$context->fromRequest(Request::createFromGlobals());
Generare un URL
Mentre la classe Symfony\Component\Routing\Matcher\UrlMatcher cerca di trovare una rotta che
sia adeguata ad una determinata richiesta, è anche possibile creare degli URL a partire da una determinata rotta:
use Symfony\Component\Routing\Generator\UrlGenerator;
$rotte = new RouteCollection();
$rotte->add(’mostra_articolo’, new Route(’/mostra/{slug}’));
$contesto = new RequestContext($_SERVER[’REQUEST_URI’]);
$generatore = new UrlGenerator($rotte, $contesto);
$url = $generatore->generate(’mostra_articolo’, array(
’slug’ => ’articolo-sul-mio-blog’
));
// /mostra/articolo-sul-mio-blog
Note: Se fosse stato definito il requisito dello _scheme, verrebbe generata un URL assoluto nel caso in cui lo
schema corrente Symfony\Component\Routing\RequestContext non fosse coerente con i requisiti.
Caricare le rotte da un file
Si è visto come sia semplice aggiungere rotte ad una collezione direttamente tramite PHP. Ma è anche possibile
caricare le rotte da diversi tipi di file differenti.
Il componente del Routing contiene diverse classi di caricamento, ognuna delle quali fornisce l’abilità di
caricare collezioni di definizioni di rotte da file esterni di diverso formato. Ogni caricatore si aspetta
di ricevere un’istanza di Symfony\Component\Config\FileLocator come argomento del costruttore. Si può usare il Symfony\Component\Config\FileLocator per definire una array di percorsi
nei quali il caricatore andrà a cercare i file richiesti. Se il file viene trova, il caricatore restituisce una
Symfony\Component\Routing\RouteCollection.
Si utilizza il caricatore YamlFileLoader, allora la definizione delle rotte sarà simile alla seguente:
# rotte.yml
rotta1:
pattern: /pippo
defaults: { controller: ’MioControllore::pippoAction’ }
rotta2:
pattern: /pippo/pluto
defaults: { controller: ’MioControllore::pippoPlutoAction’ }
Per caricare questo file, è possibile usare il seguente codice. Si presume che il file rotte.yml sia nella stessa
cartella in cui si trova i codice:
4.1. I componenti
461
Symfony2 documentation Documentation, Release 2
use Symfony\Component\Config\FileLocator;
use Symfony\Component\Routing\Loader\YamlFileLoader;
// controlla al’interno della cartella *corrente*
$cercatore = new FileLocator(array(__DIR__));
$caricatore = new YamlFileLoader($cercatore);
$collezione = $caricatore->load(’rotte.yml’);
Oltre a Symfony\Component\Routing\Loader\YamlFileLoader ci sono altri due caricatori che funzionano nello stesso modo:
• Symfony\Component\Routing\Loader\XmlFileLoader
• Symfony\Component\Routing\Loader\PhpFileLoader
Se si usa Symfony\Component\Routing\Loader\PhpFileLoader sarà necessario fornire il nome del
file php che restituirà una Symfony\Component\Routing\RouteCollection:
// FornitoreDiRotte.php
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;
$collezione = new RouteCollection();
$collezione->add(’nome_rotta’, new Route(’/pippo’, array(’controller’ => ’ControlloreEsempio’)));
// ...
return $collezione;
Rotte e Closure Esiste anche un Symfony\Component\Routing\Loader\ClosureLoader, il quale
chiama una closure e ne utilizza il risultato come una Symfony\Component\Routing\RouteCollection:
use Symfony\Component\Routing\Loader\ClosureLoader;
$closure = function() {
return new RouteCollection();
};
$caricatore = new ClosureLoader();
$collezione = $caricatore->load($closure);
Rotte e annotazioni Ultime, ma non meno importanti sono Symfony\Component\Routing\Loader\AnnotationDire
e Symfony\Component\Routing\Loader\AnnotationFileLoader usate per caricare le rotte a
partire dalle annotazioni delle classi. Questo articolo non tratterà i dettagli di queste classi.
Il router tutto-in-uno
La classe Symfony\Component\Routing\Router è un pacchetto tutto-in-uno che permette i usare rapidamente il componente Routing. Il costruttore si aspetta di ricevere l’istanza di un caricatore, un percorso per la
definizione della rotta principale e alcuni altri parametri:
public function __construct(LoaderInterface $loader, $resource, array $options = array(), RequestC
Tramite l’opzione cache_dir è possibile abilitare la cache delle rotte (cioè se si fornisce un percorso) o disabilitarla (se viene configurata a null). La cache è realizzata automaticamente nello sfondo, qualora la si volesse
utilizzare. Un semplice esempio di come sia fatta la classe Symfony\Component\Routing\Router è riportato di seguito:
$cercatore = new FileLocator(array(__DIR__));
$contestoRichiesta = new RequestContext($_SERVER[’REQUEST_URI’]);
462
Chapter 4. Componenti
Symfony2 documentation Documentation, Release 2
$router = new Router(
new YamlFileLoader($cercatore),
"rotte.yml",
array(’cache_dir’ => __DIR__.’/cache’),
$contestoRichiesta,
);
$router->match(’/pippo/pluto’);
Note: Se si utilizza la cache, il componente Routing compilerà nuove classi che saranno salvate in cache_dir.
Questo significa che lo script deve avere i permessi di scrittura nella cartella indicata.
4.1.10 Il componente YAML
Il componente YAML carica ed esporta file YAML.
Che cos’è?
Il componente YAML di Symfony2 analizza stringhe YAML da convertire in array PHP. È anche in grado di
convertire array PHP in stringhe YAML.
YAML, YAML Ain’t Markup Language, è uno standard amichevole di serializzazione di dati per tutti i linguaggi
di programmazione. YAML è un ottimo formato per i file di configurazione. I file YAML sono espressivi quanto
i file XML e leggibili quanto i file INI.
Il componente YAML di Symfony2 implementa la versione 1.2. della specifica.
Installazione
Si può installare il componente in molti modi diversi:
• Usare il repository ufficiale su Git (https://github.com/symfony/Yaml);
• Installarlo via PEAR ( pear.symfony.com/Yaml);
• Installarlo via Composer (symfony/yaml su Packagist).
Perché?
Veloce
Uno degli scopi di YAML è trovare il giusto rapporto tra velocità e caratteristiche. Supporta proprio la caratteristica
necessaria per gestire i file di configurazione.
Analizzatore reale
Dispone di un analizzatore reale, capace di analizzare un grande sotto-insieme della specifica YAML, per tutte
le necessità di configurazione. Significa anche che l’analizzatore è molto robusto, facile da capire e facile da
estendere.
Messaggi di errore chiari
Ogni volta che si ha un problema di sintassi con un proprio file YAML, la libreria mostra un utile messaggio
di errore, con il nome del file e il numero di riga in cui il problema si è verificato. Questo facilita parecchio le
operazioni di debug.
4.1. I componenti
463
Symfony2 documentation Documentation, Release 2
Supporto per l’esportazione
È anche in grado di esportare array PHP in YAML con supporto agli oggetti e configurazione in linea per output
eleganti.
Tipi supportati
Supporta la maggior parte dei tipi di YAML, come date, interi, ottali, booleani e molto altro...
Pieno supporto alla fusione di chiavi
Pieno supporto per riferimenti, alias e piena fusione di chiavi. Non occorre ripetersi usando riferimenti a bit
comuni di configurazione.
Usare il componente YAML di Symfony2
Il componente YAML di Symfony2 è molto semplice e consiste di due classi principali: una analizza le
stringhe YAML (Symfony\Component\Yaml\Parser) e l’altra esporta un array PHP in una stringa YAML
(Symfony\Component\Yaml\Dumper).
Sopra queste classi, la classe Symfony\Component\Yaml\Yaml funge da involucro leggero, il che semplifica
gli usi più comuni.
Leggere file YAML
Il metodo :method:‘Symfony\\Component\\Yaml\\Parser::parse‘ analizza una stringa YAML e la converte in
un array PHP:
use Symfony\Component\Yaml\Parser;
$yaml = new Parser();
$value = $yaml->parse(file_get_contents(’/percorso/del/file.yml’));
Se
si
verifica
un
errore
durante
l’analizi,
l’analizzatore
lancia
un’eccezione
Symfony\Component\Yaml\Exception\ParseException, che indica il tipo di errore e la riga
della stringa YAML originale in cui l’errore si è verificato:
use Symfony\Component\Yaml\Exception\ParseException;
try {
$value = $yaml->parse(file_get_contents(’/percorso/del/file.yml’));
} catch (ParseException $e) {
printf("Impossibile analizzare la stringa YAML: %s", $e->getMessage());
}
Tip: Poiché l’analizzatore è rientrante, si può usare lo stesso oggetto analizzatore per caricare diverse stringhe
YAML.
Quando si carica un file YAML, a volte
:method:‘Symfony\\Component\\Yaml\\Yaml::parse‘:
è
meglio
usare
il
metodo
involucro
use Symfony\Component\Yaml\Yaml;
$loader = Yaml::parse(’/percorso/del/file.yml’);
464
Chapter 4. Componenti
Symfony2 documentation Documentation, Release 2
Il metodo statico :method:‘Symfony\\Component\\Yaml\\Yaml::parse‘ prende una stringa YAML o un file contenente YAML. Internamente, richiama il metodo :method:‘Symfony\\Component\\Yaml\\Parser::parse‘, ma
con alcuni bonus aggiuntivi:
• Esegue il file YAML come se fosse un file PHP, quindi si possono inserire comandi PHP nei file YAML;
• Quando un file non può essere analizzato, aggiunge automaticamente il nome del file al messaggio di errore,
semplificando il debug, quando l’applicazione sta caricando numerosi file YAML.
Scrivere file YAML
Il metodo :method:‘Symfony\\Component\\Yaml\\Dumper::dump‘ esporta un array PHP nella corrispondente
rappresentazione YAML:
use Symfony\Component\Yaml\Dumper;
$array = array(’foo’ => ’bar’, ’bar’ => array(’foo’ => ’bar’, ’bar’ => ’baz’));
$dumper = new Dumper();
$yaml = $dumper->dump($array);
file_put_contents(’/percorso/del/file.yml’, $yaml);
Note: Ovviamente, l’esportatore YAML non è in grado di esportare risorse. Inoltre, anche se l’esportatore è in
grado di esportare oggetti PHP, la caratteristica è considerata come non supportata.
Se
si
verifica
un
errore
durante
l’esportazione,
Symfony\Component\Yaml\Exception\DumpException.
l’esportatore
lancia
un’eccezione
Se si ha bisogno di esportare un solo array, si può usare come scorciatoia il metodo statico
:method:‘Symfony\\Component\\Yaml\\Yaml::dump‘:
use Symfony\Component\Yaml\Yaml;
$yaml = Yaml::dump($array, $inline);
Il formato YAML supporta due tipi di rappresentazioni di array, quello espanso e quello in linea. Per impostazione
predefinita, l’esportatore usa la rappresentazione in linea:
{ foo: bar, bar: { foo: bar, bar: baz } }
Il secondo parametro del metodo :method:‘Symfony\\Component\\Yaml\\Dumper::dump‘ personalizza il livello in cui l’output cambia dalla rappresentazione espansa a quella in linea:
echo $dumper->dump($array, 1);
foo: bar
bar: { foo: bar, bar: baz }
echo $dumper->dump($array, 2);
foo: bar
bar:
foo: bar
bar: baz
Il formato YAML
Secondo il sito ufficiale di YAML, YAML è “uno standard amichevole di serializzazione dei dati per tutti i linguaggi di programmazione”.
4.1. I componenti
465
Symfony2 documentation Documentation, Release 2
Anche se il formato YAML può descrivere strutture di dati annidate in modo complesso, questo capitolo descrive
solo l’insieme minimo di caratteristiche per usare YAML come formato per i file di configurazione.
YAML è un semplice linguaggio che descrive dati. Come PHP, ha una sintassi per tipi semplici, come stringhe,
booleani, numeri a virgola mobile o interi. Ma, diversamente da PHP, distingue tra array (sequenze) e hash
(mappature).
Scalari
La sintassi per gli scalari è simile a quella di PHP.
Stringhe
Una stringain YAML
’Una string in YAML tra apici singoli’
Tip: In una stringa tra apici singoli, un apice singolo ’ va raddoppiato:
’Un apice singolo ’’ in una stringa tra apici singoli’
"Una string in YAML tra apici doppi\n"
Gli apici sono utili quando una stringa inizia o finisce con uno o più spazi significativi.
Tip: Lo stile a doppi apici fornisce un modo per esprimere stringhe arbitrarie, ma usando sequenze di escape con
\ escape. È molto utile quando occorre inserire \n o un carattere unicode in una stringa.
Quando una stringa contiene degli a capo, si può usare lo stile letterale, indicato dalla barra verticale (|), per
indicare che la stringa si estende su diverse righe. Nei letterali, gli a capo sono preservati:
|
\/ /| |\/| |
/ / | | | |__
In alternativa, le stringhe possono essere scritte con lo stile avvolto, denotato da >, in cui gli a capo sono sostituiti
da uno spazio:
>
Questa è una frase molto lunga
che si espande per diverse righe in YAML
ma che sarà resa come una stringa
senza rimandi a capo.
Note: Si notino i due spazi prima di ogni riga nell’esempio qui sopra. Non appariranno nella stringa PHP
risultante.
Numeri
# un intero
12
# un ottale
014
# un esadecimale
0xC
466
Chapter 4. Componenti
Symfony2 documentation Documentation, Release 2
# un numero a virgola mobile
13.4
# un esponenziale
1.2e+34
# infinito
.inf
Null Null in YAML può essere espresso con null o con ~.
Booleani I booleani in YAML sono espressi con true e false.
Date YAML usa lo standard ISO-8601 per esprimere le date:
2001-12-14t21:59:43.10-05:00
# data semplice
2002-12-14
Insiemi
Un file YAML è usato raramente per descrivere semplici scalari. La maggior parte delle volte, descrive un insieme. Un insieme può essere una sequenza o una mappatura di elementi. Sia le sequenze che le mappature sono
convertite in array PHP.
Le sequenze usano un trattino, seguito da uno spazio (‘‘- ‘‘):
- PHP
- Perl
- Python
Il file YAML qui sopra equivale al seguente codice PHP:
array(’PHP’, ’Perl’, ’Python’);
Le mappature usano un due punti, seguito da uno spazio (‘‘: ‘‘) per marcare ogni coppia chiave/valore:
PHP: 5.2
MySQL: 5.1
Apache: 2.2.20
che equivale a questo codice PHP:
array(’PHP’ => 5.2, ’MySQL’ => 5.1, ’Apache’ => ’2.2.20’);
Note: In una mappatura, una chiave può essere un qualsiasi scalare valido.
Il numero di spazi tra i due punti e il valore non è significativo:
PHP:
5.2
MySQL: 5.1
Apache: 2.2.20
YAML usa un’indentazione con uno o più spazi per descrivere insiemi annidati:
4.1. I componenti
467
Symfony2 documentation Documentation, Release 2
"symfony 1.0":
PHP:
5.0
Propel: 1.2
"symfony 1.2":
PHP:
5.2
Propel: 1.3
Lo YAML qui sopra equivale al seguente codice PHP:
array(
’symfony 1.0’
’PHP’
=>
’Propel’ =>
),
’symfony 1.2’
’PHP’
=>
’Propel’ =>
),
);
=> array(
5.0,
1.2,
=> array(
5.2,
1.3,
C’è una cosa importante da ricordare quando si usano le indentazioni in un file YAML: le indentazioni devono
essere fatte con uno o più spazi, ma mai con tabulazioni.
Si possono annidare sequenze e mappature a volontà:
’Capitolo 1’:
- Introduzione
- Tipi di eventi
’Capitolo 2’:
- Introduzione
- Helper
YAML può anche usare stili fluenti per gli insiemi, usando indicatori espliciti invece che le intendantazioni, per
denotare il livello.
Una sequenza può essere scritta come lista separata da virgole in parentesi quadre ([]):
[PHP, Perl, Python]
Una mappatura può essere scritta come lista separata da virgole di chiavi/valori tra parentesi graffe ({}):
{ PHP: 5.2, MySQL: 5.1, Apache: 2.2.20 }
Si possono mescolare gli stili, per ottenere una migliore leggibilità:
’Chapter 1’: [Introduzione, Tipi di eventi]
’Chapter 2’: [Introduzione, Helper]
"symfony 1.0": { PHP: 5.0, Propel: 1.2 }
"symfony 1.2": { PHP: 5.2, Propel: 1.3 }
Commenti
Si possono aggiungere commenti in YAML, usando come prefisso un cancelletto (#):
# Commento su una riga
"symfony 1.0": { PHP: 5.0, Propel: 1.2 } # Commento a fine riga
"symfony 1.2": { PHP: 5.2, Propel: 1.3 }
Note: I commenti sono semplicemente ignorati dall’analizzatore YAML e non necessitano di indentazione in
base al livello di annidamento di un insieme.
468
Chapter 4. Componenti
Symfony2 documentation Documentation, Release 2
Leggere la documentazione dei componenti.
4.1. I componenti
469
Symfony2 documentation Documentation, Release 2
470
Chapter 4. Componenti
CHAPTER
FIVE
DOCUMENTI DI RIFERIMENTO
Trovare rapidamente le risposte, con i documenti di riferimento:
5.1 Documenti di riferimento
5.1.1 Configurazione di FrameworkBundle (“framework”)
Questo riferimento è ancora provvisorio. Dovrebbe essere accurato, ma non sono pienamente coperate tutte le
opzioni.
FrameworkBundle contiene la maggior parte delle funzionalità di base del framework e può essere configurato
sotto la chiave framework nella configurazione della propria applicazione. Include impostazioni relative a
sessioni, traduzione, form, validazione, rotte e altro.
Configurazione
• charset
• secret
• ide
• test
• form
– enabled
• csrf_protection
– enabled
– field_name
• ‘session‘_
– lifetime
• ‘templating‘_
– assets_base_urls
– assets_version
– assets_version_format
471
Symfony2 documentation Documentation, Release 2
charset
tipo: stringa predefinito: UTF-8
Il set di caratteri usato nel framework.
kernel.charset.
Diventa il parametro del contenitore di servizi di nome
secret
tipo: stringa obbligatorio
Una stringa che dovrebbe essere univoca nella propria applicaizone. In pratica, è usta per generare il token antiCSRF, ma potrebbe essere usata in ogni altro contesto in cui è utili avere una stringa univoca. Diventa il parametro
del contenitore di servizi di nome kernel.secret.
ide
tipo: stringa predefinito: null
Se si usa un IDE, come TextMate o Mac Vim, allora Symfony può cambiare tutti i percorsi del file nei messaggi
di eccezione in collegamenti, che apriranno i file nel proprio IDE.
Se si usa TextMate o Mac Vim, si possono usare semplicemente uno dei seguenti valori:
• textmate
• macvim
Si può anche specificare una stringa con un collegamento personalizzato. Se lo si fa, tutti i simboli percentuale
(%) devono essere raddoppiati, per escape. Per esempio, la stringa completa per TextMate sarebbe come questa:
framework:
ide: "txmt://open?url=file://%%f&line=%%l"
Ovviamente, poiché ogni sviluppatore usa un IDE diverso, è meglio impostarlo a livello di sistema. Lo si può fare
impostando il valore xdebug.file_link_format di php.ini alla stringa del collegamento. Se questo valore
di configurazione è impostato, non occorre specificare l’opzione ide.
test
tipo: booleano
Se questo parametro di configurazione è presente e non è false, saranno caricati i servizi correlati ai test della
propria applicazione (p.e. test.client). Questa impostazione dovrebbe essere presete nel proprio ambiente
test (solitamente tramite app/config/config_test.yml). Per maggiori informazioni, vedere Test.
form
csrf_protection
sessioni
lifetime
tipo: integer predefinito: 86400
Determina il tempo di scadeza della sessione, in secondi.
472
Chapter 5. Documenti di riferimento
Symfony2 documentation Documentation, Release 2
template
assets_base_urls
predefinito: { http:
[], https:
[] }
Questa opzione consente di definire URL di base da usare per i riferimenti alle risorse nelle pagine http e https.
Si può fornire un valore stringa al posto di un array a elementi singoli. Se si forniscono più URL base, Symfony2
ne sceglierà una dall’elenco ogni volta che genera il percorso di una risorsa.
Per praticità, assets_base_urls può essere impostata direttamente con una stringa o array di stringhe, che
saranno automaticamente organizzate in liste di URL base per le richieste http e https. Se un URL inizia con
https:// o è protocol-relative (cioè inizia con // ), sarà aggiunto a entrambe le liste. Gli URL che iniziano con
http:// saranno aggiunti solo alla lista http. New in version 2.1.
assets_version
tipo: stringa
Questa opzione è usata per spaccare le risorse in cache, aggiungendo globalmente un parametro di query a tutti
i percorsi delle risorse (p.e. /images/logo.png?v2). Si applica solo alle risorse rese tramite la funzione
asset di Twig (o al suo equivalente PHP), come pure alle risorse rese con Assetic.
Per esempio, si supponga di avere il seguente:
• Twig
<img src="{{ asset(’images/logo.png’) }}" alt="Symfony!" />
• PHP
<img src="<?php echo $view[’assets’]->getUrl(’images/logo.png’) ?>" alt="Symfony!" />
Per impostazione predefinita, renderà un percorso alla propria immagine, come /images/logo.png. Ora,
attivare l’opzione assets_version:
• YAML
# app/config/config.yml
framework:
# ...
templating: { engines: [’twig’], assets_version: v2 }
• XML
<!-- app/config/config.xml -->
<framework:templating assets-version="v2">
<framework:engine id="twig" />
</framework:templating>
• PHP
// app/config/config.php
$container->loadFromExtension(’framework’, array(
// ...
’templating’
=> array(
’engines’ => array(’twig’),
’assets_version’ => ’v2’,
),
));
Ora, la stessa risora sarà resa come /images/logo.png?v2. Se si usa questa caratteristica, si deve incrementare a mano il valore di assets_version, prima di ogni deploy, in modo che il parametro della query
cambi.
Si può anche contollare il funzionamento della stringa della query, tramite l’opzione assets_version_format.
5.1. Documenti di riferimento
473
Symfony2 documentation Documentation, Release 2
assets_version_format
tipo: stringa predefinito: %%s?%%s
Specifica uno schema per sprintf(), usato con l’opzione assets_version per costruire il percorso della risorsa. Per
impostazione predefinita, lo schema aggiunge la versione della risorsa alla stringa della query. Per esempio, se
assets_version_format è impostato a %%s?version=%%s e assets_version è impostato a 5, il
percorso della risorsa sarà /images/logo.png?version=5.
Note: Tutti i simboli percentuale (%) nel formato devono essere raddoppiati per escape. Senza escape, i valori
sarebbero inavvertitamente interpretati come I parametri del servizio.
Tip: Alcuni CDN non sopportano la spaccatura della cache tramie stringa della query, quindi si rende necessario
l’inserimento della versione nel vero percorso della risorsa. Fortunatamente, assets_version_format non
è limitato alla produzionoe di stringhe di query con versioni.
Lo schema riceve il percorso originale della risorsa e la versione come primo e secondo parametro,
rispettivamente. Poiché il percorso della risorsa è un parametro, non possiamo modificarlo al volo (p.e.
/images/logo-v5.png). Tuttavia, possiamo aggiungere un prefisso al percorso della risorsa, usando uno
schema version-%%2$s/%%1$s, che risulta nel percorso version-5/images/logo.png.
Si possono quindi usare le riscritture degli URL, per togliere il prefissod con la versione prima di servire la risorsa.
In alternativa, si possono copiare le risorse nel percorso appropriato con la versione, come parte del processo di
deploy, e non usare la riscrittura degli URL. L’ultima opzione è utile se si vuole che le vecchie versioni delle
risorse restino disponibili nei loro URL originari.
Configurazione predefinita completa
• YAML
framework:
# general configuration
charset:
~
secret:
~ # Required
ide:
~
test:
~
default_locale:
en
trust_proxy_headers: false
# form configuration
form:
enabled:
csrf_protection:
enabled:
field_name:
# esi configuration
esi:
enabled:
true
true
_token
true
# profiler configuration
profiler:
only_exceptions:
false
only_master_requests: false
dsn:
sqlite:%kernel.cache_dir%/profiler.db
username:
password:
lifetime:
86400
matcher:
ip:
~
474
Chapter 5. Documenti di riferimento
Symfony2 documentation Documentation, Release 2
path:
service:
~
~
# router configuration
router:
resource:
type:
http_port:
https_port:
~ # Required
~
80
443
# session configuration
session:
auto_start:
storage_id:
name:
lifetime:
path:
domain:
secure:
httponly:
~
session.storage.native
~
86400
~
~
~
~
# templating configuration
templating:
assets_version:
~
assets_version_format: "%%s?%%s"
assets_base_urls:
http:
[]
ssl:
[]
cache:
~
engines:
# Required
form:
resources:
[FrameworkBundle:Form]
# Example:
- twig
loaders:
packages:
[]
# Prototype
name:
version:
version_format:
base_urls:
http:
ssl:
~
~
[]
[]
# translator configuration
translator:
enabled:
true
fallback:
en
# validation configuration
validation:
enabled:
true
cache:
~
enable_annotations:
false
# annotation configuration
annotations:
cache:
file
file_cache_dir:
%kernel.cache_dir%/annotations
debug:
true
5.1. Documenti di riferimento
475
Symfony2 documentation Documentation, Release 2
5.1.2 Riferimento configurazione AsseticBundle
Configurazione predefinita completa
• YAML
assetic:
debug:
use_controller:
read_from:
write_to:
java:
node:
sass:
bundles:
#
-
true
true
%kernel.root_dir%/../web
%assetic.read_from%
/usr/bin/java
/usr/bin/node
/usr/bin/sass
Defaults (all currently registered bundles):
FrameworkBundle
SecurityBundle
TwigBundle
MonologBundle
SwiftmailerBundle
DoctrineBundle
AsseticBundle
...
assets:
# Prototype
name:
inputs:
filters:
options:
[]
[]
# Prototype
name:
[]
filters:
# Prototype
name:
twig:
functions:
[]
# Prototype
name:
[]
5.1.3 Riferimento configurazione
• YAML
doctrine:
dbal:
default_connection:
connections:
default:
dbname:
host:
port:
user:
password:
driver:
476
default
database
localhost
1234
user
secret
pdo_mysql
Chapter 5. Documenti di riferimento
Symfony2 documentation Documentation, Release 2
driver_class:
MyNamespace\MyDriverImpl
options:
foo: bar
path:
%kernel.data_dir%/data.sqlite
memory:
true
unix_socket:
/tmp/mysql.sock
wrapper_class:
MyDoctrineDbalConnectionWrapper
charset:
UTF8
logging:
%kernel.debug%
platform_service:
MyOwnDatabasePlatformService
mapping_types:
enum: string
conn1:
# ...
types:
custom: Acme\HelloBundle\MyCustomType
orm:
auto_generate_proxy_classes:
false
proxy_namespace:
Proxies
proxy_dir:
%kernel.cache_dir%/doctrine/orm/Proxies
default_entity_manager:
default # The first defined is used if not set
entity_managers:
default:
# The name of a DBAL connection (the one marked as default is used if not set
connection:
conn1
mappings: # Required
AcmeHelloBundle: ~
class_metadata_factory_name:
Doctrine\ORM\Mapping\ClassMetadataFactory
# All cache drivers have to be array, apc, xcache or memcache
metadata_cache_driver:
array
query_cache_driver:
array
result_cache_driver:
type:
memcache
host:
localhost
port:
11211
instance_class: Memcache
class:
Doctrine\Common\Cache\MemcacheCache
dql:
string_functions:
test_string: Acme\HelloBundle\DQL\StringFunction
numeric_functions:
test_numeric: Acme\HelloBundle\DQL\NumericFunction
datetime_functions:
test_datetime: Acme\HelloBundle\DQL\DatetimeFunction
hydrators:
custom: Acme\HelloBundle\Hydrators\CustomHydrator
em2:
# ...
• XML
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:doctrine="http://symfony.com/schema/dic/doctrine"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/
http://symfony.com/schema/dic/doctrine http://symfony.com/schema/dic/
<doctrine:config>
<doctrine:dbal default-connection="default">
<doctrine:connection
name="default"
dbname="database"
host="localhost"
5.1. Documenti di riferimento
477
Symfony2 documentation Documentation, Release 2
port="1234"
user="user"
password="secret"
driver="pdo_mysql"
driver-class="MyNamespace\MyDriverImpl"
path="%kernel.data_dir%/data.sqlite"
memory="true"
unix-socket="/tmp/mysql.sock"
wrapper-class="MyDoctrineDbalConnectionWrapper"
charset="UTF8"
logging="%kernel.debug%"
platform-service="MyOwnDatabasePlatformService"
>
<doctrine:option key="foo">bar</doctrine:option>
<doctrine:mapping-type name="enum">string</doctrine:mapping-type>
</doctrine:connection>
<doctrine:connection name="conn1" />
<doctrine:type name="custom">Acme\HelloBundle\MyCustomType</doctrine:type>
</doctrine:dbal>
<doctrine:orm default-entity-manager="default" auto-generate-proxy-classes="false" pr
<doctrine:entity-manager name="default" query-cache-driver="array" result-cache-d
<doctrine:metadata-cache-driver type="memcache" host="localhost" port="11211"
<doctrine:mapping name="AcmeHelloBundle" />
<doctrine:dql>
<doctrine:string-function name="test_string>Acme\HelloBundle\DQL\StringFu
<doctrine:numeric-function name="test_numeric>Acme\HelloBundle\DQL\Numeri
<doctrine:datetime-function name="test_datetime>Acme\HelloBundle\DQL\Date
</doctrine:dql>
</doctrine:entity-manager>
<doctrine:entity-manager name="em2" connection="conn2" metadata-cache-driver="apc
<doctrine:mapping
name="DoctrineExtensions"
type="xml"
dir="%kernel.root_dir%/../src/vendor/DoctrineExtensions/lib/DoctrineExten
prefix="DoctrineExtensions\Entity"
alias="DExt"
/>
</doctrine:entity-manager>
</doctrine:orm>
</doctrine:config>
</container>
Panoramica della configurazione
Il seguente esempio di configurazione mostra tutte le configurazioni predefinite, che l’ORM risolve:
doctrine:
orm:
auto_mapping: true
# la distribuzione standard sovrascrive a true in debug, false altrimenti
auto_generate_proxy_classes: false
proxy_namespace: Proxies
proxy_dir: %kernel.cache_dir%/doctrine/orm/Proxies
default_entity_manager: default
metadata_cache_driver: array
query_cache_driver: array
result_cache_driver: array
Ci sono molte altre opzioni di configurazione che si possono usare per sovrascrivere determinate classi, ma sono
solo per casi molto avanzati.
478
Chapter 5. Documenti di riferimento
Symfony2 documentation Documentation, Release 2
Driver per la cache
Per i driver della cache, si può specificare “array”, “apc”, “memcache” o “xcache”.
L’esempio seguente mostra una panoramica delle configurazioni di cache:
doctrine:
orm:
auto_mapping: true
metadata_cache_driver: apc
query_cache_driver: xcache
result_cache_driver:
type: memcache
host: localhost
port: 11211
instance_class: Memcache
Configurazioni della mappatura
La definizione esplicita di tutte le entità mappate è l’unica configurazione necessaria per l’ORM e ci sono diverse
opzioni di configurazione controllabili. La mappatura dispone delle seguenti opzioni di configurazione:
• type Uno tra annotation, xml, yml, php o staticphp. Specifica quale di tipo di meta-dati usa la
mappatura.
• dir Percorso per la mappatura o per i file entità (a seconda del driver). Se questo percorso è relativo, si
assume sia relativo alla radice dei bundle. Funziona solo se il nome della propria mappatura è il nome di
un bundle. Se si vuole usare questa opzione per specificare percorsi assoluti, si dovrebbe aggiungere al
percorso un prefisso con i parametri del kernel nel DIC (per esempio %kernel.root_dir%).
• prefix Un prefisso comune di spazio dei nomi che tutte le entità di questa mappatura condividono.
Questo prefisso non deve essere in conflitto con i prefissi di altre mappature definite, altrimenti alcune
entità non saranno trovate da Doctrine. Questa opzione ha come valore predefinito lo spazio dei nomi
del bundle + Entity, per esempio per un bundle chiamato AcmeHelloBundle il prefisso sarebbe
Acme\HelloBundle\Entity.
• alias Doctrine offre un modo per avere alias di spazi dei nomi con nomi più corti e semplici, da usare
nelle query DQL o per l’accesso al Repository. Quando si usa un bundle, l’alias predefinito è il nome del
bundle.
• is_bundle Questa opzione è un valore derivato da dir ed ha true come valore predefinito, se la cartella
è fornita da una verifica con file_exists() che restituisca false. È false se la verifica restituisce
true. In questo caso, un percorso assoluto è stato specificato e i file dei meta-dati sono probabilmente in
una cartella fuori da un bundle.
Configurazione Doctrine DBAL
Note: DoctrineBundle supporta tutti i parametri che i driver predefiniti di Doctrine accettano, convertiti alla
nomenclatura XML o YML di Symfony. Vedere la documentazione DBAL di Doctrine per maggiori informazioni.
Oltre alle opzioni di Doctrine, ci sono alcune opzioni relative a Symfony che si possono configurare. Il blocco
seguente mostra tutte le voci di configurazione:
• YAML
doctrine:
dbal:
dbname:
host:
port:
5.1. Documenti di riferimento
database
localhost
1234
479
Symfony2 documentation Documentation, Release 2
user:
user
password:
secret
driver:
pdo_mysql
driver_class:
MyNamespace\MyDriverImpl
options:
foo: bar
path:
%kernel.data_dir%/data.sqlite
memory:
true
unix_socket:
/tmp/mysql.sock
wrapper_class:
MyDoctrineDbalConnectionWrapper
charset:
UTF8
logging:
%kernel.debug%
platform_service:
MyOwnDatabasePlatformService
mapping_types:
enum: string
types:
custom: Acme\HelloBundle\MyCustomType
• XML
<!-- xmlns:doctrine="http://symfony.com/schema/dic/doctrine" -->
<!-- xsi:schemaLocation="http://symfony.com/schema/dic/doctrine http://symfony.com/schema/dic
<doctrine:config>
<doctrine:dbal
name="default"
dbname="database"
host="localhost"
port="1234"
user="user"
password="secret"
driver="pdo_mysql"
driver-class="MyNamespace\MyDriverImpl"
path="%kernel.data_dir%/data.sqlite"
memory="true"
unix-socket="/tmp/mysql.sock"
wrapper-class="MyDoctrineDbalConnectionWrapper"
charset="UTF8"
logging="%kernel.debug%"
platform-service="MyOwnDatabasePlatformService"
>
<doctrine:option key="foo">bar</doctrine:option>
<doctrine:mapping-type name="enum">string</doctrine:mapping-type>
<doctrine:type name="custom">Acme\HelloBundle\MyCustomType</doctrine:type>
</doctrine:dbal>
</doctrine:config>
Se si vogliono configurare connessioni multiple in YAML, si possono mettere sotto la voce connections e dar
loro un nome univoco:
doctrine:
dbal:
default_connection:
connections:
default:
dbname:
user:
password:
host:
customer:
dbname:
user:
password:
480
default
Symfony2
root
null
localhost
customer
root
null
Chapter 5. Documenti di riferimento
Symfony2 documentation Documentation, Release 2
host:
localhost
Il servizio database_connection fa sempre riferimento alla configurazione predefinita, che è la prima
definita o l’unica configurata tramite il parametro default_connection.
Ogni connessione è anche accessibile tramite il servizio doctrine.dbal.[nome]_connection, in cui
[nome] è il nome della connessione.
5.1.4 Riferimento configurazione sicurezza
Il sistema di sicurezza è una delle parti più potenti di Symfony2 e può essere controllato in gran parte tramite la
sua configurazione.
Configurazione predefinita completa
La seguente è la configurazione predefinita completa del sistema di sicurezza. Ogni parte sarà spiegata nella
prossima sezione.
• YAML
# app/config/security.yml
security:
access_denied_url: /foo/error403
always_authenticate_before_granting: false
# se richiamare eraseCredentials sul token
erase_credentials: true
# la strategia può essere: none, migrate, invalidate
session_fixation_strategy: migrate
access_decision_manager:
strategy: affirmative
allow_if_all_abstain: false
allow_if_equal_granted_denied: true
acl:
connection: default # qualsiasi nome configurato nella sezione doctrine.dbal
tables:
class: acl_classes
entry: acl_entries
object_identity: acl_object_identities
object_identity_ancestors: acl_object_identity_ancestors
security_identity: acl_security_identities
cache:
id: service_id
prefix: sf2_acl_
voter:
allow_if_object_identity_unavailable: true
encoders:
somename:
class: Acme\DemoBundle\Entity\User
Acme\DemoBundle\Entity\User: sha512
Acme\DemoBundle\Entity\User: plaintext
Acme\DemoBundle\Entity\User:
algorithm: sha512
encode_as_base64: true
iterations: 5000
Acme\DemoBundle\Entity\User:
5.1. Documenti di riferimento
481
Symfony2 documentation Documentation, Release 2
id: my.custom.encoder.service.id
providers:
memory_provider_name:
memory:
users:
foo: { password: foo, roles: ROLE_USER }
bar: { password: bar, roles: [ROLE_USER, ROLE_ADMIN] }
entity_provider_name:
entity: { class: SecurityBundle:User, property: username }
factories:
MyFactory: %kernel.root_dir%/../src/Acme/DemoBundle/Resources/config/security_factori
firewalls:
somename:
pattern: .*
request_matcher: some.service.id
access_denied_url: /foo/error403
access_denied_handler: some.service.id
entry_point: some.service.id
provider: nome_di_un_provider_di_cui_sopra
context: name
stateless: false
x509:
provider: nome_di_un_provider_di_cui_sopra
http_basic:
provider: nome_di_un_provider_di_cui_sopra
http_digest:
provider: nome_di_un_provider_di_cui_sopra
form_login:
check_path: /login_check
login_path: /login
use_forward: false
always_use_default_target_path: false
default_target_path: /
target_path_parameter: _target_path
use_referer: false
failure_path: /foo
failure_forward: false
failure_handler: some.service.id
success_handler: some.service.id
username_parameter: _username
password_parameter: _password
csrf_parameter: _csrf_token
intention: authenticate
csrf_provider: my.csrf_provider.id
post_only: true
remember_me: false
remember_me:
token_provider: name
key: someS3cretKey
name: NameOfTheCookie
lifetime: 3600 # in seconds
path: /foo
domain: somedomain.foo
secure: true
httponly: true
always_remember_me: false
remember_me_parameter: _remember_me
logout:
path:
/logout
target: /
482
Chapter 5. Documenti di riferimento
Symfony2 documentation Documentation, Release 2
invalidate_session: false
delete_cookies:
a: { path: null, domain: null }
b: { path: null, domain: null }
handlers: [some.service.id, another.service.id]
success_handler: some.service.id
anonymous: ~
access_control:
path: ^/foo
host: mydomain.foo
ip: 192.0.0.0/8
roles: [ROLE_A, ROLE_B]
requires_channel: https
role_hierarchy:
ROLE_SUPERADMIN: ROLE_ADMIN
ROLE_SUPERADMIN: ’ROLE_ADMIN, ROLE_USER’
ROLE_SUPERADMIN: [ROLE_ADMIN, ROLE_USER]
anything: { id: ROLE_SUPERADMIN, value: ’ROLE_USER, ROLE_ADMIN’ }
anything: { id: ROLE_SUPERADMIN, value: [ROLE_USER, ROLE_ADMIN] }
Configurazione del form di login
Quando si usa l’ascoltatore di autenticazione form_login dietro un firewall, ci sono diverse opzioni comuni
per configurare l’esoerienza del form di login:
Il form e il processo di login
• login_path (tipo: stringa, predefinito: /login) È l’URL a cui l’utente sarà rinviato (a meno che
use_forward non sia true) quando prova ad accedere a una risorsa protetta, ma non è autenticato.
Questo URL deve essere accessibile da un utente normale e non autenticato, altrimenti si creerebbe un loop
infinito. Per dettagli, vedere “Evitare problemi comuni”.
• check_path (tipo: stringa, predefinito: /login_check) È l’URL a cui il form di login viene
inviato. Il firewall intercetterà ogni richiesta (solo quelle POST, per impostazione predefinita) a questo
URL e processerà le credenziali di login inviate.
Assicurarsi che questo URL sia coperto dal proprio firewall principale (cioè non creare un firewall separato
solo per l’URL check_path).
• use_forward (tipo: booleano, predefinito: false) Se si vuole che l’utente sia rimandato al form di
login invece di essere rinviato, impostare questa opzione a true.
• username_parameter (tipo: stringa, predefinito: _username) Questo il nome del campo che si
dovrebbe dare al campo username del proprio form di login. Quando si invia il form a check_path, il
sistema di sicurezza cercherà un parametro POST con questo nome.
• password_parameter (tipo: stringa, predefinito: _password) Questo il nome del campo che si
dovrebbe dare al campo password del proprio form di login. Quando si invia il form a check_path, il
sistema di sicurezza cercherà un parametro POST con questo nome.
• post_only (tipo: booleano, predefinito: true) Per impostazione predefinita, occorre inviare il proprio
form di login all’URL check_path usando una richiesta POST. Impostando questa opzione a true, si
può inviare una richiesta GET all’URL check_path.
5.1. Documenti di riferimento
483
Symfony2 documentation Documentation, Release 2
Rinvio dopo il login
• always_use_default_target_path (tipo: booleano, predefinito: false)
• default_target_path (tipo: stringa, predefinito: /)
• target_path_parameter (tipo: stringa, predefinito: _target_path)
• use_referer (tipo: booleano, predefinito: false)
5.1.5 Configurazione di SwiftmailerBundle (“swiftmailer”)
Questo riferimento è ancora provvisorio. Dovrebbe essere accurato, ma non sono pienamente coperte tutte le
opzioni. Per un elenco completo delle opzioni predefinite di configurazione, vedere Configurazione
La chiave swiftmailer configura l’integrazione di Symfony con Swiftmailer, che si occupa di creare e spedire
messaggi email.
Configurazione
• transport
• username
• password
• host
• port
• encryption
• auth_mode
• spool
– type
– path
• sender_address
• antiflood
– threshold
– sleep
• delivery_address
• disable_delivery
• logging
transport
tipo: stringa predefinito: smtp
Il metodo di trasporto usato per inviare le email. Valori validi:
• smtp
• gmail (vedere /cookbook/gmail)
• mail
• sendmail
484
Chapter 5. Documenti di riferimento
Symfony2 documentation Documentation, Release 2
• null (lo stesso che impostare disable_delivery a true)
username
tipo: stringa
Il nome utente quando si usa smtp come trasporto.
password
tipo: stringa
La pass
Scarica

questo link - zen.pn.it – Tecnologia e dintorni