Classe mysql utilisant les fonctions pdo

Soyez le premier à donner votre avis sur cette source.

Snippet vu 8 072 fois - Téléchargée 16 fois

Contenu du snippet

Ceci est une classe que j'ai développé pour mes besoin personnels.
Elle permet de se connecter à une base MySQL et utilise l'extension PHP Data Objets (PDO). Exit les traditionnelles fonctions mysql_.
PHP5 est donc requis ainsi que l'activation de la prise en charge de MySQL dans PDO.

Les fonctions présentes sont soit des fonctions que j'utilise régulièrement, soit des fonctions dont j'ai eu l'utilité à un moment donné.

Je vous le partage en espérant qu'elle puisse être utile à certains et, comme rien n'est parfait, je reste ouvert à toute critique constructive. :)

Source / Exemple :


<?php
/**

  • Classe de manipulation de base MySQL
  • Dernière mise à jour : 17/09/2010
    • /
class Db { private $connection = false; //Instance PDO private $ActiveTransaction = false; //Flag indiquant si une transaction a été démarrée private $sql = ''; //Dernière requête SQL executée /** Fonction Constructeur
  • Desc : Initialise la connexion
  • Params : (string)$Serveur = Adresse du serveur MySQL
  • (string)$Utilisateur = Nom d'utilisateur de la base MySQL
  • (string)$MotDePasse = Mot de passe MySQL
  • (string)$BaseDeDonnees = Nom de la base de données
  • Return : Void
    • /
public function __construct($Serveur, $Utilisateur, $MotDePasse, $BaseDeDonnees) { try { //On instancie l'objet PDO et on se connecte à la base $this->connection = new PDO('mysql:host='.$Serveur.';dbname='.$BaseDeDonnees, $Utilisateur, $MotDePasse, array(PDO::ATTR_PERSISTENT => true)); } catch (PDOException $e) { //En cas d'erreur, on affiche un message avec l'erreur retournée echo 'Connexion échouée : <b>'.$e->getMessage().'</b>'; } } /** Fonction BeginTransaction
  • Desc : Démarre une transaction MySQL
  • Params : None
  • Return : Void
    • /
public function BeginTransaction() { //S'il n'existe pas de transaction active, on l'active if (!$this->ActiveTransaction) $this->ActiveTransaction = $this->connection->beginTransaction(); } /** Fonction CommitTransaction
  • Desc : Valide une transaction MySQL
  • Params : None
  • Return : (bool)true si la validation s'est bien passée, sinon false
    • /
public function CommitTransaction() { try { //Si une transaction est active if ($this->ActiveTransaction) { //On la valide if ($this->connection->commit()) { //Il n'y a plus de transaction active $this->ActiveTransaction = false; //La validation s'est bien passée donc on retourne true return true; } else { //Si la validation retourne false, on créé une erreur Throw new Exception('La transaction n\'a pas pu être validée.<br />'.$this->GetError()); } } else { //S'il n'y a pas de transaction active, on créé une erreur Throw new Exception('Aucune transaction active'); } } catch (Exception $e) { //En cas d'erreur, on affiche un message avec l'erreur retournée echo 'Impossible de valider la transaction : <b>'.$e->getMessage().'</b>'; return false; } } /** Fonction GetFrom
  • Desc : Retourne un tableau contenant les enregistrements selon les conditions spécifiées
  • Params : (string|array)$Table = Table SQL
  • (string|array)$Fields = Champ(s) à retourner
  • (string|array)$Where = Conditions WHERE
  • (string|array)$Order = Champ(s) de tri
  • (string|array)$GroupBy = Champ(s) à grouper
  • (string)$Limit = Limitation de la requête
  • Return : (array)résultat de la requête
    • /
public function GetFrom($Table, $Fields = false, $Order = false, $Where = false, $GroupBy = false, $Limit = false) { //On initialise une nouvelle requête $Sql = ''; //On créé la requête section par section //Section SELECT $Sql .= $this->MakeSQLPart('SELECT', $Fields?$Fields:'*'); //Section FROM $Sql .= $this->MakeSQLPart('FROM', $Table); //Section WHERE $Sql .= $this->MakeSQLPart('WHERE', $Where); //Section ORDER BY $Sql .= $this->MakeSQLPart('ORDER BY', $Order); //Group By s'il y a $Sql .= $this->MakeSQLPart('GROUP BY', $GroupBy); //Limit s'il y a $Sql .= $this->MakeSQLPart('LIMIT', $Limit); //On retourne le tableau de résultat renvoyé par la fonction Query2Array(); return $this->Query2Array($Sql); } /** Fonction GetValue
  • Desc : Retourne une valeur recherchée dans la base
  • Params : (string)$Table = Table SQL
  • (string|array)$Field = Champ recherché
  • (string|array)$Where = Critère WHERE
  • Return : (string)valeur trouvée, sinon false
    • /
public function GetValue($Table, $Field, $Where) { //On requête pour obtenir la valeur recherchée $result = $this->GetFrom($Table, 'DISTINCT('.$Field.')', false, $Where); //Si la requête a renvoyé un résultat if (count($result) === 1) { //On vérifie si la variable $Field a été renseigné avec la table //Du type matable.monchamp //On récupère la position du point $PositionPoint = strpos($Field, '.'); //Et on déduit le nom du champ $FieldName = substr($Field, $PositionPoint ? $PositionPoint + 1 : 0); //On retourne la valeur recherchée return $result[0][$FieldName]; } else { //Sinon il n'y a aucun ou plus d'un résultat donc on retourne false //Car on veut retourner une et une seule valeur return false; } } /** Fonction InTable
  • Desc : Indique si une valeur est contenue par la table et le champ donné
  • Params : (string)$Needle = valeur recherchée
  • (string)$Table = nom de la table
  • (string)$Field = nom du champ
  • (bool)$Strict = si vaut true, recherche la valeur exacte
  • Return : (bool)true si la valeur a été trouvée, sinon false
    • /
public function InTable($Needle, $Table, $Field, $Strict = false) { //On recherche la valeur $Needle dans la table $Table et le champ $Field $result = $this->GetFrom($Table, $Field, false, $Field.'` LIKE "'.($Strict?$Needle:'%'.$Needle.'%').'"'); //S'il y a au moins un resultat, on retourne true sinon false return count($result) ? true : false; } /** Fonction LastInsertId # Desc : Retourne l'id du dernier enregistrement inseré
  • Params : None
  • Return : (int)ID du dernier enregistrement inséré
    • /
public function LastInsertId() { //Requête permettant de récupérer le dernier id inséré $sql = 'SELECT LAST_INSERT_ID() as last_id'; $query = $this->connection->query($sql); //On retourne le résultat de la requête return $query->fetchColumn(); } /** Fonction Query
  • Desc : Execute une requête SQL
  • Params : (string)$sql = Requête SQL
  • Return : (int)Nombre d'enregistrements affectés par la requête
    • /
public function Query($Sql) { //On enregistre la requête pour pouvoir la ressortir au besoin $this->sql = $Sql; try { //On execute la requête $query = $this->connection->exec($this->sql); //Si une erreur s'est produite if (($error = $this->GetError()) !== false) { //On annule la transaction $this->RollBackTransaction(); //On retourne l'erreur Throw new Exception($error); } else //Sinon on renvoie le résultat de la requête (nombre d'enregistrements affectés) return $query; } catch(Exception $e) { //On affiche le message d'erreur echo 'Une erreur s\'est produite lors de la validation de la requête : <b>'.$e->getMessage().'</b>'; //On retourne false return false; } } /** Fonction Query2Array
  • Desc : Retourne sous forme de tableau le résultat d'une requête de sélection
  • Params : (string)$sql = Requête SQL
  • Return : (array)Résultat de la requête, sinon false en cas d'erreur
    • /
public function Query2Array($Sql) { //On enregistre la requête pour pouvoir la ressortir au besoin $this->sql = $Sql; try { //On execute la requête $query = $this->connection->query($this->sql); //Si un erreur s'est produite if (($error = $this->GetError()) !== false) { //On retourne l'erreur Throw new Exception($error); } else //Sinon on retourne le tableau de résultat return $query->fetchAll(PDO::FETCH_ASSOC); } catch (Exception $e) { //On affiche le message d'erreur echo 'Erreur de requête SQL : <b>'.$e->getMessage().'</b><br />Rappel de la requête : <b>'.$this->sql.'</b>'; //On retourne false return false; } } /** Fonction RollBackTransaction
  • Desc : Annule une transaction MySQL
  • Params : None
  • Return : Void
    • /
public function RollBackTransaction() { //Si une transaction est active if ($this->ActiveTransaction) { //On l'annule if ($this->connection->rollBack()) //Et on modifie le tag de transaction $this->ActiveTransaction = false; } } /** Fonction GetError
  • Desc : Renvoi le dernier message d'erreur formaté
  • Params : None
  • Return : (string)Message d'erreur
    • /
private function GetError() { //On récupère les informations sur l'erreur $ErrorMessage = $this->connection->errorInfo(); //S'il n'y a pas d'erreur if ($ErrorMessage[0] === '00000') //On retourne false return false; else //Sinon on retourne le message d'erreur formaté return "<p>Error : ".$ErrorMessage[0]." ".$ErrorMessage[1]." ".$ErrorMessage[2]."<br />SQL : ".$this->sql."</p>"; } /** Fonction MakeSQLPart
  • Desc : Constructeur de requête SQL
  • Params : (string)Section à créer (SELECT, FROM, WHERE, ORDER BY, GROUP BY, LIMIT)
  • (string|array)Contenu de la section à créer
  • Return : (string)Partie de la requête demandée, sinon false
    • /
private function MakeSQLPart($Part, $Values) { //Sections disponibles à la création $AllowedParts = array('SELECT', 'FROM', 'WHERE', 'ORDER BY', 'GROUP BY', 'LIMIT'); //Initialisation de la requête $Sql = ''; //Si la section demandée fait bien partie des sections disponibles if (in_array($Part, $AllowedParts)) { //Si le contenu n'est pas vide if (!empty($Values)) //On créer la requête en ajoutant la section demandée $Sql .= ' '.strtoupper($Part).' '; else //Sinon le contenu est vide alors on retourne une chaine vide return $Sql; //Si le contenu est un tableau if (is_array($Values)) { //On prépare le séparateur //Si la section demandée est WHERE if ($Part == 'WHERE') //Le séparateur sera AND $separator = ' AND '; else //Sinon ce sera une virgule $separator = ', '; //Clé pour connaitre la position dans le tableau $Key = 1; //On parcours le tableau foreach ($Values as $Value) { //On ajoute la valeur courante du tableau $Sql .= $Value; //Si nous ne sommes pas à la fin du tableau if (($Key) != count($Values)) //On ajoute le séparateur $Sql .= $separator; //On incrémente de un pour l'élément suivant $Key++; } } else //Sinon le contenu est une chaine alors on l'ajoute directement à notre morceau de requête $Sql .= $Values; //Et on retourne le morceau de requête construit return $Sql; } else //Sinon si la section demandées n'est pas dans les sections disponibles, on retourne false return false; } /** Fonction GetLastSql
  • Desc : Retourne la dernière requête exécutée.
  • Params : None
  • Return : (string)Dernière requête SQL executée
    • /
public function GetLastSql () { return $this->sql; } } ?>

Conclusion :


Je pense que la classe est suffisamment commentée pour que je n'ai pas à vous montrer un exemple d'utilisation.
Mais si vous avez des questions, n'hésitez pas. J'y répondrai avec plaisir.

A voir également

Ajouter un commentaire

Commentaires

Vince66
Messages postés
28
Date d'inscription
mardi 10 février 2004
Statut
Membre
Dernière intervention
5 octobre 2011

Salut à vous deux,

Tout d'abord merci d'avoir pris le temps d'étudier mon code et de rédiger une critique très constructive :)

Pour te répondre Neigedhiver sur la gestion des erreurs, j'ai commencé à développer la classe pour des besoins perso et ça m'allait bien d'avoir un retour immédiat en cas d'erreur. Mais je me rends compte effectivement qu'à partir du moment où je la partage, il est plus logique de séparer la gestion des erreurs et la gestion des données pour qu'elle soit modulaire. Je vais travailler dessus afin d'aller dans ce sens.

Pourquoi ne pas avoir étendu PDO ? C'est surement très bête mais tout simplement parce que je n'y avais pas pensé. C'est aussi ce que je recherchais en déposant ma source sur CodesSources : avoir des retours constructifs qui me permettent de l'améliorer et corriger des points auxquels je n'avais pas pensé :)
Donc merci.

Pour la méthode Query2Array, le but était de simplifier le format de sortie des données afin de parcourir celles-ci avec un simple for ou foreach. En revanche, je comprends tout à fait la problématique des performances dégradées dues au double parcours des données.

Holala tu as entièrement raison pour le bout de code que tu cites, je n'avais absolument pas pensé au implode. Mea Culpa !

Merci pour la méthode InTable, en effet c'est plus simple.

Enfin pour vous répondre à tous les deux, dans la décomposition de requête, l'idée était encore une fois de simplifier la construction des diverses requêtes. Cependant, j'admets que cela manque de souplesse lorsque l'on veut faire des requêtes plus complexes. C'était une expérimentation très maladroite, j'avoue.

Rassure toi, j'aurais du mal à prendre pour "destructive" une critique aussi détaillée et argumentée ;)
Une remarque du style "C'est nul ! Retourne faire du Word", je l'aurais moins bien pris c'est sûr :D
Je vais prendre en compte vos remarques et essayer de faire une classe plus cohérente.
Merci encore !
neigedhiver
Messages postés
2483
Date d'inscription
jeudi 30 novembre 2006
Statut
Membre
Dernière intervention
14 janvier 2011
15
Ah une chose à laquelle je pensais depuis le début de la rédaction de mon commentaire et que j'ai perdue en cours de route...
Pour reprendre ce que dis Fenoril, ta décomposition de requêtes m'étonne : c'est comme si tu mélangeais ORM et DAO dans une sourcouche de PDO... Un joyeux mélange pas forcément facile à manipuler avec cette classe, en l'état...
neigedhiver
Messages postés
2483
Date d'inscription
jeudi 30 novembre 2006
Statut
Membre
Dernière intervention
14 janvier 2011
15
Salut,

Le code est propre, documenté, tu as choisi d'utiliser PDO : que des bons points.
Cependant, je note, vite-fait, des erreurs de conception :

1/ Dans le constructeur, tu mets un bloc try...catch : même si ça fonctionne (heureusement !) ce n'est pas très correct, dans la conception : les exceptions n'ont pas de raison, fondamentalement, d'être traitées dans une classe comme celle-ci. Ta classe est une surcouche de PDO (on y reviendra plus tard) ; PDO lève des exceptions => pourquoi ta classe les intercepterait-elle ? La seule raison valable serait : les traiter de manière plus approfondie (log dans un fichier, utilisation d'un cache, etc). Or, on ne peut pas dire qu'un vulgaire echo (pardon pour lui) soit plus approfondi qu'une véritable gestion d'exception. Conclusion : tu peux tout à fait attraper une PDOException, si tu lèves une exception perso DbException (ce qui resterait cohérent). Donc au lieu de faire un echo, lève une exception à toi : c'est à l'utilisateur de décider comment il les gère, pas à toi de lui imposer l'affichage d'un texte alors que si ça se trouve, i la décidé de ne rien faire afficher du tout par son script (genre je traite les données d'un formulaire, je n'affiche rien, puis je redirige avec header()... sauf que paf, tu as forcé l'envoi des entêtes HTTP, le script plante complètement).

2/ Comme je le disais, ta classe est une curcouche de PDO. Pourquoi ? Pourquoi ne pas simplement étendre PDO ? Tu ne fais même pas un véritable wrapper, puisque tu limites les méthodes disponibles... Pour faire un véritable wrapper, il faudrait au minimum utiliser la méthode magique __call() pour pouvoir utiliser les méthodes de PDO. Ta classe ne permet pas de se passer des méthodes de PDO que tu ne remplaces pas...

3/ Ta méthode Query2Array() me paraît inutile en plus d'être mal codée. De même que pour le constructeur, tu devrais lever une nouvelle exception plutôt que simplement intercepter celle qui se présente et afficher un message d'erreur. Qui plus est, si une erreur SQL se produit, tu lèves une exception standard : ce n'est pas rendre service à l'utilisateur qui ne va plus savoir quand intercepter des exceptions ni lesquelles peuvent être attrapées.
Par ailleurs, PDO permet d'itérer sur les résultats PDOStatement, ce que ne permettait pas l'extension mysql sans une classe supplémentaire : pourquoi tout récupérer dans un tableau ? C'est non seulement superflu, mais surtout non performant : PDO va itérer une première fois sur le résultat pour remplir le tableau, puis l'utilisateur va itérer sur le tableau pour traiter les résultats (et il est capable d'itérer encore pour les afficher... sisi, ça arrive, j'en ai vus).

Bon, d'autres détails qui me gênent un peu...

Dans ta méthode makeSQLPart() :
# foreach ($Values as $Value)
# {
# //On ajoute la valeur courante du tableau
# $Sql .= $Value;
# //Si nous ne sommes pas à la fin du tableau
# if (($Key) != count($Values))
# //On ajoute le séparateur
# $Sql .= $separator;
# //On incrémente de un pour l'élément suivant
# $Key++;
# }

Pouah, c'est répugnant... dire qu'on peut faire ça en une ligne (de manière certainement plus optimisée en terme de performances) en une seule ligne avec implode() (et en plus c'est bien plus lisible !).

Dans la méthode InTable() :
return count($result) ? true : false;
Je te propose :
return (bool) $this->GetFrom($Table, $Field, false, $Field.'` LIKE "'.($Strict?$Needle:'%'.$Needle.'%').'"');

D'une manière générale, essaie de proscrire la gestion d'erreur dans la classe avec des echo : une classe permet à l'utilisateur de modulariser son code ; il ne FAUT PAS l'obliger à afficher quelque chose s'il ne le souhaite pas.
Pour ta méthode GetError(), tu devrais simplement retourner le message d'erreur, avec le minimum de mise en forme. Ou alors, laisse l'utilisateur modifier le masque, via une propriété avec un setter, plutôt que de lui imposer un formatage html avec et
: si je veux afficher ça dans une liste
? Ou dans un
avec un id spécifique, pour le mettre en forme avec un CSS ?

Bon... Ne vois pas dans mon commentaire une critique destructrice de ta source : l'intention est louable, c'est plutôt bien codé et surtout très clair (ce qui est rare quand même !!). Vois-y plutôt un peu de matière pour pouvoir améliorer ta source et la rendre plus intéressante (y compris pour toi). Je te donne des conseils qui doivent également te permettre de coder de manière plus souple et modularisable, en séparant autant que possible l'affichage des données de leur traitement. Ta classe ne sert pas à afficher, juste à traiter : laisse l'utilisateur afficher comme il l'entend.
Et n'hésite pas à utiliser des classes d'exceptions personnalisées qui pourront réellement servir à l'utilisateur (parce que c'est à lui qu'il faut penser, pas au développeur).

Bonne continuation !
cs_fenoril
Messages postés
24
Date d'inscription
vendredi 28 mars 2008
Statut
Membre
Dernière intervention
12 juin 2011

Bonjour !

N'est-ce pas un peu dommage de ne pas utiliser les requètes préparées afin d'obtenir une sécurisations satisfaisante en plus de l'automatisation ? Y aurait-il une contrindication que je n'aurais pas compris ? (Ce n'est aucunement de l'ironie, je suis pas assez fort pour voir tous les détails...)

Ceci dit, je ne vois pas l'intéret de la décomposition des requètes sql... Qu'y trouves-tu ?

Par ailleurs merci pour le partage de code.

Vous n'êtes pas encore membre ?

inscrivez-vous, c'est gratuit et ça prend moins d'une minute !

Les membres obtiennent plus de réponses que les utilisateurs anonymes.

Le fait d'être membre vous permet d'avoir un suivi détaillé de vos demandes et codes sources.

Le fait d'être membre vous permet d'avoir des options supplémentaires.