L'utilizzo avanzato dei form
di Ryan Weaver, Fabien Potencier
Il framework dei form di symfony viene in aiuto allo sviluppatore con strumenti necessari a visualizzare e validare i form in modo semplice e orientato agli oggetti. Grazie alle classi ~sfFormDoctrine
~ e ~sfFormPropel
~ disponibili per ciascun ORM, il framework dei form può facilmente visualizzare e salvare form che fanno riferimento al livello dei dati.
Le situazioni del mondo reale, tuttavia, spesso richiedono agli sviluppatori di personalizzare ed estendere i form. In questo capitolo vengono presentati e risolti alcuni tra i problemi più comuni, ma impegnativi, sui form. Si vedrà anche il funzionamento dell'oggetto ~sfForm
~, svelando alcuni dei suoi misteri.
Mini-Progetto: Prodotti e foto
Il primo problema ruota attorno alla possibilità di gestire un singolo prodotto e un numero illimitato di foto per tale prodotto. L'utente deve essere in grado di modificare sia il prodotto che le foto sulla stessa form. Si avrà anche la necessità di permettere all'utente di caricare fino a due foto del nuovo prodotto nello stesso momento. Ecco un possibile schema:
PQuando terminato, il form apparirà simile a questo:
Imparare di più facendo esempi
Il modo migliore per imparare le tecniche avanzate è quello di seguire e testare gli esempi passo passo. Grazie alla funzionalità --installer
di symfony, viene fornito un modo semplice per creare un progetto funzionante, con un database SQLite pronto per essere usato, lo schema del database di Doctrine, alcune fixture, una applicazione frontend
e un modulo product
con cui lavorare. Scaricare lo script di installazione script ed eseguire il seguente comando per creare il progetto symfony:
$ php symfony generate:project advanced_form --installer=/path/to/advanced_form_installer.php
Questo comando crea un progetto pienamente funzionante, con lo schema per il database che è stato introdotto nel precedente paragrafo.
In questo capitolo, i percorsi dei file sono quelli di un progetto symfony che viene eseguito con Doctrine, così come è stato generato dal precedente task.
Configurazione di base del form
Poiché i requisiti comportano modifiche a due diversi modelli (Product
e ProductPhoto
), la soluzione richiederà di incorporare due differenti form di symfony (ProductForm
e ProductPhotoForm
). Per fortuna, il framework dei form può facilmene combinare form multipli in un unico form, attraverso ~sfForm::embedForm()
~. In primo luogo, impostare ProductPhotoForm
in modo indipendente. In questo esempio, si usa il campo filename
come campo per l'upload dei file:
// lib/form/doctrine/ProductPhotoForm.class.php public function configure() { $this->useFields(array('filename', 'caption')); $this->setWidget('filename', new sfWidgetFormInputFile()); $this->setValidator('filename', new sfValidatorFile(array( 'mime_types' => 'web_images', 'path' => sfConfig::get('sf_upload_dir').'/products', ))); }
Per questo form, entrambi i campi caption
e filename
sono automaticamente richiesti, ma per motivi diversi. Il campo caption
è richiesto perché la colonna correlata nello schema del database è stata definita con una proprietà notnull
impostata a true
. Il campo filename
è richiesto per impostazione predefinita, perché il default di un oggetto validatore è true
per una opzione required
.
~
sfForm::useFields()
~ è una nuova funzione di symfony 1.3 che consente allo sviluppatore di specificare esattamente quali campi dovrebbe usare il form e in quale ordine devono essere visualizzati. Tutti gli altri campi non hidden vengono rimossi dal form.
Finora non si è fatto niente di più che una normale configurazione del form. Ora verranno combinati i form in uno unico.
Unire i form
Utilizzando ~sfForm::embedForm()
~, i form indipendenti ProductForm
e ProductPhotoForms
possono essere uniti con molto poco sforzo. Il lavoro viene sempre fatto nel form principale, che in questo caso è ProductForm
. Le richieste sono la possibilità di caricare sul server fino a due foto del prodotto in una sola volta. Per fare questo, bisogna unire due oggetti ProductPhotoForm
in ProductForm
:
// lib/form/doctrine/ProductForm.class.php public function configure() { $subForm = new sfForm(); for ($i = 0; $i < 2; $i++) { $productPhoto = new ProductPhoto(); $productPhoto->Product = $this->getObject(); $form = new ProductPhotoForm($productPhoto); $subForm->embedForm($i, $form); } $this->embedForm('newPhotos', $subForm); }
Se si punta il browser al modulo product
, si avrà la possibilità di caricare due ProductPhoto
così come modificare l'oggetto stesso Product
. Symfony salva automaticamente i nuovi oggetti ProductPhoto
e li collega al corrispondente oggetto Product
. Anche l'upload dei file, definito in ProductPhotoForm
, viene eseguito normalmente.
Verificare che i record siano salvati correttamente nel database:
$ php symfony doctrine:dql --table "FROM Product"
$ php symfony doctrine:dql --table "FROM ProductPhoto"
Nella tabella ProductPhoto
si potranno trovare i nomi dei file delle foto. Tutto funziona come previsto, perché nel database si possono trovare i file con gli stessi nomi di quelli presenti nella cartella web/uploads/products/
.
Poiché i campi
filename
ecaption
sono obbligatori inProductPhotoForm
, la validazione del form principale fallirà sempre, a meno che l'utente non carichi due nuove foto. Continuare la lettura per capire come risolvere questo problema
Rifattorizzazione
Anche se il form precedente funziona come previsto, sarebbe meglio rifattorizzare il codice, per facilitare i test e per permettere al codice di essere facilmente riutilizzato.
Prima di tutto, creare un nuovo form che rappresenta un insieme di ProductPhotoForm
, basato sul codice che è già stato scritto:
// lib/form/doctrine/ProductPhotoCollectionForm.class.php class ProductPhotoCollectionForm extends sfForm { public function configure() { if (!$product = $this->getOption('product')) { throw new InvalidArgumentException('You must provide a product object.'); } for ($i = 0; $i < $this->getOption('size', 2); $i++) { $productPhoto = new ProductPhoto(); $productPhoto->Product = $product; $form = new ProductPhotoForm($productPhoto); $this->embedForm($i, $form); } } }
Questo form ha bisogno di due opzioni:
-
product
: Il prodotto per il quale creare una collezione diProductPhotoForm
; -
size
: Il numero diProductPhotoForm
da creare (predefinito a due).
È ora possibile modificare il metodo di configurazione di ProductForm
come segue:
// lib/form/doctrine/ProductForm.class.php public function configure() { $form = new ProductPhotoCollectionForm(null, array( 'product' => $this->getObject(), 'size' => 2, )); $this->embedForm('newPhotos', $form); }
Uno sguardo all'interno dell'oggetto sfForm
Al livello più elementare, un form web è un insieme di campi che sono visualizzati e inviati al server. Sotto questa luce, l'oggetto ~sfForm
~ è essenzialmente un array di campi di form. Mentre ~sfForm
~ gestisce il processo, i singoli campi sono responsabili nel definire come ciascuno verrà visualizzato e validato.
In symfony, ciascun campo del form è definito da due diversi oggetti:
-
Un widget che mostra i campi del form con il markup XHTML;
-
Un validator che pulisce e valida i dati del campo inviato.
In symfony, un widget è definito come un qualunque oggetto, il cui unico compito è quello di mostrare in output un codice XHTML. Anche se in genere è utilizzato con i form, un oggetto widget potrebbe essere creato per riprodurre qualsiasi codice.
Un form è un array
Bisogna ricordare che l'oggetto ~sfForm
~ è "essenzialmente un array di campi di form". Per essere più precisi, sfForm
utilizza sia un array di widget che un array di validatori per tutti i campi del form. Questi due array, chiamati widgetSchema
e validatorSchema
sono proprietà della classe sfForm
. Per aggiungere un campo a un form, bisogna semplicemente aggiungere il widget dei campi all'array widgetSchema
e il validatore dei campi all'array validatorSchema
. Per esempio, il seguente codice aggiungerà un campo email
a un form:
public function configure() { $this->widgetSchema['email'] = new sfWidgetFormInputText(); $this->validatorSchema['email'] = new sfValidatorEmail(); }
Gli array
widgetSchema
evalidatorSchema
in realtà sono classi speciali chiamate ~sfWidgetFormSchema
~ e ~sfValidatorSchema
~, che implementano l'interfacciaArrayAccess
.
Uno sguardo all'interno di ProductForm
Poiché la classe ProductForm
alla fine estende sfForm
, essa ospita anche tutti i suoi widget e validatori negli array widgetSchema
e validatorSchema
. Si può vedere, nell'oggetto ProductForm
, come ciascun array è organizzato.
widgetSchema => array ( [id] => sfWidgetFormInputHidden, [name] => sfWidgetFormInputText, [price] => sfWidgetFormInputText, [newPhotos] => array( [0] => array( [id] => sfWidgetFormInputHidden, [filename] => sfWidgetFormInputFile, [caption] => sfWidgetFormInputText, ), [1] => array( [id] => sfWidgetFormInputHidden, [filename] => sfWidgetFormInputFile, [caption] => sfWidgetFormInputText, ), ), ) validatorSchema => array ( [id] => sfValidatorDoctrineChoice, [name] => sfValidatorString, [price] => sfValidatorNumber, [newPhotos] => array( [0] => array( [id] => sfValidatorDoctrineChoice, [filename] => sfValidatorFile, [caption] => sfValidatorString, ), [1] => array( [id] => sfValidatorDoctrineChoice, [filename] => sfValidatorFile, [caption] => sfValidatorString, ), ), )
Proprio come
widgetSchema
evalidatorSchema
sono in realtà oggetti che si comportano come array, gli array di cui sopra, definiti dalle chiavinewPhotos
,0
e1
, sono anche oggettisfWidgetSchema
esfValidatorSchema
.
Come previsto, i campi di base (id
, name
e price
) sono rappresentati nel primo livello di ciascun array. In un form che non incorpora altri form, entrambi gli array widgetSchema
e validatorSchema
hanno solo un livello, che rappresenta i campi di base del form. Come visto sopra, i widget e i validatori di eventuali form incorporati sono rappresentati come array figli in widgetSchema
e validatorSchema
. Il metodo che gestisce questo processo è spiegato più avanti.
Dietro a ~sfForm::embedForm()
~
Bisogna sempre ricordare che un form è composto da una serie di widget e da una serie di validatori. Incorporare un form in un altro, in sostanza, vuol dire che gli array dei widget e dei validatori di un form sono aggiunti agli array dei widget e dei validatori del form principale. Questa procedura è interamente realizzata da sfForm::embedForm()
. Il risultato, come visto sopra, è sempre una aggiunta multi-dimensionale agli array widgetSchema
e validatorSchema
.
Di seguito si vedrà la configurazione di ProductPhotoCollectionForm
, che lega i singoli oggetti ProductPhotoForm
con sé stesso. Questo form intermedio agisce come "wrapper" e aiuta nell'organizzazione complessiva del form. Si può iniziare con il seguente codice da ProductPhotoCollectionForm::configure()
:
$form = new ProductPhotoForm($productPhoto); $this->embedForm($i, $form);
Il form ProductPhotoCollectionForm
stesso inizia come un nuovo oggetto sfForm
. In quanto tale, gli array widgetSchema
e validatorSchema
sono vuoti.
widgetSchema => array() validatorSchema => array()
Ogni ProductPhotoForm
, tuttavia, è già pronto con tre campi (id
, filename
, e caption
) e tre corrispondenti oggetti negli array widgetSchema
e validatorSchema
.
widgetSchema => array ( [id] => sfWidgetFormInputHidden, [filename] => sfWidgetFormInputFile, [caption] => sfWidgetFormInputText, ) validatorSchema => array ( [id] => sfValidatorDoctrineChoice, [filename] => sfValidatorFile, [caption] => sfValidatorString, )
Il metodo ~sfForm::embedForm()
~ aggiunge semplicemente gli array widgetSchema
e validatorSchema
di ogni ProductPhotoForm
agli array widgetSchema
e validatorSchema
di un oggetto vuoto ProductPhotoCollectionForm
.
Una volta terminato, gli array widgetSchema
e validatorSchema
del form wrapper (ProductPhotoCollectionForm
) sono array multi-livello che contengono i widget e i validatori di entrambi i ProductPhotoForm
.
widgetSchema => array ( [0] => array ( [id] => sfWidgetFormInputHidden, [filename] => sfWidgetFormInputFile, [caption] => sfWidgetFormInputText, ), [1] => array ( [id] => sfWidgetFormInputHidden, [filename] => sfWidgetFormInputFile, [caption] => sfWidgetFormInputText, ), ) validatorSchema => array ( [0] => array ( [id] => sfValidatorDoctrineChoice, [filename] => sfValidatorFile, [caption] => sfValidatorString, ), [1] => array ( [id] => sfValidatorDoctrineChoice, [filename] => sfValidatorFile, [caption] => sfValidatorString, ), )
Nella fase finale del processo, il form wrapper ProductPhotoCollectionForm
risultante è inserito direttamente nel ProductForm
. Questo avviene all'interno di ProductForm::configure()
, che sfrutta tutto il lavoro che è stato fatto dentro a ProductPhotoCollectionForm
:
$form = new ProductPhotoCollectionForm(null, array( 'product' => $this->getObject(), 'size' => 2, )); $this->embedForm('newPhotos', $form);
Questo fornisce la struttura finale degli array widgetSchema
e validatorSchema
vista sopra. Si noti che il metodo embedForm()
è molto simile a quello che si avrebbe combinando manualmente gli array widgetSchema
e validatorSchema
:
$this->widgetSchema['newPhotos'] = $form->getWidgetSchema(); $this->validatorSchema['newPhotos'] = $form->getValidatorSchema();
Visualizzare l'unione di form nella vista
Il template corrente _form.php
del modulo product
, è simile al seguente:
// apps/frontend/module/product/templates/_form.php <!-- ... --> <tbody> <?php echo $form ?> </tbody> <!-- ... -->
L'istruzione <?php echo $form ?>
è il modo più semplice per visualizzare dei form, anche i più complessi. È di grande aiuto per partire, ma, appena si desidera modificare il layout è necessario sostituirlo con la propria logica di visualizzazione. Rimuovere questa riga, perché verrà sostituita in questa sezione.
La cosa più importante da capire quando si visualizzano form incorporati nella vista è l'organizzazione dell'array multi livello widgetSchema
, spiegato nel paragrafo precedente. Per questo esempio, si può iniziare visualizzando nella vista i campi di base name
e price
del ProductForm
:
// apps/frontend/module/product/templates/_form.php <?php echo $form['name']->renderRow() ?> <?php echo $form['price']->renderRow() ?> <?php echo $form->renderHiddenFields() ?>
Come suggerisce il nome, renderHiddenFields()
visualizza tutti i campi hidden del form.
Il codice delle azioni non è stato volutamente mostrato qui, perché ha bisogno di un'attenzione particolare. Si dia un'occhiata al file con le azioni
apps/frontend/modules/product/actions/actions.class.php
. Assomiglia a un normale CRUD e può essere generato automaticamente attraverso il taskdoctrine:generate-module
.
Come si è già imparato, la classe sfForm
ospita gli array widgetSchema
e validatorSchema
che definiscono i campi. Inoltre, la classe sfForm
implementa l'interfaccia nativa di PHP 5 ArrayAccess
, il che significa che si può accedere direttamente ai campi del form, utilizzando la sintassi per le chiave degli array vista sopra.
Per visualizzare i campi, si può accederci direttamente e chiamare il metodo renderRow()
. Ma che tipo di oggetto è $form['name']
? Anche se ci si potrebbe aspettare di rispondere che per il campo name
possa essere il widget sfWidgetFormInputText
, la risposta è in realtà qualcosa di leggermente diverso.
Visualizzare ciascun campo del form con ~sfFormField
~
Usando gli array widgetSchema
e validatorSchema
definiti in ogni classe del form, sfForm
genera automaticamente un terzo array chiamato sfFormFieldSchema
. Questo array contiene un oggetto speciale per ciascun campo, che agisce come una classe helper responsabile per la visualizzazione dei campi. L'oggetto, di tipo ~sfFormField
~, è una combinazione di widget validator di ogni campo ed è creato automaticamente.
<?php echo $form['name']->renderRow() ?>
Nel frammento di codice sovrastante, $form['name']
è un oggetto sfFormField
, che ospita il metodo renderRow()
insieme a molte altre utili funzioni per la visualizzazione.
I metodi di visualizzazione con sfFormField
Ciascun oggetto sfFormField
può essere usato per visualizzare facilmente ogni aspetto del campo che esso rappresenta (ad esempio il campo stesso, l'etichetta, i messaggi di errore, ecc.). Alcuni degli utili metodi all'interno di sfFormField
sono i seguenti. Altri si possono trovare guardando le API di symfony 1.3.
-
sfFormField->render()
: Visualizza il campo del form (es.input
,select
) con il corretto valore, utilizzando l'oggetto widget del campo stesso. -
sfFormField->renderError()
: Visualizza sul campo gli eventuali errori di validazione, usando l'oggetto validator del campo stesso. -
sfFormField->renderRow()
: Onnicomprensivo: visualizza l'etichetta, il campo del form, l'errore e il messaggio di aiuto, dentro a un codice wrapper XHTML.
In realtà, ogni funzione di visualizzazione della classe
sfFormField
, utilizza anche le informazioni della proprietàwidgetSchema
del form (l'oggettosfWidgetFormSchema
che ospita tutti i widget del form). Questa classe assiste nella generazione di ogni attributoname
eid
del campo, tiene traccia dell'etichetta per ciascun campo e definisce il codice XHTML utilizzato conrenderRow()
.
Una cosa importante da notare è che l'array formFieldSchema
rispecchia sempre la struttura degli array widgetSchema
e validatorSchema
del form. Ad esempio, l'array formFieldSchema
del ProductForm
, avrebbe la seguente struttura, che è la chiave per visualizzare ogni campo nella vista:
formFieldSchema => array ( [id] => sfFormField [name] => sfFormField, [price] => sfFormField, [newPhotos] => array( [0] => array( [id] => sfFormField, [filename] => sfFormField, [caption] => sfFormField, ), [1] => array( [id] => sfFormField, [filename] => sfFormField, [caption] => sfFormField, ), ), )
Visualizzare il nuovo ProductForm
Utilizzando l'array di cui sopra come se fosse una mappa, si possono visualizzare facilmente nella vista i campi di ProductPhotoForm
che sono stati uniti, posizionando e visualizzando i relativi oggetti sfFormField
:
// apps/frontend/module/product/templates/_form.php <?php foreach ($form['newPhotos'] as $photo): ?> <?php echo $photo['caption']->renderRow() ?> <?php echo $photo['filename']->renderRow() ?> <?php endforeach; ?>
Il blocco sopra cicla due volte: una per i campi del form dell'array 0
e una per i campi del form dell'array 1
. Come si è visto nello schema precedente, gli oggetti di base di ogni array sono oggetti sfFormField
, che si possono visualizzare come ogni altro campo.
Salvare gli oggetti dei form
In molte circostanze, un form si riferisce direttamente a una o più tabelle di un database ed esegue le modifiche ai dati di tali tabelle, in base ai valori inviati. Symfony genera automaticamente, per ciascun modello dello schema, un oggetto form, che estende sfFormDoctrine
o sfFormPropel
a seconda dell'ORM. Ogni classe dei form è simile e in fin dei conti permette ai valori inviati di essere facilmente gestiti nel database.
~
sfFormObject
~ è una nuova classe aggiunta in symfony 1.3 per gestire tutti i task comuni disfFormDoctrine
esfFormPropel
. Ogni classe estendesfFormObject
, che ora gestisce parte del processo di memorizzazione del form descritto di seguito.
Il processo di memorizzazione del form
Nell'esempio, symfony salva automaticamente sia le informazioni di Product
che quelle di ProductPhoto
, senza alcuno sforzo supplementare da parte dello sviluppatore. Il metodo che innesca la magia, ~sfFormObject::save()
~, esegue dietro le quinte una serie di metodi. La comprensione del suo funzionamento è la chiave per estendere il processo in situazioni più avanzate.
Il processo di memorizzazione del form, consiste di una serie di metodi eseguiti internamente, che vengono lanciati dopo la chiamata di ~sfFormObject::save()
~. La maggior parte del lavoro è svolto nel metodo ~sfFormObject::updateObject()
~, che è chiamato ricorsivamente su tutti i form incorporati.
La maggior parte del processo di salvataggio avviene all'interno del metodo ~
sfFormObject::doSave()
~, che è chiamato dasfFormObject::save()
ed esegue una transazione nel database. Se c'è la necessità di modificare il processo di salvataggio stesso, generalmentesfFormObject::doSave()
è il posto migliore per farlo.
Ignorare le unioni di form
L'attuale implementazione di ProductForm
ha un importante problema. Poiché i campi filename
e caption
in ProductPhotoForm
sono obbligatori, la validazione del form principale fallirà sempre, a meno che l'utente non stia caricando due nuove foto. In altre parole, l'utente non può cambiare semplicemente il prezzo di Product
senza dover caricare due nuove foto.
Ora verranno ridefiniti i requisiti, per poter aggiungerne di nuovi. Se l'utente lascia tutti i campi di un ProductPhotoForm
vuoti, il form deve essere completamente ignorato. Invece, se almeno un campo contiene dei dati (per esempio caption
o filename
), il form deve validare e salvare in modo normale. Per realizzare ciò, si impiega una tecnica avanzata che comporta l'uso di un post validatore personalizzato.
Il primo passo però, è quello di modificare il form ProductPhotoForm
per rendere facoltativi i campi caption
e filename
:
// lib/form/doctrine/ProductPhotoForm.class.php public function configure() { $this->setValidator('filename', new sfValidatorFile(array( 'mime_types' => 'web_images', 'path' => sfConfig::get('sf_upload_dir').'/products', 'required' => false, ))); $this->validatorSchema['caption']->setOption('required', false); }
Nel codice di cui sopra, si è impostata l'opzione required
a false
, quando è stato sovrascritto il validatore predefinito per il campo filename
. Inoltre, l'opzione required
del campo caption
è stata impostata esplicitamente a false
.
Ora si può aggiungere il post validatore a ProductPhotoCollectionForm
:
// lib/form/doctrine/ProductPhotoCollectionForm.class.php public function configure() { // ... $this->mergePostValidator(new ProductPhotoValidatorSchema()); }
Un post-validatore è uno speciale tipo di validatore che valida tutti i valori inviati (l'opposto della validazione del valore di un singolo campo). Uno dei post-validatori più comuni è sfValidatorSchemaCompare
che verifica, per esempio, se un campo è inferiore a un altro campo.
Creare un validatore personalizzato
Per fortuna, la creazione di un validatore personalizzato è in realtà abbastanza semplice. Creare un nuovo file, ProductPhotoValidatorSchema.class.php
e metterlo nella cartella lib/validator/
(è necessario creare la cartella):
// lib/validator/ProductPhotoValidatorSchema.class.php class ProductPhotoValidatorSchema extends sfValidatorSchema { protected function configure($options = array(), $messages = array()) { $this->addMessage('caption', 'The caption is required.'); $this->addMessage('filename', 'The filename is required.'); } protected function doClean($values) { $errorSchema = new sfValidatorErrorSchema($this); foreach($values as $key => $value) { $errorSchemaLocal = new sfValidatorErrorSchema($this); // c'è il nome del file, ma non la didascalia if ($value['filename'] && !$value['caption']) { $errorSchemaLocal->addError(new sfValidatorError($this, 'required'), 'caption'); } // c'è la didascalia caption, ma non il nome del file if ($value['caption'] && !$value['filename']) { $errorSchemaLocal->addError(new sfValidatorError($this, 'required'), 'filename'); } // né didascalia né nome del file, rimuovere i valori vuoti if (!$value['filename'] && !$value['caption']) { unset($values[$key]); } // ci sono errori per il form incluso if (count($errorSchemaLocal)) { $errorSchema->addError($errorSchemaLocal, (string) $key); } } // invia l'errore per il form principale if (count($errorSchema)) { throw new sfValidatorErrorSchema($this, $errorSchema); } return $values; } }
Tutti i validatori estendono
sfValidatorBase
e richiedono solo il metododoClean()
. Il metodoconfigure()
può essere utilizzato anche per aggiungere opzioni o messaggi al validatore. In questo caso, vengono aggiunti due messaggi al validatore. Allo stesso modo, possono essere aggiunte le opzioni tramite il metodoaddOption()
.
Il metodo doClean()
si occupa della pulizia e della validazione dei relativi valori. La logica del validatore stesso è abbastanza semplice:
-
Se una foto è inviata con solo il nome del file o della didascalia, viene lanciato un errore (
sfValidatorErrorSchema
) con il messaggio appropriato; -
Se una foto è inviata senza nome del file e senza didascalia, si eliminano tutti i valori, in modo da evitare di salvare una foto vuota;
-
Se non si verificano errori di validazione, il metodo restituisce l'array con i valori puliti.
Poiché il validatore personalizzato in questa situazione è destinato a essere utilizzato come post-validatore, il metodo
doClean()
si aspetta un array dei valori uniti e restituisce un array di valori puliti. I validatori personalizzati, tuttavia, possono essere facilmente creati per singoli campi. In tal caso, il metododoClean()
si aspetta un solo valore (il valore del campo inviato) e restituirà un solo valore.
L'ultimo passo è quello di sovrascrivere il metodo saveEmbeddedForms()
di ProductForm
, per cancellare i form di foto vuoti ed evitare di salvare una foto vuota nel database (altrimenti dovrebbe lanciare una eccezione, dal momento che la colonna caption
è obbligatoria):
public function saveEmbeddedForms($con = null, $forms = null) { if (null === $forms) { $photos = $this->getValue('newPhotos'); $forms = $this->embeddedForms; foreach ($this->embeddedForms['newPhotos'] as $name => $form) { if (!isset($photos[$name])) { unset($forms['newPhotos'][$name]); } } } return parent::saveEmbeddedForms($con, $forms); }
Unire facilmente i form relativi a Doctrine
Una novità in symfony 1.3 è la funzione ~sfFormDoctrine::embedRelation()
~, che consente allo sviluppatore di unire automaticamente relazioni n-a-molti in un form. Si supponga, per esempio, che oltre a permettere all'utente di caricare due nuovi ProductPhotos
, si voglia anche consentire all'utente di modificare gli oggetti di questo Product
relativi a ProductPhoto
.
Allo scopo, si può usare il metodo embedRelation()
per aggiungere un ulteriore oggetto ProductPhotoForm
a ciascun esistente oggetto ProductPhoto
:
// lib/form/doctrine/ProductForm.class.php public function configure() { // ... $this->embedRelation('Photos'); }
Internamente, ~sfFormDoctrine::embedRelation()
~ fa quasi esattamente ciò che è stato fatto manualmente per incorporare i due nuovi oggetti ProductPhotoForm
. Se esistono già le due relazioni ProductPhoto
, allora i risultanti widgetSchema
e validatorSchema
del form, prendono la seguente forma:
widgetSchema => array ( [id] => sfWidgetFormInputHidden, [name] => sfWidgetFormInputText, [price] => sfWidgetFormInputText, [newPhotos] => array(...) [Photos] => array( [0] => array( [id] => sfWidgetFormInputHidden, [caption] => sfWidgetFormInputText, ), [1] => array( [id] => sfWidgetFormInputHidden, [caption] => sfWidgetFormInputText, ), ), ) validatorSchema => array ( [id] => sfValidatorDoctrineChoice, [name] => sfValidatorString, [price] => sfValidatorNumber, [newPhotos] => array(...) [Photos] => array( [0] => array( [id] => sfValidatorDoctrineChoice, [caption] => sfValidatorString, ), [1] => array( [id] => sfValidatorDoctrineChoice, [caption] => sfValidatorString, ), ), )
Il passo successivo è quello di aggiungere codice alla vista, che permetterà di visualizzare i nuovi form Photo incorporati:
// apps/frontend/module/product/templates/_form.php <?php foreach ($form['Photos'] as $photo): ?> <?php echo $photo['caption']->renderRow() ?> <?php echo $photo['filename']->renderRow(array('width' => 100)) ?> <?php endforeach; ?>
Questo frammento di codice è esattamente quello che è stato usato in precedenza per incorporare i nuovi form delle foto.
L'ultimo passo è quello di convertire il campo con il file da caricare, in uno che permetta all'utente di vedere la foto corrente e di cambiarla con una nuova (sfWidgetFormInputFileEditable
):
public function configure() { $this->useFields(array('filename', 'caption')); $this->setValidator('filename', new sfValidatorFile(array( 'mime_types' => 'web_images', 'path' => sfConfig::get('sf_upload_dir').'/products', 'required' => false, ))); $this->setWidget('filename', new sfWidgetFormInputFileEditable(array( 'file_src' => '/uploads/products/'.$this->getObject()->filename, 'edit_mode' => !$this->isNew(), 'is_image' => true, 'with_delete' => false, ))); $this->validatorSchema['caption']->setOption('required', false); }
Eventi dei form
Una novità di symfony 1.3 sono gli eventi dei form, che possono essere usati per estendere un qualsiasi oggetto form in qualunque posto del progetto. Symfony espone i seguenti quattro eventi per i form:
form.post_configure
: Questo evento è notificato dopo che ciascun form è configuratoform.filter_values
: Questo evento filtra i parametri fusi e modificati e gli array dei file appena prima di essere unitiform.validation_error
: Questo evento è notificato se fallisce qualunque validazione sul formform.method_not_found
: Questo evento è notificato ogni volta che si chiama un metodo sconosciuto
Personalizzazione dei log attraverso form.validation_error
Utilizzando gli eventi dei form, è possibile aggiungere log personalizzati per errori di validazione su qualunque form del progetto. Questo potrebbe essere utile se si vuole tenere traccia di quali form e campi stanno causando confusione agli utenti.
Si può iniziare la registrazione di un ascoltatore con il dispatcher di eventi, per l'evento form.validation_error
. Aggiungere il codice seguente al metodo setup()
di ProjectConfiguration
che è presente nella cartella config
:
public function setup() { // ... $this->getEventDispatcher()->connect( 'form.validation_error', array('BaseForm', 'listenToValidationError') ); }
BaseForm
, presente in lib/form
, è una speciale classe form che estende tutte le classi dei form. In sostanza, BaseForm
è una classe in cui può essere inserito il codice, diventando accessibile da tutti gli oggetti form del progetto. Per abilitare il log degli errori di validazione, basta aggiungere il seguente codice alla classe BaseForm
:
public static function listenToValidationError($event) { foreach ($event['error'] as $key => $error) { self::getEventDispatcher()->notify(new sfEvent( $event->getSubject(), 'application.log', array ( 'priority' => sfLogger::NOTICE, sprintf('Validation Error: %s: %s', $key, (string) $error) ) )); } }
Personalizzazione dello stile grafico quando un elemento del form ha un errore
Come esercizio conclusivo, si può passare a un argomento un po' più leggero, relativo alla grafica degli elementi di un form. Si supponga, per esempio, che la grafica per la pagina Product
includa uno stile speciale per i campi che che falliscono la validazione.
Si supponga che il grafico abbia già implementato il foglio di stile che applicherà la grafica per l'errore a ogni campo input
dentro a un div
, con la classe form_error_row
. Come si può aggiungere facilmente la classe form_row_error
ai campi con errori?
La risposta si trova in uno speciale oggetto chiamato formattatore di schema dei form. Ogni form di symfony utilizza un formattatore di schema dei form, per determinare l'esatta formattazione HTML da utilizzare quando vengono visualizzati gli elementi di un form. Per impostazione predefinita, symfony utilizza un formattatore di form che usa i tag HTML table.
Prima di tutto, bisogna creare una nuova classe formattatore per lo schema dei form, che utilizzi un codice leggero quando visualizza il form. Creare un nuovo file chiamato sfWidgetFormSchemaFormatterAc2009.class.php
e metterlo nella cartella lib/widget/
(è necessario creare questa cartella):
class sfWidgetFormSchemaFormatterAc2009 extends sfWidgetFormSchemaFormatter { protected $rowFormat = "<div class='form_row'> %label% \n %error% <br/> %field% %help% %hidden_fields%\n</div>\n", $errorRowFormat = "<div>%errors%</div>", $helpFormat = '<div class="form_help">%help%</div>', $decoratorFormat = "<div>\n %content%</div>"; }
Anche se il formato di questa classe è strano, l'idea generale è che il metodo renderRow()
utilizzerà il codice $rowFormat
per organizzare il suo output. Un formattatore di schema dei form offre molte altre opzioni di formattazione, che in questa sede non vengono mostrare in dettaglio. Per maggiori informazioni, consultare le API di symfony 1.3.
Per usare il nuovo formattatore di schema dei form in tutti gli oggetti form del progetto, aggiungere il seguente codice a ProjectConfiguration
:
class ProjectConfiguration extends sfProjectConfiguration { public function setup() { // ... sfWidgetFormSchema::setDefaultFormFormatterName('ac2009'); } }
L'obiettivo è quello di aggiungere una classe form_row_error
all'elemento div form_row
solo se un campo fallisce la validazione. Aggiungere un token %row_class%
alla proprietà $rowFormat
e sovrascrivere il metodo ~sfWidgetFormSchemaFormatter::formatRow()
~ come segue:
class sfWidgetFormSchemaFormatterAc2009 extends sfWidgetFormSchemaFormatter { protected $rowFormat = "<div class='form_row%row_class%'> %label% \n %error% <br/> %field% %help% %hidden_fields%\n</div>\n", // ... public function formatRow($label, $field, $errors = array(), $help = '', $hiddenFields = null) { $row = parent::formatRow( $label, $field, $errors, $help, $hiddenFields ); return strtr($row, array( '%row_class%' => (count($errors) > 0) ? ' form_row_error' : '', )); } }
Con questa aggiunta, ogni elemento che è visualizzato attraverso il metodo renderRow()
, sarà automaticamente circondato da un form_row_error
div
se il campo ha fallito la validazione.
Considerazioni finali
Il framework dei form è contemporaneamente uno dei più potenti e più complessi componenti all'interno di symfony. Il prezzo da pagare per la validazione stretta dei form, la protezione CSRF e gli oggetti dei form è che estendere il framework può diventare un compito arduo. Acquisire una più profonda comprensione del sistema dei form, tuttavia, è la chiave per sbloccare il suo potenziale. Speriamo che questo capitolo sia stato un passo di avvicinamento in tal senso.
I prossimi sviluppi del framework dei form si concentreranno sul mantenimento della potenza, diminuendo la complessità e fornendo maggiore flessibilità allo sviluppatore. Il framework dei form, attualmente, è solo nella sua infanzia.
インデックス
Document Index
-
L'utilizzo avanzato dei form
- Mini-Progetto: Prodotti e foto
- Imparare di più facendo esempi
- Configurazione di base del form
- Unire i form
- Rifattorizzazione
- Uno sguardo all'interno dell'oggetto sfForm
- Visualizzare l'unione di form nella vista
- Salvare gli oggetti dei form
- Ignorare le unioni di form
- Unire facilmente i form relativi a Doctrine
- Eventi dei form
- Personalizzazione dello stile grafico quando un elemento del form ha un errore
- Considerazioni finali
関連ページリスト
Related Pages
Introduzione
Utilizzo avanzato delle rotte
Migliorare la propria produttività
Email
Widget e validatori personalizzati
L'utilizzo avanzato dei form
Estendere la Web Debug Toolbar
Uso avanzato di Doctrine
Sfruttare l'ereditarietà delle tabelle di Doctrine
Symfony all'interno
Windows e symfony
Sviluppare su Facebook
Sfruttare la potenza della linea di comando
Lavorare con la cache della configurazione di symfony
Lavorare con la comunità di symfony
Appendice A - codice JavaScript per sfWidgetFormGMapAddress
A proposito degli autori
Appendice B - Esempio di installazione personalizzata
Appendice C - Licenza
A proposito dei traduttori

日本語ドキュメント
Japanese Documents
2011/01/18 Chapter 17 - Extending Symfony
2011/01/18 The generator.yml Configuration File
2011/01/18 Les tâches
2011/01/18 Emails
2010/11/26 blogチュートリアル(8) ビューの作成
リリース情報
Release Information
- 2.0 : 2.0.15(2011/05/30)
Symfony2日本語ドキュメント - 1.4 : 1.4.18(2012/05/30)
Changelog

