reseau social

Singletons et Multitons avec PHP 5

Un article de ToutProgrammer.com.

Les Design Patterns sont une solution de conception élégante et robuste qui évite d'avoir à réinventer la roue. Avec ses évolutions majeures telles que la visibilité des membres ou les constructeurs/destructeurs unifiés, PHP 5 autorise la conception par design pattern. Voyons comment mettre en œuvre les patterns simples que sont le singleton et le multiton.

Sommaire

[modifier] Le singleton qu'est-ce que c'est ?

Le singleton est ce que l'on appelle un design pattern. Son but est d'éviter de multiplier les objets (et donc les ressources en mémoire) identiques. Autrement dit, il offre la possibilité d'utiliser un même (on parle souvent d'unicité) objet tout au long d'un programme, ce qui permet donc d'améliorer votre application.

[modifier] La théorie

Voici à quoi ressemble un singleton en PHP5:

  1. <?php
  2. class Singleton
  3. {
  4. private static $_instance;
  5.  
  6. private function __construct()
  7. {
  8. echo 'construction du singleton<br/><br/>';
  9. }//fin constructeur
  10.  
  11. public static function GetInstance()
  12. {
  13. if (!isset(self::$_instance))
  14. {
  15. self::$_instance = new Singleton();
  16. }
  17. return self::$_instance;
  18. }//fin fonction GetInstance
  19. }//fin classe Singleton
  20. ?>

Avant de passer à une explication, essayer ce script avec la classe Singleton citée ci-dessus:

  1. <?php
  2. for($i=0; $i<10; $i++)
  3. {
  4. $var = Singleton::getInstance();
  5. echo '<pre>';
  6. var_dump($var);
  7. echo '</pre>';
  8. }
  9. ?>

Et observez le résultat affiché dans votre navigateur. Comme vous pouvez le voir, la variable $var est en fait toujours la même, elle n'est nullement recrée à chaque appel de la méthode GetInstance.

object(Singleton)#1 (0) {
}

object(Singleton)#1 (0) {
}

object(Singleton)#1 (0) {
}
.......

[modifier] Explications du fonctionnement

  1. <?php
  2. private static $_instance;
  3. ?>

On utilise la variable statique $_instance qui va stocker l'objet que l'on souhaite construire et ensuite utiliser. Cet attribut est définit comme privé et static.

  1. <?php
  2. private function __construct()
  3. {
  4. echo 'construction du singleton<br/><br/>';
  5. }
  6. ?>

Comme dans toute classe que vous avez pu créer, il y a un constructeur, il a été définit avec un accès privé car on ne veut pas que l'objet soit recréé à chaque fois, mais qu'il soit appelé uniquement par la méthode GetInstance si besoin est. Un constructeur est par définition public mais recrée à chaque fois un objet en mémoire, ce que nous ne voulons pas dans le cas du Singleton ;).

  1. <?php
  2. public static function GetInstance()
  3. {
  4. if (!isset(self::$_instance))
  5. {
  6. self::$_instance = new Singleton();
  7. }
  8. return self::$_instance;
  9. }//fin fonction GetInstance
  10. ?>

C'est ici que le Singleton montre sa puissance. La méthode GetInstance est déclarée statique, donc accessible sans qu'aucun objet soit instancié (Singleton::GetInstance() par exemple). Ensuite, on vérifie si l'attribut $_instance est déjà créé, si ce n'est pas le cas alors on fait appel au constructeur de la classe afin de créer un nouvel objet en mémoire. Etant donné que nous utilisons des accès statiques, donc sans création d'objet, il est impossible d'utiliser le mot clé $this, c'est pourquoi nous utilisons le mot clé selfqui permet d'accéder aux variables et méthodes statiques.

[modifier] La théorie c'est bien beau, mais ça sert à quoi ?

Justement voilà un exemple pratique qui je l'espère sera assez explicite. Lorsque vous faites interagir PHP avec un SGBD, vous serez peut-être obligé de faire plusieurs connexions à une base de données, ce qui vous ferait donc multiplier les ressources de connexion. Avec le Singleton nous réglons le problème une fois pour toute puisque dès qu'une ressource de connexion sera créée, toute autre tentative de création sera inutile et la ressource en cours vous sera renvoyée. Pour vous en rendre compte vous pouvez essayer ce petit script qui simule une connexion à une base de données MYSQL.

  1. <?php
  2. //class.mysqlConnection.php
  3. class MysqlConnection
  4. {
  5. /***************************************************/
  6. /* cette variable va nous servir pour le singleton */
  7. /***************************************************/
  8. private static $_instance;
  9.  
  10. //nom ou IP du serveur:
  11. private $_serveur;
  12. //login de connexion:
  13. private $_user;
  14. //mot de passe pour la connexion:
  15. private $_pass;
  16. //nom de la base de données:
  17. private $_db;
  18.  
  19. /***************************************************/
  20. /* <SINGLETON> */
  21. /***************************************************/
  22. public static function GetInstance($serveur, $user, $pass, $bd)
  23. {
  24. if(!isset(self::$_instance))
  25. {
  26. echo "<b>l'instance n'existait pas</b><br/>";
  27. self::$_instance = new MysqlConnection($serveur, $user, $pass, $bd);
  28. }
  29. else
  30. echo "<b>l'instance existe déjà</b><br/>";
  31.  
  32. return self::$_instance;
  33. }//fin getInstance
  34. /***************************************************/
  35. /* </SINGLETON> */
  36. /***************************************************/
  37.  
  38. //constructeur:
  39. private function __construct($serveur, $user, $pass, $db)
  40. {
  41. $this->_serveur = $serveur;
  42. $this->_user = $user;
  43. $this->_pass = $pass;
  44. $this->_db = $db;
  45. $this->_MysqlConnect();
  46. }//fin mysqlConnection
  47.  
  48. //permet la connexion et la sélection de la base de données:
  49. private function _MysqlConnect()
  50. {
  51. echo 'Connexion à la base de données :'.$this->_serveur.' '.$this->_user.' '.$this->_pass.' '.$this->_db.'<br/>';
  52. }//fin fonction mysqlConnect()
  53.  
  54. function __destruct()
  55. {
  56. echo "Destruction de l'objet MysqlConnection <br/>";
  57. }//fin destructeur
  58. }//fin classe mysqlConnection
  59. ?>

Et ensuite testez cette classe avec une petite boucle for de ce type:

  1. <?php
  2. for($i=0; $i<10; $i++)
  3. {
  4. $cnx = MysqlConnection::GetInstance('serveur', 'user', 'pass', 'db');
  5. echo '<pre>';
  6. var_dump($cnx);
  7. echo '</pre>';
  8. }
  9. ?>

Vous devriez obtenir un résultat de ce style:

l'instance n'existait pas
Connexion à la base de données :le_serveur le_user le_pass la_db

object(MysqlConnection)#1 (4) {
 ["_serveur:private"]=>
 string(10) "le_serveur"
 ["_user:private"]=>
 string(7) "le_user"
 ["_pass:private"]=>
 string(7) "le_pass"
 ["_db:private"]=>
 string(5) "la_db"
}

l'instance existe déjà

object(MysqlConnection)#1 (4) {
 ["_serveur:private"]=>
 string(10) "le_serveur"
 ["_user:private"]=>
 string(7) "le_user"
 ["_pass:private"]=>
 string(7) "le_pass"
 ["_db:private"]=>
 string(5) "la_db"
}
....

Comme vous pourrez le constater, l'objet et dans ce cas la ressource de connexion à MYSQL n'a été créée qu'une seule fois puis ensuite réutilisée.

[modifier] Le Multiton : une variante du singleton

Le Multiton, comme le singleton, est une variante intéressante qui permet de n'instancier qu'une seule fois un même objet, tout en utilisant plusieurs objets du même type. Par exemple, imaginons que l'on ait besoin d'accéder à plusieurs bases distinctes. Si l'on accède plusieurs fois à la même base, il n'est pas nécessaire d'y ouvrir plusieurs connexions. Comment alors profiter des avantages du singleton pour pouvoir gérer plusieurs bases ? Il suffit, au lieu de stocker une seule instance de la connexion, de stocker plusieurs instances dans un tableau (associatif ou non), identifiables de manière unique. Pour identifier notre connexion, il suffit d'utiliser les paramètres de connexion à la base : serveur et base. Par exemple, en les concaténant dans une chaîne de caractères.

  1. <?php
  2. class MysqlConnection {
  3. /***************************************************/
  4. /* cette variable va nous servir pour le multiton */
  5. /* Elle va stocker chaque nouvelle instance créée */
  6. /***************************************************/
  7. private var $_instances = array();
  8.  
  9. //nom ou IP du serveur:
  10. private var $_serveur;
  11.  
  12. //login de connexion:
  13. private var $_user;
  14.  
  15. //login de connexion:
  16. private var $_pass;
  17.  
  18. //nom de la base de données:
  19. private var $_db;
  20.  
  21. /***************************************************/
  22. /* <MULTITON> */
  23. /***************************************************/
  24. public static function getInstance($serveur, $user, $pass, $bd) {
  25.  
  26. // On crée un identifiant unique, qui nous permettra de ne pas
  27. // créer une deuxième instance si la même est demandée
  28. $connectId = "$serveur:$bd";
  29.  
  30. if (!isset(self::$_instances[$connectId])) {
  31. echo "<b>l'instance n'existait pas pour la connexion $connectId</b><br/>";
  32. self::$_instances[connectId] == new MysqlConnection($serveur, $user, $pass, $bd);
  33. } else {
  34. echo "<b>l'instance existe déjà pour la connexion $connectId</b><br/>";
  35. }
  36. return self::$_instances[connectId];
  37. }
  38.  
  39. //constructeur:
  40. private function __construct($serveur, $user, $pass, $db)
  41. {
  42. $this->_serveur = $serveur;
  43. $this->_user = $user;
  44. $this->_pass = $pass;
  45. $this->_db = $db;
  46. $this->_MysqlConnect();
  47. }//fin mysqlConnection
  48.  
  49. //permet la connexion et la sélection de la base de données:
  50. private function _MysqlConnect()
  51. {
  52. echo 'Connexion à la base de données :'.$this->_serveur.' '.$this->_user.' '.$this->_pass.' '.$this->_db.'<br/>';
  53. }//fin fonction mysqlConnect()
  54.  
  55. function __destruct()
  56. {
  57. echo "Destruction de l'objet MysqlConnection <br/>";
  58. }//fin destructeur
  59. }//fin classe mysqlConnection
  60. ?>

Pour tester cela, reprenons notre exemple:

  1. <?php
  2. for($i=0; $i < 10; $i++)
  3. {
  4. $cnx = MysqlConnection::getInstance('serveur', 'user', 'pass', 'db');
  5. echo '<pre>';
  6. var_dump($cnx);
  7. echo '</pre>';
  8.  
  9. $cnx = MysqlConnection::getInstance('serveur2', 'user2', 'otherpass', 'db');
  10. echo '<pre>';
  11. var_dump($cnx);
  12. echo '</pre>';
  13.  
  14.  
  15. }
  16. ?>

Qui devrait nous donner:

l'instance n'existait pas pour la connexion serveur:db
Connexion à la base de données :serveur user pass db

object(MysqlConnection)#1 (4) {
["_serveur:private"]=>
string(10) "serveur"
["_user:private"]=>
string(7) "user"
["_pass:private"]=>
string(7) "pass"
["_db:private"]=>
string(5) "db"
}

l'instance n'existait pas pour la connexion serveur2:db
Connexion à la base de données :serveur2 user2 otherpass db

object(MysqlConnection)#1 (4) {
["_serveur:private"]=>
string(10) "serveur2"
["_user:private"]=>
string(7) "user2"
["_pass:private"]=>
string(7) "otherpass"
["_db:private"]=>
string(5) "db"
}

l'instance existe déjà pour la connexion serveur:db

object(MysqlConnection)#1 (4) {
["_serveur:private"]=>
string(10) "serveur"
["_user:private"]=>
string(7) "user"
["_pass:private"]=>
string(7) "pass"
["_db:private"]=>
string(5) "db"
}

l'instance existe déjà pour la connexion serveur2:db

object(MysqlConnection)#1 (4) {
["_serveur:private"]=>
string(10) "serveur2"
["_user:private"]=>
string(7) "user2"
["_pass:private"]=>
string(7) "otherpass"
["_db:private"]=>
string(5) "db"
}
....

Le but du Multiton est de conserver une instance unique d'une classe dans un contexte unique. Dans cet exemple, le contexte peut être considéré comme étant la connexion à une base. Ainsi, nous ne somme plus limités à une connexion, mais à une connexion par base !

Pratique non ?
:

[modifier] Historique de l'article

Cet article, réalisé par LAlex et qwix, a été publié pour la première fois le 24 octobre 2004 sur le site ToutProgrammer.com (1ème version).