Créer son propre "framework" PHP, pourquoi ?

Par

Cela fait maintenant presque deux ans que je suis entré dans la vie active en tant que développeur et designer Web. Et en tant que tel, je suis souvent (comprenez : tout le temps) amené à faire quasiment les mêmes choses, bien que chaque projet soit différent.

Personnellement j’aime passer du temps à peaufiner un design. je déteste réécrire les mêmes fonctions pour chaque projet. Alors oui, il a les framework, mais pourquoi utiliser un bazooka lorsqu’on doit abattre une mouche ?

Attention, l’article est un chouilla trop long (beaucoup de lignes de code).

Oui utiliser une CMS déjà tout fait c’est bien, oui utiliser un framework en PHP Objet c’est bien. Seulement parfois, utiliser une arme de destruction massive n’est pas la bonne méthode à appliquer pour régler les choses les plus simples.

Ces dernier mois, au fil des projets qui le permettaient, j’ai pu me construire ma propre bibliothèque de fonctions, en essayant de l’améliorer à chaque itérations, tout en restant le plus générique possible pour permettre une très bonne portabilité de projet en projet. Bien sûr je n’ai pas hésité à piquer des idées dans les frameworks PHP, mais plutôt que de me trimballer toute la clique, j’ai maintenant un petit « framework » qui me permet de faire juste ce dont j’ai besoin. Cela m’a aussi permis de monter facilement notre propre CMS, lui aussi très basique.

Aujourd’hui j’ai donc envie de partager avec vous quelques unes de mes fonctions, car ça fait un petit moment que je n’ai pas publié un peut de code PHP dans mes pages.

Mes fonctions de CRUD

Etape obligatoire lorsqu’on souhaite agir sur des informations stockées en base de données, les célèbres fonctions INSERT, SELECT, UPDATE et DELETE. Après plusieurs itérations je suis arrivé à des fonctions quasiment utilisable par n’importe quel projet, même si je pense qu’elles ne sont pas encore parfaites, pour le moment elles me suffisent. Cependant, si vous avez de quoi les améliorer n’hésitez pas à me donner votre avis !

Ajouter un enregistrement dans une table

Après pas mal de projet dans lesquels j’avais une fonction d’ajout pour chaque type de contenu (page, article, etc.) enregistrés dans des tables différentes, j’ai essayé de trouver un moyen simple d’ajouter n’importe quelle données dans n’importe quelle table. Je suis donc passé de plusieurs fonction plus ou moins lourde à une bête fonction d’une quinzaine de ligne avec laquelle je peux faire ce que je veux, simplement en lui passant un tableau des données à ajouter, et le nom de la table dans laquelle faire cet INSERT.

/***
 *	insert des données dans la table en paramètre
 *	@datas	tableau des données à insérer dont la clé et le nom du champs dans la table
 *	@table	table dans laquelle insérer les données
 */
function add($datas, $table){
	$bdd = db(); //on ouvre la connexion à la base de données

	foreach($datas as $key => $value){
		$keys[] = $key;
		$values[] = $value;
	}

	$strSQL = "INSERT INTO ".$table." (";
	foreach($keys as $ky => $k){ $strSQL .= $k . ","; }

	$strSQL = substr($strSQL,0,-1) . ") VALUES(";
	foreach($values as $vl => $v){ $strSQL .= "?,"; }

	$strSQL = substr($strSQL,0,-1) . ")";

	$query = $bdd->prepare($strSQL);
	if($query->execute($values)) return $bdd->lastInsertId();
	else return false;
}

Comment utiliser la fonction ?

Après avoir vérifiées, échappées, etc., les informations soumisent à mon formulaire, il me suffit de créer un tableau dont les clés seront les noms des champs que je veux renseigner pendant mon INSERT.

Exemple : je veux renseigner le nom, l’adresse email et le commentaire dans la table « comments ».

$datas = array(
	'nom' => $nom,
	'email' => $email,
	'commentaire' => $commentaire
);

if(add($datas,'comments')) {
	return "cool";
} else return "pas cool";

Si tout c’est bien passé, la fonction add() me renvoie l’ID de l’enregistrement créé.

Mettre à jour les informations d’un enregistrement

Là aussi, c’est une fonction vraiment très simple. comme ma fonction add(), ma fonction update() requiert un tableau des données et le nom de la table à modifier. Seul petit changement, on doit aussi fournir l’ID de l’enregistrement à modifier.

/***
 *	met à jour les données de l'ID dans la table en paramètre
 *	@id	identifiant de la ligne à modifier
 *	@datas 	tableau des données à insérer dont la clé et le nom du champs dans la table
 *	@table 	table dans laquelle insérer les données
 */
function update($id, $datas, $table){
	$bdd = db();
	foreach($datas as $key => $value){
		$keys[] = $key;
		$values[] = $value;
	}

	$strSQL = "UPDATE ".$table." SET ";
	foreach($datas as $key => $value){
		$strSQL .= $key . " = ?,";
	} $strSQL = substr($strSQL,0,-1) . " WHERE id = ?";
	$values[] = $id;
	$query = $bdd->prepare($strSQL);
	if($query->execute($values)) return true;
	else return false;
}

Supprimer un enregistrement

Rien de bien compliqué là non plus, on supprime l’enregistrement dont l’ID est passé en paramètre dans la table elle aussi en paramètre.

/***
 *	supprime les données correspondant à l'ID dans la table en paramètre
 *	@id		identifiant de la ligne à supprimer
 *	@table	table sur laquelle on applique la suppression
 */
function delete($id, $table){
	$bdd = db();
	$strSQL = "DELETE FROM ".$table." WHERE id = ?";
	$query = $bdd->prepare($strSQL);
	//print_r(array($id));

	if($query->execute(array($id))) return true;
	else return false;
}

Retourner une liste d’enregistrements

Cette dernière est l’une des fonctions que j’ai eu le plus de mal à rendre générique tout en me permettant de faire beaucoup de choses avec, comme par exemple préparer le terrain pour l’utiliser dans le cadre des données scindées sur plusieurs pages.

Ici ma fonction me permet de récupérer à la fois les enregistrements de ma requête, le total d’enregistrements que retournerai ma requête si aucun paramètre LIMIT n’était renseigné, la requête exécutée (pour les tests en phase de développement) ou les erreurs possibles quant à l’exécution de cette requête.

/***
 * 	retourne le resultat d'un select
 *	@columns 	colonnes à selectionner pour la requête (ex: array('champ1','champ2') ou '*')
 *	@table 		nom de la table sur laquelle faire la requête
 *	@where 		champs sur lequels appliquer des conditions ( ex: array( 'champ1 =' => 'valeur', 'champ2 LIKE' => 'valeur%') )
 *	@concats 	[ AND | OR ]
 *	@order 		champs sur lequels appliquer le tri, et l'ordre pour chaque champs (ex: array('champ1' => 'ASC','champ2' => 'DESC') )
 *	@limit 		limit[0] => debut de la liste, limit[1] => nombre d'éléments dans la liste retournée (ex: array('0','20') )
 *
 *	return @retour	: tableau contenant la requête executée, les éventuelles erreurs et le resultat de la requête
 */
function get($columns = null, $table = null, $where = null, $concats = "AND", $order = null, $limit = null){
	$bdd = db();
	$retour = array(); //variable de type tableau, retournée par la fonction
	$rows = "";
	$clause = "";
	$sort = "";
	$limitStr = "";

	if(!is_null($columns) && !is_null($table)){

		// si $rows est un tableau ou égale à * tout va bien.
		if(is_array($columns)){
			foreach($columns as $column) { $rows .= $column .', '; }
			$rows = substr($rows,0,-2);
		} elseif($columns == '*'){
			$rows = '*';
		} else {
			$retour['erreur'] = "Les champs selectionné doivent être appelé depuis une variable Tableau";
		}

		if(!in_array(strtolower($concats),array('and','or'))){
			$retour['erreur'] = "<strong>".$concats."</strong> n'est pas une valeur autorisée pour concaténer des conditions. Utilisez 'OR' ou 'AND'.";
		}

		/*
		si @where est renseigné, on filtre les résultats grâce au tableau @where construit comme suit :
			array ('colname operateur' => 'valeur');
			ex: array('page_id =' => 5);
		sinon, on ne filtre pas les résultats
		*/
		if(!is_null($where) && is_array($where)){
			foreach($where as $k => $v){
				$clause .= $k." ? ".$concats." ";
				$values[] = $v;
			}
			$clause = " WHERE ".substr($clause,0,(-(strlen($concats)+2)));
		} elseif(!is_null($where) && !is_array($where)){
			$retour['erreur'] = "La clause WHERE doit être construite via une variable Tableau";
		} else {
			$clause = "";
		}

		//si $order est un tableau et n'est pas null
		if(!is_null($order) && is_array($order)){
			foreach($order as $k => $v){ $sort .= $k." ".$v.", "; }
			$sort = " ORDER BY ".substr($sort,0,-2);
		} elseif(!is_null($order) && !is_array($order)) {
			$retour['erreur'] = "ORDER BY doit être construit via une variable Tableau";
		} else {
			$sort = "";
		}

		if(!is_null($limit) && is_array($limit) && is_numeric($limit[0]) && is_numeric($limit[1])){
			$debut = $limit[0];
			$nbRows = $limit[1];
			$limitStr = " LIMIT " . $debut . "," . $nbRows;
		} elseif(!is_null($limit) && !is_array($limit)){
			$retour['erreur'] = "LIMIT doit être construit via un tableau de deux entiers";
		} else {
			$limitStr = "";
		}

		// on construit la requête
		$strSQL = "SELECT ".$rows." FROM ".$table.$clause.$sort.$limitStr;
		if(empty($retour['erreur'])){
			$query = $bdd->prepare($strSQL);
			$query->execute(@$values);
			$retour['requete'] = $strSQL;
			$retour['reponse'] = $query->fetchAll(PDO::FETCH_ASSOC);

			$sqlTotal = "SELECT COUNT(*) as total FROM ".$table.$clause.$sort;
			$q = $bdd->prepare($sqlTotal);
			$q->execute(@$values);
			$tot = $q->fetchAll(PDO::FETCH_ASSOC);
			$retour['total'] = $tot[0]['total'];
		}

	} else {
		$retour['erreur'] = "Impossible de créer la requete, les champs à selectionner et la table sont vide";
	}

	return $retour;
}

Comment utiliser la fonction ?

Exemple : Je veux récupérer les cinq articles les plus récents écrits par nighcrawl dont le titre contient le mot « framework » publié depuis la date à laquelle la requête est exécutée.

$champs = array('id','titre','nom','contenu');
$conditions = array(
	'nom =' => 'nighcrawl',
	'date < =' => date('Y-m-d H:i:s'),
	'titre LIKE' => '%framework%'
);
$trier = array('date' => 'DESC');
$limite = array(0, 5);

$resultat = get($champs,'articles',$conditions,"AND",$trier,$limite);
if(isset($resultat['reponse'])){
	foreach($resultat['reponse'] as $row){
		echo "<article>
		<header>
			<h1>".$row['titre']."</h1>
		</header>
		<div>".$row['contenu']."</div>
		<footer>Auteur : ".$row['nom']."</footer>
		</article>";
	}
}
else echo $resultat['erreur'];

Mettre en place un système de pagination

Je m’arrêterai ici parce que l’article est déjà pas mal long, et si vous êtes arrivé jusqu’ici je vous dis chapeau. Personnellement je me serai déjà arrêté :). On fini donc avec une fonction très utile pour générer rapidement des liens de pagination afin de répartir les données d’une requête sur plusieurs pages. Voici donc la belle fonction :

/***
 *	génère des liens de pagination : numeros de pages, 'suivants', 'précédents'
 *	@total	nombre total d'enregistremnts à paginer
 *	@nbpp	nombre d'enregistrements à afficher par page
 *	@link	chaine qui servira à construire les liens vers les différentes pages
 */
function pagination($total, $nbpp, $link){
	echo"<div class='pagination'>";
		/** Pagination **/
		//calcul du nombre de pages
		$nbLiens = ceil($total/$nbpp);

		if($nbLiens > 1){
			/** précédents **/
			if(isset($_GET['d']) && $_GET['d'] > 0){
				echo "<a href='".$link.($_GET['d']-$nbpp)."'>« Précédents</a>";
			} else {
				echo "<span>« Précédents</span>";
			}
			/** pages ***/
			for($i = 0; $i < $nbLiens; $i++){
				if($_GET['d'] == ($i*$nbpp)){
					echo "<span class='active_pagi'>".($i+1)."";
				} else {
					echo "<a href='".$link.($i*$nbpp)."'>".($i+1)."</a>";
				}
			}

			/** suivants **/
			if(isset($_GET['d']) && $_GET['d'] >= 0 && $_GET['d'] < ($total-$nbpp)){
				echo "<a href='".$link.($_GET['d']+$nbpp)."'>Suivants »</a>";
			} else {
				echo "<span>Suivants »</span>";
			}
		}
	echo "</div>";
}

La pagination va s’effectuée en deux temps, d’abord l’appel de la fonction get(), puis l’appel de la fonction pagination(). Si on réutilise l’exemple précédent :

$nbpp = 5; //5 articles par page
$limite = array(intval($_GET['d']),$nbpp);
$resultat = get($champs,'articles',$conditions,"AND",$trier,$limite);

if(isset($resultat['reponse'])){
	foreach ($resultat as $row) {
		//affichage des articles
		...
	}
	pagination($resultat['total'],$nbpp,'index.php?parametre=valeur&d=');
} else echo $resultat['erreur'];

Fin !

Merci de m’avoir lu jusqu’au bout. Je vous libère ici ! En espérant que ces quelques fonctions puissent vous être utiles. Elles ne sont pas parfaites et certaines pourraient être encore améliorées, alors si vous avez des idées, n’hésitez pas à les partager dans les commentaires.

  • Bruno

    Je bosse sur le même principe, un framework homemade. J’y ai ajouté d’autres fonctions comme par exemple :
    - transformation automatique d’un champ contenant la chaîne date_ en date Mysql
    - gestion des champs requis avec validation en JS et en PHP (si JS désactivé)
    - gestion des champs de type file, avec paramètres supplémentaires pour le redimensionnement
    - ma fonction add fonctionne pour les insert et les update. Simplement, s’il y a un ID dans les variables en paramètre, ça fait un update, sinon un insert.

  • http://www.mess-land.be/ Benoît

    Merci pour l’article, et oui, je l’ai lu complètement :)

  • http://www.infowebmaster.fr/ Tony

    L’inconvénient des framework PHP ne m’encourage pas à les utiliser sur la plupart des mes projets. Je n’ai pas encore lu l’intégralité de cet article, mais je le met en bookmark pour regarder ça plus sérieusement. Merci pour le partage, ça prend un peu de temps à écrire de tels fonctions.

  • Pingback: Le petit journal du web : PHP, WordPress, Javascript, CSS, Ruby, Responsive Webdesign

  • https://twitter.com/#!/DavidDupreNet David Dupré

    Je ressens moi même le besoin de développer mon propre Framework pour les petits projets ( > à 80% des cas) et la question que je me pose est MVC ou organisation par fichiers ?

    Quelle orientations avez-vous choisie ?

    Excellent article.

    • http://anggge.me Ange Chierchia

      J’arrive à la fin de la guerre (c’est mal, je sais) mais mieux vaut tard que jamais, comme on dit.

      Personnellement, je n’utilise pas le modèle MVC, simplement un gros fichier dans lequel je regroupe toutes mes fonctions de traitement sur la base de données.

      Le CMS maison que j’ai développé pour les besoins de mon employeur actuel est conçu en « modules » (pages, news, galeries photos, etc.) séparés dans plusieurs fichiers s’occupant de traiter les différents formulaires avant de les transmettre à mes fonctions CRUD, ce qui me permet de rajouter facilement une fonctionnalité si les besoins du client le nécessitent.

  • dodev

    Bonjour,
    C’est super sympa et c’est simple. J’aurai juste une petite remarque ou deux(constructive(s) :-).
    Tu ouvres tes connexions DB avant d’exécuter tes boucles, tu devrais faire l’inverse pour optimiser ton code.(je chipote)
    J’ai trouvé dommage de ne pas laisser la main sur le design(comprenez temple) de la pagination – puis ce que censé être réutilisable.
    Après il est sûr que c’est propre et simple.

    Tu devrais gérer les requêtes dans une classe spécial.(pour la suite xD)
    Joli boulo ! ;-)

  • Ludovic

    Merci pour cet article. Personnellement, j’utilise un framework MVC (CodeIgniter) pour mes projets. C’est vrai que dans notre quotidien, on fait 80% du temps la même chose. Pour répondre à une des interrogations, je pense que le modèle MVC est un véritable plus pour un projet. C’est tellement plaisant de tout séparer et de pouvoir donner du travail à chacun des acteurs d’une équipe (designer, developper) indépendamment. Je recommande ce framework adaptable à de nombreuses situations !

  • http://twitter.com/HerveThouzard Herve Thouzard

    tu peux remplacer :

    foreach($keys as $ky => $k){ $strSQL .= $k . « , »; }

    par

    $strSQL = implode(‘,’, array_keys($keys));

    et du coup pas besoin du substr d’apès (et c’est plus rapide)

    • http://anggge.me Ange Chierchia

      Merci beaucoup pour l’infos. C’est beaucoup plus simple à comprendre par la suite également, je trouve :)