Caution: You are browsing the legacy symfony 1.x part of this website.

Capitolo 11 - Integrazione con Doctrine

1.4
Symfony version Language

In un progetto web, la maggior parte dei form è usata per creare o modificare oggetti del modello. Questi oggetti sono solitamente serializzati in un database grazie a un ORM. Il sistema di form di symfony offre un livello addizionale per interfacciarsi con Doctrine, l'ORM predefinito in symfony, rendendo l'implementazione dei form basati su questi oggetti più facile.

Questo capitolo dettaglia il modo in cui integrare i form con gli oggetti del modello di Doctrine. È caldamente consigliato di essere già pratici con Doctrine e la sua integrazione in symfony. Se non fosse così, fare riferimento al capitolo All'interno del layer Modello nella guida a symfony.

Prima di cominciare

In questo capitolo creeremo un sistema di gestione di articoli. Inizieremo con lo schema del database, che è composto da cinque tabelle: article, author, category, tag e article_tag, come mostrato nel Listato 4-1.

Listato 4-1 - Schema del database

# config/doctrine/schema.yml
Article:
  actAs: [Sluggable, Timestampable]
  columns:
    title:
      type: string(255)
      notnull: true
    content:
      type: clob
    status: string(255)
    author_id: integer
    category_id: integer
    published_at: timestamp
  relations:
    Author:
      foreignAlias: Articles
    Category:
      foreignAlias: Articles
    Tags:
      class: Tag
      refClass: ArticleTag
      foreignAlias: Articles
Author:
  columns:
    first_name: string(20)
    last_name: string(20)
    email: string(255)
    active: boolean
Category:
  columns:
    name: string(255)
Tag:
  columns:
    name: string(255)
ArticleTag:
  columns:
    article_id:
      type: integer
      primary: true
    tag_id:
      type: integer
      primary: true
  relations:
    Article:
      onDelete: CASCADE
    Tag:
      onDelete: CASCADE

Ecco le relazioni tra le tabelle:

  • Relazione 1-n tra la tabella article e la tabella author: un articolo è scritto da uno e un solo autore
  • Relazione 1-n tra la tabella article e la tabella category: un articolo appartiene a una o nessuna categoria
  • Relazione n-n tra le tabelle article e tag

Generazione delle classi dei form

Vogliamo modificare le informazioni sulle tabelle article, author, category e tag. Per poterlo fare, abbiamo bisogno di creare dei form legati a ciascuna di queste tabelle e di configurare dei widget e dei validatori correlati allo schema del database. Pur essendo possibile creare tali form a mano, è un processo lungo, noioso e soprattutto che costringe a ripetere lo stesso tipo di informazione in diversi file (nomi di colonne e campi, dimensione massima di colonne e campi, ecc...). Inoltre, ogni volta che cambiamo il modello, dobbiamo cambiare anche le relative classi dei form. Fortunatamente, il plugin Doctrine ha un task doctrine:build-forms che automatizza il processo di generazione dei form legati agli oggetti del modello:

$ ./symfony doctrine:build-forms

Durante la generazione dei form, il task crea una classe per tabelle con validatori e widget per ogni colonna, usando l'introspezione del modello e considerando le relazioni tra le tabelle.

note

Anche doctrine:build-all e doctrine:build-all-load aggiornano le classi dei form, invocando automaticamente il task doctrine:build-forms.

Dopo aver eseguito questi task, una struttura di file è stata creata

nella cartella lib/form/. Ecco i file creati per il nostro schema di esempio:

lib/
  form/
    doctrine/
      ArticleForm.class.php
      ArticleTagForm.class.php
      AuthorForm.class.php
      CategoryForm.class.php
      TagForm.class.php
      base/
        BaseArticleForm.class.php
        BaseArticleTagForm.class.php
        BaseAuthorForm.class.php
        BaseCategoryForm.class.php
        BaseFormDoctrine.class.php
        BaseTagForm.class.php

Il task doctrine:build-forms genera due classi per ogni tabella dello schema, una classe base nella cartella lib/form/base e una nella cartella lib/form/. Ad esempio la tabella author ha le classi generate BaseAuthorForm e AuthorForm nei file lib/form/base/BaseAuthorForm.class.php e lib/form/AuthorForm.class.php.

La tabella seguente riassume la gerarchia tra le varie classi coinvolte nella definizione del form AuthorForm.

Classe Package Per Descrizione
AuthorForm project sviluppatore Sovrascrive il form generato
BaseAuthorForm project symfony Basato sullo schema e sovrascritto a ogni esecuzione di doctrine:build-forms
BaseFormDoctrine project sviluppatore Consente la personalizzazione globale dei form Doctrine
sfFormDoctrine Doctrine plugin symfony Base dei form Doctrine
sfForm symfony symfony Base dei form symfony

Per creare o modificare un oggetto della classe Author, useremo la classe AuthorForm, descritta nel Listato 4-2. Come si può notare, questa classe non contiene metodi perché eredita da BaseAuthorForm, che viene generata tramite la configurazione. La classe AuthorForm è la classe che useremo per personalizzare e sovrascrivere la configurazione.

Listato 4-2 - Classe AuthorForm

class AuthorForm extends BaseAuthorForm
{
  public function configure()
  {
  }
}

Il Listato 4-3 mostra la classe BaseAuthorForm, con i validatori e i widget generati tramite l'introspezione del modello per la tabella author.

Listato 4-3 - Classe BaseAuthorForm, che rappresenta il form della tabella author

class BaseAuthorForm extends BaseFormDoctrine
{
  public function setup()
  {
    $this->setWidgets(array(
      'id'         => new sfWidgetFormInputHidden(),
      'first_name' => new sfWidgetFormInputText(),
      'last_name'  => new sfWidgetFormInputText(),
      'email'      => new sfWidgetFormInputText(),
    ));
 
    $this->setValidators(array(
      'id'         => new sfValidatorDoctrineChoice(array('model' => 'Author', 'column' => 'id', 'required' => false)),
      'first_name' => new sfValidatorString(array('max_length' => 20, 'required' => false)),
      'last_name'  => new sfValidatorString(array('max_length' => 20, 'required' => false)),
      'email'      => new sfValidatorString(array('max_length' => 255)),
    ));
 
    $this->widgetSchema->setNameFormat('author[%s]');
 
    $this->errorSchema = new sfValidatorErrorSchema($this->validatorSchema);
 
    parent::setup();
  }
 
  public function getModelName()
  {
    return 'Author';
  }
}

La classe generata assomiglia molto ai form che abbiamo già creato nel capitolo precedente, tranne per alcuni aspetti:

  • La classe base è BaseFormDoctrine invece di sfForm
  • Le configurazioni del validatore e del widget stanno nel metodo setup() invece che nel metodo configure()
  • Il metodo getModelName() restituisce la classe Doctrine correlata a questo form

sidebar

Personalizzazione globale dei form Doctrine

Oltre alle classi generate per ogni tabella, doctrine:build-forms genera anche la classe BaseFormDoctrine. Questa classe vuota è la classe base della cartella lib/form/base/ e consente di configurare il comportamento di ogni form Doctrine globalmente. Per esempio, è possibile cambiare facilmente il formattatore predefinito di tutti i form Doctrine:

abstract class BaseFormDoctrine extends sfFormDoctrine
{
  public function setup()
  {
    sfWidgetFormSchema::setDefaultFormFormatterName('div');
  }
}

Avrete notato che la classe BaseFormDoctrine eredita dalla classe sfFormDoctrine. Questa classe incorpora delle funzionalità specifiche di Doctrine e tra le altre cose gestisce la serializzazione degli oggetti nel database dai valori inviati nel form.

tip

Le classi base usano il metodo setup() per la configurazione al posto del metodo configure(). Questo consente allo sviluppatore di sovrascrivere la configurazione delle classi generate vuote senza gestire la chiamata a parent::configure().

I nomi dei campi del form sono identici ai nomi delle colonne dello schema: id, first_name, last_name, email.

Per ogni colonna della tabella author, il task doctrine:build-forms genera un widget e un validatore secondo la definizione dello schema. Il task genera sempre i validatori più sicuri possibile. Consideriamo il campo id. Potremmo solo verificare se il valore è un intero valido. Invece il validatore generato ci consente anche di validare che l'identificatore esista veramente (per modificare un oggetto esistente) o che sia vuoto (per poter creare un nuovo oggetto). Questa è una validazione più forte.

I form generati possono essere usati immediatamente. Aggiungete un'istruzione <?php echo $form ?> e questo consentirà di creare dei form funzionali con validazione senza scrivere una sola riga di codice.

Oltre alla possibilità di creare rapidamente dei prototipi, i form generati sono facilmente estensibili senza dover modificare le classi generate. Questo grazie al meccanismo di ereditarietà delle classi base e delle classi form.

Alla fine di ogni evoluzione dello schema del database, il task consente di generare nuovamente i form per considerare le modifiche allo schema, senza sovrascrivere le personalizzazioni che potreste aver fatto.

Il generatore CRUD

Ora che abbiamo le classi generate dei form, vediamo quanto è facile creare un modulo symfony per gestire gli oggetti da un browser. Vogliamo creare, modificare, cancellare gli oggetti delle classi Article, Author, Category, Tag. Iniziamo con la creazione del modulo per la classe Author. Anche se potremmo creare manualmente un modulo, il plugin Doctrine fornisce il task doctrine:generate-module, che genera un modulo CRUD basato sulle classi dei modelli degli oggetti Doctrine. Usando il form che abbiamo generato nella sezione precedente:

$ ./symfony doctrine:generate-module frontend author Author

doctrine:generate-module accetta tre parametri:

  • frontend : nome dell'applicazione in cui vuoi creare il modulo
  • author : nome del modulo che vuoi creare
  • Author : nome della classe del modello per cui vuoi creare il modulo

note

CRUD sta per Creation / Retrieval / Update / Deletion (Creazione / Recupero / Aggiornamento / Cancellazione) e riassume le quattro operazioni basilari che possiamo usare con i dati dei modelli.

Nel Listato 4-4 vediamo che il task ha generato sei azioni, che ci consentono di elencare (index), creare (create), mostrare i nuovi (new), modificare (edit), salvare (update), cancellare (delete) gli oggetti della classe Author.

Listato 4-4 - La classe authorActions generata dal task

// apps/frontend/modules/author/actions/actions.class.php
class authorActions extends sfActions
{
  public function executeIndex()
  {
    $this->author_list = Doctrine::getTable('Author')
      ->createQuery('a')
      ->execute();
  }
 
  public function executeNew(sfWebRequest $request)
  {
    $this->form = new AuthorForm();
  }
 
  public function executeCreate(sfWebRequest $request)
  {
    $this->forward404Unless($request->isMethod('post'));
 
    $this->form = new AuthorForm();
 
    $this->processForm($request, $this->form);
 
    $this->setTemplate('new');
  }
 
  public function executeEdit(sfWebRequest $request)
  {
    $this->forward404Unless($author = Doctrine::getTable('Author')->find($request->getParameter('id')), sprintf('Object author does not exist (%s).', $request->getParameter('id')));
    $this->form = new AuthorForm($author);
  }
 
  public function executeUpdate(sfWebRequest $request)
  {
    $this->forward404Unless($request->isMethod('post') || $request->isMethod('put'));
    $this->forward404Unless($author = Doctrine::getTable('Author')->find($request->getParameter('id')), sprintf('Object author does not exist (%s).', $request->getParameter('id')));
    $this->form = new AuthorForm($author);
 
    $this->processForm($request, $this->form);
 
    $this->setTemplate('edit');
  }
 
  public function executeDelete(sfWebRequest $request)
  {
    $request->checkCSRFProtection();
 
    $this->forward404Unless($author = Doctrine::getTable('Author')->find($request->getParameter('id')), sprintf('Object author does not exist (%s).', $request->getParameter('id')));
    $author->delete();
 
    $this->redirect('author/index');
  }
 
  protected function processForm(sfWebRequest $request, sfForm $form)
  {
    $form->bind($request->getParameter($form->getName()));
    if ($form->isValid())
    {
      $author = $form->save();
 
      $this->redirect('author/edit?id='.$author->getId());
    }
  }
}

In questo modulo, il ciclo di vita del form è gestito da quattro metodi: create, edit, update e processForm. È anche possibile chiedere al task di generare solo un metodo che copre le funzionalità di questi quattro metodi, come mostra il listato 4-5.

Listato 4-5 - La classe authorActions rifattorizzata

// In authorActions, sostituisce i metodi create, edit, update e processForm
public function executeEdit($request)
{
  $this->form = new AuthorForm(Doctrine::getTable('Author')->find($request->getParameter('id')));
 
  if ($request->isMethod('post'))
  {
    $this->form->bind($request->getParameter('author'));
    if ($this->form->isValid())
    {
      $author = $this->form->save();
      $this->redirect('author/edit?id='.$author->getId());
    }
  }
}

note

Gli esempi seguenti usano lo stile predefinito e prolisso, in modo da consentire delle personalizzazioni, se si vuole seguire l'approccio del listato 4-5. Ad esempio, nel template del form, si avrà bisogno solo di puntare il form all'azione di modifica, senza curarsi se l'oggetto sia nuovo o vecchio.

Il task genera anche tre template e un partial: indexSuccess, editSuccess, newSuccess e _form. Il template _form è stato generato senza usare l'istruzione <?php echo $form ?>. Possiamo modificare questo comportamento, usando --non-verbose-templates:

$ ./symfony doctrine:generate-module frontend author Author --non-verbose-templates

Questa opzione è utile durante la fase di prototipizzazione, come mostra il Listato 4-6.

Listato 4-6 - Il template editSuccess

// apps/frontend/modules/author/templates/_form.php
<?php include_stylesheets_for_form($form) ?>
<?php include_javascripts_for_form($form) ?>
 
<form action="<?php echo url_for('author/'.($form->getObject()->isNew() ? 'create' : 'update').(!$form->getObject()->isNew() ? '?id='.$form->getObject()->getId() : '')) ?>" method="post" <?php $form->isMultipart() and print 'enctype="multipart/form-data" ' ?>>
<?php if (!$form->getObject()->isNew()): ?>
<input type="hidden" name="sf_method" value="put" />
<?php endif; ?>
  <table>
    <tfoot>
      <tr>
        <td colspan="2">
          &nbsp;<a href="<?php echo url_for('author/index') ?>">Cancel</a>
          <?php if (!$form->getObject()->isNew()): ?>
            &nbsp;<?php echo link_to('Delete', 'author/delete?id='.$form->getObject()->getId(), array('method' => 'delete', 'confirm' => 'Are you sure?')) ?>
          <?php endif; ?>
          <input type="submit" value="Save" />
        </td>
      </tr>
    </tfoot>
    <tbody>
      <?php echo $form ?>
    </tbody>
  </table>
  </form>

tip

L'opzione --with-show ci consente di generare un'azione e un template che possiamo usare per vedere un oggetto (in sola lettura).

Ora si può aprire in un browser l'URL /frontend_dev.php/author per vedere il modulo generato (Figura 4-1 e Figura 4-2). Prendetevi un po' di tempo per giocare con l'interfaccia. Grazie al modulo generato puoi elencare gli autori, aggiungerne uno nuovo, modificare e anche cancellare. Noterete anche che le regole di validazione stanno funzionando. Notate che nelle figure seguenti abbiamo scelto di togliere il campo "active".

Figura 4-1 - Lista degli autori

Lista degli autori

Figura 4-2 - Modifica di un autore con errori di validazione

Modifica di un autore con errori di validazione

Possiamo ora ripetere l'operazione con la classe Article:

$ ./symfony doctrine:generate-module frontend article Article --non-verbose-templates

Il form ArticleForm usa il widget sfWidgetFormDoctrineSelect per rappresentare la relazione tra l'oggetto Article e l'oggetto Author. Questo widget crea un menù a tendina con gli autori. Durante la visualizzazione, gli oggetti autori sono convertiti in stringhe di caratteri usando il metodo magico __toString(), che deve essere definito nella classe Author, come mostrato nel Listato 4-7.

Listato 4-7 - Implementare il metodo __toString() per la classe Author

class Author extends BaseAuthor
{
  public function __toString()
  {
    return $this->getFirstName().' '.$this->getLastName();
  }
}

Proprio come la classe Author, puoi creare dei metodi __toString() per le altre classi del nostro modello: Article, Category, Tag.

note

sfDoctrineRecord cercherà di indovinare nel metodo __toString() base, se non si sta specificando il proprio. Verifica le colonne chiamate 'title', 'name', 'subject' e 'id' per usarle come rappresentazione di stringa. Se nessuno di questi campi viene trovato, Doctrine restituirà una stringa di avviso.

tip

L'opzione method del widget sfWidgetFormDoctrineSelect cambia il metodo usato per rappresentare un oggetto in formato testuale.

La Figura 4-4 mostra come creare un articolo dopo aver implementato il metodo __toString().

Figura 4-4 - Creare un articolo

Creare un articolo

note

Nella figura 4-4 si noterà che alcuni campi non compaiono nel form, ad esempio created_at e updated_at. Questo perché abbiamo personalizzato la classe del form. Vedremo come nella prossima sezione.

Personalizzare i form generati

I task doctrine:build-forms e doctrine:generate-module ci consentono di creare moduli funzionali in symfony per elencare, creare, modificare e cancellare gli oggetti del modello. Questi moduli tengono in considerazione non solo le regole di validazione del modello, ma anche le relazioni tra le tabelle. Tutto ciò avviene senza scrivere una sola riga di codice!

È arrivato il momento di personalizzare il codice generato. Se i form delle classi considerano già diversi elementi, alcuni aspetti avranno bisogno di una personalizzazione.

Personalizzare i validatori e i widget

Iniziamo col configurare i validatori e i widget generati di default.

Il form ArticleForm ha un campo slug. Lo slug è una stringa di caratteri che rappresenta univocamente l'articolo nell'URL. Per esempio, lo slug di un articolo il cui titolo è "Ottimizzare lo sviluppo con symfony" è 12-ottimizzare-lo-sviluppo-con-symfony, in cui 12 è l'id dell'articolo. Questo campo solitamente viene calcolato in modo automatico quando l'oggetto viene salvato e dipende dal titolo, ma ha la possibilità di essere sovrascritto esplicitamente dall'utente. Anche se questo campo è obbligatorio nello schema, non può esserlo nel form. Per questo motivo modifichiamo il validatore e lo rendiamo opzionale, come nel Listato 4-8. Personalizzeremo anche il campo content, aumentando la sua dimensione e forzando l'utente a inserirvi almeno cinque caratteri.

Listato 4-8 - Personalizzare i validatori e i widget

class ArticleForm extends BaseArticleForm
{
  public function configure()
  {
    // ...
 
    $this->validatorSchema['slug']->setOption('required', false);
    $this->validatorSchema['content']->setOption('min_length', 5);
 
    $this->widgetSchema['content']->setAttributes(array('rows' => 10, 'cols' => 40));
  }
}

Abbiamo usato gli oggetti validatorSchema e widgetSchema come array PHP. Questi array prendono il nome del campo come chiave e restituiscono rispettivamente l'oggetto validatore e il relativo oggetto widget.

note

Per poter usare gli oggetti come array PHP, le classi sfValidatorSchema e sfWidgetFormSchema implementano l'interfaccia ArrayAccess, disponibile dalla versione 5 di PHP.

Per assicurarci che due articoli non abbiano lo stesso slug, una costante di univocità è stata aggiunta nella definizione dello schema. Questa costante a livello di database ha una corrispondenza nel form ArticleForm tramite il validatore sfValidatorDoctrineUnique. Questo validatore può verificare l'univocità di ogni campo del form. È utile tra l'altro per verificare ad esempio l'univocità di un indirizzo email usato come login. Il Listato 4-9 mostra come usarlo nel form ArticleForm.

Listato 4-9 - Usare il validatore sfValidatorDoctrineUnique per verificare l'univocità di un campo

class BaseArticleForm extends BaseFormDoctrine
{
  public function setup()
  {
    // ...
 
    $this->validatorSchema->setPostValidator(
      new sfValidatorDoctrineUnique(array('model' => 'Article', 'column' => array('slug')))
    );
  }
}

Il validatore sfValidatorDoctrineUnique è un postValidator che gira su tutti i dati dopo la validazione individuale di ciascun campo. Per poter validare l'univocità dello slug, il validatore deve poter accedere non solo al valore di slug, ma anche al valore della chiave primaria (o delle chiavi primarie). Le regole di validazione sono quindi diverse tra creazione e modifica, poiché slug può restare lo stesso durante l'aggiornamento di un articolo.

Ora personalizziamo il campo active della tabella author, usato per sapere se l'utente è attivo. Il Listato 4-10 mostra come escludere gli autori inattivi dal form ArticleForm, modificando l'opzione query del widget sfWidgetFormDoctrineSelect connesso al campo author_id. L'opzione query accetta un oggetto Query di Doctrine, consentendo di accorciare la lista delle opzioni disponibili.

Listato 4-10 - Personalizzare il widget sfWidgetFormDoctrineSelect

class ArticleForm extends BaseArticleForm
{
  public function configure()
  {
    // ...
 
    $query = Doctrine_Query::create()
      ->from('Author a')
      ->where('a.active = ?', true);
    $this->widgetSchema['author_id']->setOption('query', $query);
  }
}

Anche se la personalizzazione dei widget può farci accorciare la lista delle opzioni disponibili, non dobbiamo dimenticare di considerare questa abbreviazione a livello di validatore, come mostrato nel Listato 4-11. Come il widget sfWidgetProperSelect, il validatore sfValidatorDoctrineChoice accetta un'opzione query per accorciare le opzioni valide per un campo.

Listato 4-11 - Personalizzare il validatore sfValidatorDoctrineChoice

class ArticleForm extends BaseArticleForm
{
  public function configure()
  {
    // ...
 
    $query = Doctrine_Query::create()
      ->from('Author a')
      ->where('a.active = ?', true);
 
    $this->widgetSchema['author_id']->setOption('query', $query);
    $this->validatorSchema['author_id']->setOption('query', $query);
  }
}

Nell'esempio precedente abbiamo definito l'oggetto Query direttamente nel metodo configure(). Nel nostro progetto, questi criteri saranno certamente utili in altre circostanze, quindi è meglio creare un metodo getActiveAuthorsQuery() nella classe AuthorTable e richiamarlo da ArticleForm, come mostrato nel Listato 4-12.

Listato 4-12 - Rifattorizzare Query nel modello

class AuthorTable extends Doctrine_Table
{
  public function getActiveAuthorsQuery()
  {
    $query = Doctrine_Query::create()
      ->from('Author a')
      ->where('a.active = ?', true);
 
    return $query;
  }
}
 
class ArticleForm extends BaseArticleForm
{
  public function configure()
  {
    $authorQuery = Doctrine::getTable('Author')->getActiveAuthorsQuery();
    $this->widgetSchema['author_id']->setOption('query', $authorQuery);
    $this->validatorSchema['author_id']->setOption('query', $authorQuery);
  }
}

Cambiare un validatore

Essendo email definita come string(255) nello schema, symfony ha creato un validatore sfValidatorString() che limita la lunghezza massima a 255 caratteri. Inoltre si suppone di avere in questo campo un'email valida. Il Listato 4-14 sostituisce il validatore generato con un validatore sfValidatorEmail.

Listato 4-13 - Cambiare il validatore del campo email della classe AuthorForm

class AuthorForm extends BaseAuthorForm
{
  public function configure()
  {
    $this->validatorSchema['email'] = new sfValidatorEmail();
  }
}

Aggiungere un validatore

Abbiamo osservato nel capitolo precedente come modificare i validatori generati. Ma nel caso del campo email, sarebbe utile mantenere la validazione della lunghezza massima. Nel Listato 4-14, usiamo il validatore sfValidatorAnd per garantire la validità dell'email e verificare la lunghezza massima consentita per il campo.

Listato 4-14 - Uso di un validatore multiplo

class AuthorForm extends BaseAuthorForm
{
  public function configure()
  {
    $this->validatorSchema['email'] = new sfValidatorAnd(array(
      new sfValidatorString(array('max_length' => 255)),
      new sfValidatorEmail(),
    ));
  }
}

L'esempio precedente non è perfetto, poiché se decidiamo più avanti di modificare la lunghezza del campo email nello schema del database, dovremo preoccuparci di farlo anche nel form. Invece di sostituire il validatore generato, è meglio aggiungerne uno, come mostrato nel Listato 4-15.

Listato 4-15 - Aggiunta di un validatore

class AuthorForm extends BaseAuthorForm
{
  public function configure()
  {
    $this->validatorSchema['email'] = new sfValidatorAnd(array(
      $this->validatorSchema['email'],
      new sfValidatorEmail(),
    ));
  }
}

Cambiare widget

Nello schema del database, il campo status della tabella article memorizza lo status dell'articolo come una stringa di caratteri. I valori possibili sono stati definiti nella classe ArticeTable, come mostrato nel Listato 4-16.

Listato 4-16 - Definizione degli status disponibili nella classe ArticleTable

class ArticleTable extends Doctrine_Table
{
  static protected $statuses = array('draft', 'online', 'offline');
 
  static public function getStatuses()
  {
    return self::$statuses;
  }
 
  // ...
}

Quando si modifica un articolo, il campo status deve essere rappresentato da un menù a tendina invece che da un campo di testo. Per fare questo, cambiamo il widget che abbiamo usato, come mostrato nel Listato 4-17.

Listato 4-17 - Modifica del widget del campo status

class ArticleForm extends BaseArticleForm
{
  public function configure()
  {
    $this->widgetSchema['status'] = new sfWidgetFormSelect(array('choices' => ArticleTable::getStatuses()));
  }
}

Per scrupolo, cambiamo anche il validatore, per assicurarci che lo status scelto appartenga veramente alla lista delle possibili opzioni (Listato 4-18).

Listato 4-18 - Modifica del validatore del campo status

class ArticleForm extends BaseArticleForm
{
  public function configure()
  {
    $statuses = ArticleTable::getStatuses();
 
    $this->widgetSchema['status'] = new sfWidgetFormSelect(array('choices' => $statuses));
 
    $this->validatorSchema['status'] = new sfValidatorChoice(array('choices' => array_keys($statuses)));
  }
}

Cancellare un campo

La tabella article ha due colonne speciali, created_at e updated_at, il cui aggiornamento è gestito automaticamente da Doctrine. Quindi dobbiamo cancellarle dal form, come mostra il Listato 4-19, per impedire all'utente di modificarli.

Listato 4-19 - Cancellare un campo

class ArticleForm extends BaseArticleForm
{
  public function configure()
  {
    // ...
 
    unset($this->validatorSchema['created_at']);
    unset($this->widgetSchema['created_at']);
 
    unset($this->validatorSchema['updated_at']);
    unset($this->widgetSchema['updated_at']);
 
    unset($this->validatorSchema['published_at']);
    unset($this->widgetSchema['published_at']);
  }
}

Per poter cancellare un campo, è necessario cancellare il suo validatore e il suo widget. Il Listato 4-20 mostra come sia possibile cancellarli entrambi in un solo colpo, usando il form come un array PHP.

Listato 4-20 - Cancellare un campo usando il form come array PHP

class ArticleForm extends BaseArticleForm
{
  public function configure()
  {
    unset($this['created_at'], $this['updated_at'], $this['published_at']);
  }
}

Riassunto

Per riassumere, il Listato 4-21 e il Listato 4-22 mostrano i form ArticleForm e AuthorForm dopo la nostra personalizzazione.

Listato 4-21 - form ArticleForm

class ArticleForm extends BaseArticleForm
{
  public function configure()
  {
    $authorQuery = Doctrine::getTable('Author')->getActiveAuthorsQuery();
 
    // widget
    $this->widgetSchema['content']->setAttributes(array('rows' => 10, 'cols' => 40));
    $this->widgetSchema['status'] = new sfWidgetFormSelect(array('choices' => ArticleTable::getStatuses()));
    $this->widgetSchema['author_id']->setOption('query', $authorQuery);
 
    // validatori
    $this->validatorSchema['slug']->setOption('required', false);
    $this->validatorSchema['content']->setOption('min_length', 5);
    $this->validatorSchema['status'] = new sfValidatorChoice(array('choices' => array_keys(ArticleTable::getStatuses())));
    $this->validatorSchema['author_id']->setOption('query', $authorQuery);
 
    unset($this['created_at'], $this['updated_at'], $this['published_at']);
  }
}

Listato 4-22 - form AuthorForm

class AuthorForm extends BaseAuthorForm
{
  public function configure()
  {
    $this->validatorSchema['email'] = new sfValidatorAnd(array(
      $this->validatorSchema['email'],
      new sfValidatorEmail(),
    ));
  }
}

L'uso di doctrine:build-forms consente di generare automaticamente la maggior parte degli elementi, tramite l'introspezione del modello. Questa automatizzazione è utile per diverse ragioni:

  • Rende più facile la vita dello sviluppatore, risparmiandogli del lavoro ripetitivo e ridondante. Egli può quindi focalizzarsi sulla personalizzazione dei validatori e dei widget, secondo le specifiche business rule del progetto.

  • Inoltre, quando lo schema del database viene aggiornato, i form generati sono aggiornati automaticamente. Lo sviluppatore deve solo raffinare la personalizzazione che ha già eseguito.

La prossima sezione descriverà la personalizzazione delle azioni e dei template generati dal task doctrine:generate-module.

Serializzazione dei form

La sezione precedente ci ha mostrato come personalizzare i form generate dal task doctrine:build-forms. In questa sezione personalizzeremo il ciclo di vita dei form, iniziando dal codice generato dal task doctrine:generate-module.

Valori di default

Un'istanza Doctrine è sempre connessa a un oggetto Doctrine. L'oggetto Doctrine collegato appartiene sempre alla classe restituita dal metodo getModelName(). Ad esempio, il form AuthorForm può essere collegato solo a oggetti che appartengono alla classe Author. Questo oggetto è o un oggetto vuoto (un'istanza vuota della classe Author), oppure l'oggetto inviato al costruttore come primo parametro. Dal momento che il costruttore di un form "medio" accetta un array di valori come primo parametro, il costruttore di un form Doctrine accetta un oggetto Doctrine. Tale oggetto viene usato per definire il valore di default di ogni campo del form. Il metodo getObject() restituisce l'oggetto correlato all'istanza corrente e il metodo isNew() consente di sapere se l'oggetto è stato inviato tramite il costruttore:

// creazione di un nuovo oggetto
$authorForm = new AuthorForm();
 
print $authorForm->getObject()->getId(); // visualizza null
print $authorForm->isNew();              // visualizza true
 
// modifica di un oggetto esistente
$author = Doctrine::getTable('Author')->find(1);
$authorForm = new AuthorForm($author);
 
print $authorForm->getObject()->getId(); // visualizza 1
print $authorForm->isNew();              // visualizza false

Gestire il ciclo di vita

Come abbiamo osservato all'inizio del capitolo, l'azione edit, mostrata nel Listato 4-23, gestisce il ciclo di vita del form.

Listato 4-23 - I metodi executeNew, executeEdit, executeCreate e processForm del modulo author

// apps/frontend/modules/author/actions/actions.class.php
class authorActions extends sfActions
{
  // ...
  public function executeNew(sfWebRequest $request)
  {
    $this->form = new AuthorForm();
  }
 
  public function executeCreate(sfWebRequest $request)
  {
    $this->forward404Unless($request->isMethod('post'));
 
    $this->form = new AuthorForm();
 
    $this->processForm($request, $this->form);
 
    $this->setTemplate('new');
  }
 
  public function executeEdit(sfWebRequest $request)
  {
    $this->forward404Unless($author = Doctrine::getTable('Author')->find($request->getParameter('id')), sprintf('Object author does not exist (%s).', $request->getParameter('id')));
    $this->form = new AuthorForm($author);
  }
 
  protected function processForm(sfWebRequest $request, sfForm $form)
  {
    $form->bind($request->getParameter($form->getName()));
    if ($form->isValid())
    {
      $author = $form->save();
 
      $this->redirect('author/edit?id='.$author->getId());
    }
  }
}

Anche se l'azione edit assomiglia alle azioni che possiamo aver descritto nei capitoli precedenti, possiamo puntualizzare alcune differenze:

  • Un oggetto Doctrine dalla classe Author è inviato come primo parametro al costruttore del form:

    $author = Doctrine::getTable('Author')->find($request->getParameter('id'));
    $this->form = new AuthorForm($author);
  • Il formato dell'attributo name dei widget è personalizzato automaticamente per consentire il recupero dei dati inviati in un array PHP che prende il nome dalla tabella relativa (author):

    $this->form->bind($request->getParameter('author'));
  • Quando il form è valido, una semplice chiamata al metodo save() crea o aggiorna l'oggetto Doctrine legato al form:

    $author = $this->form->save();

Creare e modificare un oggetto Doctrine

Il codice del Listato 4-23 gestisce con un solo metodo la creazione e la modifica di oggetti della classe Author:

  • Creazione di un nuovo oggetto Author:

    • L'azione index viene richiamata senza parametro id ($request->getParameter('id') è null)

    • La chiamata a find() di conseguenza restituisce null

    • L'oggetto form quindi è collegato a un oggetto Doctrine Author vuoto

    • La chiamata a $this->form->save() crea di conseguenza un nuovo oggetto Author quando un form valido viene inviato

  • Modifica di un oggetto Author esistente:

    • L'azione index viene richiamata con un parametro id ($request->getParameter('id') è la chiave primaria dell'oggetto Author da modificare)

    • La chiamata a find() restituisce l'oggetto Author relativo alla chiave primaria

    • L'oggetto form quindi è collegato all'oggetto appena trovato

    • La chiamata a $this->form->save() aggiorna l'oggetto Author quando un form valido viene inviato

Il metodo save()

Quando un form Doctrine è valido, il metodo save() aggiorna l'oggetto correlato e lo memorizza nel database. Questo metodo in realtà memorizza non solo l'oggetto principale, ma anche gli eventuali oggetti correlati. Ad esempio, il form ArticleForm aggiorna i tag connessi a un articolo. Essendo la relazione tra le tabelle article e tag n-n, i tag relativi all'articolo sono salvati nella tabella article_tag (usando il metodo generato saveArticleTagList()).

Per assicurare una serializzazione coerente, il metodo save() inserisce ogni aggiornamento in una transazione.

note

Vedremo nel Capitolo 9 che il metodo save() aggiorna automaticamente anche le tabelle internazionalizzate.

sidebar

Usare il metodo bindAndSave()

Il metodo bindAndSave() collega i dati che l'utente ha inviato nel form, valida tale form e aggiorna gli oggetti correlati nel database, tutto in una sola operazione:

class articleActions extends sfActions
{
  public function executeCreate(sfWebRequest $request)
  {
    $this->form = new ArticleForm();
 
    if ($request->isMethod('post') && $this->form->bindAndSave($request->getParameter('article')))
    {
      $this->redirect('article/created');
    }
  }
}

Gestione dell'invio di file

Il metodo save() aggiorna automaticamente gli oggetti Doctrine, ma non può gestire gli elementi collaterali come l'invio di file.

Vediamo come allegare un file a ogni articolo. I file sono memorizzati nella cartella web/uploads e un riferimento al percorso del file viene tenuto nel campo file della tabella article, come mostrato nel Listato 4-24.

Listato 4-24 - Schema per la tabella article con file associato

# config/schema.yml
doctrine:
  article:
    // ...
    file: string(255)

Dopo ogni aggiornamento dello schema, si deve aggiornare il modello degli oggetti, il database e i relativi form:

$ ./symfony doctrine:build-all

caution

Ricordare che il task doctrine:build-all cancella tutte le tabelle dello schema e le ricrea. I dati presenti nelle tabelle sono quindi sovrascritti. Per questo è importante creare dei dati di test (fixtures), che si possono ricaricare a ogni modifica del modello.

Il Listato 4-25 mostra come modificare la classe ArticleForm per collegare un widget e un validatore al campo file.

Listato 4-25 - Modifica del campo file del form ArticleForm.

class ArticleForm extends BaseArticleForm
{
  public function configure()
  {
    // ...
 
    $this->widgetSchema['file'] = new sfWidgetFormInputFile();
    $this->validatorSchema['file'] = new sfValidatorFile();
  }
}

Come per tutti i form che consentono l'invio di file, non dimenticare di aggiungere nel template l'attributo enctype al tag form (si veda il Capitolo 2 per ulteriori informazioni sulla gestione dell'invio dei file).

Il Listato 4-26 mostra le modifiche da applicare quando si salva il form per caricare il file sul server e memorizzarne il percorso nell'oggetto article.

Listato 4-26 - Salvataggio dell'oggetto article e file caricato nell'azione

public function executeEdit($request)
{
  $author = Doctrine::getTable('Author')->find($request->getParameter('id'));
  $this->form = new ArticleForm($author);
 
  if ($request->isMethod('post'))
  {
    $this->form->bind($request->getParameter('article'), $request->getFiles('article'));
    if ($this->form->isValid())
    {
      $file = $this->form->getValue('file');
      $filename = sha1($file->getOriginalName()).$file->getExtension($file->getOriginalExtension());
      $file->save(sfConfig::get('sf_upload_dir').'/'.$filename);
 
      $article = $this->form->save();
 
      $this->redirect('article/edit?id='.$article->getId());
    }
  }
}

Il salvataggio del file caricato su filesystem consente all'oggetto sfValidatedFile di conoscere il percorso assoluto del file. Durante la chiamata al metodo save(), i valori dei campi sono usati per aggiornare l'oggetto relativo e, come per il campo file, l'oggetto sfValidatedFile viene convertito in una stringa grazie al metodo __toString(), rimandando il percorso assoluto al file. La colonna file della tabella article conterrà tale percorso assoluto.

tip

Se si vuole memorizzare il percorso relativo alla cartella sfConfig::get('sf_upload_dir'), si può creare una classe che eredita da sfValidatedFile e usare l'opzione validated_file_class per inviare al validatore sfValidatorFile il nome della nuova classe. Il validatore restituirà un'istanza della propria classe. Vedremo nel resto di questo capitolo un altro approccio, che consiste nel modificare il valore della colonna file prima di salvare l'oggetto nel database.

Personalizzare il metodo save()

Abbiamo osservato nella sezione precedente come salvare un file caricato nell'azione edit. Uno dei principi della programmazione orientata agli oggetti è la riusabilità del codice, grazie al suo incapsulamento in classi. Invece di duplicare il codice usato per salvare il file in ogni azione usando il form ArticleForm, è meglio spostarlo nella classe ArticleForm. Il Listato 4-27 mostra come sovrascrivere il metodo save() per salvare anche il file ed eventualmente cancellare un file esistente.

Listato 4-27 - Sovrascrivere il metodo save() della classe ArticleForm

class ArticleForm extends BaseFormDoctrine
{
  // ...
 
  public function save($con = null)
  {
    if (file_exists($this->getObject()->getFile()))
    {
      unlink($this->getObject()->getFile());
    }
 
    $file = $this->getValue('file');
    $filename = sha1($file->getOriginalName()).$file->getExtension($file->getOriginalExtension());
    $file->save(sfConfig::get('sf_upload_dir').'/'.$filename);
 
    return parent::save($con);
  }
}

Dopo aver spostato il codice al form, l'azione edit è identica al codice generato inzialmente dal task doctrine:generate-module.

sidebar

Rifattorizzare il codice del modello nel form

Le azioni generati dal task doctrine:generate-module non dovrebbero solitamente essere modificate.

La logica che si potrebbe aggiungere nell'azione edit, specialmente durante la serializzazione del form, deve solitamente essere spostata nelle classi del modello o nelle classi dei form.

Abbiamo appena visto un esempio di rifattorizzazione nella classe del form per considerare il salvataggio di un file caricato. Vediamo un altro esempio legato al modello. il form ArticleForm ha un campo slug. Abbiamo osservato che questo campo dovrebbe essere calcolato automaticamente dal nome del campo title che potrebbe essere eventualmente sovrascritto dall'utente. Questa logica non dipende dal form. Appartiene piuttosto al modello, come mostrato nel codice seguente:

class Article extends BaseArticle
{
  public function save($con = null)
  {
    if (!$this->getSlug())
    {
      $this->setSlugFromTitle();
    }
 
    return parent::save($con);
  }
 
  protected function setSlugFromTitle()
  {
    // ...
  }
}

Lo scopo principale di questa rifattorizzazione è quello di rispettare la separazione tra i livelli applicativi e specialmente la riusabilità degli sviluppi.

Personalizzare il metodo doSave()

Abbiamo osservato che il salvataggio di un oggetto è stato fatto con una transazione, per poter garantire che ogni operazione legata al salvataggio sia processata correttamente. Quando sovrascriviamo il metodo save(), come abbiamo fatto nella sezione precedente, per salvare il file caricato, il codice eseguito è indipendente dalla transazione.

Il Listato 4-28 mostra come usare il metodo doSave() per inserire il nostro codice di salvataggio del file caricato nella transazione globale.

Listato 4-28 - Sovrascrivere il metodo doSave() nel form ArticleForm

class ArticleForm extends BaseFormDoctrine
{
  // ...
 
  protected function doSave(DoctrinePDO $con = null)
  {
    if (file_exists($this->getObject()->getFile()))
    {
      unlink($this->getObject()->getFile());
    }
 
    $file = $this->getValue('file');
    $filename = sha1($file->getOriginalName()).$file->getExtension($file->getOriginalExtension());
    $file->save(sfConfig::get('sf_upload_dir').'/'.$filename);
 
    return parent::doSave($con);
  }
}

Essendo il metodo doSave() richiamato dalla transazione creata dal metodo save(), se la chiamata al metodo save() dell'oggetto file() solleva un'eccezione, l'oggetto non viene salvato.

Personalizzare il metodo updateObject()

A volte è necessario modificare l'oggetto connesso con il form tra l'aggiornamento e il salvataggio nel database.

Nel nostro esempio di invio di file, invece di memorizzare il percorso assoluto del file caricato nella colonna file, vogliamo memorizzare il percorso relativo alla cartella sfConfig::get('sf_upload_dir').

Il Listato 4-29 mostra come sovrascrivere il metodo updateObject() del form ArticleForm per cambiare il valore della colonna file dopo l'aggiornamento automatico ma prima che venga salvato.

Listato 4-29 - Sovrascrivere il metodo updateObject() e la classe ArticleForm

class ArticleForm extends BaseFormDoctrine
{
  // ...
 
  public function updateObject($values = null)
  {
    $object = parent::updateObject($values);
 
    $object->setFile(str_replace(sfConfig::get('sf_upload_dir').'/', '', $object->getFile()));
 
    return $object;
  }
}

Il metodo updateObject() viene chiamato dal metodo doSave() prima di salvare l'oggetto nel database.