Lab : Best Practice PHP : chargement dynamique objets
Deux étapes : chargement explicite et ensuite chargement implicite via le "magic autoload"
Chargement explicite de package ou d'objet
Après avoir développé dans d'autres langages, on se rend vite compte que les include_once & Co. sont peu fonctionnels, un peu brouillons et n'incitent guère à avoir un structure propre des fichiers. Aussi je me suis inspiré de la gestion des objets java & Actionscript (3) pour créer ma fonction import en php. Au début cette fonction était là juste pour éviter les include_once ou require_once qui augmentent sensiblement le temps d'exécution de chargement et aussi gérer plus vite un problème de chargement d'objet dans un projet (alerte par mail).
Avant de développer ce petit outils, j'intégrais mes objets de la façon suivante (comme à peu près tout le monde) :
<?php
//Exemple de chargement de mon connecteur db "universel" (dbc) utilisant mon connecteur mysql pour ses requêtes.
//Chargement de la classe dont Dbc est l'extension
require_once('mon_dossier_principal/sites/be/myconcept/classes/php_lab/bases/EventDispatcher.php');
//Chargement connecteur mysql utilisé par Dbc
require_once('mon_dossier_principal/sites/be/myconcept/classes/php_lab/net/dbc/connectors/MySQL_DBC.php');
//Chargement du connecteur (l'adapteur) "universel"
require_once('mon_dossier_principal/sites/be/myconcept/classes/php_lab/net/dbc/DBC.php');
$db_connector=new DBC::factory(Registry::$database);
? >
Mais suite à mes benchs, j'ai vite remplacé les _once par de simple require avec une condition 'class_exists' mais ça restait fort brouillon et deplus les copies de ces lignes apparaissaient dans les fichiers des objets ayant besoin d'un héritage.
Donc multiplication des lignes de codes, inclusions répétées évitées par le '_once', appels inutiles à certains fonctions, risque de structure 'anarchique' des objets d'un projet à l'autre,... Il fallait appeler ces classes plus proprement et donner/forcer une structure de dossiers afin de rapidement retrouver ses jeunes d'un projet à un autre d'où la création de ma méthode import.
<?php
/**
* Chargement des objets
* @param string $class L'emplacement de l\objet
* @trigger E_USER_ERROR Si le fichier n'existe pas
* @return boolean Si l'import a été effectué ou pas
* @copyright Copyright © 2006 - Yannick Molitor
* @license MIT Licence > http://fr.wikipedia.org/wiki/Licence_X11
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
**/
function import($class){
if(substr($class,-1)=="*"){
package(substr($class,0,-2));
}
else {
//Ici on fait le contrôle pour voir si l'objet n'a pas été chargé
if(!isset(Registry::$loaded_classes[$class])){
$file=str_replace(".","/",$class).".php";
if(file_exists(Registry::ClassPath.$file)){
include(Registry::ClassPath.$file);
Registry::$loaded_classes[$class]=true;
return true;
}
else{
trigger_error($class.' Object File not found',E_USER_ERROR);
return false;
}
}
else {
return false;
}
}
}
/**
* Chargement récursif si l'on souhaite charger un dossier d'objets
* @param string $package le nom du package à inclure
* @return void
**/
function package($package){
$path=str_replace(".","/",$package);
if ($dh = opendir(Registry::ClassPath.$path)) {
while (($file = readdir($dh)) !== false) {
if(strpos($file,".php")){
$class=$package.".".substr($file,0,-4);
import($class);
}
}
}
closedir($dh);
}
? >
Avec ces outils je peux donc inclure une série d'objet ou un objet seul de manière élégante et avec une gestion des erreurs que je peux paramétrer :
<?php
//Ex de chargement de tous les objets de bases dont la plupart des objets sont dérivés
import("be.myconcept.phplab.bases.*");
//Ex de chargement de tous les objets outils nécessaires pour le debug (logger, benchmarker,...)
import("be.myconcept.phplab.tools.debug.*");
//Reprise du premier exemple > chargement du connecteur db universel et de son connecteur mysql
import("be.myconcept.phplab.net.dbc.connectors.MySQL_DBC");
import("be.myconcept.phplab.net.dbc.DBC");
$db_connector=new DBC::factory(Registry::$database);
? >
A ce niveau, le code est déjà plus propre, plus rapide, les erreurs de chargement mieux gérées et la structure de dossier par projet est imposée. Il reste cependant encore un souci : il faut taper ces quelques lignes avant de pouvoir instancier l'objet d'où la suite de cette article
.
Chargement dynamique des fichiers contenant les classes via la fonction magique __autoload de php
PHP nous offre la possibilité lors de l'appel d'un Objet ou lors de son instanciation d'appeler implicitement une fonction __autoload si la classe n'a pas encore été chargée.
Cette fonction fait partie des "Magic Functions" de PHP (cf. php.net pr plus d'infos).
Bon avec cette fonction je vois deux possibilités : instancier l'objet en mettant tout le chemin lors de l'instanciation du style :
<?php
$test=new be.myconcept.php_lab.foobar.Toto("hello","world");
? >
Mais cette solution peut lisible ne gérait pas directement le problème du chargement des classes connexes à celle appelée (parents, objets utilisés,...) donc on oublie.
Une autre solution était de stocker les emplacements de chaque classe dans un fichier de données et d'y faire référence lors de l'instanciation d'un objet :
<?php
$test=new Toto("hello","world");
? >
J'ai choisi la seconde solution pour plusieurs raisons :
- Meilleure lisibilité;
- Déplacement d'un package plus facile -> si le package debug devient un sous package de tools suite au refactoring d'un projet, il est plus facile de changer la référence à un endroit plutôt que de devoir se taper toutes les pages de code de tous les projets liés pour faire l'update;
- Quand on travaille avec des classes écrites de plusieurs endroits/projets différents (ex : be.rcnsm..., net.php.pear..., org.phpclasses..., be.monautreprojet.tools...) quand on fait un release du projet il faut aussi sauver tous les objets liés au projet et il faut donc une liste des objets à prendre en compte.
<?php
/**
* Chargement automatique du fichier pour l'instanciatio d' un objet
* @param string $class Le nom de la classe
* @trigger E_USER_ERROR Si l'emplacement de l'objet n'est pas défini
* @return void
**/
function __autoload($class){
//Recherche l'emplacement du fichier ds le registre et si ok l'importe
if(isset(Registry::$object_list[$class])){
import(Registry::$object_list[$class]);
}
else {
trigger_error($class.' Object Not Defined - Please Retry Later',E_USER_ERROR);
}
}
//Avec dans l'objet registre une liste du type :
Registry::$object_list["BenchMark"] = "be.myconcept.phplab.debug.benchmarker";
Registry::$object_list["Logger"] = "be.myconcept.phplab.debug.Logger";
Registry::$object_list["Library"] = "be.myconcept.phplab.optimizers.lib_manager";
Registry::$object_list["Event"] = "be.myconcept.phplab.events.Event";
Registry::$object_list["EventDispatcher"] = "be.myconcept.phplab.events.EventDispatcher";
...
? >
Et donc après avoir ajouter la liste des emplacements pour chaque objet voici à quoi ressemble mon code (celui du tout premier exemple) :
<?php
$db_connector=new DBC::factory(Registry::$database);
? >
Voilà voilà ! Donc au finish quelques avantages non négligeables :
- plus de vilains include_once ou require_once qui ralentisent le code;
- plus nécessaire de se demander si on a déjà chargé l'objet quelque part en amont ds le code;
- pour les schtroumpfs qui mettaient des @include ou @require qui sont une horreur en terme de performances en PHP et de développement proactif en général (ce n'est pas en cachant une erreur qu'elle disparaitra toute seule) fini de ralentir son code il vous suffira juste de ne pas faire de else en cas d'échec...
- plus de prise de tête pour retrouver le fichier d'un objet;
- faciliter de refactoring si on bosse sur plusieurs projets (plus la contrainte de tout passer en revue après avoir déplacer une classe ou un package);
- faciliter pour retrouver/archiver les dépendances d'un release de projet;
- on gère directement l'erreur de chargement d'un fichier et on peut réagir en fonction;
- rapidité d'exécution mais surtout de dev car moins de code à taper;
- on peut taper un service de stats pour, par exemple, savoir quels sont les objets les plus utilisés afin d'éventuellement voir comment optimiser les scripts les plus courants en priorité, etc...
- beaucoup plus joli visuellement ce qui confirme ce que disait Mies Van der Rohe : "Less is More" (moins c'est mieux). Comme quoi le terme d'Architecte en développement a une certaine cohérence avec celui de l'Architecte traditionnel (immobilier)
