Le lazy load en PHP

N'initialiser que les objets réellement utilisés

06 janvier 2014

Il existe de nombreuses façons de faire du lazy load, cet article va en présenter deux très proches l'une de l'autre.

Le terme lazy load représente la faculté de n'initialiser des entités que lors de leur première utilisation. Ceci garantit de ne pas consommer de ressources inutilement. Attention cependant, comme vous allez le voir, les techniques présentées ont un coût non négligeable lorsqu'elles sont appliquées à grande échelle : méthodes magiques, appels de fonctions... Assurez vous donc de la rentabilité du lazy load pour votre cas d'utilisation.

Première méthode : lazy load avec des closures

Lazy load grâce aux closures

class Client
{
        protected $commandes = null;
        protected $prenom = null;

        public function __construct()
        {
                $this->prenom = 'Nicolas';

                /* L'attribut commandes utilise le lazy load car son initialisation est une operation lourde */
                $this->commandes = function ()
                {
                        $model = new ClientModel();
                        return $model->getToutesLesCommandes();
                };
        }

        public function __get($key)
        {
                if (is_callable($this->$key))
                {
                        $this->$key = call_user_func($this->$key);
                }

                return $this->$key;
        }
}

class ClientModel
{
        public function getToutesLesCommandes()
        {
                /* Operation tres couteuse */
                sleep(5);
                echo "Et 5 tres longues secondes plus tard...";
                return array(
                        'commande1' => 123456,
                        'commande2' => 456789
                );
        }
}

$o = new Client();

var_dump($o->prenom);

/* Si nous nous etions arrete la, il aurait ete dommage de recuperer les commandes pour rien. */

var_dump($o->commandes);
var_dump($o->commandes);

/* Affichage :
 *
 *      string 'Nicolas' (length=7)
 *
 *      Et 5 tres longues secondes plus tard...
 *
 *      array (size=2)
 *              'commande1' => int 123456
 *              'commande2' => int 456789
 *
 *      array (size=2)
 *              'commande1' => int 123456
 *              'commande2' => int 456789
 */

On voit dans l'affichage que la fonction "getToutesLesCommandes" n'est appelée qu'une fois lors de la première utilisation de l'attribut "commandes". Si nous n'avions jamais utilisé cet attribut, l'algorithme coûteux n'aurait jamais été exécuté. L'attribut "commandes" étant devenu un tableau et non plus une fonction, la seconde fois qu'il est utilisé le résultat est instantanément retourné.

Vous constaterez que l'attribut "prenom" n'utilise pas le lazy load afin d'éviter des pertes de performances inutiles.

Si vous souhaitez utiliser une méthode de la classe courante pour initialiser une variable, ce n'est pas beaucoup plus compliqué :

Lazy load avec une méthode de la classe courante

class Client
{
        protected $commandes = null;

        public function __construct()
        {
                /* Version PHP < 5.4 : On ne peut pas utiliser $this directement dans une closure avant PHP 5.4 */
                $client = $this;

                $this->commandes = function () use ($client)
                {
                        /* attention la methode doit etre publique */
                        return $client->getToutesLesCommandes();
                };

                /* Version PHP > 5.4 */

                $this->commandes = function ()
                {
                        return $this->getToutesLesCommandes();
                };
        }

        public function getToutesLesCommandes()
        {
                /* Operation tres couteuse */
                echo "Et 5 tres longues secondes plus tard...";
                sleep(5);
                return array(
                        'commande1' => 123456,
                        'commande2' => 456789
                );
        }

        public function __get($key)
        {
                if (is_callable($this->$key))
                {
                        $this->$key = call_user_func($this->$key);
                }

                return $this->$key;
        }
}

$o = new Client();

var_dump($o->commandes);
var_dump($o->commandes);

/* Affichage :
 *
 *      Et 5 tres longues secondes plus tard...
 *
 *      array (size=2)
 *              'commande1' => int 123456
 *              'commande2' => int 456789
 *
 *      array (size=2)
 *              'commande1' => int 123456
 *              'commande2' => int 456789
 */

Seconde méthode : on automatise un peu

Cette méthode est la même que la précédente en moins verbeuse. Au lieu de déclarer manuellement les fonctions anonymes, nous utiliserons tout le temps une méthode spécifique de la classe qui sera appelée automatiquement.

Lazy load avec des méthodes build

class Client
{
        protected $commandes = null;
        protected $adresses = null;

        public function buildCommandes()
        {
                /* Operation tres couteuse */
                echo "Et 5 tres longues secondes plus tard...";
                sleep(5);
                $this->commandes = array(
                        'commande1' => 123456,
                        'commande2' => 456789
                );
        }

        public function buildAdresses()
        {
                /* Operation tres couteuse */
                echo "Et 3 autres tres longues secondes plus tard...";
                sleep(3);
                $this->adresses = array(
                        'domicile' => 'paris',
                        'travail' => 'paris'
                );
        }

        public function __get($key)
        {
                if (is_null($this->$key))
                {
                        $method = 'build' . ucfirst($key);
                        $this->$method();
                }

                return $this->$key;
        }
}

$o = new Client();

var_dump($o->commandes);
var_dump($o->commandes);
var_dump($o->adresses);
var_dump($o->adresses);

/* Affichage :

        Et 5 tres longues secondes plus tard...

        array (size=2)
          'commande1' => int 123456
          'commande2' => int 456789

        array (size=2)
          'commande1' => int 123456
          'commande2' => int 456789

        Et 3 autres tres longues secondes plus tard...

        array (size=2)
          'domicile' => string 'paris' (length=5)
          'travail' => string 'paris' (length=5)

        array (size=2)
          'domicile' => string 'paris' (length=5)
          'travail' => string 'paris' (length=5)

 */

Evidemment il faut sécuriser ce code avant de l'utiliser, chose que je n'ai pas fait pour simplifier les exemples. Le minimum serait de vérifier l'existence et la portée de $this->$key ou de la méthode buildXXXX avant de tenter de les utiliser.

A bientôt !

Par
Créateur et administrateur.

Dans la même catégorie

Regexp en PHP, le mémo indispensable
La fonction isset et la valeur null
Les différentes façons de fusionner deux tableaux en PHP
La priorité des opérateurs en PHP

Commentaire(s)