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).

4 commenti:

Massimo ha detto...

Mi auto-commento per segnalare che nella revisione 33 sono stati aggiunti alla tabella articoli (classe LyraArticle) due campi (num_comments, num_active_comments) contenenti rispettivamente il numero totale dei commenti e il numero dei commenti pubblicati per ogni articolo. Sono stati aggiunti a LyraComment i metodi per tenere aggiornati questi contatori quando un commento viene inserito, cancellato, pubblicato o de-pubblicato. In questo modo i totali non devono essere ricalcolati per ogni riga dell'elenco articoli, con ovvio guadagno in efficienza.

Anonimo ha detto...

ciao e complimenti davvero!! sto seguendo i tuoi post in quanto ho da poco iniziato a studiarmi Symfony. Grazie!!
Se possibile volevo chiederti come faresti un override di save/update di un form creato appunto attraverso doctrine:generate-admin.
Penso al metodo executeUpdate(), come faccio se voglio ad esempio cambiare valore a quanto scritto dall'utente?
Lo stesso, al caricamento del form, come faccio ad impostare via codice un valore diverso da quanto verrebbe caricato dal database?
Grazie mille!!
Matteo

Massimo ha detto...

Le classi delle azioni generate dall'admin generator sono nella cartella cache. Esempio cache/backend/dev/modules/nome_modulo/actions/action.class.php. Puoi sovrascriverne i metodi nella classe delle azioni che si trova nella cartella actions del tuo modulo che è derivata dalla classe creata in cache. Consiglio una lettura del capitolo su admin generarator del tutorial Jobeet (qui)

Anonimo ha detto...

ottimo! grazie

Matteo

Posta un commento

Nota. Solo i membri di questo blog possono postare un commento.