Doctrine DBAL vs. ORM

1. Doctrine DBAL

DBAL gives low-level SQL access.
You write SQL or use the QueryBuilder manually.
You get arrays, not entities.

$user = QueryBuilder()
    ->select('*')
    ->from('users')
    ->where('id = :id')
    ->setParameter('id', $id)
    ->executeQuery()
    ->fetchAssociative();

Return type: associative array of raw database values
Pros: Fast, direct SQL, no entity overhead
Cons: No hydration (Turning SQL result sets into entity objects)

When you fetch rows using DBAL (for example with fetchAssociative()), you get raw values exactly as they come from the database driver.

That means:

  • Numeric fields are returned as strings (for example "123", not 123)
  • Boolean fields are returned as "0" or "1"
  • Datetime fields are returned as strings like "2025-10-22 14:30:00"
  • JSON fields are returned as strings containing JSON text
  • If you use Field Encryption extension, the field value is encrypted
  • There is no automatic conversion to PHP objects, DateTime instances, or other types

2. Doctrine ORM

ORM works with entities (classes mapped to database tables).
You get objects, not arrays.

// User table is from the main database so you can use EntityManager() without argument
$user = EntityManager()->getRepository(Db\Entity\User::class)->find($userId);

// Or you can simply use GetRepository()
$user = GetRepository(Db\Entity\User::class)->find($userId);

// The user table is a special table, you can also use UserRepository()
$user = UserRepository()->find($userId);

// You can also select a record by criteria
$user = UserRepository()->findOneBy(['email' => 'test@example.com']); // 'email' is property name, not column name

Return type: User|null
Pros: Automatic hydration, lifecycle callbacks
Cons: Slower, heavier, more memory usage

When using ORM, fields are mapped to PHP types via metadata. A conversion layer exists, for example, Doctrine knows that createdAt is a datetime type.

#[Column(type: 'datetime')]
private ?DateTimeInterface $createdAt;

So when you do:

$user = UserRepository()->find(1);
$createAt = $user->getCreatedAt();

Doctrine automatically converts "2025-10-22 14:30:00" into a DateTime object.
If you use Field Encryption, the field value is decrypted.
This is called hydration or type conversion, handled by Doctrine’s Type system.

How to know the entity class name of a table (for getting the repository of the table) and the field property names? See Database, Table and Field Variable Names.