I posted before that I started a query languages library. I knew it would have some iterations, but I wanted to take others along in the process.
The pivotal moment for me was adding MongoDB to the library. I made quite a few changes, and it became clear I needed to split up the library.
So that is why I will not commit any more on the current repository.
In this post I want to elaborate on the changes that are coming and the lessons I learned.
The name
I choose composable queries because it was a mix of the diversity of database systems I want the library to accommodate.
And the way you can use the library by exposing a lot of the inner workings through the use of functions instead of objects.
I think identifiable queries is a more suitable name going forward. The core of library revolves around the IdentifierInterface.
The goal from the start was to have a separation between the names of the data in the application and in the database. With ORM's that is the task of objects. But I didn't want the baggage a class brings with it, so I landed on enumerations.
While the name is not the most important part, I want to get it right.
The package separation
I mentioned it already in an update in my previous post. The reason is simple, functions are loaded up front.
If I made the library with objects it would have taken me longer realize I needed to split up the library. Because I would have known the objects are only loaded when they are used, so this also means that the dependencies that the library requires will only be used when needed.
But I realized this is not good if I want to create a more utilitarian library.
So I'm going to create a core library package and a package per database system. The database system package will have a core package dependency.
The biggest advantage I see in this structure is that database packages can be as close to the actual database systems as possible. This is one of the main problems of ORM libraries, they only provide wrappers for the common functionality of the database systems they target.
A common language
While the functionality of database systems varies, I still want to provide a level of comfort by reusing function names for different database systems.
The best example for now is the PDO getStatement, Redis getStatement and the MongoDB getStatement functions. While the functionality is a little different between the different databases, the main function stays the same.
Not throwing exceptions
With the release of PHP 8.5 and the pipe operator feature so close. I wanted to provide a way to use the feature the best way possible.
This means returning exceptions, the main problem with that is you don't want a match in every function to be prepared for every exception a previous function in the chain can output.
This is reason I created the Error object.
As soon as a function returns an Error instance, the other functions in the chain short circuit and pass that instance to the end of the chain.
When you look at the code you will see quite a few functions with similar code.
if ($statement instanceof Error) {
return $statement;
}
The bonus of not throwing exceptions is that the return type of the function is complete. If you throw exceptions you need DocBlock comments to make third party tools aware of the behavior of the function.
Next steps
When I separated the packages I'm going to add them to Packagist and provide documentation for each package.
In the beginning it could get a bit messy because I'm still thinking about where the best place for certain functions is.
I never felt "building on the shoulders of giants" more as with this library. There are so much great things I learned along the way, that are the foundations of the ideas behind the library.
Top comments (2)
Interesting concept, but the article needs significant improvements in clarity and justification. Here are my main concerns:
🔴 Critical Issues
What problem are you solving that existing solutions don't?
Why not use Doctrine DBAL, Laravel Query Builder, or other established libraries?
What's the actual pain point with current ORMs that justifies a new approach?
The article reads like a changelog, not a case study. Show me the problem first, then the solution.
IdentifierInterface
Error object
Functions vs objects approach
Enum-based identifiers
But show none of it. How can readers evaluate your library without seeing actual code? Compare this to the Elasticsearch article - it shows real, runnable examples throughout.
Needed:
php// Before (with ORM)
$user = User::where('email', $email)->first();
// After (with your library)
$user = ??? // Show me!
"I didn't want the baggage a class brings with it, so I landed on enumerations."
What "baggage"? This is vague. Are you talking about:
Memory overhead? (Negligible in modern PHP)
Complexity? (Enums add their own complexity)
Performance? (Need benchmarks)
"This is one of the main problems of ORM libraries, they only provide wrappers for the common functionality"
This is actually a feature, not a bug. Abstraction enables:
Database portability
Testability
Team consistency
If I need database-specific features, I can always drop to raw queries. What's your library's advantage here?
🟡 Structural Problems
"With the release of PHP 8.5 and the pipe operator feature so close..."
PHP 8.5 isn't released yet, and the pipe operator is still RFC stage. Building a library around an unreleased feature is premature.
Also, you can chain methods without pipes:
php// Method chaining works fine
$query->where('status', 'active')
->orderBy('created_at')
->limit(10);
PHP 8 union types: function(): User|Error
Exceptions (the PHP standard)
Null object pattern
Problems with your approach:
Every function needs the error check boilerplate
Diverges from PHP conventions (exceptions are idiomatic)
Third-party integrations expect exceptions
Debugging is harder (stack traces vs passing Error objects)
This posted isn't meant as a technical post. It is a follow up post.
It feels like I triggered some anger with my comment on your post. That was not my intention.
Models come with a lot of functionality to create an abstraction on top of a query language.
The only thing I wanted is an abstraction to have a centralized way to identify table/set/label/key/fields over multiple database types.
According to the PHP RFC page the pipe operator is implemented.
I think method chaining is overused and it can create impure functions. I think composition over inheritance is a good rule of thumb to follow if it makes sense.
I see it more as the Go way of creating functions. It is a divisive way of handling errors, but for me the clarity wins over the negative consequences.
The problem I have with throwing exceptions is that you can't get it out of the function definition. You need to read the body to find out.
You can throw the exception in your application after running a function, I leave that up to your preferences.
That is one of the things that did bother me for a while. But now I am sure the positives outweigh the negatives.