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 <script>alert('ciao!')</script> 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="<div><label class=" required">$$name$$</label>< </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