I recently completed one of the most challenging milestones of my framework: the ORM and QueryBuilder system.
It’s a complex component that required deep understanding of OOP, SQL abstraction, and design principles.
The Goal
I wanted to understand what truly happens behind the scenes when writing fluent database queries.
Instead of using Laravel or Symfony, I decided to build my own ORM from scratch — focusing on architecture, not shortcuts.
Architecture Overview
The Soft ORM system is inspired by modern framework technologies,
but built entirely from scratch.
Every Model represents a table, and the QueryBuilder dynamically generates and executes SQL statements.
Execution Flow
-
User::find(1)→ Extends the base Model -
Model.php→ Creates the instance and forwards static calls to the QueryBuilder -
QueryBuilder.php→ Builds the SQL query (SELECT * FROM ...) -
Database.php→ Singleton PDO that executes the query -
ResultSet→ Handles model hydration
Design Patterns
| Pattern | Purpose |
|---|---|
| Active Record | Each Model manages its table and CRUD operations. |
| Builder | The QueryBuilder constructs SQL queries fluently. |
| Dependency Injection | PDO is injected via container for separation of concerns. |
| Chain of Responsibility | Methods like where() and join() return self for chaining. |
| Fail Fast Design | Invalid Model structures throw ModelStructureException. |
Writing an ORM from scratch forces a focus on architecture, abstraction, and class responsibility.
It reveals the value of separation between logic, data, and infrastructure layers.
The first unit tests for the QueryBuilder validate SQL generation and parameter binding.
They ensure the ORM behaves consistently before adding advanced features such as relationships and eager loading.
Try It Out
Repository:
GitHub – dev-iadicola/soft-php-mvc
bash
git clone https://github.com/dev-iadicola/soft-php-mvc.git
cd soft-php-mvc
composer install
php soft serve

Top comments (2)
That is a big effort.
I'm sad to see you used Eloquent
$fillableproperty over Doctrine's property per field solution. I think the doctrine way provides a more solid solution when using the models in the application. A typo happens faster when using['emial' => 'me@test'], where with$user->emial = 'me@test'your editor will show you there is a typo.The problem I have with query builders is that they never provide all the SQL features. That is why I moved away from query builder and ORM solutions.
Thanks a lot for the feedback — and I completely agree with you.
In fact, the ORM has already gone through a major refactoring for exactly the reasons you mentioned.
My next step is to remove $fillable entirely and move toward fully typed models, introducing Value Objects instead of primitives or string-based casting systems.
You basically guessed the next reconstruction phase of the project:
strong typing on every property, real domain objects, and zero magic.
I also share your point about query builders.
Most of them expose only a subset of SQL, which is why my new architecture pushes everything through ActiveQuery and keeps the builder extremely transparent, predictable, and close to real SQL.
It’s great to see PHP developers who think this way — and who look beyond the “classic” solutions we’ve all used for years.
Your comment is exactly the direction I want this project to take.