39

I'm trying to implement a pretty basic search engine for my database where the user may include different kinds of information. The search itself consists of a couple of a union selects where the results are always merged into 3 columns.

The returning data however is being fetched from different tables.

Each query uses $term for matchmaking, and I've bound it to ":term" as a prepared parameter.

Now, the manual says:

You must include a unique parameter marker for each value you wish to pass in to the statement when you call PDOStatement::execute(). You cannot use a named parameter marker of the same name twice in a prepared statement.

I figured that instead of replacing each :term parameter with :termX (x for term = n++) there must be a be a better solution?

Or do I just have to bind X number of :termX?

Edit Posting my solution to this:

$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);

Alright, here is a sample. I don't have time for sqlfiddle but I will add one later if it is necessary.

(
    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
        )
)
Daniel
  • 3,726
  • 4
  • 26
  • 49
  • First of all why *must* there be a better solution? (btw. you have forgotten to provide specifics what *better* means in your case) and secondary, why doesn't unnamed parameters work for you? (see *Example #2 Prepare an SQL statement with question mark parameters* http://php.net/pdo.prepare) – hakre Aug 29 '13 at 13:01
  • 2
    @hakre Unnamed parameters impose the same issue since there must be equally as many bound values as `?`? A better solution in my case would be to `->bindValue(':term', $term)` and use :term multiple times instead of first building the query, and then parsing it to finally be able to prepare it. I guess `?` would only make it harder to parse the final query since there are other parameter types as well. – Daniel Aug 29 '13 at 13:07
  • So again, why *must* there be a better solution? Because you want it? I then get the feeling your question is off topic as looking for an off-site resource or library. I do not see the underlying programming problem asked. Sorry. – hakre Aug 29 '13 at 13:12
  • 3
    @hakre Are you for real or what? The underlying programming problem is that the API is probably missing a key-feature. That is what I'm trying to figure out. I have no problem with extending PDO, but I don't feel the need to reinvent the wheel. As you can see, I've already worked around it, but felt that there must be a better way. – Daniel Aug 29 '13 at 13:17
  • 1
    If you have got a feature request for PDO, feel free to open an issue for that, but I doubt this is a key feature. The key feature is actually to provide bound parameters and that's it. What you are looking for is probably a library that makes it easier to formulate prepared statements, something like an SqlExpression class or a class that wraps / constitutes / represents a prepared statment. And I can not see you worked around it, where is the code? You so far have outlined your requirements, but that alone is not a programming question IMHO. And missing feature can be highly biased. – hakre Aug 29 '13 at 13:26
  • Related: [PDO bind unknown number of parameters? (May 2011)](http://stackoverflow.com/a/6071769/367456) – hakre Aug 29 '13 at 13:41
  • What problem did you exactly solve? I really can't bring myself to see it. Does content of $term changes? If not, what's the point of multiple parameters? If yes, why in god's name are you using variables prefixed with $i and not an array? Why aren't you creating the statement dynamically where you don't have to use preg_match in order to extract term's number? This has nothing to do with PDO lacking features, it's simply to do with lack of thinking. – N.B. Aug 29 '13 at 13:50
  • That is the most simplest of queries in my example... Binding a single parameter is not the the problem. The problem is when the bound parameters is used in an unknown order. – Daniel Aug 29 '13 at 13:55
  • But if you use contents of variable $term which doesn't change, why would you come up with multiple parameters that would bind to the exactly same value? That makes no sense to me, I really can't see what problem you're solving. I might be dumb for missing something obvious. – N.B. Aug 29 '13 at 13:58
  • @N.B. That is exactly what I'm saying... Why do I have to bind :termX multiple times instead of reusing the parameter? **The answer is probably** that PDO casts a named parameter to ? for MYSQL, and not the actual name of the parameter. – Daniel Aug 29 '13 at 14:12
  • Now where did you read that you have to bind it multiple times? You use the parameter multiple times and bind the value once. I've no idea how or why you got to the (wrong) conclusion. Maybe you missed out something in your testing? – N.B. Aug 29 '13 at 14:13
  • @N.B. Read my quote from the PHP manual in my question. – Daniel Aug 29 '13 at 14:14
  • @Daniel - out of curiosity.. have you actually tried to use `:term` twice in the query? You might have a different query (compared to one I used, which is OR clause with the same parameter) which produces the fault. I used the parameter more than once, bound it once, got the results fine. – N.B. Aug 29 '13 at 14:18
  • @N.B. Yes, I have tried it. I use up to 5 sub-queries and UNION the results. – Daniel Aug 29 '13 at 14:22
  • Could you post that query so we can try and replicate the fault you're getting? I've been using named parameters for years, multiple times in complex queries and I never ran into any issue. – N.B. Aug 29 '13 at 14:23
  • it was unnecessary. a simple `SELECT :term t1, :term t2` is enough to test the behavior. – Your Common Sense Aug 29 '13 at 15:02
  • if you still didn't notice, I already fixed that sully typo in my code. So, there is no point in popping your question up - it is already solved – Your Common Sense Aug 30 '13 at 08:34
  • @YourCommonSense Well, it doesn't work. So I can't check your answer as correct. Even the manual states that it's not possible. Your answer works if there is only **one** bound parameter. Also, I didn't bump this question, I edited the title. – Daniel Aug 30 '13 at 10:02
  • Quite contrary, it works fine. I don't mind if you won't check it, though. – Your Common Sense Aug 30 '13 at 10:12
  • @YourCommonSense your answer doesn't work for me at all. – low_rents Jun 26 '15 at 09:06
  • The reason you are getting different results is because the results are different depending on whether you have PDO::ATTR_EMULATE_PREPARES set - [see this answer](http://stackoverflow.com/a/35375592/1438796) – Stack Man May 06 '16 at 07:11

5 Answers5

25

I have ran over the same problem a couple of times now and I think i have found a pretty simple and good solution. In case i want to use parameters multiple times, I just store them to a MySQL User-Defined Variable.
This makes the code much more readable and you don't need any additional functions in 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
}

The only downside might be that you need to do an additional MySQL query - but imho it's totally worth it.
Since User-Defined Variables are session-bound in MySQL there is also no need to worry about the variable @term causing side-effects in multi-user environments.

low_rents
  • 4,481
  • 3
  • 27
  • 55
11

I created two functions to solve the problem by renaming double used terms. One for renaming the SQL and one for renaming the bindings.

    /**
     * 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;
    }

Example of usage:

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

Explanation about the variable naming:
p: parameter, l: local in function
str: string, n: numeric, a: array, m: mixed

pascalvgemert
  • 1,247
  • 1
  • 13
  • 28
  • This code works for me. The code snippet should be a standard part of anyone's library to make PDO usage easier and more flexible. Thanks! – alds Jul 29 '14 at 11:04
  • Thanks amigo, I agree! – pascalvgemert Jul 29 '14 at 11:11
  • Saw this question today, sorry for not responding... This is pretty much what I'm doing today as well. I'll flag this as the solution. – Daniel Oct 08 '14 at 09:29
  • A neater solution than the accepted answer of using UDVs, imho. – Codemonkey Oct 10 '17 at 00:07
  • This is not a perfect solution. Imagine having a hard coded value in your query (not binded I mean) : **description = "Airport:terminal C"**. Here you will replace the ":term" in the string which is not correct. – JeanBlaguin Nov 05 '19 at 16:43
11

I don't know if it's changed since the question was posted, but checking the manual now, it says:

You cannot use a named parameter marker of the same name more than once in a prepared statement, unless emulation mode is on.

http://php.net/manual/en/pdo.prepare.php -- (Emphasis mine.)

So, technically, allowing emulated prepares by using $PDO_obj->setAttribute( PDO::ATTR_EMULATE_PREPARES, true ); will work too; though it may not be a good idea (as discussed in this answer, turning off emulated prepared statements is one way to protect from certain injection attacks; though some have written to the contrary that it makes no difference to security whether prepares are emulated or not. (I don't know, but I don't think that the latter had the former-mentioned attack in mind.)

I'm adding this answer for the sake of completeness; as I turned emulate_prepares off on the site I'm working on, and it caused search to break, as it was using a similar query (SELECT ... FROM tbl WHERE (Field1 LIKE :term OR Field2 LIKE :term) ...), and it was working fine, until I explicitly set PDO::ATTR_EMULATE_PREPARES to false, then it started failing.

(PHP 5.4.38, MySQL 5.1.73 FWIW)

This question is what tipped me off that you can't use a named parameter twice in the same query (which seems counterintuitive to me, but oh well). (Somehow I missed that in the manual even though I looked at that page many times.)

Community
  • 1
  • 1
Aaron Wallentine
  • 2,318
  • 24
  • 22
  • 2
    "this answer" actually says the same as another. If you skip all the scandalous stuff at the beginning, in the end it says that as long as you're using supported version of PHP and set charset through DSN, there are no known vulnerabilities for the emulation mode. – Your Common Sense Feb 13 '16 at 04:38
  • BTW, somehow you missed other answers about such setting on this page as well. – Your Common Sense Feb 13 '16 at 04:40
4

It's possible only if you enable prepared statement emulation. You can do it by setting PDO::ATTR_EMULATE_PREPARES to true.

A working solution:

$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();
Dharman
  • 30,962
  • 25
  • 85
  • 135
Your Common Sense
  • 156,878
  • 40
  • 214
  • 345
  • not a solution, pdo returns only strings if you do this – Gaby_64 Jan 14 '20 at 16:38
  • Doesn't this open your query to sql injection as explained [here](https://stackoverflow.com/a/12118602)? – AaA Feb 22 '23 at 10:41
  • @Gaby_64, what do you mean, returns strings? does it mean all numbers will be omitted from recordset or change all values returned to string? – AaA Feb 22 '23 at 10:42
  • @AaA it's a confused comment. Of course, numbers won't be omitted. They will be returned as strings in regard of variable type. Most likely you won't even notice it. – Your Common Sense Feb 22 '23 at 10:49
  • @AaA It doesn't open your query to sql injection. The answer you linked to is extremely muddled, and written mostly to show off. But you can read what it says in the end: "If you: Use Modern Versions of MySQL AND / $mysqli->set_charset() / PDO's DSN charset parameter You're 100% safe." – Your Common Sense Feb 22 '23 at 10:51
  • Good to know that answer doesn't apply anymore. however I came across a situation that records that are updated from different application doesn't appear in my results for a few minutes. I'm not sure if it is related to this, but would it be possible because this turns on query caching? sorry if my question is noob! – AaA Feb 24 '23 at 02:25
  • @AaA no, your issue is unrelated to anything like this or query cache – Your Common Sense Feb 24 '23 at 05:36
  • @YourCommonSense Of course you probably wont notice numbers are strings, thats why I mentioned it, you will only notice when unintended consequences appear and this will not be most unlikely, actual will be very likely across many projects using pdo. – Gaby_64 May 12 '23 at 15:35
0

User defined variables its one way to go and use a the same variable multiple times on binding values to the queries and yeah that works well.

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

I didn't wanted to use user defined variables at all like one of the solutions posted here. I didn't wanted also to do param renaming like the other solution posted here. So here it's my solution that works without using user defined variables and without renaming anything in your query with less code and it doesn't care about how many times the param is used in the query. I use this on all my project and it's works well.

//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);
    }
}

And here is a simple working solution!

Hope this helps someone in the near future.

revobtz
  • 606
  • 10
  • 12