La comunità italiana su CakePHP

You are not logged in. Please login or register.


Pages: 1

Atom RSS

Posts [ 9 ]

zuck

Topic: [1.2] Taggable Behavior

Salute a tutti! wink

Allora, premetto che so bene che l'argomento viene trattato spesso, ma il fatto è che anche guardando in giro (e leggendo diversi articoli) non riesco a venirne fuori, anche se penso che manchi qualcosa di molto piccolo.

Bando alle ciance, il mio problema è il seguente:

1. Ho un Wizard (una procedura guidata) che passa attraverso delle azioni. Ad ogni passo i dati del form vengono salvati in una chiave della sessione.

2. Il Wizard serve a creare o aggiornare una pagina (un post se volete).

3. La pagina (modello: 'Page') possiede le seguenti associazioni:

    - HABTM con i tags (modello: Tag)
    - HABTM con gli utenti (modello: User)
    - belongsTo con le immagini (modello: Image --> "Image" possiede un "hasMany" con "Page")

4. I campi della mia pagina sono:

    - id
    - created/modified
    - name
    - type
    - title
    - subtitle
    - body
    - from
    - image_id

5. Quando arrivo al termine della procedura guidata di inserimento, il codice che utilizzo è il seguente (controller: PagesController):

        // Ottiene nuovamente i dati integri dalle chiavi di sessione salvate al termine
        // dei passi precedenti a quest ultimo (in quanto il form della vista di riepilogo
        // dei dati inseriti ne invia solo alcuni e non utilizzo campi nascosti).
        $page = $this->Session->read($this->Wizard->sessionKey.'.data');
        $image = $this->Session->read($this->Wizard->sessionKey.'.image');

        $this->data['Page'] = $page['Page'];
        $this->data['Page']['image_id'] = $image['Page']['image_id'];
        
        // Verifica se deve aggiornare oppure creare una nuova riga nella
        // tabella delle pagine.
        if (isset($this->data['Page']['id'])) {

            $this->Page->id = $this->data['Page']['id'];

        } else {

            $this->Page->create();
        }
    
        return ($this->Page->save($this->data));

Questa funzione serve a validare i dati ottenuti dal passo corrente del "Wizard" (che è l'ultimo, quello di riepilogo dei dati inseriti). Questa funzione mi restituisce "falso", segno che il salvataggio non va a buon fine...

Cosa c'e' che non va? Ho provato anche a impostare manualmente i campi mancanti (Tags e Users) con le seguenti righe prima del metodo per il salvataggio:

$this->data['User']['User']['id'] = 1;
$this->data['Tag'] = array('Tag' => array(1, 2));

Ma niente da fare, il salvataggio continua a non andare a buon fine. Che cosa sbaglio?

Grazie e saluti!! wink

Saiborg

Re: [1.2] Taggable Behavior

Un po' difficile così al volo. Hai provato a mettere DEBUG 3? Così da vedere anche come sta traducendo le cose in SQL.

Luizz

Re: [1.2] Taggable Behavior

SI, in effetti dovresti fare un debug delle query eseguite per verificare che i dati che tu passi con

 $this->data['Page'] = $page['Page'];

siano corretti.
In linea di massima dovresti assegnare i valori alla proprietà $data nel seguente modo

$data = array ('Page’ => array(’id’ => 4) ,
‘Tag’ => array(’Tag’ => array (23,56,67,58), // id dei tag associati
‘User’ => array(’User’ => array (3,6)) // id degli utenti associati
);
$this->Page->create($data);
$this->Page->save();

Ho scritto un appunto sul mio sito, vedi se ti può essere utile e se non va ancora documenta meglio il problema che si presenta (con query e errori annessi).

zuck

Re: [1.2] Taggable Behavior

Un'altra cosa: "Page" e "Image" hanno un "behavior" chiamato "Taggable". La cosa importante è ciò che accade in Taggable::beforeSave che riporto qui sotto:

/* In "setup" */
$default = array( 'table_label' => 'tags', 'tag_label' => 'name', 'separator' => ' ');

/**/
    function beforeSave(&$model) {
        
        // Define the new tag model
        $tag =& new Tag;
        
        if (isset($model->data[$model->name][$this->settings[$model->name]['table_label']]) && 
            $tag->hasField($this->settings[$model->name]['tag_label'])) {

            // Parse out all of the tags.
            $tag_list = $this->_parseTags($model->data[$model->name][$this->settings[$model->name]['table_label']], $this->settings[$model->name]);
            
            $tag_data = array(); // New tag array to store tag id and names from db
            
            foreach($tag_list as $t) {
                
                if ($res = $tag->find($this->settings[$model->name]['tag_label'] . " LIKE '" . $t . "'")) {
                    
                    $tag_data[] = $res['Tag']['id'];
                    
                } else {
                    
                    $tag->save(array('Tag' => array($this->settings[$model->name]['tag_label'] => $t)));
                    
                    $tag_data[] = sprintf($tag->getLastInsertID());
                }
                
                unset($res);
            }
    
            // This prepares the linking table data...
            $model->data['Tag']['Tag'] = $tag_data;
        }
        
        return true;
    }

Questo pezzo di codice sembra funzionare (sicuramente funzionava prima, quando non usavo il Wizard e i dati erano mappati 1:1 sui campi di un form). Come piccolo tentativo ulteriore di debug ho piazzato un bel "echo" alla fine di ogni metodo "beforeValidate" e "beforeSave" di ogni modello e comportamento interessato. La sequenza risultante però è decisamente triste:

Page.beforeValidate()
Taggable.beforeSave()

Segno che dopo la conclusione del metodo "taggable::beforeSave" avviene qualcosa che fa bloccare il tutto.

Per la query...uhm...sarebbe facile se nel layout della pagina non richiedessi contemporaneamente anche altre azioni (ci sono alcuni box che visualizzano determinate risorse o pannelli facendo ricorso ad azioni dei vari controller). Questo implica che le query SQL che interessano sono in mezzo a diverse altre...Comunque questo dovrebbe essere l'estratto corretto:

Nr    Query

1    SELECT `Page`.`id`, `Page`.`name`, `Page`.`type`, `Page`.`created`, `Page`.`modified`, `Page`.`title`, `Page`.`subtitle`, `Page`.`body`, `Page`.`image_id`, `Page`.`from`, `Image`.`id`, `Image`.`name`, `Image`.`alt`, `Image`.`width`, `Image`.`height` FROM `pages` AS `Page` LEFT JOIN `images` AS `Image` ON (`Page`.`image_id` = `Image`.`id`) WHERE `Page`.`name` = 'wizard' LIMIT 1

2    SELECT `Image`.`id`, `Image`.`name`, `Image`.`alt`, `Image`.`width`, `Image`.`height` FROM `images` AS `Image` WHERE `Image`.`id` = 0 LIMIT 1

3    SELECT `Page`.`id`, `Page`.`name`, `Page`.`type`, `Page`.`created`, `Page`.`modified`, `Page`.`title`, `Page`.`subtitle`, `Page`.`body`, `Page`.`image_id`, `Page`.`from` FROM `pages` AS `Page` WHERE `Page`.`image_id` IN ('0') 

4    SELECT `Tag`.`id`, `Tag`.`name`, `ImagesTag`.`id`, `ImagesTag`.`image_id`, `ImagesTag`.`tag_id` FROM `tags` AS `Tag` JOIN `images_tags` AS `ImagesTag` ON (`ImagesTag`.`image_id` IN ('0') AND `ImagesTag`.`tag_id` = `Tag`.`id`)

Last edited by zuck (06-02-2008 17:26:03)

Luizz

Re: [1.2] Taggable Behavior

Sarebbe opportuno che tu esaminassi il valore di $model->data dopo il beforeSave, perchè a quel punto dovrebbe contenere la struttura  necessaria a salvare correttamente i dati nel DB.
Inoltre le query che hai riportato sono solo select, mentre sarebbe necessario verificare che non ci siano query che riportano errori.

zuck

Re: [1.2] Taggable Behavior

Ho trovato l'errore, incredibilmente stupido e banale....come sempre del resto in questi casi tongue

Avevo dimenticato di restituire "True" dopo un "beforeSave()" di un behavior (che fra l'altro pensavo di aver tolto)...

Morale della favola: Ricordatevi di restituire sempre un booleano dai metodi "beforeXXX" wink big_smile

Scusate wink (magari il codice che ho riportato potrà servire da snippet per qualcuno wink )

Luizz

Re: [1.2] Taggable Behavior

Perchè non posti tutto il behavior ? Se non ho capito male dovrebbe prendere dei tag da un campo del form, inserirli nella tabella Tag se non esiste e creare la corretta relazione HABTM con il model corrente .... giusto ?

zuck

Re: [1.2] Taggable Behavior

Si, è una versione modificata di un analogo behavior che ho trovato su CakeForge (o su il Bakery, non ricordo).

Il funzionamento è semplice:

- Nel form del modello a cui associare i tags si inserisce un campo per scrivere una sequenza di tags distanziate da un separatore (impostabile).

- Il behavior si occupa di fare il "parsing" della stringa, di ottenere i singoli tags e di inserirli nell'apposita tabella (se non sono già presenti). Infine di associarli al modello che utilizza il behavior.

- Offre anche alcune funzioni per avere il processo inverso in modo da riottenere automaticamente la stringa delle tags pronta, da usare ad esempio per la visualizzazione.

Ecco il codice:

<?php 

/**
 * Taggable Behavior class file.
 *
 * Model Behavior to support tags.
 *
 * @author        Emanuele 'Zuck' Bertoldi
 * @filesource
 * @package       app
 * @subpackage    models.behaviors
 */
class TaggableBehavior extends ModelBehavior {
    
    /**
     * Initiate behaviour for the model using specified settings.
     *
     * @param object $model    Model using the behaviour
     * @param array $settings    Settings to override for model.
     *
     * @access public
     */
    function setup(&$model, $settings = array()) {

        $default = array( 'table_label' => 'tags', 'tag_label' => 'name', 'separator' => ' ');
        
        if (!isset($this->settings[$model->name])) {
            
            $this->settings[$model->name] = $default;
        }
        
        $this->settings[$model->name] = array_merge($this->settings[$model->name], ife(is_array($settings), $settings, array()));

    }
    
    /**
     * Run before a model is saved, used to set up tag for model.
     *
     * @param object $model    Model about to be saved.
     *
     * @access public
     * @since 1.0
     */
    function beforeSave(&$model) {
        
        // Define the new tag model
        $tag =& new Tag;
        
        if (isset($model->data[$model->name][$this->settings[$model->name]['table_label']]) && 
            $tag->hasField($this->settings[$model->name]['tag_label'])) {

            // Parse out all of the tags.
            $tag_list = $this->_parseTags($model->data[$model->name][$this->settings[$model->name]['table_label']], $this->settings[$model->name]);
            
            $tag_data = array(); // New tag array to store tag id and names from db
            
            foreach($tag_list as $t) {
                
                if ($res = $tag->find($this->settings[$model->name]['tag_label'] . " LIKE '" . $t . "'")) {
                    
                    $tag_data[] = $res['Tag']['id'];
                    
                } else {
                    
                    $tag->save(array('Tag' => array($this->settings[$model->name]['tag_label'] => $t)));
                    
                    $tag_data[] = sprintf($tag->getLastInsertID());
                }
                
                unset($res);
            }
    
            // This prepares the linking table data...
            $model->data['Tag']['Tag'] = $tag_data;
        }
        
        return true;
    }
    
    /**
     * Run after a model is deleted, used to clear unreferenced tags.
     *
     * @param object $model    Model about to be deleted.
     *
     * @access public
     * @since 1.0
     */
    function afterDelete(&$model) {

        // TODO:.... Garbage collector and reference counting.
    }
    
    /**
     * Run after a model is found, used to fill model tag string.
     *
     * @param object $model    Model about to be found.
     * @param mixed $results   Model rows found.
     *
     * @access public
     * @since 1.0
     */
    function afterFind(&$model, &$results) {
        
        if (!empty($results)) {
            
            foreach ($results as &$m) {
                
                $m[$model->name][$this->settings[$model->name]['table_label']] = $this->_tagString($m, $this->settings[$model->name]);
            }
        }
    }

    /**
     * Parse the tag string and return a properly formatted array
     *
     * @param string $string    String.
     * @param array $settings    Settings to use (looks for 'separator' and 'length')
     *
     * @return array    Tags for given string.
     *
     * @access private
     */
    function _parseTags($string, $settings) {
        
        $string = strtolower($string);
       
        $string = preg_replace('/[^a-z0-9' . $settings['separator'] . ' ]/i', '', $string);
        $string = preg_replace('/' . $settings['separator'] . '[' . $settings['separator'] . ']*/', $settings['separator'], $string);

        $string_array = preg_split('/' . $settings['separator'] . '/', $string);
        $return_array = array();
    
        foreach($string_array as $t) {
            
            if (strlen($t) > 0) {
                
                $return_array[] = $t;
            }
        }
    
        return $return_array;
    }
    
    /**
     * Parse the tag array and return a properly formatted string
     *
     * @param mixed &$model    Model.
     * @param array $settings    Settings to use (looks for 'separator' and 'length')
     *
     * @return string    Tag for given string.
     *
     * @access private
     */
    function _tagString($data, $settings) {
        
        $string = '';
        $tag_array = $data['Tag'];
    
        foreach($tag_array as $t) {
            
            $word = $t[$settings['tag_label']];
            
            if (strlen($word) > 0) {
                
                $string .= $word.$settings['separator'];
            }
        }

        return $string;
    }
}

?>

A questo punto basta impostare il modello così (esempio):

<?php

class Page extends AppModel
{
    var $name = 'Page';
    
    var $actsAs = 'Taggable';
    
    /**
     * Associazione "molti-a-molti" con le tags.
     */
    var $hasAndBelongsToMany = 'Tag';
}

?>

E il gioco è fatto, impostando un form del genere:

<?php echo $form->create('Page', array('url' => ...)); ?>

    <?php echo $form->error('title'); ?>
    <?php echo $form->label('title','Titolo (*):'); ?>
    <?php echo $form->text('title', array('class' => 'text')); ?>

    <?php echo $form->error('body'); ?>
    <?php echo $form->label('body','Corpo della pagina (*):'); ?>
    <?php echo $form->textarea('body'); ?>

     // Separate ad esempio da uno spazio, da ', ' o altro... (impostabile)
    <?php echo $form->label('tags','Parole chiave:'); ?>
    <?php echo $form->text('tags', array('class' => 'text')); ?>
  
    <?php echo $form->submit('Pulisci', array('div' => false, 'name' => 'previous')); ?>
    <?php echo $form->submit('Invia', array('div' => false, 'name' => 'next')); ?>
  
<?php echo $form->end(); ?>

Sicuramente si può migliorare: ad esempio è possibile spostare l'HABTM dentro al behavior? In questo modo sarebbe ancora più automatico: colleghi, imposti e via...Un altro perfezionamento riguarda i parametri impostabili: per ora si può scegliere il nome del campo nel modello 'Tag' che ne rappresenta il valore (il nome) e il nome del campo "ausiliario" nel form che viene passato contenente la stringa da "parsare".

Nella versione originale la stringa veniva memorizzata come attributo del modello che utilizzava il comportamento, ma questo creava ridondanze e un campo in più assolutamente brutto e inutile. Nella mia versione la stringa viene parsata e ricreata a richiesta e non c'e' bisogno di conservarla memorizzata nel DB in quanto bastano già le singole righe nella tabella "Tag" e relative associazioni.

Last edited by zuck (07-02-2008 11:53:39)

Namaless

Re: [1.2] Taggable Behavior

Modifico il titolo in quanto il problema è stato risolto e servirà a chi deve usare un sistema di "tags" nelle proprie Cake Applications smile

Saluti.

Posts [ 9 ]

Guest posting is disabled. You must login or register to post a reply.

Pages: 1

Topic info

1 guests and 0 users are reading this topic now


Forum quick jump menu

Currently used extensions: pun_topic_online_users, pun_karma, pun_admin_hook_navigator, pun_bbcode. Copyright © 2008 PunBB

[ Generated in 0.041 seconds, 11 queries executed ]