Eloquent Power Joins

repository·master·Indexed 23 days ago

https://github.com/kirschbaum-development/eloquent-power-joins

A Laravel package that enables complex database joins using Eloquent relationship definitions, model scopes, and aggregations. It provides tools for relationship-based joins (including polymorphic and nested), existence querying via powerJoin alternatives to whereHas, and advanced sorting using joined columns or aggregates. The library supports Soft Deletes, Global Scopes, and table aliasing to make queries more readable and efficient.

Tokens
2.9K
Snippets
9
Records
16
Agent score
82%

What's inside Eloquent Power Joins

  1. Overview of Eloquent Power Joins features

    master

    Eloquent Power Joins enhances Laravel's Eloquent ORM by providing a more readable and expressive way to handle database joins. It addresses several limitations in standard Eloquent by providing:

    • Relationship-based joins: Use existing Eloquent relationship definitions to perform joins.
    • Scope support: Use model scopes within different join contexts.
    • Existence querying: Query the existence of relationships using joins instead of whereExists subqueries.
    • Advanced sorting: Easily sort results based on columns or aggregations from related tables.
  2. Use model scopes inside join callbacks

    master

    You can invoke model scopes defined on the joined model directly within the join callback. This allows you to reuse existing business logic for filtering joined data.

    Important Constraints:

    1. Inside the scope, you cannot type-hint the $query parameter in the first argument of the scope method.
    2. You are limited to conditions supported by SQL joins.
    // Assuming Post has a scopePublished()
    User::joinRelationship('posts', function ($join) {
        $join->published();
    });
    User::joinRelationship('posts', function ($join) {
        $join->published();
    });
  3. Apply conditions and callbacks to joins

    master

    You can customize joins by passing a callback as the second argument to joinRelationship().

    Basic Conditions

    Use the $join instance to apply standard join conditions like where() or to change the join type:

    User::joinRelationship('posts', fn ($join) => $join->where('posts.approved', true));
    
    // Change join type inside callback
    User::joinRelationship('posts', fn ($join) => $join->left());

    Nested Callbacks

    For nested relationships, pass an array where keys are the relationship names and values are the callbacks:

    User::joinRelationship('posts.comments', [
        'posts' => fn ($join) => $join->where('posts.published', true),
        'comments' => fn ($join) => $join->where('comments.approved', true),
    ]);

    Belongs To Many

    For belongsToMany relationships, pass an array containing the relationship key and a nested array for the tables involved (the relationship table and the pivot table):

    User::joinRelationship('groups', [
        'groups' => [
            'groups' => function ($join) {
                // ...
            },
            'group_members' => fn ($join) => $join->where('group_members.active', true),
        ]
    ]);
    User::joinRelationship('posts', fn ($join) => $join->where('posts.approved', true));
    
    User::joinRelationship('posts.comments', [
        'posts' => fn ($join) => $join->where('posts.published', true),
        'comments' => fn ($join) => $join->where('comments.approved', true),
    ]);
  4. Install Eloquent Power Joins via Composer

    master

    Install the package using Composer. For modern Laravel versions, use the standard requirement command. If you are using an older version of Laravel, specify the compatible major version.

    • Laravel 10, 11, 12, or 13: Use the latest version.
    • Laravel < 10: Use version 3.*.
    • Laravel < 8: Use version 2.*.
    composer require kirschbaum-development/eloquent-power-joins
  5. Handle Soft Deletes and Global Scopes in joins

    master

    Eloquent Power Joins respects Laravel's model features like Soft Deletes and Global Scopes.

    Soft Deletes

    By default, if a joined model uses SoftDeletes, the package automatically adds deleted_at IS NULL to the join. To include trashed records, use withTrashed() or onlyTrashed() in the callback:

    UserProfile::joinRelationship('users', fn ($join) => $join->withTrashed());
    UserProfile::joinRelationship('users', fn ($join) => $join->onlyTrashed());

    Global Scopes

    To enable global scopes on a joined model, call withGlobalScopes() in the callback:

    UserProfile::joinRelationship('users', fn ($join) => $join->withGlobalScopes());

    Constraint: Your global scope cannot type-hint the Eloquent\Builder class in its apply method, or it will cause errors during joins.

    Relationship Scopes

    If your relationship definition in the Model includes a scope, it is automatically applied to the join:

    // In User model
    public function publishedPosts() {
        return $this->hasMany(Post::class)->published();
    }
    
    // Usage
    User::joinRelationship('publishedPosts'); // Automatically applies 'published' scope
    UserProfile::joinRelationship('users', fn ($join) => $join->withTrashed());
    
    UserProfile::joinRelationship('users', fn ($join) => $join->withGlobalScopes());
  6. Join polymorphic relationships

    master

    Eloquent Power Joins automatically handles polymorphic relationships by applying the necessary type constraints (e.g., imageable_type = Post::class).

    MorphMany / MorphToMany

    Simply call joinRelationship() as usual:

    Post::joinRelationship('images');

    MorphTo

    When joining a MorphTo relationship, you must specify the morphable type:

    Image::joinRelationship('imageable', morphable: Post::class);

    Note: Querying morph to relationships only supports one morphable type at a time.

    Post::joinRelationship('images');
    
    Image::joinRelationship('imageable', morphable: Post::class);
  7. Order results by joined columns

    master

    You can sort query results using columns from joined tables using the orderByPowerJoins family of methods.

    Basic Ordering

    User::orderByPowerJoins('profile.city');
    
    // Using raw values
    User::orderByPowerJoins(['profile', DB::raw('concat(city, ", ", state)')]);

    Aggregate Ordering

    Sort by counts, sums, averages, etc.:

    • orderByPowerJoinsCount('relation.column', 'direction')
    • orderByPowerJoinsSum('relation.column', 'direction')
    • orderByPowerJoinsAvg('relation.column', 'direction')
    • orderByPowerJoinsMin('relation.column', 'direction')
    • orderByPowerJoinsMax('relation.column', 'direction')

    Example: Sort users by highest number of posts:

    $users = User::orderByPowerJoinsCount('posts.id', 'desc')->get();

    Left Joins for Ordering

    If you want to include records that have no related entries in the sort (using a LEFT JOIN), use the orderByLeft... prefix:

    Post::orderByLeftPowerJoinsCount('comments.votes');
    User::orderByPowerJoins('profile.city');
    
    $users = User::orderByPowerJoinsCount('posts.id', 'desc')->get();
    
    $posts = Post::orderByPowerJoinsAvg('comments.votes', 'desc')->get();
    
    Post::orderByLeftPowerJoinsCount('comments.votes');
  8. Use table aliases in joins

    master

    When joining the same table multiple times, you may need to use aliases. Use joinRelationshipUsingAlias() or the as() method within a callback.

    Simple Aliasing

    For non-nested joins, pass the alias as the second parameter:

    Post::joinRelationshipUsingAlias('category', 'category_alias')->get();

    Nested Aliasing

    For nested joins, use the as() method within the callback array:

    Post::joinRelationship('category.parent', [
        'category' => fn ($join) => $join->as('category_alias'),
        'parent' => fn ($join) => $join->as('category_parent'),
    ])->get();

    Belongs To Many Aliasing

    For many-to-many relationships, provide an array for the tables involved:

    Group::joinRelationship('posts.user', [
        'posts' => [
            'posts' => fn ($join) => $join->as('posts_alias'),
            'post_groups' => fn ($join) => $join->as('post_groups_alias'),
        ],
    ])->toSql();
    Post::joinRelationshipUsingAlias('category', 'category_alias')->get();
    
    Post::joinRelationship('category.parent', [
        'category' => fn ($join) => $join->as('category_alias'),
        'parent' => fn ($join) => $join->as('category_parent'),
    ])->get();
  9. Query relationship existence using joins

    master

    Standard Laravel has() and whereHas() methods use WHERE EXISTS subqueries. This package provides powerJoin alternatives that use joins instead, which can be more performant depending on your data structure.

    Method Mapping

    Laravel NativePower Join Equivalent
    has('posts')powerJoinHas('posts')
    has('posts.comments')powerJoinHas('posts.comments')
    has('posts', '>', 3)powerJoinHas('posts', '>', 3)
    whereHas('posts', callback)powerJoinWhereHas('posts', callback)
    doesntHave('posts')powerJoinDoesntHave('posts')

    Using Callbacks with Many-to-Many/One-to-Many

    When using powerJoinWhereHas on relationships involving multiple tables, use the array syntax to pass callbacks for the related tables:

    User::powerJoinWhereHas('commentsThroughPosts', [
        'comments' => fn ($query) => $query->where('body', 'a')
    ])->get();
    User::powerJoinHas('posts');
    User::powerJoinHas('posts.comments', '>', 3);
    User::powerJoinWhereHas('posts', function ($join) {
        $join->where('posts.published', true);
    });
    User::powerJoinDoesntHave('posts');
  10. Join relationships using joinRelationship()

    master

    Instead of manually writing SQL joins, use joinRelationship() to join tables based on existing Eloquent relationships. This hides implementation details and automatically handles nested relationships using dot notation.

    Join Types

    You can specify the join type using specific methods:

    • joinRelationship(): Performs an inner join.
    • leftJoinRelationship(): Performs a left join.
    • rightJoinRelationship(): Performs a right join.

    Nested Relationships

    Use dot notation to join through multiple levels of relationships:

    User::joinRelationship('posts.comments');
    User::joinRelationship('posts');
    
    // Nested
    User::joinRelationship('posts.comments');
    
    // Left/Right
    User::leftJoinRelationship('posts.comments');
    User::rightJoinRelationship('posts.comments');