Prendre Rendez-vous
Logo Olympe Studio blanc

Comment écrire un Singleton en PHP ?

Le Singleton est un paterne de développement qui permet de n’avoir qu’une seule instance d’une classe durant toute l’exécution d’une application.

C’est très pratique si vous voulez vous assurer que quelque chose dans votre application ne soit représenté qu’une seule fois.

Par exemple, vous pouvez utiliser un Singleton pour créer un Logger ou encore pour représenter une connexion à votre base de données.

Il existe plein de cas pratiques pour les Singleton en PHP et, bien que certains ne les apprécient pas tellement dans la communauté, ils sont un bon indicateur de l’optimisation de votre programme.

Sommaire

Pourquoi utilise-t-on les Singleton ?

Utiliser un Singleton en PHP est essentiellement une pratique d’optimisation.

En les utilisant, vous vous assurez de ne pas créer en cascades des instances d’une classe à chaque fois que vous en avez besoin dans votre programme.

Cela induit plusieurs optimisations :

  • Vous ne re-exécuterai pas de code inutilement puisque votre instance est toujours prête à l’emploi,
  • Vous limitez les chances de créer des bugs difficiles à identifier,
  • Vous gardez un code super lisible.

Mais, les singletons n’ont pas ces seuls avantages.

Ils sont aussi, et surtout, super simples à utiliser grâce à l’héritage des classes. Il vous suffira de copier-coller notre Singleton un peu plus bas et de créer vos classes par-dessus pour en faire des Singletons.

Super simple n’est-ce pas ? Et surtout, très lisible.

Quelle différence avec les classes statiques ?

Les classes statiques peuvent parfois répondre aux mêmes problématiques qu’un singleton.

Cela dit, ce n’est pas toujours le cas, premièrement, et en second lieu, les singletons sont beaucoup plus polyvalents parce qu’ils disposent des caractéristiques suivantes :

  • Ils sont instanciables. Ce qui signifie qu’ils peuvent avoir un constructeur ainsi que des états dynamiques.
  • Ils sont beaucoup plus faciles à réutiliser, puisqu’il vous suffit de laisser une classe hériter de votre factory pour en faire un Singleton.

Ainsi, les Singletons peuvent vous permettre de facilement réaliser des tâches complexes de manière optimales et sans vous contraindre puisque vous disposerai de toutes les fonctionnalités d’une classe normale.

Quand utiliser un Singleton en PHP ?

PHP, par son design de base, rend l’usage des Singletons beaucoup moins puissant. Ça ne veut cependant pas dire qu’ils sont inutiles.

Pourquoi ?

Tout simplement parce que PHP s’execute de bout en bout à chaque requête. Donc, vous limitez le risque de créer des milliers d’instances inutiles d’une classe.

Cela dit, les Singletons ont d’autres avantages que la simple optimisation de la mémoire de votre application.

Chez Olympe Studio, nous avons une règle simple quant à l’emploi des Singleton : Tout est un Singleton, à l’exception des classes qui représentent des entités.

  • Une classe pour écrire les logs ? Singleton.
  • Une classe pour se connecter à la base de données ? Singleton.
  • Une classe pour représenter un Client d’un service tier ? Singleton.

Vous avez compris l’idée.

Si vous n’avez jamais plusieurs instances d’une classe, vous avez sans doute un Singleton à placer.

À contrario,

Si vous devez représenter des entités qui fonctionnent de manière indépendante, telles qu’un utilisateur, une page web ou une session de cours en ligne, vous ne devriez pas utiliser un Singleton.

Écrire un Singleton en PHP

Nous allons partager avec vous notre factory sur les Singleton.

Cette classe permet de créer des Singleton, ce n’en est pas un à proprement parler parce que cette classe est abstraite et ne peut donc pas être instanciée.

Mais avant ça, intéressons-nous à l’écriture classique.

Le Singleton Simple

Voici comment écrire un Singleton :

<?php

class AppConfig {
    private $config;
    private static $_instance;  // On stock l'instance dans cette propriété statique.

    /**
    * Cette méthode permet d'instancer ou de récupérer l'instance de la class.
    **/
    public static function getInstance(...$args) {
      if (is_null(self::$_instance)) {
        self::$_instance = new AppConfig(...$args);
      }
      return self::$_instance;
    }

    /**
    * Le constrcuteur est privé, il n'y a que la méthode getInstance qui peut créer l'instance.
    **/
    private function __construct(string $file) {
      $config = [];
      if (file_exists($file)) {
        $config = require_once($file);
      }

      $this->config = $config;
    }

    /**
     * Empêche de cloner l'instance avec `$a = clone $instance`.
     */
    private function __clone() {}

    /**
     * Empêche l'unserialisation de l'instance.
     */
    final public function __wakeup() {}

    /**
     * Récupère un élément de la config
     *
     * @param string $key  La clé de configuration.
     *
      *  @return mixed|null
      **/
    public function get($key) {
      if (isset($this->config[$key])) {
        return $this->config[$key];
      }
    }
    /**
     * insère ou modifie un élément dans la config
     *
     * @param string $key     La clé de configuration.
     * @param string $value   La valeur
     *
      *  @return mixed|null
      **/
    public function set($key, $value) {
      $this->config[$key] = $value;
    }
}

Dans cet exemple, on crée une classe qui représente la configuration de notre application. Bien entendu, cette configuration soit être accessible de n’importe où dans l’application.

Son état est global, c’est à dire qu’il n’y a qu’une configuration pour toute l’application, et que si quoi que ce soit la modifie à un instant, tout le reste de l’application doit être à jour sur ce changement.

C’est donc un cas idéal pour un singleton.

La magie se passe ici :

<?php

class AppConfig {
    private static $_instance;  // On stock l'instance dans cette propriété statique.

    public static function getInstance(...$args) {
      if (is_null(self::$_instance)) {
        self::$_instance = new AppConfig(...$args);
      }
      return self::$_instance;
    }

    // ...
}

La méthode getInstance permet de renvoyer l’instance unique de la classe OU de la créer si elle n’existe pas. Pour cela, on stocke l’instance dans la propriété statique $_instance.

Ainsi, vous pouvez récupérer la configuration comme ceci :

<?php

// L'instance n'existe pas, on la crée.
$config = AppConfig::getInstance();

// L'instance existe, on la récupère.
$configB = AppConfig::getInstance();

Ici, $config et $configB sont identiques.

Si par exemple je modifiais un élément de la configuration avec $config, voici ce qui se passerait :

<?php

$config = AppConfig::getInstance();
$configB = AppConfig::getInstance();

$config->get('hello'); // > 'World!'
$configB->get('hello'); // > 'World!'

// On change la configuration.
$configB->set('hello', 'Le Monde !');

// La configuration est changé pour toutes les instances.
$config->get('hello'); // > 'Le Monde !'
$configB->get('hello'); // > 'Le Monde !'

L’état est global, puisqu’on a toujours qu’une et une seule instance.

Créer une Factory

Les factories sont des « usines ».

C’est globalement des classes qui permettent de répéter un pattern plus rapidement avec les fonctionnalités d’héritage.

Dans mon exemple, j’ai créé un Singleton, directement dans la classe. Cela dit, si vous avez plusieurs Singletons dans votre application, vous allez dupliquer du code à chaque fois, et ça, on aimerait l’éviter.

Cela dit, il est possible de créer une factory pour créer des singletons plus rapidement.

Voici notre factory :

abstract class Singleton {
  private static array $_instances = [];

  protected function __construct() {
  }

  /**
   * Avoid clone instance
   */
  private function __clone() {
    // Private clone method to prevent cloning of the instance
  }

  /**
   * Prevent instance unserialization
   */
  final public function __wakeup() {
    // Private wakeup method to prevent unserializing of the instance
  }

  /**
   * Return the unique instance of the class called.
   *
   * @return object The class classed as a unique instance.
   */
  public static function getInstance(...$args) {
    $calledClass = get_called_class();

    if (!isset(self::$_instances[$calledClass])) {
      self::$_instances[$calledClass] = new $calledClass(...$args);
    }

    return self::$_instances[$calledClass];
  }
}

On note ici plusieurs changements :

  • __construct() n’est plus privée. Elle est protégée parce qu’on veut permettre aux classes qui vont hériter de Singleton de pouvoir définir leur propre constructeur.
  • $_instance est devenu $_instances, un tableau clé-valeur qui prend en clé le nom d’une classe et en valeur son instance unique.
  • Singleton est une classe abstraite. On ne veut pas qu’elle soit instanciée sans son contexte.

Vous pouvez ensuite construire des Singletons de la manière suivante :

<?php

use App\Factory\Signleton;


class AppConfig extends Singleton {

  /**
  * Vous pouvez définir votre logique Constructeur ici.
  */
  protected function __construct(string $file) {
    $config = [];
    if (file_exists($file)) {
      $config = require_once($file);
    }

    $this->config = $config;
  }

  /**
   * Récupère un élément de la config
   *
   * @param string $key  La clé de configuration.
   *
    *  @return mixed|null
    **/
  public function get($key) {
    if (isset($this->config[$key])) {
      return $this->config[$key];
    }
  }
  
  /**
   * insère ou modifie un élément dans la config
   *
   * @param string $key     La clé de configuration.
   * @param string $value   La valeur
   *
    *  @return mixed|null
    **/
  public function set($key, $value) {
    $this->config[$key] = $value;
  }
}

Beaucoup plus lisible n’est-ce pas ?

Et, comme vous pouvez le voir, l’ajout de la fonctionnalité « Singleton » est vraiment super simple, puisqu’il suffit d’hériter de la Factory pour en bénéficier.

Attention tout de même avec l’héritage. C’est une fonctionnalité super puissante, mais il est facile d’en faire trop et de transformer votre application en joyeux bordel.

N’en abusez pas !

Conclusion

Le Singleton est un patron de conception qui permet de vous assurer qu’il n’existe qu’une seule instance d’un objet dans votre application.

Ils disposent d’à peu près les mêmes avantages et inconvénients que les variables globales, à l’exception qu’ils sont beaucoup plus lisibles et compréhensibles pour le développeur.

En utilisant le concept de Factory combiné au Singleton, vous pourrez mettre en place une procédure de création de classes uniques super lisible et efficace.

C’est un concept de développement super efficace qui permet également d’économiser de la mémoire et de limiter les bogues.

Aller plus loin

Pour aller plus loin dans le concept, ou pour découvrir l’approche différemment :

https://refactoring.guru/fr/design-patterns/singleton/php/example
https://grafikart.fr/tutoriels/singleton-569