Maintenant que nous avons initialisé Doctrine (vois l’article sur l’introduction à Doctrine), nous allons pouvoir commencer à l’utiliser.
Comme nous l’avons dit, avec Doctrine tout est objet. Nous aurons donc accès à des classes qui vont nous permettre de travailler les données. On appelle ça des composants. Doctrine fournit plusieurs composants. En voici trois essentiels :
- Le composant Record qui représente un enregistrement d’une table
- Le composant Collection qui représente un ensemble de Record
- Le composant Table qui représente notre table et nous permettra d’accéder aux composants Collection et Record
Dans cet article nous allons étudier le composant Table. Il permet comme son nom l’indique d’accéder à une table de notre base de données et d’interagir avec elle. Pour cela il nous faut une instance de la classe représentant notre table :
1 2 3 | <?php require_once('bootstrap.php'); $table = Doctrine_Core::getTable('Produits'); |
Vous remarquerez le nom particulier de notre table. En effet, dans notre base de données notre table s’appelle produits et dans Doctrine on l’appellera Produits. Chaque classe commence donc par une majuscule. En fait, chaque mot qui compose le nom de la table commence par une majuscule. Ainsi si on a une table nouveaux_produits on l’appellera NouveauxProduits dans Doctrine.
Cet objet va nous permettre d’accéder à une fonctionnalité très intéressante des Table : les finders.
Les finders sont des méthodes qui nous permettent de faire des recherches dans notre table et qui retournent soit un Record, soit une Collection. Voici un exemple :
1 | $record = $table->find(4); |
La méthode find() permet d’effectuer une recherche sur la clé primaire de notre table. Dans notre cas la clé primaire de la table produits est produits_id. L’équivalent SQL de ce que nous venons de faire est :
1 | SELECT produits_id, produits_name, produits_prix FROM produits WHERE produits_id = 4 |
En fait find() fait plus que cela, il permet de sélectionner non seulement l’enregistrement mais aussi de rendre accessible les enregistrements des autres tables qui sont en relation avec cet enregistrement. Pour comprendre cela, nous allons étudier en détail le comportement de Doctrine. J’ai également récupéré les logs MySQL afin de voir comment Doctrine travaillait. Soit la base de données suivante :
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 | 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 `groupes` -- ----------------------------------------------------- CREATE TABLE IF NOT EXISTS `groupes` ( `groupes_id` INT NOT NULL AUTO_INCREMENT , `groupes_name` VARCHAR(45) NOT NULL , PRIMARY KEY (`groupes_id`) ) ENGINE = InnoDB DEFAULT CHARACTER SET = utf8 COLLATE = utf8_general_ci; -- ----------------------------------------------------- -- Table `user` -- ----------------------------------------------------- CREATE TABLE IF NOT EXISTS `user` ( `user_id` INT NOT NULL AUTO_INCREMENT , `groupes_id` INT NOT NULL , `user_pseudo` VARCHAR(100) NULL , PRIMARY KEY (`user_id`) , CONSTRAINT `user_groupes_id` FOREIGN KEY (`groupes_id` ) REFERENCES `groupes` (`groupes_id` ) ON DELETE CASCADE ON UPDATE NO ACTION) ENGINE = InnoDB DEFAULT CHARACTER SET = utf8 COLLATE = utf8_general_ci; CREATE INDEX `user_groupes_id` ON `user` (`groupes_id` ASC) ; -- ----------------------------------------------------- -- Table `phone` -- ----------------------------------------------------- CREATE TABLE IF NOT EXISTS `phone` ( `phone_id` INT NOT NULL AUTO_INCREMENT , `user_id` INT NOT NULL , `phone_number` VARCHAR(20) NOT NULL , PRIMARY KEY (`phone_id`) , CONSTRAINT `phone_user_id` FOREIGN KEY (`user_id` ) REFERENCES `user` (`user_id` ) ON DELETE CASCADE ON UPDATE NO ACTION) ENGINE = InnoDB DEFAULT CHARACTER SET = utf8 COLLATE = utf8_general_ci; CREATE INDEX `phone_user_id` ON `phone` (`user_id` ASC) ; SET SQL_MODE=@OLD_SQL_MODE; SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS; SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS; |
Voici une représentation schématique de cette base :
Comme on peut le voir, on a une table d’utilisateur. Chaque utilisateur ne peut appartenir qu’à un seul groupe (many-to-one), mais peut posséder plusieurs numéros de téléphone (one-to-many). Si je fais :
1 | $user = Doctrine_Core::getTable('User')->find(1); |
MySQL va faire la requête suivante :
1 | SELECT u.user_id AS u__user_id, u.groupes_id AS u__groupes_id, u.user_pseudo AS u__user_pseudo FROM user u WHERE (u.user_id = '1') LIMIT 1 |
Je vais ainsi avoir accès aux champs de l’enregistrement récupéré grâce aux propriétés suivantes:
1 2 3 | echo $user->user_id . '<br />'; echo $user->groupes_id . '<br />'; echo $user->user_pseudo . '<br />'; |
On va également avoir la possibilité d’accéder aux champs de la table groupes dont dépend notre enregistrement :
1 | echo $user->Groupes->groupes_name . '<br />'; |
$user->Groupes représente un objet Record de la table Groupes. C’est un Record car selon le type de relation définit , un utilisateur ne peut appartenir qu’à un seul et unique groupe.
Sauf que dans la requête effectuée par Doctrine, on ne va pas chercher les informations de la table groupes. En fait la requête sera effectuée uniquement au moment où je demande d’accéder à la propriété $user->Groupes->groupes_name. Ainsi, à ce moment et uniquement lors du premier appel, Doctrine va effectuer la requête suivante :
1 | SELECT g.groupes_id AS g__groupes_id, g.groupes_name AS g__groupes_name FROM groupes g WHERE (g.groupes_id = '1') |
Doctrine va ensuite mettre ce résultat en cache. C’est à dire que si on demande à nouveau à accéder à une propriété de l’objet $user->Groupes, Doctrine ne va pas refaire la requête.
Tout comme nous avons un objet $user->Groupes, nous avons un objet $user->Phone. Sauf qu’il y a une différence : ici un utilisateur peut posséder plusieurs numéros de téléphone. Ainsi l’objet $user->Phone ne sera pas une instance de Record, mais une instance de Collection. Nous verrons plus loin les collections en détail, mais rapidement voici comment nous accèderons aux éléments de notre collection:
1 2 | echo $user->Phone[0]->phone_number; echo $user->Phone[1]->phone_number; |
Au moment de l’appel à l’objet $user->Phone, Doctrine va effectuer la requête suivante :
1 | SELECT p.phone_id AS p__phone_id, p.user_id AS p__user_id, p.phone_number AS p__phone_number FROM phone p WHERE (p.user_id IN ('1')) |
et récupérer l’ensemble des résultats.
find() est donc bien pratique pour récupérer l’enregistrement d’une table, mais c’est moins glorieux lorsque l’on doit accéder aux relations de cet enregistrement. Même si Doctrine fait attention de ne pas effectuer de requêtes superflues, il fait en deux requêtes ce que l’on aurait pu faire en une seule avec une jointure.
Dans les finders, nous avons vu comment récupérer un enregistrement. Il est également possible de récupérer tous les enregistrements :
1 | $users = Doctrine_Record::getTable('User')->findAll(); |
findAll() retourne donc une Collection qui contient tous les enregistrements de la table user. On peut itérer dans cette collection afin d’extraire les informations :
1 2 3 | foreach ($users as $user) { echo $user->user_pseudo . '<br />'; } |
La requête SQL de Doctrine qui va aller chercher les résultats est la suivante :
1 | SELECT u.user_id AS u__user_id, u.groupes_id AS u__groupes_id, u.user_pseudo AS u__user_pseudo FROM user u |
On peut également afficher le nom du groupe de chaque utilisateur :
1 2 3 | foreach ($users as $user) { echo $user->Groupes->groupes_name . '<br />'; } |
Là c’est encore pire au niveau de l’optimisation. En effet, nous avons vu plus haut que Doctrine n’effectuait les requêtes des tables en relation que lorsque l’on y accède. Insérons les données suivantes dans nos tables :
1 2 3 4 5 6 7 8 | INSERT INTO `groupes` (`groupes_id`, `groupes_name`) VALUES (1, 'groupe 1'), (2, 'groupe 2'); INSERT INTO `user` (`user_id`, `groupes_id`, `user_pseudo`) VALUES (1, 1, 'User 1'), (2, 2, 'User 2'), (3, 1, 'User 3'); |
Si nous analysons les requêtes effectuées par Doctrine lors de la boucle qui accède à la table groupe voici ce que l’on voit :
1 2 3 | SELECT g.groupes_id AS g__groupes_id, g.groupes_name AS g__groupes_name FROM groupes g WHERE (g.groupes_id = '1') SELECT g.groupes_id AS g__groupes_id, g.groupes_name AS g__groupes_name FROM groupes g WHERE (g.groupes_id = '2') SELECT g.groupes_id AS g__groupes_id, g.groupes_name AS g__groupes_name FROM groupes g WHERE (g.groupes_id = '1') |
Là ce n’est plus du tout optimisé puisque l’on constate que Doctrine va effectuer une requête pour chacun des enregistrements de la Collection $users, même si certaines sont communes. Si ce n’est pas grave dans notre cas, imaginez sur une table de 10 000 entrées… N’importe qui vous dirait que dans ce cas il faut effectuer une requête sur la table user avec une jointure sur la table groupes pour récupérer les champs du groupe. Cela nous pourrons le faire en DQL. En attendant sachez donc que si findAll() est parfaitement adapté pour récupérer les enregistrements d’une table, il ne l’est plus du tout lorsqu’il s’agit d’accéder aux relations de cette table.
Les finders ont d’autre possibilités assez intéressantes. Par exemple, il est possible d’avoir des finders personnalisés selon notre table. On les appelle les Magic Finders. Imaginons que nous voulions récupérer toutes les entrées de user dont le groupes_id est 1 :
1 | $users = Doctrine_Core::getTable('User')->findByGroupes_id(1); |
Cette ligne exécute la requête suivante :
1 | SELECT u.user_id AS u__user_id, u.groupes_id AS u__groupes_id, u.user_pseudo AS u__user_pseudo FROM user u WHERE (u.groupes_id = '1') |
Et renverra une instance de Doctrine_Collection. Ce qui est vraiment top, c’est que ce finder, qui dépend complètement des champs présents dans la table n’a pas besoin d’être codé par le développeur, Doctrine le propose automatiquement, voilà pourquoi on les appelle les Magic Finders.
Il est également possible de ne retourner qu’un seul enregistrement au lieu d’une Collection :
1 | $users = Doctrine_Core::getTable('User')->findOneByGroupes_id(1); |
Encore une fois il s’agit d’un finder personnalisé qui va effectuer la requête suivante :
1 | SELECT u.user_id AS u__user_id, u.groupes_id AS u__groupes_id, u.user_pseudo AS u__user_pseudo FROM user u WHERE (u.groupes_id = '1') LIMIT 1 |
Les finders personnalisés peuvent aller encore plus loin. Regardez le finder suivant :
1 | $users = Doctrine_Core::getTable('User')->findOneByGroupes_idAndUser_pseudo(1, 'User 1'); |
La requête SQL produite est la suivante :
1 | SELECT u.user_id AS u__user_id, u.groupes_id AS u__groupes_id, u.user_pseudo AS u__user_pseudo FROM user u WHERE (u.groupes_id = '1' AND u.user_pseudo = 'User 1') LIMIT 1 |
On peut donc utiliser les mots-clés And et Or dans les Magic Finders afin de composer notre recherche
Voici donc un petit récap sur les finders :
- findAll() renvoie une Collection qui contient tous les enregistrements de la table
- find() renvoi un Record. La recherche se fera sur la clé primaire de la table
- findBy + le nom du champ commençant par une majuscule : Renvoie une Collection. La recherche se fait sur le champ indiqué après By
- findOneBy + le nom du champ commençant par une majuscule : Renvoie un Record. La recherche se fait sur le champ indiqué après By
- On peut utiliser les mots-clés And et Or dans les Magic Finders afin de composer une recherche sur plusieurs champs
Ces finders sont très pratiques mais retenez qu’il ne faut jamais les utiliser si vous avez besoin d’accéder aux relations des enregistrements à cause de leur manque d’optimisation. Pour cela nous nous tournerons vers le DQL.