Classe singleton d'accès aux SGBD intégrant PDO
J'ai commencé à créer ma propre classe d'accès aux bases de données car j'avais véritablement envie d'implémenter des interfaces telles que Iterator ou Countable. Seulement voilà, il existe déjà une classe de ce type native à PHP : PDO.
Présentation brève de l'extension PDO
Cette classe est particulièrement intéressante et pratique à utiliser car elle gère directement l'abstraction (comprenez qu'elle ne dépend pas d'un Système de Gestion de Base de Données (SGBD) pour fonctionner), la protection des données, les requêtes préparées, etc. En y reflechissant, je me suis dit que j'étais en train de perdre mon temps à essayer de refaire ce qui se fait déjà, probablement en moins bien ! C'est une chose de ne pas aimer le code tout fait (i.e. les Frameworks PHP), c'en est une autre de ne pas vouloir utiliser les classes natives.
Il y a cependant certaines choses que j'aimerai retrouver dans PDO : notamment le design pattern Singleton. C'est parti pour la conception de ma classe que j'ai nommée très sobrement SPDO (Singletoned-PDO, sympa non ?).
Le pattern Singleton
Le singleton c'est quoi ? C'est une manière de s'assurer de ne disposer que d'une seule instance de votre classe durant toute l'exécution du script. Il y aurait bien la solution de déclarer PDO dans une variable en tête d'une page appelée systématiquement mais pour moi, cette solution est à proscrire.
Singleton : redéfinition du constructeur en accès private<?phpclass MyClass extends PDO{/*** Constructeur** @param string $path* @param string $user* @param string $path* @return void* @access private* @see PDO::__construct()*/private function __construct($path, $user, $pass){parent::__construct($path, $user, $pass);}}?>
Vous croyez vraiment que ça allait être aussi simple? Essayez d'instancier la classe MyClass, et vous aurez droit au message suivant :
Fatal error: Access level to MyClass::__construct() must be public (as in class PDO) in /path/to/your/script/pdo.class.php on line 6
Vous avez cependant la possibilité de faire un presque-singleton en laissant simplement le constructeur publique, mais pour moi ce serait tout simplement une hérésie. La solution est donc la composition.
Présentation de la classe SPDO
Qu'est ce que la composition ? C'est une autre manière de voir la programmation orientée objet, au même titre que l'héritage. C'est à dire que de cette manière nous allons nous contenter de stocker dans la classe une instance de PDO, plutôt que d'en hériter :
<?phpclass SPDO{/*** Instance de la classe SPDO** @var SPDO* @access private*/private $PDOInstance = null;/*** Constante: nom d'utilisateur de la bdd** @var string*/const DEFAULT_SQL_USER = 'user';/*** Constante: hôte de la bdd** @var string*/const DEFAULT_SQL_HOST = 'localhost';/*** Constante: hôte de la bdd** @var string*/const DEFAULT_SQL_PASS = 'xxxxxxxxx';/*** Constante: nom de la bdd** @var string*/const DEFAULT_SQL_DTB = 'database';/*** Constructeur** @param void* @return void* @see PDO::__construct()*/public function __construct(){$this->PDOInstance = new PDO('mysql:dbname='.self::DEFAULT_SQL_DTB.';host='.self::DEFAULT_SQL_HOST,self::DEFAULT_SQL_USER ,self::DEFAULT_SQL_PASS);}}?>
Vous avez ensuite la possibilité d'implémenter votre Singleton :
<?phpclass SPDO{/*** Instance de la classe PDO** @var PDO* @access private*/private $PDOInstance = null;/*** Instance de la classe SPDO** @var SPDO* @access private* @static*//*** Constante: nom d'utilisateur de la bdd** @var string*/const DEFAULT_SQL_USER = 'root';/*** Constante: hôte de la bdd** @var string*/const DEFAULT_SQL_HOST = 'localhost';/*** Constante: hôte de la bdd** @var string*/const DEFAULT_SQL_PASS = '';/*** Constante: nom de la bdd** @var string*/const DEFAULT_SQL_DTB = 'jobeet';/*** Constructeur** @param void* @return void* @see PDO::__construct()* @access private*/private function __construct(){$this->PDOInstance = new PDO('mysql:dbname='.self::DEFAULT_SQL_DTB.';host='.self::DEFAULT_SQL_HOST,self::DEFAULT_SQL_USER ,self::DEFAULT_SQL_PASS);}/*** Crée et retourne l'objet SPDO** @access public* @static* @param void* @return SPDO $instance*/{{self::$instance = new SPDO();}return self::$instance;}/*** Exécute une requête SQL avec PDO** @param string $query La requête SQL* @return PDOStatement Retourne l'objet PDOStatement*/public function query($query){return $this->PDOInstance->query($query);}}
Enfin, en redéfinissant les méthodes query() ou prepare() de PDO, vous aurez la possibilité d'ajouter un log des requêtes exécutées, etc.
Pour l'utilisation, rien de plus simple. Admettons que nous ayons une table "membre" dans notre base de données qui contient n colonnes dont les champs suivants : id, nom et prénom.
Comme nous avons encapsuler (ou wrapper) la méthode query() de PDO dans la méthode query() de SPDO, alors nous pouvons simplement parcourir notre table de la manière suivante :
<?phpforeach (SPDO::getInstance()->query('SELECT id, nom, prenom FROM membre m') as $membre){}
Rien ne vous empêche à présent de redéfinir chaque méthode de PDO ou bien d'utiliser la méthode magique __call() pour faire une redéfinition dynamique de toutes les méthodes de PDO.
Les commentaires
2. Par Piko le 26/12/2008 17:44
"Il y aurait bien la solution de déclarer PDO dans une variable en tête d'une page appelée systématiquement mais pour moi, cette solution est à proscrire."
Pourquoi cela ?
Je comprends pas pourquoi ce n'est pas une bonne solution, on est au moins sûr d'avoir qu'une seule et unique instance...
Piko.
3. Par saturn1 le 26/12/2008 19:41
Bon article , comme d'habitude!
J'utilise ce script et je n'ai pas à m'en plaindre !!
Bravo
4. Par Tim le 30/12/2008 22:38
En réponse a x@v il y a une différence entre $this et self, ce que chacun référence en fait $this référence l'objet courant (donc la classe doit etre instancié pour pouvoir l'utiliser). self référence la classe et donc il ne faut pas spécialement que la classe soit instancié pour pouvoir utiliser self
5. Par gtraxx le 05/02/2009 16:33
je suppose que l'on peut redéfinir ainsi chaque méthode de PDO avec un singleton ?
genre :
public function setMode(){
return $this->PDOInstance->setFetchMode(PDO::FETCH_ASSOC);
}
ou encore modifier le comportement de fetch par exemple.
Ou mieux se créer ses propres fonctions d'insertion
6. Par Bob le 10/02/2009 17:12
Ce script est super mais comment faire si je veux me connecter à plusieurs DB différentes ?
7. Par Emacs le 10/02/2009 19:32
@Bob : tu ne peux pas ! C'est bien là le principe du Singleton, c'est de n'avoir qu'une et une seule instance unique. Si tu veux pouvoir faire plusieurs connexions, tu vas devoir implémenter un pattern multiton.
8. Par gtraxx le 11/02/2009 09:23
J'ai essayer de définir d'autre fonction que query afin d'avoir un pattern plus complet mais j'ai quelque souçi.
dans la class :
/**
* config setFetchMode
*
* @return bool
*/
public function setMode($mode){
$fetch = array(
'assoc' => PDO::FETCH_ASSOC,
'class' =>PDO::FETCH_CLASS,
'column'=>PDO::FETCH_NUM
);
foreach ($fetch as $key){
$fetchmode = $key[$mode];
}
return $fetchmode;
}
/**
* function prepare
*
* @param request $sql
* @return array()
*/
public function Prepare($sql){
return $this->PDOInstance->prepare($sql);
}
public function FetchAll($sql,$array=false){
$prepare = $this->Prepare($sql);
$result = $prepare->setFetchMode($this->setMode('assoc');
$result .= $array ? $prepare->execute($array = array()) : $prepare->execute();
$result .= $prepare->fetchAll();
$result .= $prepare->closeCursor();
$result .= $prepare->null;
return $result;
}
Cela me retourne un tableau vide avec :
11Array1
Warning: Invalid argument supplied for foreach() in C:\wamp\www\wizard\_scriptroot\index\index.php on line 31
je l'utilise comme sa :
function testDb(){
$ini = new DataOjects();
$sql = 'SELECT * FROM test';
//$prepare = $ini->getInstance()->Prepare($sql);
$result = $ini->getInstance()->FetchAll($sql);
//var_dump($result);
return $result;
}
function displayIndex(){
$result=array();
$result = $this->testDb();
print_r($result);
foreach ($result as $tabs => $key){
$fetch = $key['nom'];
}
}
9. Par Babynus le 26/02/2009 13:39
L'idée est bonne, mais pourquoi se compliquer avec un singleton alors qu'une classe statique suffirait largement ?
Le seul point que je vois serait de pouvoir supprimer dans le futur le caractère singleton sans rien changer dans le code ... ce qui n'est pas possible si on est parti en statique.
Donc juste ave cune classe statique contenant l'instance de PDO (statique) avec une methode de type getPDO() qui initialise l'instance si elle ne l'est pas ou la retourne simplement dans le cas contraire.
On a juste à faire un SPDO::getPDO()->methodPDO()... (en remplacant le -> par :: pour valider l'appel statique.
Et pour répondre à Bob, la solution est toute simple : au lieu de stocker un seul objet PDO, tu peux stocker un tableau de couple (type DB, Objet PDO)
10. Par Kirk le 29/03/2009 15:36
Bonjour,
je n'ai pas vraiment compris comment fonctionnait la composition ni à quoi servait la variable $instance. Ça semble lié mais bon ça m'échappe.
J'ai créé un presque singleton d'hérétique pour pouvoir utiliser les méthodes de la classe PDO (en gros j'ai collé getintance() dans le constructeur). Est ce vraiment cradingue?
Merci, en tout cas pour ces tutoriels qui manquent cruellement sur le net.
11. Par thierry le 18/05/2009 22:37
Babynus > Pourrais-tu donner le code complet de ce à quoi tu penses ? Car je ne saisis pas exactement ce que tu proposes.
Kirk > Le problème de ton "presque singleton" c'est le "presque" Ton constructeur est public, donc tu ne garantis pas l'unicité de l'instance dans ton programme, ce qui est contraire à la définition du singleton.
12. Par tyty le 11/10/2009 18:51
Bonjour,
j'utilise ta classe et après je veux faire ça :
$db=Connexion_serveur::getInstance();
$sth = $db->prepare('INSERT INTO logs_erreurs (date, texte) VALUES (?, ?';
$sth->bindParam(1, "test", PDO:ARAM_STR);
$sth->bindParam(2, "texte", PDO:ARAM_STR);
$sth->execute();
Mais ca marche pas sais-tu pourquoi ? Il me marque : : Call to undefined method Connexion_serveur:repare()
Merci
1. Par x@v le 25/12/2008 15:01