I. En quoi Pomm est-il différent d'un ORM et pourquoi l'utiliser ?▲
Il est difficile de répondre rapidement à cette question sans tomber dans l'ornière du débat pro / anti ORM. L'auteur développe avec PHP et PostgreSQL depuis plus d'une dizaine d'années. L'avènement des ORM a certes changé la façon d'utiliser les bases de données en apportant des vraies couches modèles au sein du MVC, mais ils ont également apporté un certain nombre d'inconvénients très handicapants pour les habitués des fonctionnalités des bases de données en général et de PostgreSQL en particulier. Pomm part donc du parti pris de ne fonctionner qu'avec PostgreSQL et son objectif est de permettre aux développeurs PHP de tirer parti de ses fonctionnalités au maximum.
Une des limitations des ORM est qu'en calquant une logique orientée objet sur des structures SQL, ils figent ces dernières suivant la définition de classes (PHP ou autres) alors que :
- les bases de données ne manipulent que des ensembles de tuples ;
- que les opérations ensemblistes sont insensibles à la taille de ces tuples ;
- que le système de projection (SELECT) a été conçu pour les façonner.
Un ensemble de base de données est donc par essence tout sauf figé. Nous verrons comment Pomm tire parti de la souplesse de PHP pour créer des objets élastiques s'adaptant à notre besoin. Ceci est d'autant plus appréciable que PostgreSQL sait manipuler des entités comme des objets, nous verrons comment faire des requêtes « orientées objet » en SQL.
Un autre des problèmes des ORM est lié à la couche d'abstraction : ils proposent un langage pseudo SQL orienté objet qui se cantonne souvent au plus petit commun dénominateur des fonctionnalités partagées entre tous les moteurs de bases de données et il est souvent délicat de trouver comment faire quelque chose qu'on sait déjà faire en SQL classique. Nous verrons comment Pomm permet de faire directement des requêtes SQL sans les inconvénients de la construction fastidieuse - que probablement certains d'entre vous ont connu - qui menait à des scripts peu maintenables et peu testables.
Le présent article vous propose de créer une application web qui cherche et affiche des informations sur les employés de la société El-Caro Corporation.
I-A. Mise en place de l'application▲
L'application suivante n'utilise pas de framework et est volontairement minimaliste. Il est bien sûr fortement conseillé d'en utiliser un, il existe à ce propos un adaptateur pour Silex et Symfony . Ne vous étonnez donc pas de ne pas trouver de belles URL (routing), de contrôleurs encapsulés (et testables), de moteur de template (fort utile) et autres bonnes pratiques. Cela va forcément s'éloigner de ce à quoi pourrait ressembler une application respectueuse des préceptes RESTFULL, mais cela va nous permettre de nous concentrer sur le sujet de cet article.
Nous allons utiliser Composer pour installer Pomm et instancier un autoloading dans notre projet. Pour cela, il n'est pas utile de créer plus qu'un fichier composer.json comme suit dans un répertoire vierge :
2.
3.
4.
5.
6.
{
"
minimum-stability
"
:
"
dev
"
,
"
require
"
:
{
"
pomm/pomm
"
:
"
dev-master
"
}
}
Reste à appeler le script composer.phar install pour que Composer installe Pomm et le prenne en compte dans son autoloader.
I-B. Bienvenue dans la société El-Caro Corp.▲
Ce tutoriel vous propose de créer une application simpliste de gestion des salariés de la société informatique « El-Caro Corporation ». Cette société est divisée en départements hiérarchisés et chaque employé appartient à un département lui-même pouvant être fils d'un autre service. La structure de la base de données est la suivante :
Nous allons créer un schéma nommé company dans notre base de données pour y créer la structure décrite ci-dessus :
2.
3.
4.
$>
CREATE
SCHEMA
company;
$>
SET
search_path TO
company, public
;
$>
SHOW
search_path;
company, public
La commande SHOW doit nous retourner company, public signe que le client va d'abord chercher les objets dans le schéma company puis ensuite dans le schéma par défaut public. Il y a plusieurs avantages à utiliser un schéma, le plus important est de pouvoir intégrer des extensions contenant des tables à notre application sans risquer une collision de nom. Un autre avantage est que si l'on souhaite « passer l'éponge », il suffit de lancer un DELETE SCHEMA company CASCADE et de recommencer. Une fois le schéma créé, implémentons la structure :
2.
3.
4.
5.
$>
CREATE
TABLE
department (
department_id serial
PRIMARY
KEY
,
name
varchar
NOT
NULL
,
parent_id integer
REFERENCES
department (
department_id)
)
;
Tel que nous l'avons décrite, cette table possède un identifiant technique - un entier - qui s'auto-incrémente à l'aide d'une séquence qui est autogénérée et initialisée à la création de la table comme l'indique PostgreSQL. Notons que le parent_id même s'il est indiqué comme référent au département parent peut être nul dans le cas du département racine. En revanche la contrainte de clé étrangère forcera tout département indiqué comme père à exister au préalable dans la table.
2.
3.
4.
5.
6.
7.
8.
9.
$>
CREATE
TABLE
employee (
employee_id serial
PRIMARY
KEY
,
first_name varchar
NOT
NULL
,
last_name varchar
NOT
NULL
,
birth_date date
NOT
NULL
CHECK
(
age(
birth_date)
>=
'18 years'
::interval
)
,
is_manager boolean
NOT
NULL
DEFAULT
false
,
day_salary numeric
(
7
,2
)
NOT
NULL
,
department_id integer
NOT
NULL
REFERENCES
department (
department_id)
)
;
Nous voyons ici que la structure d'un employé est fortement contrainte. Une vérification - pour l'exemple - d'âge est faite pour vérifier que la date de naissance entrée ne correspond pas à un mineur. Dans le cas d'un employé, l'appartenance à un département est rendue obligatoire par la contrainte NOT NULL sur le champ de clé étrangère department_id vers la table department.
Un jeu de données est disponible dans ce Gist .
I-C. Génération du modèle PHP▲
À partir de cette structure de base de données, Pomm sait construire les classes qui correspondent aux tables pour nous permettre de nous affranchir des traitements fastidieux de PDO. Dans un premier temps, nous créons un fichier appelé bootstrap.php qui sera inclus par nos scripts et dont le but est d'initialiser l'autoloading et la base de données.
Notez que nous spécifions le répertoire lib comme répertoire par défaut pour trouver les namespaces à l'autoloader.
Pour maintenant générer les fichiers de mapping, créons le fichier generate_model.php dont une version plus générale est disponible dans ce Gist .
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
<?php
//generate_model.php
$connection
=
require(__DIR__.
"/bootstrap.php"
);
$scan
=
new
Pomm\Tools\ScanSchemaTool(array
(
'schema'
=>
'company'
,
'database'
=>
$connection
->
getDatabase(),
'prefix_dir'
=>
__DIR__.
"/lib"
));
$scan
->
execute();
$scan
->
getOutputStack()->
setLevel(254
);
foreach
( $scan
->
getOutputStack() as
$line
)
{
printf("%s
\n
"
,
$line
);
}
Ce script utilise un des outils fournis avec Pomm : le scanner de schéma . Cet outil utilise l'inspecteur de base de données de Pomm pour générer des classes de mapping liées aux structures stockées en base. Dans le cas présent, nous lui demandons de scanner le schéma company et de générer les fichiers dans le sous-répertoire lib, là où nous avons fait pointer l'autoloader par défaut dans le fichier bootstrap.php. Un appel à ce script va nous générer la structure de fichiers suivante:
2.
3.
4.
5.
6.
7.
8.
9.
10.
lib/
&
#9492;&
#9472;&
#9472; ElCaro
&
#9492;&
#9472;&
#9472; Company
&
#9500;&
#9472;&
#9472; Base
&
#9474; &
#9500;&
#9472;&
#9472; DepartmentMap.php
&
#9474; &
#9492;&
#9472;&
#9472; EmployeeMap.php
&
#9500;&
#9472;&
#9472; DepartmentMap.php
&
#9500;&
#9472;&
#9472; Department.php
&
#9500;&
#9472;&
#9472; EmployeeMap.php
&
#9492;&
#9472;&
#9472; Employee.php
Cette architecture ne choquera pas les utilisateurs habitués à utiliser des ORM. Nous pouvons constater que le namespace utilisé par les classes de modèle est \ElCaro\Company, c'est-à-dire le nom de la base de données passé en paramètre lors de l'instanciation de la classe Database avec le nom du schéma. Ainsi, il est possible d'avoir plusieurs classes de tables portant le même nom, mais déclarées dans des schémas PostgreSQL différents. D'autre part, chaque table génère trois classes :
- une classe portant le même nom que la table à la casse près ;
- une classe portant le même nom, mais affublé du suffixe Map ;
- la même classe dans le sous namespace Base.
Les classes du sous namespace Base contiennent la définition déduite depuis la structure de la base de données. Ces fichiers seront écrasés à chaque introspection en cas d'évolution de la structure de la base, il serait donc malvenu qu'elles contiennent du code que nous aurions pu placer là. C'est pour cela que la classe Map hérite de sa consœur dans Base. Vous pouvez y placer votre code, cette classe ne sera pas écrasée.
Les utilisateurs d'ORM ne seront pas non plus surpris d'apprendre que la classe Map est l'outil qui s'occupera de gérer la vie de leur entité correspondante avec la base de données, à savoir :
- DepartmentMap sauvegarde, génère et renvoie des collections d'entités Department ;
- EmployeeMap renvoie des collections d'entités Employee.
I-D. Premiers pas▲
Pour notre première interface, nous allons afficher la liste des employés. Créons le fichier index.php avec le code PHP suivant :
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.
<?php
//index.php
// CONTROLLER
$connection
=
require(__DIR__.
"/bootstrap.php"
);
$employees
=
$connection
->
getMapFor('\ElCaro\Company\Employee'
)
->
findAll();
// TEMPLATE
?>
<
html>
<
head>
<
meta http-
equiv=
"
Content-Type
"
content=
"
text/html; charset=utf-8
"
>
</
head>
<
body>
<
h1>
El-
Caro -
Workers list</
h1>
<?php
if
($employees
):
?>
<
ul>
<?php
foreach
($employees
as
$employee
):
?>
<
li><
a href=
"
/show_employee.php?employee_id=<?php echo
$employee["employee_id"]
?>
"
>
<?php
echo $employee
[
"first_name"
].
" "
.
$employee
[
"last_name"
]
?>
</
a></
li>
<?php
endforeach
?>
</
ul>
<?php
else
:
?>
<
p>
No employees !?!?
There must be a bug somewhere...</
p>
<?php
endif
?>
</
body>
</
html>
Commentons le code ci-dessus :
- La connexion nous permet d'obtenir des instances de classes Map ;
- La classe Map sait faire des requêtes qui ramènent des collections de leur entité correspondante ;
- Ces collections sont accessibles via foreach et retournent leurs entités ;
- Les valeurs internes des entités sont accessibles entre autres via la notation de tableau.
L'utilisation de la notation de tableau est pratique dans les templates et elle équivaut complètement à l'utilisation d'accesseurs. Ainsi $employee['first_name'] est équivalent à $employee->getFirstName(). Cela permet par exemple, si on souhaite formater le prénom capitalisé et le nom en majuscule, de juste avoir à surcharger getFirstName() et getLastName() dans la classe Employee :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
<?php
// lib/ElCaro/Company/Employee.php
namespace
ElCaro\Company;
use
\Pomm\Object\BaseObject;
use
\Pomm\Exception
\Exception
;
class
Employee extends
BaseObject
{
public
function
getFirstName()
{
return
ucwords($this
->
get('first_name'
));
}
public
function
getLastName()
{
return
strtoupper($this
->
get('last_name'
));
}
public
function
__toString
()
{
return
sprintf("%s %s"
,
$this
[
'first_name'
],
$this
[
'last_name'
]
);
}
}
Seuls les accesseurs génériques get(), set(), has() et clear() ne peuvent être surchargés, car ils sont utilisés pour accéder aux valeurs brutes de l'objet. Nous en aurions besoin ici si nous souhaitions par exemple implémenter une recherche par le prénom alors que la méthode getFirstName() ne nous retourne pas la valeur effectivement stockée en base.
Dans la vraie vie™, un tel exemple ne serait pas vraiment exploitable à cause du volume de données dès que le nombre d'employés dépasse quelques dizaines. Cela ne nous aurait pas coûté tellement plus cher de les classer par ordre alphabétique et de paginer notre liste de résultats dans le contrôleur.
I-E. Entités élastiques▲
Intéressons-nous maintenant à l'affichage des données d'un utilisateur :
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.
<?php
// show_employee.php
$connection
=
require(__DIR__.
"/bootstrap.php"
);
// CONTROLLER
if
(!
$employee
=
$connection
->
getMapFor('\ElCaro\Company\Employee'
)
->
findByPk(array
('employee_id'
=>
$_GET
[
'employee_id'
]
)))
{
printf("No such user."
);
exit;
}
// TEMPLATE
?>
<
html>
<
head>
<
meta http-
equiv=
"
Content-Type
"
content=
"
text/html; charset=utf-8
"
>
</
head>
<
body>
<
h1>
El-
Caro -
<?php
echo $employee
?>
(<?php
echo $employee
[
"employee_id"
]
?>
)</
h1>
<
p><
a href=
"
/index.php
"
>
Back to the homepage</
a>.</
p>
<
ul>
<
li>
Birth date:
<?php
echo $employee
[
"birth_date"
]->
format("d/m/Y"
) ?>
.</
li>
<
li>
day salary indice:
<?php
printf("%05.2f"
,
$employee
[
"day_salary"
]
) ?>
.</
li>
<
li>
Status:
<?php
echo $employee
[
"is_manager"
]
?
"manager"
:
"worker"
?>
.</
li>
<
li>
Department:
<?php
echo $employee
[
"department_id"
]
?>
.</
li>
</
ul>
</
body>
</
html>
Là encore, nous pouvons voir que le convertisseur a fait son travail, la date de naissance est un objet PHP DateTime, le champ is_manager est un booléen et on peut formater day_salary convenablement.
Imaginons maintenant qu'en plus de la date de naissance, nous avions besoin d'avoir directement l'âge de la personne. Il est bien sûr facile de créer un accesseur supplémentaire getAge() dans la classe Employee dans laquelle nous calculerions en PHP l'âge à partir de la date de naissance, mais pourquoi ne pas le demander directement à PostgreSQL en utilisant la fonction age() ?
Il faut savoir que Pomm n'utilise jamais l'alias * dans ses requêtes, il utilise pour cela la méthode getSelectFields() définie dans les classes Map. Par défaut, cette méthode retourne tous les champs de la table, mais il est possible de la surcharger pour en enlever ou en ajouter. En d'autres termes, cette méthode définit la projection de l'objet en base de données vers l'objet entité PHP .
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
<?php
// lib/ElCaro/Company/EmployeeMap.php
namespace
ElCaro\Company;
use
ElCaro\Company\Base\EmployeeMap as
BaseEmployeeMap;
use
ElCaro\Company\Employee;
use
\Pomm\Exception
\Exception
;
use
\Pomm\Query\Where;
class
EmployeeMap extends
BaseEmployeeMap
{
public
function
getSelectFields($alias
=
null
)
{
$fields
=
parent
::
getSelectFields($alias
);
$fields
[
'age'
]
=
'age(birth_date)'
;
return
$fields
;
}
}
Et ajoutons la ligne suivante dans la partie template de show_employee.php :
<li>Age: <?php echo $employee['age'
] ?>.</li>
En rafraîchissant la page, celle-ci affiche désormais quelque chose ressemblant à Age: 27 years 11 mons 2 days.. C'est la sortie brute de la commande age() de PostgreSQL, Pomm ne sachant comment interpréter cette sortie, la convertit au format String. Il est possible d'étendre la définition de notre entité en y ajoutant le type de cette nouvelle colonne virtuelle afin qu'elle soit prise en charge par le convertisseur quand elle existe :
2.
3.
4.
5.
6.
7.
8.
<?php
// lib/ElCaro/Company/EmployeeMap.php
// [...]
public
function
initialize()
{
parent
::
initialize();
$this
->
addVirtualField('age'
,
'interval'
);
}
Si vous rafraîchissez désormais la page, celle-ci présente une erreur comme quoi PHP ne sait pas comment afficher une instance de la classe PHP DateInterval : le convertisseur a bien fait son travail. Changez l'affichage de l'âge par la ligne suivante :
<li>Age: <?php echo $employee['age'
]->format("%y") ?> years old.</li>
Avant de conclure ce chapitre, notons que la méthode getSelectFields() que nous avons surchargée est génératrice de problèmes, car le nouveau champ age que nous avons ajouté est insensible à l'alias. Cela peut - et va - poser des problèmes lors de requêtes complexes où ce champ peut apparaître dans plusieurs ensembles. La laisser ainsi occasionnerait des erreurs SQL de type « champ ambigu » assez délicates à déboguer. Pour prévenir cela, corrigeons la méthode comme suit :
I-F. Requêtes SQL personnalisées▲
Si désormais, nous souhaitons afficher le nom du service au lieu du department_id, appeler findByPk() est insuffisant, nous allons devoir créer une jointure pour ramener cette information. Créons une méthode dans notre classe de modèle dont le but sera de ramener un employé avec des informations sur son service. On peut coucher immédiatement la requête nécessaire (le NATURAL JOIN de Postgres permet de faire une jointure sur deux ensembles en prenant les champs homonymes) :
SELECT
*
, dept.name
FROM
employee NATURAL
JOIN
department dept WHERE
employee_id =
?
Cependant, la requête sous cette forme présente des inconvénients :
- elle n'utilise pas la méthode de projection getSelectFields() et n'affichera pas le champ age ;
- indiquer le nom des tables « en dur » peut nous poser des problèmes d'évolutivité.
Les classes Map de Pomm proposent pour cela des méthodes pour avoir ces informations de façon dynamique. Idéalement, notre requête pourrait être vue sous cette forme :
SELECT
%
A, dept.name
FROM
%
B NATURAL
JOIN
%
C WHERE
employee_id =
?
- %A est la liste des colonnes que l'on souhaite ramener de la table B ;
- %B est la table des employés ;
- %C est la table des départements.
B et C sont facilement remplacés grâce à la méthode getTableName() de chaque classe Map. Nous savons que l'on peut obtenir la liste des colonnes à ramener avec la méthode getSelectFields(), mais cette méthode retourne un tableau associatif dont la clé est l'alias du champ et la valeur hé bien… sa valeur. Il faut donc formater ce tableau en une liste de champs. Les classes Map proposent pour cela des méthodes dédiées,les formateurs :
- formatFields(methode, alias) ;
- formatFieldsWithAlias(methode, alias).
2.
3.
4.
$this
->
formatFields('
getSelectFields
'
,
'
pika
'
);
// "pika.employee_id", "pika.first_name", "pika.last_name", "pika. ....
$this
->
formatFieldsWithAlias('
getSelectFields
'
,
'
plop
'
);
// "plop.employee_id" AS "employee_id", "plop.first_name" AS "first_name", ...
Ainsi formé, il est facile de se concentrer sur ce que font les requêtes plutôt que sur la syntaxe elle-même :
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.
<?php
// lib/ElCaro/Company/EmployeeMap.php
// [...]
public
function
initialize()
{
// [...]
$this
->
addVirtualField('department_name'
,
'varchar'
);
}
public
function
getEmployeeWithDepartment($employee_id
)
{
$department_map
=
$this
->
connection->
getMapFor('\ElCaro\Company\Department'
);
$sql
=
<<<SQL
SELECT
:employee_fields_emp, dept.name AS department_name
FROM
:employee_table emp
NATURAL JOIN :department_table dept
WHERE
emp.employee_id = ?
SQL;
$sql
=
strtr($sql
,
array
(
':employee_fields_emp'
=>
$this
->
formatFieldsWithAlias('getSelectFields'
,
'emp'
),
':employee_table'
=>
$this
->
getTableName(),
':department_table'
=>
$department_map
->
getTableName()
));
return
$this
->
query($sql
,
array
($employee_id
))->
current();
}
Remplaçons dans notre contrôleur l'appel à findByPk par cette méthode :
Et dans le template correspondant :
<
li>
Department:
<?php
echo $employee
[
"department_name"
]
?>
.</
li>
La possibilité de faire des requêtes SQL depuis les classes Map est une fonctionnalité extrêmement puissante, car elle permet d'utiliser tous les mécanismes SQL de PostgreSQL. Par exemple, les départements sont une structure arborescente, nous pouvons demander à PostgreSQL de ramener sous forme de tableaux l'ensemble des services auxquels chaque utilisateur appartient. Pour cela, nous utilisons une requête récursive avec un agrégateur de tableaux et déclarons notre colonne comme un tableau de chaînes de caractères :
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.
<?php
// lib/ElCaro/Company/EmployeeMap.php
// [...]
public
function
initialize()
{
parent
::
initialize();
$this
->
addVirtualField('age'
,
'interval'
);
$this
->
addVirtualField('department_names'
,
'varchar[]'
);
}
public
function
getEmployeeWithDepartment($employee_id
)
{
$department_map
=
$this
->
connection->
getMapFor('\ElCaro\Company\Department'
);
$sql
=
<<<SQL
WITH RECURSIVE
depts (department_id, name, parent_id) AS (
SELECT :department_fields_alias_d FROM :department_table d NATURAL JOIN :employee_table emp WHERE emp.employee_id = ?
UNION ALL
SELECT :department_fields_alias_d FROM depts parent JOIN :department_table d ON parent.parent_id = d.department_id
)
SELECT
:employee_fields_alias_emp, array_agg(depts.name) AS department_names
FROM
:employee_table emp,
depts
WHERE
emp.employee_id = ?
GROUP BY
:employee_group_by_emp
SQL;
$sql
=
strtr($sql
,
array
(
':department_fields_alias_d'
=>
$department_map
->
formatFieldsWithAlias('getSelectFields'
,
'd'
),
':department_table'
=>
$department_map
->
getTableName(),
':employee_fields_alias_emp'
=>
$this
->
formatFieldsWithAlias('getSelectFields'
,
'emp'
),
':employee_table'
=>
$this
->
getTableName(),
':employee_group_by_emp'
=>
$this
->
formatFields('getGroupByFields'
,
'emp'
),
));
return
$this
->
query($sql
,
array
($employee_id
,
$employee_id
))->
current();
}
Et dans le template :
La requête ci-dessus, utilise la clause SQL WITH qui permet de créer des ensembles nommés et de les rappeler. Cela évite de faire des sub-select. Le premier ensemble aliasé depts est la clause récursive. Elle possède un ensemble de départ - le département direct de l'employé - uni à une requête récursive qui remonte l'arbre jusqu'à ce que ça ne soit plus possible. L'ensemble depts va donc contenir tous les départements de l'employé. La requête finale va tout simplement faire un CROSS JOIN entre les informations de l'employé et l'agrégat en tableaux du nom de ses départements.
Une remarque concernant la construction de requêtes, il a été évoqué dans l'introduction de cet article, alors qu'ici la condition est connue d'avance : emp.employee_id = ?. Dans les interfaces de recherche, il se peut qu'on ne puisse savoir à l'avance sur quels critères la recherche va porter. Pomm propose pour cela une classe de construction de clauses where qui respecte les priorités ET et OU et qui peut être passée directement en paramètre à la méthode findWhere().
I-G. Requêtes orientées objet▲
Afficher l'arbre des départements de chaque utilisateur est une bonne chose, mais cela serait plus intéressant si on pouvait avoir un lien sur chaque département qui mènerait à la fiche du département contenant tous ses employés. Dans l'état actuel de la requête, on ne ramène que les noms des départements, il nous faudrait également leurs identifiants. On peut toujours ajouter une colonne contenant un tableau d'identifiant, mais cela n'est pas très pratique.
PostgreSQL propose une fonctionnalité intéressante : lorsque vous déclarez une table, PostgreSQL va automatiquement créer le type composite correspondant. C'est-à-dire que le type company.department existe et que vous pouvez faire des requêtes de ce type directement :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
elcaro$>
SELECT department FROM department;
&
#9484;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9488;
&
#9474; department &
#9474;
&
#9500;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9508;
&
#9474; (1,"el caro corp.",) &
#9474;
&
#9474; (2,siège,1) &
#9474;
&
#9474; (3,direction,2) &
#9474;
&
#9474; (4,comptabilité,1) &
#9474;
&
#9474; (5,"direction technique",3) &
#9474;
&
#9474; (6,"hotline niveau II",5) &
#9474;
&
#9474; (7,"datacenter skynet",1) &
#9474;
&
#9474; (8,"technique &
réseau",7) &
#9474;
&
#9474; (9,"Hotline niveau I",7) &
#9474;
&
#9474; (10,Direction,7) &
#9474;
&
#9492;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9472;&
#9496;
(10 rows)
Le résultat de la requête ci-dessus ne possède qu'une seule colonne de type department. Ce que l'on appelle pompeusement requête orientée objet en PostgreSQL n'est que le fait de manipuler des tuples comme des valeurs, mais cela est déjà extrêmement puissant, car si on modifie la ligne suivante dans la requête SQL vue plus haut :
2.
3.
4.
5.
6.
7.
...
SELECT
%
s, array_agg(
depts)
AS
departments
FROM
%
s emp,
depts
...
Dès lors, la colonne departments contiendra un tableau d'entités department. Il convient de changer la ligne correspondante dans le template pour afficher les liens :
Mais cela ne suffit pas. PHP va se plaindre que l'argument passé à array_map n'est pas un tableau et il aura raison. N'ayant pas déclaré la colonne departments de la classe Employee, Pomm va juste y placer la chaîne de caractères renvoyée par PostgreSQL. Il faut donc renseigner le convertisseur que la colonne departments contient un tableau de type department. Seul souci : Pomm ne charge pas par défaut de convertisseur pour ce type de données. Nous allons donc déclarer ce convertisseur auprès de l'instance de base de données dans le fichier bootstrap.php :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
<?php
// bootstrap.php
$loader
=
require __DIR__.
"/vendor/autoload.php"
;
$loader
->
add(null
,
__DIR__.
"/lib"
);
$database
=
new
Pomm\Connection\Database(array
('dsn'
=>
'pgsql://greg/greg'
,
'name'
=>
'el_caro'
));
$database
->
registerConverter(
'Department'
,
new
\Pomm\Converter\PgEntity($database
->
getConnection()->
getMapFor('\ElCaro\Company\Department'
)),
array
('company.department'
)
);
return
$database
->
getConnection();
Nous déclarons un nouveau convertisseur appelé Department qui associe le(s) type(s) Postgres company.department à l'instance de convertisseur PgEntity. Reste à utiliser ce convertisseur dans la colonne virtuelle departments de chaque Employee :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
<?php
// lib/ElCaro/Company/EmployeeMap.php
// [...]
class
EmployeeMap extends
BaseEmployeeMap
{
public
function
initialize()
{
parent
::
initialize();
$this
->
addVirtualField('age'
,
'interval'
);
$this
->
addVirtualField('departments'
,
'company.department[]'
);
}
Le fichier show_department.php est laissé à titre d'exercice :)
I-H. Écrire dans la base▲
Imaginons maintenant que l'interface show_employee.php permette de changer le status 'manager / worker' d'un employé en cliquant dessus. Modifions le template pour créer le lien :
<
li>
Status:
<
a href=
"
employee_change_status.php?status=<?php echo
$employee['is_manager']
? 1 : 0 ?>&employee_id=<?php echo
$employee["employee_id"]
?>
"
>
<?php
echo $employee
[
"is_manager"
]
?
"manager"
:
"worker"
?>
</
a>.</
li>
Les utilisateurs d'ORM auront probablement le réflexe d'écrire le contrôleur de sauvegarde de la façon suivante :
- Je récupère l'employé par son id ;
- S'il n'existe pas, je renvoie une notification d'erreur ;
- Sinon je le mets à jour ;
- Je renvoie une réponse.
PostgreSQL permet de faire tout cela presque en un seul temps. Regardons la structure d'un UPDATE :
UPDATE
:table
SET
:champs1 =
:valeur1, [:champsN = :valeurN]
WHERE
:clause_where RETURNING :list_champs
C'est-à-dire qu'en faisant cet update, on peut mettre à jour un enregistrement sur un id précis – s'il existe – et retourner de quoi hydrater une entité avec les valeurs mises à jour de la base de données, c'est ce que fait la méthode updateByPk() :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
<?php
// employee_change_status.php
$connection
=
require(__DIR__.
"/bootstrap.php"
);
// CONTROLLER
if
(!
$employee
=
$connection
->
getMapFor('\ElCaro\Company\Employee'
)
->
updateByPk(array
('employee_id'
=>
$_GET
[
'employee_id'
]
),
array
("is_manager"
=>
$_GET
[
"status"
]
==
0
)))
{
printf("No such employee !"
);
exit;
}
header(sprintf("Location: show_employee.php?employee_id=%d"
,
$employee
[
"employee_id"
]
));
Cette méthode ne fait que mettre à jour un enregistrement sur un nombre défini d'attributs. Il est de la même façon possible de sauvegarder et / ou créer des entités en base avec la méthode saveOne() sans vous soucier s'il s'agit d'une mise à jour ou une insertion. Toutes ces méthodes retournent - grâce à l'emploi en interne de RETURNING :select_fields - une entité mise à jour avec les valeurs de la base de données. Plus d'information est disponible dans la documentation .
I-I. Pour conclure▲
Au cours de cet article, nous n'avons fait qu'égratigner la surface des possibilités offertes par PostgreSQL. Nous pourrions continuer et mettre à contribution le fameux type clé -> valeur HStore, faire des tags hiérarchiques en utilisant des chemins matérialisés LTree, créer un historique des changements d'un employé en créant une table dont une des colonnes serait de type employee … la liste des exemples est encore longue.
Pomm est un outil dont l'objectif est de permettre aux développeurs de profiter pleinement des fonctionnalités de PostgreSQL. Effectivement, la barrière peut parfois sembler mince entre Pomm et un ORM. À la différence d'un ORM, Pomm est un outil spécialisé qui va permettre de gagner en vitesse et en performances. En s'appuyant sur des fonctionnalités uniques du moteur sous-jacent, Pomm ouvre des perspectives intéressantes qu'il était difficile d'envisager directement avec juste PDO ou avec un ORM.
Vous pouvez retrouver l'entrepôt Git de cet article sur Github .
II. À propos de l'auteur▲
Grégoire HUBERT mêle depuis plus de 10 ans des activités professionnelles d'hébergement et de développement toujours dans la librosphère. Il commence le développement avec PHP et PostgreSQL au XXe siècle puis après avoir embrassé Symfony dès 2006 en travaillant chez Sensio et pris la direction technique de KnpLabs en 2011, il continue le développement web en tant que freelance aujourd'hui. Auteur de Pomm, fan de GNU/Linux, PostgreSQL, Vim et tout ce qui apporte en général plus de solutions que de problèmes, vous pouvez le retrouver sur [twitter]( https://twitter.com/chanmix51 ) ou sur gregoire (point) hubert (point) pro (at) gmail (point) com.
III. Remerciements▲
Un grand merci à Julien Bianchi qui a été jusqu'à packager une VM Vagrant / VirtualBox pour ce tutoriel. Vous pouvez retrouver de quoi la construire dans son répository . Merci également à Nicolas Joseph dont l'aide très efficace a beaucoup apporté à cet article.
Merci également à f-leb pour le travail de relecture et de correction orthographique.