DEV Community

Dev-Iadicola
Dev-Iadicola

Posted on

A Clean, Driver-Aware ORM Architecture in PHP

Overview

I recently completed a major refactor of my custom ORM, evolving it from a hybrid Active Record structure into a clean, layered, driver-aware architecture.
The new design removes hidden “magic”, improves type-safety, and gives each component a precise, single responsibility.

Diagram of ORM

1. Model Layer

Models now define only structure and metadata.

They no longer execute queries or manage persistence.

Instead, each Model delegates all operations to a dedicated ActiveQuery pipeline, ensuring:

  • deterministic behavior
  • zero hidden state
  • strict separation of concerns

In future updates, I will remove the "fillable" property entirely and introduce true encapsulated domain fields, powered by Value Objects.

2. ORM Runtime

The ORM runtime stores only two global resources:

  • the active PDO instance
  • the SQL driver currently in use

No additional state is leaked across queries.

This makes the system predictable, stable, and fully driver-agnostic.

3. ActiveQueryFactory

This factory assembles the complete ORM pipeline for every Model:

  • loads model metadata
  • creates the correct QueryBuilder based on the driver
  • instantiates the QueryExecutor
  • instantiates the ModelHydrator
  • returns a clean, isolated ActiveQuery instance

Every query begins with a fresh builder and clean state.

4. Driver-Specific Query Builders

The QueryBuilder is now split into two layers:

AbstractBuilder

Contains all shared SQL logic:

  • SELECT, WHERE, JOIN
  • GROUP BY, HAVING, ORDER BY
  • LIMIT, OFFSET
  • binding system
  • SQL composition

Per-Driver Builders (e.g., MySQL, PostgreSQL)

Override only what is truly different:

  • syntax differences
  • identifier quoting
  • RETURNING clauses
  • last inserted ID behavior
  • Postgres vs MySQL parameterization nuances

This dramatically simplifies maintenance and allows the ORM to support new databases easily.

5. ActiveQuery

ActiveQuery is the central point of the ORM:

  • receives the QueryBuilder
  • executes the SQL through QueryExecutor
  • hydrates results with ModelHydrator
  • returns model instances, collections, or primitives

Its API mirrors the simplicity of modern ORMs but keeps everything explicit and controlled.

6. Hydration Layer

ModelHydrator takes care of:

  • instantiating model objects
  • assigning attributes safely
  • handling single results vs collections

In the future, this layer will also handle:

  • typed model properties
  • value objects
  • domain transformations

Conclusion

This architecture brings my ORM very close to the clarity of Doctrine’s design while preserving the approachability of Active Record.

It is driver-aware, predictable, cleanly separated, and ready for future features like typed fields and full domain-driven modeling.

This work represents an important evolution toward a modern, stable, framework-quality ORM written entirely in PHP.

GitHub repo GitHub

Top comments (0)