Symfony 1.2, doctrine, héritage, et relations many-to-many

rosier times

Aujourd'hui, nous allons tester la bêta 1.2 de Symfony, et son plugin doctrine. Exit Propel : place aux jeunes ! Pour ce faire, nous allons construire un mini-sigb, système intégré de gestion de bibliothèque.

La gestion de bibliothèque nécessite de mettre en oeuvre quelques concepts intéressants. Par exemple, les relations n à n (many-to-many) : un abonné peut emprunter plusieurs livres, un livre peut être emprunté par plusieurs abonnés. On peut également utiliser l'héritage de tables : un dvd, un cd audio, un livre, un magazine sont des médias empruntables, avec des attributs communs (durée de prêt, date d'achat) et spécifiques (nombre de pistes, nombre de chapitres, mois de parution pour les mensuels, etc.)

L'environnement de développement

Je vous ferai grâce de la procédure d'installation, qui est déjà abondamment commentée. La suite ne change pas : virtual host, génération de projet, rien de nouveau.

Maintenant que notre environnement est en place, désactivons Propel pour activer Doctrine. Pour cela, éditons config/ProjectConfiguration.class.php :

class ProjectConfiguration extends sfProjectConfiguration
{
  public function setup()
  {
    $this->enablePlugins(array('sfDoctrinePlugin'));
    $this->disablePlugins(array('sfPropelPlugin'));
  }
}

Ensuite, configurons notre base de donnée dans config/databases.yml :

all:
  doctrine:
    class:        sfDoctrineDatabase
    param:
      dsn:        mysql:dbname=bibli;host=localhost
      username:   root
      password:
      encoding:   utf8
      persistent: true
      pooling:    true

Le modèle

Maintenant, passons aux choses sérieuses avec la création du modèle. Ne faites pas comme moi, ne perdez pas un quart d'heure avant de vous rendre compte que le bon fichier est bien config/*doctrine*/schema.yml :

User:
  columns:
    name: varchar(150)
    email: varchar(150)
    phone: varchar(20)
  relations:
    Media:
      refClass: Loan
      local: user_id
      foreign: media_id


Media:
  columns:
    title: varchar(150)
    author: varchar(150)
    summary: varchar(1000)
  relations:
    User:
      refClass: Loan
      local: media_id
      foreign: user_id

Book:
  inheritance:
    extends: Media
    type: column_aggregation
    keyField: type
    keyValue: 1
  columns:
    number_of_pages: integer

DVD:
  tableName: dvd
  inheritance:
    extends: Media
    type: column_aggregation
    keyField: type
    keyValue: 2
  columns:
    duration: integer

Loan:
  actAs:
    Timestampable
  columns:
    media_id:
      type: integer
      primary: true
    user_id:
      type: integer
      primary: true

    start_date: date
    end_date: date

Remarquez de quelle façon doctrine gère les relations n-n, par l'ajout d'options "relations" dans les tables User et Media. Note : l'attribut "relations" est optionnel dans la table Media : il aurait été rajouté automatiquement.

Jetons un coup d'oeil au code généré (qui soit dit en passant est agréablement plus propre que celui de propel) :

// lib/model/doctrine/base/BaseUser.class.php
abstract class BaseUser extends sfDoctrineRecord
{
  public function setTableDefinition()
  {
      ...
  }

  public function setUp()
  {
    $this->hasMany('Media', array('refClass' => 'Loan',
                                  'local' => 'user_id',
                                  'foreign' => 'media_id'));
  }
}

// lib/model/doctrine/base/BaseMedia.class.php
abstract class BaseMedia extends sfDoctrineRecord
{
  public function setTableDefinition()
  {
      ...
  }

  public function setUp()
  {
    $this->hasMany('User', array('refClass' => 'Loan',
                                 'local' => 'media_id',
                                 'foreign' => 'user_id'));
  }
}

// lib/model/doctrine/base/BaseLoan.class.php
abstract class BaseLoan extends sfDoctrineRecord
{
  public function setTableDefinition()
  {
    $this->hasColumn('media_id', 'integer', null, array('type' => 'integer', 'primary' => true));
    $this->hasColumn('user_id', 'integer', null, array('type' => 'integer', 'primary' => true));
    ...
  }
}

Intéressons nous maintenant à l'héritage avec doctrine. L'ORM peut gérer de trois façons distinctes l'héritage :

  • Héritage simple : héritage basique, toutes les tables filles partagent exactement les mêmes colonnes que la table mère. En pratique, on a une seule table dans la BD, mais une classe par table.
  • Héritage concret : ce type d'héritage permet d'obtenir autant de tables que de classes. Chaque table fille contient tous les attributs, y compris les attributs hérités (c'est à dire ceux de la classe mère).
  • Héritage par aggrégation : Cette méthode permet d'obtenir une seule table, qui contient tous les champs possibles de toutes les classes filles.

Dans notre cas, on obtient donc trois tables, dont la table media, qui contient tous les champs déclarés par ses enfants :

mysql> show tables;
+-----------------+
| Tables_in_bibli |
+-----------------+
| loan            |
| media           |
| user            |
+-----------------+
mysql> desc media;
+-----------------+--------------+------+-----+---------+----------------+
| Field           | Type         | Null | Key | Default | Extra          |
+-----------------+--------------+------+-----+---------+----------------+
| id              | bigint(20)   | NO   | PRI | NULL    | auto_increment |
| title           | varchar(150) | YES  |     | NULL    |                |
| author          | varchar(150) | YES  |     | NULL    |                |
| summary         | text         | YES  |     | NULL    |                |
| type            | varchar(255) | YES  |     | NULL    |                |
| number_of_pages | bigint(20)   | YES  |     | NULL    |                |
| duration        | bigint(20)   | YES  |     | NULL    |                |
+-----------------+--------------+------+-----+---------+----------------+

Je vous laisse consulter le manuel de Doctrine sur l'héritage, et celui sur les relations many-to-many si vous voulez en savoir plus.

Les données de test

Histoire de pouvoir travailler, nous allons remplir le fichier data/fixtures/fixtures.yml de quelques données de test :

User:
  thibault:
    name: Thibault Jouannic
    email: teeboo@gmail.com
    phone: '+33467453834'

  garcin:
    name: Garcin Fony
    email: garcin@symfony.fr
    phone: '+33666666666'


Book:
  symfobook:
    title: Symfony book
    author: Fabien Potencier
    summary: Un livre sur symfony
    number_of_pages: 250

  phpbook:
    title: php avancé
    author: rasmus
    summary: un livre sur php
    number_of_pages: 300

DVD:
  aikido:
    title: DVD Symfony
    author: Fabien Potencier
    summary: Tutoriaux en directs
    duration: 90

Loan:
  l1:
    User: thibault
    Media: phpbook
    start_date: ''
    end_date: '2008-11-25'

  l2:
    User: garcin
    Media: aikido
    start_date: ''
    end_date: '2008-12-30'

  l3:
    User: garcin
    Media: symfobook
    start_date: ''
    end_date: '2008-11-05'

Il ne nous reste plus qu'à laisser symfony tout générer :

symfony doctrine:build-all-reload --no-confirmation

Et voilà ! Notre environnement est en place. Tout ceci nous promet de belles réjouissances, mais ce sera pour la prochaine fois. En attendant, couvrez vous bien, et à++;