Cette année je suis passé à Doctrine. Les raisons étaient diverses:
- Il était temps pour moi de laisser tomber la vieille extension mysql de PHP et de passer à de l’objet.
- Trouver un système qui s’intègre facilement dans un modèle MVC
- Pouvoir faire de l’abstraction de base de données
- Idéalement un système qui va me faire gagner du temps dans mes développements.
Après avoir regardé plusieurs solutions, je me suis donc tourné vers Doctrine:
- Il s’interface avec PDO, donc abstraction de base de données
- Les performances semblent au rendez-vous
- Il génère automatiquement le modèle du MVC
- Support des fonctionnalités récentes de MySQL
- Utilisé par défaut dans Symfony
- Tout est objet
Bref je me suis dis que c’était le top. Et c’est vrai que quand on le maîtrise c’est vraiment sympa. Sauf que la transition s’est quand même un peu faite dans la douleur. La doc est présente mais dans certains cas j’ai quand même perdu pas mal de temps à chercher des solutions à mes problèmes. Je me suis donc dit qu’il était peut-être possible d’apporter quelques compléments d’information.
Comprendre la philosophie Doctrine
Doctrine est un ORM (Object Relational Mapper), c’est à dire qu’il va s’interfacer avec notre base de données afin de nous permettre d’y accéder comme s’il s’agissait d’un objet. Ainsi il sera possible d’accéder aux données d’une table comme s’il s’agissait de propriétés d’une classe, mais en plus cette classe va nous proposer des méthodes permettant de mettre en œuvre des fonctionnalité comme l’enregistrement, la suppression, la mise à jour etc.
Prenons un exemple simple : soit la table mysql suivante:
1 2 3 4 5 | CREATE TABLE produits ( produits_id int(11) NOT NULL auto_increment, produits_name varchar(255) NOT NULL PRIMARY KEY (produits_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; |
Imaginons que nous voulions insérer un nouvel enregistrement, nous pourrons le réaliser comme ceci :
1 2 3 | $produit = new Produits(); $produit->produits_name = 'Casserole'; $produit->save(); |
C’est tout ! Et en bon ORM qu’il est, Doctrine propose une interface objet pour de nombreux éléments comme la connexion à la base, les tables, les enregistrements des tables, etc. ce que nous verrons plus tard.
Doctrine peut également réaliser des requêtes similaires à ce que l’on connaît sous MySQL mais avec son propre langage: le DQL (Doctrine Query Langage). Sincèrement, on s’y fait très vite, voici un exemple de sélection du produit ayant l’ID 4 :
1 2 3 4 | $produit = Doctrine_Query::create() ->from('Produits a') ->where('a.produits_id = ?', 4) ->fetchOne(); |
La documentation officielle sur le DQL est plutôt bien faite et le « langage » est très explicite. Il n’y aura donc aucune difficulté particulière. Cependant nous en parlerons quand même dans un prochain article.
La génération du modèle et le fichier YAML
Pour arriver à tout cela, Doctrine va devoir utiliser des classes qui représenteront nos tables. Ces classes, on ne va pas les créer car Doctrine s’en charge pour nous. Il va donc se charger de générer des fichiers PHP qui seront notre modèle de données. Il y a plusieurs façons de réaliser cela :
- On peut créer notre modèle sur Doctrine qui va ensuite se charger de créer les tables sur MySQL
- On peut créer nos tables MySQL et Doctrine en déduira son modèle.
En ce qui me concerne, je modélise ma base de données avec MySQL Workbench, donc je pars toujours d’une base de données existante pour générer mon modèle Doctrine (je vais considérer que vous ferez pareil). Quelle que soit la solution choisie, il y a un passage qui semble indispensable : le fichier YAML. Il s’agit d’un fichier de configuration qui va contenir le schéma de notre base de données et sur lequel Doctrine va se baser.
La génération du modèle va donc se dérouler ainsi:
Base de données MySQL existante -> Génération du fichier YAML -> Génération du modèle
Il existe une solution qui permet de générer le modèle directement à partir de la base de données, mais je ne l’ai pas trouvée intéressante car elle manque de fonctionnalités. En effet, nous verrons plus tard que Doctrine propose des fonctionnalités très intéressantes mais qui nécessitent d’être paramétrées dans le fichier YAML et que la conversion directe MySQL -> Modèle va ignorer.
Si vous ne connaissez pas YAML, il s’agit d’un langage de sérialisation de données comparable à JSON. Vous pourrez trouver plus de renseignements sur Wikipédia et sur le site officiel. Bien que je le trouve un peu plus complexe que JSON, YAML reste facilement compréhensible. Il ne sera donc pas compliqué de retravailler le modèle pour Doctrine.
Téléchargement et installation de Doctrine
Les informations qui suivent sont valables pour Doctrine 1.2.1. Il est possible de télécharger le sandbox à cette adresse: http://www.doctrine-project.org/download
On va créer un répertoire doctrine_test. Dans ce répertoire, nous allons créer le répertoire lib/vendor/doctrine dans lequel nous allons copier la librairie.
Nous devrions donc avoir dans le répertoire doctrine_test/lib/vendor/dotrine les éléments suivants:
- Un répertoire Doctrine
- Un répertoire vendor
- Un fichier Doctrine.php
Le bootstrap
Ce que l’on va appeler bootstrap, c’est un fichier PHP qui va contenir le code de connexion à la base de données et que l’on va inclure dans chacun de nos fichiers afin d’activer l’autoload, d’établir la connexion à la base et de définir plusieurs paramètres. Dans le répertoire doctrine_test, nous allons créer un fichier bootstrap.php dont voici le contenu :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | <?php // bootstrap.php /** * Bootstrap Doctrine.php, register autoloader specify * configuration attributes and load models. */ require_once('lib/vendor/doctrine/Doctrine.php'); spl_autoload_register(array('Doctrine', 'autoload')); spl_autoload_register(array('Doctrine', 'modelsAutoload')); $manager = Doctrine_Manager::getInstance(); $conn = Doctrine_Manager::connection('mysql://user:password@host/dbname','doctrine'); $manager->setAttribute(Doctrine_Core::ATTR_VALIDATE, Doctrine_Core::VALIDATE_ALL); $manager->setAttribute(Doctrine_Core::ATTR_AUTO_ACCESSOR_OVERRIDE, true); $manager->setAttribute(Doctrine_Core::ATTR_AUTOLOAD_TABLE_CLASSES, true); $manager->setAttribute(Doctrine_Core::ATTR_MODEL_LOADING, Doctrine_Core::MODEL_LOADING_CONSERVATIVE); Doctrine_Core::loadModels('models'); |
Une note très importante si vous avez déjà utilisé Doctrine : vous remarquerez un deuxième autoload qui n’est pas indiqué dans la documentation. En effet cet autoload est nécessaire et n’est pas documenté et j’ai galéré un bon moment avant de trouver la solution sur leur forum.
L’explication de ce fichier est relativement simple :
1 | require_once('lib/vendor/doctrine/Doctrine.php'); |
On inclue la librairie Doctrine
1 2 | spl_autoload_register(array('Doctrine', 'autoload')); spl_autoload_register(array('Doctrine', 'modelsAutoload')); |
On déclare les autoloads
1 | $manager = Doctrine_Manager::getInstance(); |
On récupère une instance Doctrine_Manager (singleton) qui va nous permettre de définir certaines options
1 | $conn = Doctrine_Manager::connection('mysql://user:password@host/dbname','doctrine'); |
On se connecte à la base de données avec une syntaxe de type DSN compatible avec PDO
1 2 3 4 | $manager->setAttribute(Doctrine_Core::ATTR_VALIDATE, Doctrine_Core::VALIDATE_ALL); $manager->setAttribute(Doctrine_Core::ATTR_AUTO_ACCESSOR_OVERRIDE, true); $manager->setAttribute(Doctrine_Core::ATTR_AUTOLOAD_TABLE_CLASSES, true); $manager->setAttribute(Doctrine_Core::ATTR_MODEL_LOADING, Doctrine_Core::MODEL_LOADING_CONSERVATIVE); |
Ici on définit les options de Doctrine au niveau global (on peut définir des options sur 3 niveaux: au niveau global, au niveau d’une connexion en particulier, au niveau d’une table en particulier). Voici la signification de ces options:
1 | $manager->setAttribute(Doctrine_Core::ATTR_VALIDATE, Doctrine_Core::VALIDATE_ALL); |
Doctrine possède un validateur de données intégré. Si jamais vous essayez par exemple d’insérer une chaîne de caractère dans une champ de type entier, Doctrine va générer une erreur avant même de tenter de l’insérer en base. C’est assez utile et ici on déclare vouloir l’utiliser.
1 | $manager->setAttribute(Doctrine_Core::ATTR_AUTO_ACCESSOR_OVERRIDE, true) |
l’AUTO_ACCESSOR_OVERRIDE va nous permettre de personnaliser l’assignation de données. J’essaierai de faire un article complet sur la personnalisation des classes car j’ai trouvé la documentation assez légère sur les possibilités offertes du coup je pense moi-même ne pas exploiter cette fonctionnalité à fond.
1 | $manager->setAttribute(Doctrine_Core::ATTR_AUTOLOAD_TABLE_CLASSES, true); |
Doctrine permet de personnaliser également les classes de table en permettant de créer des méthodes propres à une table. Ce paramètre permet de charger le fichier contenant nos méthodes personnalisées.
1 | $manager->setAttribute(Doctrine_Core::ATTR_MODEL_LOADING, Doctrine_Core::MODEL_LOADING_CONSERVATIVE); |
Ce paramètre permet d’utiliser l’autoload pour le chargement des classes du modèle.
1 | Doctrine_Core::loadModels('models'); |
Enfin on déclare quel répertoire contient nos fichiers de modèle. Vous en déduirez donc qu’il va falloir créer un dossier models 🙂
Base de données de test et génération du modèle
Nous allons commencer avec une base de données simple dont voici le schema :
Voici le code SQL de création des tables (à copier / coller dans phpMyAdmin) :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 | SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0; SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0; SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='TRADITIONAL'; -- ----------------------------------------------------- -- Table `produits` -- ----------------------------------------------------- CREATE TABLE IF NOT EXISTS `produits` ( `produits_id` INT NOT NULL AUTO_INCREMENT , `produits_name` VARCHAR(100) NOT NULL , `produits_prix` FLOAT NOT NULL DEFAULT 0 , PRIMARY KEY (`produits_id`) ) ENGINE = InnoDB DEFAULT CHARACTER SET = utf8; -- ----------------------------------------------------- -- Table `tailles` -- ----------------------------------------------------- CREATE TABLE IF NOT EXISTS `tailles` ( `tailles_id` INT NOT NULL AUTO_INCREMENT , `tailles_name` VARCHAR(45) NULL , PRIMARY KEY (`tailles_id`) ) ENGINE = InnoDB DEFAULT CHARACTER SET = utf8; -- ----------------------------------------------------- -- Table `couleurs` -- ----------------------------------------------------- CREATE TABLE IF NOT EXISTS `couleurs` ( `couleurs_id` INT NOT NULL AUTO_INCREMENT , `couleurs_name` VARCHAR(45) NOT NULL , PRIMARY KEY (`couleurs_id`) ) ENGINE = InnoDB DEFAULT CHARACTER SET = utf8; -- ----------------------------------------------------- -- Table `produits_having_tailles` -- ----------------------------------------------------- CREATE TABLE IF NOT EXISTS `produits_having_tailles` ( `produits_id` INT NOT NULL , `tailles_id` INT NOT NULL , PRIMARY KEY (`produits_id`, `tailles_id`) , INDEX `pht_produits_id` (`produits_id` ASC) , INDEX `pht_tailles_id` (`tailles_id` ASC) , CONSTRAINT `pht_produits_id` FOREIGN KEY (`produits_id` ) REFERENCES `produits` (`produits_id` ) ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT `pht_tailles_id` FOREIGN KEY (`tailles_id` ) REFERENCES `tailles` (`tailles_id` ) ON DELETE CASCADE ON UPDATE NO ACTION) ENGINE = InnoDB DEFAULT CHARACTER SET = utf8; -- ----------------------------------------------------- -- Table `produits_having_couleurs` -- ----------------------------------------------------- CREATE TABLE IF NOT EXISTS `produits_having_couleurs` ( `produits_id` INT NOT NULL , `couleurs_id` INT NOT NULL , PRIMARY KEY (`produits_id`, `couleurs_id`) , INDEX `phc_produits_id` (`produits_id` ASC) , INDEX `phc_couleurs_id` (`couleurs_id` ASC) , CONSTRAINT `phc_produits_id` FOREIGN KEY (`produits_id` ) REFERENCES `produits` (`produits_id` ) ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT `phc_couleurs_id` FOREIGN KEY (`couleurs_id` ) REFERENCES `couleurs` (`couleurs_id` ) ON DELETE CASCADE ON UPDATE NO ACTION) ENGINE = InnoDB DEFAULT CHARACTER SET = utf8; SET SQL_MODE=@OLD_SQL_MODE; SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS; SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS; |
Votre base de données étant créée, on va commencer par générer notre fichier YAML. Pensez à bien paramétrer le DSN dans le fichier bootstrap.php pour vous connecter à la base. Dans le répertoire doctrine_test, créez un répertoire yaml et un répertoire models accessibles en écriture pour Apache. On va ensuite créer un fichier generate_yaml.php contenant le code suivant :
1 2 3 | <?php require_once('bootstrap.php'); Doctrine::generateYamlFromDb('yaml', array('doctrine')); |
Si vous exécutez ce fichier, vous trouverez dans le répertoire yaml un fichier schema.yml. Vous pouvez éditer ce fichier pour voir à quoi ressemble notre modèle. Nous allons ensuite générer les fichiers de classe à proprement parler. Créez le fichier generate_model.php et insérez le code :
1 2 3 | <?php require_once('bootstrap.php'); Doctrine::generateModelsFromYaml('yaml', 'models', array('generateTableClasses' => true)); |
Si vous l’exécutez , c’est maintenant dans le répertoire models que seront générés les fichiers. On y trouvera un répertoire generated dans lequel on trouvera les classes définissant nos tables, ainsi qu’une série de fichiers qui nous permettra par la suite de personnaliser nos classes.
Nous sommes maintenant fin prêts à utiliser Doctrine. Dans un prochain article nous verrons comment commencer à exploiter notre base de données.