Skip to content

About

What if we could leverage the database for what it was meant for while retaining the Eloquent experience?

Resources

Stars

20 stars

Watchers

1 watching

Forks

Latest commit

 

History

15 Commits

Folders and files

Repository files navigation

Eloquentify Logo

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)
  • morphTo relations 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 ->pivot models
  • Silent failures became exceptions

License

This library is open-sourced software licensed under the MIT license.

About

What if we could leverage the database for what it was meant for while retaining the Eloquent experience?

Resources

Stars

20 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages