Widget e validatori personalizzati
di Thomas Rabaix
Questo capitolo spiega come costruire widget e validatori personalizzati da usare nel framework dei form. Verrà spiegata la struttura interna di sfWidgetForm
e sfValidator
, così come il modo in cui si costruiscono widget semplici e complessi.
Funzionamento interno di widget e validatori
Funzionamento interno di sfWidgetForm
Un oggetto della classe ~sfWidgetForm
~ rappresenta la realizzazione visiva di come i relativi dati potranno essere modificati. Un valore stringa, per esempio, potrebbe essere modificato con una semplice casella di testo, o con un avanzato editor di tipo WYSIWYG. La classe sfWidgetForm
, allo scopo di poter essere completamente configurabile, ha due importanti proprietà: options
e attributes
.
-
options
: usato per configurare il widget (ad esempio le query al database, da usare quando si crea un elenco per un menu a tendina) -
attributes
: attributi HTML aggiunti all'elemento che deve essere visualizzato
Inoltre, la classe sfWidgetForm
implementa due importanti metodi:
-
configure()
: definisce quali opzioni sono facoltative o obbligatorie. Anche se non è una buona pratica sovrascrivere il costruttore, il metodoconfigure()
può essere tranquillamente ignorato. -
render()
: visualizza l'HTML per il widget. Il metodo ha un primo parametro obbligatorio, il nome HTML del widget e un secondo parametro opzionale, il valore.
Un oggetto
sfWidgetForm
non sa nulla sul suo nome o il suo valore. Il componente è responsabile solo della visualizzazione del widget. Il nome e il valore sono gestiti da un oggettosfFormFieldSchema
, che è il collegamento tra i dati e i widget.
Funzionamento interno di sfValidatorBase
La classe ~sfValidatorBase
~ è la classe base di ogni validatore. Il metodo ~sfValidatorBase::clean()
~ è il più importante di questa classe, perché controlla se il valore è valido in base alle opzioni fornite.
Internamente, il metodo clean()
esegue diverse azioni:
- toglie, dal valore di input, gli spazi iniziali e finali per i valori di tipo stringa (se specificato tramite l'opzione
trim
) - controlla se il valore è vuoto
- chiama il metodo del validatore
doClean()
.
Il metodo doClean()
, è il metodo che implementa la logica principale di convalida. Non è buona pratica sovrascrivere il metodo clean()
. È meglio gestire ogni logica personalizzata attraverso il metodo doClean()
.
Un validatore può anche essere usato come un componente a se stante per controllare l'integrità di un input. Ad esempio, il validatore sfValidatorEmail
controlla se una email è valida:
$v = new sfValidatorEmail(); try { $v->clean($request->getParameter("email")); } catch(sfValidatorError $e) { $this->forward404(); }
Quando un form viene legato ai valori della request, l'oggetto
sfForm
mantiene i riferimenti al valore originale ("sporco") e al valore validato ("pulito"). I valori originali sono usati quando il form viene ridisegnato, mentre i valori puliti sono usati dall'applicazione (ad esempio, per salvare l'oggetto).
L'attributo options
Entrambi gli oggetti sfWidgetForm
e sfValidatorBase
hanno numerose opzioni: alcune sono facoltative, mentre altre sono obbligatorie. Queste opzioni sono definite all'interno di ogni metodo configure()
della classe, attraverso:
addOption($name, $value)
: definisce un'opzione con un nome e un valore predefinitoaddRequiredOption($name)
: definisce un'opzione obbligatoria
Questi due metodi sono molto utili, in quanto garantiscono che i valori di dipendenza siano correttamente passati al validatore o al widget.
Creare un semplice widget e un validatore
Questa sezione spiega come creare un semplice widget. Questo particolare widget sarà chiamato "Trilean". Il widget visualizzerà un menu a tendina con tre scelte: No
, Si
e Null
.
class sfWidgetFormTrilean extends sfWidgetForm { public function configure($options = array(), $attributes = array()) { $this->addOption('choices', array( 0 => 'No', 1 => 'Yes', 'null' => 'Null' )); } public function render($name, $value = null, $attributes = array(), $errors = array()) { $value = $value === null ? 'null' : $value; $options = array(); foreach ($this->getOption('choices') as $key => $option) { $attributes = array('value' => self::escapeOnce($key)); if ($key == $value) { $attributes['selected'] = 'selected'; } $options[] = $this->renderContentTag( 'option', self::escapeOnce($option), $attributes ); } return $this->renderContentTag( 'select', "\n".implode("\n", $options)."\n", array_merge(array('name' => $name), $attributes )); } }
Il metodo configure()
definisce i valori delle opzioni per l'elenco, attraverso l'opzione choices
. Questo array può essere ridefinito (ad esempio per cambiare l'etichetta associata a ogni valore). Non vi è alcun limite al numero di opzioni che un widget può definire. La classe base del widget, tuttavia, dichiara alcune opzioni standard che quindi è come se fossero opzioni riservate:
-
id_format
: il formato id, il predefinito è '%s' -
is_hidden
: valore booleano per definire se il widget è un campo nascosto (usato dasfForm::renderHiddenFields()
per rendere tutti i campi nascosti in una sola volta) -
needs_multipart
: valore booleano per definire se il tag del form deve includere l'opzione multipart (ad esempio per l'upload dei file) -
default
: Il valore predefinito che deve essere utilizzato per rendere il widget se non è fornito nessun valore -
label
: L'etichetta predefinita del widget
Il metodo render()
genera il codice HTML corrispondente a un menu a tendina. Il metodo chiama la funzione predefinita renderContentTag()
che viene in aiuto nella visualizzazione dei tag HTML.
Ora il widget è completo. Si può creare il validatore corrispondente:
class sfValidatorTrilean extends sfValidatorBase { protected function configure($options = array(), $messages = array()) { $this->addOption('true_values', array('true', 't', 'yes', 'y', 'on', '1')); $this->addOption('false_values', array('false', 'f', 'no', 'n', 'off', '0')); $this->addOption('null_values', array('null', null)); } protected function doClean($value) { if (in_array($value, $this->getOption('true_values'))) { return true; } if (in_array($value, $this->getOption('false_values'))) { return false; } if (in_array($value, $this->getOption('null_values'))) { return null; } throw new sfValidatorError($this, 'invalid', array('value' => $value)); } public function isEmpty($value) { return false; } }
Il validatore sfValidatorTrilean
definisce tre opzioni nel metodo configure()
. Ogni opzione è un insieme di valori validi. Siccome questi sono definiti come opzioni, lo sviluppatore può personalizzare i valori a seconda delle specifiche.
Il metodo doClean()
verifica se il valore corrisponde a un insieme di valori validi e restituisce il valore "pulito". Se il valore non viene trovato, il metodo lancerà un sfValidatorError
, che è l'errore standard di validazione nel framework dei form.
L'ultimo metodo isEmpty()
è sovrascrivibile, perché il comportamento predefinito di questo metodo è di restituire true
se il valore è null
.
Se
isEmpty()
restituisce true, il metododoClean()
non verrà mai chiamato.
Anche se questo widget è abbastanza semplice, ha introdotto alcune importanti caratteristiche di base che saranno necessarie andando avanti. Nella sezione successiva verrà creato un widget più complesso, con più campi e una interazione JavaScript.
Il widget per le mappe di Google
In questa sezione, si andrà a costruire un widget più complesso. Saranno introdotti nuovi metodi e il widget avrà anche qualche interazione JavaScript. Il widget sarà chiamato "GMAW": "Google Map Address Widget".
Cosa si vuole ottenere? Il widget dovrebbe fornire un metodo semplice rivolto all'utente finale, per aggiungere un indirizzo. Utilizzando un campo input di testo e con il servizio mappe di Google, si può raggiungere questo obiettivo.
Caso d'uso 1:
- L'utente digita un indirizzo.
- L'utente clicca il bottone "lookup".
- I campi nascosti di latitudine e longitudine vengono aggiornati e sulla mappa viene creato un nuovo marcatore. Il marcatore è posizionato in corrispondenza della posizione dell'indirizzo. Se il servizio di Google Geocoding non riesce a trovare l'indirizzo, viene mostrato un popup con un messaggio di errore.
Caso d'uso 2:
- L'utente clicca sulla mappa.
- I campi nascosti di latitudine e longitudine vengono aggiornati.
- Viene utilizzata una ricerca al contrario, per trovare l'indirizzo.
I seguenti campi devono essere inviati e gestiti dal form:
latitude
: float, tra 90 e -90longitude
: float, tra 180 e -180address
: string, solo testo
Le specifiche funzionali del widget sono state appena definite, ora si possono definire gli strumenti tecnici e i loro ambiti:
- Servizi Google map e Geocoding: visualizzano la mappa e recuperano le informazioni sull'indirizzo
- jQuery: aggiunge interazioni JavaScript tra il form e il campo
- sfForm: richiama il widget e convalida gli input
Il widget sfWidgetFormGMapAddress
Siccome un widget è la rappresentazione visiva dei dati, il metodo configure()
del widget deve avere diverse opzioni per modificare la mappa di Google o modificare gli stili di ogni elemento. Una delle opzioni più importanti è quella di template.html
, che definisce il modo in cui tutti gli elementi vengono ordinati. Quando si costruisce un widget, è molto importante pensare alla riusabilità e all'estensibilità.
Un'altra cosa importante è la definizione delle risorse esterne. Una classe sfWidgetForm
può implementare due metodi specifici:
-
getJavascripts()
deve restituire un array di file JavaScript; -
getStylesheets()
deve restituire un array di file di fogli di stile (dove la chiave è il percorso e il valore è il nome del media).
Il widget attuale richiede poco JavaScript per poter funzionare, quindi non è necessario nessun foglio di stile. In questo caso, tuttavia, il widget non consente di gestire l'inizializzazione del JavaScript di Google, anche se il widget si avvarrà del geocoding e dei servizi per le mappe di Google. Quindi, sarà responsabilità dello sviluppatore inserirlo nella pagina. La ragione che sta dietro a questo, è che i servizi di Google possono essere utilizzati da altri elementi della pagina e non solo dal widget.
Ecco il codice:
class sfWidgetFormGMapAddress extends sfWidgetForm { public function configure($options = array(), $attributes = array()) { $this->addOption('address.options', array('style' => 'width:400px')); $this->setOption('default', array( 'address' => '', 'longitude' => '2.294359', 'latitude' => '48.858205' )); $this->addOption('div.class', 'sf-gmap-widget'); $this->addOption('map.height', '300px'); $this->addOption('map.width', '500px'); $this->addOption('map.style', ""); $this->addOption('lookup.name', "Lookup"); $this->addOption('template.html', ' <div id="{div.id}" class="{div.class}"> {input.search} <input type="submit" value="{input.lookup.name}" id="{input.lookup.id}" /> <br /> {input.longitude} {input.latitude} <div id="{map.id}" style="width:{map.width};height:{map.height};{map.style}"></div> </div> '); $this->addOption('template.javascript', ' <script type="text/javascript"> jQuery(window).bind("load", function() { new sfGmapWidgetWidget({ longitude: "{input.longitude.id}", latitude: "{input.latitude.id}", address: "{input.address.id}", lookup: "{input.lookup.id}", map: "{map.id}" }); }) </script> '); } public function getJavascripts() { return array( '/sfFormExtraPlugin/js/sf_widget_gmap_address.js' ); } public function render($name, $value = null, $attributes = array(), $errors = array()) { // definisce le variabili principali del template $template_vars = array( '{div.id}' => $this->generateId($name), '{div.class}' => $this->getOption('div.class'), '{map.id}' => $this->generateId($name.'[map]'), '{map.style}' => $this->getOption('map.style'), '{map.height}' => $this->getOption('map.height'), '{map.width}' => $this->getOption('map.width'), '{input.lookup.id}' => $this->generateId($name.'[lookup]'), '{input.lookup.name}' => $this->getOption('lookup.name'), '{input.address.id}' => $this->generateId($name.'[address]'), '{input.latitude.id}' => $this->generateId($name.'[latitude]'), '{input.longitude.id}' => $this->generateId($name.'[longitude]'), ); // evita errori per formati non validi di $value $value = !is_array($value) ? array() : $value; $value['address'] = isset($value['address']) ? $value['address'] : ''; $value['longitude'] = isset($value['longitude']) ? $value['longitude'] : ''; $value['latitude'] = isset($value['latitude']) ? $value['latitude'] : ''; // definisce il widget per l'indirizzo $address = new sfWidgetFormInputText(array(), $this->getOption('address.options')); $template_vars['{input.search}'] = $address->render($name.'[address]', $value['address']); // definisce i campi per longitudine e latitudine $hidden = new sfWidgetFormInputHidden; $template_vars['{input.longitude}'] = $hidden->render($name.'[longitude]', $value['longitude']); $template_vars['{input.latitude}'] = $hidden->render($name.'[latitude]', $value['latitude']); // fonde template e variabili return strtr( $this->getOption('template.html').$this->getOption('template.javascript'), $template_vars ); } }
Il widget usa il metodo generateId()
per generare l'id
di ciascun elemento. La variabile $name
è definita da sfFormFieldSchema
, quindi la variabile $name
è composta dal nome del form, eventuali nomi nidificati nello schema del widget e dal nome del widget così come definito nel configure()
del form.
Per esempio, se il nome del modulo è
user
, il nome dello schema annidato èlocation
e il nome del widget èaddress
, ilname
finale saràuser[location][address]
e l'id
saràuser_location_address
. In altre parole,$this->generateId($name.'[latitude]')
genererà un valido e unicoid
per il campolatitude
.
I diversi elementi degli attributi id
sono molto importanti in quanto vengono passati al blocco JavaScript (attraverso la variabile template.js
), in modo che il codice JavaScript sia in grado di gestire correttamente i vari elementi.
Il metodo render()
istanzia anche due widget interni: un widget sfWidgetFormInputText
, che è usato per rendere il campo indirizzo e un widget sfWidgetFormInputHidden
, che è usato per rendere i campi nascosti.
Il widget può essere testato rapidamente con questo piccolo pezzo di codice:
$widget = new sfWidgetFormGMapAddress(); echo $widget->render('user[location][address]', array( 'address' => '151 Rue montmartre, 75002 Paris', 'longitude' => '2.294359', 'latitude' => '48.858205' ));
Il risultato in output è:
<div id="user_location_address" class="sf-gmap-widget"> <input style="width:400px" type="text" name="user[location][address][address]" value="151 Rue montmartre, 75002 Paris" id="user_location_address_address" /> <input type="submit" value="Lookup" id="user_location_address_lookup" /> <br /> <input type="hidden" name="user[location][address][longitude]" value="2.294359" id="user_location_address_longitude" /> <input type="hidden" name="user[location][address][latitude]" value="48.858205" id="user_location_address_latitude" /> <div id="user_location_address_map" style="width:500px;height:300px;"></div> </div> <script type="text/javascript"> jQuery(window).bind("load", function() { new sfGmapWidgetWidget({ longitude: "user_location_address_longitude", latitude: "user_location_address_latitude", address: "user_location_address_address", lookup: "user_location_address_lookup", map: "user_location_address_map" }); }) </script>
La parte JavaScript del widget prende i differenti attributi id
e li lega a degli ascoltatori jQuery, in modo che venga richiamato un certo JavaScript quando vengono eseguite delle azioni. Il JavaScript aggiorna i campi nascosti con la longitudine e la latitudine forniti dal servizio di geocoding di Google.
L'oggetto JavaScript ha alcuni metodi interessanti:
-
init()
: il metodo in cui sono inizializzate tutte le variabili e gli eventi vengono legati a input diversi -
lookupCallback()
: un metodo statico usato dal metodo geocoder per cercare l'indirizzo fornito dall'utente -
reverseLookupCallback()
: è un altro metodo statico usato dal geocoder per convertire le longitudini e latitudini fornite, in un indirizzo valido.
Il codice javascript finale si può vedere nell'Appendice A.
Si prega di fare riferimento alla documentazione di Google map per maggiori dettagli sulle funzionalità delle API.
Il validatore sfValidatorGMapAddress
La classe sfValidatorGMapAddress
estende sfValidatorBase
che già esegue una validazione: nello specifico, se il campo è impostato come required allora il valore non può essere null
. Pertanto, sfValidatorGMapAddress
ha bisogno di validare solamente i valori diversi: latitude
, longitude
e address
. La variabile $value
dovrebbe essere un array, ma siccome l'input dell'utente non è affidabile, il validatore verifica la presenza di tutte le chiavi, in modo che i validatori interni possano essere passati come valori validi.
class sfValidatorGMapAddress extends sfValidatorBase { protected function doClean($value) { if (!is_array($value)) { throw new sfValidatorError($this, 'invalid'); } try { $latitude = new sfValidatorNumber(array( 'min' => -90, 'max' => 90, 'required' => true )); $value['latitude'] = $latitude->clean(isset($value['latitude']) ? $value['latitude'] : null); $longitude = new sfValidatorNumber(array( 'min' => -180, 'max' => 180, 'required' => true )); $value['longitude'] = $longitude->clean(isset($value['longitude']) ? $value['longitude'] : null); $address = new sfValidatorString(array( 'min_length' => 10, 'max_length' => 255, 'required' => true )); $value['address'] = $address->clean(isset($value['address']) ? $value['address'] : null); } catch(sfValidatorError $e) { throw new sfValidatorError($this, 'invalid'); } return $value; } }
Un validatore solleva sempre l'eccezione
sfValidatorError
quando un valore non è valido. Questo è il motivo per cui la validazione è circondata da un bloccotry/catch
. In questo validatore, il validatore rilancia una nuova eccezioneinvalid
, il che equivale a un errore di validazioneinvalid
sul validatoresfValidatorGMapAddress
.
I test
Perché è importante fare i test? Il validatore è il collante tra l'input dell'utente e l'applicazione. Se il validatore è mal fatto, l'applicazione è vulnerabile. Per fortuna, symfony ha lime
, che è una libreria per i test molto facile da usare.
Come si può testare il validatore? Come già detto, un validatore solleva un'eccezione su un errore di convalida. Il test può inviare valori validi e invalidi al validatore e verificare che l'eccezione viene lanciata nelle circostanze corrette.
$t = new lime_test(7, new lime_output_color()); $tests = array( array(false, '', 'empty value'), array(false, 'string value', 'string value'), array(false, array(), 'empty array'), array(false, array('address' => 'my awesome address'), 'incomplete address'), array(false, array('address' => 'my awesome address', 'latitude' => 'String', 'longitude' => 23), 'invalid values'), array(false, array('address' => 'my awesome address', 'latitude' => 200, 'longitude' => 23), 'invalid values'), array(true, array('address' => 'my awesome address', 'latitude' => '2.294359', 'longitude' => '48.858205'), 'valid value') ); $v = new sfValidatorGMapAddress; $t->diag("Testing sfValidatorGMapAddress"); foreach($tests as $test) { list($validity, $value, $message) = $test; try { $v->clean($value); $catched = false; } catch(sfValidatorError $e) { $catched = true; } $t->ok($validity != $catched, '::clean() '.$message); }
Quando viene chiamato il metodo sfForm::bind()
, il form esegue il metodo clean()
di ciascun validatore. Il test riproduce questo comportamento istanziando direttamente il validatore sfValidatorGMapAddress
e testando valori diversi.
Considerazioni finali
L'errore più comune quando si crea un widget è quello di essere troppo concentrati su come le informazioni saranno memorizzate nel database. Il framework dei form è semplicemente un contenitore di dati e un framework di validazione. Pertanto, un widget deve gestire solamente le proprie informazioni. Se i dati sono validi, i diversi valori "puliti" possono poi essere utilizzati dal modello o dal controllore.
インデックス
Document Index
関連ページリスト
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

