Relations Zend_Db_Table

Logo Zend Framework

Zend_Db_Table permet de définir des relations entre les tables. Si par exemple on a une table factures et une table clients :

CREATE TABLE `clients` (
  `id` smallint(5) unsigned NOT NULL AUTO_INCREMENT,
  `nom` char(32) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8;
 
CREATE TABLE `factures` (
  `id` smallint(5) unsigned NOT NULL AUTO_INCREMENT,
  `numero` char(20) NOT NULL,
  `client_id` smallint(5) unsigned NOT NULL,
  PRIMARY KEY (`id`),
  KEY `client_id` (`client_id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8;
 
ALTER TABLE `factures`
  ADD CONSTRAINT `client_ibfk` FOREIGN KEY (`client_id`) 
  REFERENCES `clients` (`id`) ON DELETE CASCADE;

La table factures est liée à la table client par la clé étrangère client_id. C'est une relation un-vers-N (un client peut être associé à plusieurs factures).

Avec Zend_Db_Table, on peut définir cette relation dans le modèle à l'aide du tableau _referenceMap :

<?php
class Model_Factures extends Zend_Db_Table_Abstract
{    
    protected 
$_referenceMap = array(
        
'client' => array(
            
'columns' => 'client_id',
            
'refTableClass' => 'Model_Clients'  
        
)
    );
}
?>

columns correspond à la clé étrangère (donc à une ou plusieurs colonnes du modèle courant). refTableClass correspond à la classe dérivée de Zend_Db_Table_Abstract qui représente la table clients.

Une fois cette structure mise en place, on peut retrouver le client lié à une facture à l'aide de la méthode findParentRow de Zend_Db_Table_Row_Abstract :

<?php
$client 
$facture->findParentRow('Model_Clients');
echo 
$client->nom;
?>

On peut utiliser également une méthode "magique" :

<?php
$client 
$facture->findParentModel_Clients();
echo 
$client->nom;
?>

Tout ça n'est pas très joli. Ne serait ce pas mieux si on pouvait faire tout simplement :

<?php
echo $facture->client->nom;
?>

Le pire, c'est que ce n'est pas vraiment pas très compliqué à mettre en place. Il suffit de créer une classe dérivée de Zend_Db_Table_Row_Abstract et de surcharger sa méthode magique __get :

<?php
class Wiip_Db_Table_Row_Abstract extends Zend_Db_Table_Row_Abstract 
{
    public function 
__get($columnName)
    {
        if (!isset(
$this->_data[$columnName])) {
            
$referenceMap $this->_table->info('referenceMap');
            if (isset(
$referenceMap[$columnName])) {
               
$this->_data[$columnName] = $this->findParentRow(
                   
$referenceMap[$columnName]['refTableClass']
               );
               return 
$this->_data[$columnName];
            }
        }
        return 
parent::__get($columnName);
    }
}
?>

Mise à jour du 18/07 : dans ma première version, j'avais choisi de stocker l'objet parent dans une propriété publique pour éviter les appels à la méthode __set, mais cela ne fonctionne pas car l'affectation entraine l'appel de la méthode magique __set. Cette dernière échoue car le nom de la règle ne correspond pas à une colonne de la table.

Mise à jour du 22/07 : finalement, ça ne marche toujours pas.

Cette méthode est similaire à celle que je vous avais présenté dans un précédent article pour transformer les colonnes de type date en objet Zend_Date.

On récupère la table des références, puis on regarde si il y a une règle qui correspond au nom de la colonne demandée. Si c'est le cas, on récupère l'enregistrement avec findParentRow et on le stocke dans le tableau _data. De cette façon, si la colonne est demandée une deuxième fois, la méthode __get ne sera plus appelée.

Attention cependant, il ne faut pas utiliser cette technique dans une boucle, car il y aurait une ou plusieurs requêtes SQL exécutées à chaque itération. Dans ce cas, il vaut mieux utiliser une jointure SQL pour récupérer les colonnes des enregistrements parents (et on peut également utiliser les relations Zend_Db_Table pour faciliter l'opération). Je vous expliquerai ça dans un prochain article.

Si la relation est optionnelle, on va alors autoriser les valeurs NULL dans la colonne client_id. Dans ce cas, il faut tester cette colonne avant d'essayer d'accéder à l'objet. Par exemple :
<?php
if ($facture->client_id) echo $facture->client->nom;
?>

Ajouter un commentaire