mercoledì 21 ottobre 2009

Lyra, cataloghi ed etichette

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

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

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

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

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

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

ed un altro articolo

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

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

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

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

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

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

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

lib/form/doctrine/LyraArticleForm.class.php

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

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

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

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

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

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

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

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

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

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

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

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

3 commenti:

David ha detto...

Buon giorno Massimo,

Ho problemi per capire cosa significano nella table 'labels' le colonne 'lft' 'rgt'.

Potresti spiegare meglio a cosa servono? Vedo que alcuni sono consecutivi:
7 8
5 6
6 7
...

E una altra domanda: potrebbe avere stato il root_id dei label livello, avanzato, intermedio, elementare 2 invece di 6? C'è alcun motivo per essere 6?

Grazie mille

David ha detto...

Giorgio,

Nessun bisogno di spiegazione. Ho letto l'articolo http://dev.mysql.com/tech-resources/articles/hierarchical-data.html, que domandi di leggere più avanti, ho fatto un diagrama del Nested Set Model e ho capito tutto.

Grazie.

Massimo ha detto...

Bene, anche perché non sono in grado di dare una spiegazione dei nested set migliore di quella contenuta nell'articolo citato.

Posta un commento

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