DEV Community

Dev-Iadicola
Dev-Iadicola

Posted on

Build a PHP QueryBuilder from scratch!

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

  1. User::find(1) → Extends the base Model
  2. Model.php → Creates the instance and forwards static calls to the QueryBuilder
  3. QueryBuilder.php → Builds the SQL query (SELECT * FROM ...)
  4. Database.php → Singleton PDO that executes the query
  5. 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
Enter fullscreen mode Exit fullscreen mode

Top comments (2)

Collapse
 
xwero profile image
david duymelinck

That is a big effort.

I'm sad to see you used Eloquent $fillable property 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.

Collapse
 
dev_iadicola profile image
Dev-Iadicola

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.