PHP et Drupal, comment retrouver son chemin ?

Un problème récurent en PHP est de trouver le bon chemin qui mène à la bonne ressource. Qui ne s'est jamais demandé comment inclure le fichier toto.inc qui se trouve pourtant si près et pourquoi une fois que ça marche, tout se casse la figure dés que l'on touche à Apache... Et avec Drupal, le casse-tête prend une dimension de plus avec les modules, les thèmes, les fichiers attachés, les fichiers temporaires, etc. L'objectif de ce tutorial vise donc simplement à éviter de paumer ses petits.

Se repérer du côté serveur

Le dossier de travail d'un script PHP

Le dossier de travail d'un script PHP est l'endroit où il se trouve, physiquement. Donc, quelle que soit l'URL, on exécute le script se trouvant en /var/www/scritps/index.php, et que l'on cherche à y ouvrir le fichier qui lui se trouve dans /var/www/scritps/utilitaires/fichier.txt, cela nous donne :

fopen ("utilitaires/fichier.txt");
...
/var/www/scritps/index.php

Maintenant, si l'on effectue la même opération dans un script scripts/utilitaires/utilitaire.inc qui est inclus (directive include ou require) par index.php, le chemin sera... exactement le même. En effet, le dossier de travail de tous les fichiers inclus reste le même que celui du script principal. On peut s'en convaincre en utilisant la commande getcwd() qui retourne le dossier de travail :

print "Répertoire de travail : ".getcwd();
fopen ("utilitaires/fichier.txt");
...
/var/www/utilitaires/utilitaire.inc

Le dossier d'un fichier PHP

La syntaxe précédente est tout sauf une bonne solution, surtout si l'on déplace ou renomme jour utilitaires. Il y a heureusement un moyen d'éviter ce problème en utilisant la macro PHP __FILE__ qui renvoiele nom de fichier complet (dossiers compris) du script qui la contient. Utilisé conjointement avec la fonction dirname, cette macro permet donc d'ouvrir plus naturellement notre fichier, devenant ainsi indépendant du nom et de l'emplacement final du dossier utilitaire.

print "Dossier de ce script : ".dirname(__FILE__);
fopen(dirname(__FILE__)."/fichier.txt", "r");
/var/www/utilitaires/utilitaire.inc

Les spécificités de l'inclusion (include et require)

Même si tout ce qui s'applique à un script principal s'applique aussi fichiers inclus, ces derniers ont de petites spécificités amusantes qu'il est intéressant de noter.

Par défaut, l'inclusion se fait en cherchant dans la liste de chemins contenus dans la variable include_path présente dans le fichier /etc/php.ini. Cette liste fonctionne un peu comme la variable PATH du système d'exploitation, PHP cherche de manière séquentielle les fichiers demandés dans cette liste de dossiers en commençant par le premier.

Généralement cette liste contient d'abord le dossier de travail représenté par un ., puis, selon les installations, une série de chemins vers des librairies (pear, librairies standard, etc...).

La présence du . en tête de liste implique un fonctionnement proche de ce que nous avons déjà vu plus haut avec fopen. Imaginons que le script a.php inclus b.inc qui inclus à son tour c.inc. Nous savons déjà que toute inclusion se trouvant dans c.inc sera recherchée dans les dossier de a.php et de c.inc. Mais, petite subtilité, il sera aussi cherché dans le dossier de b.inc.

La commande include (ou require) se comporte donc comme si à chaque appel, elle ajoutait dans include_path le dossier du script inclus. Comportement aussi pratique que dangereux car on ne sait plus bien où l'on en est au bout d'un temps, ni qui a la priorité sur qui. Sachant donc cela, le mieux reste à mon sens de coller à la logique générale, en oubliant se comportement particulier.

Le dossier d'un module Drupal

D'un point de vue général, le dossier de travail de drupal est celui où se trouve le fichier index.php. Ainsi tout ce qui a été dit plus haut fonctionne logiquement avec les scripts contenus dans Drupal, que ce soit pour le script principal, mais aussi pour les modules, thèmes et autre, qui ne sont que des include aux yeux de PHP.

La différence est que pour un module, le dossier final ne peut être prévu à l'avance. Il peut tout aussi bien se trouver dans drupal/modules que par exemple dans drupal/site/all/modules. Il est donc impossible de se baser sur un chemin en dur pour accéder aux ressources (include, fichiers textes, etc...). Et c'est encore plus vrai lorsque l'on doit référencer des éléments se trouvant dans un autre module.

Pour ces raisons, il est préconisé d'utiliser plutôt la fonction Drupal prévue à cet effet, drupal_get_path(...) qui permet de localiser précisément n'importe quel module :

$module_path=drupal_get_path('module', 'mon_module');
fopen($module_path."/fichier.txt");
...
mon_module.module

Il est donc souhaitable d'utiliser cette fonction pour inclure dans la page finale les scripts et autres feuilles de style :

// Récupération du dossier du module
$module_path=drupal_get_path('module', 'mon_module');

// Association d'un script contenu mon_module/scripts/script.js
drupal_add_js($path . '/scripts/script.js');

// Association d'une feuille de style contenu mon_module/styles/style.js
drupal_add_css($path . '/styles/style.js');
mon_module.module

A noter que cette fonction ne renvoie pas un chemin absolu (comme _FILE) mais un chemin relative à la racine de Drupal (le répertorie de travail d'index.php), sans / ni au début, ni à la fin. Mais nous allons voir plus loin comment retrouver le dossier d'installation de Drupal dans le cas où nous aurions besoin d'un chemin complet.

Le dossier du thème courant

Encore plus que celui du module, il est très utile de connaître le chemin vers le thème courant, utilisable par exemple dans les templates. Pour ce faire, nous avons deux options :

// De manière générale, en utilisant la même méthode que pour le module mais en la
// paramétrant cette fois pour un thème
$theme_path=drupal_get_path('theme', 'mon_theme');

// Plus portable, en récupérant le chemin du thème courant
$theme_path=path_to_theme();
mon_template.inc.tpl

A partir de là, comme pour le module, il est possible d'accéder à toutes les ressources du thème (images, styles, etc..) avec des chemins relatifs.

Dossier des fichiers uploadés

Pour accéder au dossier files, seul emplacement avec le dossier temporaire où il est possible de créer des dossiers et d'écrire des données, vous pouvez faire appel à la fonction file_directory_path() :

// ouverture en écriture d'un fichiers dans le dossier "files"
fopen (file_directory_path()."/donnes.txt", "w");
...
mon_module.module

Comme path_to_theme() renvoie un chemin relatif au dossier de base de Drupal, débarrassé du slash de début et de fin.

Dossier des fichiers temporaires

Pour accéder cette fois au dossier temporaire, vous pouvez faire appel à la fonction file_directory_temp() :

// ouverture en écriture d'un fichiers dans le dossier temporaire.
fopen (file_directory_temp()."/log.txt", "w");
...
mon_module.module

Contrairement aux fonctions précédentes, file_directory_temp revoie un chemin absolu sans slash final (ex. /tmp).

Du côté client

La quête du chemin de base

Les pages générées par Drupal, prises cette fois du côté client WEB (firefox), contiennent sans aucun doute des références à des images et autres feuilles de styles par le biais d'URL. Comme nous l'avons vu plus haut, une partie de ces ressources (feuilles de style, scriptes) sont prisent en charge directement par Drupal avec la fonction drupal_add_css par exemple. En revanche c'est plus compliqué lorsqu'il s'agit, par exemple, de faire référence à une image contenue dans un module.

En effet, et c'est aussi vrai en simple PHP, l'URL qui est utilisée par le navigateur pour atteindre le script index.php contient un chemin qui peut être sans rapport avec le chemin physique du script. Cette différence tenant à la manière dont a été configuré l'ami Apache.

Par exemple, imaginons que noter site drupal est accessible par l'URL http://mon_site/drupal et que je veuille utiliser l'image photo.png contenue dans le dossier de mon_module, j'aurais tendance à écrire cela :

// Récupération du dossier du module
$module_path=drupal_get_path('module', 'mon_module');

// Référence à notre image
print "<img src='/$module_path/mon_image'/>";
mon_module.php

Alors comme cela, ça a l'air de marcher, et ça va marcher dans un seul cas, si l'URL de base de drupal est "simplement" http://mon_drupal/. En revanche cela va planter si l'url ressemble à quelque chose comme http://mon_site/drupal. Dans ce cas, la ressource va être recherchée au mauvais endroit, ou pire, dans un endroit qui n'existe pas.

La solution évidente semble être de passer en notation relative, en enlevant le premier slash. Et effectivement, cela va régler notre problème précédent mais il nous en reste un : les URL simplifiées.

En effet, si vous laissez Drupal configuré par défaut avec des URL du type http://mon_site/drupal/?q=node/12 vous n'aurez pas de problème. En revanche si vous activez la simplification d'URL (clean URL), cela devient http://mon_site/drupal/node/12. Et là les ennuis recommencent car en notation relative, vous allez demander au navigateur d'aller chercher votre image en /drupal/node/12 et non pas en /drupal.

Pour accéder de manière universelle à une ressource d'un module, il faut donc commencer par récupérer le /drupal/ quelque part. Et ce "quelque part" c'est le fichier settings.php, et plus précisément la variable globale $base_url qui contient l'URL de base de Drupal, soit dans notre exemple http://mon_site/drupal (sans / final). Le code de l'exemple précédent se corriger donc comme suit :

// Récupération du dossier du module
$module_path=drupal_get_path('module', 'mon_module');

// Déclaration de la variable globale $base_url
global $base_url

// ouverture en écriture d'un fichiers dans le dossier temporaire.
print "<img src='$base_url/$module_path/mon_image'/>";
mon_module.php

Cette solution marche très bien mais souffre d'un dernier inconvénient, celui d'inscrire dans le code HTML généré une URL absolue. Ce n'est pas très gênant, sauf si l'on a 1000 images impliquant une augmentation inutile de la taille du fichier.

Une bien meilleurs solution est donc d'utiliser cette fois la fonction base_path() qui ne renvoie que la partie correspondant au chemin de la variable $base_url (ex. drupal pour http://mon_site/drupal. Cela nous donne le code final : Ainsi nous pouvons réécrire notre exemple précédent :

// Récupération du dossier du module
$module_path=drupal_get_path('module', 'mon_module');

// ouverture en écriture d'un fichiers dans le dossier temporaire.
print "<img src='".base_url()."/$module_path/mon_image'/>";
mon_module.php

Maintenant l'URL de notre image est relative et fonctionne dans tous les cas de figure. Bien évidement la même méthode peut être appliquée pour une ressource relative à un thème ou au dossier files.

Les menus Drupal

Contrairement à ce que l'on imagine spontanément, un menu Drupal est avant tout une URL qui référence un traitement effectué par un module. A ce titre, /node/12 est un menu. Les menus "qui s'affichent sur le côté" ne sont du coup qu'un cas particulier d'un menu Drupal.

Très souvent dans un module, du code est généré pour faire référence à un menu drupal. Par exemple imaginons que nous désirions générer une page contenant le lien vers le menu node/12. La mauvaise méthode consisterait à écrire quelque chose comme cela :

print "<a href='/node/12'>Affiche la page 12</a>";
mon_module.php

Mauvaise pour les mêmes raisons que celles évoquées plus haut car si l'url contient un dossier en plus (http://mon_site/drupal), le menu devient alors http://mon_site/node/12 et cela ne marchera plus.

Drupal connaît "à l'avance" tous les menus possible car chaque module qui en donne, a le devoir de les faire enregistrer pour qu'ils soient utilisables. Il existe du coup une méthode magique qui permet de transformer un menu (ex. /node/12) en son équivalent URL, il s'agit de la fonction url(). En l'utilisant, notre code se transforme ainsi :

print "<a href='".url("node/12")."'>Affiche la page 12</a>";
mon_module.php

Cette méthode a plus d'un mérite. Tout d'abord elle va utiliser base_url() et gérer l'éventuelle absence de simplification d'url en mettant un ?q= si besoin est. De plus, si vous avec activé le module d'alias (aka path), elle va traduire node/12 en une-belle-url. Enfin elle renvoie une URL relative (donc plus courte) mais peut aussi créer une URL absolue en ajoutant des paramètres : url("node/12", null, null, true). Pour plus d'informations sur cette fonction incontournable, allez faire un tour ici.

Pour simplifier encore le travail, il existe une autre fonction qui fait appel à url() et qui permet de générer l'ensemble du lien. Avec son aide, notre code devient tout simplement :

print l("Administration des modules", "node/12");

Les feuilles de style

Un autre moyen de référencer une ressource comme typiquement une image est de l'inclure dans une définition CSS. L'avantage de cette approche est que cette fois le chemin de la ressource n'est pas relatif à quoi que ce soit d'autre que la feuille de style elle-même. Ainsi si nous avons une feuille de style dans un dossier XXX, une image background.png dans le sous-dossier XXX/images, le référencement de cette image se fait tout simplement :

body {
  background-image:url("images/background.png");
}
ma_feuille.css

Conclusion

Voilà, c'est la fin de cette session de repérage dans l'arborescence d'une application WEB. Si vous voulez avoir un résumé de toutes ces fonctions en un coup d'oeil, vous pouvez créer un module simple, un menu avec un callback sur la fonction suivante :

function afficher_tous_les_chemins()
{
    global $base_url;
    $output="<pre>";
    $output.="$_SERVER["PHP_SELF"]                    : ".$_SERVER["PHP_SELF"]."";
    $output.="getcwd()                                : ".getcwd()."";
    $output.="$base_url                               : ".$base_url."";
    $output.="";
    $output.="dirname(__FILE__)                       : ".dirname(__FILE__)."";
    $output.="drupal_get_path('module','mon_module')  : ".drupal_get_path('module', 'hermes_wiki')."";
    $output.="";
    $output.="path_to_theme()                         : ".path_to_theme()."";
    $output.="path_to_engine()                        : ".path_to_engine()."";
    $output.="file_directory_path()                   : ".file_directory_path()."";
    $output.="$base_url                              : ".$base_url."";
    $output.="base_path()                              : ".base_path()."";
    $output.="</pre>";
    return $output;
}
mon_module.module

Vus : 1124
Publié par arNuméral : 54