kobykorman / eloquentify
Easily transform complex custom query results into fully hydrated hierarchical Eloquent models.
Requires
- php: ^8.2
- laravel/framework: ^11.0|^12.0|^13.0
Requires (Dev)
- orchestra/testbench: ^9.0|^10.0|^11.0
- pestphp/pest: ^3.8|^4.0
- pestphp/pest-plugin-laravel: ^3.2|^4.0
README
Eloquentify for Laravel
👎 Lazy Loading (N+1 queries)
😑 Eager Loading (R+1 queries)
😎 Greedy Loading (1 query)
Why Eloquentify?
âš¡ Single Query: N+1/R+1 round trips to the DB become one
💯 Eloquent API: custom SQL in, fully hydrated Eloquent models out
✨ Zero Config: your existing relation methods are all the wiring
Using Eloquent can be costly in terms of how many queries are fired behind the scenes when a model has many relationships. What if we could leverage the database for what it was meant for while retaining the Eloquent experience?
Eloquentify easily transforms the result of a single custom query into nested Eloquent models, so you can continue enjoying the Eloquent API and all of its benefits. And since every one of those replaced queries was a sequential round trip to your database, collapsing them into one is a difference you can see.
Installation
composer require kobykorman/eloquentify
Quick Start
1. Add the trait:
use Illuminate\Database\Eloquent\Model as BaseModel; use KobyKorman\Eloquentify\EloquentifiesQueries; class Model extends BaseModel { use EloquentifiesQueries; }
2. Write one query, aggregating each relation into a JSON column named after its relation method:
// PostgreSQL $result = DB::select(" SELECT users.id, users.name, users.email, (SELECT json_build_object('id', profiles.id, 'bio', profiles.bio) FROM profiles WHERE profiles.user_id = users.id) AS profile, (SELECT json_agg(json_build_object( 'id', roles.id, 'name', roles.name, 'permissions', (SELECT json_agg(json_build_object('id', permissions.id, 'name', permissions.name)) FROM permissions JOIN permission_role ON permission_role.permission_id = permissions.id WHERE permission_role.role_id = roles.id) )) FROM roles JOIN role_user ON role_user.role_id = roles.id WHERE role_user.user_id = users.id) AS roles FROM users WHERE users.id = ? ", [$id]);
3. Hydrate:
$user = User::eloquentify($result)->first();
4. Enjoy real Eloquent models:
$user->profile->bio; // hydrated HasOne $user->roles->first()->permissions; // nested collections, any depth $user->roles->first()->name = 'Admin'; $user->roles->first()->save(); // they are real models
How it works
For each column in your result, Eloquentify checks whether the model has a relation method with the same name (it must declare a relation return type, e.g. : HasMany). If it does, the column is decoded as JSON and hydrated recursively: a JSON object for to-one relations, a JSON array for to-many. Every other column becomes a plain attribute. That's the whole contract: column alias = relation method name.
Mistakes fail fast: invalid JSON, shape mismatches (an array where a to-one belongs), and ambiguous columns throw clear exceptions instead of guessing. Empty relations behave as you'd expect: NULL hydrates a to-one relation as null and a to-many as an empty collection, including the [null] arrays that aggregating over a left join produces.
Pivot data (BelongsToMany)
Include a pivot key (or your custom ->as() accessor name) inside each related object and it hydrates into the real pivot model, custom pivot classes included:
json_build_object('id', tags.id, 'name', tags.name, 'pivot', json_build_object('created_at', post_tag.created_at))
$post->tags->first()->pivot->created_at; // works exactly like native eager loading
JSON functions per database
| to-many (JSON array) | to-one (JSON object) | |
|---|---|---|
| PostgreSQL 9.4+ | json_agg(...) |
json_build_object(...) |
| MySQL 5.7.22+ | JSON_ARRAYAGG(...) |
JSON_OBJECT(...) |
| SQLite 3.38+ (or 3.9+ compiled with JSON1) | json_group_array(...) |
json_object(...) |
Requirements
- PHP 8.2+, Laravel 11+
- A database with JSON aggregation: PostgreSQL 9.4+, MySQL 5.7.22+, or SQLite 3.38+ (see table above)
- Relation methods must declare relation return types (e.g.
: HasMany), standard modern Laravel style - One result row per root model, which is what JSON aggregation naturally produces
Limitations
- Relation method names are reserved: a scalar column aliased like a relation (
AS posts) throws, so alias it differently (AS posts_count) morphTorelations can't be hydrated (the related class varies per row)- Relation classes from third-party packages (e.g. belongs-to-through) are assumed to-many unless they extend Eloquent's to-one relations; a mismatch throws rather than misclassifies silently
- Binary columns can't ride JSON: encode them (e.g. base64) and cast back
- For high-precision decimals, emit text (
amount::text) to avoid float precision loss in JSON - Database JSON encoders reformat temporal values (PostgreSQL: ISO 8601
2026-06-12T10:00:00; MySQL: microseconds appended). Date casts parse these identically to native loading; only an uncast date column read as a raw string sees the format difference
New in v2 (for v1 users)
v2 replaced v1's column prefixes with JSON aggregation (json_agg & friends). In one move:
- No row multiplication: sibling "many" relations of any size are fine
- No prefixes: no collisions, no aliasing rules, no naming conventions
- Use any method name for the relation: alias the column after the method, including two relations to the same model and self-references
- Pivot data hydrates into real
->pivotmodels - Silent failures became exceptions
License
This library is open-sourced software licensed under the MIT license.
