For example, if you want to get a user by username (from user input):
Raw SQL (vulnerable)
$username = $_GET['username'] ?? '';
$sql = "SELECT * FROM users WHERE username = '$username' LIMIT 1";
$user = ExecuteRow($sql);
Using ExecuteRow() is fine if the SQL does not contain user input, but it becomes vulnerable if it accepts user input (e.g. $username) which may contain malicious part, e.g. ' OR 1=1; --, your sql will become:
SELECT * FROM users WHERE username = '' OR 1=1; --'
Problems:
- Direct concatenation of
$username→ SQL injection risk. - Manual escaping would be needed, e.g.
AdjustSql($username).
DBAL QueryBuilder (safe)
use Doctrine\DBAL\Types\Types;
$username = $_GET['username'] ?? '';
$user = QueryBuilder(); // Assume main database
->select('*')
->from('users')
->where('username = :username')
->setParameter('username', $username, Types::STRING)
->setMaxResults(1)
->executeQuery()
->fetchAssociative();
Yes, more code, but more benefits:
-
SQL Injection Safe
- User input is bound as a parameter, never concatenated into SQL.
- Even if
$username = "' OR 1=1 --", the database treats it as a string literal.
-
Readable and Fluent
- Method chaining clearly shows the query structure.
-
Dynamic and Extendable
- Adding extra filters is easy:
if ($status !== null) { $qb->andWhere('status = :status') ->setParameter('status', $status); } -
Portable
- Works across different databases without changing SQL syntax.
-
Type-safe
- Using
Types::STRINGensures the DB interprets it correctly.
- Using
What types are available? See source code of \Doctrine\DBAL\Types\Types.