5
votes

I'm kind of new with PDO and currently developing the API call that returns search results. How do I set a prepare statement if there are 2 optional parameters for the search query?

$app->get('/get/search', function () {
    $sql = 'SELECT * FROM user WHERE name LIKE :name AND city = :city AND gender = :gender';
    try {
        $stmt = cnn()->prepare($sql);
        $stmt->bindParam(':name', '%'.$_GET['name'].'%', PDO::PARAM_STR);
        $stmt->bindParam(':city', '%'.$_GET['city'].'%', PDO::PARAM_STR);
        $stmt->bindParam(':gender', $_GET['gender'], PDO::PARAM_INT);
        $stmt->execute();
        if($data = $stmt->fetchAll()) {
            echo json_encode($data);
        } else {
            echo json_encode(array('error' => 'no records found');
        }
    } catch(PDOException $e) {
        echo json_encode(array('error' => $e->getMessage()));
    }
}

The issue here, is that both $_GET['city'] and $_GET['gender'] are optional. If I try to run the code above, it will asume that any empty variable should match an empty value in the column as well; in the other hand, if I do something like this:

if($_GET['gender']) $stmt->bindParam(':gender', $_GET['gender'], PDO::PARAM_INT);

...it will return this error: "SQLSTATE[HY093]: Invalid parameter number: number of bound variables does not match number of tokens"

So, what's the solution if I want to keep the prepared sql statement for optional parameters? Thanks!

Update

This is the solution based on the accepted answer and some comments (by deceze and bill-karwin):

if($_GET['name']) $where[] = 'name LIKE :name';
if($_GET['city']) $where[] = 'city LIKE :city';
if(isset($_GET['gender'])) $where[] = 'gender = :gender';
if(count($where)) {
    $sql = 'SELECT * FROM user WHERE '.implode(' AND ',$where);
    $stmt = cnn()->prepare($sql);
    $name = '%'.$_GET['name'].'%';
    if($_GET['name']) $stmt->bindValue(':name', '%'.$_GET['name'].'%', PDO::PARAM_STR);
    $city = '%'.$_GET['city'].'%';
    if($_GET['city']) $stmt->bindParam(':city', $city, PDO::PARAM_STR);
    if(isset($_GET['gender'])) $stmt->bindParam(':gender', $_GET['gender'], PDO::PARAM_BOOL);
    $stmt->execute();
    if($data = $stmt->fetchAll()) {
        echo json_encode($data);
    }
}
2
actually it is a tiny int (0 or 1). but that is not the issue. the issue is that both gender and city (or any other future parameter) should be optional in the querystring. Is that feasible with a prepared statement? - Andres SK
@LuigiSiri I am now using PDO::PARAM_BOOL, since it can only be 1 or 0. - Andres SK

2 Answers

16
votes

Some good old dynamic SQL query cobbling-together...

$sql = sprintf('SELECT * FROM user WHERE name LIKE :name %s %s',
               !empty($_GET['city'])   ? 'AND city   = :city'   : null,
               !empty($_GET['gender']) ? 'AND gender = :gender' : null);

...

if (!empty($_GET['city'])) {
    $stmt->bindParam(':city', '%'.$_GET['city'].'%', PDO::PARAM_STR);
}

...

You can probably express this nicer and wrap it in helper functions etc. etc, but this is the basic idea.

2
votes

There is a nice little function which can help: tiniest query builder. No frameworks or ORMs needed to make code look like this:

public function updateUser(int $id, string $email = '', string $password = '', string $name = '') {
    $sql = \App\Utils\build_query([
        [               'UPDATE "users"'],
        [$email         ,'SET', 'email=:email'],
        [$password      ,',',   'password=:password'],
        [$name          ,',',   'name=:name'],
        [               'WHERE "id"=:id']
    ]);

    $stmt = $this->db->prepare($sql);
    $stmt->bindValue(':id', $id, \PDO::PARAM_INT);
    // Optional bindings.
    $email &&       $stmt->bindValue(':email', $email, \PDO::PARAM_STR);
    $password &&    $stmt->bindValue(':password', $password, \PDO::PARAM_STR);
    $name &&        $stmt->bindValue(':name', $name, \PDO::PARAM_STR);

    $stmt->execute();
}

Note how neatly query components are created, with support for optional ones of course. The && experssions by bindings simply check whether this parameter is given, and if it is, then appropriate bindValue are called.