databases storage and caching
Prepared Statements
Prepared statements separate the SQL command from the values being supplied to it. They are one of the most important habits in PHP database work because they prevent user input from being treated as SQL syntax.
Use prepared statements for values from forms, URLs, JSON bodies, cookies, sessions, queues, CSV imports, and third-party APIs. Even internal values should usually be bound as parameters because it keeps SQL consistent and reviewable.
Basic Example
<?php
declare(strict_types=1);
$pdo = new PDO('sqlite::memory:');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
$pdo->exec('CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT NOT NULL)');
$pdo->exec("INSERT INTO users (email) VALUES ('sam@example.com')");
$email = 'sam@example.com';
$stmt = $pdo->prepare('SELECT id, email FROM users WHERE email = :email');
$stmt->execute(['email' => $email]);
$user = $stmt->fetch();
echo $user['email'] . PHP_EOL;
// Prints:
// sam@example.com
The SQL contains :email. The value is passed separately to execute().
Named And Positional Placeholders
PDO supports named placeholders and positional placeholders.
<?php
declare(strict_types=1);
$namedSql = 'SELECT id FROM users WHERE email = :email';
$positionalSql = 'SELECT id FROM users WHERE email = ?';
echo $namedSql . PHP_EOL;
echo $positionalSql . PHP_EOL;
// Prints:
// SELECT id FROM users WHERE email = :email
// SELECT id FROM users WHERE email = ?
Named placeholders are often easier to read in application code.
Dynamic Filters
Prepared statements do not mean every query must be fixed. You can build allowed SQL fragments while still binding values.
<?php
declare(strict_types=1);
function userListSql(array $filters): array
{
$where = [];
$parameters = [];
if (isset($filters['status'])) {
$where[] = 'status = :status';
$parameters['status'] = $filters['status'];
}
if (isset($filters['email_search'])) {
$where[] = 'email LIKE :email';
$parameters['email'] = '%' . $filters['email_search'] . '%';
}
$sql = 'SELECT id, email, status FROM users';
if ($where !== []) {
$sql .= ' WHERE ' . implode(' AND ', $where);
}
return ['sql' => $sql, 'parameters' => $parameters];
}
print_r(userListSql(['status' => 'active', 'email_search' => 'example.com']));
// Prints:
// SELECT id, email, status FROM users WHERE status = :status AND email LIKE :email
// [email] => %example.com%
The column names and operators are chosen by the application. The values are still bound.
What Cannot Be Parameterised
Placeholders are for values, not SQL structure. You cannot safely bind a table name, column name, sort direction, or SQL keyword as a normal parameter.
For dynamic sorting, use an allow-list.
<?php
declare(strict_types=1);
function orderByClause(string $requestedSort): string
{
return match ($requestedSort) {
'email' => 'ORDER BY email ASC',
'newest' => 'ORDER BY created_at DESC',
default => 'ORDER BY id ASC',
};
}
echo orderByClause('newest') . PHP_EOL;
echo orderByClause('anything unsafe') . PHP_EOL;
// Prints:
// ORDER BY created_at DESC
// ORDER BY id ASC
Never paste a raw request value into ORDER BY, table names, or column names.
Binding Types
execute(['id' => 123]) is enough for many cases. Sometimes you need explicit types, especially for LIMIT, booleans, or driver-specific behaviour.
<?php
declare(strict_types=1);
$sql = 'SELECT id, email FROM users ORDER BY id LIMIT :limit';
$limit = 20;
echo $sql . PHP_EOL;
echo 'Bind :limit as integer: ' . $limit . PHP_EOL;
// Prints:
// SELECT id, email FROM users ORDER BY id LIMIT :limit
// Bind :limit as integer: 20
In real PDO code, use $statement->bindValue(':limit', $limit, PDO::PARAM_INT) before executing.
What To Check
Before moving on, make sure you can:
- Explain why prepared statements prevent SQL injection.
- Use named placeholders with
prepare()andexecute(). - Build dynamic filters while binding values.
- Use allow-lists for dynamic SQL structure.
- Handle
LIKEpatterns deliberately. - Know when explicit parameter types are useful.
Practice
Practice: Prepared Search Query
Write a PHP function that builds and runs a prepared user search query.
Requirements
- Use SQLite in memory so the example is runnable.
- Insert sample users.
- Search by optional status and email text.
- Use placeholders for all values.
- Use an allow-list for sort options.
- Print the matching emails for at least two searches.
Show solution
This solution binds values and uses an allow-list for the sort clause.
<?php
declare(strict_types=1);
$pdo = new PDO('sqlite::memory:');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
$pdo->exec('CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT NOT NULL, status TEXT NOT NULL)');
$pdo->exec("INSERT INTO users (email, status) VALUES
('sam@example.com', 'active'),
('lee@example.com', 'disabled'),
('alex@test.com', 'active')
");
function sortClause(string $sort): string
{
return match ($sort) {
'email' => 'ORDER BY email ASC',
'newest' => 'ORDER BY id DESC',
default => 'ORDER BY id ASC',
};
}
function searchUsers(PDO $pdo, array $filters, string $sort = 'id'): array
{
$where = [];
$parameters = [];
if (isset($filters['status'])) {
$where[] = 'status = :status';
$parameters['status'] = $filters['status'];
}
if (isset($filters['email'])) {
$where[] = 'email LIKE :email';
$parameters['email'] = '%' . $filters['email'] . '%';
}
$sql = 'SELECT email FROM users';
if ($where !== []) {
$sql .= ' WHERE ' . implode(' AND ', $where);
}
$sql .= ' ' . sortClause($sort);
$statement = $pdo->prepare($sql);
$statement->execute($parameters);
return $statement->fetchAll(PDO::FETCH_COLUMN);
}
foreach (searchUsers($pdo, ['status' => 'active'], 'email') as $email) {
echo $email . PHP_EOL;
}
foreach (searchUsers($pdo, ['email' => 'example.com'], 'newest') as $email) {
echo $email . PHP_EOL;
}
// Prints:
// alex@test.com
// sam@example.com
// lee@example.com
// sam@example.com
The sort value changes only between known SQL fragments. The status and email values are passed as parameters.