Abstraction SQL, overload, variable de fonction

Questions sur le développement PHP.

Modérateur : Modérateurs

Avatar de l’utilisateur
karrakis
Membre ancien
Membre ancien
Messages : 444
Inscription : lun. 26 avr. 2004, 12:29
Localisation : Paris
Contact :

Abstraction SQL, overload, variable de fonction

Messagepar karrakis » lun. 15 nov. 2004, 11:36

En PHP, comme dans les autres langages, nous sommes souvent obliger des planter des forets de conifères ou bien utiliser un switch quand on veux implémenter un switch ou bien une 'abstraction'. Le terme abstraction ici n'est pas à prendre dans le sens d'une classe abstraite dont les methodes abstraite doivent etre implémenter dans les classes filles, mais une classe qui permets de nous debarrasser d'un code spécifique à un module (ex:mysql) pour obtenir un code 'générique'. Dans cet article, je vais essayer de présenter des mécanismes qui permette d'implémenter de telle choses sans pour autant nuire à la lisibilité du code. Afin, d'illustrer mes propos on discutera de la création d'une librairie pour se démarquer d'une base de données spécifique.

I Ce que l'on commence par faire.

Dans un premier temps, on est tenté de faire une classe avec des fonctions qui appellerais les fonctions de la bdd avec un switch ou des ifs.
Ce qui donnerais :

Code : Tout sélectionner

 class bdd {
  var $type;
  function connect($host, $user, $pass){
  switch ($this->type){
     case 'mysql' :
    mysql_connect($host, $user, $pass);
    //un certain nombre de fonction
    default :
   
  }
   }
   //toutes les fonctions.
 
 }
 

Cette méthode a un gros problème de lisibilité et de taille lorsque l'on implémente beaucoup de fonction.
La liste des function *_connect en php :
cyrus_connect
dbx_connect
hw_connect
hw_pconnect
ibase_connect
ifx_connect
ifx_pconnect
ldap_connect
mcve_connect
msql_connect
msql_pconnect
mysql_connect
oci_connect
oci_pconnect
odbc_connect
pg_connect
pg_pconnect
sesam_connect
yaz_connect

Il en existe autant pour les fonction *_query etc.

II Des variables, des fonctions, des variables de fonctions.

PHP nous propose les variable de fonctions qui sont des variables qui permettent d'appeler une fonction. Ces variables sont des simples chaînes de caractères dont la valeur correspond à une fonction.

par exemple :

Code : Tout sélectionner

function aurevoir(){
   print "bye";
}

function hello($nom){
   print "bonjour, $nom";
}

$a="hello";
$a("karrakis"); //Affichera => bonjour, karrakis.
$a="aurevoir;
$a(); //Affichera => bye

Bien sur, si la fonction n'existe pas ce code generera une erreur fatale. Pour éviter ce genre de soucis, il conviens de vérifier que la fonction existe. On peut le faire soit par la function : function_exists($fonction); soit en utilisant la fonction call_user_func($fonction); qui appellera la fonction $fonction et generera un avertissement si celle ci n'existe pas.

Revenons à notre class bdd, celle ci subit du coup une cure d'amaigrissement, et à mon avis deviens beaucoup plus lisible. En effet, les fonction connect() sont de toute de type : bdd_connect(); on peut donc simplement ajouter '_connect' à la fin du type de bdd pour obtenir notre appel.

Code : Tout sélectionner

class bdd{
var $type;
   bdd($type){
  $this->type=$type;
  return $this;
   }
   function connect($host, $user, $pass){
  $str_fct=$this->type ."_connect";
  if(function_exists($str_fct)){
     return $str_fct($host, $user, $pass);
  }else{
     print "<b>Warning<b> : Undefined function $str_fct";
     return false;
  }
   }
}

Bien que plus legère, nous avons encore une dose impressionnante de fonction a implémenté, par exemple pour mysql :
mysql_affected_rows
mysql_change_user
mysql_client_encoding
mysql_close --
mysql_connect --
mysql_create_db --
mysql_data_seek --
mysql_db_name --
mysql_db_query --
mysql_drop_db --
mysql_errno --
mysql_error --
mysql_escape_string --
mysql_fetch_array --
mysql_fetch_assoc --
mysql_fetch_field --
mysql_fetch_lengths --
mysql_fetch_object --
mysql_fetch_row --
mysql_field_flags --
mysql_field_len --
mysql_field_name --
mysql_field_seek --
mysql_field_table --
mysql_field_type --
mysql_free_result --
mysql_get_client_info --
mysql_get_host_info --
mysql_get_proto_info --
mysql_get_server_info --
mysql_info --
mysql_insert_id --
mysql_list_dbs --
mysql_list_fields --
mysql_list_processes --
mysql_list_tables --
mysql_num_fields --
mysql_num_rows --
mysql_pconnect --
mysql_ping --
mysql_query --
mysql_real_escape_string --
mysql_result --
mysql_select_db --
mysql_stat --
mysql_tablename --
mysql_thread_id --
mysql_unbuffered_query --

Vous en avez le courage? Pas moi car il y a encore 'mieux' à faire.

III Le module overload.

L'overload en PHP n'est, contrairement à son nom, pas un module qui permets de surcharger une méthode, mais plutôt de surcharger des membres et surtout, d'avoir une fonction qui est appelée si la méthode appelée n'est pas définie.
Pour ce faire, il y a 3 méthodes magiques :
__set(); pour ajouter des membres dynamiquement.
__get(); pour obtenir un membre crée dynamiquement.
__call(); pour gérer une méthode inexistante. (?)

En terme plus français, si la méthode existe php appelle la méthode sinon il appelle __call.
Cela va nous permettre de nous affranchir de la tripoté de fonction que nous avons à gérer dans notre class bdd, qui devenant vraiment abstraite s'appellera désormais abstractbdd.

La définition de call est la suivante :
bool __call(string methode_name, array arguments, mixed return_value);
si la valeur de retour de call est vrai c'est que la fonction est considéré comme définie.
si la valeur de retour de call est false alors la fonction est considéré comme non définie.

Dans un premier temps, nous allons construire l'appel de fonction :
Il nous suffit d'ajouter method_name à $this->type, ensuite, nous devons vérifier que la fonction existe bien si tel est le cas, on appellera cette fonction sinon nous nous contentons d'afficher un avertissement et de nous arrêter la.

Code : Tout sélectionner

   $str_tmp=$this->type . '_' . $method_name;
   if(function_exists($str_tmp)){//Si la fonction existe on peut continuer;
  //On fait quelque chose
 
   }else {
  print "<br/><b>Warning:</b> Undefined method $str_tmp<br/>";
   return true;
   }

Dans un deuxième temps, il faut construire la liste des arguments de la fonction. En effet, à la base nous ignorons le nombre d'arguments que nous allons avoir. Donc nous allons affecter la valeur de chaque arguments à une variable via la variable de variable ($$str_args dans notre exemple), et ensuite les ajouter dans une chaîne de caractère.

Code : Tout sélectionner

   $i=0;
   foreach($arguments as $value){
  $str_args='param'.$i; // Création du nom
  $$str_args=$value; //Affectation
  $param_list[]='$' . $str_args; //Mémorisation du nom.
  $i++; 
   }

Arrivé ici, nous avons x param et un tableau mémorisant les noms. Il nous reste plus qu' à créer la listes des paramètres et à évaluer la fonction et ses paramètres. Pour obtenir la liste des arguments, il nous suffis de joindre le tableau avec ','. Ensuite, nous utiliserons la fonction eval($str); qui évalue le chaine $str comme du code PHP.

Code : Tout sélectionner

   $params= join($param_list,',');//on crée la liste d'argument $param0,$param1,...,$paramN
   eval("\$return_value=$str_tmp($params);");
   return true;

Voici le code complet de notre class :

Code : Tout sélectionner

class abstractbdd{
var $type;
   function abstractbdd($type){
  $this->type=$type;
   }

   function __call( $method_name , $arguments , &$return_value){
  //Création de l'appel de fonction.
  $str_tmp=$this->type . '_' . $method_name;
  if(function_exists($str_tmp)){//Si la fonction existe on peut continuer;
  // On ne sais pas combien d'argument la fonction reçoit.
  $i=0;
  foreach($arguments as $value){
     $str_args='param'.$i; // Création du nom
     $$str_args=$value; //Affectation
     $param_list[]='$' . $str_args; //Mémorisation du nom.
     $i++; 
  }
  $params= join($param_list,',');//on crée la liste d'argument $param0,$param1,...,$paramn
  eval("\$return_value=$str_tmp($params);");
  return true;
  }else {
  print "<br/><b>Warning:</b> Undefined method $str_tmp<br/>";
  return true;
  }
   }
   
}

overload('abstractbdd');

overload('abstractbdd'); permets justement la mise en place de l'overload en PHP, c'est à dire d'appeler __class si la méthode n'existe pas.
Attention : Une class overloader ne peut pas être hériter par une class non overloader.

IV Fignolage.

Le dernier défaut de notre class est que pour l'instant si j'utilise 2 sgbd qui n'ont pas exactement les mêmes fonctions (comme mysql et sqlite). Pour regler ce dernier détail, il nous faut gérer des équivalences, par exemple entre open et connect.
Pour ce faire, on va définir une constante qui mémorisera les équivalences, et construire l'appel de fonction par rapport aux equivalences définies.

Code : Tout sélectionner

   $str_tmp=$this->type . '_' . $method_name;
   if(!function_exists($str_tmp)){//Si la fonction n'existe pas on cherche dans les équivalences;
  $equiv=explode(';',abddEquiv);
  $fct_list=preg_grep("/$method_name/", $equiv);
  if(empty($fct_list)){//Il n'y a pas d'équivalence.
     print "<br/><b>Warning</b> : Undefined method : $method_name </br/>";
     $return_value=false;
     return true;
  }
  $fct_list=array_values($fct_list);
  $fct_list=explode(',', $fct_list[0]);
  $i=0;//On recherche la fonction équivalente qui existe.
  while(!function_exists($this->type .'_' . $fct_list[$i]) && $i<count($fct_list)){
     $i++;
  }
  print $i . " " . $fct_list[$i];
  $str_tmp=$this->type ."_" . $fct_list[$i];
  // Si $this->type ne reference pas un sgbd connu $str_tmp contiendra une fonction non définie
   }
   

Le passage d'arguments n'ayant pas changé on mettre de suite le code complet de la classe.

Code : Tout sélectionner

define('abddEquiv', "connect,open;query,execute");
class abstractbdd{

var $type;
   function abstractbdd($type){
  $this->type=$type;
   }

   function __call( $method_name , $arguments , &$return_value){
  //Création de l'appel de fonction.
  $str_tmp=$this->type . '_' . $method_name;
  if(!function_exists($str_tmp)){//Si la fonction n'existe pas on cherche dans les équivalences;
     $equiv=explode(';',abddEquiv);
     $fct_list=preg_grep("/$method_name/", $equiv);
     if(empty($fct_list)){//Il n'y a pas d'équivalence.
    print "<br/><b>Warning</b> : Undefined method : $method_name </br/>";
    $return_value=false;
    return true;
     }
     $fct_list=array_values($fct_list);
     $fct_list=explode(',', $fct_list[0]);
     $i=0;//On recherche la fonction équivalente qui existe.
     while(!function_exists($this->type .'_' . $fct_list[$i]) && $i<count($fct_list)){
    $i++;
     }
       $str_tmp=$this->type ."_" . $fct_list[$i];
        //soit on a une fonction valide soit le type de bdd est faux.
   if(!function_exists($str_tmp)){
             $return_value=false;
             return true;
        }
  }
  //la fonction existe ou bien une équivalence.
  $i=0;
  foreach($arguments as $value){
     $str_args='param'.$i; // Création du nom
     $$str_args=$value; //Affectation
     $param_list[]='$' . $str_args; //Mémorisation du nom.
     $i++; 
  }
  if(count($param_list)>0)
     $params= join($param_list,',');//on crée la liste d'argument $param0,$param1,...,$paramn
  else
     $params='';
  eval("\$return_value=$str_tmp($params);");
  return true;
   }
   
}

overload('abstractbdd');


On peut reprocher à cette classe l'utilisation d'un eval pour executer notre fonction. Encore une fois PHP nous aide gentiment en proposant la fonction call_user_func_array "mixed call_user_func_array(string fonction, array argument);" qui permets d'apeller la fonction fonction en lui passant la liste d'arguments dans un tableau (et celui-ci se debrouille pour que cela ne gene pas la fonction). Du coup, notre classe deviendrais :

Code : Tout sélectionner

define('abddEquiv', "connect,open;query,execute");

class abstractbdd{

   var $type;
   function abstractbdd($type){
     $this->type=$type;
   }

   function __call( $method_name , $arguments , &$return_value){
     //Création de l'appel de fonction.
     $str_tmp=$this->type . '_' . $method_name;
     if(!function_exists($str_tmp)){//Si la fonction n'existe pas on cherche dans les équivalences;
       $equiv=explode(';',abddEquiv);
       $fct_list=preg_grep("/$method_name/", $equiv);
       if(empty($fct_list)){//Il n'y a pas d'équivalence.
         print "<br/><b>Warning</b> : Undefined method : $method_name </br/>";
         $return_value=false;
         return true;
       }
       $fct_list=array_values($fct_list);
       $fct_list=explode(',', $fct_list[0]);
       $i=0;//On recherche la fonction équivalente qui existe.
       while(!function_exists($this->type .'_' . $fct_list[$i]) && $i<count($fct_list)){
         $i++;
       }
         $str_tmp=$this->type ."_" . $fct_list[$i];
          //soit on a une fonction valide soit le type de bdd est faux.
     if(!function_exists($str_tmp)){
                 $return_value=false;
                 return true;
          }
     }
  $return_value=call_user_func_array($str_tmp, $arguments);
     return true;
   }

}

overload('abstractbdd');



Conclusion :

Voila, on a atteins la fin de cet 'article' présentant le module overload qui permets de créer des abstraction assez puissante. Enfin, overload marche avec php > 4.3.0 et <PHP 5. Ce mécanisme existe naturellement en PHP 5, par contre, la déclaration de la fonction __call est différent :
mixed __call(string method_name, array arguments);
Et elle se comporte comme une fonction classique en retournant la valeur de return.
Ps : Cette classe conserve des limitations. Il est fortement déconseillé d'utiliser une fonctions propres à un sgbd (mysql_insert_id par exemple), car il se peut qu'il n'y ai aucune équivalence. Mais si le developpeur se limite aux fonctions standard (connect, query, fetch_row, fetch_array, num_rows, close) il lui permets de s'affranchir définitivement de la BDD.

Changelog :
Correction d'un 'bug' dans la parti fignolage.
Précision du terme abstraction
Ajout du PS.
Proposition d'une classe sans eval().

Revenir vers « PHP »

Qui est en ligne ?

Utilisateurs parcourant ce forum : Aucun utilisateur inscrit et 1 invité