web-dev-qa-db-fra.com

Utiliser le paramètre lié plusieurs fois

J'essaie d'implémenter un moteur de recherche assez basique pour ma base de données, où l'utilisateur peut inclure différents types d'informations. La recherche elle-même consiste en un couple d'une union sélectionne où les résultats sont toujours fusionnés en 3 colonnes.

Les données renvoyées sont toutefois extraites de différentes tables.

Chaque requête utilise $ term pour la correspondance, et je l'ai liée à ": term" en tant que paramètre préparé.

Maintenant, le manuel dit:

Vous devez inclure un marqueur de paramètre unique pour chaque valeur que vous souhaitez transmettre à l'instruction lorsque vous appelez PDOStatement :: execute (). Vous ne pouvez pas utiliser deux fois un marqueur de paramètre nommé du même nom dans une instruction préparée.

J'ai pensé qu'au lieu de remplacer chaque paramètre: term par: termX (x pour term = n ++), il devait y avoir une meilleure solution?

Ou dois-je simplement lier le numéro X de: termX?

Editer Publier ma solution sur ceci:

$query = "SELECT ... FROM table WHERE name LIKE :term OR number LIKE :term";

$term = "hello world";
$termX = 0;
$query = preg_replace_callback("/\:term/", function ($matches) use (&$termX) { $termX++; return $matches[0] . ($termX - 1); }, $query);

$pdo->prepare($query);

for ($i = 0; $i < $termX; $i++)
    $pdo->bindValue(":term$i", "%$term%", PDO::PARAM_STR);

Bon, voici un échantillon. Je n'ai pas le temps pour sqlfiddle, mais je vais en ajouter un plus tard si nécessaire.

(
    SELECT
        t1.`name` AS resultText
    FROM table1 AS t1
    WHERE
        t1.parent = :userID
        AND
        (
            t1.`name` LIKE :term
            OR
            t1.`number` LIKE :term
            AND
            t1.`status` = :flagStatus
        )
)
UNION
(
    SELECT
        t2.`name` AS resultText
    FROM table2 AS t2
    WHERE
        t2.parent = :userParentID
        AND
        (
            t2.`name` LIKE :term
            OR
            t2.`ticket` LIKE :term
            AND
            t1.`state` = :flagTicket
        )
)
26
Daniel

J'ai couru le même problème plusieurs fois maintenant et je pense avoir trouvé une solution assez simple et bonne. Si je souhaite utiliser plusieurs fois les paramètres, je les enregistre dans un User-Defined Variable MySQL.
Cela rend le code beaucoup plus lisible et vous n'avez pas besoin de fonctions supplémentaires en PHP:

$sql = "SET @term = :term";

try
{
    $stmt = $dbh->prepare($sql);
    $stmt->bindValue(":term", "%$term%", PDO::PARAM_STR);
    $stmt->execute();
}
catch(PDOException $e)
{
    // error handling
}


$sql = "SELECT ... FROM table WHERE name LIKE @term OR number LIKE @term";

try
{
    $stmt = $dbh->prepare($sql);
    $stmt->execute();
    $stmt->fetchAll();
}
catch(PDOException $e)
{
    //error handling
}

Le seul inconvénient est que vous devez effectuer une requête MySQL supplémentaire, mais à mon humble avis, cela en vaut la peine.
Étant donné que User-Defined Variables est lié à une session dans MySQL, il n’est pas nécessaire de s’inquiéter de la variable @term qui entraîne des effets secondaires dans les environnements multi-utilisateurs. 

15
low_rents

J'ai créé deux fonctions pour résoudre le problème en renommant les termes à double utilisation. Un pour renommer le SQL et un pour renommer les liaisons.

    /**
     * Changes double bindings to seperate ones appended with numbers in bindings array
     * example: :term will become :term_1, :term_2, .. when used multiple times.
     *
     * @param string $pstrSql
     * @param array $paBindings
     * @return array
     */
    private function prepareParamtersForMultipleBindings($pstrSql, array $paBindings = array())
    {
        foreach($paBindings as $lstrBinding => $lmValue)
        {
            // $lnTermCount= substr_count($pstrSql, ':'.$lstrBinding);
            preg_match_all("/:".$lstrBinding."\b/", $pstrSql, $laMatches);

            $lnTermCount= (isset($laMatches[0])) ? count($laMatches[0]) : 0;

            if($lnTermCount > 1)
            {
                for($lnIndex = 1; $lnIndex <= $lnTermCount; $lnIndex++)
                {
                    $paBindings[$lstrBinding.'_'.$lnIndex] = $lmValue;
                }

                unset($paBindings[$lstrBinding]);
            }
        }

        return $paBindings;
    }

    /**
     * Changes double bindings to seperate ones appended with numbers in SQL string
     * example: :term will become :term_1, :term_2, .. when used multiple times.
     *
     * @param string $pstrSql
     * @param array $paBindings
     * @return string
     */
    private function prepareSqlForMultipleBindings($pstrSql, array $paBindings = array())
    {
        foreach($paBindings as $lstrBinding => $lmValue)
        {
            // $lnTermCount= substr_count($pstrSql, ':'.$lstrBinding);
            preg_match_all("/:".$lstrBinding."\b/", $pstrSql, $laMatches);

            $lnTermCount= (isset($laMatches[0])) ? count($laMatches[0]) : 0;

            if($lnTermCount > 1)
            {
                $lnCount= 0;
                $pstrSql= preg_replace_callback('(:'.$lstrBinding.'\b)', function($paMatches) use (&$lnCount) {
                    $lnCount++;
                    return sprintf("%s_%d", $paMatches[0], $lnCount);
                } , $pstrSql, $lnLimit = -1, $lnCount);
            }
        }

        return $pstrSql;
    }

Exemple d'utilisation:

$lstrSqlQuery= $this->prepareSqlForMultipleBindings($pstrSqlQuery, $paParameters);
$laParameters= $this->prepareParamtersForMultipleBindings($pstrSqlQuery, $paParameters);
$this->prepare($lstrSqlQuery)->execute($laParameters);

Explication sur le nom de la variable:
p: paramètre, l: local en fonction
str: chaîne de caractères, n: numérique, a: tableau, m: mixte

9
pascalvgemert

Je ne sais pas si cela a changé depuis que la question a été posée, mais en vérifiant le manuel maintenant, il est indiqué:

Vous ne pouvez pas utiliser plusieurs fois un marqueur de paramètre nommé du même nom dans une instruction préparée, sauf si le mode d'émulation est activé.

http://php.net/manual/en/pdo.prepare.php - (Soulignez le mien.)

Ainsi, techniquement, autoriser les préparations émulées en utilisant $PDO_obj->setAttribute( PDO::ATTR_EMULATE_PREPARES, true ); fonctionnera également; bien que ce ne soit peut-être pas une bonne idée (comme indiqué dans cette réponse , désactiver les déclarations préparées simulées est un moyen de se protéger de certaines attaques par injection; bien que certains ont écrit le contraire que cela ne fait aucune différence à la sécurité, que les préparatifs soient imités ou non (je ne sais pas, mais je ne pense pas que ce dernier ait pensé à l'attaque mentionnée précédemment). 

J'ajoute cette réponse dans un souci d'exhaustivité; lorsque j'ai désactivé emulate_prepares sur le site sur lequel je travaillais, la recherche a été interrompue, car elle utilisait une requête similaire (SELECT ... FROM tbl WHERE (Field1 LIKE :term OR Field2 LIKE :term) ...), et tout fonctionnait correctement jusqu'à ce que je règle explicitement PDO::ATTR_EMULATE_PREPARES sur false, puis l'échec a commencé .

(PHP 5.4.38, MySQL 5.1.73 FWIW) 

Cette question est ce qui m'a fait comprendre que vous ne pouvez pas utiliser un paramètre nommé deux fois dans la même requête (ce qui me semble contre-intuitif, mais bon). (D'une manière ou d'une autre, j'ai raté cela dans le manuel, même si j'ai consulté cette page plusieurs fois.)

5
Aaron Wallentine

Une solution de travail:

$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, TRUE);
$query = "SELECT * FROM table WHERE name LIKE :term OR number LIKE :term";
$term  = "hello world";
$stmt  = $pdo->prepare($query);
$stmt->execute(array('term' => "%$term%"));
$data  = $stmt->fetchAll();
2
Your Common Sense

Les variables définies par l'utilisateur sont un moyen d'utiliser une même variable plusieurs fois pour lier les valeurs aux requêtes, ce qui fonctionne bien.

//Setting this doesn't work at all, I tested it myself 
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, TRUE);

Je ne voulais pas utiliser de variables définies par l'utilisateur du tout comme l'une des solutions publiées ici. Je ne voulais pas aussi renommer les paramètres comme l’autre solution publiée ici. Voici donc ma solution qui fonctionne sans utiliser de variables définies par l'utilisateur et sans renommer quoi que ce soit dans votre requête avec moins de code et le nombre de fois où le paramètre est utilisé dans la requête ne tient pas compte. Je l'utilise sur tout mon projet et ça marche bien.

//Example values
var $query = "select * from test_table where param_name_1 = :parameter and param_name_2 = :parameter";
var param_name = ":parameter";
var param_value = "value";

//Wrap these lines of codes in a function as needed sending 3 params $query, $param_name and $param_value. 
//You can also use an array as I do!

//Lets check if the param is defined in the query
if (strpos($query, $param_name) !== false)
{
    //Get the number of times the param appears in the query
    $ocurrences = substr_count($query, $param_name);
    //Loop the number of times the param is defined and bind the param value as many times needed
    for ($i = 0; $i < $ocurrences; $i++) 
    {
        //Let's bind the value to the param
        $statement->bindValue($param_name, $param_value);
    }
}

Et voici une solution de travail simple!

J'espère que cela aidera quelqu'un dans un proche avenir.

0
revobtz