venerdì 30 ottobre 2009

Personalizzare l'aspetto dei form in symfony

I form per l'inserimento e la modifica dei dati sono una parte importante di un cms come di ogni altra applicazione web. Durante lo sviluppo di Lyra (vedi Lyra, inserimento e modifica articoli) abbiamo già incontrato diversi esempi delle possibilità offerte dal framework per personalizzare l'aspetto ed il funzionamento dei form. Ad esempio:

  • variare l'ordine posizione dei campi;
  • modificare l'etichetta;
  • modificare il tipo di controllo usato dall'utente per inserire i dati (widget nella terminologia di symfony);
  • aggiungere dinamicamente campi e relativi widget;
  • impostare regole di validazione.

È possibile però fare molto di più ed arrivare a personalizzare completamente il codice HTML che determina la struttura del form. Per fare questo è necessario creare una classe form formatter personalizzata, ne vedremo un esempio tra poco realizzando il form per l'inserimento di un commento ad un articolo.

Form commenti

Dopo aver sviluppato le funzioni di visualizzazione dei commenti è necessario creare il form per l'invio di un commento. La classe relativa a questo form (LyraCommentForm) è stata generata automaticamente a partire dalle informazioni contenute nello schema dati.

Poiché il form viene visualizzato dopo la lista dei commenti sulla vista a tutta pagina dell'articolo, l'oggetto relativo viene creato nell'azione show del modulo article per essere passato al template.

apps/frontend/modules/article/actions/actions.class.php

class articleActions extends sfActions
{
...
  public function executeShow(sfWebRequest $request)
  {
    ...
    $this->form = new LyraCommentForm();
    $this->form->setDefault('article_id', $this->item->getId());
  }
}

Viene impostato il valore di default del campo article_id per legare il commento all'articolo.

Come si è fatto per la lista commenti, la visualizzazione del form viene demandata ad un partial richiamato in showSuccess.php.

apps/frontend/modules/article/templates/showSuccess.php

if($form) {
  include_partial('article/comment_form', array('form'=>$form));
}
?> // fine showSuccess.php
apps/frontend/modules/article/templates/_comment_form.php

<h3 id="comment-form"><?php echo __('HEAD_SUBMIT_COMMENT') ?></h3>
<div id="form-wrapper">
  <form action="<?php echo url_for('article/comment?id=' . $form['article_id']->getValue()) ?>" method="post">
    <?php echo $form ?>
    <input type="submit" value="Submit" />
  </form>
</div>

Come si vede dal valore dell'attributo action, i dati inviati tramite il form vengono processati da un'azione comment del modulo article. In questa fase non mi è sembrato utile creare un nuovo modulo nel frontend dedicato alla gestione dei commenti, in futuro potrebbe essere necessario farlo.

apps/frontend/modules/article/actions/actions.class.php

class articleActions extends sfActions
{
...
  public function executeComment(sfWebRequest $request)
  {
    $this->forward404Unless($request->isMethod('post'));
    $this->item = Doctrine::getTable('LyraArticle')->find($request->getParameter('id'));
    $this->forward404Unless($this->item);
    $this->form = new LyraCommentForm();
    $this->processCommentForm($request, $this->form);
    $this->comments = $this->item->getActiveComments();
    $this->setTemplate('show');
  }
  ...
  protected function processCommentForm(sfWebRequest $request, sfForm $form)
  {
    $form->bind($request->getParameter($form->getName()));
    if ($form->isValid())
    {
      $comment = $form->save();
      $this->getUser()->setFlash('notice', 'MSG_COMMENT_SAVED');
      $this->redirect('article/show?id='.$comment->getArticleId());
    }
  }
}

Una volta che il form è stato salvato senza errori si viene reindirizzati alla pagina dell'articolo. Il metodo setFlash() imposta un messaggio di conferma da visualizzzare dopo il redirect

Resta da personalizzare la classe LyraCommentForm. Un primo livello di personalizzazione avviene nel metodo configure() della classe. Si tratta di operazioni in gran parte già viste e comunque ben documentate.

class LyraCommentForm extends BaseLyraCommentForm
{
  public function configure()
  {
    $this->removeFields();

Si rimuovono i campi che non devono essere visualizzati sul form. Perché si utilizzi un metodo protetto per questa operazione sarà chiaro quando svilupperemo la gestione di backend dei commenti, ma chi ha seguito il tutorial Jobeet lo sa già.

    $this->widgetSchema['article_id'] = new sfWidgetFormInputHidden();

Come si è visto, il valore del campo article_id viene impostato nell'azione e non deve essere modificato dall'utente: si cambia quindi il tipo di widget da testo a campo nascosto.

    $this->widgetSchema['author_name']->setLabel('AUTHOR_NAME');
    $this->widgetSchema['author_email']->setLabel('AUTHOR_EMAIL');
    $this->widgetSchema['author_url']->setLabel('AUTHOR_URL');
    $this->widgetSchema['content']->setLabel(false);

Si modificano le etichette dei campi. Passando false al metodo setLabel() si rimuove l'etichetta per il campo content (l'area di testo per l'inserimento del commento).

    $this->widgetSchema['content']->setAttribute('rows',12);
    $this->widgetSchema['content']->setAttribute('cols',45);
    $this->widgetSchema->setHelp('author_email','AUTHOR_EMAIL_HELP');

Si impostano attributi per l'area di testo ed un testo di aiuto che sarà visualizzato sotto il campo e-mail.

    $this->validatorSchema['author_name']->addMessage('required','AUTHOR_NAME_REQUIRED');
    $this->validatorSchema['content']->addMessage('required','CONTENT_REQUIRED');
    $this->validatorSchema['author_email'] = new sfValidatorEmail(
      array('required'=>true),
      array('required'=>'AUTHOR_EMAIL_REQUIRED','invalid'=>'AUTHOR_EMAIL_INVALID')
    );
    $this->validatorSchema['author_url'] = new sfValidatorUrl(
      array('required'=>false),
      array('invalid'=>'AUTHOR_URL_INVALID')
    );

Si impostano i messaggi di errore che sono visualizzati in caso di mancata validazione del campo. Per i campi author_email e author_url si impostano validatori standard che bloccano l'inserimento di un indirizzo e-mail o URL non validi.

    $this->widgetSchema->setFormFormatterName('LyraComment');

Imposta il nome della classe form formatter da utilizzare per il form.

  } //fine configure()
  protected function removeFields()
  {
    unset($this['created_at'], $this['updated_at'], $this['is_active']);
  }
}// fine LyraCommentForm

Form formatter personalizzati

Tornando al partial che visualizza il form (_comment_form.php), si nota che nel codice sono stati inclusi solo i tag <form> e <input> per il pulsante Invia, mentre il codice HTML per tutti campi e le etichette è generato da una singola istruzione

echo $form;

I tag (<input>, <textarea>,<select>) e gli attributi dei singoli campi sono determinati dal widget creato per il campo nella classe base del form ed eventualmente modificato nel metodo configure() della classe derivata. Anche i tag <label> sono generati grazie ad informazioni impostate nella classe del form, invece il codice HTML che 'incornicia' campi ed etichette viene generato da una classe form formatter.

Nel framework viene definita una classe base sfWidgetFormSchemaFormatter e due classi da essa derivate: sfWidgetFormSchemaFormatterTable e sfWidgetFormSchemaFormatterList.

sfWidgetFormSchemaFormatterTable verrà utilizzata per tutti i form a meno che non si indichi al framework (con il metodo setFormFormatterName() che abbiamo visto sopra) di utilizzare la classe sfWidgetFormSchemaFormatterList o una nostra classe personalizzata al posto di quella predefinita.

Questa è la classe creata per il form commenti di Lyra.

class sfWidgetFormSchemaFormatterLyraComment extends sfWidgetFormSchemaFormatter
{
  protected
      $rowFormat = '<div class="row">%error%%field%%label%%help%%hidden_fields%</div>',
      $helpFormat = '<div class="field-help">%help%</div>';
}

Il file con la definizione della classe va inserito nella cartella lib del proprio progetto. La classe deve essere derivata dal formatter base e il nome deve essere sfWidgetFormSchemaFormatter + il nome che vogliamo dare al nostro formatter e che passaremo a setFormFormatterName() nel metodo configure() della classe form.

A me serviva solo includere in tag <div> campo ed etichetta, posizionare l'etichetta a destra del campo sulla stessa riga, gli eventuali messaggi di errori sopra il campo e creare un ulteriore <div> per racchiudere il testo di aiuto che viene mostrato sotto i campi. Quindi è stato sufficiente personalizzare le proprietà $rowFormat e $helpFormat.

In questo modo il form appare così:

Il tutto è incluso nella revisione 15. Chi si allinea al repository, deve ricordarsi di eseguire subito dopo il checkout il comando

./symfony cc

Male non fa mai, ma è obbligatorio quando si aggiungono nuove classi in lib. Con queste modifiche è già possibile inserire commenti, mancando però le funzioni di moderazione nel backend, per vedere i nuovi commenti nel frontend è necessario impostare manualmente il campo is_active a 1 nel database in quanto i nuovi commenti vengono salvati con il valore predefinito 0 (cioè non pubblicato).

mercoledì 28 ottobre 2009

Lyra, visualizzazione commenti articolo

Quando abbiamo generato i dati di prova per lo sviluppo ed il test iniziale dell'applicazione (symfony, creazione dati di prova (fixtures)), si sono inclusi alcuni record di commenti legati agli articoli. Non è stata per ora scritta alcuna funzione di visualizzazione di questi dati ed è il momento di provvedere.

La gestione dei commenti sarà integrata nel cms, le funzionalità minime che dovranno essere previste sono almeno le seguenti:

  • attivazione e disattivazione dei commenti su singoli articoli o categorie (etichette);
  • possibilità di limitare solo agli utenti registrati l'inserimento dei commenti o consentirlo anche agli utenti anonimi;
  • pubblicazione automatica o moderazione dei commenti;
  • protezione da spam tramite captcha o altre misure anti-robot sul modulo di inserimento commenti;
  • filtri contro i contenuti o link indesiderati nei commenti;
  • notifiche dell'inserimento / approvazione di un commento;
  • possibilità di ricevere i commenti inseriti su un articolo / categoria via e-mail o feed.

La lista può non essere completa. Chiaramente non tutto può essere sviluppato adesso, quindi inizieremo con le funzioni di visualizzazione.

Poiché la lista dei commenti viene visualizzata quando l'articolo è a tutta pagina dovremo intervenire sul codice dell'azione show del modulo article ed il relativo template (showSuccess.php), mentre la funzione per estrarre dal database la lista dei commenti relativi ad un determinato articolo deve essere implementata in una classe del modello.

Azione

apps/frontend/modules/article/actions/actions.class.php

class articleActions extends sfActions
{
...
  public function executeShow(sfWebRequest $request)
  {   
    ...
    $this->comments = $this->item->getActiveComments();
  }
...
} // fine class articleActions

$this->item contiene il record articolo da visualizzare ritornato dal metodo find() nella parte del codice che ho omesso e che abbiamo visto in Lyra, visualizzazione articolo.

Modello

LyraArticle

lib/model/doctrine/LyraArticle.class.php

class LyraArticle extends BaseLyraArticle
{
...
  public function getActiveComments()
  {
    return $this->getActiveCommentsQuery()
      ->execute();
  }
  protected function getActiveCommentsQuery()
  {
    $q = Doctrine::getTable('LyraComment')
      ->getActiveItemsQuery();

    $q->andWhere($q->getRootAlias() .'.article_id = ?', $this->getId());
      
    return $q;
  }
} // fine classe LyraArticle

LyraCommentTable

lib/model/doctrine/LyraCommentTable.class.php

class LyraCommentTable extends Doctrine_Table
{
  public function getActiveItemsQuery() {
    $q = $this->createQuery('c');

    $q->andWhere('c.is_active = ?', true);
    $q->addOrderBy('c.created_at DESC');

    return $q;
  }
}

La query SQL per la selezione dei commenti da visualizzare viene generata da due classi: LyraCommentTable genera la parte che seleziona i commenti pubblicati (campo is_active è true) e imposta il criterio di ordinamento, LyraArticle aggiunge un criterio di selezione basato sull'ID articolo.

Sembrano troppi metodi per costruire una query semplice, ma il fatto è che non resterà così semplice: dovranno necessariamente essere previste delle condizioni di visualizzazione e dei criteri di selezione e ordinamento dei commenti dipendenti da parametri di configurazione. Ad esempio:

  • la configurazione articoli dovrà prevedere la possibilità di disattivare la visualizzazione dei commenti su un singolo articolo;
  • la configurazione commenti dovrà consentire di impostare diversi criteri di ordinamento in aggiunta all'ordinamento per data discendente fisso come adesso.

Mi sembra opportuno che la prima condizione sia verificata in LyraArticle e il secondo parametro impostato in LyraCommentTable: tra tutte le condizioni che determinano la visibilità e l'aspetto della lista dei commenti ogni classe gestisce quelle che dipendono dall'oggetto che le compete. Almeno al momento mi torna bene fatto così, se mi sbaglio ci sarà il tempo di correggere.

Template

apps/frontend/modules/article/templates/showSuccess.php
...
<?php
if(count($comments)) {
  include_partial('article/comments', array('comments'=>$comments));
}
?> // fine showSuccess.php

Per visualizzare i commenti si utilizza un partial, ormai ne abbiamo incontrati diversi negli articoli precedenti.

apps/frontend/modules/article/templates/_comments.php

<?php use_helper('Date');?>
<h2 id="comments"><?php echo __('HEAD_COMMENTS') ?></h2>
<?php foreach($comments as $comment):?>
<div class="comment-wrapper">
<div class="comment-header">
  <?php
  $author = $comment->getAuthorName();
  if($comment->getAuthorUrl()) {
    $author = link_to($author, $comment->getAuthorUrl());
  }
  echo __('COMMENT_HEADER',
    array(
      '%name%'=>'<span class="author">' . $author . '</span>',
      '%date%'=>'<span class="date">' . format_date($comment->getCreatedAt(),'dd MMMM yyyy') . '</span>',
      '%time%'=>'<span class="time">' . format_date($comment->getCreatedAt(),'HH:mm') . '</span>'
    ));
  ?>
</div>
<div class="comment-content"><?php echo $comment->getContent()?></div>
</div>
<?php endforeach;?>

L'unica cosa rilevante da notare è l'uso della funzione helper per la traduzione (__()) per creare l'intestazione del commento, la riga che contiene autore, data ed eventuale link al sito web dell'autore. È la prima volta che ne vediamo l'uso con parametri (%name%, %date%, %time%). Nel file di lingua abbiamo:

apps/frontend/i18n/it/messages.xml

...
  <trans-unit id="20">
    <source>COMMENT_HEADER</source>
    <target>%name% il %date% alle %time%</target>
  </trans-unit>
...

Che produce questa intestazione.

L'uso di una costante nel file di lingua oltre che alla traduzione serve a modificare l'intestazione senza toccare il codice. Ad esempio modificando COMMENT_HEADER in questo modo

  <trans-unit id="20">
    <source>COMMENT_HEADER</source>
    <target>il %date% alle %time% %name% ha scritto</target>
  </trans-unit>

avremo questo risultato.

Sono state necessarie anche modifiche al file web/css/main.css per inserire le classi necessarie alla formattazione dei commenti. I dettagli sono su Google Code (revisione 14) come del resto tutto il codice riportato sopra. La prossima volta creeremo il form per l'inserimento di un commento.

venerdì 23 ottobre 2009

Utilizzare FCKeditor in symfony

Chi ha avuto voglia di provare ad inserire o modificare un articolo con l'ultima revisione di Lyra avrà senz'altro notato la mancanza di un editor WYSIWYG. Ho pensato che anche in questa fase iniziale dello sviluppo avrebbe fatto comodo avere a disposizione qualcosa di più di una semplice area di testo standard, anche solo per inserire dei contenuti di prova.

Ho quindi seguito le istruzioni di questo post sul forum ufficiale di symfony per creare un widget che consenta l'inserimento di contenuti tramite FCKeditor. La procedura è semplice:

  • si copia il file scaricato dal forum (sfWidgetFormTextareaFCKEditor.class.php) contenente la definizione della classe del widget nella cartella lib del proprio progetto;
  • si scarica il pacchetto dell'editor (FCKeditor non CKEditor) dal sito del produttore e si scompatta in web/js/. Come risultato della decompressione avremo una sottocartella fckeditor da cui ho rimosso una serie di file non indispensabili come documentazione ed esempi vari;
  • si includono le istruzioni per la creazione del widget nel metodo configure() della classe LyraArticleForm.

Alla fine bisogna ricordarsi di eseguire il comando

./symfony cc

La configurazione dell'editor (larghezza, altezza, tipo di barra degli strumenti) al momento è fissa, naturalmente in seguito dovranno essere previsti dei parametri di configurazione per consentire all'utente di personalizzare o anche disattivare l'editor per chi preferisce inserire i contenuti utilizzando l'area di testo semplice.

Ho creato una barra di strumenti che include i pulsanti più comuni modificando il file web/js/fckeditor/fckconfig.js. Da notare che la funzione di caricamento delle immagini sul server per ora è disabilitata.

Queste modifiche sono incluse nella revisione 13.

mercoledì 21 ottobre 2009

Lyra, cataloghi ed etichette

In Lyra la categorizzazione dei contenuti avviene tramite etichette, se si preferisce si possono chiamare categorie perché il concetto è quello. Le etichette sono suddivise in cataloghi, questo per permettere criteri di classificazione multipli.

Ad esempio, nei dati predisposti per il test iniziale dell'applicazione abbiamo creato questi cataloghi ed etichette:

Argomento      <- catalogo
  PHP          <- etichetta
  Javascript   <- etichetta
    Mootools   <- etichetta
    jQuery     <- etichetta

Livello        <- catalogo
  Elementare   <- etichetta
  Intermedio   <- etichetta
  Avanzato     <- etichetta

In questo modo gli articoli possono essere classificati in base all'argomento e al livello di difficoltà. Quindi potremmo avere un articolo

"L'uso delle variabili in PHP"
Argomento (catalogo): PHP (etichetta)
Livello (catalogo): Elementare (etichetta)

ed un altro articolo

"La programmazione a oggetti in PHP"
Argomento (catalogo): PHP (etichetta)
Livello (catalogo): Avanzato (etichetta)

Ogni etichetta appartiene ad un catalogo (e ad uno solo), esistono inoltre relazioni gerarchiche (padre-figlio) tra etichette (nell'esempio l'etichetta Mootools è figlia di Javascript). Ad un contenuto possono essere assegnate più etichette appartenenti a più cataloghi.

I cataloghi utilizzabili per un contenuto dipendono dal tipo di contenuto. In questo momento è gestito un solo tipo di contenuto (articolo), ma in futuro ne saranno creati altri ed ognuno potrà avere i propri cataloghi. Immaginiamo ad esempio un tipo di contenuto 'galleria di immagini': difficilmente le etichette usate per gli articoli, potranno andare bene per categorizzare una galleria, quindi si potrà creare un catalogo (o anche più di uno) specifico per questo tipo di contenuto.

Quanto descritto sopra a parole trova riscontro nello schema dati in termini di relazioni tra le tabelle catalogs, labels, articles, article_label (tabella intermedia della relazione molti a molti tra articoli ed etichette), content_types, content_type_catalog (tabella intermedia della relazione molti a molti tra tipi di contenuto e cataloghi) e le rispettive classi del modello: LyraCatalog, LyraLabel, LyraArticle, LyraArticleLabel, LyraContentType e LyraContentTypeCatalog.

Riporto per ogni classe solo le chiavi primarie e le relazioni che ci interessano in questo momento come definite in config/doctrine/schema.yml.

LyraCatalog:
   tableName: catalogs
...
   columns:
     id:
       type: integer(4)
       primary: true
       autoincrement: true
...
LyraLabel:
   tableName: labels
...
   columns:
     id:
       type: integer(4)
       primary: true
       autoincrement: true
     catalog_id:
       type: integer(4)
...
   relations:
      LabelCatalog:
         class: LyraCatalog
         local: catalog_id
         foreign: id
         foreignAlias: CatalogLabels
         onDelete: CASCADE
...
LyraArticle:
  tableName: articles
...
  columns:
    id:
      type: integer(4)
      primary: true
      autoincrement: true
...
  relations:
    ArticleLabels:
      class: LyraLabel
      refClass: LyraArticleLabel
      local: article_id
      foreign: label_id
      foreignAlias: LabelArticles
...
LyraArticleLabel:
   tableName: article_label
   columns:
      article_id:
         type: integer(4)
         primary: true
      label_id:
         type: integer(4)
         primary: true
   relations:
      Article:
         class: LyraArticle
         local: article_id
         foreign: id
         onDelete: CASCADE
      Label:
         class: LyraLabel
         local: label_id
         foreign: id
         onDelete: CASCADE
LyraContentTypeCatalog:
   tableName: content_type_catalog
   columns:
      ctype_id:
         type: integer(4)
         primary: true
      catalog_id:
         type: integer(4)
         primary: true
   relations:
      ContentType:
        class: LyraContentType
        local: ctype_id
        foreign: id
        onDelete: CASCADE
      Catalog:
        class: LyraCatalog
        local: catalog_id
        foreign: id
        onDelete: CASCADE

Con queste premesse e tenendo presente la gestione delle relazioni tra tabelle in Doctrine, si può capire meglio il codice per la personalizzazione del form di inserimento e modifica di un articolo lasciato in sospeso la volta scorsa. In particolare la parte che genera le liste di selezione utilizzate per assegnare una o più etichette all'articolo.

lib/form/doctrine/LyraArticleForm.class.php

class LyraArticleForm extends BaseLyraArticleForm
{
  public function configure()
  {
     ...
     $ctype = Doctrine::getTable('LyraContentType')
      ->findOneByModule('article');

Recuperiamo il record del tipo di contenuto gestito dal modulo article.

     $def = array();
     if(!$this->isNew()) {     
       $def = $this->getObject()
         ->getArticleLabels()
         ->getPrimaryKeys();
     }

Se stiamo modificando un articolo esistente (metodo isNew() dell'oggetto form ritorna false), il metodo getPrimaryKeys() ci ritorna un array con i valori delle chiavi primarie dei record etichetta collegati all'articolo a loro volta ritornati da getArticleLabels(): ArticleLabels è il nome della relazione articoli-etichette definita nello schema, vedere sopra la classe LyraArticle. Questi valori sono utilizzati come default per le liste di selezione delle etichette.

    $after = 'subtitle';    
    foreach ($ctype->ContentTypeCatalogs as $cg) {
        $query = Doctrine_Query::create()
          ->from('LyraLabel l')
          ->where('l.catalog_id = ? AND l.level > 0', $cg->id);
        $k = 'label_'.$cg->getId();
        $this->widgetSchema[$k] = new sfWidgetFormDoctrineChoiceMany(array('model'=>'LyraLabel', 'query'=>$query, 'label'=>$cg->name, 'default'=>$def, 'method'=>'getIndentName'));
        $this->validatorSchema[$k] = new sfValidatorDoctrineChoiceMany(array('model'=>'LyraLabel', 'required'=>false));
        $this->widgetSchema->moveField($k, sfWidgetFormSchema::AFTER, $after);
        $after = $k;
    }
  } //fine configure()

Viene creata una lista di selezione per ciascun catalogo legato al tipo di contenuto. Gli elementi delle liste vengono estratti dalla tabella labels in base all'ID del catalogo: la query esclude l'elemento a livello zero nel nested set, la radice del catalogo che non deve essere visualizzata nella lista.

Per i dettagli sul widget utilizzato per le liste di selezione, rimando alla documentazione ufficiale: symfony forms - Widgets, paragrafo "Scelta legata ad un modello Doctrine".

Le liste vengono posizionate (con moveField()) una dopo l'altra dopo il campo subtitle

  protected function doSave($con = null)
  {
    parent::doSave($con);
    $this->saveLabels($con);
  }

Implementando il metodo doSave() in LyraArticleForm possiamo eseguire codice quando il record principale viene salvato. In questo caso eseguiamo saveLabels() per salvare i legami articolo / etichette.

Tralascio l'esame in dettaglio del metodo saveLabels(), richiamo solo l'attenzione di chi è interessato sui metodi link() e unlink() per costruire e rimuovere i legami tra i record delle tabelle Articoli ed Etichette che sono tra loro in una relazione molti a molti attraverso la tabella intermedia article_label (classe modello LyraArticleLabel).

Nessuna modifica sul repository in quanto tutto il codice sopra riportato era già incluso nella revisione 12.

lunedì 19 ottobre 2009

Lyra, inserimento e modifica articoli (frontend)

Come accennato quando sono state delineate le funzionalità principali della versione iniziale di Lyra, gli articoli potranno essere inseriti e modificati sia dal frontend che dal backend dell'applicazione.

Il backend ancora non c'è, ma si può iniziare lo sviluppo delle funzioni di inserimento e modifica dei contenuti da frontend. Questo permette tra l'altro di spendere qualche parola sulla gestione dei form in symfony.

Classi form

Quando si eseguono i comandi doctrine:build-forms, doctrine:build-all o doctrine:build-all-reload il framework utilizza le informazioni contenute nel file config/doctrine/schema.yml per genereare automaticamente le classi dei form: in particolare in lib/form/doctrine/base troviamo le classi base che vengono ricreate ogni volta che si esegue uno dei comandi appena nominati, in lib/form/doctrine troviamo le classi derivate dalle classi base che sono inizialmente create vuote e mai sovrascritte da comandi del framework. Quindi è in queste ultime che dovremo scrivere il codice per personalizzare un form. Al momento la classe che ci interessa è LyraArticleForm.

Azioni di inserimento, modifica, cancellazione

Procediamo con ordine iniziando dalle azioni. Dato che l'inserimento, modifica e cancellazione dei dati sono operazioni frequenti che è facile standardizzare, symfony genera le relative azioni automaticamente quando si crea un modulo. I metodi che processano queste azioni per il modulo article si trovano nella classe articleActions in apps/frontend/modules/article/actions/actions.class.php. Quelli che ci interessano (vedremo la cancellazione un'altra volta) sono:

  • executeNew() visualizza il form per l'inserimento di un articolo;
  • executeCreate() valida il form di inserimento (chiamando il metodo protetto processForm()) e, in mancanza di errori, crea un nuovo articolo;
  • executeEdit() visualizza il form per la modifica di un articolo;
  • executeUpdate() valida il form di modifica (sempre tramite processForm()) e, in mancanza di errori, salva le modifiche.

Il flusso tra queste operazioni, visualizzazione del form, validazione e salvataggio dello stesso con creazione o modifica del record o rivisualizzazione del form con errori in caso di mancata validazione, è spiegato in dettaglio nel tutorial ufficiale: Giorno 10: Form, vedere in particolare il diagramma sotto il paragarafo "L'azione del form".

Barra di amministrazione

Ovviamente gli utenti devono avere a disposizione dei link attraverso i quali eseguire una una delle azioni appena viste. Ispirandomi a Jobeet, l'applicazione di esempio di symfony, creo una barra di amministrazione: un semplice DIV che si sovrappone al contenuto e racchiude una lista di link. La barra dovrà essere visibile solo agli utenti autorizzati alle operazioni di inserimento, modifica e cancellazione articoli, ma per il momento, mancando qualsiasi gestione utenti, sarà visibile a tutti. Questo non è un problema perché in questa fase l'applicazione viene eseguita solo in locale.

Creiamo come prima cosa un partial

apps/frontend/modules/article/templates/_admin_bar.php

<div id="admin-bar">
  <span class="action"><?php echo link_to(__('LINK_NEW'), 'article/new') ?></span>
  <span class="action"><?php echo link_to(__('LINK_EDIT'), 'article/edit?id='.$item->getId()) ?></span>
  <span class="action"><a href="#"><?php echo __('LINK_LOGOUT') ?></a></span>
</div>

Il formato delle rotte utilizzate nelle funzioni helper link_to() è quello già visto in precedenza: modulo/azione/parametri. Per il testo (ancora) dei link si utilizza una costante che sarà sostituita dalla traduzione nella lingua selezionata per l'interfaccia. Di questo si occupa la funzione helper __(), già incontrata in Lyra, azioni del modulo article a proposito della traduzione del link 'leggi tutto'. I file di traduzione si trovano nella cartella apps/frontend/i18n

Il partial viene richiamato dal template che visualizza l'articolo a tutta pagina.

apps/frontend/modules/article/templates/showSuccess.php

<?php include_partial('article/admin_bar', array('item' => $item)); ?>
<h1 class="article-title">
...

Personalizzazione del form

Già a questo punto le operazioni di inserimento e modifica di un articolo funzionerebbero grazie al codice autogenerato presente nei metodi di gestione delle azioni. Bisogna però personalizzare l'aspetto del form nel metodo configure() della classe LyraArticleForm. Non riporto per intero il codice perché si tratta di operazioni semplici ed esempi analoghi di configurazione di un form si trovano nel codice di Jobeet: vengono rimossi dalla visualizzazione tutti i campi che non devono essere gestiti dall'utente, vengono rinominate le etichette di campi utilizzando costanti stringa da tradurre e viene modificato l'ordine di alcuni campi.

L'unica parte più complessa è la generazione delle liste di selezione attraverso le quali si scelgono le etichette da assegnare all'articolo. Per poter dare anche solo qualche cenno di spiegazione di questa parte, occorre prima capire l'utilizzo di Cataloghi ed Etichette per categorizzare i contenuti in Lyra. Ho già accennato qualcosa, ma arrivati a questo punto è necessaria una trattazione più esauriente che merita un post a parte, il prossimo.

Tutte le modifiche sono incluse nella revisione 12. Per allinearsi a questa revisione oltre al checkout della propria copia di lavoro locale è necessario eseguire i comandi

./symfony doctrine:build-all-reload
./symfony cc

A questo punto scrivendo nella barra indirizzi del browser

http://lyra/frontend_dev.php/

si apre la prima pagina con gli articoli dei dati di esempio. Visualizzando un articolo a pagina intera compare la barra di amministrazione: è già possibile modificare o inserire un articolo anche se il form è ancora abbastanza rudimentale. Bisogna ricordarsi di impostare sempre lo stato Pubblicato e Prima pagina perché senza backend e avendo nel frontend solo la prima pagina, questo è l'unico modo per vedere l'articolo inserito.

giovedì 15 ottobre 2009

Lyra, gestione metatag articolo

In Lyra come si può notare dallo schema del database (schema.yml, LyraArticle), esistono campi per impostare su ogni articolo il contenuto di alcuni metatag:

  • meta description
  • meta keywords
  • meta robots
  • meta title

Della visualizzazione dei metatag si occupano apposite funzioni helper richiamate nel layout.

apps/frontend/templates/layout.php
...
<head>
  <?php include_http_metas() ?>
  <?php include_metas() ?>
  <?php include_title() ?>
  ...
</head>
...

I valori dei metatag si impostano nell'azione show del modulo article utilizzando metodi dell'oggetto response (sfWebResponse) la cui istanza otteniamo con il metodo getResponse().

apps/frontend/modules/article/actions/actions.class.php

public function executeShow(sfWebRequest $request)
{
  ...
  $this->item->setMetaTags($this->getResponse());
}
Il metodo setMetaTags() è implementato nel modello.
lib/model/doctrine/LyraArticle.class.php

class LyraArticle extends BaseLyraArticle
{
  ...
  public function setMetaTags(sfWebResponse $response)
  {
    $mt = $this->getMetaTitle();
    if(!$mt) {
      $mt = $this->getTitle();
    }
    $response->setTitle($mt);

    if($mt = $this->getMetaDescr()) {
        $response->addMeta('description', $mt);
    }
    if($mt = $this->getMetaKeys()) {
        $response->addMeta('keywords', $mt);
    }
    if($mt = $this->getMetaRobots()) {
        $response->addMeta('robots', $mt);
    }
  }
...
}

Come si può notare se al momento della creazione dell'articolo non si è inserito alcun valore nel campo meta_title, si utilizza come meta-titolo il titolo dell'articolo, che non può essere vuoto in quanto campo obbligatorio. Gli altri metatag possono invece essere tranquillamente omessi.

Queste poche modifiche sono incluse nella revisione 11 su Google Code.

mercoledì 14 ottobre 2009

Lyra, visualizzazione articolo

Dopo aver implementato, almeno a grandi linee, l'azione index per la visualizzazione dell'elenco articoli in prima pagina, passiamo all'azione show che serve a visualizzare un singolo articolo a tutta pagina. Il procedimento è del tutto analogo a quello visto in precedenza per cui non mi dilungo molto.

apps/frontend/modules/article/actions/actions.class.php

class articleActions extends sfActions
{
...
  public function executeShow(sfWebRequest $request)
  {
    $this->item = Doctrine::getTable('LyraArticle')
      ->find($request->getParameter('id'));
    $this->forward404Unless($this->item);
  }
...

Il metodo find() seleziona uno o più record in base alla chiave primaria. Come si è visto nell'articolo precedente quando si sono generati i link 'leggi tutto' la rotta per la pagina di un articolo ha il formato

article/show/id/x

dove x è il valore del campo ID chiave primaria del record; questo valore viene passato nella richiesta e letto con il metodo getParameter() della classe sfWebRequest.

Il metodo forward404Unless() genera un errore 404 se l'argomento che gli viene passato è false, cioè se il metodo find() non ha trovato un record con chiave corrispondente.

Passiamo al template corrispondente all'azione show: il codice generato automaticamente dal framework è sostituito dal seguente:

apps/frontend/modules/article/templates/showSuccess.php

<h1 class="article-title">
  <?php echo $item->getTitle() ?>
</h1>
<?php if($item->getSubtitle()): ?>
  <div class="article-subtitle">
    <?php echo $item->getSubtitle() ?>
  </div>
<?php endif ?>
<div class="article-date">
  <?php echo $item->getCreatedAt() ?>
</div>
<div class="article-content">
  <?php echo $item->getContent(ESC_RAW) ?>
</div>

$item è l'articolo che deve essere visualizzato, un oggetto istanza della classe LyraArticle. I valori dei campi si ottengono con i metodi già incontrati.

Esiste un campo sottotitolo che ho incluso solo nella visualizzazione dell'articolo a tutta pagina, se lo si vuole visualizzare anche quando gli articoli sono mostrati con sommario e 'leggi tutto' basta aggiungere il blocco nel partial _list.php.

Oggi si lavora poco, con queste modifiche siamo alla revisione 10.

lunedì 12 ottobre 2009

Lyra, azioni del modulo article

Fino a questo punto abbiamo modificato il layout globale dell'applicazione frontend di Lyra. È il momento di iniziare a scrivere il codice che dovrà gestire le varie azioni del modulo article.

Quando abbiamo creato il modulo article con il comando doctrine:generate-module è stata creata una classe articleActions in apps/frontend/modules/article/actions/actions.class.php e sono stati generati alcuni metodi per processare un certo numero di azioni base: tra queste, index viene di regola utilizzata per mostrare un elenco di record, nel nostro caso l'elenco degli articoli pubblicati sulla prima pagina del sito.

La prima pagina in Lyra

In questa fase di sviluppo iniziale la prima pagina del sito è costituita semplicemente da un elenco di articoli ordinati per data decrescente in "stile blog": titolo, data, autore (quando ci sarà perché non abbiamo ancora la gestione utenti), sommario o testo introduttivo ed eventuale link 'leggi tutto'.

Come si è visto nell'articolo precedente, l'azione index è processata da un metodo executeIndex() della classe articleActions.

apps/frontend/modules/article/actions/actions.class.php

class articleActions extends sfActions
{
   public function executeIndex(sfWebRequest $request)
   {
      $this->items = Doctrine::getTable('LyraArticle')->getFrontPageItems();
   }
  ...
}

Per ottenere l'elenco degli articoli in prima pagina è necessaria ovviamente un'interrogazione del database e, per le considerazioni fatte la volta scorsa, questo è il compito di un oggetto Model. Otteniamo quindi un'istanza della classe LyraArticleTable e ne invochiamo il metodo getFrontPageItems() che scriveremo tra poco.

Il risultato è un array di record che viene salvato come proprietà items. Quando creiamo una proprietà in questo modo nel codice di un'azione il valore assegnato sarà visibile dalla view (template) come variabile.

Si è visto (symfony, generazione del Modello) che in lib/model/doctrine esistono due classi per ogni tabella dello schema. Per quanto riguarda gli articoli:

  • LyraArticleTable che rappresenta la tabella;
  • LyraArticle che rappresenta un record della tabella.

Di solito la prima contiene i metodi che ritornano i risultati delle interrogazioni sulla tabella, è lì che implementeremo getFrontPageItems().
lib/model/doctrine/LyraArticleTable.class.php

class LyraArticleTable extends Doctrine_Table
{
  public function getFrontPageItems()
  {
    $q = $this->getActiveItemsQuery()
      ->andWhere('a.is_featured = ?', true);
    return $q->execute();
  }
  
  public function getActiveItems()
  {
    $q = $this->getActiveItemsQuery();
    return $q->execute();
  }

  public function getActiveItemsQuery()
  {
    return $this->createQuery('a')
      ->where('a.is_active = ?', true)
      ->addOrderBy('a.is_sticky DESC, a.created_at DESC');
  }
}

Il metodo getActiveItemsQuery() ritorna una query per la selezione degli articoli pubblicati (is_active è true) ordinati secondo il criterio di ordinamento predefinito (is_sticky a true indica che l'articolo sta sempre in testa alla lista).

Il metodo getFrontPageItems() aggiunge un proprio criterio di selezione (is_featured è true per gli articoli da visualizzare in prima pagina) ed esegue la query.

Le query sono scritte utilizzando Doctrine Query Language (DQL) su cui si possono trovare tutte le informazioni nella documentazione ufficiale. Faccio notare soltanto:

  • al metodo createQuery() basta passare un alias ('a'), non serve sia specificato il nome della tabella in quanto implicitamente è quello della tabella di riferimento della classe (nel nostro caso articles);
  • il '?' nelle condizioni where funge da segnaposto per un parametro che viene passato come argomento successivo;
  • Il risultato dell'esecuzione della query con il metodo execute() è un array di oggetti istanze della classe LyraArticle.

Resta da creare il template. La prima considerazione da fare è che il formato di visualizzazione degli articoli in prima pagina (titolo, sommario con link 'leggi tutto') sarà utilizzato anche in altre parti del sito, ad esempio nelle pagine che mostrano gli articoli appartenenti ad una categoria. Conviene da subito mettersi in condizione di evitare duplicazioni di codice: quindi per prima cosa creiamo un partial, cioè un blocco di codice che può essere richiamato da più di un template.

apps/frontend/modules/article/templates/_list.php

<?php foreach ($items as $item): ?>
  <h2 class="article-title">
    <?php echo link_to($item->getTitle(), 'article/show?id='.$item->getId())?>
  </h2>
  <div class="article-date">
    <?php echo $item->getCreatedAt() ?>
  </div>
  <div class="article-summary">
    <?php
    echo $item->getSummary(ESC_RAW);
    if($item->showReadmore()): ?>
      <span class="article-readmore">
      <?php echo link_to(__('LINK_READMORE'), 'article/show?id='.$item->getId(), array('title'=>$item->getTitle()))?>
      </span>
    <?php endif ?>
  </div>
  <div class="article-separator"></div>
<?php endforeach; ?>

Un partial ha sempre un nome di file che inizia con il carattere sottolineato:_list.php viene incluso nel template indexSuccess.php con questa sintassi:

apps/frontend/modules/article/templates/indexSuccess.php

<?php include_partial('article/list', array('items'=>$items)); ?>

Alcune cose da notare. L'helper link_to() serve a generare un link HTML: il primo parametro è il testo da usare per l'ancora, il secondo la rotta in base alla quale generare la URL per il link; il formato della rotta (modulo/azione/parametri) è quello già visto nell'articolo precedente. L'azione show servirà a mostrare l'articolo a tutta pagina, i parametri (nel nostro caso solo l'id dell'articolo da visualizzare) sono passati nel formato usato per una query string: coppie chiave=valore, separate dal carattere '&' se sono più di una.

Come accennato, il valore della proprietà items del controller (assegnato con $this->items in executeIndex()), viene visto come variabile ($items) nel template e passato al partial con lo stesso identificatore items (dal codice array('items'=>$items)).

Nel partial il contenuto dei singoli campi di ogni record è letto tramite metodi getter. Il parametro ESC_RAW si usa quando si vuole il contenuto del campo senza mascheramento delle entità HTML, questo è necessario quando il campo (nel nostro caso il sommario dell'articolo) può contenere codice HTML.

Richiamo anche l'attenzione sul modo in cui viene creata l'ancora per il link 'leggi tutto': si utilizza una costante simbolica che sarà tradotta in base ai file di lingua. Questo modo di procedere è diverso da quello del tutorial ufficiale (Internazionalizzazione e Localizzazione) dove si utilizza una lingua predefinita nei template e poi si generano i file di traduzione solo per le lingue diverse da quella predefinita.

Io preferisco utilizzare delle costanti nei template per poi generare i file di traduzione per tutte le lingue (vedremo in seguito come). In questo modo la personalizzazione dell'interfaccia è più semplice: ad esempio se si preferisce avere non 'leggi tutto', ma 'Continua ...' o altro non sarà necessario modificare il sorgente dell'applicazione, ma solo il file di traduzione.

__() è un helper che serve appunto a tradurre una stringa in diversi lingue: perché possa funzionare è necessario che le funzioni multilingua siano attivate nel file delle impostazioni.

apps/frontend/config/settings.yml
...
all:
  .settings:
    standard_helpers: [Partial, Cache, I18N]
    i18n: on
    default_culture: it
...

Ho eseguito il commit della revisione 9 su Google Code. Se si è configurato un proprio ambiente di sviluppo secondo le indicazioni date negli articoli precedenti, si possono vedere i risultati del lavoro facendo il checkout della revisione e digitando nella barra indirizzi del browser

http://lyra/frontend_dev.php/article

Poiché index è l'azione predefinita, non serve indicarla nella URL. L'indirizzo seguente produrrebbe lo stesso risultato

http://lyra/frontend_dev.php/article/index

Visto che non abbiamo generato i file delle traduzioni, come ancora del link leggi tutto sarà visualizzata la costante.

giovedì 8 ottobre 2009

symfony, struttura progetto

Prima di proseguire penso sia utile fare il punto su quanto sviluppato fino ad ora. Questo ci consente tra l'altro di spendere qualche parola su come è strutturato un progetto symfony.

Il progetto

La prima fase dello sviluppo (a parte l'installazione del framework che però si risolve semplicemente in un'operazione di copia di file) è stata la generazione della struttura del progetto. È quello che abbiamo fatto in Creazione progetto e applicazione.

Le applicazioni

Il progetto è diviso in una o più applicazioni. Per Lyra abbiamo iniziato lo sviluppo dell'applicazione di frontend, esisterà anche un'applicazione di backend.

Model View Controller

Un progetto symfony è strutturato secondo la logica del design pattern Model View Controller in base al quale un'applicazione può essere suddivisa in tre parti fondamentali.

La parte Model offre un'interfaccia verso il database e determina la logica applicativa. In symfony è costituita da una serie di classi in lib/model: alcune di queste sono generate dal framework in base alle informazioni contenute nel file config/doctrine/schema.yml (che abbiamo creato in Creazione schema database) e non devono essere modificate, altre possono essere personalizzate secondo le esigenze specifiche. Ne abbiamo parlato nell'articolo Generazione del Modello.

In un progetto come Lyra queste classi offrono i metodi per eseguire interrogazioni del database e compiere le operazioni di creazione, modifica, validazione e salvataggio dei dati.

La parte View si occupa della rappresentazione dei dati. In symfony è costituita da una serie di template scritti in PHP + HTML che si integrano con il layout globale di cui abbiamo parlato nell'articolo creazione modulo frontend e layout

La parte Controller elabora le richieste dell'utente. A seconda della richiesta specifica, se necessario viene invocato un metodo di un oggetto Model e restituita una risposta. La risposta tipica, anche se non l'unica possibile, è un documento HTML il cui aspetto è determinato dal layout e da un template. In symfony il Controller è costituito da un front controller ed una serie di action.

Front controller

Sono generati dal framework e suddivisi per applicazione ed ambiente; di ambienti non ho avuto occasione di parlare, per semplificare con riguardo a Lyra, abbiamo per il momento un ambiente di sviluppo e un ambiente di produzione.

Il front controller per l'ambiente di produzione della prima applicazione generata nel progetto (in Lyra, come di solito accade, il frontend) è chiamato index.php, gli altri front controller prendono il nome dell'applicazione più, per tutti gli ambienti diversi da produzione, un suffisso determinato in base all'ambiente.

I front controller costituiscono il punto di ingresso di ogni applicazione e devono poter essere invocati direttamente dal browser dell'utente: sono quindi collocati nella cartella web che come abbiamo visto (configurazione host virtuale in Creazione progetto e applicazione) è impostata come Document Root di Apache.

In Lyra questa cartella al momento contiene:

  • index.php - front controller di produzione dell'applicazione frontend.
  • frontend_dev.php - front controller di sviluppo dell'applicazione frontend.

In seguito saranno generati anche:

  • backend.php - front controller di produzione dell'applicazione backend.
  • backend_dev.php - front controller di sviluppo dell'applicazione backend.

Aprendo con un editor uno dei front controller notiamo che contiene poche righe di codice standard, che di regola non è necessario modificare, la cui funzione è quella di dare il via ad un processo che determina l'azione richiesta dall'utente.

Actions

Sono raggruppate in moduli. Ad ogni azione corrisponde di regola un template utilizzato per la risposta.

Ad esempio quando abbiamo creato il modulo article per il frontend di Lyra (vedi creazione modulo frontend e layout) è stato generato un file actions.class.php in apps/frontend/modules/article/actions.

Se apriamo il file noteremo una serie di metodi pubblici il cui nome inizia per execute: ne esiste uno per ogni azione elaborata dal controller. In apps/frontend/modules/article/templates troviamo i template corrispondenti.

Ad esempio l'azione show è processata da

class articleActions extends sfActions
{
  public function executeShow(sfWebRequest $request)
  {
      ...
  }
}

e il template corrispondente è showSuccess.php.

URL e routing

Gli utenti interagiscono con una applicazione web in diversi modi. Ad esempio:

  • digitando un indirizzo nel browser;
  • navigando un link;
  • inviando dati attraverso un form;
  • iniziando una richiesta AJAX.

Ognuna di queste modalità implica ricevere o inviare dati da/a una determinata URL. Quindi è attraverso la URL che si può determinare l'azione che l'utente richiede all'applicazione. In symfony le URL seguono un formato standard predefinito che come vedremo può però essere personalizzato:

front_controller.php/modulo/azione/parametri

dove i parametri sono costituiti da coppie di chiavi/valori separate da '/'. Poniamo di digitare il seguente indirizzo nel browser:

http://www.example.com/index.php/article/show/id/1

Assumendo che index.php sia il front controller dell'applicazione frontend nell'ambiente produzione (abbiamo visto sopra che di solito è così), stiamo eseguendo l'azione show del modulo article. Il framework eseguirà il metodo executeShow() della classe articleActions (apps/frontend/modules/article/actions/actions.class.php), all'interno del quale potremo accedere ai parametri della richiesta in questo modo:

class articleActions extends sfActions
{
  public function executeShow(sfWebRequest $request)
  {
      $id = $request->getParameter('id');
      ...
  }
}

Se la richiesta ha successo la risposta sarà data dal template showSuccess.php in apps/frontend/modules/article/templates, che ricordo verrà incorporato nel layout (apps/frontend/templates/layout.php).

Quindi una URL costituisce una rotta verso una determinata azione di un modulo. Le informazioni relative alle diverse rotte utilizzabili dall'applicazione si trovano in file in formato YAML (apps/frontend/config/routing.yml e i file omologhi per il backend ed eventuali altre applicazioni del progetto) che avremo modo di modificare più volte nel corso dello sviluppo.

La prossima volta si riprenderà il lavoro sull'applicazione, un quadro riassuntivo mi è sembrato utile per capire le operazioni che svolgeremo da ora in poi.

martedì 6 ottobre 2009

symfony, creazione modulo frontend e layout

Con i dati di esempio inseriti nel database possiamo creare il modulo per la visualizzazione degli articoli nel frontend. In symfony un'applicazione è suddivisa in moduli ed ogni modulo fornisce un determinato insieme di funzionalità. In particolare in Lyra avremo un modulo per ogni tipo di contenuto gestito dal cms. Il modulo article sarà quindi il primo che creeremo

Generazione di un modulo

./symfony doctrine:generate-module --with-show  --non-verbose-templates frontend article LyraArticle

A patto che si sia configurato un host virtuale per il progetto come descritto in symfony, creazione progetto e applicazione, si può già vedere qualcosa nel browser con

http://lyra/frontend_dev.php/article

L'aspetto non è certo gradevole da vedere, gli articoli inclusi nei dati di esempio sono visualizzati in un formato tabellare senza alcuna particolare formattazione. Questo perché non abbiamo configurato alcun layout.

Personalizzazione del layout

In symfony ogni pagina HTML è generata a partire da una sorta di template globale, il layout, che ne determina la struttura (aspetto e dimensioni di header, footer, area principale e colonne) quelle parti di codice cioè che sono comuni a più pagine.

Il contenuto specifico di ogni pagina è invece generato da template che vengono incorporati nel layout. Questa figura tratta dalla documentazione ufficiale rende bene l'idea del meccanismo.

Per il layout ho adattato un template gratuito dalla struttura molto semplice.

apps/frontend/templates/layout.php

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
   "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <?php include_http_metas() ?>
    <?php include_metas() ?>
    <?php include_title() ?>
    <?php use_stylesheet('main.css') ?>
  </head>
  <body>
    <div id="wrapper">
      <div id="content">
        <div id="top">
          <div id="logo">
            <h1>Logo</h1>
            <h4>Slogan</h4>
          </div>
          <div id="links">
            <ul>
              <li><?php echo link_to('Home','@homepage');?></li>
              <li><a href="#">Articoli</a></li>
              <li><a href="#">Contattaci</a></li>
            </ul>
          </div>
        </div>
        <div id="header">
          <h3>Titolo pagina</h3>
        </div>
        <div id="contentarea">
          <div id="leftbar">
            <?php if ($sf_user->hasFlash('notice')): ?>
            <div class="flash_notice">
              <?php echo __($sf_user->getFlash('notice')) ?>
            </div>
            <?php endif; ?>

            <?php if ($sf_user->hasFlash('error')): ?>
            <div class="flash_error">
              <?php echo __($sf_user->getFlash('error')) ?>
            </div>
            <?php endif; ?>
            <?php echo $sf_content ?>
          </div>
          <div id="rightbar">
            <h4>Menu</h4>
          </div>
        </div>
        <div id="footer">
          <div id="lyra-powered">
            <a href="#">Powered by Lyra</a>
          </div>
          <div id="validator">
          <p>Valid <a href="http://validator.w3.org/check?uri=referer">XHTML</a></p>
          </div>
        </div>
      </div>
    </div>
  </body>
</html>

Credo si veda chiaramente che questa è la 'cornice' della pagina. I contenuti sono inseriti da queste istruzioni

<?php echo $sf_content ?>

include il contenuto principale della pagina (corpo dell'articolo, lista articoli sulla pagina di una categoria e simili) che come vedremo è generato da una action e dal relativo template.

<?php include_http_metas() ?>
<?php include_metas() ?>
<?php include_title() ?>

includono i metatag e il contenuto del tag <TITLE>. Da notare anche

<?php use_stylesheet('main.css') ?>

che genera il tag <link> per includere un foglio di stile. Il file main.css è stato inserito in web/css.

Oltre al file del layout e al foglio di stile sono necessarie le immagini per lo sfondo e la testata: sono state inserite in web/images. Delle altre parti del codice del layout avremo modo di parlare in futuro.

Se a questo punto digitiamo di nuovo nel browser

http://lyra/frontend_dev.php/article

vediamo che l'aspetto della pagina rispecchia il layout anche se il contenuto (nell'esempio l'elenco articoli) è ancora in un formato tabellare 'grezzo'.

Questo perché non abbiamo ancora personalizzato il template della action. Cosa che faremo la prossima volta. A questo punto ho eseguito il commit della revisione 8 su Google Code.

lunedì 5 ottobre 2009

symfony, creazione dati di prova (fixtures)

La preparazione dei dati di esempio per lo sviluppo ed il test di un'applicazione è sempre una operazione abbastanza noiosa che fortunatamente symfony permette di semplificare.

I dati di esempio (fixtures) sono definiti in file in formato YAML collocati in data/fixtures. Il contenuto della cartella può essere sfogliato su Google Code, qui riporterò solo un estratto di alcuni dei file con qualche breve nota di commento.

Articoli

data/fixtures/articles.yml

LyraArticle:
  art1:
    title: Un articolo su PHP
    summary: Sommario primo articolo
    content: Contenuto primo articolo
    meta_title: Meta Title di articolo su PHP
    meta_descr: Meta Description di articolo su PHP
    is_featured: true
    is_active: true
    ArticleLabels: [Php,Intermedio]
  art2:
    title: Un articolo su Mootools
    ...
    ArticleLabels: [Mootools,Elementare]

Da notare che:

  • Non è necessario valorizzare le chiavi primarie dei record. Visto che sono definite in schema.yml come ID autoincrementanti, i valori vengono generati nel momento in cui il record viene inserito nel database. Quando è necessario riferirsi ad un record, ad esempio per impostare una relazione nel modo che vedremo tra poco, si utilizza un identificatore alfanumerico (in questo caso art1, art2);
  • Non tutti i campi della tabella devono essere valorizzati e questo è abbastanza ovvio. Se si omette il valore di un campo sarà usato il valore di default, se definito nello schema.
  • I record possono essere 'legati' utilizzando le relazioni tra tabelle definite nello schema. Ad esempio con ArticleLabels: [Php,Intermedio] nel record art1 si 'assegnano' al record le etichette Php e Intermedio (identificatori di record nella tabella Etichette come vedremo).

Commenti

data/fixtures/comments.yml

LyraComment:
  comm1:
    author_name: Pippo
    author_email: example@example.com
    author_url: http://www.example.com
    content: Articolo molto interessante
    is_active: true
    CommentArticle: art1
    ...

Penso sia interessante anche in questo caso notare come i record della tabella commenti vengono legati ai relativi articoli. Non viene valorizzato direttamente il campo article_id, visto che non conosciamo il valore delle chiavi primarie degli articoli che, come abbiamo detto, sono autogenerate; vengono invece assegnati gli identificatori dei record (art1, art2) sfruttando la relazione definita nello schema (CommentArticle).

Etichette

data/fixtures/labels.yml

LyraCatalog:
  argomento:
    name: Argomento
    is_active: true
  livello:
    name: Livello
    is_active: true

LyraLabel:
  Rarg:
    name: Argomento
    is_active: true
    LabelCatalog: argomento
    children:
      Php:
        name: PHP
        title: Articoli su PHP
        is_active: true
        LabelCatalog: argomento
      Javascript:
        name: Javascript
        title: Articoli su Javascript
        is_active: true
        LabelCatalog: argomento
        children:
          Mootools:
            name: Mootools
            title: Articoli su Mootools
            is_active: true
            LabelCatalog: argomento
          jQuery:
            name: jQuery
            title: Articoli su jQuery
            is_active: true
            LabelCatalog: argomento
  Rliv:
    name: Livello
    is_active: true
    LabelCatalog: livello
    children:
      Elementare:
        name: Elementare
        is_active: true
        LabelCatalog: livello
      Intermedio:
        name: Intermedio
        is_active: true
        LabelCatalog: livello
      Avanzato:
        name: Avanzato
        is_active: true
        LabelCatalog: livello

Come si vede ogni etichetta è legata ad un catalogo tramite la relazione LabelCatalog; esistono inoltre relazioni gerarchiche tra le etichette. Ad esempio le etichette Mootools e jQuery sono figlie di Javascript. Questo è possibile perché nello schema è stato definito un comportamento predefinito NestedSet per la classe LyraLabel.

config/doctrine/schema.yml
...
LyraLabel:
   tableName: labels
   actAs:
     ...
     NestedSet:
        hasManyRoots: true
        rootColumnName: root_id
...

Non mi dilungo in questo momento su cosa siano i nested set e rimando alla documentazione ufficiale di Doctrine: in particolare Hierarchical Data e Fixtures for Nested Sets. In futuro ci sarà forse modo di approfondire il discorso.

I dati di esempio si salvano nel database con il comando

./symfony doctrine:data-load

Arrivati a questo punto conviene eseguire anche il comando

./symfony doctrine:build-all-reload

In questo modo viene ricreata la struttura del database a partire dalle informazioni in config/doctrine/schema.yml, vengono rigenerate le classi in lib/model/doctrine e lib/form/doctrine e infine vengono ricaricati i dati di esempio. Si riceverà una richiesta di conferma perché il contenuto del database viene cancellato e rimpiazzato dai dati di esempio.

Ho eseguito il commit della revisione 7. Ecco come allinearsi alla revisione corrente per chi non abbia avuto la pazienza di riprodurre sul proprio server di sviluppo tutte le operazioni descritte negli articoli fino a questo momento. Si assume un sistema Linux con cartella progetto ~/sfprojects/lyra, LAMP e client Subversion devono essere installati e si deve aver creato un database vuoto ed un utente con i privilegi sul database come spiegato in symfony, generazione del Modello.

Da una finestra terminale digitare

cd ~/sfprojects/lyra
svn checkout -r7 http://lyra-cms.googlecode.com/svn/trunk/ .

Importante mettere il punto finale. Una volta terminato il checkout, rimanendo sempre nella cartella del progetto

gedit config/databases.yml

Modificare il file con i propri dati di connessione. Se si è utilizzato lyra come nome del proprio database e utente basta cambiare solo la password.

./symfony doctrine:build-all-reload
./symfony cc

Ora che abbiamo qualche dato di esempio nel database si possono cominciare a sviluppare le funzioni di visualizzazione nel frontend. Alla prossima.

venerdì 2 ottobre 2009

symfony, generazione del Modello

Il file schema.yml visto la volta scorsa (symfony, creazione schema database) contiene tutte le informazioni necessarie a generare sia le classi del Modello che gli script SQL per la creazione delle tabelle del database. Prima di procedere occorre però creare un database vuoto per il nostro progetto.

Creazione database del progetto

Il database necessario alla nostra applicazione va creato con gli strumenti amministrativi di MySQL. Può essere usato phpMyAdmin se lo abbiamo installato sul server di sviluppo, altrimenti da una finestra terminale digitiamo

mysql --user=root --password="password root"

Poi al prompt mysql> inseriamo i seguenti comandi

create database lyra character set utf8;
create user lyra@localhost identified by "user password";
grant all on lyra.* to lyra@localhost;
exit

Al posto di user password va ovviamente la password desiderata per l'utente. Sul server di sviluppo si potrebbe utilizzare l'utente root per connettersi al database, ma per abitudine preferisco creare un utente specifico.

La configurazione database predefinita in simfony utilizza Propel, è bene rimuoverla poiché per Lyra utilizziamo Doctrine. Da una finestra terminale, posizionati nella cartella radice del progetto digitiamo

rm config/databases.yml

A questo punto creiamo la nuova configurazione con

./symfony configure:database --name=doctrine --class=sfDoctrineDatabase "mysql:host=localhost;dbname=lyra" lyra "user password"

"user password" è ovviamente la password scelta precedentemente per l'utente con i privilegi sul database lyra. Il comando genera un nuovo file config/databases.yml contenente i parametri di connessione.

Generazione classi del Modello

Sempre da terminale e sempre dalla cartella del progetto eseguiamo il comando

./symfony doctrine:build-model

In lib/model/doctrine vengono generate tre classi per ogni tabella definita nel file schema.yml. Ad esempio per la tabella Articoli

~
  sfprojects
    lyra
      lib
        model
          base
            BaseLyraArticle.class.php
            ...
          LyraArticle.class.php
          LyraArticleTable.class.php
          ...
        vendor
    ...

Le classi in lib/model/doctrine/base vengono sempre rigenerate ogni volta che si esegue doctrine:build-model quindi non devono mai essere modificate.

Come vedremo le classi in lib/model/doctrine possono invece essere personalizzate. In effetti sono create vuote da Doctrine. Ad esempio:

lib/model/doctrine/LyraArticle.class.php

<?php
/**
 * This class has been auto-generated by the Doctrine ORM Framework
 */
class LyraArticle extends BaseLyraArticle
{

}

Generazione script SQL

./symfony doctrine:build-sql

Dopo l'esecuzione del comando, in data/sql/schema.sql troviamo lo script per la creazione delle tabelle del database. Si può notare come siano state create le opportune chiavi esterne per ogni relazione che abbiamo definito in schema.yml.

Per eseguire lo script SQL e creare le tabelle nel database si utilizza il comando

./symfony doctrine:insert-sql

Generazione moduli

Sempre a partire dalle informazioni contenute nel file schema.yml, Doctrine è in grado di generare le classi per la gestione dei moduli di inserimento dati e la loro validazione.

./symfony doctrine:build-forms

Dopo l'esecuzione del comando troveremo classi base (in lib/form/doctrine/base) e classi derivate (in lib/form/doctrine) che definiscono i moduli di inserimento e modifica dei dati con le relative regole di validazione per ogni tabella dello schema. Anche in questo caso bisogna ricordare che le classi nella sottocartella base non devono essere modificate in quanto sovrascritte ogni volta che eseguiremo il comando. Le classi in lib/form/doctrine possono invece contenere nostro codice come vedremo in seguito.

Infine esiste un comando scorciatoia

./symfony doctrine:build-all

con il quale si eseguono in sequenza:

  • doctrine:build-model
  • doctrine:build-sql
  • doctrine:build-forms
  • doctrine:insert-sql

I risultati di tutto questo lavoro, più del framework che nostro, sono fissati alla revisione 6 del progetto su Google Code.

Adesso che abbiamo il database è il caso di metterci dentro un po' di dati almeno per cominciare a vedere qualcosa nel frontend della nostra applicazione. Questo è quello che faremo la prossima volta.