The More with symfony book

Widget e validatori personalizzati

You are currently browsing
the website for symfony 1

Visit the Symfony2 website


About

You are currently reading "The More with symfony book" which is licensed under the Creative Commons Attribution-Share Alike 3.0 Unported License license.

Master symfony

Be trained by SensioLabs experts (2 to 6 day sessions -- French or English).
trainings.sensiolabs.com

Books on symfony

Learn more about symfony with the official guides.
books.sensiolabs.com

L'audit Qualité par SensioLabs

200 points de contrôle de votre applicatif web.
audit.sensiolabs.com

Chapter Content

Funzionamento interno di widget e validatori

Funzionamento interno di sfWidgetForm

Funzionamento interno di sfValidatorBase

L'attributo options

Creare un semplice widget e un validatore

Il widget per le mappe di Google

Il widget sfWidgetFormGMapAddress

Il validatore sfValidatorGMapAddress

I test

Considerazioni finali

symfony training
Be trained by symfony experts
Feb 21: Köln (Getting Started with Symfony2 - English)
Feb 27: Köln (Mastering Symfony2 - English)
Mar 05: Köln (Web Development with Symfony2 - Deutsch)
Mar 05: Montreal (Web Development with Symfony2 - English)
Mar 05: Montreal (Getting Started with Symfony2 - English)
and more...

Search


powered by google
You are currently browsing "The More with symfony book" in Italian for the 1.4 version - Switch to language:
Creative Commons License This work is licensed under a Creative Commons Attribution-Share Alike 3.0 Unported License.
More with Symfony
Support symfony!
Buy this book
or donate.
Buy More with Symfony from amazon.com

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.

Inoltre, la classe sfWidgetForm implementa due importanti metodi:

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 oggetto sfFormFieldSchema, 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:

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:

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:

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 metodo doClean() 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.

Mashup "Google Map Address Widget"

Caso d'uso 1:

Caso d'uso 2:

I seguenti campi devono essere inviati e gestiti dal form:

Le specifiche funzionali del widget sono state appena definite, ora si possono definire gli strumenti tecnici e i loro ambiti:

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:

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, il name finale sarà user[location][address] e l'id sarà user_location_address. In altre parole, $this->generateId($name.'[latitude]') genererà un valido e unico id per il campo latitude.

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:

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 blocco try/catch. In questo validatore, il validatore rilancia una nuova eccezione invalid, il che equivale a un errore di validazione invalid sul validatore sfValidatorGMapAddress.

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.

L'utilizzo avanzato dei form »
« Email

Questions & Feedback

If you find a typo or an error, please register and open a ticket.

If you need support or have a technical question, please post to the official user mailing-list.