Eloquent HasManyDeep

repository·main·Indexed 25 days ago

https://github.com/staudenmeir/eloquent-has-many-deep

An extension of Laravel's HasManyThrough relationship that allows for unlimited intermediate models. It supports many-to-many and polymorphic relationships, composite keys, and the ability to concatenate existing relationships. Compatible with Laravel 5.5 and above, it provides HasManyDeep and HasOneDeep classes to retrieve collections or single instances across deep relationship paths.

Tokens
3.8K
Snippets
12
Records
20
Agent score
82%

What's inside eloquent-has-many-deep

  1. Retrieve Intermediate and Pivot Data

    main

    Use withIntermediate() to retrieve attributes from intermediate models or tables.

    • Basic usage: ->withIntermediate(Model::class) retrieves all columns.
    • Specific columns: ->withIntermediate(Model::class, ['col1', 'col2']).
    • Nested accessors: You can nest accessors to reach multiple levels, e.g., ->withIntermediate(Post::class)->withIntermediate(User::class, ['*'], 'post.user').

    For BelongsToMany or MorphToMany relationships, use withPivot() to access pivot table data.

    Pivot usage: ->withPivot('pivot_table', ['column']).

    // Accessing intermediate model data
    public function comments(): \Staudenmeir\EloquentHasManyDeep\HasManyDeep
    {
        return $this->hasManyDeep(Comment::class, [User::class, Post::class])
            ->withIntermediate(Post::class, ['id', 'title'], 'accessor');
    }
    
    // Accessing pivot data
    public function permissions(): \Staudenmeir\EloquentHasManyDeep\HasManyDeep
    {
        return $this->hasManyDeep(Permission::class, ['role_user', Role::class])
            ->withPivot('role_user', ['expires_at']);
    }
  2. Get Unique Results from deep relationships

    main

    Deep relationships involving many-to-many segments may return duplicate models.

    • Collection level: Use ->get()->unique() to remove duplicates from the resulting collection.
    • Query level (for pagination): Use ->distinct() on the relationship. If distinct() fails, use groupBy() on the related table's primary key.
    // Query level grouping for pagination
    $results = Country::find($id)->comments()
        ->getQuery()
        ->select('comments.*')
        ->groupBy('comments.id')
        ->get();
  3. Install Eloquent HasManyDeep via Composer

    main

    Install the package using Composer. For standard environments, use the ^1.7 version constraint. If you are using PowerShell on Windows (e.g., within VS Code), use the ^^^^1.7 constraint to avoid potential shell parsing issues.

    composer require staudenmeir/eloquent-has-many-deep:"^1.7"
  4. Include Soft Deleted models in deep relationships

    main

    By default, soft-deleted intermediate models are excluded from results. To include them, use the withTrashed() method and specify the column used for soft deletes (e.g., users.deleted_at).

    public function comments(): \Staudenmeir\EloquentHasManyDeep\HasManyDeep
    {
        return $this->hasManyDeep(Comment::class, [User::class, Post::class])
            ->withTrashed('users.deleted_at');
    }
  5. Define MorphMany, MorphToMany, and MorphedByMany deep relationships

    main

    Polymorphic relationships can be included in the intermediate path by specifying the polymorphic foreign keys as an array, starting with the *_type column.

    • MorphMany: Use [null, ['*_type', '*_id']] for the keys.
    • MorphToMany: Add the pivot table to the intermediate models and specify polymorphic keys.
    • MorphedByMany: Add the pivot table and specify the polymorphic local keys (starting with *_type).
    • BelongsTo: Include the intermediate path and swap the foreign/local keys.
    // MorphMany example
    public function postComments(): \Staudenmeir\EloquentHasManyDeep\HasManyDeep
    {
        return $this->hasManyDeep(
            Comment::class,
            [Post::class],
            [null, ['commentable_type', 'commentable_id']]
        );
    }
  6. Define ManyToMany deep relationships

    main

    To include ManyToMany relationships in a deep path, add the pivot tables to the intermediate models array.

    ManyToMany → HasMany: Add the pivot table name to the array. ManyToMany → ManyToMany: Add both pivot tables to the array.

    Important: When specifying custom keys for pivot tables, you must swap the foreign and local keys on the "right" side of the pivot table.

    // ManyToMany -> HasMany
    public function permissions(): \Staudenmeir\EloquentHasManyDeep\HasManyDeep
    {
        return $this->hasManyDeep(Permission::class, ['role_user', Role::class]);
    }
    
    // ManyToMany -> ManyToMany
    public function permissions(): \Staudenmeir\EloquentHasManyDeep\HasManyDeep
    {
        return $this->hasManyDeep(Permission::class, ['role_user', Role::class, 'permission_role']);
    }
  7. Configure IDE Helper support

    main

    The package supports barryvdh/laravel-ide-helper to provide correct type hints for deep relations. This is enabled by default via Package Discovery.

    To disable the IDE Helper hook:

    1. Via .env: Set ELOQUENT_HAS_MANY_DEEP_IDE_HELPER_ENABLED=false.
    2. Via config: Publish the config (php artisan vendor:publish --tag=eloquent-has-many-deep) and set 'ide_helper_enabled' => false in config/eloquent-has-many-deep.php.
    3. Via composer.json: Add the package to dont-discover in the extra.laravel section.
  8. Define deep relationships with Composite Keys

    main

    If multiple columns are required to match between tables in the path, use the Staudenmeir\EloquentHasManyDeep\Eloquent\CompositeKey class within the keys arguments of hasManyDeep().

    use Staudenmeir\EloquentHasManyDeep\Eloquent\CompositeKey;
    
    public function projects(): \Staudenmeir\EloquentHasManyDeep\HasManyDeep
    {
        return $this->hasManyDeep(
            Project::class,
            [Task::class],
            [new CompositeKey('team_id', 'category_id'), 'id'],
            [new CompositeKey('team_id', 'category_id'), 'project_id']
        );
    }
  9. Handle Table Aliases in deep relationships

    main

    If a model appears multiple times in a relationship path, you must use table aliases to avoid ambiguity.

    1. Use the HasTableAlias trait in the models being aliased.
    2. Specify the alias in the intermediate models array using the Model::class . ' as alias' syntax.
    3. For concatenated relationships, use setAlias('alias') on the relationship instance.
    // Using alias in manual definition
    public function commentReplies(): \Staudenmeir\EloquentHasManyDeep\HasManyDeep
    {
        return $this->hasManyDeep(Comment::class, [Comment::class . ' as alias'], [null, 'parent_id']);
    }
    
    // Using alias in concatenated relationships
    public function commentReplies(): \Staudenmeir\EloquentHasManyDeep\HasManyDeep
    {
        return $this->hasManyDeepFromRelations(
            $this->comments(),
            (new Comment())->setAlias('alias')->replies()
        );
    }
  10. Concatenate existing relationships using hasManyDeepFromRelations

    main

    You can define a deep relationship by chaining existing Eloquent relationships. Use hasManyDeepFromRelations() to pass the first relationship and subsequent relationships as arguments. To retrieve a single instance instead of a collection, use hasOneDeepFromRelations().

    Note on Constraints: By default, constraints from the concatenated relationships are not transferred. To include them, use hasManyDeepFromRelationsWithConstraints(), passing the relationships as callable arrays. Always qualify column names (e.g., table.column) if they exist in multiple tables.

    class Country extends Model
    {
        use \Staudenmeir\EloquentHasManyDeep\HasRelationships;
    
        public function comments(): \Staudenmeir\EloquentHasManyDeep\HasManyDeep
        {
            return $this->hasManyDeepFromRelations($this->posts(), (new Post())->comments());
        }
    
        public function posts()
        {
            return $this->hasManyThrough(Post::class, User::class);
        }
    }
    
    class Post extends Model
    {
        public function comments()
        {
            return $this->hasMany(Comment::class);
        }
    }
  11. Reverse a deep relationship

    main

    You can define a relationship in the opposite direction of an existing deep relationship using hasManyDeepFromReverse() or hasOneDeepFromReverse(). Pass the original relationship as the argument.

    class Comment extends Model
    {
        public function country(): \Staudenmeir\EloquentHasManyDeep\HasOneDeep
        {
            return $this->hasOneDeepFromReverse(
                (new Country())->comments()
            );
        }
    }