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.