Ajouter des champs dans une table SPIP Ou comment rendre générique du code spécifique

, par Matthieu Marcillaud

Hier, en lisant Ajouter un champ dans une table SPIP, vous avez dit, c’est bien SPIP, mais si je dois faire un plugin à chaque fois que je veux ajouter un ou plusieurs champs, c’est totalement casse pied ! Je dirais volontiers « Les gens ne sont jamais contents ! ». En même temps, vous avez un peu raison. Tentons de créer un plugin plus générique.

La problématique est la suivante, Tom a besoin d’un champ ’PS’ sur les rubriques, Sarah d’un champ ’Coordonnees’ sur les articles et Didier d’un champ ’Métier’ sur les auteurs... Alors, comment répondre à leurs attentes ? On pourrait créer 3 plugins totalement identiques avec beaucoup de code redondant. Mais nous allons plutôt essayer de créer un plugin principal offrant des fonctions génériques pour des plugins secondaires.

Au bout du compte, il y aura 4 plugins, 1 générique, plus 3 spécifiques par personnes, mais avec beaucoup moins de code redondant et donc plus de facilité de maintenance. Voyons comment s’y prendre.

Bien sûr, vous aurez lu en détail le précédent tutoriel (Ajouter un champ dans une table SPIP) de sorte que je ne vais pas répéter ce qui s’y est dit.

Quelques généralités

Lorsque l’on commence un développement il est toujours plus rapide, au début, de créer un code spécifique à un projet. C’est avec le temps que l’on remarque que trop penser spécifique empêche ensuite toute adaptabilité.

Hier le « ps » des rubriques est une excellent exemple. Pressé par l’arrivée de Noël, ou la date butoir de la fin du projet, on ne s’occupe pas de généraliser son code. Au bout du compte, on est le seul à pouvoir l’utiliser, il n’y a aucun branchement de prévu pour d’autres actions, bref, ça fonctionne, mais c’est limité.

Un jour, nous aurons le besoin de réutiliser le code, et de l’adapter. Lorsque le temps passe, si aucun commentaire n’était accoté aux fonctions ou au code lu, reprendre et comprendre le code devient vite une horrible prise de tête. C’est pourtant ce qui arrive souvent, trop pressés de « faire marcher le bousin ».

Mais on pourrait aussi directement penser et coder "générique", c’est à dire, utiliser les outils à disposition de PHP et de SPIP pour offrir des moyens d’utiliser un plugin d’une façon différente à celle prévue au départ. Ou, d’un autre point de vue, c’est créer du code qui pourra facilement être adapté par la suite, qui sera peu sensible aux évolutions de SPIP, c’est à dire qui utilise pleinement les points d’entrées et les fonctions prévues.

Pour exemple, j’ai passé 5 heures hier à coder le plugin « post scriptum » pour les rubriques en écrivant en même temps le tutoriel et en le testant. J’ai passé 8 heures aujourd’hui pour faire exactement la même chose, mais en carrément plus générique. Bien sûr, il y avait l’expérience et les souvenirs d’hier, mais la différence de temps n’est pas si importante finalement.

Ainsi, cet exemple cherche à expliquer comment a été transformé le plugin d’hier pour le rendre plus adaptable à différents besoins. Un pipeline et une API (interface de programmation) est développée et tout de suite le code devient plus clair, explicite et extensible.

Je préviens tout de suite, l’explication est longue. Bonne lecture !

Créer le plugin générique

Ainsi, nous commençons par créer le plugin plugins/champs_extras/ avec un plugin.xml comme indiqué ci-dessous. Nous indiquons dès maintenant l’utilisation de certains pipelines dont on sait par avance qu’ils vont nous être utiles.

<plugin>
	<nom>Champs Extras</nom>
	<auteur>Matthieu Marcillaud [->magraine.net]</auteur>
	<licence>GNU/GLP</licence>
	<version>0.1</version>
	<description>
	Creer de nouveaux champs aux objets d'&eacute;dition de SPIP
	</description>
	<etat>dev</etat>
	<prefix>champsextras</prefix>
	<options>champsextras_options.php</options>
	<necessite id="SPIP" version="[2.0;]" />
 	<pipeline>
		<nom>declarer_tables_principales</nom>
		<inclure>base/champsextras.php</inclure>
	</pipeline>	
 	<pipeline>
		<nom>editer_contenu_objet</nom>
		<inclure>champsextras_pipelines.php</inclure>
	</pipeline>	
 	<pipeline>
		<nom>afficher_contenu_objet</nom>
		<inclure>champsextras_pipelines.php</inclure>
	</pipeline>	
 	<pipeline>
		<nom>pre_edition</nom>
		<inclure>champsextras_pipelines.php</inclure>
	</pipeline>	
</plugin>

Le principe d’utilisation que nous allons proposer est le suivant : un autre plugin déclare les champs qu’il veut voir en plus, suivant une syntaxe précise, en s’appuyant sur des fonctionnalités de ce plugin. L’installation, la désinstallation, l’affichage seront gérés par le plugin générique, offrant une API (interface de programmation) pour cela.

Permettre de déclarer les champs à inserer

Les plugin dépendant du plugin générique « champs extras » vont devoir déclarer les champs ajoutés. Pour cela nous allons créer deux choses : une méthode pour ajouter un champ ainsi qu’un pipeline pour que chaque plugin les déclarent facilement.

Commençons par créer le pipeline de déclaration « declarer_champs_extras » dans notre plugin. Pour cela, il faut déclarer à SPIP son existance. C’est ce que l’on fait dans le fichier champsextras_options.php contenant :

<?php
$GLOBALS['spip_pipeline']['declarer_champs_extras'] = '';
?>

Une fois ce pipeline déclaré, d’autres plugins peuvent l’utiliser pour ajouter des contenus dedans par son intermédiaire. Nous verrons plus tard cela.

Installons d’abord les fichiers champsextras_pipelines.php et base/champsextras.php indispensables au plugin, vides pour le moment.

Créer une API pour insérer les champs dans le pipeline

Nous allons maintenant prévoir un outil pour que les plugins ajoutent facilement leurs champs. Il y a plusieurs choses à déclarer pour chaque champ :

  • sur quelle table
  • le nom sql du champ
  • la nature du champ (text ou varchar, ça suffira ici !)
  • le type de formulaire attendu (input ou textarea)
  • le code de langue du label

Comme en plus nous sommes à peu près certain que la structure évoluera au fil du développement du plugin, nous allons essayer de faire des fonctions assez propres.

Je propose de passer par une classe PHP (c’est un choix comme un autre !). Créons un fichier pour mettre nos API, inc/champsextras.php contenant :

<?php
 
class ChampExtra{
	var $table = ''; // type de table ('rubrique')
	var $champ = ''; // nom du champ ('ps')
	var $label = ''; // label du champ, code de langue ('monplug:mon_label')
	var $type = 'textarea'; // type (input/textarea)
	var $sql = ''; // declaration sql (text NOT NULL DEFAULT '')
 
	// constructeur
	function ChampExtra($params=array()) {
		$this->definir($params);
	}
 
	// definir les champs
	function definir($params=array()) {
		foreach ($params as $cle=>$valeur) {
			if (isset($this->$cle)) {
				// si une fonction specifique existe pour ce type, l'utiliser
				if (method_exists('ChampExtra','set_'.$cle)) {
					$this->{'set_'.$cle}($valeur);
				} else {
					$this->$cle = $valeur;
				}
			}
		}
	}
 
	// declarations specifiques
	function set_type($val='textarea') {
		if (!in_array($val, array('textarea','input'))) {
			$val = 'textarea';
		}
		$this->type = $val;	
	}
}
?>

Cette classe permettra à un autre plugin de déclarer un champ extra en l’utilisant comme ceci :

include_spip('inc/champsextras');
$ps = new ChampExtra(array(
	'table' => 'rubrique',
	'champ' => 'ps',
	'label' => 'info_post_scriptum',
	'type' => 'textarea',
	'sql' => "text NOT NULL DEFAULT ''",
));

Créer un plugin pour le ps de rubrique

A ce stade, nous n’avons encore pas fait grand chose. On ne peut toujours rien tester, il ne se passe rien. Bref... pas simple. Mais comme nous savons que nous devons créer des plugins qui se basent sur le plugin « champs extras », nous allons commencer à en créer un premier, pour qu’il déclare le nouveau champ. Cela va nous permettre d’avancer un peu dans notre développement.

Recréons donc le plugin « post scriptum de rubriques » pour qu’il s’appuie sur le futur plugin « champs extras ». Créons un dossier plugins/post_scriptum_extra/ contenant le fichier plugin.xml ci-dessous. Nous indiquons qu’il y a un fichier d’installation et un pipeline utilisé, celui pour déclarer des champs extras dans les tables.

<plugin>
	<nom>Post Scriptum de Rubriques</nom>
	<auteur>Matthieu Marcillaud [->magraine.net]</auteur>
	<licence>GNU/GLP</licence>
	<version>0.1</version>
	<description>
	Ajoute un champ "ps" sur les rubriques de SPIP.
	</description>
	<etat>dev</etat>
	<prefix>postscriptum</prefix>
	<necessite id="champsextras" version="[0.1;]" />
	<install>base/postscriptum_install.php</install>
	<pipeline>
		<nom>declarer_champs_extras</nom>
		<inclure>base/postscriptum.php</inclure>
	</pipeline>
</plugin>

Créons les deux fichiers nécessaires, base/postscriptum.php et base/postscriptum_install.php.Nous n’avons pas encore gérés l’installation, par contre la déclaration peut être définie. Plaçons ce contenu dans le fichier base/postscriptum.php :

<?php
if (!defined("_ECRIRE_INC_VERSION")) return;
 
function postscriptum_declarer_champs_extras($champs = array()){
	$champs[] = new ChampExtra(array(
		'table' => 'rubrique',
		'champ' => 'ps',
		'label' => 'info_post_scriptum',
		'type' => 'textarea',
		'sql' => "text NOT NULL DEFAULT ''",
	));
	return $champs;
}
?>

Le fichier se compose d’une fonction ajoutant un élément ChampExtra au tableau $champs. Simplement.

Déclarer l’ensemble des champs à SPIP

Chaque plugin pouvant déclarer, pour le moment des ’ChampExtra’, il faut maintenant les transformer en syntaxe compréhensive pour SPIP. C’est le rôle de declarer_champs_extras(), à créer dans le fichier inc/champsextras.php, du plugin Champs Extra donc, qui traduit la syntaxe de l’objet ChampExtra en syntaxe de déclaration de table SPIP :

function declarer_champs_extras($champs, $tables){
	// ajoutons les champs un par un
	foreach ($champs as $c){
		$table = table_objet_sql($c->table);
		if (isset($tables[$table]) and $c->champ and $c->sql) {
			$tables[$table]['field'][$c->champ] = $c->sql;
		}
	}	
	return $tables;
}

La fonction parcoure simplement tous les champs transmis et les ajoute au tableau selon la syntaxe correcte.

Lier le tout : déclarations, pipeline et SPIP

Tous les éléments sont réunis pour la mayonnaise : le pipeline declarer_champs_extras utilisé dans les plugins et la fonction declarer_champs_extras. Le tout prend son sens dans le pipeline declarer_tables_principales du plugin Champs Extras de la sorte (fichier base/champsextras.php) :

<?php
if (!defined("_ECRIRE_INC_VERSION")) return;
 
function champsextras_declarer_tables_principales($tables_principales){
	// pouvoir utiliser la class ChampExtra
	include_spip('inc/champsextras');
	// recuperer les champs crees par les plugins
	$champs = pipeline('declarer_champs_extras', array());
	// ajouter les champs au tableau spip
	return declarer_champs_extras($champs, $tables_principales);
}
?>

La fonction charge les fonctions de la librairie inc/champsextras.php, execute le pipeline de déclaration de champs extras puis traduit les champs obtenus pour SPIP.

Gérer l’installation des champs

A ce stade là, les champs sont déclarés, mais l’installation ne se fait toujours pas. Nous allons créer une fonction pour installer les champs prévus à l’installation d’un plugin et une autre pour les désinstaller. Pour cela, on ajoute à la librairie inc/champsextras.php ce qui manque :

function creer_champs_extras($champs, $nom_meta_base_version, $version_cible) {
	$current_version = 0.0;
 
	if ((!isset($GLOBALS['meta'][$nom_meta_base_version]))
	|| (($current_version = $GLOBALS['meta'][$nom_meta_base_version])!=$version_cible)){
 
		// cas d'une installation
		if ($current_version==0.0){
			include_spip('base/create');
			// on recupere juste les differentes tables a mettre a jour
			$tables = array();
			foreach ($champs as $c){ 
				if ($table = table_objet_sql($c->table)) {
					$tables[$table] = $table;
				}
			}		
			// on met a jour les tables trouvees
			foreach($tables as $table) {
				maj_tables($table);
			}
			ecrire_meta($nom_meta_base_version,$current_version=$version_cible,'non');
		}
	}	
}
 
function vider_champs_extras($champs, $nom_meta_base_version) {
	// on efface chaque champ trouve
	foreach ($champs as $c){ 
		if ($table = table_objet_sql($c->table) and $c->champ and $c->sql) {
			sql_alter("TABLE $table DROP $c->champ");
		}
	}
	effacer_meta($nom_meta_base_version);	
}

Les deux nouvelles fonctions sont normalement assez claires, voyons comment les utiliser dans le plugin postscriptum. On écrit le code suivant dans le fichier base/postscriptum_install.php :

<?php
if (!defined("_ECRIRE_INC_VERSION")) return;
 
include_spip('inc/champsextras');
include_spip('base/postscriptum');
 
function postscriptum_upgrade($nom_meta_base_version,$version_cible){
	$champs = postscriptum_declarer_champs_extras();
	creer_champs_extras($champs, $nom_meta_base_version, $version_cible);
}
 
function postscriptum_vider_tables($nom_meta_base_version) {
	$champs = postscriptum_declarer_champs_extras();
	vider_champs_extras($champs, $nom_meta_base_version);
}
?>

Dans les deux fonctions SPIP d’installation et de désinstallation, on récupère la liste des champs du plugin, puis l’on appelle les fonctions adaptées du plugin champs extra, qui s’occupe alors de gérer installation et désinstallation.

A cet instant, les deux plugins permettent de gérer la création et suppression de champs dans la base de donnée. Occupons nous maintenant du contenu, ce qui va être un peu plus simple tout de même !

Ajout des champs de formulaires

C’est à ce moment là qu’intervient le fichier champsextras_pipelines.php et les pipelines adaptés.

Commençons par ajouter les champs de formulaires lorsqu’il y en a besoin. Tout d’abord, on se crée 2 fichiers contenant les squelettes en question, un pout afficher un textarea, l’autre pour afficher un input de type texte. Appelons-les formulaires/inc-champ-formulaire-input.html et formulaires/inc-champ-formulaire-textarea.html, ils contiennent respectivement :

#SET{name,#ENV{champextra}}
#SET{valeur,#ENV{#ENV{champextra}}}
#SET{label,#ENV{#VAL{label_}|concat{#GET{name}}}|_T}
#SET{erreurs,#ENV**{erreurs}|table_valeur{#GET{name}}}
<li class="editer_[(#GET{name})][ (#GET{erreurs}|oui)erreur]">
	<label for="#GET{name}">#GET{label}</label>
		[<span class='erreur_message'>(#GET{erreurs})</span>]
		<input type='text' class='text' name='#GET{name}' id='#GET{name}' value="#GET{valeur}" />
</li>
#SET{name,#ENV{champextra}}
#SET{valeur,#ENV{#ENV{champextra}}}
#SET{label,#ENV{#VAL{label_}|concat{#GET{name}}}|_T}
#SET{erreurs,#ENV**{erreurs}|table_valeur{#GET{name}}}
<li class="editer_[(#GET{name})][ (#GET{erreurs}|oui)erreur]">
	<label for="#GET{name}">#GET{label}</label>
		[<span class='erreur_message'>(#GET{erreurs})</span>]
		<textarea name='#GET{name}'[ lang='(#LANG)'] id='#GET{name}' rows='7'>#GET{valeur}</textarea>
</li>

Le principe est le suivant, on envoie au squelette une variable « champextra » contenant le nom de la variable du formulaire à obtenir. Dans notre exemple, « champextra » va valoir « ps ». Ce que l’on écrit ensuite, est grosso-modo l’équivalent php de $valeur=$$champextra;, mais en SPIP. Pour obtenir la valeur de ’ps’, on fait #ENV{#GET{name}} ou #ENV{#ENV{champextra}}.

Ajoutons le code pour le pipeline editer_contenu_objet dans champsextras_pipelines.php :

Une fonction champsextras_creer_contexte() calcule les éléments nouveaux à transmettre aux squelettes affichant les formulaires. La fonction insère la valeur actuelle de l’extra demandé, renseigne la variable ’champextra’ aussi.

La seconde fonction, du pipeline, ajoute les éléments de formulaire lorsque le type de formulaire correspond aux type d’extra.

<?php
if (!defined("_ECRIRE_INC_VERSION")) return;
 
// pouvoir utiliser la class ChampExtra
include_spip('inc/champsextras');
 
// Calcule des elements pour le contexte de compilation
// des squelettes de champs extras
// en fonction des parametres donnes dans la classe ChampExtra
function champsextras_creer_contexte($c, $contexte_flux) {
	$contexte = array();
	$contexte['champextra'] = $c->champ;
	$contexte['label_' . $c->champ] = $c->label;
 
	// retrouver la valeur du champ demande
	$table = table_objet_sql($c->table);
	$_id = id_table_objet($c->table);
 
	// attention, l'ordre est important car les pipelines afficher et editer
	// ne transmettent pas les memes arguments
	if (isset($contexte_flux[$_id])) {
		$id = $contexte_flux[$_id];		
	} elseif (isset($contexte_flux['id_objet'])) {
		$id = $contexte_flux['id_objet'];
	} elseif (isset($contexte_flux['id']) and intval($contexte_flux['id'])) { // peut valoir 'new'
		$id = $contexte_flux['id'];
	}
 
	$contexte[$c->champ] = sql_getfetsel($c->champ, $table, $_id . '=' . sql_quote($id));
	return array_merge($contexte_flux, $contexte);
}
 
// ajouter les champs sur les formulaires CVT editer_xx
function champsextras_editer_contenu_objet($flux){
 
	// recuperer les champs crees par les plugins
	if ($champs = pipeline('declarer_champs_extras', array())) {
		foreach ($champs as $c) {
			// si le champ est du meme type que le flux
			if ($flux['args']['type']==objet_type($c->table) and $c->champ and $c->sql) {
 
				$contexte = champsextras_creer_contexte($c, $flux['args']['contexte']);
 
				// calculer le bon squelette et l'ajouter
				$extra = recuperer_fond('formulaires/inc-champ-formulaire-'.$c->type, $contexte);	
				$flux['data'] = preg_replace('%(<!--extra-->)%is', $extra."\n".'$1', $flux['data']);
			}
		}
	}
 
	return $flux;
}

Prendre en compte les enregistrements

Il faut renseigner le pipeline pre_edition. C’est le plus simple à faire :

// ajouter les champs extras soumis par les formulaire CVT editer_xx
function champsextras_pre_edition($flux){
 
	// recuperer les champs crees par les plugins
	if ($champs = pipeline('declarer_champs_extras', array())) {
		foreach ($champs as $c) {
			// si le champ est du meme type que le flux
			if ($flux['args']['table']==table_objet_sql($c->table) and $c->champ and $c->sql) {
				if ($extra = _request($c->champ)) {
					$flux['data'][$c->champ] = corriger_caracteres($extra);
				}				
			}
		}
	}
 
	return $flux;
}

Rendre visible les résultats enregistrés sur les pages de visualisation

Dernier pipeline à utiliser, afficher_contenu_objet avec le code suivant. Rien à dire de plus, vous connaissez la routine.

// ajouter le champ extra sur la visualisation de l'objet
function champsextras_afficher_contenu_objet($flux){
	// recuperer les champs crees par les plugins
	if ($champs = pipeline('declarer_champs_extras', array())) {
		foreach ($champs as $c) {
			// si le champ est du meme type que le flux
			if ($flux['args']['type']==objet_type($c->table) and $c->champ and $c->sql) {
 
				$contexte = champsextras_creer_contexte($c, $flux['args']['contexte']);
 
				// calculer le bon squelette et l'ajouter
				$extra = recuperer_fond('prive/contenu/inc-champ-extra', $contexte);	
				$flux['data'] .= "\n".$extra;
			}
		}
	}
	return $flux;
}

Cette fonction calcule le squelette à créer prive/contenu/inc-champ-extra par exemple en lui donnant ce code :

#SET{name,#ENV{champextra}}
#SET{valeur,#ENV{#ENV{champextra}}}
#SET{label,#ENV{#VAL{label_}|concat{#GET{name}}}|_T}
[<div class="[(#GET{name})]">
	<strong>[(#GET{label})]</strong>
	(#GET{valeur})
</div>]

Voilà, nous avons fini, et nous devrions avoir un plugin assez générique pour que d’autres s’appuient dessus.

Sarah et Didier

Oui, il faudrait pas les oublier ces deux là. Pour simplifier, on va dire qu’ils sont frère et soeur et n’ont besoin que d’un seul plugin commun pour ajouter les champs qui leurs manquent.

Rien de plus simple : on copie le plugin « postscriptum_extra » dans un autre dossier par exemple « sarah_et_didier » et on modifie le fichier plugin.xml pour changer le titre, le préfixe et la description, les noms des fichiers.

<plugin>
	<nom>Sarah et Didier</nom>
	<auteur>Matthieu Marcillaud [->magraine.net]</auteur>
	<licence>GNU/GLP</licence>
	<version>0.1</version>
	<version_base>0.1</version_base>
	<description>
	Ajoute un champ "coordonnees" sur les articles de SPIP.
	Ajoute un champ "metier" sur les auteurs de SPIP
	</description>
	<etat>dev</etat>
	<prefix>sarahetdidier</prefix>
	<necessite id="champsextras" version="[0.1;]" />
	<install>base/sarahetdidier_install.php</install>
	<pipeline>
		<nom>declarer_champs_extras</nom>
		<inclure>base/sarahetdidier.php</inclure>
	</pipeline>
</plugin>

On renomme ensuite les fichiers et les noms des fonctions. On en profite pour créer un répertoire lang/ contenant le fichier sarahetdidier_fr.php en écrivant dedans :

<?php
$GLOBALS[$GLOBALS['idx_lang']] = array(
	//C
	'coordonnees' => 'Coordonn&eacute;es',
	//M
	'metier' => 'M&eacute;tier',
);
?>

Enfin, on modifie la fonction declarer_champs_extras dans le fichier base/sarahetdidier.php

<?php
if (!defined("_ECRIRE_INC_VERSION")) return;
 
function sarahetdidier_declarer_champs_extras($champs = array()){
	$champs[] = new ChampExtra(array(
		'table' => 'auteur',
		'champ' => 'metier',
		'label' => 'sarahetdidier:metier',
		'type' => 'input',
		'sql' => "text NOT NULL DEFAULT ''",
	));
	$champs[] = new ChampExtra(array(
		'table' => 'article',
		'champ' => 'coordonnees',
		'label' => 'sarahetdidier:coordonnees',
		'type' => 'input',
		'sql' => "text NOT NULL DEFAULT ''",
	));	
	return $champs;
}
?>

Coup de pouce

Pour me maintenir en éveil et en pleine forme physique et mentale, vous pouvez me faire le cadeau d'un jus de fruit pressé.

En plus de m'hydrater, de m'offrir une alimentation saine crudivore et frugivore, cela peut aussi me motiver à produire d'autres documentations ou peut-être à prendre des vacances ! :)

Vous pouvez également me « Flattrer » si vous utilisez le service en ligne très malin Flattr de microdonations qui permet d'allouer un budget mensuel à des créateurs de contenu. Votre budget est partagé chaque mois entre toutes les personnes que vous avez flattées ce mois là.