Home > PHP > Coder une classe d’abstraction pour manipuler des objets en PHP5

Coder une classe d’abstraction pour manipuler des objets en PHP5

Sur chaque site, dans tous les projets, on a une base de données qui contient des tables, dont chaque ligne sont des items que l’on souhaitera manipuler à un moment où à un autre. Le code PHP à écrire pour effectuer les actions nécessaires – Ajouter des items, les modifier, les supprimer, récupérer leurs attributs… – varie peu, et pourtant ce travail est souvent répété d’un projet à l’autre. Il est possible de créer une classe générique d’abstraction, qui pourra manipuler toutes sortes d’objets de manière transparente.

Note: On m’a souvent déconseillé d’utiliser l’introspection (et donc notamment la méthode __get() ) pour des problèmes de performances. Or, et malheureusement, il est de nos jours nécessaire de se rappeler que l’hébergement d’un site, même si ses coûts pourraient être réduits par de l’optimisation poussée/poussive sur le code, coûtera bien moins cher qu’un développeur qualifié, à plein temps, occupé à maintenir du code “optimisé” mais dont on pourrait aisément se passer en achetant un serveur plus puissant. J’utilise donc __get et si ça vous choque, louez un serveur dédié (et en plus je me permets un lien d’affiliation…) et arrêtez de vous demander si vous devez utiliser echo ou print… Ce n’est pas là que sera votre valeur ajoutée en tant que développeur. Il est largement plus rentable à moyen et long terme de faire du code modulaire, réutilisable, en capitalisant sur les temps de développement.

Magic function : __get

Dans la programmation orientée objets en php5, il existe des méthodes de classes qui sont dites “magiques”. La plus célèbre, __construct(), est appélée à l’instanciation de la classe.
Une autre, moins connue, est appelée lorsqu’on tente d’accéder à une propriété qui n’est pas définie. C’est __get(). Cette méthode reçoit un argument, qui est le nom de la propriété en question. C’est celle-ci que nous allons utiliser pour réaliser notre classe d’abstraction. Pour l’exemple, nous assumerons que nous avons une variable globale (oui, je sais, encore quelque chose que je ne devrais pas faire… mais j’attends toujours qu’on m’explique pourquoi) $oDb qui est un objet Database capable de renvoyer la valeur d’une ligne (getRow) d’une table de la base de données.

Comme d’habitude, on va gérer une table Employes contenant des champs ID, nom, prenom et salaire (respectivement int, varchar, varchar et int…) :

  1. class GenericObject {
  2.     public function __get($var) {
  3.         print "Magic : get $var";
  4.     }
  5. }
  6. $emp = new GenericObject;
  7. print $emp->nom;

Comme on le voit, on essaye à la ligne 7 d’accéder à la propriété “nom”, alors que celle-ci n’existe pas. La méthode __get() est alors appelée et reçoit en argument le nom de la propriété inexistante (le script affichera donc bien Magic: get nom.

Dès lors, on peut imaginer construire une classe acceptant un nom de table (string) et un identifiant (integer) en argument et pouvant, déjà, rappatrier la valeur de n’importe quel champ. Imaginons le script suivant :

  1. class GenericObject {
  2.     public function __construct($table, $id) {
  3.         global $db;
  4.         // on charge la ligne de la base de données
  5.         $this->infos = $db->getRow("select * from $table where id = ‘$id’");
  6.     }
  7.     public function __get($var) {
  8.         // on renvoie la valeur tirée de la valeur (tableau) de la propriété $this->infos
  9.         return $this->infos[$var];
  10.     }
  11. }
  12. $emp = new GenericObject(‘emp’,1);
  13. var_dump(isset($emp->nom));
  14. print $emp->nom;

Ici, la classe est instanciée dans l’objet $emp. On lui passe l’ID (en base) de notre employé fictif. Dès lors, on peut, en une ligne, récupérer la totalité des informations le concernant. Pour preuve, la ligne 14 affiche bien “Dupont”. A noter, la ligne 13 qui renvoie bool(false) : $nom c’est vraiment pas défini avant le get.

Magic function : __set()

Bien sur, parralèlement à __get, on peut utiliser la méthode __set() pour changer la valeur d’une propriété qui n’est pas définie. Cette méthode accepte, cette fois, deux paramètres: le nom de la propriété en question et la valeur que l’on souhaite lui affecter. L’intérêt dans ce cas (pour différer du comportement “normal” de PHP, est, par exemple, de récupérer la liste des champs de la base dans une propriété, et de vérifier à chaque __set que la variable qu’on tente de déclarer aura bien une correspondance dans la table de la base de données.

Magic function : __destruct()

Quitte à utiliser des fonctions “magiques”, autant le faire jusqu’au bout. On peut, grâce à la méthode __destruct(), effectuer une action lorsque l’objet sera désinstancié. Cela peut se passer si on le unset, ou lorsque le script se terminera. Par exemple, pourquoi ne pas enregistrer les changements de notre objet dans la base de données ? Dans notre exemple, il suffit de faire une boucle foreach sur le tableau $this->infos et de formater une requête SQL UPDATE.

Bon, certes, j’ai pris pas mal de raccourcis, donc le code n’est pas vraiment exploitable “tel quel”, mais avouez que la classe est efficace dans le sens où elle peut gérer toutes sortes d’objets. Il faudrait notamment ajouter, avec __set, le système qui vérifie que le champ existera dans la table (en récupérant son type pour faire des contrôles avant d’insérer). Dans le __destruct, il faut faire en sorte de ne sauver que les champs ayant été modifiés. Et bien sur, éviter l’édition de l’ID sur un objet que l’on vient de charger. Tout un programme ;)
On pourrait par exemple utiliser ce système pour gérer les utilisateurs d’un site, avec gestion des droits en bit bashing.

On pourrait coder par-dessus tout ça un système d’itérateurs pour les gérer en lots. (et effectuer simplement des tâches comme “Afficher tous les employés”, “Augmenter tout le monde de 100 €”, etc).

Je tiens mon “work in progress” (WIP©) de la classe à disposition pour qui voudra, n’hésitez pas à me le demander dans les commentaires ou par mail. :)

Ce post vous a été utile ? Re-Twittez le ! ReTwittez ce post

PHP , , ,

  1. | #1

    Très bonne idée, cela dit, c’est un peu déroutant d’un point de vue conceptualisation.
    Mais efficace ;)

  2. | #2

    Salut,

    Je ne suis pas tout à fait d’accord avec ce tutoriel bien qu’il soit bien expliqué.

    Je commence concernant la variable globale d’accès à la DB. A mon sens, déclarer une variable comme globale dans une classe (ou une fonction) oblige à la déclarer quelque part ailleurs en dehors de la classe, ce qui est loin d’être maintenable, évolutif et modulaire (comme vous l’évoquiez au sujet de l’optimisation abusive). Une meilleure solution, serait certainement de stocker cette instance dans une variable superglobale du tableau $GLOBALS. Ou encore mieux, d’utiliser par exemple un singleton (ou multiton si l’on veut gérer plusieurs connexions) à l’intérieur de la classe. Par exemple :

    class GenericObject
    {
    protected $_infos = array();

    public function save()
    {
    $oDb = DatabaseManager::getInstance();
    $oDb->query(‘INSERT INTO table (champ) VALUES(“toto”)’);
    }

    }

    Ainsi, on ne se trimballe plus une variable globale mais une instance unique de l’objet d’accès à la DB que l’on manipule à travers une classe.

    Concernant les performances, j’aimerai que vous nous éclairiez car je ne vois pas en quoi _get(), __set() ou encore les APIs “Reflexion” posent problème au niveau des performances. Je n’ai jamais entendu à ce jour que l’utilisation de ces APIs puisse mettre à mal un serveur mutualisé… Et puis avec des caches d’opcodes on compenses grandement les pertes de performances car les scripts ne sont plus réinterprêtés et précompilés à chaque requête.

    Enfin, il ne faut pas oublier de mentionner que malgré leur côté très pratique, ces deux méthodes magiques ne permettent pas de générer de la documentation technique du code, ni de permettre l’auto-complétion dans les IDE tels qu’Eclipse. Donc je suis personnellement très sceptique à les utiliser et je préfère de loin me taper à la main l’écriture des getters et setters de mes classes qui me permettent :

    1/ De pouvoir générer de la documentation technique du code
    2/ De pouvoir profiter de l’auto-complétion dans Eclipse et donc gagner finalement du temps quand j’utilise mes objets
    3/ D’éviter d’avoir du code monolithique dans la seule méthode __get() lorsque l’on a besoin de faire du contrôle d’accès.
    4/ De pouvoir effectivement faire du contrôle d’accès propre, maintenable et modulaire sur mes attributs.

    Le problème en utilisant les méthodes magiques __get() et __set() c’est que l’on ne peut pas facilement traiter les types et valeurs de chaque attribut virtuel représentant un champ de la table. Admettons que ma table est constitué de 20 champs de type très hétérogènes (int, varchar, blob, date, datetime, double…). La seule solution pour faire du contrôle d’accès sur les valeurs, c’est d’écrire à la main dans les deux méthodes, une série de if() ou un long switch()… Par exemple, si je fais :

    $oTransaction = new Transaction()
    $oTransaction->from = ‘Pierre’;
    $oTransaction->to = ‘Hugo’;
    $oTransaction->amount = 54.76;
    $oTransaction->time = ‘2008-08-11 20:45:43′;
    $oTransaction->save();

    Je me retrouve ici avec deux chaines, un nombre et une date. Pour m’assurer que ces valeurs ont des formats et des valeurs correctes, je serai obligé de les contrôler dans __set()

    public function __set($property, $value)
    {
    switch($property)
    {
    case ‘to’:
    $this->to = (string) $value;
    break;
    case ‘from’:
    $this->from = (string) $value;
    break;
    case ‘amount’:
    if(!is_numeric($value) || ($value amount = (double) $amount;
    break;
    case ‘time’:
    if(!icheckdate($value) || (strtotime($value) time = (int) strtotime($amount);
    break;
    default:
    throw new Exception(‘Attribut non reconnu’);
    break;
    }
    }

    De même pour retourner les bons types et format en lisant la valeur d’une propriété, il faudra tester chaque propriété et appliquer la conversion de format et / ou le transtypage adéquat.

    On se retrouve donc avec du code monolithique et imbuvable, alors qu’avec des getters() et setters() on a la chance de s’y retrouver plus facilement puisqu’à une méthode getXXX() et setXXX() correspond le traitement d’un attribut XXX.

    Donc il faut voir les méthodes magiques comme une fonctionnalité fort appréciable pour développer vite mais pas forcément pour développer bien… Et malheureusement, je rebondis sur ce que vous dites concernant la valeur ajoutée du développeur aujourd’hui pour dire que oui je suis d’accord sur ce point de vue là mais que malheureusement on ne peut pas coder vite et bien. C’est un choix à prendre pour une entreprise. Soit on décide absolument de vendre pas cher, de casser les prix et de mettre la pression aux développeurs pour qu’ils écrivent le moins de code possible au risque que ce dernier soit complètement peu modulaire et maintenable ; ou bien on prend un peu plus de temps ce qui permet d’écrire plus de code mais certainement du meilleur code, bien documenté, plus maintenable, plus modulaire… Personnellement je partage plus le deuxième esprit mais malheureusement rares sont les entreprises qui décident cette voie là puisqu’elle n’est pas suffisamment rentable pour elles… Dommage car cela ouvre davantage de portes aux mauvais développeurs et aux codeurs du dimanche qui écrivent des applications bancales et pas toujours sécurisées comme il se devrait.

    A bientôt,

    Hugo.

  3. | #3

    > Or, et malheureusement, il est de nos jours nécessaire de se rappeler que l’hébergement d’un site, même si ses coûts pourraient être réduits par de l’optimisation poussée/poussive sur le code, coûtera bien moins cher qu’un développeur qualifié, à plein temps, occupé à maintenir du code “optimisé” mais dont on pourrait aisément se passer en achetant un serveur plus puissant

    Mais LOL !

  4. Didier
    | #4

    Hugo: il est du devoir de l’équipe technique de faire comprendre aux “instance dirigeantes” comment la deuxième option (code propre plus cher à produire) peut être rentable à moyen ou long terme. En codant proprement, on pourrait déjà éviter de recommencer les sites “from scratch” tous les 2 ou 3 ans, ce qui semble étonnamment être une norme dans le milieu. J’ai déjà vu des entreprises (et pas des moindres) reconnaitre que le code d’un projet avait “mal vieilli”… avant même sa mise en production.
    La méthode d’accès à la DB fait partie des “raccourcis” que j’ai consentis à prendre pour alléger au maximum les exemples. En effet, un singleton serait beaucoup plus approprié.
    Merci pour toutes ces précisions, notamment au niveau des restrictions imposées à la génération de documentation.

    Palleas: bah quoi ! :p

  5. | #5

    @En codant proprement, on pourrait déjà éviter de recommencer les sites “from scratch” tous les 2 ou 3 ans, ce qui semble étonnamment être une norme dans le milieu.

    Ca ne m’étonne pas du tout que l’on ait à redévelopper “from scratch” les sites au bout de 2 ou 3 ans dans la mesure où les technologies du web évoluent en permanence. 2, voire même 3 ans, c’est une longue période dans le domaine du web contrairement au monde du progiciel où l’on développe parfois pendant plus de 5 ans.

    Je pense que les entreprises ne devraient pas se focaliser sur de la réutilisabilité du code à trop long terme (2 à 3 ans) car il y’aura clairement des évolutions majeures pendant cette période. Au contraire, je suis plutôt d’avis de dire : “il est temps de (presque) jeter (tout) à la poubelle ce que l’on a fait il y’a un an et demi et redévelopper proprement.

    Je comprends néanmoins que les entreprises ne veuillent pas prendre ces voies là car forcément ça leur coûte beaucoup d’argent en investissement mais c’est aussi un risque à prendre si l’on veut rester innovant, à la pointe des dernières technos et des dernières bonnes pratiques. De plus, se dire que l’on recommence avec une mise à jour de la techno (par exemple passer à PHP 5 quand on est encore en PHP 3 / 4) permet à l’équipe de développement de se former à nouveau et d’acquérir de nouvelles compétences. Un développeur web qui a passé 3 ans ou plus à faire du PHP 4 dans une société aura très certainement du mal à trouver une autre société “plus moderne” s’il n’a jamais pratiqué PHP 5. Et aujourd’hui, un développeur peut difficilement afficher sur son CV qu’il maîtrise PHP 4 tellement c’est devenu obsolète. Il est donc du devoir de l’entreprise de savoir prendre du temps à recoder certains de leurs produits avec des nouvelles technos à la fois pour une meilleure qualité, de meilleures performances, une meilleure sécurité et pour continuer de former ses salariés.

    ++

  6. | #6

    Pour la question des performances je ne peux que contredire, les problèmes de __get() n’ont rien à voir avec la différence echo/print et si vous commencez à monter trop d’architectures inutiles sur ces méthodes magiques ça risque vite de vous couter bien plus cher que les unes ou deux semaines de développement nécessaires pour programmer proprement au départ.

    Mais ce n’est pas la question.

    Déjà ton truc est finalement plus complexe que trois bêtes lignes de code :

    $this->infos = $db->getRow(“select * from $table where id = ‘$id’”)

    Moi je le remplacerai par

    $infos = $db->getRow(“select * from $table where id = ‘$id’”)
    foreach( $infos as $name => $value ) $this->{$name} = $value ;

    Ce n’est pas beaucoup plus long mais au moins tu n’as pas besoin de méthodes magiques. Ces méthodes magiques sont quasiment toujours une mauvaise idée. Elles ne sont au mieux qu’un paliatif quand on récupère un projet déjà peu extensible et qu’on a une correction/évolution avec la contrainte “toucher le moins possible au code”.

    Les seules utilisations vraiment sensées que j’ai vu de __get/__set/__call c’est pour l’accès à des objets distants ou des objets proxy dont ni l’utilisateur ni le créateur ne connait la liste des attributs. Un bon exemple est la manière dont fonctionne le client SOAP natif de PHP.

    Ce n’est pas faute d’aimer le concept d’attribut virtuel, j’adore ça au contraire, mais l’implémentation qu’a choisit PHP n’a quasiment aucun avantage.

  7. Didier
    | #7

    L’utilisation de __get et __set peut permettre de monter, par l’intermédiaire, par exemple, d’une propriété $editableFields, un système de champs modifiables ou pas. Dans le cas d’une gestion d’utilisateur (exemple pris au hasard), on peut éditer les informations personnelles, mais pas l’ID ni le login. Ca permet de “verrouiller” certains champs qu’il ne faut absolument pas éditer.
    Quant aux performances, je veux bien te croire sur parole, mais je vais quand même monter rapidement un mini-bechmark, histoire d’en avoir le coeur net. Je me ferais un plaisir de vous tenir informés des résultats…
    En tout cas, merci beaucoup pour ces précisions !

  8. | #8

    C’est encore plus amusant de jumeler avec des interfaces et du rewind engine tout en utilisant les design patterns.
    On peu alors avoir une application puissante et facile à maintenir.
    Je le démontre avec ma librairie en cours de développement.
    Je cherche notamment des contributeurs et testeurs pour la version alpha qui arrive sous peu.
    Le lien est dans mon pseudo, je serai ravi d’avoir un retour ou peut être un article de l’administrateur de ce blog très intéressant.

  1. No trackbacks yet.