Boucles avec des conditions en OR (et explication des critères)

Comme vous le savez il n’est pas possible dans l’écriture des critères de boucle d’indiquer une condition OR, autrement appelé « OU logique », autrement dit d’indiquer : « trouve les éléments avec ça OU avec ça. »

La seule possibilité [1] est de créer un critère spécifique. C’est ce que nous allons voir ici pour un cas assez simple.

Quelqu’un est venu hier soir sur l’IRC de SPIP pour demander s’il était possible d’écrire une boucle brèves qui cherche (BREVES) {titre LIKE %#TITRE%} OU {texte LIKE %#TITRE%}, le #TITRE étant pris dans une boucle parente (une boucle ARTICLES il me semble, mais cela a peu d’importance...)

Une solution à ce problème est de créer son propre critère, puisque SPIP fait systématiquement des ET entre ses critères (ils sont cumulatifs).

J’annonce tout de suite que créer un critère peut être simple, mais peut aussi être très compliqué ! Par chance la demande ici est simple, et c’est pourquoi elle offre une jolie base pour un exemple.

Nous allons donc imaginer un critère nommé « va_chercher » qui prendra un argument. Son utilisation sera : {va_chercher toto} ou encore {va_chercher #TITRE}

Comme toute nouvelle définition (balise, filtre, critère, boucle), on mettra le code de notre critère dans un fichier de fonctions, par exemple dans squelettes/mes_fonctions.php.

Déclaration de base du critère

Voici une déclaration de la fonction PHP de notre critère. Ici il n’y a que la déclaration et aucune action encore.

function critere_va_chercher_dist($idb, &$boucles, $crit) {
	$boucle = &$boucles[$idb];
}

Un critère arrive avec trois arguments :

  • $idb est le nom de la boucle
  • &$boucles est l’arbre de construction de toutes les boucles du squelette en question. $boucle, calculé ensuite, est celui de la boucle que le critère va modifier directement. Le & est important, il signifie en PHP que la variable est passée « par référence », autrement dit que toute modification de cette variable à l’intérieur de la fonction du critère affecte la variable à l’extérieur de l’appel. On peut aussi dire que le & permet de ne pas avoir une copie du contenu de la variable demandée, mais le contenu lui-même.
  • $crit est l’arbre de construction du critère, il permet de savoir ce que contient le critère en question (nom, opérateur, négation, arguments...)

Effectuer une action

Nous allons voir comment ajouter un critère de sélection dans la boucle grâce à ce critère. Nous allons ajouter un élément dans le tableau $boucle->where.

Notons déjà, que tous les éléments dans ce tableau where sont enchaînés par des opérateurs AND. Cependant, et c’est ce que nous verrons plus loin, un élément de ce tableau peut contenir des sous-éléments, et pour ceux-là, nous pouvons décrire précisément le type de liaison qui les affecte parmi une panoplie d’opérateurs à disposition (AND, OR, NOT, LIKE, REGEXP, >, <, =, >=, <= et je dois en oublier)

Commençons simplement. D’une part, le $table calculé contient le nom de la table SQL concernant notre boucle. Le tableau $c contient trois éléments : Opérateur, Valeur1, Valeur2. Cela sera traduit ensuite par « Valeur1 Operateur Valeur2 » soit ici : « titre = ’’ ». En fonction des opérateurs, il n’y a pas obligatoirement trois éléments dans le tableau ; ainsi NOT ne compare qu’à une valeur. À la fin, le tableau $c est ajouté à where.

function critere_va_chercher_dist($idb, &$boucles, $crit) {
	$boucle = &$boucles[$idb];
	$table = $boucle->id_table;

	$c = array("'='", "'$table.titre'", "''");

	$boucle->where[] = $c;
}

Comme vous le voyez, le contenu du tableau $c possède une drôle d’écriture à base de doubles guillemets tel que "'='". Il faut comprendre, qu’on est à un niveau d’abstraction un peu au dessus de celui des squelettes. On fait écrire, dans le critère, le code PHP que SPIP produira pour calculer la boucle.

On comprendra donc que ces deux lignes ne donnent pas le même résultat

$c = array("'='", "'$table.titre'", "sql_quote('toto')");
$c = array("'='", "'$table.titre'", "'sql_quote(\'toto\')'");

Cela écrira en PHP à peu près :

'spip_articles.titre' .  '=' .  sql_quote('toto')
'spip_articles.titre' . '=' . 'sql_quote(\'toto\')'

Puis à l’execution, la rèquete SQL :

spip_articles.titre='toto'
spip_articles.titre=sql_quote('toto')

La deuxième ligne créerait une erreur MySQL puisque sql_quote('toto') est une fonction PHP pour SPIP et non une fonction d’un gestionnaire de requête SQL.

Cependant la seconde écriture peut être souhaitée si on veut utiliser une fonction SQL, tel que « MIN » ou « MAX », ou je ne sais quoi... Il faut donc bien comprendre ce qui se passe et ce que l’on souhaite obtenir :)

Ajouter la négation

La négation, c’est la présence dans le critère de NOT, représenté par le point d’exclamation (!) comme dans : {!va_chercher toto}.

Prendre en compte la négation consiste —en général [2]— à demander l’inverse du résultat attendu, c’est à dire à encapsuler tout notre tableau $c dans un NOT avant de le donner à manger au where. Pour savoir si notre critère a une négation, on interroge $crit->not.

function critere_va_chercher_dist($idb, &$boucles, $crit) {
	$boucle = &$boucles[$idb];
	$table = $boucle->id_table;

	$c = array("'='", "'$table.titre'", "''");

	// Inversion de la condition ?
	if ($crit->not) {
		$c = array("'NOT'", $c);
	}

	$boucle->where[] = $c;
}

Comme vous le voyez, $c, lorsqu’il y a une condition NOT devient un tableau d’une profondeur plus importante. C’est grâce à ce principe que nous pourrons faire un OU logique plus loin.

Récupérer un paramètre de critère

Nous allons rechercher le paramètre ’toto’ ou ’#TITRE’ passé à notre critère via {va_chercher toto} ou {va_chercher #TITRE}. Pour cela, nous allons tester la présence de $crit->param[ n ] ou le nombre indique la présence du paramètre n (en comptant de 0), et utiliser la fonction adaptée calculer_liste.

S’il n’y a pas de paramètre 0, nous pourrions mettre une valeur par défaut, mais nous dirons ici que c’est une erreur de squelettes pour forcer à avoir ce paramètre dans le critère. On aurait pu, aussi, en absence de paramètre récupérer automatiquement un paramètre de l’environnement ou le #TITRE de la boucle parente, mais cela n’entre pas dans l’objet de ce tutoriel. Sachez simplement que c’est possible, tout comme réaliser des jointures ou faire des calculs complexes. Vous pouvez en exemple regarder les critères de certains plugins (agenda, organiseur, tradsync, ... ) et de SPIP (ecrire/public/criteres.php)

function critere_va_chercher_dist($idb, &$boucles, $crit) {
	$boucle = &$boucles[$idb];
	$table = $boucle->id_table;

	// chercher quoi ?
	if (isset($crit->param[0])) {
		$quoi = calculer_liste($crit->param[0], array(), $boucles, $boucles[$idb]->id_parent);
	} else {
		# $quoi = "''";
		// rendons obligatoire ce parametre
		return (array('zbug_critere_necessite_parametre', array('critere' => $crit->op )));
	}

	$c = array("'='", "'$table.titre'", "$quoi");

	// Inversion de la condition ?
	if ($crit->not) {
		$c = array("'NOT'", $c);
	}

	$boucle->where[] = $c;
}

Ajouter la condition OR

Le dernier élément, le plus simple finalement maintenant que tout est compris, est d’ajouter l’opérateur OR entre deux sélections. Nous l’obtenons avec :

	$c = array("'OR'",
		array("'LIKE'", "'$table.titre'", "sql_quote('%' . $quoi . '%')"),
		array("'LIKE'", "'$table.texte'", "sql_quote('%' . $quoi . '%')")
	);

Je n’en dis pas plus. Voici le code complet :

// {va_chercher #TITRE}
function critere_va_chercher_dist($idb, &$boucles, $crit) {
	$boucle = &$boucles[$idb];
	$table = $boucle->id_table;
	$not = $crit->not;

	// chercher quoi ?
	if (isset($crit->param[0])) {
		$quoi = calculer_liste($crit->param[0], array(), $boucles, $boucles[$idb]->id_parent);
	} else {
		// rendons obligatoire ce parametre
		return (array('zbug_critere_necessite_parametre', array('critere' => $crit->op )));
	}

	$c = array("'OR'",
		array("'LIKE'", "'$table.titre'", "sql_quote('%' . $quoi . '%')"),
		array("'LIKE'", "'$table.texte'", "sql_quote('%' . $quoi . '%')")
	);

	// Inversion de la condition ?
	if ($crit->not) {
		$c = array("'NOT'", $c);
	}
	
	$boucle->where[] = $c;
}

Notes

[1hormis une sorte de contournement sur plusieurs boucles avec le critère {doublons}

[2Ce n’est pas systématiquement vrai, notamment pour des critères complexes

Discussion

6 discussions

  • Exemple simple pour renvoyer un OR sur une requête de recherche classique.
    On ajoute un filtre à la balise #RECHERCHE pour récupérer les mots recherchés.

    /*
     * filtre à placer dans un fichier nomdevotreplugin_fonctions.php
     * Code dans le squelette, au dessus de la boucle
     * [(#SET{where, [(#RECHERCHE|requete_search)]})]
     * utilisation en critère de la boucle {where #GET{where}}
     *
    **/
    function requete_search($recherche){
    	//le separateur est un espace par defaut
    	$separator = ' ';
    	
    	//le separateur peut être un +
    	if (strpos($recherche, '+') !== false) {
    		$separator = '+';
    	}
    	
    	//recupere les mots de la recherche
    	$mots_search = explode($separator, $recherche);
    	$nb = count($mots_search);
    	$requete = null;
    	$i = 0;
    	if($nb>0){
    		foreach ($mots_search as $mot){
    			if($i==0){
    				$requete = "<span class="base64" title="PGNvZGUgY2xhc3M9InNwaXBfY29kZSBzcGlwX2NvZGVfaW5saW5lIiBkaXI9Imx0ciI+bmFtZTwvY29kZT4="></span> LIKE "."'%".$mot."%' ";
    			}
    			if($i>0){
    				$requete .= " OR <span class="base64" title="PGNvZGUgY2xhc3M9InNwaXBfY29kZSBzcGlwX2NvZGVfaW5saW5lIiBkaXI9Imx0ciI+bmFtZTwvY29kZT4="></span> LIKE "."'%".$mot."%' ";
    			}
    			$i++;
    		}
    	}
    	return $requete;
    }

    Répondre à ce message

  • Depuis, de l’eau coulé sous les ponts, et on a maintenant un joli critère {where}
    Trop beau SPIP 💗

    Répondre à ce message

  • Bonjour,

    en prenant exemple sur cet excellent tutoriel, je me suis fabriqué un critère

    {membres ...}

    Destiné à être exclusivement utilisé sur les tables d’une base de données externe, ce critère prend obligatoirement ou bien le paramètre ’actuels’ ou bien ’anciens’, i.e. les deux seules syntaxes autorisées sont :

    {membres actuels}

    ou bien

    {membres anciens}

    Dans mon code j’ai un switch($quoi) qui me génère une requête SQL (très) différente suivant le paramètre donné en entrée ($quoi). Précisons ici que c’est une requête SQL qui ne contient pas les strings ’actuels’ et ’anciens’, parce que je veux conserver un certain niveau d’abstraction entre SPIP et ma base de données externes.

    A première vue, ça fonctionne bien, mais maintenant si j’essaye de faire

    {membres #GET{type_membre}}

    je me prends une erreur, car dans la variable $quoi, au lieu d’avoir la chaîne ’actuels’, comme je m’y attendrais, j’ai à la place cette ligne de code PHP :

    table_valeur($Pile["vars"], (string)'statut', null)

    Or, le problème, c’est que je n’arrive pas à exécuter cette ligne de code PHP pour obtenir le contenu de ce #GET... J’ai bien essayé de faire

    $quoi=eval("return ".$quoi.";");

    mais ça ne fonctionne pas car apparemment $Pile n’est pas défini dans le contexte d’exécution de ma fonction critere_membres_dist()... Alors comment faire ? C’est grave docteur ?

    Répondre à ce message

  • Natacha Courcelles

    Génial ! merci
    cette fonction m’a sauvé

    je l’ai adapté pour avoir plus de 2 champs
    voici ma modif

    $c =array(« ’OR’ »,
    array(« ’OR’ »,
    array(« ’LIKE’ », « ’$table.champ1’ », « sql_quote(’%’ . $quoi . ’%’) »),
    array(« ’LIKE’ », « ’$table.champ2’ », « sql_quote(’%’ . $quoi . ’%’) »)
    ),
    array(« ’OR’ »,
    array(« ’LIKE’ », « ’$table.champ3’ », « sql_quote(’%’ . $quoi . ’%’) »),
    array(« ’LIKE’ », « ’$table.champ4’ », « sql_quote(’%’ . $quoi . ’%’) »)
    )
    ) ;

    si ça peut aider
    bien cordialement

    Répondre à ce message

  • Encore une fois parfaitement brillant. Juste à la portée de tous. Dit comme ça les choses sont tellement plus simples ! Bravo et merci :)

    Répondre à ce message

  • Excellent tutoriel, merci !

    Répondre à ce message

Ajouter un commentaire

Avant de faire part d’un problème sur un plugin X, merci de lire ce qui suit :

  • Désactiver tous les plugins que vous ne voulez pas tester afin de vous assurer que le bug vient bien du plugin X. Cela vous évitera d’écrire sur le forum d’une contribution qui n’est finalement pas en cause.
  • Cherchez et notez les numéros de version de tout ce qui est en place au moment du test :
    • version de SPIP, en bas de la partie privée
    • version du plugin testé et des éventuels plugins nécessités
    • version de PHP (exec=info en partie privée)
    • version de MySQL / SQLite
  • Si votre problème concerne la partie publique de votre site, donnez une URL où le bug est visible, pour que les gens puissent voir par eux-mêmes.
  • En cas de page blanche, merci d’activer l’affichage des erreurs, et d’indiquer ensuite l’erreur qui apparaît.

Merci d’avance pour les personnes qui vous aideront !

Par ailleurs, n’oubliez pas que les contributeurs et contributrices ont une vie en dehors de SPIP.

Qui êtes-vous ?
[Se connecter]

Pour afficher votre trombine avec votre message, enregistrez-la d’abord sur gravatar.com (gratuit et indolore) et n’oubliez pas d’indiquer votre adresse e-mail ici.

Ajoutez votre commentaire ici

Ce champ accepte les raccourcis SPIP {{gras}} {italique} -*liste [texte->url] <quote> <code> et le code HTML <q> <del> <ins>. Pour créer des paragraphes, laissez simplement des lignes vides.

Ajouter un document

Suivre les commentaires : RSS 2.0 | Atom