Multi word LIKE Prepared statement for PDO query

I am trying to build the logic to create a multi word LIKE statement for use with PDO.

This takes the search string $str to build the multiple parts of the LIKE section:

$str = $_POST['str'];

$keywords = preg_split('/[\s]+/', $str);
$totalKeywords = count($keywords);

$search = "%$str%";
$sql_str = " AND post_content LIKE :search0 ";

for($i=1 ; $i < $totalKeywords; $i++){
    $search_bit = ":search" . $i;
    $sql_str .= " AND post_content LIKE $search_bit ";
}

This is the SQL statement - with the $sql_str slotted into the correct point:

$sql = "SELECT d.ID
             , d.post_date
             , d.post_content
             , d.post_cat_id
             , d.post_label
             , c.fld_cat
          FROM tbl_log_days d
             , tbl_log_cats c
         WHERE d.post_cat_id = c.fld_id " . $sql_str . "
      ORDER BY post_date";

Then for binding the variables, I have tried two approaches:

$stmt = $pdo->prepare($sql);

if (!empty($sql_str)) {

    foreach ($keywords as $key => &$keyword){
        $foo = '%'.$keyword.'%';
        $stmt->bindParam(':search' . $key, $foo);
    }

}

And also this (without the ampersand before the $keyword in the foreach line):

$stmt = $pdo->prepare($sql);

if (!empty($sql_str)) {

    foreach ($keywords as $key => $keyword){
        $foo = '%'.$keyword.'%';
        $stmt->bindParam(':search' . $key, $foo);
    }

}

However, when I search for e.g. “past hill” and check the resulting SQL that is actually run (I enabled query logging in MySQL), it only takes the last word in the search string:

SELECT d.ID
     , d.post_date
     , d.post_content
     , d.post_cat_id
     , d.post_label
     , c.fld_cat
  FROM tbl_log_days d
     , tbl_log_cats c
 WHERE d.post_cat_id = c.fld_id  AND post_content LIKE '%past%'  AND post_content LIKE '%past%' 
  ORDER BY post_date

I have done a var_dump of the $keyword variable when running a search and it returns:

string(4) "hill"
string(4) "past"

I also did a var_dump of the $key variable and confirmed it is incrementing by one each time as it is taken from the $keywords array.

I prefer to use named placeholders in the prepared statement as I have other fields in the search form not included in this example code for the sake of simplicity (e.g. category / date ranges etc). Using named placeholders makes it simpler when adding in all of the different bindParam lines.

I can’t work this one out. Is it possible to do what I’m trying to do?

use bindValue() instead of bindParam(). bound parameters are resolved on execution time not on assignment time, and since you always bind the same variable ($foo) only the last assignment (%past%) is used.

the problem something different, as he is getting the first word bound, not the last one.
Besides, his SQL string is also prepared for only one keyword as well. So the problem is likely with input values and/or the algorithm.

I’ve seen his question on Stack Overflow but had no time to debug the code for him.

Is that the case? OP says he searches for “past hill” but then when showing the var_dump results for $keyword he shows them in the opposite order, suggesting the search was for “hill past”.

The SQL seems to me to be prepared for more than one keyword as he appends a count variable to :search each time. I’m not exactly sure why he treats :search0 differently at the start, but the only issue I can see there is that (in the code on here, not necessarily the real code) it will always assume there’s a single search term.

To me it seems this way:

He wants to have as many lines like

 AND post_content LIKE '%kword%'  AND post_content LIKE '%kword%' 

as many keywords in the source search string.

thus, as there is only one such line in the resulting query, there is either only one keyword or some flaw in the algorithm.

On a side note, he is probably need OR instead of AND

I read it as the OP building up $sql_str to contain the "and post_content like ‘%word%’ " condition as many times as there are entries in the $keywords array, with the parameter being named as :search followed by a number. The loop at the end runs through the array and binds the parameter to the appropriate keyword.

The code in your post is the result of him having two entries in the $keywords array twice, and should contain parameters named :search0 and :search1 before the values are substituted.

Hi - thanks for your reply. That’s what I’m trying to do.

So e.g. if I enter “past hill” in the search field, I wanted the resulting SQL to be:

SELECT d.ID
     , d.post_date
     , d.post_content
     , d.post_cat_id
     , d.post_label
     , c.fld_cat
  FROM tbl_log_days d
     , tbl_log_cats c
 WHERE d.post_cat_id = c.fld_id  AND post_content LIKE '%past%'  AND post_content LIKE '%hill%' 
  ORDER BY post_date

But whatever I do, the parameter value in the LIKE statement is always the value of the last entry in the search string.

If it’s the last one, then @dormilich has the key to it in post #2. bindParam() denotes where to get the value for the parameter substitution when the execute() is called, and by that time, your variable contains the final value in the loop. Effectively it says “when I execute the query, here’s the place you can find the value”, it doesn’t copy the value into the query at that point. How about something like

$stmt = $pdo->prepare($sql);

if (!empty($sql_str)) {

    for ($x = 0; $x<$totalKeywords; $x++) {
        // add the percent signs, or make a new copy of the array first if you want to keep the parameters
        $keywords[$x] = "%" . $keywords[$x] . "%";  
        $stmt->bindParam(':search' . $x, $keywords[$x]);
        }
}

Thank you very much @droopsnoot that has fixed it.
I appreciate your patient help.

No problem, glad it’s working, to be fair it was dormilich that pointed out the bindparam/bindvalue thing - I looked through the code a few times and it didn’t pop out.

Thanks @Dormilich - your advice here helped @droopsnoot work out the solution.

Just a suggestion. As you discovered, building non-trivial sql strings by hand can be a bit challenging and perhaps error prone. Glad you got it to work but imagine coming back in a month or two and having to modify it. Won’t be fun.

There are query builder libraries out there such a Doctrine’s Database Access Layer which abstracts away much of the effort: http://doctrine-orm.readthedocs.io/projects/doctrine-dbal/en/latest/reference/query-builder.html

Thanks for the suggestion @ahundiak. Unfortunately I’m not very smart when it comes to programming, hence why I have to come to places like Sitepoint and Stack Overflow, to ask cleverer people how to do things. I looked at OOP and frameworks a couple of months ago when I decided to start learning PHP and quickly realised I’d have to go down the clunky procedural route, to suit my clunky brain…

Thanks again

to be fair, hardly any tutorial author seems to be aware of the difference between bindValue() & bindParam() since mysqli doesn’t have an equivalent to bindValue().

To my taste, neither should be used.
Especially in case when the data is already stored in an array, and thus can be sent right into execute()

Good point, would need some modification to the array but nothing more significant than adding the parameter name as array index, and adding the other parameters that the OP mentioned into the array as well.

Not trying to hijack the thread but here is a reusable solution for dealing with multiple WHERE statements using the Doctrine database access layer query builder.

public function findPools(array $criteria)
{
    $qb = $this->conn->createQueryBuilder();

    // Just grab everything for now
    $qb->select('*')->from('poolTeams')->orderBy('poolKey,poolTeamKey');

    // All possible where clauses
    $whereMeta = [
        'projectIds'   => 'projectId',

        'poolKeys'     => 'poolKey',
        'poolTypeKeys' => 'poolTypeKey',
        'poolTeamKeys' => 'poolTeamKey',
        'poolTeamIds'  => 'poolTeamId',

        'programs'     => 'program',
        'genders'      => 'gender',
        'ages'         => 'age',
        'divisions'    => 'division',
    ];
    list($values,$types) = $this->addWhere($qb,$whereMeta,$criteria);
    $stmt = $qb->getConnection()->executeQuery($qb->getSQL(),$values,$types);
    
    while($poolTeamRow = $stmt->fetch()) { ...

The $criteria is a simple array. The $whereMeta simply maps all the possible criteria keys to a database column. The andWhere (implemented as a trait for code sharing) look like:

protected function addWhere(QueryBuilder $qb, array $metas, array $criteria, array $values = [], array $types = [])
{
    foreach($metas as $key => $col) {
        if (isset($criteria[$key]) && count($criteria[$key])) {
            $qb->andWhere($col . ' IN (?)');
            $values[] = $criteria[$key];
            $types[]  = Connection::PARAM_STR_ARRAY;
        }
    }
    return [$values,$types];
}

In this case I am actually using WHERE IN(?) clauses with ? being an array of 1 or more values. Seems to work okay and helps to implement moderately complex searches.

that’s fine as long as you don’t have a data type other than string or integer.

This topic was automatically closed 91 days after the last reply. New replies are no longer allowed.