mercoledì 30 dicembre 2009

Lyra, personalizzare il form articoli

Ho lavorato un po' sul form per l'inserimento e modifica degli articoli in Lyra. Infatti fino ad ora la struttura del form è stata quella generata automaticamente da symfony, a parte piccole modifiche. Questo è invece il risultato del lavoro svolto (cliccare l'immagine per ingrandirla).

I campi sono organizzati in blocchi o 'pannelli' e il form si sviluppa su due colonne per sfruttare lo spazio orizzontale. Ecco a grandi linee i passi seguiti per personalizzare l'aspetto del form in questo modo.

Per prima cosa ho creato una classe form formatter come già fatto per il form commenti (vedi Personalizzare l'aspetto dei form in symfony ).

La classe sfWidgetFormSchemaFormatterLyraContent è molto semplice: la proprietà rowFormat serve a stabilire l'ordine degli elementi (etichetta, campo, messaggio di aiuto e messaggio di errore di validazione) all'interno di ogni riga del form; inoltre è stato ridefinito il metodo generateLabel() in modo da aggiungere un attributo di classe particolare (field-bool) a tutti i tag <label> relativi a campi boolean, questo per poter allineare, tramite css, l'etichetta di questi campi in modo diverso rispetto a quelle degli altri campi.

Ci si potrebbe chiedere la ragione di un form formatter per determinare l'apetto di un singolo form. Adesso il form è uno solo, quando saranno gestiti altri tipi di contenuto ognuno avrà il suo form di inserimento e modifica e tutti dovranno avere un aspetto consistente: la classe formatter ci faciliterà questo compito.

Si sono rese necessarie diverse modifiche alla classe LyraArticleForm. Invece di postare tutto il codice qui è preferibile un link al repository che mostri le differenze tra le revisioni 31 e 32 della classe.

La proprietà panels determina l'ordine dei pannelli e i campi contenuti in ciascun pannello, la proprietà break_at determina l'inizio della seconda colonna: entrambe vengono impostate nel metodo configure() e utilizzate nel template (_form.php).

Il pannello Etichette viene gestito in modo particolare. Visto che è l'utente che sceglie quali e quanti cataloghi possono essere utilizzati per categorizzare ogni tipo di contenuto, come si è visto a suo tempo, le liste di selezione delle etichette devono essere generate dinamicamente. Ho rimosso questa parte di codice dalla classe LyraArticleForm e creato un form dedicato (classe LyraLabelListsForm) che viene incluso nel form principale da queste istruzioni

lib/form/doctrine/LyraArticleForm.class.php

class LyraArticleForm extends BaseLyraArticleForm
{
...
  public function configure()
  {
...
    $label_lists_form = new LyraLabelListsForm(array(), array('ctype_id' => $ctype_id, 'selected' => $selected));
    $this->embedForm('labels', $label_lists_form);
...
  }
}

Demandare la creazione delle liste di selezione delle etichette ad una classe separata ci permetterà di evitare duplicazioni di codice quando sarà necessario gestire altri tipi di contenuto in aggiunta agli articoli. Il metodo saveLabels() è stato modificato per tenere conto di questa modifica.

Non è difficile ottenere lo stesso aspetto del form anche nel backend. L'admin generator di symfony è predisposto per suddividere i campi dei form in fieldset impostati nel file generator.yml. Per vedere come basta confrontare le differenze del file nelle revisioni 31 e 32. Non resta che modificare il template per utilizzare i fieldset come pannelli disposti su due colonne: _form.php.

Oltre a tutto questo ho aggiunto un campo ctype_id alla tabella articoli (modello LyraArticle) che viene utilizzato in schema.yml come chiave di una relazione ContentType che lega articoli e tipi di contenuto. Mi rendo conto che al momento non se ne capisce molto l'utilità in quanto tutti i record della tabella articoli hanno lo stesso tipo di contenuto ed i tipi di contenuto che saranno creati in seguito avranno proprie tabelle. Vedremo però che ci sono casi in cui tornerà comodo avere l'ID del tipo di contenuto sui singoli record.

Per il momento l'immediata conseguenza di questa modifica è la necessità per chi si allinea alla revisione 32 di eseguire dopo il checkout i comandi

./symfony doctrine:build --all --and-load
./symfony cc

La sintassi del comando doctrine:build è quella propria di symfony 1.3 e 1.4, come già detto la vecchia sintassi (doctrine:build-all-reload) è ancora valida in symfony 1.3, ma non lo sarà più dalla versione 1.4 per cui è bene abbandonarla subito.

La spiegazione delle modifiche è stata sintetica, chi abbia bisogno di qualsiasi chiarimento può lasciare un commento (finisco anche in rima). Al prossimo anno.

mercoledì 23 dicembre 2009

Aggiornato Lyra a symfony 1.3

Ho aggiornato Lyra a symfony 1.3.1. Per farlo ho cancellato la cartella symfony dal repository Google Code ed impostato la proprietà svn:externals su lib/vendor a questo valore:

symfony http://svn.symfony-project.com/tags/RELEASE_1_3_1/

In effetti inizialmente avevo importato nel repository l'intera copia locale del progetto incluso il framework. Ma continuando in questo modo sarei stato costretto a fare il commit dell'intero framework ad ogni aggiornamento di symfony, con perdita di tempo e spreco di spazio sul repository. Visto che non è certo mia intenzione fare modifiche al framework è molto meglio utilizzare svn:externals in modo da ricevere il contenuto di lib/vendor/symfony direttamente dal repository di symfony. Questa è anche la procedura raccomandata dalla documentazione ufficiale.

Aggiornato il framework, ho seguito le istruzioni riportate sul sito di symfony per il passaggio di un progetto dalla versione 1.2 alla 1.3 ed eseguito dalla cartella principale dell'applicazione il comando

./symfony project:upgrade1.3

Ho subito ricevuto un errore! Riporto la causa perché può capitare a chi si trovi nella stessa situazione. Visto che per Lyra utilizzo Doctrine, nella classe ProjectConfiguration era presente questa istruzione per disabilitare il plugin Propel.

config/ProjectConfiguration.class.php
...
class ProjectConfiguration extends sfProjectConfiguration
{
...
$this->disablePlugins(array('sfPropelPlugin'));
...
}

Dato che in symfony 1.3 Propel non è più l'ORM predefinito, questa riga va rimossa altrimenti la disabilitazione di un plugin non abilitato produce un errore che fa abortire il task project:upgrade1.3.

Una volta risolto questo problema la procedura di aggiornamento è stata portata a termine correttamente. Quindi è bastato ricostruire le classi del modello con

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

Sono state poi necessarie alcune piccole correzioni dovute alla nuova versione di Doctrine (symfony 1.3 utilizza Doctrine 1.2). Chi fosse interessato ai dettagli può consultare il log SVN relativo alla revisione 31: i file modificati sono molti, ma la maggior parte delle modifiche sono state fatte automaticamente dal task project:upgrade1.3. L'aggiornamento, a parte il primo intoppo iniziale, è stato molto semplice.

A chi volesse provare la nuova versione, dopo il checkout dal repository consiglierei di ricreare non solo le classi, ma anche di azzerare e generare il database ricaricando i dati di esempio. Almeno è quello che ho fatto io con

./symfony doctrine:build --all --and-load

il comando è equivalente a

./symfony doctrine:build-all-reload

che per il momento può essere ancora utilizzato anche se è bene iniziare ad usare la nuova sintassi che sarà l'unica accettata da symfony 1.4. L'elenco dei nuovi comandi 'build' di Doctrine si ottiene con

./symfony help doctrine:build

Alla fine non fa male la pulizia della cache

./symfony cc

Al più presto sarà fatto l'aggiornamento a symfony 1.4.

giovedì 17 dicembre 2009

symfony, metodi 'magici' find di Doctrine

Doctrine offre la possibilità di interrogare una tabella del database attraverso metodi speciali definiti 'magic finders'. Vediamo alcuni esempi che utilizzano il database di Lyra. Ad esempio per interrogare la tabella articles (classe LyraArticle) per prima cosa si ottiene un'istanza della classe Doctrine_Table

$dt = Doctrine::getTable('LyraArticle');

Il caso più semplice è la ricerca di un record attraverso la chiave primaria

$article = $dt->find(1);

$article contiene il record con chiave primaria (nel nostro caso il campo ID di articles) uguale a 1. Per effettuare una ricerca su un campo diverso dalla chiave primaria si utilizza la variante findBynomecampo

$articles = $dt->findByIsActive(true);

$articles contiene l'insieme degli articoli pubblicati (campo is_active è true). Da notare che quando, come in questo caso, il nome del campo è composto da più parole separate dal carattere sottolineato, il nome del metodo si crea utilizzando il camel case (campo: is_active, metodo: findByIsActive()).

Si può effettuare una ricerca su più campi, ad esempio per ottenere tutti gli articoli pubblicati (is_active true) ed impostati per apparire in prima pagina (is_featured true)

$articles = $dt->findByIsActiveAndIsFeatured(true, true);

Oltre ad AND si può utilizzare anche OR.

I metodi findBy restituiscono un insieme di record (Doctrine_Collection), quando si preferisce che venga restituito un singolo oggetto record (Doctrine_Record) si deve utilizzare findOneBynomecampo, ad esempio

$article = $dt->findOneByTitle('Titolo articolo');

Anche se esistesse più di un articolo con titolo 'Titolo articolo' solo il primo record sarebbe restituito in $article.

Abbiamo anche la possibilità di ottenere array invece che oggetti come valore di ritorno da ogni metodo 'magic finder'. In base alla scelta cambia naturalmente il modo di accedere ai valori dei campi. Ad esempio

$articles = Doctrine::getTable('LyraArticle')
  ->FindByIsActive(true);

$articles è una collezione di oggetti, quindi i valori dei campi si ottengono come proprietà dell'oggetto record.

foreach($articles as $a) {
  echo $a->title;
}

Alternativamente

$articles = Doctrine::getTable('LyraArticle')
      ->FindByIsActive(true, Doctrine::HYDRATE_ARRAY);

$articles è un array ed i valori dei singoli campi si ottengono tramite indice.

foreach($articles as $a) {
  echo $a['title'];
}

giovedì 10 dicembre 2009

Demo Lyra online

Ho messo online un mini sito dimostrativo di Lyra. Il demo è limitato al frontend, in seguito mi ripropongo di creare un account utente di prova per consentire a chi lo desideri di giocare un po' anche con il backend. Per fare questo devo lavorare ancora sulla gestione dei permessi in quanto l'unico tipo di utente disponibile al momento è il super-amministratore al cui account, per ovvie ragioni, non posso dare accesso pubblico.

Naturalmente chi ha già familiarità con symfony o ha avuto la pazienza di seguire gli articoli pubblicati fino a questo momento, può fare il checkout dal repository di Google Code e testare l'intera applicazione in locale.

Questo è l'indirizzo

http://lyra.latenight-coding.com/

Con l'occasione ho aggiunto un componente archive per visualizzare un archivio mensile degli articoli (vedere menù nella colonna destra), modificato la 'byline' dell'articolo per includere l'autore oltre alla data e fatto altri aggiustamenti. Tutto si trova nelle revisioni 24, 25, 26, 27.

mercoledì 25 novembre 2009

Lyra, gestione utenti backend

Arrivati a questo punto è necessario iniziare lo sviluppo delle funzione di gestione utenti. Chi ha scaricato l'applicazione dal repository avrà notato che non viene richiesto alcun login per accedere al backend, con la gestione gestione utenti saranno implementate le funzioni di controllo dell'accesso sia al backend che a quelle azioni del frontend che richiedono un'autenticazione (ad esempio inserimento e modifica articoli).

Seguirò le linee del tutorial ufficiale che prevede di utilizzare il plugin sfDoctrineGuardPlugin. Per prima cosa si installa il plugin con

./symfony plugin:install sfDoctrineGuardPlugin

Si attiva il plugin.

config/ProjectConfiguration.class.php

class ProjectConfiguration extends sfProjectConfiguration
{
  public function setup()
  {
    $this->enablePlugins(array('sfDoctrinePlugin','sfDoctrineGuardPlugin'));
    $this->disablePlugins(array('sfPropelPlugin'));
  }
}

Questo almeno è come ho fatto io. Se nel metodo setup() della classe ProjectConfiguration si utilizza il metodo enableAllPluginsExcept() può non essere necessario abilitare singolarmente il plugin.

Si generano le classi del modello messe a disposizione dal plugin e si pulisce la cache di symfony come al solito quando si creano nuove classi.

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

Poi si modifica la classe myUser e la si fa derivare dalla classe base che ci viene fornita dal plugin.

apps/backend/lib/myUser.class.php

class myUser extends sfGuardSecurityUser
{
}
Nella configurazione si abilitano i moduli di sfDoctrineGuardPlugin e si imposta l'azione che viene eseguita quando un utente richiede il login (signin).
apps/backend/config/settings.yml
...
all:
  .settings:
    enabled_modules: [default, sfGuardAuth, sfGuardUser, sfGuardGroup]
    ...
  .actions:
    login_module: sfGuardAuth
    login_action: signin
...

Adesso abbiamo quello che ci serve per attivare la funzione di login al backend.

apps/backend/config/security.yml

default:
  is_secure: on

Naturalmente a questo punto bisogna creare almeno un utente, lo si può fare da linea di comando con

./symfony guard:create-user admin admin

Fino a che restiamo in locale possiamo utilizzare admin come nome utente e password. Promuoviamo l'utente a superamministratore.

./symfony guard:promote admin

Il layout (layout.php in apps/backend/templates) del backend deve essere modificato per impedire la visualizzazione del menù principale agli utenti non autenticati ed aggiungervi un link per il logout e i link alla gestione utenti e gruppi utente che sono messe a disposizione dai moduli sfGuardUser e sfGuardGroup.

Questi moduli possono essere personalizzati con lo stesso metodo utilizzato per i moduli generati nell'applicazione impostando cioè i parametri di configurazione nel filegenerator.yml. Però non conviene modificare direttamente i file installati dal plugin in plugins/sfDoctrineGuardPlugin e sottocartelle, ma creare una cartella per ogni modulo in apps/backend/modules e inserirvi le personalizzazioni volute. In questo modo potremo fare gli aggiornamenti di sfDoctrineGuardPlugin senza sovrascrivere file che abbiamo modificato.

Ho creato una cartella apps/backend/modules/sfGuardUser/config e vi ho copiato il file generator.yml che si trova in plugins/sfDoctrineGuardPlugin/modules/sfGuardUser/config. Ho quindi modificato il file per personalizzare l'aspetto della lista utenti.

La stessa cosa è stata fatta per il modulo sfGuardGroup. Le personalizzazioni al momento sono minime, il contenuto dei file può essere consultato sul repository. In modo analogo è possibile personalizzare la classe actions e i template, esempi si trovano nella documentazione di sfDoctrineGuardPlugin.

Ho creato un file data/fixtures/users.yml per l'inserimento di un utente superamministratore nei dati di esempio. In questo modo durante lo sviluppo non si è costretti ad eseguire i comandi guard:create-user e guard:promote ogni volta che si ricreano le tabelle con doctrine:build-all-reload.

data/fixtures/users.yml

sfGuardUser:
  admin:
    username:       admin
    password:       admin
    is_super_admin: true

Ho inoltre definito le relazioni articolo-utente

config/doctrine/schema.yml

LyraArticle:
...
  relations:
    ...
    ArticleCreatedBy:
      class: sfGuardUser
      local: created_by
      foreign: id
      foreignAlias: CreatedArticles
    ArticleUpdatedBy:
      class: sfGuardUser
      local: updated_by
      foreign: id
      foreignAlias: UpdatedArticles

e relazioni analoghe nella tabella etichette

LyraLabel:
...
  relations:
      ...
      LabelCreatedBy:
        class: sfGuardUser
        local: created_by
        foreign: id
        foreignAlias: CreatedLabels
      LabelUpdatedBy:
        class: sfGuardUser
        local: updated_by
        foreign: id
        foreignAlias: UpdatedLabels

I file articles.yml e labels.yml in data/fixtures sono stati modificati in modo che quando si creano i dati di esempio le chiavi esterne dei record siano valorizzate opportunamente in base alle relazioni appena definite.

Tutto questo è incluso nella revisione 23. Visto che abbiamo modificato schema.yml e i file dei dati di esempio, dopo il checkout è necessario eseguire:

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

A questo punto possiamo effettuare il login al backend.

http://lyra/backend_dev.php

Login e password: admin.

lunedì 23 novembre 2009

Lyra, backend cataloghi ed etichette

Credo sia chiaro a chiunque abbia seguito gli articoli precedenti che lo sviluppo del backend di un'applicazione symfony segue linee abbastanza standard: le funzioni base di ogni modulo come la visualizzazione dell'elenco record e le funzioni di inserimento, modifica, cancellazione sono generate automaticamente con il comando doctrine:generate-admin; le personalizzazioni per la maggior parte si realizzano impostando parametri nel file di configurazione (generator.yml nella cartella config del modulo) anche se è naturalmente possibile scrivere proprio codice per realizzare funzionalità aggiuntive o che comunque si discostano dallo standard.

Per questo motivo non mi soffermerò nei dettagli dello sviluppo della parte restante del backend di Lyra perché si finirebbe per ripetere quanto già detto in precedenza. Di volta in volta metterò in evidenza solo gli aspetti su cui a mio parere vale la pena soffermarsi, d'altra parte il codice completo è sempre disponibile nel repository su Google Code.

Gestione cataloghi

./symfony doctrine:generate-admin backend LyraCatalog --module=catalog

Dopo la consueta personalizzazione di generator.yml si ottiene questo risultato.

Il contenuto delle colonne Etichette e Pubblicato è generato da due partial (_labels.php e _published.php in apps/backend/modules/catalog/templates): il primo visualizza il contatore delle etichette inserite nel catalogo e mostra un link per accedere alla relativa lista.

apps/backend/modules/catalog/templates/_labels.php

<?php
    echo link_to($lyra_catalog->countLabels()-1,'@lyra_label_label?id='.$lyra_catalog->getId());
?>&nbsp;(<?php echo link_to(__('LINK_SHOW_LABELS'),'@lyra_label_label?id='.$lyra_catalog->getId());?>)

Il secondo genera i link per le icone usate per modificare lo stato pubblicato / non pubblicato del catalogo e funziona nel modo già visto per la gestione articoli e commenti.

La configurazione del form per l'inserimento e modifica di un catalogo (classe LyraCatalogForm) non presenta particolarità. Ogni catalogo potrà essere associato ad uno o più tipi di contenuto, selezionabili da una lista di checkbox nel form; non è necessario scrivere codice particolare per creare l'insieme delle checkbox in quanto Doctrine genera automaticamente un widget sfWidgetFormDoctrineChoiceMany in base alla relazione cataloghi - tipi di contenuto definita in schema.yml. Basta impostare l'attributo expanded a true in configure() per avere un'insieme di checkbox al posto della lista di selezione.

Quando si crea un nuovo catalogo viene anche inserito nella tabella etichette un record che costituisce la 'radice' dell'albero delle etichette appartenenti al catalogo. Questo viene fatto nel metodo doSave() di LyraCatalogForm che viene eseguito da symfony quando si salva il record.

lib/form/doctrine/LyraCatalogForm.class.php

class LyraCatalogForm extends BaseLyraCatalogForm
{
  public function configure()
  {
    unset($this['created_at'], $this['updated_at'], $this['locked_by']);
    $this->widgetSchema['name']->setLabel('NAME');
    $this->widgetSchema['description']->setLabel('DESCRIPTION');
    $this->widgetSchema['is_active']->setLabel('IS_ACTIVE');
    $this->widgetSchema['catalog_content_types_list']->setLabel('CATALOG_CONTENT_TYPES');

    $this->widgetSchema['catalog_content_types_list']->setOption('expanded', true);
  }
  protected function doSave($con = null)
  {
    $savingnew = $this->isNew();
    parent::doSave($con);

    if ($savingnew) {
      $label = new LyraLabel();
      $label->setName($this->object->getName());

      $label->setCatalogId($this->object->getId());
      $label->save();
      $treeObject = Doctrine::getTable('LyraLabel')->getTree();
      $treeObject->createRoot($label);
    }
  }
}

Ho eseguito a questo punto il commit della revisione 21.

Gestione etichette

./symfony doctrine:generate-admin backend LyraLabel --module=label

Non esiste un link nel menù principale che porti ad una lista di tutte le etichette, è possibile visualizzare le etichette di ogni catalogo dal link 'Mostra' nella colonna Etichette della lista cataloghi.

La colonna Nome non mostra direttamente il contenuto di un campo, ma il risultato del metodo getIndentName() della classe LyraLabel che fa sì che il nome venga indentato a seconda del livello che l'etichetta occupa nell'albero.

lib/model/doctrine/LyraLabel.class.php

class LyraLabel extends BaseLyraLabel
{
  ...
  function getIndentName()
  {
    $indent = $this->level-1;
    if($indent < 0) {
      $indent = 0;
    }
    return str_repeat('-- ', $indent).$this->name;
  }
}

La colonna Ordina contiene due frecce per modificare l'ordine di un'etichetta rispetto alle etichette di pari livello (i nodi fratelli). In questo caso si utilizza un partial per generare il contenuto della colonna.

apps/backend/modules/label/templates/_order.php

<?php
$node = $lyra_label->getNode();

if($node->hasNextSibling()) {
  echo link_to(image_tag('backend/arrow-down.png', array('alt' => __('LINK_T_MOVE_DOWN'))),'label/move?id='.$lyra_label->getId().'&dir=0',array('title' => __('LINK_T_MOVE_DOWN')));
}
if($node->hasPrevSibling()) {
  echo link_to(image_tag('backend/arrow-up.png', array('alt' => __('LINK_T_MOVE_UP'))),'label/move?id='.$lyra_label->getId().'&dir=1',array('title' => __('LINK_T_MOVE_UP')));
}
I link sono collegati ad un'azione move implementata in labelActions.
apps/backend/modules/label/actions/actions.class.php

class labelActions extends autoLabelActions
{
  ...  
  public function executeMove(sfWebRequest $request)
  {
    $record = $this->getRoute()->getObject();
    
    switch($request->getParameter('dir')) {
      case 0:
        $next = $record->getNode()->getNextSibling();
        if($next) {
          $record->getNode()->moveAsNextSiblingOf($next);
        }
        break;
      case 1:
        $prev = $record->getNode()->getPrevSibling();
        if($prev) {
          $record->getNode()->moveAsPrevSiblingOf($prev);
        }
        break;
    }
    $this->redirect('@lyra_label_label');
  }
  ...
}
Per quanto riguarda l'utilizzo dei metodi getNode(), hasNextSibling(), hasPrevSibling(), moveAsNextSiblingOf(), moveAsPrevSiblingOf() utilizzati nel partial e nel metodo executeMove() della classe action rimando alla documentazione ufficiale di Doctrine sui nested set.

L'unica particolarità che presenta la configurazione del form per l'inserimento e modifica di un'etichetta(classe LyraLabelForm) è data dalla generazione della lista per la selezione dell'etichetta 'padre'. Il problema è che mentre una nuova etichetta può essere inserita come figlia di una qualsiasi delle etichette esistenti, quando si modifica un'etichetta non si può selezionare come padre nessuna delle etichette sue discendenti.

Serve un esempio. Data una struttura di questo tipo

PHP
-- Framework
---- Symfony
---- CakePHP

Quando si modifica l'etichetta PHP non deve essere possibile selezionare come nuovo padre né Framework, nè Symfony, né CakePHP, perché in questo modo si distruggerebbe la struttura dell'albero. Questo controllo è fatto nel metodo configure()

lib/form/doctrine/LyraLabelForm.class.php

class LyraLabelForm extends BaseLyraLabelForm
{
  public function configure()
  {
    ...
    $query = Doctrine_Query::create()->from('LyraLabel l');

    if($this->isNew()) {
        $catalog_id = sfContext::getInstance()->getUser()->getAttribute('lyra_catalog_id');
        if($catalog_id) {
          $this->setDefault('catalog_id', $catalog_id);
          $query->where('l.catalog_id = ?', $catalog_id);
        }
    } else {
        $query->where('l.catalog_id = ? AND (l.lft < ? OR l.rgt > ?)', array($this->object->getCatalogId(), $this->object->getLft(), $this->object->getRgt()));
    }

    $this->widgetSchema['parent_id'] = new sfWidgetFormDoctrineChoice(array('model'=>'LyraLabel', 'order_by'=>array('root_id, lft', ''), 'method'=>'getIndentName', 'query'=>$query));
    $this->validatorSchema['parent_id'] = new sfValidatorDoctrineChoice(array('required'=>false, 'model'=>'LyraLabel'));
    ...
  }

In inserimento si mostra nella lista tutto l'albero delle etichette appartenenti al catalogo, in modifica si escludono i discendenti del record etichetta che stiamo modificando. Da dove viene fuori la query nell'else? Per capirlo occorre conoscere più nei dettagli come viene costruito un nested set e in particolare la funzione dei campi lft e rgt: una spiegazione che personalmente ho trovato utile è questa.

Per riassumere la gestione etichette nel backend: con le frecce nell'elenco si modifica l'ordine tra etichette sullo stesso livello, entrando in modifica del record si può spostare un'etichetta e tutti i suoi discendenti (un ramo) sotto un'altra. Per ora è così, questa parte della gestione è un po' complessa e richiederà altro lavoro.

Chi volesse giocare un po' con le nuove funzioni può allinearsi alla revisione 22 di Google Code. Ricordo che per entrare nel backend basta inserire nel browser l'indirizzo

http://lyra/backend_dev.php o http://lyra/backend.php

Questo se si è configurato un virtual host secondo le istruzioni date nei primi articoli.

lunedì 16 novembre 2009

Utilizzare symfony in Windows con WampServer

Ho pensato potesse essere utile a chi volesse seguire lo sviluppo di Lyra sotto Windows una breve guida su come configurare symfony per l'utilizzo in questo sistema operativo.

Servono innanzi tutto Apache, MySql e PHP. Il modo più veloce per installarli in Windows è utilizzare un 'pacchetto' come Wamp, Xampp o EasyPHP; la procedura riportata di seguito è stata provata su WampServer versione 2.0-i, per chi utilizza uno degli altri prodotti la sostanza non cambia, ma possono esserci differenze nei percorsi dei file di configurazione di Apache e dell'eseguibile PHP.

Serve poi un client subversion. TortoiseSVN è facile da utilizzare e gratuito.

Una volta installate queste componenti creiamo una cartella per l'applicazione, ad esempio C:\sfprojects\lyra.

Checkout da repository

Come prima cosa conviene scaricare dal repository su Google Code la versione più recente dell'applicazione. In Windows Explorer fare clic con il pulsante destro sulla cartella C:\sfprojects\lyra e selezionare SVN Checkout (se non c'è l'opzione non si è installato correttamente TortoiseSVN); nella finestra di dialogo inserire:

  • URL of repository: http://lyra-cms.googlecode.com/svn/trunk/
  • Checkout directory: C:\sfprojects\lyra
  • Checkout depth: fully recursive (la casella 'Omit external' deve essere non selezionata)
  • Head revision

Il primo checkout richiede del tempo perché deve essere scaricato tutto il framework. Gli aggiornamenti a versioni successive saranno più veloci.

Configurazione virtual host

Personalmente trovo comodo lavorare in locale attraverso l'utilizzo dei virtual host: in questo modo si possono gestire molti progetti senza dover creare una miriade di sotto-cartelle nella Document Root di Apache.

Assumo che l'installazione di Wamp sia stata fatta nella cartella predefinita C:\wamp. Aprire con un editor di testi il file httpd.conf che si trova nella cartella

C:\wamp\bin\apache\Apache2.2.11\conf

Il numero di versione di Apache può essere diverso a seconda della versione di Wamp installata. Trovare questa riga

#Include conf/extra/httpd-vhosts.conf

e decommentarla rimuovendo il '#' iniziale, salvare poi il file.

Aprire il file httpd-vhosts.conf che si trova nella cartella

C:\wamp\bin\apache\Apache2.2.11\conf\extra

Accertarsi che sia presente e attiva (cioè non preceduta da '#') la riga

NameVirtualHost *:80

Aggiungere alla fine del file

<VirtualHost *:80>
    ServerName localhost
    DocumentRoot "c:/wamp/www/"
</VirtualHost>

<VirtualHost *:80>  
ServerName lyra  
DocumentRoot "c:/sfprojects/lyra/web/"  
DirectoryIndex index.php
<Directory "c:/sfprojects/lyra/web/">
    AllowOverride All
    Allow from All
</Directory>

Alias /sf "c:/sfprojects/lyra/lib/vendor/symfony/data/web/sf"
<Directory "c:/sfprojects/lyra/lib/vendor/symfony/data/web/sf">
 AllowOverride All
 Allow from All
</Directory>

Alias /sfDoctrinePlugin "c:/sfprojects/lyra/lib/vendor/symfony/lib/plugins/sfDoctrinePlugin/web"
<Directory "c:/sfprojects/lyra/lib/vendor/symfony/lib/plugins/sfDoctrinePlugin/web">
 AllowOverride All
 Allow from All
</Directory>
</VirtualHost>

Aprire il file hosts che si trova nella cartella

C:\WINDOWS\system32\drivers\etc

Aggiungere subito dopo

127.0.0.1 localhost

questa riga

127.0.0.1 lyra

Al posto di lyra si può ovviamente usare il nome che si vuole basta che corrisponda a ServerName nella configurazione del virtual host. A questo punto bisogna eseguire Wamp, se è già in esecuzione si deve comunque riavviare Apache dal menù disponibile cliccando l'icona di notifica di Wamp.

Attivazione mod_rewrite

Bisogna anche attivare il modulo Apache mod_rewrite per gestire URL semplificate (SEF) in symfony. Basta fare clic sull'icona di WampServer nell'area di notifica, selezionare l'opzione del menù Apache >> Apache modules >> scorrere la lista dei moduli fino a trovare rewrite_module e selezionarlo.

Configurazione PHP

Durante lo sviluppo di un'applicazione symfony è frequente eseguire script PHP da riga di comando. Per prima cosa copiare nella cartella C:\sfprojects\lyra il file symfony.bat che si trova nella cartella

C:\sfprojects\lyra\lib\vendor\symfony\data\bin

Occorre poi configurare una variabile di ambiente PHP_COMMAND che serve a far conoscere allo script symfony.bat il percorso dell'eseguibile PHP che nel nostro caso è

C:\wamp\bin\php\php5.3.0\php.exe

Il numero di versione di PHP può essere diverso a seconda della versione di Wamp installata.

In Windows XP (io a quello sono rimasto): Pannello di controllo >> Sistema >> tab Avanzate >> in fondo alla finestra di dialogo premere il pulsante 'Variabili di ambiente', nella finestra successiva premere 'Nuovo' sotto 'Variabili utente' (o sotto 'Variabili di sistema' se si preferisce) e inserire

  • Nome variabile: PHP_COMMAND
  • Valore variabile: C:\wamp\bin\php\php5.3.0\php.exe

Chiudere tutto con OK. Per verificare di aver fatto tutto bene aprire una finestra terminale e scrivere

cd c:\sfprojects\lyra
symfony -V

Deve comparire il numero della versione di symfony.

Configurazione database MySql

Da phpMyAdmin (con Wamp si lancia navigando con il browser all'indirizzo http://localhost/phpmyadmin/) creare un database ed un nuovo utente assegnandogli i privilegi sul database.

Modificare il file c:\sfprojects\lyra\config\databases.yml inserendo il nome database, utente e password che si sono appaena creati.

Tornare alla finestra terminale, accertarsi di essere ancora in c:\sfprojects\lyra e scrivere

symfony doctrine:build-all-reload

Dare conferma all'avviso che le tabelle del database saranno ricreate. Se non ci sono errori nella configurazione del database, si vedranno una serie di messaggi che avvisano del progresso delle varie operazioni (generazione delle classi del modello, creazione tabelle, caricamento fixtures). Alla fine scrivere

symfony cc

Aprendo il browser e digitando http://lyra/frontend_dev.php ci si trova sulla prima pagina dell'applicazione. Per entrare nel backend l'indirizzo è http://lyra/backend_dev.php.

La procedura sembra complessa, ma la gran parte del lavoro (configurazione virtual host, creazione database, impostazione PHP) si fa una volta sola. Il comando doctrine:build-all-reload va eseguito obbligatoriamente dopo il primo checkout, in seguito quando ci si aggiorna alle successive revisioni non è necessario a meno che non vi siano state modifiche alla struttura del database. Eseguire il comando comunque non fa danno, ma va tenuto presente che le tabelle vengono cancellate, ricreate e ricaricati i dati di prova (fixtures), quindi si perdono eventuali dati inseriti.

La pulizia della cache di symfony con il comando 'cc' va eseguita ogni volta dopo un checkout o update dal repository.

Tortoise SVN come ogni altro client subversion consente di creare una copia di lavoro locale aggiornata non solo alla versione più recente dell'applicazione, ma ad un qualsiasi numero di revisione che indicheremo nella finestra di dialogo dell'opzione checkout.

Come detto a suo tempo, questo permette di ripercorrere l'intero sviluppo seguendo gli articoli pubblicati dall'inizio, ma anche da un qualsiasi momento successivo in quanto in ogni articolo è indicato il numero di revisione a cui si riferisce il codice.

Tutta la procedura è stata testata, chi dovesse incontrare qualche problema lasci un commento e proverò ad aiutarlo.

giovedì 12 novembre 2009

Lyra, gestione commenti backend

Iniziamo lo sviluppo della gestione commenti nel backend. Prima di tutto creiamo il modulo.

./symfony doctrine:generate-admin backend LyraComment --module=comment

Poi passiamo alla personalizzazione del file generator.yml.

apps/backend/modules/comment/config/generator.yml

...
config:
      ...
      list:
        title: TITLE_COMMENTS
        display: [comment_article,author_name,content,_published,created_at]
        fields:
          comment_article: {label: TH_ARTICLE_TITLE}
          author_name: {label: TH_AUTHOR_NAME}
          author_email: {label: TH_AUTHOR_EMAIL}
          author_url: {label: TH_AUTHOR_URL}
          content: {label: TH_CONTENT}
          created_at: {label: TH_CREATED_AT, date_format: dd.MM.yy hh:ss}
          updated_at: {label: TH_UPDATED_AT, date_format: dd.MM.yy hh:ss}
        batch_actions:
          _delete: ~
          publish: {label: PUBLISH}
          unpublish: {label: UNPUBLISH}
        table_method: getBackendItemsQuery
...

Vediamo i vari 'blocchi' uno ad uno.

Elenco commenti

In list definiamo la configurazione delle lista commenti: impostiamo il titolo della pagina (title) in una forma che dovrà essere tradotta nei file di lingua, l'elenco delle colonne (display) con le relative intestazioni e formato (fields) e le azioni batch delete (predefinita), publish e unpublish che dovremo implementare in modo del tutto analogo a quanto fatto per la gestione articoli (simfony, personalizzare il backend).

Da notare la presenza di comment_article nella lista dei campi da mostrare nelle colonne dell'elenco: non è un campo della tabella Commenti, ma l'identificatore di una relazione impostata nello schema, solo che nel file di configurazione si separano le parole con il carattere sottolineato, nella definizione dello schema si utilizza il 'camel case'.

config/doctrine/schema.yml

LyraComment:
 ...
  relations:
    CommentArticle:
      class: LyraArticle
      local: article_id
      foreign: id
      foreignAlias: ArticleComments
      onDelete: CASCADE

In questo modo possiamo avere nell'elenco il titolo dell'articolo invece dell'ID.

La colonna _published mostra il contenuto di un partial. Serve a consentire la modifica dello stato pubblicato non pubblicato del commento restando nella visualizzazione elenco. La spiegazione nei dettagli è nell'articolo precedente perché il funzionamento è identico a quello della stessa colonna della gestione articoli: le azioni publish ed unpublish, collegate alle icone nel partial (apps/backend/modules/comment/templates/_published.php), sono gestite come di regola dalla classe commentActions, il metodo publish() è implementato in LyraComment.

Azioni batch

Anche per quanto riguarda questa parte posso tranquillamente rimandare all'articolo precedente visto che il funzionamento delle azioni batch publish e unpublish è identico a quello della gestione articoli.

Query creazione lista

Quando in una lista che mostra i record di una tabella il contenuto di una colonna viene ottenuto da una tabella correlata, come nel nostro caso il titolo dell'articolo dalla tabella articoli, conviene modificare la query utilizzata per l'estrazione dei record ed aggiungere una JOIN tra le tabelle. In questo modo si evita l'esecuzione di una query SQL distinta per ogni riga della lista.

La chiave table_method indica il nome del metodo che ritorna la query da eseguire e che viene implementato nel modello.

lib/model/doctrine/LyraCommentTable.class.php

class LyraCommentTable extends Doctrine_Table
{
  ...
  public function getBackendItemsQuery(Doctrine_Query $q)
  {
    $rootAlias = $q->getRootAlias();
    $q->leftJoin($rootAlias . '.CommentArticle a');

    return $q;
  }
}

Filtri commenti

apps/backend/modules/comment/config/generator.yml

...
    config:
      ...
      list:
      ...
      filter:
        display: [article_id, is_active]
      ...

In filter impostiamo i filtri di ricerca che ci consentono di visualizzare nell'elenco i commenti pubblicati o non pubblicati (is_active) e tutti i commenti di un determinato articolo (article_id).

Modifica commento

apps/backend/modules/comment/config/generator.yml

...
    config:
     ...
      list:
       ...
      filter:
       ...
      form:
        class: BackendLyraCommentForm
       ...

Come si è visto la pubblicazione di un commento può essere fatta rimanendo nella visualizzazione elenco, se invece serve modificare il testo o uno degli altri campi si utilizza la funzione modifica standard.

In form specifichiamo la classe da utilizzare per il modulo di inserimento e modifica commenti del backend. La classe deriva da quella creata per il frontend a sua volta derivata dalla classe base generata da Doctrine.

lib/form/doctrine/BackendLyraCommentForm.class.php

class BackendLyraCommentForm extends LyraCommentForm
{
  public function configure()
  {
      parent::configure();
      $this->widgetSchema['is_active']->setLabel('IS_ACTIVE');

      $this->widgetSchema->setHelp('author_email',false);
      $this->widgetSchema->moveField('is_active', sfWidgetFormSchema::FIRST);

  }
  protected function removeFields()
  {
      unset($this['created_at'], $this['updated_at']);
  }
}

Nel metodo configure(), prima richiamiamo la configurazione della classe base e poi impostiamo le configurazioni specifiche per il backend. Sovrascrivendo il metodo removeFields() possiamo rendere visibili nel form di backend campi che erano stati resi invisibili nel form di frontend, in particolare il campo is_active che contiene lo stato pubblicato / non pubblicato del commento.

Resta da sistemare il link che porta dall'elenco articoli alla lista dei commenti inseriti su un articolo.

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

<?php
$total = $lyra_article->countComments();
echo $total . ' / ' . ($total - $lyra_article->countActiveComments());
?>
 (<?php echo link_to(__('LINK_SHOW_COMMENTS'),'@lyra_comment_comment?id=' . $lyra_article->getId()); ?>)

Si crea il link in base alla rotta che porta alla lista commenti (azione index) aggiungendo un parametro id (articolo). Poi nella classe actions del modulo commenti si legge il parametro per impostare automaticamente un filtro che visualizzi i commenti dell'articolo con quel determinato ID.

apps/backend/modules/comment/actions/actions.class.php

class commentActions extends autoCommentActions
{
  ...
  public function executeIndex(sfWebRequest $request)
  {
    if($request->getParameter('id')) {
      $this->setFilters(array('article_id'=>$request->getParameter('id')));
    }
    parent::executeIndex($request);
    
  }
}

Impostato il filtro, si passa il controllo all'azione della classe base che è poi quella creata dal generatore del backend.

Ho infine corretto un errore nella classe actions del modulo article del frontend: dopo la modifica delle rotte degli articoli (in Generare URL SEF in symfony) il reindirizzamento alla pagina dell'articolo dopo aver inserito un commento non funzionava più. Tutto questo si trova nella revisione 20.

lunedì 9 novembre 2009

symfony, personalizzare il backend

Arrivati a questo punto è necessario iniziare lo sviluppo delle funzionalità di backend di Lyra. Sarà anche l'occasione per una veloce panoramica sul generatore di backend di symfony. Chi non ha grande familiarità con il framework può trovare molti maggiori dettagli nel tutorial ufficiale (Admin Generator).

Per prima cosa creiamo l'applicazione con il comando

./symfony generate:app --escaping-strategy=on --csrf-secret=xgt67jhbv backend

Si utilizzano le stesse opzioni viste per la generazione dell'applicazione frontend (in symfony, creazione progetto e applicazione): escaping-strategy impostato a on e csrf-secret con una sequenza di caratteri scelti a caso.

Eseguito il comando si noterà che è stata creata una cartella apps/backend e nella cartella web i front controller per l'applicazione backend:

  • backend.php
  • backend-dev.php

Backend articoli

Symfony consente di generare automaticamente un'interfaccia di backend per ciascuna classe del modello. Iniziamo con la gestione articoli.

./symfony doctrine:generate-admin backend LyraArticle --module=article

A questo punto inserendo nel browser l'indirizzo

http://lyra/backend_dev.php/article

siamo già in grado di visualizzare l'elenco degli articoli inseriti. Questo dando per scontato che si siano seguite le istruzioni degli articoli precedenti per creare un virtual host e il database del progetto e si siano eseguiti i comandi per la generazione del modello, delle tabelle e l'inserimento dei dati di esempio.

Anche se l'interfaccia è funzionante e già consente di inserire, modificare e cancellare gli articoli, l'aspetto della pagina è piuttosto brutto da vedere perché non abbiamo personalizzato il layout del backend e l'elenco degli articoli risulta troppo largo in quanto è stata creata una colonna per ciascun campo della tabella.

La prima cosa da fare è la modifica del layout (apps/backend/templates/layout.php) con l'aggiunta del relativo foglio di stile (web/css/admin.css).

Bisogna anche modificare questa riga di apps/backend/config/view.yml

stylesheets:    [main.css]

in questo modo

stylesheets:    []

Questo perché utilizziamo la funzione helper use_stylesheet() per richiamare il foglio di stile admin.css nel layout del backend.

Il layout è ancora abbastanza grezzo, ma ritengo che in questa fase sia più importante mettere in piedi qualcosa che funzioni e pensare alle rifiniture in un secondo tempo. Per cui non riporto neppure il codice che dovrà necessariamente essere modificato parecchio nel corso dello sviluppo. Il contenuto dei due file può comunque essere visualizzato su Google Code (layout.php, admin.css).

Un primo livello di personalizzazione del backend si effettua modificando le impostazioni nel file di configurazione generator.yml nella cartella config di ogni modulo. La procedura è abbastanza standardizzata e ben documentata, mi limiterò ad elencare in estrema sintesi le operazioni svolte.

apps/backend/modules/article/config/generator.yml
...
  config:
      actions: ~
      fields:  ~
      list:
        title: TITLE_ARTICLES
        display: [=title,_comments,_published,_front_page,created_at,id]
        fields:
          title: {label: TH_TITLE}
          created_at: {label: TH_CREATED_AT, date_format: dd.MM.yy hh:mm}
          updated_at: {label: TH_UPDATED_AT, date_format: dd.MM.yy hh:mm}
          id: {label: TH_ID}
        ...
      filter:
        display: [title,is_active]
...

Viene impostato un titolo per la pagina, vengono scelti i campi da mostrare nell'elenco in modo da ridurre il numero delle colonne e modificate le etichette delle intestazioni colonna. Il testo effettivo per titolo ed etichette come al solito sarà incluso nei file delle traduzioni.

Vengono scelti i filtri di ricerca. Al momento si consente una ricerca per titolo articolo e stato pubblicazione.

Da notare le colonne _comments, _published, _front_page impostate nel parametro display. il carattere '_' iniziale ci dice che il contenuto di queste colonne non è il valore di un campo, ma il risultato della elaborazione di altrettanti partial definiti in apps/backend/modules/article/templates. Vediamoli uno per uno.

Contatori numero commenti articolo

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

<?php
$total = $lyra_article->countComments();
echo $total . ' / ' . ($total - $lyra_article->countActiveComments());
?>
 (<a href="#"><?php echo __('LINK_SHOW_COMMENTS') ?></a>)

Dovrebbe essere abbastanza comprensibile: nella colonna vengono visualizzati il numero totale dei commenti dell'articolo seguito dal numero dei commenti in attesa di moderazione e da un link che porterà all'elenco dei commenti dell'articolo. Il link non funzionerà fino a che non avremo sviluppato il modulo commenti nel backend. I metodi countComments() e countActiveComments() sono implementati nel modello (classe LyraArticle) e utilizzano due semplici query per calcolare i valori dei contatori.

lib/model/doctrine/LyraArticle.class.php

class LyraArticle extends BaseLyraArticle
{
  ...
  public function countActiveComments()
  {
    return $this->getActiveCommentsQuery()
      ->count();
  }
  public function countComments()
  {
    return $this->getCommentsQuery()
      ->count();
  }
  ...
  protected function getCommentsQuery()
  {
    $q = Doctrine_Query::create()
      -> from('LyraComment c')
      ->andWhere('c.article_id = ?', $this->getId());
    return $q;
  }
  protected function getActiveCommentsQuery()
  {
    $q = Doctrine::getTable('LyraComment')
      ->getActiveItemsQuery();

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

Stato pubblicato / non pubblicato

Nel backend standard di symfony nelle colonne relative ai campi boolean viene mostrato un segno di spunta se il campo è true, se è false la cella viene lasciata vuota. Per modificare il valore del campo da vero a falso e viceversa si deve entrare in modifica del record. Sono abituato al backend di Joomla dove in questi casi il cambio di stato (ad esempio pubblicato / non pubblicato) si può effettuare rimanendo in visualizzazione elenco semplicemente facendo click sull'icona nella cella e ho voluto mantenere questa scorciatoia. La cosa si può fare abbastanza semplicemente mostrando un partial (_published) al posto del valore del campo.

apps/backend/modules/article/templates/_published.php

<?php if ($lyra_article->getIsActive()): ?>
  <?php echo link_to(image_tag('backend/yes.png', array('alt' => __('LINK_T_PUBLISHED'))),'article/unpublish?id='.$lyra_article->getId(), array('title' => __('LINK_T_PUBLISHED'))) ?>
<?php else: ?>
  <?php echo link_to(image_tag('backend/no.png', array('alt' => __('LINK_T_UNPUBLISHED', array(), 'sf_admin'))),'article/publish?id='.$lyra_article->getId(), array('title' => __('LINK_T_UNPUBLISHED'))) ?>
<?php endif; ?>

Se l'articolo è pubblicato si mostra l'icona del segno di spunta collegata all'azione per de-pubblicare ) l'articolo; l'inverso se l'articolo non è pubblicato.

Aggiungiamo la gestione delle azioni publish / unpublish.

apps/backend/modules/article/actions/actions.class.php
..
class articleActions extends autoArticleActions
{
  public function executePublish(sfwebRequest $request)
  {
    $this->lyra_article = $this->getRoute()->getObject();
    $this->lyra_article->publish();
    $this->getUser()->setFlash('notice', 'MSG_ARTICLE_PUBLISHED');
    $this->redirect('@lyra_article_article');
  }
  public function executeUnpublish(sfwebRequest $request)
  {
    $this->lyra_article = $this->getRoute()->getObject();
    $this->lyra_article->publish(false);
    $this->getUser()->setFlash('notice', 'MSG_ARTICLE_UNPUBLISHED');
    $this->redirect('@lyra_article_article');
  }
  ...
} // fine articleActions

Il metodo publish() va implementato nel modello

lib/model/doctrine/LyraArticle.class.php

class LyraArticle extends BaseLyraArticle
{
  ...
  public function publish($on = true)
  {
    $this->setIsActive($on);
    $this->save();
  }
  ...
} //fine LyraArticle

Stato in prima pagina / non in prima pagina

La stessa cosa viene fatta per lo stato di pubblicazione in prima pagina. Non riporto il codice che è praticamente identico: il partial è _front_page e le azioni feature e unfeature.

Azioni batch

Tramite azioni batch si può compiere una determinata operazione 'in serie' su un gruppo di record selezionati nell'elenco. Il generatore di backend di symfony crea automaticamente un'azione di questo tipo che consente la cancellazione di record multipli.

Ne creeremo altre due personalizzate per impostare / rimuovere lo stato di pubblicato su più articoli. Per prima cosa definiamo le azioni batch nel file di configurazione.

apps/backend/modules/article/config/generator.yml
...
    config:
      actions: ~
      fields:  ~
      list:
        #parte già esaminata sopra
        batch_actions:
          _delete: ~
          publish: {label: PUBLISH}
          unpublish: {label: UNPUBLISH}
      ...

Scriviamo poi i gestori delle azioni nella classe actions del modulo article.

class articleActions extends autoArticleActions
{
 
  public function executeBatchPublish(sfWebRequest $request)
  {
    $ids = $request->getParameter('ids');
    Doctrine::getTable('LyraArticle')->publish($ids);
    $this->getUser()->setFlash('notice', 'MSG_ARTICLE_PUBLISHED');
    $this->redirect('@lyra_article_article');
  }
  public function executeBatchUnpublish(sfWebRequest $request)
  {
    $ids = $request->getParameter('ids');
    Doctrine::getTable('LyraArticle')->publish($ids, false);
    $this->getUser()->setFlash('notice', 'MSG_ARTICLE_UNPUBLISHED');
    $this->redirect('@lyra_article_article');
  }
} // fine articleActions

I nomi dei metodi che processano un'azione batch devono seguire uno standard: executeBatch + nome azione. Il parametro ids della richiesta contiene un array con gli ID dei record selezionati tramite le caselle di selezione nell'elenco.

Implementiamo il metodo publish() in LyraArticleTable

class LyraArticleTable extends Doctrine_Table
{
  public function publish($ids, $on = true)
  {
    $q = $this->createQuery('a')
      ->whereIn('a.id', $ids);

    foreach ($q->execute() as $item) {
      $item->publish($on);
    }
  }
} //fine LyraArticleTable

Viene fatta una query per selezionare i record in base al valore degli ID passati dall'azione. Per ogni record si invoca il metodo publish() di LyraArticle che abbiamo creato poco sopra.

Tema di backend personalizzato

Per ottenere un livello di personalizzazione maggiore di quelo che si ottiene attraverso le impostazioni del file di configurazione, ci si può creare un proprio tema per il backend da utilizzare al posto di quello predefinito.

La procedura è spiegata nella documentazione ufficiale (Admin Generator).

La cosa più semplice da fare è partire dal tema per il backend predefinito che si trova in

lib/vendor/symfony/lib/plugins/sfDoctrinePlugin/data/generator/sfDoctrineModule/admin

Copiare il contenuto della cartella admin (cioè le cartelle parts, skeleton e template) in data/generator/sfDoctrineModule/admin

Possiamo a questo punto personalizzare il tema in questa cartella senza rischiare di perdere le nostre modifiche al successivo aggiornamento del framework. Al momento le personalizzazioni sono minime, ho soltanto creato uno slot per visualizzare il titolo delle pagine in una posizione diversa.

Da così

a così

data/generator/sfDoctrineModule/admin/template/templates/indexSuccess.php
...
[?php slot('page_title',<?php echo $this->getI18NString('list.title') ?>) ?]
<div id="sf_admin_container">
//La riga seguente viene rimossa
<h1>[?php echo <?php echo $this->getI18NString('list.title') ?> ?]</h1>
...
Lo slot è richiamato in questa parte del layout
apps/backend/templates/layout.php
...
      <div id="header">
        <h1>
          <?php include_slot('page_title') ?>
        </h1>
      </div>
...

Modifiche del tutto analoghe sono state fatte nei file editSuccess.php e newSucces.php.

Le personalizzazioni apportate al tema in questo modo saranno applicate anche ai moduli creati successivamente con il comando doctrine:generate-admin. Lo vedremo la prossima volta quando sarà creato il modulo per la gestione dei commenti da backend.

Il codice della parte discussa fino a questo momento è come al solito su Google Code (revisione 19).

giovedì 5 novembre 2009

Generare URL SEF in symfony

Quando si sono delineate le funzionalità essenziali di Lyra, nell'elenco era presente la creazione di URL semplificate, 'pulite' o SEF (ognuno utilizzi la terminologia che preferisce). Fino a questo momento abbiamo incontrato tre tipi di URL.

1) Prima pagina e questa ovviamente non pone problemi particolari.

http://www.example.com/

2) Vista articolo a tutta pagina. Esempio:

http://www.example.com/article/show/id/2

3) Lista articoli per etichetta. Esempio:

http://www.example.com/article/label/id/1

Queste URL non sono troppo lunghe e non presentano una lista di parametri sotto forma di query string, sono sicuramente migliori di, ad esempio:

http://www.example.com/index.php?module=article&action=show&id=2

Però credo che molti utenti avrebbero come minimo qualche dubbio a definirle vere e proprie URL SEF. Va detto che hanno il vantaggio di poter essere generate in base a rotte predefinite senza che sia necessario modificare alcun file di configurazione, se si è disposti a rinunciare a questo vantaggio si possono personalizzare le URL della propria applicazione come si preferisce.

Come esempio modificheremo la URL della vista articolo a tutta pagina in modo che risulti così:

http://www.example.com/article/titolo-articolo.html

Per prima cosa definiamo una nuova rotta. L'ordine delle definizioni è importante per cui inseriamo la nostra prima di quelle già presenti (rotte di default)

apps/frontend/config/routing.yml

article_show:
  url: /article/:slug.html
  class: sfDoctrineRoute
  options:
    type: object
    model: LyraArticle
    method: findItem
  param:
    module: article
    action: show
  requirements:
    sf_method: [get]

  • url indica il formato della URL generata dalla rotta: gli identificatori preceduti da ':' (es. :slug) sono variabili;
  • class è la classe della rotta: sfDoctrineRoute indica che questa rotta è associata ad un oggetto (record) o insieme di oggetti (collezione di record) istanze di una classe modello Doctrine;
  • options indica che la rotta è associata ad un singolo record (type è object, l'alternativa, list indicherebbe l'associazione con una collezione di record) di classe LyraArticle. findItem è il metodo del modello che servirà a reperire l'oggetto corrispondente alla rotta, nel nostro caso il record articolo da visualizzare;
  • param indica che la richiesta che corrisponde a questa rotta verrà processata dall'azione show del modulo article;
  • requirements indica che il metodo della richiesta deve essere GET.

Bisogna poi modificare i template in modo che i link agli articoli siano generati in base alla rotta appena definita.

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

...
  <h2 class="article-title">
    <?php echo link_to($item->getTitle(), '@article_show?slug=' . $item->getSlug())?>
  </h2>
...
  <span class="article-readmore">
    <?php echo link_to(__('LINK_READMORE'), '@article_show?slug=' . $item->getSlug(), array('title'=>$item->getTitle()))?>
  </span>
...

Alla funzione helper link_to() passiamo direttamente il nome della rotta preceduto da '@' a cui vengono accodati i valori per le parti variabili della rotta (:slug) nella stessa forma che sarebbe utilizzata per costruire una query string. Chiaramente il link risultante viene generato come indicato dal parametro url nella definizione della rotta, cioè in un formato a 'segmenti' senza alcun parametro in forma di query string.

Resta da vedere come viene individuato l'articolo da visualizzare quando viene cliccato uno dei link generati nel modo appena visto. Abbiamo detto che una rotta è associata ad un oggetto (o ad una collezione di oggetti), quindi partendo dalla rotta possiamo risalire all'oggetto corrispondente. Questo avviene nell'azione (le righe sbarrate sono quelle presenti nella versione precedente e ora rimosse)

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);
    $this->item = $this->getRoute()->getObject();
    ...
   }
 ...  
} // fine articleActions

Il metodo getObject() invoca il metodo definito nelle options della rotta (findItem()) e gli passa un array di parametri contenente le variabili della rotta. Nel metodo dobbiamo eseguire una query in base a questi parametri e ritornare l'oggetto oppure false: nel primo caso l'oggetto viene passato indietro all'azione ed il flusso continua; nel secondo caso nel codice di getObject() viene sollevata un'eccezione che genera un errore 404.

Proprio perché nel caso di oggetto non trovato l'errore viene gestito internamente abbiamo rimosso dall'azione la riga

$this->forward404Unless($this->item);

Se per qualsiasi ragione vogliamo continuare a gestire questa situazione di errore nel codice dell'azione, basta modificare la definizione della rotta in questo modo

article_show:
  ...
  options:
    model: LyraArticle
    type: object
    method: findItem
    allow_empty: true
...

Con l'opzione allow_empty impostata a true il metodo getObject() ritorna null in caso di oggetto non trovato senza generare internamente nessuna eccezione. Chiaramente a questo punto dovremmo reintrodurre la chiamata al metodo forward404Unless() o comunque gestire la situazione opportunamente. Il codice ad esempio dovrebbe essere modificato così:

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

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

Resta solo da implementare findItem() nel modello.

lib/model/doctrine/LyraArticleTable.class.php

class LyraArticleTable extends Doctrine_Table
{
...
  public function findItem($params = array())
  {
    if(!isset($params['slug'])) {
      return false;
    }
    $q = $this->getActiveItemsQuery();
    $q->andWhere($q->getRootAlias() .'.slug = ?', $params['slug']);
    
    return $q->fetchOne();
  }
} // fine LyraArticleTable

Nel nostro esempio l'array di parametri contiene un solo elemento, il valore del campo slug dell'articolo.

Ho inoltre aggiunto una rotta (article_label) per dare questo formato alle URL della pagina con l'elenco degli articoli per etichetta generate dal componente visto nei due articoli precedenti:

http://www.example.com/article/label/slug-etichetta

Il procedimento è del tutto analogo a quello appena visto, il codice è disponibile nel repository (revisione 18) insieme a tutto quello illustrato sopra.

Naturalmente le possibilità offerte da symfony non si fermano certo qui. Si potrebbe includere la data dell'articolo nella URL per creare un permalink in stile blog

http://www.example.com/article/2009/11/titolo-articolo.html

Oppure includere le etichette/categorie

http://www.example.com/article/javascript/jquery/articolo-su-jquery.html

Ma è prematuro continuare lo sviluppo di questa parte adesso. Il formato delle URL dei contenuti è un aspetto importante di un cms e richiederà ulteriore lavoro. Al momento va data la precedenza ad alcune parti essenziali dell'applicazione che mancano completamente, ad esempio la gestione del backend di cui non abbiamo scritto neppure una riga. Si inizierà dalla prossima volta.

martedì 3 novembre 2009

Lyra, elenco articoli per etichetta

Come si è visto nell'articolo precedente i link del menù Etichette hanno questo formato

article/label/id/x

Dove x è l'ID del record etichetta.

Dobbiamo scrivere il codice per l'azione label del modulo article ed il relativo template che determinerà l'aspetto della pagina con l'elenco degli articoli catalogati sotto una certa etichetta. L'elenco può naturalmente essere anche lungo quindi avremo bisogno di una funzione di paginazione, symfony ci mette a disposizione una classe dedicata a questo scopo.

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

class articleActions extends sfActions
{
...
  public function executeLabel(sfWebRequest $request)
  {
    $this->forward404Unless(
      $this->label = Doctrine::getTable('LyraLabel')
        ->find($request->getParameter('id'))
    );
    $this->pager = new sfDoctrinePager('LyraLabel', 25);
    $this->pager->setQuery($this->label->getItemsQuery());
    $this->pager->setPage($request->getParameter('page', 1));
    $this->pager->init();
  }
...
}
All'oggetto pager viene passata la query per la selezione degli articoli. Si imposta un valore costante (25) per il numero di record per pagina, in seguito questo parametro dovrà essere gestito dalla configurazione. La creazione della query avviene nel modello.
lib/model/doctrine/LyraLabel.class.php

class LyraLabel extends BaseLyraLabel
{
  public function getItemsQuery() {
    $q = Doctrine::getTable('LyraArticle')
      ->getActiveItemsQuery();

    $q->innerJoin($q->getRootAlias().'.ArticleLabels l')
      ->andWhere('l.id = ?', $this->getId());
    return $q;
  }
  ...
}

Interessante notare la sintassi DQL (Doctrine Query Language) usata per la query. Le tabelle articoli (articles, modello LyraArticle) ed etichette (labels, modello LyraLabel) sono tra loro in una relazione molti a molti tramite una tabella intermedia (article_label, modello LyraArticleLabel).

La relazione è definita in config/doctrine/schema.yml (vedere relations di LyraArticle) ed identificata come ArticleLabels: questo identificatore è l'unica informazione che passiamo al metodo innerJoin(), come si vede non ci sono clausole ON né nomi di tabelle in quanto tutte le informazioni necessarie a Doctrine per creare la query si trovano nello schema.

Veniamo al template labelSuccess.php.

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

<?php 
slot('page_title', $label->title);
...

Impostiamo uno slot per visualizzare il titolo della pagina. Con gli slot si può inserire del contenuto in zone predeterminate del layout.

...
include_partial('article/list', array('items'=>$pager->getResults()));
?>
<?php if ($pager->haveToPaginate()): ?>
  <?php 
    $base = 'article/label?id=' . $label->getId() . '&page=';
  ?>
  <div class="pagination">
    <?php echo link_to('First', $base . '1');?>
    <?php echo link_to('Prev', $base . $pager->getPreviousPage());?>
    <?php foreach ($pager->getLinks() as $page): ?>
      <?php if ($page == $pager->getPage()): ?>
        <?php echo $page ?>
      <?php else:
        echo link_to($page, $base . $page);
      endif; ?>
    <?php endforeach; ?>
    <?php echo link_to('Next', $base . $pager->getNextPage());?>
    <?php echo link_to('Last', $base . $pager->getLastPage());?>
  </div>
<?php endif; ?>
<!-- Fine  labelSuccess.php -->

Per la visualizzazione dell'elenco articoli riutilizziamo il partial creato per la prima pagina: di ogni articolo sarà visualizzato il titolo, la data e il sommario con link 'leggi tutto'. Il metodo getResults() dell'oggetto pager restituisce gli articoli da visualizzare in base al numero di pagina che abbiamo impostato nell'azione. La parte restante del template serve alla visualizzazione dei link per la paginazione.

Resta da aggiungere al layout l'istruzione per includere il contenuto dello slot impostato nel template.

Questa parte

apps/frontend/templates/layout.php
...
<div id="header">
  <h3>Titolo pagina</h3>
</div>
...

va modificata in questo modo

apps/frontend/templates/layout.php
...
<div id="header">
  <h3><?php include_slot('page_title'); ?></h3>
</div>
...

Tutto questo si trova nella revisione 17 su Google Code.

lunedì 2 novembre 2009

Lyra, componente etichette

Sui blog o altri siti che pubblicano notizie e articoli è abbastanza comune trovare un menù con una lista di collegamenti a pagine che mostrano l'elenco degli articoli appartenenti ad una determinata categoria. Un esempio è data dal riquadro Etichette presente nella colonna destra delle pagine di questo blog. Vediamo come implementare questa funzione in Lyra.

Quando si devono visualizzare informazioni in una colonna laterale o comunque in un'area diversa dal corpo principale della pagina si può utilizzare un componente.

Vediamo innanzi tutto come il componente viene richiamato dal layout:

apps/frontend/templates/layout.php

...
<div id="rightbar">
  <?php include_component('article', 'labels', array('catalog'=>'Argomento')) ?>
</div>
...

Gli argomenti passati alla funzione helper include_component() sono nell'ordine: il nome del modulo, il nome del componente, un array opzionale contenente parametri che saranno accessibili dal codice del componente (vedremo subito come).

In symfony un componente consiste in una action ed un template.

Action di un componente

Viene implementata come metodo [componente]Execute() di una classe [modulo]Components derivata da sfComponents e definita in apps/frontend/modules/[modulo]/actions/components.class.php. Nel nostro caso (componente labels, modulo article) quindi

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

class articleComponents extends sfComponents
{
  public function executeLabels(sfWebRequest $request)
  {
    $catalog = Doctrine::getTable('LyraCatalog')
      ->findOneByName($this->catalog);
    $this->tree = $catalog->getLabelTree();
  }
}

$this->catalog contiene il valore del parametro catalog contenuto nell'array di parametri passato ad include_component(). Questo valore viene passato a findOneByName() per ottenere l'oggetto record corrispondente, poi con getLabelTree() otteniamo l'albero delle etichette associate al catalogo. Questo metodo va ovviamente implementato nella classe LyraCatalog.

lib/model/doctrine/LyraCatalog.class.php

class LyraCatalog extends BaseLyraCatalog
{
  public function getLabelTree()
  {
    $q = Doctrine_Query::create()
      ->from('LyraLabel l')
      ->where('l.catalog_id = ? and l.level = 0', $this->getId());

    $root = $q->fetchOne();

    return $root->getNode()->getDescendants();
  }
}

Per capire il codice va tenuto presente che esistono relazioni gerarchiche tra i record della tabella Etichette. I record a livello zero (ne esiste uno per ogni catalogo) sono le radici dell'albero delle categorie associate ad un catalogo. Con una query selezioniamo il record radice del catalogo ($root): i suoi discendenti sono l'albero delle categorie che ci interessa e che possiamo ottenere semplicemente con la chiamata ad un singolo metodo getDescendants() perché la tabella Etichette è costruita come nested set (vedere LyraLabel in config/doctrine/schema.yml).

Template di un componente

Il template di un componente non è diverso da quelli già visti per le singole azioni di un modulo.

<h4><?php echo __('HEAD_LABELS')?></h4>
<ul class="label-list">
<?php foreach($tree as $node): ?>
  <li class="lev<?php echo $node->getLevel(); ?>">
    <?php echo link_to($node->getName(),'article/label?id='.$node->getId()); ?>
  </li>
<?php endforeach ?>
</ul>

Tutto dovrebbe essere abbastanza chiaro. Faccio notare solo che:

  • $tree nel template contiene il valore della proprietà assegnata nell'azione con $this->tree con il meccanismo già visto in precedenza;
  • sarà necessario creare una nuova azione (label) nel modulo article che servirà a visualizzare la pagina con l'elenco degli articoli appartenenti alla categoria. La funzione helper link_to() genera un link per questa azione.

L'implementazione dell'azione label sarà fatta la prossima volta. Per il momento quanto sviluppato fino a questo punto si trova nella revisione 16 su Google Code.

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.