The examples above used the so-called unbound statements. This means that we were supplying the values for the query in an array passed to the PDOStatement::
execute() method. PDO also supports bound statements where you can explicitly bind an immediate value or a variable to a named or positional placeholder.
To bind an immediate value to a statement, the PDOStatement::bindValue() method is used. This method accepts the placeholder identifier and a value. The placeholder identifier is the 1-based index of the question mark in the query for positional placeholders or the name of the named placeholder. For example, we could rewrite the example with positional placeholders to use bound values in the following way:
$stmt = $conn->prepare(
'INSERT INTO authors(firstName, lastName, bio) VALUES(?, ?, ?)');
foreach($authors as $author) {
$stmt->bindValue(1, $author['firstName']);
$stmt->bindValue(2, $author['lastName']);
$stmt->bindValue(3, $author['bio']);
$stmt->execute();
}
If you prefer named placeholders, you can write:
$stmt = $conn->prepare(
'INSERT INTO authors(firstName, lastName, bio) ' . 'VALUES(:last, :first, :bio)');
foreach($authors as $author) {
$stmt->bindValue(':first', $author['firstName']);
$stmt->bindValue(':last', $author['lastName']);
$stmt->bindValue(':bio', $author['bio']);
$stmt->execute();
}
As you can see, in both cases we don't supply anything in the call to
PDOStatement::execute(). Again, as with unbound statements, if you don't bind a value for every placeholder, the call to PDOStatement::execute() will fail, leading to an exception.
PDO can also bind result set columns to PHP variables for SELECT queries. These variables will be modified with corresponding column values on every call to PDOStatement::fetch(). This is an alternative to fetching the result set row as an array or an object as discussed in Chapter 2. Consider the following example:
$stmt = $conn->prepare('SELECT firstName, lastName FROM authors');
$stmt->execute();
$stmt->bindColumn(1, $first);
$stmt->bindColumn(2, $last);
while($stmt->fetch(PDO::FETCH_BOUND)) {
echo "$last, $first <br>";
}
This will render all the authors in the table. The variables are bound in the call to the PDOStatement::bindColumn() method, which expects the first parameter to be the 1-based index of the column in the result set or the column name as returned from the database, and the second parameter is the variable to be updated.
Note that when using bound columns, the PDOStatement::fetch() method should be called with the PDO::FETCH_BOUND mode, or this should be preset with a PDOStatement::setFetchMode(PDO::FETCH_BOUND) call. Also, the call to the PDOStatement::bindColumn() method must be made after the call to PDOStatement::execute() method so that PDO knows how many columns there are in the result set.
Let's get back to our library application now and enhance it with some prepared statements. Since the only pages that rely on the values supplied by the user are add/edit a book and add/edit an author, we will rewrite the two corresponding scripts, editBook.php and editAuthor.php.
Of course, we will only rewrite those bits of the code that update the database.
For editBook.php these are lines 65 to 102. I will present these lines here for your convenience:
if(@$book['id']) {
$sql = "UPDATE books SET title=" . $conn->quote($_POST['title']) . ', author=' . $conn->quote($_POST['author']) .
', isbn=' . $conn->quote($_POST['isbn']) .
', publisher=' . $conn->quote($_POST['publisher']) .
', year=' . $conn->quote($_POST['year']) . ', summary=' . $conn->quote($_POST['summary']) . " WHERE id=$book[id]";
} else {
$sql = "INSERT INTO books(title, author, isbn, publisher, year, summary) VALUES(" . $conn->quote($_POST['title']) . ', ' . $conn->quote($_POST['author']) .
', ' . $conn->quote($_POST['isbn']) . ', ' . $conn->quote($_POST['publisher']) . ', ' . $conn->quote($_POST['year']) . ', ' . $conn->quote($_POST['summary']) . ')';
}
// Now we are updating the DB.
// We wrap this into a try/catch block // as an exception can get thrown if // the ISBN is already in the table.
try {
$conn->query($sql);
// If we are here, then there is no error.
// We can return back to books listing header("Location: books.php");
exit;
}
catch(PDOException $e) {
$warnings[] = 'Duplicate ISBN entered. Please correct';
}
As we can see, the part that constructs the query is very long. With a prepared statement, this code snippet can be rewritten as follows:
if(@$book['id']) {
$sql = "UPDATE books SET title=?, author=?, isbn=?, publisher=?
year=?, summary=? WHERE id=$book[id]";
} else {
$sql = "INSERT INTO books(title, author, isbn, publisher, year, summary) VALUES(?, ?, ?, ?, ?, ?)";
}
$stmt = $conn->prepare($sql);
// Now we are updating the DB.
// We wrap this into a try/catch block // as an exception can get thrown if // the ISBN is already in the table.
try {
$stmt->execute(array($_POST['title'], $_POST['author'], $_POST['isbn'], $_POST['publisher'], $_POST['year'], $_POST['summary']));
// If we are here, then there is no error.
// We can return back to books listing.
header("Location: books.php");
exit;
}
catch(PDOException $e) {
$warnings[] = 'Duplicate ISBN entered. Please correct';
}
We follow the same logic—if we are editing an existing book, we construct an UPDATE query. If we are adding a new book, then we have to use an INSERT query.
The $sql variable will hold the appropriate statement template. In both cases, the statement has six positional placeholders, and I intentionally hard-coded the book ID into the UPDATE query so that we can create and execute the statement regardless of the required operation.
After we have instantiated the statement, we wrap the call to its execute() method into a try…catch block as an exception that may get thrown if the ISBN already existed in the database. Upon successful execution of the statement we redirect the browser to the books listing page. If the call fails, we alert the user with a note that the ISBN is incorrect (or that the book already exists in the database).
You can see that our code is now much shorter. Also, we don't need to quote the values as the prepared statement does this for us. Now you can play with this a bit and change the databases between MySQL and SQLite in common.inc.php to see that prepared statements work for both of them. You may also want to rewrite this code to use named placeholders instead of positional ones. If you do, remember to supply placeholder names in the array passed to the PDOStatement::execute() method.
Now let's look at the corresponding code block in editAuthor.php (lines 42 to 59):
if(@$author['id']) {
$sql = "UPDATE authors SET firstName=" . $conn->quote($_POST['firstName']) .
', lastName=' . $conn->quote($_POST['lastName']) . ', bio=' . $conn->quote($_POST['bio']) .
" WHERE id=$author[id]";
} else {
$sql = "INSERT INTO authors(firstName, lastName, bio) VALUES(" . $conn->quote($_POST['firstName']) .
', ' . $conn->quote($_POST['lastName']) . ', ' . $conn->quote($_POST['bio']) . ')';
}
$conn->query($sql);
header("Location: authors.php");
exit;
As we don't expect an exception here, the code is shorter. Now let's rewrite it to use a prepared statement:
if(@$author['id']) {
$sql = "UPDATE authors SET firstName=?, lastName=?, bio=?
WHERE id=$author[id]";
} else {
$sql = "INSERT INTO authors(firstName, lastName, bio) VALUES(?, ?, ?)";
}
$stmt = $conn->prepare($sql);
$stmt->execute(array($_POST['firstName'], $_POST['lastName'], $_POST['bio']));
header("Location: authors.php");
exit;
Again, depending on the required operation, we create the SQL template and assign it to the $sql variable. Then we instantiate the PDOStatement object and call its execute method with the author's details. As our query should never fail (except for an unforeseen database failure) we don't expect an exception here and redirect to the authors listing pages.
Make sure that you test this code with both MySQL and SQLite.