Bases de données Objet singleton pour la connexion 01:08:06 Programmation Web 2012-2013
Problème posé Développement d'un site nécessitant une BD : Connexion en début de chaque page PHP Requête(s) Déconnexion en fin de chaque page PHP Développement objet d'un site avec une BD : Connexions à divers endroits dans les méthodes Requête(s) à divers endroits dans les méthodes Déconnexion en fin de page PHP Quand doit-on se connecter, se déconnecter ? 01:08:06 Programmation Web 2012-2013
Singleton : objet à instance unique Réalisation : Solution : Singleton Singleton : objet à instance unique Réalisation : 1 attribut statique Singleton 1 point d'accès méthode statique limiter l'accès au constructeur privé / protégé interdire le clonage 01:08:06 Programmation Web 2012-2013
Solution : Singleton Adaptation à la connexion BD PDO : myPDO 1 attribut statique mypdo Ressource BD = objet PDO 1 point d'accès Méthode statique get() constructeur privé établit la connexion à la BD destructeur termine la connexion à la BD mise hors service de la méthode __clone throw new Exception("…") ; 01:08:06 Programmation Web 2012-2013
Durée de vie de l'objet myPDO Nature de l'objet membre statique d'une classe : variable globale Construction créé au premier appel de myPDO::get() Destruction variable globale : durée de vie = le script détruit en fin de script Bilan : connexion lors de la première requête déconnexion à la fin du script fonctionnement propre, transparent pour l'utilisateur 01:08:06 Programmation Web 2012-2013
Une implémentation <?php /// Classe permettant de faire une connexion unique et automatique à la BD final class myPDO { /// Singleton private static $mypdo = null ; /// Message de debogage private static $debug = true ; /// Data Source Name private static $dsn = null ; /// Utilisateur private static $user = null ; /// Mot de passe private static $pass = null ; /// Connexion à la base private $pdo = null ; 01:08:06 Programmation Web 2012-2013
Une implémentation /// Constructeur privé private function __construct() { self::msg("Demande construction PDO...") ; if ( is_null(self::$dsn) || is_null(self::$user) || is_null(self::$pass)) throw new Exception("Construction impossible : les paramètres de connexion sont absents") ; // Etablir la connexion $this->pdo = new PDO(self::$dsn, self::$user, self::$pass) ; // Mise en place du mode "Exception" pour les erreurs PDO $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION) ; self::msg("Construction PDO terminée") ; } 01:08:06 Programmation Web 2012-2013
Une implémentation /// Destructeur public function __destruct() { self::msg("Demande de destruction PDO...") ; // S'il y a une connexion établie... if (!is_null($this->pdo)) { // ... il faut se deconnecter self::msg("Demande de déconnexion...") ; $this->pdo = null ; self::$mypdo = null ; self::msg("Deconnexion effectuée") ; } self::msg("Destruction PDO terminée") ; } 01:08:06 Programmation Web 2012-2013
Une implémentation /// Récupérer le singleton public static function donneInstance() { self::msg("Recherche de l'instance...") ; // Une instance est-elle disponible ? if (!isset(self::$mypdo)) self::$mypdo = new myPDO() ; self::msg("Instance trouvée") ; return self::$mypdo->pdo ; } /// Fixer les paramètres de connexion public static function parametres($_dsn, $_user, $_pass) { self::$dsn = $_dsn ; self::$user = $_user ; self::$pass = $_pass ; 01:08:06 Programmation Web 2012-2013
Une implémentation /// Interdit le clonage du singleton public function __clone() { throw new Exception("Clonage de ".__CLASS__." interdit !") ; } 01:08:06 Programmation Web 2012-2013
Simple, non ? Utilisation require_once "connexion.pdo.template.class.php" ; // Paramétrage du singleton myPDO::parametres('oci:dbname=bd11', 'scott', 'tiger') ; // Connexion automatique $pdostat = myPDO::donneInstance()->query( "SELECT * FROM Images") ; while (($ligne = $pdostat->fetch()) !== false) { echo $ligne['id']."<br>\n" ; } // Déconnexion automatique en fin de script Simple, non ? 01:08:06 Programmation Web 2012-2013
Raffinement possible : Débogage Possibilité d'afficher des messages informatifs pour se rendre compte de ce qu'il se passe et dépister les erreurs : connexion… requête… Cadre Web : comment afficher des informations qui ne perturbent pas l'affichage de la page commentaires HTML <!-- … --> doit pouvoir être désactivé contenus non HTML Implémentation : attribut booléen statique et méthodes statiques 01:08:06 Programmation Web 2012-2013
Raffinement possible : Débogage Problème majeur, les méthodes de requête, préparation, etc sont des méthodes de l'objet PDO et non de myPDO Solution basique la méthode get retourne l'objet myPDO et non PDO l'objet myPDO doit implémenter toutes les méthodes de PDO qui consistent à lancer celles de PDO Solution "avancée" la méthode get() retourne l'objet myPDO et non PDO myPDO doit implémenter __call 01:08:06 Programmation Web 2012-2013
Une implémentation v2 /// Récupérer le singleton public static function donneInstance() { self::msg("Recherche de l'instance...") ; // Une instance est-elle disponible ? if (!isset(self::$mypdo)) self::$mypdo = new myPDO() ; self::msg("Instance trouvée") ; // return self::$mypdo->pdo ; return self::$mypdo ; } 01:08:06 Programmation Web 2012-2013
Une implémentation v2 /// Surcharge de toutes les méthodes indisponibles de myPDO pour pouvoir appeler celles de PDO public function __call($methodName/** Nom de la méthode */, $methodArguments /** Tableau des paramètres */) { // La méthode appelée fait-elle partie de la classe PDO if (!method_exists($this->pdo, $methodName)) throw new Exception("PDO::$methodName n'existe pas") ; // Message de debogage self::msg("PDO::$methodName (".implode($methodArguments, ", ").")") ; // Appel de la méthode avec l'objet PDO $result = call_user_func_array( array($this->pdo, $methodName), $methodArguments) ; return $result ; } 01:08:06 Programmation Web 2012-2013
Une implémentation v2 /// Affichage de messages de contrôle public static function msg($m /** Le message */) { if (self::$debug) Traceur::trace($m) ; } /// Mise en marche du debogage public static function debug_on() { self::$debug = true ; /// Arrêt du debogage public static function debug_off() { self::$debug = false ; 01:08:06 Programmation Web 2012-2013
Une implémentation v2 - Débogage /// Singleton permettant de collecter des messages informatifs class Traceur { // Gestion de l'instance unique private static $_instance = null ; /// Objet Traceur // Atttributs de l'objet private $_messages = array() ; /// Tableau des messages private $_temps = null ; /// Instant de création /// Constructeur privé private function __construct() { $this->_temps = microtime(true) ; } /// Interdire le clonage private function __clone() { throw new Exception("Clonage de ".__CLASS__." interdit !") ; 01:08:06 Programmation Web 2012-2013
Une implémentation v2 - Débogage /// Accesseur à l'instance qui sera créée si nécessaire private static function donneInstance() { if (is_null(self::$_instance)) { self::$_instance = new self() ; } return self::$_instance ; } /// Méthode statique de collecte de messages public static function trace($msg) { $instance = self::donneInstance() ; $instance->messages[] = $instance->duree() . " secondes : " .$msg ; 01:08:06 Programmation Web 2012-2013
Une implémentation v2 - Débogage /// Calcul du temps écoulé depuis la création du traceur private function duree() { return number_format(microtime(true) - $this->_temps, 4) ; } /// Méthode statique d'affichage des messages collectés public static function affiche($avant = "<!--", $apres = "-->") { $messages = self::donneInstance()->messages ; $traces = null ; if (count($messages)) { $traces .= "{$avant}\n" ; foreach ($messages as $m) { $traces .= "{$m}\n" ; } $traces .= "{$apres}\n" ; } return $traces ; } } 01:08:06 Programmation Web 2012-2013
Une implémentation v2 - Débogage /// Calcul du temps écoulé depuis la création du traceur private function duree() { return number_format(microtime(true) - $this->_temps, 4) ; } /// Méthode statique d'affichage des messages collectés public static function affiche($avant = "<!--", $apres = "-->") { $messages = self::donneInstance()->messages ; $traces = null ; if (count($messages)) { $traces .= "{$avant}\n" ; foreach ($messages as $m) { $traces .= "{$m}\n" ; } $traces .= "{$apres}\n" ; } return $traces ; } } 01:08:06 Programmation Web 2012-2013
Une implémentation v2 - Débogage Utilisation de la classe Traceur dans la classe myPDO : affichage des traces à la destruction /// Destructeur de myPDO public function __destruct() { self::msg("Demande de destruction PDO...") ; // S'il y a une connexion établie... if (!is_null($this->pdo)) { // ... il faut se deconnecter self::msg("Demande de déconnexion...") ; $this->pdo = null ; self::$mypdo = null ; self::msg("Deconnexion effectuée") ; } self::msg("Destruction PDO terminée") ; echo Traceur::affiche() ; } 01:08:06 Programmation Web 2012-2013
Raffinement possible : Débogage (suite) La solution est partielle : Les messages d'information concernent uniquement les méthodes de PDO Une requête préparée retourne un objet PDOStatement qui effectuera les méthodes bindValue, execute, fetch, … Comment tracer TOUTES les actions effectuées sur la base de données ? Implémenter un objet myPDOStatement 01:08:06 Programmation Web 2012-2013
Une implémentation V3 /// Encapsulation de PDOStatement final class myPDOStatement { /// L'objet PDOStatement private $pdoStatement ; /// Constructeur public function __construct($_pdoStatement /** L'objet PDOStatement */) { myPDO::msg("Construction PDOStatement") ; $this->pdoStatement = $_pdoStatement ; } /// Destructeur public function __destruct() { myPDO::msg("Destruction PDOStatement") ; $this->pdoStatement = null ; 01:08:06 Programmation Web 2012-2013
Une implémentation V3 /// Surcharge de toutes les méthodes indisponibles de myPDOStatement pour pouvoir appeler celles de PDOStatement public function __call($methodName/** Nom de la méthode */, $methodArguments /** Tableau des paramètres */) { // La méthode appelée fait-elle partie de la classe PDOStatement if (!method_exists($this->pdoStatement, $methodName)) throw new Exception("PDOStatement::$methodName n'existe pas") ; // Message de debogage myPDO::msg("PDOStatement::".$methodName. " (".var_export($methodArguments, true).")") ; // Appel de la méthode avec l'objet PDOStatement return call_user_func_array( array($this->pdoStatement, $methodName), $methodArguments) ; } 01:08:06 Programmation Web 2012-2013
Une implémentation V3.1 Utilisation de myPDOStatement au lieu de PDOStatement ? public function __call($methodName/** Nom de la méthode */, $methodArguments /** Tableau des paramètres */) { // La méthode appelée fait-elle partie de la classe PDO if (!method_exists($this->pdo, $methodName)) throw new Exception("PDO::$methodName n'existe pas") ; // Message de debogage self::msg("PDO::$methodName (".implode($methodArguments, ", ").")") ; // Appel de la méthode avec l'objet PDO $result = call_user_func_array( array($this->pdo, $methodName), $methodArguments) ; return $result ; } // Selon le nom de la méthode switch ($methodName) { // Cas 'prepare' ou 'query' => fetchNamed case "prepare" : case "query" : $result->setFetchMode(PDO::FETCH_NAMED) ; // Retourne un objet myPDOStatement return new myPDOStatement($result) ; // Dans tous les autres cas default : // Retourne le résultat return $result ; } 01:08:06 Programmation Web 2012-2013
Aide au débogage, suite (et fin ?) error_reporting(E_ALL) ; /** Mise en place d'une capture des exceptions non attrapées */ function exceptionHandler($exception /** L'Exception non attrapée */) { echo "<pre>\n" ; echo $exception->getMessage()."\n" ; echo "Trace d'execution :\n" ; echo $exception->getTraceAsString() ; echo "</pre>\n" ; } set_exception_handler('exceptionHandler') ; 01:08:06 Programmation Web 2012-2013