#211 Validations in Rails 3
- Download:
- source codeProject Files in Zip (93.4 KB)
- mp4Full Size H.264 Video (15.9 MB)
- m4vSmaller H.264 Video (10.2 MB)
- webmFull Size VP8 Video (25.7 MB)
- ogvFull Size Theora Video (20.5 MB)
In questo episodio daremo un’occhiata alle nuove aggiunte ai meccanismi di validazione di Rails 3. Prima di farlo, comunque, aggiorneremo Rails alla versione beta più recente, la beta 3.
Aggiornare Rails 3
Possiamo installare questa nuova versione allo stesso modo in cui abbiamo installato le versioni precedenti, lanciando:
gem install rails --pre
Dopo aver installato la nuova beta, potreste avere un po’ di difficoltà a lanciare il comando rails
e vedere messaggi di errore tipo questi:
$ rails store /Users/eifion/.rvm/gems/ruby-head/gems/rails-3.0.0.beta3/bin/rails:1:in `require': no such file to load -- rails/cli (LoadError) from /Users/eifion/.rvm/gems/ruby-head/gems/rails-3.0.0.beta3/bin/rails:1:in `<top (required)>' from /Users/eifion/.rvm/gems/ruby-head/bin/rails:19:in `load' from /Users/eifion/.rvm/gems/ruby-head/bin/rails:19:in `<main>'
Questo tipo di errore è causato da errori di retro-compatibilità con la precedente versione beta. Per sistemare questa cosa, possiamo lanciare:
gem cleanup
che rimuoverà le versioni più vecchie dei gem installati. Ora potete lanciare il comando rails
senza problemi e creare l’applicazione che useremo per questo episodio:
rails store
Le nuove funzionalità
Ora che abbiamo un’applicazione Rails funzionante, possiamo cominciare a guardare alcune nuove funzionalità dei meccanismi di validazione. Cominciamo generando uno scaffold per il modello User
con due attributi: un name e un indirizzo email:
rails g scaffold user name:string email:string
Poi migriamo il database al solito modo:
rake db:migrate
Se diamo un’occhiata al partial della form generato dallo scaffold, vedremo il codice che mostra gli errori di validazione inline:
<%= form_for(@user) do |f| %> <% if @user.errors.any? %> <div id="errorExplanation"> <h2><%= pluralize(@user.errors.count, "error") %> prohibited this user from being saved:</h2> <ul> <% @user.errors.full_messages.each do |msg| %> <li><%= msg %></li> <% end %> </ul> </div> <% end %> <div class="field"> <%= f.label :name %><br /> <%= f.text_field :name %> </div> <div class="field"> <%= f.label :email %><br /> <%= f.text_field :email %> </div> <div class="actions"> <%= f.submit %> </div> <% end %>
Tutto ciò è diverso dal codice per mostrare dei messaggi di errore che ci potremmo aspettare di vedere normalmente: &ehrave; piuttosto verboso per una vista. La ragione è che nell’ultima versione il metodo helper error_messages_for
non è più disponibile e neppure:
<%= f.error_messages %>
Questi metodi sono stati estratti e messi in un plugin chiamato dynamic_form. Installando questo plugin, riavremo i metodi e potremo proseguire senza preoccupazioni come prima. Tuttavia, ci sono delle ragioni per cui questi metodi sono stati resi disponibili come plugin anzichè come framework standard, per cui non è, consigliabile farlo.
La ragione per cui si è deciso di rimuovere del framework i messaggi di errore è che la visualizzazione di tali messaggi ha spesso bisogno di essere personalizzata, e farlo attraverso i vecchi metodi era un po’ scomodo e poco flessibile rispetto ad avere i messaggi di errore inline come HTML come è ora. Avere a che fare direttamente con l’HTML all’interno delle viste significa che possiamo cambiare la visualizzazione dei messaggi di errore nel come vogliamo.
Naturalmente il rovescio della medaglia di avere i messaggi di errore inline su ogni form di ogni modello è che si tendono a generare duplicazioni nel codice. Per ridurle, possiamo estrarre il codice dell’ errore in un partial. Per fare in modo di poter usare tale partial su diversi modelli, lo mettiamo in una cartella shared
e lo chiamiamo _error_messages.html.erb
, passandogli l’oggetto della questione (target), nel nostro caso il nostro user:
<%= form_for(@user) do |f| %> <%= render "shared/error_messages", :target => @user %> <div class="field"> <%= f.label :name %><br /> <%= f.text_field :name %> </div> <div class="field"> <%= f.label :email %><br /> <%= f.text_field :email %> </div> <div class="actions"> <%= f.submit %> </div> <% end %>
Nel nuovo partial incolliamo il codice tagliato via dalla form, cambiando la variabile @user
in target
, in modo che il riferimento all’oggetto sia parametrico rispetto a quanto passato al metodo. Cambiamo anche il messaggio di errore per renderlo più generico:
<% if target.errors.any? %> <div id="errorExplanation"> <h2><%= pluralize(target.errors.count, "error") %> prohibited this record from being saved:</h2> <ul> <% target.errors.full_messages.each do |msg| %> <li><%= msg %></li> <% end %> </ul> </div> <% end %>
Possiamo testare il nuovo partial aggiungendo la validazione all’attributo email
del nostro modello User
:
class User < ActiveRecord::Base validates_presence_of :email end
Se andiamo ora alla form di inserimento di un nuovo utente e facciamo il submit senza inserire il campo email, vedremo al solito gli errori di validazione:
Dunque, è piuttosto semplice spostare il codice dei messaggi di errore in un partial e mantenere il tutto flessibile. Se vogliamo invece usare i vecchi metodi helper, possiamo usare una tecnica simile a quella usata da Ryan Bates nel suo Nifty Generators. Questo plugin ha un module ErrorMessagesHelper
che include un metodo error_messages_for
e un metodo error_messages
per i costruttori delle form. Sono versioni ridotte degli stessi metodi un tempo forniti direttamente da Rails e sono semplici da personalizzare secondo le necessità, per cui se volete usarli, potete aggiungere questo metodo helper all’applicazione.
Introspezione sulle validazioni
Un’altra nuova funzionalità di Rails 3 è la possibilità di introspezione sui validatori. La possiamo utilizzare, per esempio, se volessimo mettere un asterisco vicino ad ogni campo della form che è obbligatorio. Un tempo, per fare la stessa cosa, avremmo dovuto installare un plugin esterno per poter determinare quali validazioni erano state disposte per il modello sotteso alla form (nel nostro caso il modello User), ma ora che tutto ciò è incluso in Rails, è molto più facile aggiungere un indicatore ad ogni campo.
Possiamo illustrare la cosa facilmente da console. Possiamo chiamare validators
su di un modello, per ottenere un array delle validazioni che si applicano a quel modello:
ruby-head > User.validators => [#<ActiveModel::Validations::PresenceValidator:0x00000100badfc8 @attributes=[:email], @options={}>]
Abbiamo solamente una validazione sul nostro modello User, per cui il metodo ci restituirà solo quell’unico validatore che, dal momento che la validazione è una validates_presence_of
, sarà di tipo PresenceValidator
.
Possiamo ottenere i validatori per un campo specifico, anzichè tutti, usando il metodo validators_on
:
ruby-head > User.validators_on(:email) => [#<ActiveModel::Validations::PresenceValidator:0x00000100badfc8 @attributes=[:email], @options={}>]
Quest’ultimo restituirà un validatore come quello che agisce sull’attributo email
. Se chiamiamo il metodo sull’attributo name, che non ha validatori, allora verrà restituito un array vuoto:
ruby-head > User.validators_on(:name) => []
Idealmente vorremmo eseguire questo controllo nel costruttore della form per fare in modo che l’asterisco sia aggiunto in automatico nel modo più efficiente, ma tutto ciò andrebbe oltre agli obiettivi di questo episodio, per cui aggiungeremo un metodo helper personalizzato che chiameremo mark_required
. Questo metodo accetterà due parametri: un modello e un attributo per quel modello. Per l’attributo email
del nostro modello User
, questo metodo sarà invocato così:
<%= mark_required(@user, :email) %>
Creiamo il metodo mark_required
nell’ApplicationHelper
:
module ApplicationHelper def mark_required(object, attribute) "*" if object.class.validators_on(attribute).map(&:class).include? ActiveModel::Validations::PresenceValidator end end
Il metodo restituisce una stringa contenente un asterisco se il validatore dell’attributo del modello include un PresenceValidator
. Ora che abbiamo creato questo metodo, possiamo usarlo con ogni campo della nostra form:
<%= form_for(@user) do |f| %> <%= render "shared/error_messages", :target => @user %> <div class="field"> <%= f.label :name %><%= mark_required(@user, :name) %><br /> <%= f.text_field :name %> </div> <div class="field"> <%= f.label :email %><%= mark_required(@user, :email) %><br /> <%= f.text_field :email %> </div> <div class="actions"> <%= f.submit %> </div> <% end %>
Come abbiamo detto poco fa, questo non è il modo migliore per ottenere il nostro scopo in generale, dal momento che ci costringe ad aggiungere manualmente una chiamata al metodo su ogni campo, ma estendere FormBuilder
per gestire questa cosa non dovrebbe essere troppo complicato.
Se ricarichiamo la pagina con la form di registrazione del nuovo utente, ora troveremo un asterisco a fianco del campo email
, ma non in prossimità di quello relativo alla property name
:
Validazioni a livello di modello
Fino ad ora abbiamo trattato solo di validazioni a livello di viste, ma esistono alcune eccellenti aggiunte al meccanismo di validazione anche per il livello di modello. Per ulteriori dettagli in merito, vale la pena di leggersi l’articolo sul blog di Mikel Lindsaar. Un’importante differenza è che le validazioni sono state spostate fuori dalla classe ActiveRecord, nella nuova classe ActiveModel. Questo rende più semplice l’aggiunta di validazioni ad oggetti che non sono di tipo ActiveRecord ed è un qualcosa che esamineremo più in dettaglio nei futuri episodi.
Ciò su cui ci focalizzeremo oggi è il come rendere le validazioni nel nostro modello User
più efficienti. Abbiamo aggiunto un paio di validazioni al modello, che ora appare così:
class User < ActiveRecord::Base validates_presence_of :email validates_uniqueness_of :email validates_format_of :email, :with => /^[\w\d]+$/ :on => :create, :message => "is invalid" end
In Rails 3 è possibile chiamare un metodo validates
e passargli un hash di attributi per definire le validazioni, anzichè definire ciascuna validazione in modo singolo:
class User < ActiveRecord::Base validates :email, :presence => true, :uniqueness => true, :format => { :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i } end
Nel modello User
stiamo ancora una volta validando che il campo abbia un valore e che quel valore sia unico. Per la validazione del formato ci sono una serie di opzioni che possiamo passare, per cui usiamo un hash secondario per definirle.
Possiamo fornire un qualsiasi numero di validazioni per un determinato attributo con un singolo comando. Anche se utile, potrebbe divenire fonte di confusione se ci sono molte validazioni da fare su un campo, comunque per la maggior parte dei casi funziona in modo molto utile.
Scrivere un validatore personalizzato
Diamo un’occhiata ora a come possiamo rendere l’opzione :format
più concisa e pulirla un po’. Capita spesso di voler validare indirizzi email ed avere la stessa lunga espressione regolare in ogni validatore è piuttosto bruttino e introduce duplicazioni nel codice. Possiamo estrarre queste logica in un validatore separato, creando una nuova classe nella cartella /lib
della nostra applicazione. Chiameremo il file email_format_validator.rb
:
class EmailFormatValidator < ActiveModel::EachValidator def validate_each(object, attribute, value) unless value =~ /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i object.errors[attribute] << (options[:message] || "is not formatted properly") end end end
La classe EmailFormatValidator
eredita da ActiveModel::EachValidator
. Dobbiamo definire un metodo nella classe, il validate_each
, che accetti tre parametri chiamati, rispettivamente, object
, attribute
e value
. Il metodo poi deve controllare che il valore (value) faccia match con l’espressione regolare che abbiamo usato per validare l’indirizzo email di prima, e in caso contrario, aggiunga l’attributo all’insieme degli errori (errors
) dell’oggetto.
Possiamo usare questa tecnica per definire qualsiasi tipo di validazione vogliamo. Per esempio, nel post del blog citato poco fa, si usa un insieme molto più complesso di validazioni per controllare il campo email. Ora che abbiamo a disposizione il nostro validatore personalizzato, possiamo aggiornare la classe di modello User per usarlo:
class User < ActiveRecord::Base validates :email, :presence => true, :uniqueness => true, :email_format => true end
Avere una chiave email_format
nell’hash validates
significa che il validatore cercherà una classe chiamata email_format_validator
e le passerà il controllo per la validazione.
Se proviamo ora a creare un nuovo utente, inserendo un indirizzo email non valido, vedremo il messaggio di errore atteso:
L’essere in grado di creare validatori personalizzati come questo, ci permette di ripulire i validatori sulle classi di modello e rende più semplice la rimozione di duplicazioni fra i validatori.