Adoptant ce que propose le framework Rails pour le langage de programmation Ruby, le framework WdCore offre une API permettant de manipuler les enregistrements en base de données comme de simples objets. On peut ainsi créer des tables, ajouter des enregistrements, les modifier, les supprimer sans jamais avoir à saisir la moindre commande SQL. Cet article a pour but de présenter les méthodes permettant de récupérer un objet, ou une collection d'objets, depuis une base de données, mais aussi de spécifier l'ordre dans lequel les objets sont récupérés, comment les grouper, ou encore comment ne récupérer que certains champs.
Les requêtes se font sur un objet modèle qui est une représentation d'une ou plusieurs tables en base de données. Au long de cette démonstration, la variable $model fera référence au modèle de données primaire du module « Nœuds », que l'on peut obtenir avec la ligne suivante :
<?php
$model = $core->models['system.nodes'];
Avant de continuer la lecture de cet article, vous devez être familiarisé avec les concepts objet de PHP ainsi que PDO, son interface d'accès aux bases de données. Vous pouvez retrouver le code source de la classe
WdActiveRecordQuerysur Github.
Récupérer des objets depuis la base de données
L'API ActiveRecord propose plusieurs méthodes permettant de récupérer des objets depuis la base de données. Chacune de ces méthodes permet de définir les fragments de la requête SQL qui sera soumise à la base de données, sans que l'on aie jamais à saisir d'expression SQL brute. Les méthodes reprennent le vocabulaire de SQL, en voici quelques une :
- where
- select
- group
- order
- limit
- offset
- joins
Chacune de ces méthodes retourne une instance de la classe WdActiveRecordQuery, permettant d'enchainer les méthodes les unes à la suite des autres.
Nous verrons qu'il existe de nombreuses façons de récupérer des données, notamment au travers des propriétés all et one, mais pour le moment intéressons-nous à la plus simples d'entre elles : la méthodes find qui permet de récupérer un objet ou un jeu d'objets.
Récupérer un objet unique
L'API ActiveRecord permet de récupérer un objet unique très simplement. En utilisant « $model->find(primary_key) » ou « $model[primary_key] », on peut récupérer l'objet correspondant à une clé primaire :
<?php
$article = $model->find(10);
On peut également accéder aux modèles de données comme s'il s'agissait de tableaux associatifs, c'est d'ailleurs cette forme que l'on privilégiera lorsque l'on récupère un enregistrement unique :
<?php
$article = $model[10];
Récupérer un ensemble d'objets
En utilisant la même méthode, il est également possible de récupérer un ensemble d'objets :
<?php
$articles = $model->find(array(10, 32, 89));
# ou
$articles = $model->find(10, 32, 89);
Récupérer des enregistrements en utilisant la méthode find met en jeu deux fonctionnalités supplémentaires : les exceptions et la mise en cache des enregistrements.
Exceptions et mise en cache
Que l'on récupère un objet unique ou un jeu d'objets, une exception de type WdMissingRecordException est lancée lorsque qu'un objet s'avère manquant dans la base de données.
Une fois récupérés sans encombre, les objets sont mis en cache, c'est à dire que si l'on demande une seconde fois un objet, il sera servi depuis le cache, évitant ainsi une requête à la base de données.
Conditions
Afin de restreindre le nombre d'enregistrements retournés, il est possible de préciser des conditions de recherche. Les conditions peuvent être précisées sous forme de chaines de caractères ou de tableaux associatifs.
Chaine de caractères de conditions
Si l'on souhaite ajouter une condition à notre recherche, on peut la préciser aussi simplement que « $model->where('is_online = 1'); ». Cela retournera tous les enregistrements dont le champ is_online a pour valeur « 1 ».
Attention, construire votre propre condition sous la forme d'une chaine de caractères vous rend particulièrement vulnérable aux attaques d'injection SQL, parce qu'on ne sait jamais ce que les petits malins sont capables de faire pour exploiter nos données. La section suivante vous renseignera sur la meilleure façon de passer des arguments à vos conditions.
Tableau associatif de conditions
Si la valeur « 1 » est variable, parce qu'elle provient d'un paramètre qui peut être extérieur à l'application, on utilisera la forme de condition suivante :
<?php
$model->where('is_online = ?', $_GET['online']);
ou encore :
<?php
$model->where(array('is_online' => $_GET['online']));
Avec deux conditions à préciser, cela donne :
<?php
$model->where('is_online = ? AND is_home_excluded = ?', $_GET['online'], false);
ou bien :
<?php
$model->where(array('is_online' => $_GET['online'], 'is_home_excluded' => false));
Conditions de sous-ensembles
Si l'on souhaite rechercher les enregistrements qui appartiennent à un sous-ensemble, on peut fournir un tableau associatif en tant que valeur de la condition :
<?php
$model->where(array('orders_count' => array(1,3,5)));
Ce code générera quelque chose comme : ... WHERE (orders_count IN (1,3,5)).
Modificateurs
Lorsque les conditions sont précisées sous la forme d'un tableau associatif, il est possible de modifier la fonction de comparaison en préfixant le champ par un signe d'exclamation « ! ». Ainsi, au lieu de rechercher la commande ayant « 2 » pour identifiant, on pourra rechercher les commandes n'ayant pas « 2 » pour identifiant :
<?php
$model->where(array('!order_count' => 2));
De la même manière on pourra rechercher les enregistrements qui n'appartiennent pas à un sous-ensemble :
<?php
$model->where(array('!order_count' => array(1,3,5)));
Filtres dynamiques
Pour chaque colonne définie par le modèle, l'API ActiveRecord fournie automatiquement une méthode de recherche.
Parce le modèle Nœuds définit le champ slug, on peut utiliser la méthode de recherche
find_by_slug :
<?php
$model->find_by_slug('creer-nuage-mots-cle');
Si l'on a besoin de connaitre les articles en ligne de l'utilisateur ayant pour identifiant 3 on
peut tout simplement enchainer les filtres avec le séparateur _and_ :
<?php
$model->find_by_is_online_and_uid(true, 3);
Ce qui, si l'on utilise la méthode de recherche where, équivaut à :
<?php
$model->where(array('is_online' => true, 'uid' => 3));
Portées
Les portées peuvent être considérées comme des macros de recherche, des options toutes prêtes, rapides à utiliser. Chaque modèle peut définir ses propres portées ou surcharger celles du modèle dont il hérite. Par exemple, voici la définition de la portée visible par le modèle Nœuds :
<?php
...
protected function scope_visible(WdActiveRecordQuery $query)
{
global $core;
return $query->where('is_online = 1 AND (siteid = 0 OR siteid = ?) AND (language = "" OR language = ?)', $core->site->siteid, $core->site->language);
}
...
On peut alors très simplement obtenir la liste des enregistrements disponibles pour le site :
<?php
$model->visible;
On peut bien sûr combiner les portées ainsi que les autres méthodes de recherche :
<?php
$model->find_by_uid(1)->visible->where('YEAR(created) = 2011');
Ordonner les enregistrements
Pour récupérer les enregistrements dans un certain ordre, on peut utiliser la méthode « order ».
Par exemple, si l'on souhaite récupérer les enregistrements dans l'ordre chronologique de leur date de création (enregistrée dans le champ « created ») :
<?php
$model->order('created');
# ou
$model->order('created ASC');
Pour un ordre antichronologique :
<?php
$model->order('created DESC');
On peut également ordonner les enregistrements selon plusieurs critères :
<?php
$model->order('created DESC, title');
Grouper les données
La clause SQL GROUP BY peut être spécifiée en utilisant la méthode group(). Par exemple, si
l'on souhaite obtenir le premier enregistrement crée par jour, on utilisera le code suivant :
<?php
$model->group('date(created)')->order('created');
Appliquer un filtre au groupe
La clause HAVING est utilisée pour spécifier les conditions de la clause GROUP BY. Par exemple,
si l'on souhaite obtenir le premier enregistrement crée par jour pour le mois passé, on utilisera
le code suivant :
<?php
$model->group('date(created)')->having('created > ?', date('Y-m-d', strtotime('-1 month')))->order('created')
Limite et décalage
La méthode « limit » peut être utilisée pour limiter le nombre d'enregistrements retournés. Elle accepte un ou deux arguments numériques. Ces arguments doivent être des entiers constants.
Avec un argument, la valeur spécifie le nombre de lignes à retourner depuis le début du jeu de résultat. Si deux arguments sont donnés, le premier indique le décalage du premier enregistrement à retourner, le second donne le nombre maximum d'enregistrements à retourner. Le décalage du premier enregistrement est 0 (pas 1) :
<?php
$model->where('is_online = 1')->limit(10); // retourne les 10 premiers enregistrements
$model->where('is_online = 1')->limit(5, 10); // retourne les enregistrements 6 à 16
La méthode « offset » peut être utilisée pour définir seulement le décalage :
<?php
$model->where('is_online = 1')->offset(5); // retourne les enregistrements de 6 jusqu'au dernier
$model->where('is_online = 1')->limit(10)->offset(5);
Sélectionner des champs spécifiques
Par défaut, tous les champs sont sélectionnés (SELECT *) et les enregistrements sont retournés
sous la forme d'objets dont la classe dépend du modèle de données. Il est cependant possible de ne
sélectionner qu'un sous ensemble de champs grâce à la méthode select. Dans ce cas, chaque ligne
de résultat est renvoyée sous la forme d'un tableau associatif. Par exemple si l'on souhaite
obtenir l'identifiant d'un nœud, sa date de création et son titre :
<?php
$model->select('nid, created, title');
Parce que les champs spécifiés sont utilisés tels-quels pour construire la requête SQL, il est tout à fait possible d'utiliser les fonctions SQL :
<?php
$model->select('nid, created, CONCAT_WS(":", title, language)');
Joindre des tables
L'API Active Record fournie une méthode de recherche qui permet de spécifier la clause JOIN de la
requête SQL. Grâce à la méthode joins, il est possible de spécifier un fragment brut ou d'utiliser les
relations qu'il existe entre les modèles.
Utiliser un fragment brut
On peut spécifier un fragment brut, il sera inclus tel quel dans la requête finale :
<?php
$model->joins('INNER JOIN contents USING(nid)');
Utiliser une référence à un modèle
On peut profiter des relations définies entre les modèles et laisser faire le framework :
<?php
$model->joins(':contents');
La requête produira le même effet que la précédente, sans que nous ayons à nous soucier des conditions de la jointure. On notera les deux points « : » utilisés pour identifier l'utilisation d'un nom de modèle plutôt qu'un fragment brut.
Récupérer les données
Il existe de nombreuses façons de récupérer les lignes du jeu d'enregistrement. Nous avons déjà vu la méthode find, voici les méthodes et les propriétés que l'on peut utiliser conjointement aux méthodes de recherche :
Par itération
Parce que l'objet WdActiveRecordQuery est traversable, l'itération est la façon la plus simple
de récupérer les lignes du jeu d'enregistrements :
<?php
foreach ($model->where('is_online = 1') as $node)
{
...
}
Récupérer tous les enregistrements
Le jeu de résultat peut être renvoyé sous la forme d'un tableau associatif grâce à la propriété
all :
<?php
$array = $model->order('created DESC')->all;
Récupérer seulement le premier enregistrement
Il arrive souvent que l'on ne souhaite récupérer que le premier objet d'une requête, dans ce cas
on utilisera la propriété one :
<?php
$record = $model->order('created DESC')->one;
Récupérer des paires de valeurs
Lorsque l'on ne sélectionne que deux colonnes, il est possible de récupérer un résultat sous la
forme clé/valeur grâce à la propriété pairs :
<?php
$model->select('nid, title')->pairs;
Dans ce cas la première colonne est utilisée comme clé et la seconde comme valeur, pour un résultat similaire à celui-ci :
@raw array 34 => string 'Créer un nuage de mots-clé' (length=28) 57 => string 'Générer à la volée des miniatures avec mise en cache' (length=56) 307 => string 'Mes premiers pas de développeur sous Ubuntu 10.04 (Lucid Lynx)' (length=63) … raw@
Récupérer la valeur de la première colonne de la première ligne
On peut récupérer la valeur de la première colonne de la première ligne en utilisant la propriété rc :
<?php
$model->select('title')->rc;
@raw string 'Créer un nuage de mots-clé' (length=28) raw@
Choisir le type des enregistrements
En général on laissera le framework décider du type des enregistrements, mais il est possible de décider cela à
sa place grâce à la méthode mode :
<?php
$model->select('nid, title')->mode(PDO::FETCH_NUM);
La méthode mode prend les même arguments que la méthode
PDOStatement::setFetchMode.
Choisir le type des enregistrements avec les méthodes all et one
Il est également possible de définir le type des données depuis les méthodes all et one :
<?php
$array = $model->order('created DESC')->all(PDO::FETCH_ASSOC);
$record = $model->order('created DESC')->one(PDO::FETCH_ASSOC);
Tester l'existence d'objets
Pour simplement vérifier l'existence d'objets on utilise la méthode exists. Comme la méthode
find, cette méthode interroge la base de données à la recherche d'objets, mais au lieu de
retourner un objet ou une collection d'objets elle retourne TRUE ou FALSE selon la présence de
l'objet, ou des objets, dans la base.
<?php
$model->exists(1);
La méthode exists accepte également les jeux d'identifiants, mais au lieu de retourner TRUE ou
FALSE, elle retourne un tableau associatif où chaque clé est la valeur de la clé primaire de
l'objet, et la valeur de cette clé est TRUE ou FALSE selon que l'objet existe ou pas.
<?php
$model->exists(1,2,999)
# ou
$model->exists(array(1,2,999));
Ce qui peut donner le résultat suivant :
@raw array 1 => boolean true 2 => boolean true 999 => boolean false raw@
Il est également possible d'utiliser la méthode exists sans argument sur un modèle ou une
requête :
<?php
$model->where('author = ?', 'Madonna')->exists;
La requête ci-dessus retourne TRUE si au moins un auteur a pour nom « Madonna », FALSE dans le cas
contraire.
<?php
$model->exists;
La requête ci-dessus retourne FALSE si la table est vide, et TRUE dans le cas contraire.
Fonctions de calcul
Cette section utilise la méthode count comme exemple, mais les options décrites s'appliquent à
toutes les sous-sections, même si la méthode count possède quelques particularités.
Les méthodes de calcul peuvent s'appliquer directement sur le modèle :
<?php
$model->count;
Où sur une recherche :
<?php
$model->where('firstname = ?', 'Ryan').count;
Bien sûr, toutes les méthodes de recherche peuvent être utilisées :
<?php
$model->find_by_firstname('Ryan')->joins(':content')->where('YEAR(date) = 2011')->count;
Compter
La méthode count permet de connaitre le nombre d'enregistrements. Si l'on veut être plus
spécifique, on peut connaitre le nombre d'enregistrements selon la valeur d'un champ :
<?php
$model->count('is_online');
Renvera un tableau avec pour clé la valeur de la colonne, et pour valeur le nombre d'enregistrements ayant la même valeur pour la colonne :
@raw array 0 => string '35' (length=2) 1 => string '145' (length=3) raw@
Ici, il y a 35 enregistrements en ligne et 145 hors ligne.
Attention, ceci est une particularité de la méthode de calcul
count. Aucune autre méthode de calcul ne fonctionne de cette manière.
Moyenne, Minimum, Maximum et somme
Les méthodes de calcul average, minimum, maximum et sum permettent respectivement,
pour une colonne, de calculer la moyenne de ses valeurs, la valeur minimum, la valeur maximum et
la somme de ses valeurs.
Contrairement à la méthode de calcul count, ces méthodes requièrent le nom de la colonne sur
laquelle appliquer le calcul :
<?php
$model->average('comments_count');
$model->minimum('created');
$model->maximum('created');
$model->sum('comments_count');
Récapitulatif de l'ensemble des méthodes sous forme d'exemples
Obtenir des objets :
<?php
$record = $model[10];
# ou
$record = $model->find(10);
$records = $model->find(10, 15, 19);
# ou
$records = $model->find(array(10, 15, 19));
Conditions :
<?php
$model->where('is_online = ?', true);
$model->where(array('is_online' => true, 'is_home_excluded' => false));
$model->where('siteid = 0 OR siteid = ?', 1)->where('language = '' OR language = ?', "fr");
# Sous-ensembles
$model->where(array('order_count' => array(1, 2, 3));
$model->where(array('!order_count' => array(1, 2, 3)); # contraire
# Filtres dynamiques
$model->find_by_nid(1);
$model->find_by_siteid_and_language(1, 'fr');
# Portées
$model->visible;
Grouper, ordonner :
<?php
$model->group('date(created)')->order('created');
$model->group('date(created)')->having('created > ?', date('Y-m-d', strtotime('-1 month')))->order('created');
Limites et décalage :
<?php
$model->where('is_online = 1')->limit(10); // retourne les 10 premiers enregistrements
$model->where('is_online = 1')->limit(5, 10); // retourne les enregistrements 6 à 16
$model->where('is_online = 1')->offset(5); // retourne les enregistrements de 6 jusqu'au dernier
$model->where('is_online = 1')->offset(5)->limit(10);
Sélection de champs :
<?php
$model->select('nid, created, title');
$model->select('nid, created, CONCAT_WS(":", title, language)');
Jointures :
<?php
$model->joins('INNER JOIN contents USING(nid)');
$model->joins(':contents');
Récupérer les données :
<?php
$model->all;
$model->order('created DESC')->all(PDO::FETCH_ASSOC);
$model->order('created DESC')->mode(PDO::FETCH_ASSOC)->all;
$model->order('created DESC')->one;
$model->select('nid, title')->pairs;
$model->select('title')->rc;
Tester l'existence d'objets :
<?php
$model->exists;
$model->exists(1, 2, 3);
$model->exists(array(1, 2, 3));
$model->where('author = ?', 'madonna')->exists;
Fonctions de calcul :
<?php
$model->count;
$model->count('is_online');
$model->average('score');
$model->minimum('age');
$model->maximum('age');
$model->sum('comments_count');
Bref, de quoi faire.
Eric
Le code de la classe
WdActiveRecordQueryest vraiment très propre, ce n'est pas souvent, je trouve, dans ce genre d'exercice.Par contre, je ne comprend pas l'intérêt : Si j'ai bien compris :
va devenir :
Quel est l'avantage par rapport à la première écriture ?
Olivier
L'intérêt ne se situe pas vraiment dans la réécriture des requêtes SQL, et on peut très bien s'en passer :
Par contre, que se passe-t-il lorsqu'un nombre de fonctions peut modifier la requête SQL pour changer l'ordre des enregistrements, modifier les champs à retourner, ajouter de nouvelles conditions ou jointures, même changer les limites ? Manipuler la chaine de caractères risque de ne pas être très amusant.
L'objet
WdActiveRecordQueryest au contraire très malléable puisqu'on peut le modifier à tout moment, dans n'importe quel ordre, sans pour autant connaitre la requête initiale.Par exemple, tous les types de contenu du mon CMS sont présentés sous forme de tableaux. Chaque type de contenu étant légèrement différent, il hérite de la classe tableau du type de contenu parent, pour y ajouter ses spécificités, ou surcharger celles de son parent. Pour autant, la classe racine n'a pas besoin de connaitre tous les détails, elle se contente de créer un objet de requête initial et de demander sa modification :
On peut voir des choses comme ça (les
wheresont additifs) :Parce que l'objet est réutilisable, il devient très simple d'avoir un compte de tous les enregistrements disponibles pour une recherche, et de n'en charger qu'un jeu :
Sans parler des portées, puisque dans l'exemple ci-dessus,
visibleest un raccourcis pour :Je t'assure, dans un CMS entièrement orienté objet, c'est utile :-)
oxman
Et si on veut l'utiliser en standalone, quelle est la procédure ?
Olivier
Si par « standalone » tu veux dire « sans le framework », ça risque d'être compliqué. Comme tu peux l'imaginer, les classes qui gèrent les données sont assez dépendantes. Si tu veux utiliser l'interface de requête de la classe WdActiveRecordQuery, il te faudra au moins les classes suivantes :
Ensuite il faudra remplacer toutes les exceptions de classe WdException sinon il faudra que tu également inclure cette classe ainsi que le système d'I18n.
Bref, c'est possible, mais c'est du boulot. Sinon le framework pèse dans les 300Ko, avec tous ces composants…
Je t'invite à consulter l'article « Vue d'ensemble des connexions, modèles et enregistrements actifs (activerecords) », peut-être trouveras-tu d'autres choses intéressantes.