PHP 5.3 : namespace : les espaces de noms (partie 1)

24 novembre 2008namespace, php, php-5.3

Les exemples correspondant à ce point se trouvent dans le répertoire “namespace”.

Pour éviter que cet article ne soit trop long, je l’ai découpé en deux parties : une que je publie aujourd’hui (lundi 24/11), et l’autre qui sera pour demain, mardi 25 novembre 2008.

Sommaire de cette première partie :


Introduction

Les « Espaces de noms », ou « namespaces », sont probablement une des fonctionnalités les plus attendues de PHP 5.3… Et sans le moindre doute celle qui a fait couler le plus d’encre et de pixels !

Quelques mots sur le pourquoi des namespaces

Un des reproches qui est parfois fait à PHP est la longueur des noms de classes que proposent les bibliothèques avec lesquelles nous travaillons.

Pour ne prendre qu’un exemple, que pensez-vous de la portion de code reproduite ci-dessous ?

class PHPUnit_Extensions_Database_DB_MetaData_MySQL
    extends PHPUnit_Extensions_Database_DB_MetaData_InformationSchema
{
    // ...
}

Combien de temps, montre en main, pour définir une classe héritant de celle-ci, ou même l’instancier, sans faute de frappe ?
OK, je l’admet, j’ai volontairement pris un exemple quelque peu extrême — mais vous voyez ce que je veux dire ?


Un autre problème souvent rencontré lorsque nous travaillons avec du code développé par d’autres (y compris lorsque nous utilisons des bibliothèques qui ne respectent pas la convention PEAR) est celui de redéfinition de classes ou de fonctions…

Typiquement, si deux bibliothèques définissent toutes deux une classe « MyClass », et que vous utilisez ces deux librairies, vous vous prendrez une Fatal Error de ce type :

namespace-fatal-error-cannot-redeclare-class.png

C’est justement pour cela que l’on utilise souvent la convention PEAR et ses noms de classes longs — mais uniques…
Mais, malheureusement, on trouve encore bien souvent du code qui ne respecte pas cette habitude, y compris dans des bibliothèques fort utiles !


L’implémentation des espaces de noms en PHP 5.3 permet de répondre à ces deux problèmes, en autorisant la définition d’aliases pour les noms de classes, ainsi qu’en offrant la possibilité « d’encapsuler » l’ensemble des classes d’une bibliothèque dans un espace de noms.


Discussions enflammées

Les namespaces ont souvent, et longuement, été source de désaccord au sein de la communauté : il fallait trouver une implémentation qui satisfasse le plus grand nombre, et il n’est jamais facile de répondre à tous les besoins :

  • Avant tout, il faut que ce qui existait avant PHP 5.3 continue à fonctionner,
  • L’implémentation retenue doit être suffisamment proche, en termes de fonctionnalités, de celles existant dans d’autres langages, pour que l’utilisateur ne soit pas « trompé » par le terme « namespace »,
  • Tout en respectant les particularités de PHP — en particulier le fait qu’il s’agit d’un langage dynamique, non compilé, avec des fonctionnalités du type Autoload, … et devant demeurer un minimum accessible aux débutants,
  • Et le tout, bien entendu, en étant utilisable facilement, ce qui signifie une syntaxe claire et compréhensible, évocatrice, facilement accessible au clavier[1], …

Enfin, en bref, vous l’aurez compris (et constaté si vous suivez quelques blogs traitant de PHP), il est difficile de trouver une implémentation qui mette tout le monde d’accord ^^

C’est d’ailleurs la principale raison pour laquelle une troisième version alpha de PHP 5.3 était nécessaire : au moment de la seconde version alpha, les discussions autour des espaces de noms étaient loin d’être terminée — et de grosses différences, du point de vue utilisateur notamment, ont été introduites entre les versions alpha2 et alpha3.

La différence la plus visible est sans le moindre doute le choix de l’opérateur utilisé pour les namespaces : l’antislash « \  », qui remplace les doubles deux-points « :: » qui étaient utilisés jusqu’à la version alpha2 incluse.

Cela signifie notamment que tous les exemples que vous pouvez trouver sur Internet ou dans des magazines, datant d’avant novembre 2008 et utilisant « :: » pour les namespaces, sont faux, et ne fonctionneront pas sur la version finale de PHP 5.3 !


Les version alpha3 de PHP devrait être publiée dans les prochains jours, probablement aux environs du 26 novembre ; sachant que les tests effectués pour la rédaction de cet article sont basés sur un snapshot du 22/11 — avec un peu de chance, il ne devrait pas y avoir de différence majeure entre les implémentations proposées par ces deux versions…


Premiers espaces de noms

Pour commencer, nous allons faire simple ; plus compliqué — et probablement plus proche de ce que vous utiliserez — suivra ;-)

Un premier espace de noms

Un espace de noms se déclare à l’aide du nouveau mot-clef namespace, suivi du nom de l’espace de noms au sein duquel vous voulez travailler, et d’un point-virgule.

Tout ce qui suit cette déclaration, jusqu’à la prochaine instruction namespace, sera déclaré au sein de l’espace de noms.

Par exemple, pour définir un espace de noms nommé « LIB » et contenant deux fonctions, nous pourrions utiliser la portion de code suivante :

<?php
namespace LIB;
// Ce qui est déclaré maintenant l'est dans le namespace "LIB"

function a() {
    var_dump('LIB\a');
}

function b() {
    var_dump('LIB\b');
}

Ensuite, pour utiliser les fonctions définies dans votre espace de noms, il vous faut, d’une part, inclure le fichier les contenant (sur ce point, pas de différence avec PHP < 5.3), et, d’autre part, utiliser le séparateur de namespace pour séparer votre espace de noms et les fonctions qu’il contient.
J’insiste : ce séparateur de namespace est l’opérateur « \  » — et absolument pas « :: » !

Par exemple, pour appeler nos deux fonctions, nous utiliserions la syntaxe suivante :

<?php
require_once(__DIR__ . '/lib.php');

LIB\a();
LIB\b();

Cette portion de code entrainera l’affichage de la sortie suivante :

string 'LIB\a' (length=5)
string 'LIB\b' (length=5)

En somme, notre espace de noms agit comme un nouveau type de conteneur, et l’opérateur \ est utilisé pour séparer le nom de l’espace de noms des noms des fonctions qu’il contient.


Séparateur \ et caractères d’espacement

L’utilisation du caractère \ comme séparateur de namespace peut vous sembler peu judicieuse en termes de lisibilité[2] ; à tout hasard, si vous souhaitez améliorer les choses, sachez que vous pouvez librement ajouter des caractères d’espacement autour.

Par exemple, si vous définissez un espace de noms « NamespaceA » contenant une fonction « myFunctionA » de la manière suivante :

<?php
namespace NamespaceA;
// Ce qui est déclaré maintenant l'est dans le namespace "NamespaceA"

function myFunctionA() {
    var_dump(__NAMESPACE__);
    var_dump(__FUNCTION__);
}

Vous pouvez ensuite appeler cette fonction comme ceci :

<?php
require_once(__DIR__ . '/lib-A.php');

NamespaceA \ myFunctionA();

Les caractères d’espacement autour du séparateur \ ne posent pas de problème.


namespace : première instruction d’un fichier

Vous noterez que l’instruction namespace doit impérativement être la première instruction d’un fichier : elle ne doit pas être précédée d’une autre portion de code !

Par exemple, si vous essayez d’utiliser le code suivant :

<?php

function test()
{
    echo 'glop';
}

namespace Mon\NS;

function myFunction() {
    var_dump(__NAMESPACE__);
}

Vous obtiendrez une Fatal Error :

namespace-fatal-error-namespace-declaration-statement-has-to-be-the-very-first-statement.png

(Elle est explicite, et décrit précisément ce que je viens de dire qu’il ne fallait pas faire ^^ )

A noter : il n’est pas non plus possible d’utiliser une instruction d’inclusion de code, du type « require/include », avant le mot-clef namespace : il doit réellement constituer la première instruction du fichier !


Namespaces, et organisation des fichiers

Voyons maintenant comment il est possible d’organiser vos fichiers lorsque vous travaillez avec des espaces de noms…

namespace sur plusieurs fichiers

Tout d’abord, et ce sera particulièrement utile pour les bibliothèques de classes / fonctions, un espace de noms peut être défini sur plusieurs fichiers.

Cela signifie que vous pouvez continuer à organiser votre code en le séparant en plusieurs fichiers distincts, tout en regroupant les contenus de ceux-ci au sein d’un même espace de noms.

Par exemple, créons le fichier lib-1.php :

<?php
namespace LIB;

function a() {
    var_dump('LIB\a');
}

Et le fichier lib-2.php :

<?php
namespace LIB;

function b() {
    var_dump('LIB\b');
}

Ces deux fichiers définissent chacun une fonctions, toutes deux regroupées au sein de l’espace de noms LIB.

Nous pouvons ensuite appeler ces fonctions comme nous l’avons fait plus haut, lorsque notre espace de noms ne portait sur le contenu que d’un seul fichier :

<?php
require_once(__DIR__ . '/lib-1.php');
require_once(__DIR__ . '/lib-2.php');

LIB\a();
LIB\b();

Et nous obtiendrons bien en sortie ce à quoi nous pouvions nous attendre, à savoir :

string 'LIB\a' (length=5)
string 'LIB\b' (length=5)

C’est-à-dire le même résultat que lorsque l’espace de noms regroupant nos deux fonctions était défini en un seul fichier.
(Finalement, rien de surprenant ici non plus ^^)


Plusieurs namespaces dans un seul fichier

A l’inverse, il est aussi possible de définir plusieurs espaces de noms au sein d’un seul fichier…

Nous avons dit deux choses, plus haut :

  • Tout ce qui suit la définition d’un espace de nom (instruction namespace) est défini dans cet espace de noms,
  • Et l’instruction namespace doit être la première du fichier.

Et bien… Le second point est vrai : l’instruction namespace doit être la première d’un fichier…
… Mais elle peut aussi se retrouver plus bas dans un fichier — tant qu’il commence déjà par une instruction namespace.

Ce qui signifie qu’il est possible de créer un fichier ayant un contenu de la forme suivante :

<?php

namespace A;
// Ce qui est déclaré maintenant l'est dans le namespace "A"
function a() {
    var_dump('A\a');
}

namespace B;
// Et ce qui est déclaré maintenant l'est dans le namespace "B"
function b() {
    var_dump('B\b');
}

L’utilisation des fonctions définies dans ces deux espaces de noms se fait exactement de la même manière que précédemment — mais nous n’avons plus qu’un seul fichier à inclure :

<?php
require_once(__DIR__ . '/lib.php');

A\a();
B\b();

Et l’affichage obtenu en sortie, encore une fois, est des plus logiques :

string 'A\a' (length=3)
string 'B\b' (length=3)

Il est probable que cette fonctionnalité ne soit pas utilisée « nativement » : pour des raisons évidentes de lisibilité et de facilité de maintenance, vous aurez probablement plutôt tendance à utiliser quelques fichiers de plus (du genre un fichier par classe, comme nous le faisions déjà souvent depuis des années), plutôt que de tout regrouper en un seul…

Cette possibilité a surtout été implémentée pour répondre aux besoins de performance de quelques gros frameworks, qui distribuent parfois une version en un seul fichier regroupant le code de tout le framework — ou, du moins, des parties du framework les plus utilisées — de manière à réduire le nombre de fichiers devant être chargés par PHP.
(Je pense par exemple au Framework ORM Doctrine, qui utilise cette méthode comme optimisation — et ce déjà avec une version qui n’est pas encore basée sur des espaces de noms)


Espaces de noms : noms composés

Jusqu’à présent, nous avons utilisé comme noms pour nos namespaces des libellés du genre « A », « LibA », ou « MyNamespace ».

L’implémentation retenue pour PHP 5 ne permet pas une réelle « imbrication » d’espaces de noms, mais elle permet de la simuler : il est possible d’utiliser le séparateur de namespaces, l’antislash \ , à l’intérieur du nom d’un espace de noms.

Par exemple, nous pourrions créer les trois fichiers suivants :

lib.php :

<?php
namespace Mon\NS;

require_once(__DIR__ . '/lib-A.php');
require_once(__DIR__ . '/lib-B.php');

function myFunction() {
    var_dump(__NAMESPACE__);
}

lib-A.php :

<?php
namespace Mon\NS\A;

function myFunction() {
    var_dump(__NAMESPACE__);
}

Et lib-B.php :

<?php
namespace Mon\NS\B;

function myFunction() {
    var_dump(__NAMESPACE__);
}

Le script principal de notre application contenant alors :

<?php
require_once(__DIR__ . '/lib.php');

Mon\NS\myFunction();
Mon\NS\A\myFunction();
Mon\NS\B\myFunction();

Et nous obtiendrions la sortie écran reproduite ci-dessous :

string 'Mon\NS' (length=6)
string 'Mon\NS\A' (length=8)
string 'Mon\NS\B' (length=8)

En somme, les \ font parti des noms de chacun des trois espaces de noms que nous avons défini, mais ils n’ont pas de signification particulière — du moins en PHP 5.3 ^^
(Cela dit, qui sait si cela n’évoluera pas d’ici les prochaines versions de PHP ?)


Espace de noms global, espace de noms courant

__NAMESPACE__

Pour vous faciliter la tâche, PHP 5.3 introduit une nouvelle constante magique, contenant le nom de l’espace de noms courant : __NAMESPACE__.

Passons tout de suite à un exemple : créons le fichier lib-A.php :

<?php
namespace NamespaceA;

function myFunctionA() {
    var_dump(__NAMESPACE__);
    var_dump(__FUNCTION__);
}

Et le fichier lib-B.php :

<?php
namespace B\C\D;

function otherFunctionB() {
    var_dump(__NAMESPACE__);
    var_dump(__FUNCTION__);
}

Et utilisons les deux méthodes que nous venons de définir, depuis un troisième fichier :

<?php
require_once(__DIR__ . '/lib-A.php');
require_once(__DIR__ . '/lib-B.php');

NamespaceA\myFunctionA();
echo '<hr />';
B\C\D\otherFunctionB();

echo '<hr />';
var_dump(__NAMESPACE__);  // Nous ne sommes pas dans un namespace !

La sortie obtenue sera alors la suivante :

string 'NamespaceA' (length=10)
string 'NamespaceA\myFunctionA' (length=22)

string 'B\C\D' (length=5)
string 'B\C\D\otherFunctionB' (length=20)

string '' (length=0)

A noter, donc :

  • La constante magique __NAMESPACE__ contient le nom de l’espace de nom au sein duquel nous l’écrivons,
  • Les constantes magiques du type __FUNCTION__ nous donnent le nom complètement qualifié des données qu’elles représentent ; dans le cas d’une fonction, on voit que son nom interne commence par le nom de l’espace de noms où elle est définie, pour pouvoir être identifiée de manière sûre et unique.
  • Si nous ne sommes pas dans un espace de noms, la constante magique __NAMESPACE__ aura pour valeur une chaine de caractères vide.


Utiliser namespace pour désigner l’espace de noms courant

Lorsque l’on écrit du code se trouvant dans un espace de nom, on peut appeler des fonctions se trouvant dans le même espace de noms ; par défaut, c’est d’ailleurs ce qui se fait : si vous essayez d’appeler une fonction d’un certain nom, pour peu qu’une fonction de ce nom soit définie au sein de votre espace de noms courant, c’est celle-ci qui sera appelée.

Il est toutefois possible de spécifier explicitement que vous souhaitez ce comportement — ne serait-ce que pour des questions de facilitation de la lecture…
Dans ce cas, vous réutiliserez le mot-clef namespace.

Prenons l’exemple suivant : lib-1.php :

<?php
namespace My\Lib;

function a() {
    // fonction b dans l'espace de noms courant (My\Lib)
    b();
    namespace\b();
    
    // Fonction c dans l'espace de noms My\Lib\Sous\Composant
    Sous\Composant\c();
    namespace\Sous\Composant\c();
}

function b() {
    var_dump(__FUNCTION__);
}

Et lib-2.php, qui simule un espace de noms imbriqué au sein du premier :

<?php
namespace My\Lib\Sous\Composant;

function c() {
    var_dump(__FUNCTION__);
}

Et le fichier principal de l’application, qui lance le tout :

<?php
require_once(__DIR__ . '/lib-1.php');
require_once(__DIR__ . '/lib-2.php');

My\Lib\a();

La sortie obtenue sera la suivante :

string 'My\Lib\b' (length=8)
string 'My\Lib\b' (length=8)

string 'My\Lib\Sous\Composant\c' (length=23)
string 'My\Lib\Sous\Composant\c' (length=23)

En somme, dans un cas comme dans l’autre, que ce soit pour appeler une fonction définie dans l’espace de noms courant ou dans un espace de noms « enfant » de l’espace de noms courant, nous avons deux possibilités :

  • Soit nous utilisons directement le nom de la fonction, relativement à l’espace de noms au sein duquel nous nous trouvons lorsque nous écrivons le code,
  • Soit nous précisons explicitement que nous travaillons relativement à celui-ci, en employant le mot-clef namespace.

Dans un cas comme dans l’autre, ici, puisque les fonctions appelées sont toutes existantes, le résultat sera le même.

Nous verrons plus tard, lorsque nous nous pencherons plus en détails sur les règles de résolution et sur l’autoloading, que l’utilisation du mot-clef namespace dans cette situation peut avoir un impact… Mais au moins, nous savons déjà que, en termes de syntaxe, c’est chose possible ^^


Faire appel à un élément du namespace global

Tout le code que vous ne placez pas explicitement au sein d’un espace de noms est — ce qui revient au même que ce que nous obtenions en PHP < 5.3, lorsque nous n’avions pas de namespace — automatiquement placé dans l’espace de noms global.

Il en vas de même pour les classes, fonctions, et constantes définies par PHP.

Cet espace de noms global est nommé '\', comme vous avez pu vous en douter lorsque j’ai dit que NAMESPACE valait chaine vide lorsqu’on l’utilisait en dehors de tout namespace — ce qui revient en fait à l’utiliser depuis l’espace de noms global.

Lorsque vous vous trouvez dans un espace de noms au sein duquel vous choisissez de redéfinir une fonction globale, celle-ci est masquée par votre fonction… Mais vous pouvez toujours l’appeler, simplement en la préfixant du nom de son espace de nom, avec une syntaxe de ce genre : \fonction_globale(...)

Par exemple, définissons l’espace de noms suivant, au sein duquel nous redéfinissons la fonction strlen :

<?php
namespace NS_A;

function strlen($str) {
    return 'glop';
}

function test() {
    var_dump(strlen('Hello, World!'));    // NS_A::strlen  (NS_A est l'espace de noms courant)
    var_dump(namespace\strlen('Hello, World!'));    // NS_A::strlen  (NS_A est l'espace de noms courant)
    var_dump(\strlen('Hello, World!'));  // espace de noms global
    
    //var_dump(NS_A\strlen('Hello, World!'));    // Fatal error: Call to undefined function NS_A\NS_A\strlen()
}

Et utilisons ce code à l’aide des instructions suivantes, placées dans un autre fichier, incluant le premier :

<?php
require_once(__DIR__ . '/lib.php');

NS_A\test();

La sortie sera alors la suivante :

string 'glop' (length=4)
string 'glop' (length=4)
int 13

Lorsque nous sommes dans l’espace de noms où la fonction strlen est redéfinie, c’est notre version de la fonction qui est appelée, tant que nous ne spécifions pas explicitement que nous voulons travailler avec la version se trouvant dans l’espace de noms global.

C’est pour cela que seul le troisième appel renvoit 13, la véritable longueur de notre chaine !

Nous en verrons un peu plus à ce sujet demain, en seconde partie de cet article…


Espaces de noms et constantes

Nous avons vu, jusqu’à présent, que nous pouvions utiliser la nouvelle fonctionnalité d’espaces de noms introduite en PHP 5.3 pour regrouper des fonctions…

Et bien, il est aussi possible de placer des constantes au seins d’espaces de noms, exactement comme nous l’avons fait jusqu’à présent pour les fonctions.

Par exemple, nous pouvons tout à fait envisager de créer un fichier lib.php contenant le code suivant :

<?php
namespace LIB;

const CONSTANTE_A = 'ma constante A dans le namespace LIB';

function test()
{
    var_dump(CONSTANTE_A);
}

Et de l’utiliser à partir d’un autre fichier, dont le contenu sera celui-ci :

<?php
require_once(__DIR__ . '/lib.php');

const CONSTANTE_A = 'ma constante A hors namespace';

var_dump(CONSTANTE_A);

var_dump(LIB\CONSTANTE_A);

LIB\test();

L’affichage obtenu en sortie serait alors le suivant :

string 'ma constante A hors namespace' (length=29)
string 'ma constante A dans le namespace LIB' (length=36)
string 'ma constante A dans le namespace LIB' (length=36)

Pour le premier affichage, nous ne sommes pas dans l’espace de noms LIB ; c’est dont la constante en version globale qui est utilisée.

Pour les deux autres cas, soit nous spécifions explicitement que nous souhaitons travailler avec la version de la constante « namespacée », soit nous nous trouvons dans l’espace de nom — auquel cas les règles de résolution sont telles que c’est la version la plus « proche » de nous, celle située dans l’espace de noms où nous nous trouvons lorsque nous écrivons le code, qui est utilisée.


Espaces de noms et classes

Nous avons jusqu’à présent vu que les espaces de noms introduits en PHP 5.3 permettaient de regrouper des fonctions et des constantes… La notion de namespaces étant une notion plutôt orientée « objet » dans le concept, et nos développements se plaçant de plus en plus dans un contexte objet, c’est en toute logique que les espaces de noms permettent aussi de regrouper des classes !

Le principe est exactement le même que celui que nous avons vu pour les fonctions : à partir du moment où l’on utilise le mot-clef namespace au début d’un fichier, tout ce qui suit est déclaré dans l’espace de noms — y compris les classes.

Prenons pour exemple le fichier lib-A.php reproduit ci-dessous :

<?php
namespace NS_A;

class ClassA {
    public function method() {
        var_dump(__NAMESPACE__ . ' \ ' . __CLASS__ . ' \ ' . __FUNCTION__);
    }
}

Et le fichier lib-B.php :

<?php
namespace NS_B;

class ClassB {
    public static function method() {
        var_dump(__METHOD__ );
    }
}

Ces fichiers sont tous deux inclus par le fichier principal de notre application :

<?php
require_once(__DIR__ . '/lib-A.php');
require_once(__DIR__ . '/lib-B.php');

$a = new NS_A\ClassA();
$a->method();

NS_B\ClassB::method();

Le résultat obtenu à l’exécution de ce script sera le suivant :

string 'NS_A \ NS_A\ClassA \ method' (length=27)
string 'NS_B\ClassB::method' (length=19)

Quelques points peuvent être notés, ici :

  • Il est parfaitement possible — et c’est heureux — de placer des classes dans des espaces de noms
  • Le nom interne (« fully qualified ») de la classe commence par le nom du namespace au sein duquel elle se trouve
  • Il en est de même pour les noms de méthodes, accessible à l’aide de la constante magique __METHOD__, qui reprennent de toute manière le nom de leur classe.


Nous voici arrivés à la fin de la première partie de cet article sur les espaces de noms.

La seconde partie, que je mettrai en ligne demain (mardi 25/11) matin nous permettra de traiter, en particulier, des points suivants :

  • Autoloading
  • Type-hinting
  • Utilisation du met-clef use pour importer un namespace
  • Quelques mots sur les règles de résolution
  • Quelques points qui n’ont pas été retenus pour l’implémentation des espaces de noms en PHP 5.3

A demain, donc !


Notes

[1] Et à ce sujet, puisque les claviers AZERTY que nous autres français utilisons de sont pas les plus répandus au sein de la communauté, je vous laisse imaginer le résultat — ça ne nous sera pas forcément bénéfique :-(

[2] Notez que je n’entrerai pas dans le Troll à propos du choix de \ comme séparateur de namespaces : ce choix présente à la fois des avantages et des inconvénients, c’est certain — mais ce choix est fait, et une fois la poussière un peu retombée, on s’y fera !

Vous avez apprécié cet article ? Faites-le savoir !

Ce blog a récemment été migré vers un générateur de sites statiques et je n'ai pas encore eu le temps de remettre un mécanisme de commentaires en place.

Avec un peu de chance, je parviendrai à m'en occuper d'ici quelques semaines ;-)