staudenmeir/eloquent-has-many-deep

Laravel Eloquent HasManyThrough 关系,支持无限层级


README

CI Code Coverage Latest Stable Version Total Downloads License

这个 HasManyThrough 扩展版本允许与无限中间模型建立关系。
它支持 多对多多态 关系及其所有可能的组合。它还支持一些 第三方包

支持 Laravel 5.5+。

安装

composer require staudenmeir/eloquent-has-many-deep:"^1.7"

如果您在 Windows 的 PowerShell 中(例如在 VS Code 中),请使用此命令

composer require staudenmeir/eloquent-has-many-deep:"^^^^1.7"

版本

用法

该包提供两种定义深度关系的方式
您可以连接 现有关系 或手动指定中间模型、外键和本地键 手动

连接现有关系

考虑 Laravel 文档中的这个 示例,增加一个层级
Country → has many → User → has many → Post → has many → Comment

您可以通过连接现有关系来定义 HasManyDeep 关系

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);
    }
}

如果您只想检索单个相关实例,请使用 hasOneDeepFromRelations() 定义 HasOneDeep 关系

约束

默认情况下,连接关系的约束不会转移到新的深度关系上。使用 hasManyDeepFromRelationsWithConstraints() 并将关系作为可调用的数组应用这些约束

class Country extends Model
{
    use \Staudenmeir\EloquentHasManyDeep\HasRelationships;

    public function comments(): \Staudenmeir\EloquentHasManyDeep\HasManyDeep
    {
        return $this->hasManyDeepFromRelationsWithConstraints([$this, 'posts'], [new Post(), 'comments']);
    }

    public function posts()
    {
        return $this->hasManyThrough(Post::class, User::class)->where('posts.published', true);
    }
}

class Post extends Model
{
    public function comments()
    {
        return $this->hasMany(Comment::class)->withTrashed();
    }
}

如果约束的列名在多个表中出现,请确保限定这些列名
->where('posts.published', true) 而不是 ->where('published', true)

第三方包

除了本机 Laravel 关系外,您还可以连接来自这些第三方包的关系

手动定义关系

如果您没有所有必要的现有关系来连接它们,您也可以通过指定中间模型、外键和本地键来手动定义深层关系。

HasMany

考虑 Laravel 文档中的这个 示例,增加一个层级
Country → has many → User → has many → Post → has many → Comment

class Country extends Model
{
    use \Staudenmeir\EloquentHasManyDeep\HasRelationships;

    public function comments(): \Staudenmeir\EloquentHasManyDeep\HasManyDeep
    {
        return $this->hasManyDeep(Comment::class, [User::class, Post::class]);
    }
}

就像 hasManyThrough() 一样,hasManyDeep() 的第一个参数是相关模型。第二个参数是一个中间模型数组,从远端父模型(定义关系的模型)到相关模型。

默认情况下,hasManyDeep() 使用 Eloquent 的外键和本地键约定。您也可以将自定义外键作为第三个参数,将自定义本地键作为第四个参数指定

class Country extends Model
{
    use \Staudenmeir\EloquentHasManyDeep\HasRelationships;

    public function comments(): \Staudenmeir\EloquentHasManyDeep\HasManyDeep
    {
        return $this->hasManyDeep(
            Comment::class,
            [User::class, Post::class], // Intermediate models, beginning at the far parent (Country).
            [
               'country_id', // Foreign key on the "users" table.
               'user_id',    // Foreign key on the "posts" table.
               'post_id'     // Foreign key on the "comments" table.
            ],
            [
              'id', // Local key on the "countries" table.
              'id', // Local key on the "users" table.
              'id'  // Local key on the "posts" table.
            ]
        );
    }
}

您可以使用 null 代替默认键

class Country extends Model
{
    use \Staudenmeir\EloquentHasManyDeep\HasRelationships;

    public function comments(): \Staudenmeir\EloquentHasManyDeep\HasManyDeep
    {
        return $this->hasManyDeep(Comment::class, [User::class, Post::class], [null, 'custom_user_id']);
    }
}

ManyToMany

您可以在中间路径中包含 ManyToMany 关系。

ManyToMany → HasMany

考虑来自 Laravel 文档的此 示例,并增加一个 HasMany 级别
User → 多对多 → Role → 有许多 → Permission

将连接表添加到中间模型

class User extends Model
{
    use \Staudenmeir\EloquentHasManyDeep\HasRelationships;

    public function permissions(): \Staudenmeir\EloquentHasManyDeep\HasManyDeep
    {
        return $this->hasManyDeep(Permission::class, ['role_user', Role::class]);
    }
}

如果您指定了自定义键,请记住在连接表的“右侧”交换外键和本地键

class User extends Model
{
    use \Staudenmeir\EloquentHasManyDeep\HasRelationships;

    public function permissions(): \Staudenmeir\EloquentHasManyDeep\HasManyDeep
    {
        return $this->hasManyDeep(
            Permission::class,
            ['role_user', Role::class], // Intermediate models and tables, beginning at the far parent (User).
            [           
               'user_id', // Foreign key on the "role_user" table.
               'id',      // Foreign key on the "roles" table (local key).
               'role_id'  // Foreign key on the "permissions" table.
            ],
            [          
              'id',      // Local key on the "users" table.
              'role_id', // Local key on the "role_user" table (foreign key).
              'id'       // Local key on the "roles" table.
            ]
        );
    }
}
ManyToMany → ManyToMany

考虑来自 Laravel 文档的此 示例,并增加一个 ManyToMany 级别
User → 多对多 → Role → 多对多 → Permission

将连接表添加到中间模型

class User extends Model
{
    use \Staudenmeir\EloquentHasManyDeep\HasRelationships;

    public function permissions(): \Staudenmeir\EloquentHasManyDeep\HasManyDeep
    {
        return $this->hasManyDeep(Permission::class, ['role_user', Role::class, 'permission_role']);
    }
}

MorphMany

您可以在中间路径中包含 MorphMany 关系。

考虑来自 Laravel 文档的此 示例,并增加一个级别
User → 有许多 → Post → 形状许多 → Comment

将多态外键指定为数组,从 *_type 列开始

class User extends Model
{
    use \Staudenmeir\EloquentHasManyDeep\HasRelationships;

    public function postComments(): \Staudenmeir\EloquentHasManyDeep\HasManyDeep
    {
        return $this->hasManyDeep(
            Comment::class,
            [Post::class],
            [null, ['commentable_type', 'commentable_id']]
        );
    }
}

MorphToMany

您可以在中间路径中包含 MorphToMany 关系。

考虑来自 Laravel 文档的此 示例,并增加一个级别
User → 有许多 → Post → 形状到多 → Tag

将连接表添加到中间模型,并将多态外键指定为数组,从 *_type 列开始

class User extends Model
{
    use \Staudenmeir\EloquentHasManyDeep\HasRelationships;

    public function postTags(): \Staudenmeir\EloquentHasManyDeep\HasManyDeep
    {
        return $this->hasManyDeep(
            Tag::class,
            [Post::class, 'taggables'],
            [null, ['taggable_type', 'taggable_id'], 'id'],
            [null, null, 'tag_id']
        );
    }
}

请记住在连接表的“右侧”交换外键和本地键

MorphedByMany

您可以在中间路径中包含 MorphedByMany 关系。

考虑来自 Laravel 文档的此 示例,并增加一个级别
Tag → 形状由多 → Post → 有许多 → Comment

将连接表添加到中间模型,并将多态本地键指定为数组,从 *_type 列开始

class Tag extends Model
{
    use \Staudenmeir\EloquentHasManyDeep\HasRelationships;

    public function postComments(): \Staudenmeir\EloquentHasManyDeep\HasManyDeep
    {
        return $this->hasManyDeep(
            Comment::class,
            ['taggables', Post::class],
            [null, 'id'],
            [null, ['taggable_type', 'taggable_id']]
        );
    }
}

BelongsTo

您可以在中间路径中包含 BelongsTo 关系。
Tag → 形状由多 → Post → 属于 → User

交换外键和本地键

class Tag extends Model
{
    use \Staudenmeir\EloquentHasManyDeep\HasRelationships;

    public function postAuthors(): \Staudenmeir\EloquentHasManyDeep\HasManyDeep
    {
        return $this->hasManyDeep(
            User::class,
            ['taggables', Post::class],
            [null, 'id', 'id'],
            [null, ['taggable_type', 'taggable_id'], 'user_id']
        );
    }
}

HasOneDeep

如果您只想检索单个相关实例,请定义 HasOneDeep 关系

class Country extends Model
{
    use \Staudenmeir\EloquentHasManyDeep\HasRelationships;

    public function latestComment(): \Staudenmeir\EloquentHasManyDeep\HasOneDeep
    {
        return $this->hasOneDeep(Comment::class, [User::class, Post::class])
            ->latest('comments.created_at');
    }
}

复合键

如果两个表之间需要匹配多个列,您可以使用 CompositeKey 类定义复合键。

考虑来自 compoships 文档的此 示例,并增加一个级别
User → 有许多(匹配 team_id & category_id)→ Task → 属于 → Project

use Staudenmeir\EloquentHasManyDeep\Eloquent\CompositeKey;

class User extends Model
{
    use \Staudenmeir\EloquentHasManyDeep\HasRelationships;

    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']
        );
    }
}

中间和枢纽数据

使用 withIntermediate() 检索中间表的属性

public function comments(): \Staudenmeir\EloquentHasManyDeep\HasManyDeep
{
    return $this->hasManyDeep(Comment::class, [User::class, Post::class])
        ->withIntermediate(Post::class);
}

foreach ($country->comments as $comment) {
    // $comment->post->title
}

默认情况下,这将检索表的所有列。请注意,这将执行一个单独的查询来获取列的列表。

您可以将选定的列作为第二个参数指定

public function comments(): \Staudenmeir\EloquentHasManyDeep\HasManyDeep
{
    return $this->hasManyDeep(Comment::class, [User::class, Post::class])
        ->withIntermediate(Post::class, ['id', 'title']);
}

作为第三个参数,您可以选择自定义访问器

public function comments(): \Staudenmeir\EloquentHasManyDeep\HasManyDeep
{
    return $this->hasManyDeep(Comment::class, [User::class, Post::class])
        ->withIntermediate(Post::class, ['id', 'title'], 'accessor');
}

foreach ($country->comments as $comment) {
    // $comment->accessor->title
}

如果您从多个表中检索数据,可以使用嵌套访问器

public function comments(): \Staudenmeir\EloquentHasManyDeep\HasManyDeep
{
    return $this->hasManyDeep(Comment::class, [User::class, Post::class])
        ->withIntermediate(Post::class)
        ->withIntermediate(User::class, ['*'], 'post.user');
}

foreach ($country->comments as $comment) {
    // $comment->post->title
    // $comment->post->user->name
}

使用 withPivot()BelongsToManyMorphToMany/MorphedByMany 关系中的数据透视表

public function permissions(): \Staudenmeir\EloquentHasManyDeep\HasManyDeep
{
    return $this->hasManyDeep(Permission::class, ['role_user', Role::class])
        ->withPivot('role_user', ['expires_at']);
}

foreach ($user->permissions as $permission) {
    // $permission->role_user->expires_at
}

您可以将自定义数据透视模型作为第三个参数,并将自定义访问器作为第四个参数

public function permissions(): \Staudenmeir\EloquentHasManyDeep\HasManyDeep
{
    return $this->hasManyDeep(Permission::class, ['role_user', Role::class])
        ->withPivot('role_user', ['expires_at'], RoleUser::class, 'pivot');
}

foreach ($user->permissions as $permission) {
    // $permission->pivot->expires_at
}

中间和枢纽约束

您可以对中间表和数据透视表应用约束

class Country extends Model
{
    use \Staudenmeir\EloquentHasManyDeep\HasRelationships;

    public function comments(): \Staudenmeir\EloquentHasManyDeep\HasManyDeep
    {
        return $this->hasManyDeep(Comment::class, [User::class, Post::class]);
    }
}

$commentsFromActiveUsers = $country->comments()->where('users.active', true)->get();

表别名

如果您的关联路径中包含相同的模型多次,您可以指定一个表别名

class Post extends Model
{
    use \Staudenmeir\EloquentHasManyDeep\HasRelationships;

    public function commentReplies(): \Staudenmeir\EloquentHasManyDeep\HasManyDeep
    {
        return $this->hasManyDeep(Comment::class, [Comment::class . ' as alias'], [null, 'parent_id']);
    }
}

在您要别名的模型中使用 HasTableAlias 特性

class Comment extends Model
{
    use \Staudenmeir\EloquentHasManyDeep\HasTableAlias;
}

对于数据透视表,这需要自定义模型

class User extends Model
{
    use \Staudenmeir\EloquentHasManyDeep\HasRelationships;

    public function permissions(): \Staudenmeir\EloquentHasManyDeep\HasManyDeep
    {
        return $this->hasManyDeep(Permission::class, [RoleUser::class . ' as alias', Role::class]);
    }
}

class RoleUser extends Pivot
{
    use \Staudenmeir\EloquentHasManyDeep\HasTableAlias;
}

使用 setAlias() 在连接现有关系时指定表别名

class Post extends Model
{
    use \Staudenmeir\EloquentHasManyDeep\HasRelationships;

    public function commentReplies(): \Staudenmeir\EloquentHasManyDeep\HasManyDeep
    {
        return $this->hasManyDeepFromRelations(
            $this->comments(),
            (new Comment())->setAlias('alias')->replies()
        );
    }

    public function comments()
    {
        return $this->hasMany(Comment::class);
    }
}

class Comment extends Model
{
    use \Staudenmeir\EloquentHasManyDeep\HasTableAlias;

    public function replies()
    {
        return $this->hasMany(self::class, 'parent_id');
    }
}

软删除

默认情况下,软删除的中间模型将被排除在结果之外。使用 withTrashed() 包含它们

class Country extends Model
{
    use \Staudenmeir\EloquentHasManyDeep\HasRelationships;

    public function comments(): \Staudenmeir\EloquentHasManyDeep\HasManyDeep
    {
        return $this->hasManyDeep(Comment::class, [User::class, Post::class])
            ->withTrashed('users.deleted_at');
    }
}

class User extends Model
{
    use SoftDeletes;
}

获取唯一结果

与多对多段的多级关系可能在其结果中包含重复的模型。如果您想要获取唯一的结果,可以从结果集合中删除重复项

$uniqueComments = Country::find($id)->comments()->get()->unique();

如果您需要在查询中删除重复项(例如,用于分页),请尝试添加 distinct()

$uniqueComments = Country::find($id)->comments()->distinct()->get();

distinct() 并非对所有情况都有效。如果它对您不起作用,请使用 groupBy() 代替

$uniqueComments = Country::find($id)->comments()
    ->getQuery()             // Get the underlying query builder
    ->select('comments.*')   // Select only columns from the related table
    ->groupBy('comments.id') // Group by the related table's primary key 
    ->get();

反转关系

您可以通过使用 hasManyDeepFromReverse()/hasOneDeepFromReverse() 反转现有的多级关系来定义 HasManyDeep/HasOneDeep 关系

class Country extends Model
{
    use \Staudenmeir\EloquentHasManyDeep\HasRelationships;

    public function comments(): \Staudenmeir\EloquentHasManyDeep\HasManyDeep
    {
        return $this->hasManyDeep(Comment::class, [User::class, Post::class]);
    }
}

class Comment extends Model
{
    use \Staudenmeir\EloquentHasManyDeep\HasRelationships;

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

IDE 辅助工具

如果您使用 barryvdh/laravel-ide-helper,此包提供了一个模型钩子,当生成类型提示时会正确添加关系。默认情况下,使用 Package Discovery 启用模型钩子。

要手动启用它,请将 model hook 添加到 model_hooks 数组中。

    // File: config/ide-helper.php

    /*
    |--------------------------------------------------------------------------
    | Models hooks
    |--------------------------------------------------------------------------
    |
    | Define which hook classes you want to run for models to add custom information
    |
    | Hooks should implement Barryvdh\LaravelIdeHelper\Contracts\ModelHookInterface.
    |
    */

    'model_hooks' => [
        \Staudenmeir\EloquentHasManyDeep\IdeHelper\DeepRelationsHook::class,
    ],

要禁用模型钩子,您有三个选择

使用 .env 禁用

更新您的 .env 文件以包含

ELOQUENT_HAS_MANY_DEEP_IDE_HELPER_ENABLED=false

使用配置禁用

发布配置并直接禁用设置

php artisan vendor:publish --tag=eloquent-has-many-deep
    // File: config/eloquent-has-many-deep.php
    
    /*
    |--------------------------------------------------------------------------
    | IDE Helper
    |--------------------------------------------------------------------------
    |
    | Automatically register the model hook to receive correct type hints
    |
    */
    'ide_helper_enabled' => false,

通过退出 Package Discovery 禁用

使用以下内容更新您的 composer.json

"extra": {
    "laravel": {
        "dont-discover": [
            "staudenmeir/eloquent-has-many-deep"
        ]
    }
},

贡献

有关详细信息,请参阅 CONTRIBUTINGCODE OF CONDUCT