baril/smoothie

为Laravel的Eloquent添加了一些有趣的特性:模糊日期、双向多对多关系、可排序行为、闭包表。

v1.3.1 2020-07-08 15:07 UTC

This package is auto-updated.

Last update: 2024-09-09 00:28:32 UTC


README

为Laravel的Eloquent添加一些有趣的特性

⚠️ 注意:仅MySQL经过测试并得到积极支持。

杂项

保存模型并恢复修改后的属性

此包为save方法添加了新的选项restore

$model->save(['restore' => true]);

这会强制模型在保存之前从数据库中刷新其original属性数组。当数据库行在当前$model实例之外发生变化时,这很有用,您需要确保$model的当前状态将被精确保存,即使恢复当前实例中未更改的属性

$model1 = Article::find(1);
$model2 = Article::find(1);

$model2->title = 'new title';
$model2->save();

$model1->save(['restore' => true]); // the original title will be restored
                                    // because it hasn't changed in `$model1`

要使用此选项,您需要您的模型扩展Baril\Smoothie\Model类而不是Illuminate\Database\Eloquent\Model

仅更新

Laravel的本地update方法不仅会更新提供的字段,还会更新模型之前修改的任何属性

$article = Article::create(['title' => 'old title']);
$article->title = 'new title';
$article->update(['subtitle' => 'new subtitle']);

$article->fresh()->title; // "new title"

此包提供了一个名为updateOnly的另一个方法,该方法将更新提供的字段,但不会更改其他行

$article = Article::create(['title' => 'old title']);
$article->title = 'new title';
$article->updateOnly(['subtitle' => 'new subtitle']);

$article->fresh()->title; // "old title"
$article->title; // "new title"
$article->subtitle; // "new subtitle"

要使用此方法,您需要您的模型扩展Baril\Smoothie\Model类而不是Illuminate\Database\Eloquent\Model

显式排序查询结果

该包为Eloquent集合添加了以下方法

$collection = YourModel::all()->sortByKeys([3, 4, 2]);

它允许显式地对集合按主键排序。在上面的示例中,返回的集合将包含(按此顺序)

  • id为3的模型,
  • id为4的模型,
  • id为2的模型,
  • 原始集合中的其他任何模型,按调用sortByKeys之前的相同顺序。

类似地,使用模型的findInOrder方法或查询构建器上的findInOrder方法,而不是findMany,将保留提供的id的顺序

$collection = Article::findMany([4, 5, 3]); // we're not sure that the article
                                            // with id 4 will be the first of
                                            // the returned collection

$collection = Article::findInOrder([4, 5, 3]); // now we're sure

为了使用这些方法,您需要将Smoothie的服务提供程序注册到您的config\app.php(或使用包自动发现)

return [
    // ...
    'providers' => [
        Baril\Smoothie\SmoothieServiceProvider::class,
        // ...
    ],
];

时间戳作用域

Baril\Smoothie\Concerns\ScopesTimestamps特质为具有created_atupdated_at列的模型提供了一些作用域

  • $query->orderByCreation($direction = 'asc'),
  • $query->createdAfter($date, $strict = false)$date参数可以是任何可转换为日期时间的类型,如果要将$strict参数设置为true以使用严格不等式,则可以设置$strict参数),
  • $query->createdBefore($date, $strict = false),
  • $query->createdBetween($start, $end, $strictStart = false, $strictEnd = false),
  • $query->orderByUpdate($direction = 'asc'),
  • $query->updatedAfter($date, $strict = false),
  • $query->updatedBefore($date, $strict = false),
  • $query->updatedBetween($start, $end, $strictStart = false, $strictEnd = false).

跨数据库关系

使用Laravel,可以声明不属于同一连接的模型之间的关系,但在某些情况下可能会失败

  • 计数关系和查询其存在不会工作(因为它使用子查询),
  • 多对多关系仅在枢轴表与相关模型位于同一数据库中时才会工作(因为连接)。

本包提供了一个crossDatabase方法,通过在表名前添加数据库名来解决此问题。当然,只有当所有数据库都在同一台服务器上时,它才有效。

用法如下:

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

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

对于多对多关系,您可以指定枢纽表是否与父表或相关表位于同一数据库中。在下面的示例中,枢纽表与posts表位于同一数据库中

class Post
{
    public function tags()
    {
        // same database as parent table (posts):
        return $this->belongsToMany(Tag::class)->crossDatabase('parent');
    }
}

class Tag
{
    public function posts()
    {
        // same database as related table (posts):
        return $this->belongsToMany(Post::class)->crossDatabase('related');
    }
}

调试

该包向Builder类添加了一个debugSql方法。它与toSql类似,只是它返回一个实际的SQL查询,其中绑定已被其值替换。

Article::where('id', 5)->toSql(); // "SELECT articles WHERE id = ?" -- WTF?
Article::where('id', 5)->debugSql(); // "SELECT articles WHERE id = 5" -- much better

为了使用此方法,您需要在您的config\app.php中注册Smoothie的服务提供者(或使用包自动发现)。

(此方法的功劳归功于Broutard,谢谢!)

字段别名

基本用法

Baril\Smoothie\Concerns\AliasesAttributes特质提供了一个简单的方法,如果您正在使用现有的数据库且不喜欢列名称,则可以规范化模型的属性名称。

有2种不同的方法来定义别名

  • 定义一个列前缀:所有以此前缀为前缀的列将神奇地作为未加前缀的属性访问,
  • 为给定的列定义一个显式的别名。

假设您正在处理以下表(此示例来自Dotclear博客应用)

dc_blog
    blog_id
    blog_uid
    blog_creadt
    blog_upddt
    blog_url
    blog_name
    blog_desc
    blog_status

然后您可以定义您的模型如下

class Blog extends Model
{
    const CREATED_AT = 'blog_creadt';
    const UPDATED_AT = 'blog_upddt';

    protected $primaryKey = 'blog_id';
    protected $keyType = 'string';

    protected $columnsPrefix = 'blog_';
    protected $aliases = [
        'description' => 'blog_desc',
    ];
}

现在可以通过这种方式简单地访问blog_id列:$model->id。对于所有其他以blog_为前缀的列也是一样。

此外,还可以使用更明确的别名description来访问blog_desc列。

原始名称仍然可用。这意味着实际上有3种不同的方法可以访问blog_desc

  • $model->blog_desc(原始列名),
  • $model->desc(因为blog_前缀),
  • $model->description(多亏了显式别名)。

注意:您不能为另一个别名(显式或隐式)设置别名。别名仅用于实际的列名。

冲突和优先级

如果别名与实际的列名冲突,则它将具有优先级。这意味着在上述示例中,如果表中实际上有一个名为descdescription的列,您将无法再访问它。但是,您仍然可以为此列定义另一个别名。

class Article
{
    protected $aliases = [
        'title' => 'meta_title',
        'original_title' => 'title',
    ];
}

在上面的示例中,模型的title属性返回数据库中meta_title列的值。可以通过original_title属性访问title列的值。

此外,显式别名具有高于由列前缀隐式定义的别名的优先级。这意味着当一个“隐式别名”与实际的列名冲突时,您可以定义一个显式别名来恢复原始列名

class Article
{
    protected $aliases = [
        'title' => 'title',
    ];
    protected $columnsPrefix = 'a_';
}

在这里,模型的title属性将返回数据库中title列的值。可以通过a_title属性(或为其定义另一个别名)来访问a_title列。

访问器、类型转换器和修改器

您可以在原始属性名称、别名或两者上定义访问器。

  • 如果只有原始名称上有访问器,它将始终适用,无论您使用原始名称还是别名访问属性。
  • 如果只有别名上有访问器,它将仅当您使用别名访问属性时才适用。
  • 如果有原始名称和别名上的访问器,每个将单独适用(并将接收原始的$value)。
class Blog extends Model
{
    const CREATED_AT = 'blog_creadt';
    const UPDATED_AT = 'blog_upddt';

    protected $primaryKey = 'blog_id';
    protected $keyType = 'string';

    protected $columnsPrefix = 'blog_';
    protected $aliases = [
        'description' => 'blog_desc',
    ];

    public function getPrDescAttribute($value)
    {
        return trim($value);
    }

    public function getDescriptionAttribute($value)
    {
        return htmlentities($value);
    }
}

$blog->pr_desc; // will return the trimmed description
$blog->desc; // will return the trimmed description
$blog->description; // will return the untrimmed, HTML-encoded description

相同的逻辑也适用于类型转换器和修改器。

⚠️ 注意:如果你在别名上定义了类型转换,并在原始属性名上定义了访问器,则访问器不会应用于别名,只有类型转换会。

特征冲突解决

AliasesAttributes》特征覆盖了Eloquent的《Model》类中的《getAttribute》和《setAttribute》方法。如果你使用此特征与覆盖相同方法的另一个特征,你可以只需将其他特征的函数别名设置为《getUnaliasedAttribute》和《setUnaliasedAttribute》。当解析别名后,《AliasesAttributes::getAttribute》和《AliasesAttributes::setAttribute》将调用《getUnaliasedAttribute》或《setUnaliasedAttribute》。

class MyModel extends Model
{
    use AliasesAttributes, SomeOtherTrait {
        AliasesAttributes::getAttribute insteadof SomeOtherTrait;
        SomeOtherTrait::getAttribute as getUnaliasedAttribute;
        AliasesAttributes::setAttribute insteadof SomeOtherTrait;
        SomeOtherTrait::setAttribute as setUnaliasedAttribute;
    }
}

访问器缓存

基本用法

有时你在模型中定义的访问器需要一些计算时间或执行一些查询,你不想每次调用此访问器时都经过整个流程。这就是为什么这个包提供了一个特征,它可以“缓存”(在对象的保护属性中)访问器的结果。

你可以使用《$cacheable》属性或《$uncacheable》属性来定义要缓存哪些访问器。如果两者都没有设置,则一切都将被缓存。

class MyModel extends Model
{
    use \Baril\Smoothie\Concerns\CachesAccessors;

    protected $cacheable = [
        'some_attribute',
        'some_other_attribute',
    ];
}

$model = MyModel::find(1);
$model->some_attribute; // cached
$model->yet_another_attribute; // not cached

清除缓存

每次设置此属性时,都会清除该属性的缓存。如果你有一个依赖于另一个属性B的属性A的访问器,你可能想在设置B时清除A的缓存。你可以使用《$clearAccessorCache》属性来定义此类依赖。

class User extends Model
{
    use \Baril\Smoothie\Concerns\CachesAccessors;

    protected $clearAccessorCache = [
        'first_name' => ['full_name', 'name_with_initial'],
        'last_name' => ['full_name', 'name_with_initial'],
    ];

    public function getFullNameAttribute()
    {
        return $this->first_name . ' ' . $this->last_name;
    }

    public function getNameWithInitialAttribute()
    {
        return substr($this->first_name, 0, 1) . '. ' . $this->last_name;
    }
}

$user = new User([
    'first_name' => 'Jean',
    'last_name' => 'Dupont',
]);
echo $user->full_name; // "Jean Dupont"
$user->first_name = 'Lazslo';
echo $user->full_name; // "Lazslo Dupont": cache has been cleared

缓存和别名

如果你想在同一个模型中使用《AliasesAttributes》特征》和《CachesAccessors》特征,最好的方法是用《AliasesAttributesWithCache》特征,它正确地合并了这两个特征的功能。设置属性或别名将自动清除同一属性的别名访问器的缓存。

模糊日期

该包提供了一个修改版的《Carbon》类,可以处理SQL“模糊”日期(其中天或月和天为零)。

使用原始版本的Carbon,此类日期不会被正确解释,例如,《2010-10-00》会被解释为《2010-09-30》。

在这个版本中,零是允许的。提供了一个额外的方法来确定日期是否模糊。

$date = Baril\Smoothie\Carbon::createFromFormat('Y-m-d', '2010-10-00');
$date->day; // will return null
$date->isFuzzy(); // will return true if month and/or day is zero

format》和《formatLocalized》方法现在有两个额外的(可选)参数《$formatMonth》和《$formatYear》。如果日期是模糊的,该方法将自动回退到适当的格式。

$date = Baril\Smoothie\Carbon::createFromFormat('Y-m-d', '2010-10-00');
$date->format('d/m/Y', 'm/Y', 'Y'); // will display "10/2010"

⚠️ 注意:由于模糊日期不能转换为时间戳,因此内部在转换为时间戳之前,日期《2010-10-00》会转换为《2010-10-01》。因此,任何依赖于时间戳值的函数或获取器可能会返回“意外”的结果。

$date = Baril\Smoothie\Carbon::createFromFormat('Y-m-d', '2010-10-00');
$date->dayOfWeek; // will return 5, because October 1st 2010 was a friday

如果你需要在你的模型中使用模糊日期,请使用《Baril\Smoothie\Concerns\HasFuzzyDates》特征。然后,将字段类型转换为《date》或《datetime》时,将使用这个修改版的Carbon。

class Book extends \Illuminate\Database\Eloquent\Model
{
    use \Baril\Smoothie\Concerns\HasFuzzyDates;

    protected $casts = [
        'is_available' => 'boolean',
        'release_date' => 'date', // will allow fuzzy dates
    ];
}

或者,你可以扩展《Baril\Smoothie\Model》类来实现相同的结果。这个类已经使用了《HasFuzzyDates》特征(以及后续章节中描述的一些其他特征)。

class Book extends \Baril\Smoothie\Model
{
    protected $casts = [
        'is_available' => 'boolean',
        'release_date' => 'date', // will allow fuzzy dates
    ];
}

⚠️ 注意:你需要禁用你的《database.php》配置文件中的MySQL严格模式,才能使用模糊日期。

return [
    'connections' => [
        'mysql' => [
            'strict' => false,
            // ...
        ],
    ],
    // ...
];

如果你不想禁用严格模式,另一个选项是使用3个单独的列并将它们合并成一个。为了轻松实现这一点,你可以在访问器中使用《mergeDate》方法,而《splitDate》方法是修改器。

class Book extends \Illuminate\Database\Eloquent\Model
{
    use \Baril\Smoothie\Concerns\HasFuzzyDates;

    public function getReleaseDateAttribute()
    {
        return $this->mergeDate(
            'release_date_year',
            'release_date_month',
            'release_date_day'
        );
    }

    public function setReleaseDateAttribute($value)
    {
        $this->splitDate(
            $value,
            'release_date_year',
            'release_date_month',
            'release_date_day'
        );
    }
}

这两个方法的最后两个参数可以省略,如果你的列名使用了后缀《_year》、《_month》和《_day》。下面的示例与上面的示例类似。

class Book extends \Illuminate\Database\Eloquent\Model
{
    use \Baril\Smoothie\Concerns\HasFuzzyDates;

    public function getReleaseDateAttribute()
    {
        return $this->mergeDate('release_date');
    }

    public function setReleaseDateAttribute($value)
    {
        $this->splitDate($value, 'release_date');
    }
}

⚠️ 注意:你的《_month》和《_day》列必须是可空的,因为“零”月或天将存储为《null》。

相互属于多对多关系

用法

这种新的关系定义了对同一表/模型的许多到许多、互惠关系。Laravel 的原生 BelongsToMany 关系可以处理自引用关系,但需要指定方向(例如 sellers/buyers)。区别在于 MutuallyBelongsToManySelves 关系旨在处理“互惠”关系(如 friends

class User extends \Illuminate\Database\Eloquent\Model
{
    use \Baril\Smoothie\Concerns\HasMutualSelfRelationships;

    public function friends()
    {
        return $this->mutuallyBelongsToManySelves();
    }
}

使用这种关系,将 $user1 添加到 $users2friends 将会隐式地将 $user2 添加到 $user1friends

$user1->friends()->attach($user2->id);
$user2->friends()->get(); // contains $user1

同样,断开关系的任一侧也会断开另一侧

$user2->friends()->detach($user1->id);
$user1->friends()->get(); // doesn't contain $user2 any more

mutuallyBelongsToManySelves 方法的完整原型与 belongsToMany 相似,但没有第一个参数(因为我们已经知道相关的类是本身)

public function mutuallyBelongsToManySelves(

        // Name of the pivot table (defaults to the snake-cased model name,
        // concatenated to itself with an underscore separator,
        // eg. "user_user"):
        $table = null,

        // First pivot key (defaults to the model's default foreign key, with
        // an added number, eg. "user1_id"):
        $firstPivotKey = null,

        // Second pivot key (the pivot keys can be passed in any order since
        // the relationship is mutual):
        $secondPivotKey = null,

        // Parent key (defaults to the model's primary key):
        $parentKey = null,

        // Relation name (defaults to the name of the caller method):
        $relation = null)

为了使用 mutuallyBelongsToManySelves 方法,您的模型需要使用 Baril\Smoothie\Concerns\HasMutualSelfRelationships 或扩展 Baril\Smoothie\Model 类。

清理命令

为了避免重复,MutuallyBelongsToManySelves 类将确保将 $model1 添加到 $model2 将插入与将 $model2 添加到 $model1 相同的枢轴行:定义的第一个枢轴键始终接收较小的 id。如果您正在处理现有数据,并且不确定您的枢轴表的内容是否遵循此规则,可以使用以下 Artisan 命令来检查数据并在需要时修复它

php artisan smoothie:fix-pivots "App\\YourModelClass" relationName

N元多对多关系

假设您正在构建一个项目管理应用程序。您的应用程序的每个用户在您的 ACL 系统中都有许多角色:项目经理、开发者……但每个角色适用于特定的项目而不是整个应用程序。

您的基本数据库结构可能看起来像这样

projects
    id - integer
    name - string

roles
    id - integer
    name - string

users
    id - integer
    name - string

project_role_user
    project_id - integer
    role_id - integer
    user_id - integer

当然,您可以在模型之间定义经典的 belongsToMany 关系,甚至可以添加 withPivot 子句以包含第三个枢轴列

class User extends Model
{
    public function projects()
    {
        return $this->belongsToMany(Project::class, 'project_role_user')->withPivot('role_id');
    }

    public function roles()
    {
        return $this->belongsToMany(Role::class, 'project_role_user')->withPivot('project_id');
    }
}

但这不会非常令人满意,因为

  • 查询 $user->projects()$user->roles() 可能会返回重复的结果(如果用户在同一个项目中具有两个不同的角色,或在两个不同的项目中具有相同的角色),
  • 这两个关系互不关联,因此没有优雅的方法来检索特定项目的用户角色,或用户具有特定角色的项目。

这就是 belongsToMultiMany 关系派上用场的地方。

设置

步骤 1:为您的枢轴表添加主键。

class AddPrimaryKeyToProjectRoleUserTable extends Migration
{
    public function up()
    {
        Schema::table('project_role_user', function (Blueprint $table) {
             $table->increments('id')->first();
        });
    }
    public function down()
    {
        Schema::table('project_role_user', function (Blueprint $table) {
             $table->dropColumn('id');
        });
    }
}

步骤 2:让您的模型使用 Baril\Smoothie\Concerns\HasMultiManyRelationships 特性(或扩展 Baril\Smoothie\Model 类)。

步骤 3:使用 belongsToMultiMany 而不是 belongsToMany 定义您的关系。这两种方法的原型相同,除了

  • belongsToMultiMany 的第二个参数(枢轴表名称)是必需的(因为我们无法猜测),
  • 还有一个额外的第三个(可选)参数,它是枢轴表主键的名称(默认为 id)。
class User extends Model
{
    use HasMultiManyRelationships;

    public function projects()
    {
        return $this->belongsToMultiMany(Project::class, 'project_role_user');
    }

    public function roles()
    {
        return $this->belongsToMultiMany(Role::class, 'project_role_user');
    }
}

您可以在所有三个类中做同样的事情,这意味着您将声明 6 个不同的关系。请注意

  • 为了避免混淆,最好(但不强制)给类似的关系相同的名称(Project::roles()User::roles())。
  • 如果您知道您永远不会需要某些关系,则不必定义所有 6 个关系。

此外,请注意,关系的定义是独立的:这里没有任何东西表明 projectsroles 互相关联。魔法只会因为它们被定义为“多对多”关系并且它们使用相同的枢轴表而发生。

查询关系

总的来说,多对多关系的行为与多对多关系完全相同。但是有两个区别。

第一个区别是多对多关系将返回“折叠”的(即去重的)结果。例如,如果$user在2个不同的项目中具有admin角色,$user->roles将只返回一次admin(与常规的BelongsToMany关系相反)。如果您需要获取“未折叠”的结果,只需链式调用unfolded()方法即可。

$user->roles()->unfolded()->get();

第二个(也是最重要的)区别是,当您“链式”两个(或更多)“兄弟”多对多关系时,每个关系的返回结果将自动受先前链式关系(s)的限制。

请看以下示例

$roles = $user->projects->first()->roles;

在这里,一个常规的BelongsToMany关系会返回所有与项目相关的角色,无论它们是附加到这个$user还是另一个用户。但是,使用多对多关系,$roles只包含$user在这个项目中的角色。

如果您需要,可以始终通过链式调用all()方法来取消这种行为

$project = $user->projects->first();
$roles = $project->roles()->all()->get();

现在$roles包含所有来自$project的角色,无论它们来自这个$user还是任何其他用户。

使用多对多关系的另一种方法是

$project = $user->projects->first();
$roles = $user->roles()->for('project', $project)->get();

这将只返回$user$project上的角色。这是一种更简洁的方式来编写以下内容

$project = $user->projects->first();
$roles = $user->roles()->withPivot('project_id', $project->id)->get();

for方法的参数是

  • 父类中“其他”关系的名称(这里:projects,如在方法User::projects()中),或其单数形式(project),
  • 可以是模型对象或id,或模型或id的集合,或id的数组。

预加载

上述行为也适用于预加载

$users = User::with('projects', 'projects.roles')->get();
$user = $users->first();
$user->projects->first()->roles; // only the roles of $user on this project

类似于上述的all()方法,如果您不想限制第二个关系,可以使用withAll

$users = User::with('projects')->withAll('projects.roles')->get();

注意:对于非多对多关系或“非约束”的多对多关系,withAllwith的别名

$users = User::with('projects', 'status')->withAll('projects.roles')->get();
// can be shortened to:
$users = User::withAll('projects', 'projects.roles', 'status')->get();

查询关系存在

查询关系的存在也将具有相同的行为

User::has('projects.roles')->get();

上述查询将返回在任何项目中具有角色的用户。

附加/分离相关模型

将模型附加到多对多关系将填充所有先前链式“兄弟”多对多关系的连接值

$user->projects()->first()->roles()->attach($admin);
// The new pivot row will receive $user's id in the user_id column.

从关系中分离模型也将考虑所有“关系链”

$user->projects()->first()->roles()->detach($admin);
// Will detach the $admin role from this project, for $user only.
// Other admins of this project will be preserved.

同样,上述描述的行为可以通过链式调用all()方法来禁用

$user->projects()->first()->roles()->all()->attach($admin);
// The new pivot row's user_id will be NULL.

$user->projects()->first()->roles()->all()->detach($admin);
// Will delete all pivot rows for this project and the $admin role,
// whoever the user is.

多对多关系“包装器”

WrapMultiMany关系提供了一种处理多对多关系的替代方法。它可以与BelongsToMultiMany关系一起使用,也可以独立使用。

与其将三元关系视为可以链式连接的六个多对多关系,我们可以这样看待

  • 一个用户有许多角色/项目对,
  • 这些对中的每一个都有一个角色和一个项目。

当然,类似地,一个角色有许多用户/项目对,一个项目有许多角色/用户对。

为了实现这一点,我们可以为连接表创建一个模型,然后手动定义所有关系,但WrapMultiMany关系提供了一种更快的方法。

class User extends Model
{
    use HasMultiManyRelationships;

    public function authorizations()
    {
        return $this->wrapMultiMany([
            'project' => $this->belongsToMultiMany(Project::class, 'project_role_user'),
            'role' => $this->belongsToMultiMany(Role::class, 'project_role_user'),
        ]);
    }
}

上面的authorizations方法定义了以下关系

  • User到连接表的HasMany关系,
  • 从连接表到Project的命名为projectBelongsTo关系,
  • 从连接表到Role的命名为roleBelongsTo关系。

您可以使用任何常规关系查询这些关系,甚至可以预加载它们。

$users = User::with('authorizations', 'authorizations.role', 'authorizations.project')->get();

foreach ($users as $user) {
    foreach ($user->authorizations as $authorization) {
        dump($authorization->role);
        dump($authorization->project);
    }
}

您可以使用以下方法向连接表插入或更新数据

$user->authorizations()->attach($pivots, $additionalAttributes);
$user->authorizations()->sync($pivots);
$user->authorizations()->detach($pivots);

$pivots参数可以是不同类型

$pivots = $user->authorizations->first(); // a Model
$pivots = $user->authorizations->slice(0, 2); // an EloquentCollection of Models
$pivots = ['role_id' => $roleId, 'project_id' => $projectId]; // an associative array keyed by the column names...
$pivots = ['role' => $roleId, 'project' => $projectId]; // ... or the relation names
$pivots = ['role' => Role::first(), 'project' => Project::first()]; // ... where values can be ids or Models
$pivots = [ ['role_id' => $roleId, 'project_id' => $projectId] ]; // an array of such associative arrays
$pivots = collect([ ['role_id' => $roleId, 'project_id' => $projectId] ]); // or even a Collection

动态关系

Baril\Smoothie\Concerns\HasDynamicRelations特质为您提供了在模型上动态定义新关系的能力。

这些关系可以全局定义(对于类中所有实例),或者局部定义(对于特定实例)。

首先,在你的模型上使用HasDynamicRelations特性

class Asset extends Model
{
    use \Baril\Smoothie\Concerns\HasDynamicRelations;
}

现在,你可以通过调用defineRelation方法来定义你的关系,无论是静态的还是实例级别的

// This relation will now be available on all your Assets:
Asset::defineRelation($someName, function () use ($someClass) {
    return $this->belongsTo($someClass);
});

// This relation will now be available on this instance only:
$asset = new Asset;
$asset->defineRelation($someOtherName, function () use ($someOtherClass) {
    return $this->belongsTo($someOtherClass);
});

在这两种情况下,一旦定义了关系,就可以像使用任何Eloquent关系一样使用它

$entities = $asset->$someName()->where('status', 1)->get(); // regular call
$attachments = $asset->$someOtherClass; // dynamic property

可排序行为

为Eloquent模型添加可排序行为(来自https://github.com/boxfrommars/rutorika-sortable)。

设置

首先,在你的模型中添加一个position字段(下面将说明如何更改此名称)

public function up()
{
    Schema::create('articles', function (Blueprint $table) {
        // ... other fields ...
        $table->unsignedInteger('position');
    });
}

然后,在你的模型中使用\Baril\Smoothie\Concerns\Orderable特性。由于不会手动填充,因此应该保护position字段。

class Article extends Model
{
    use \Baril\Smoothie\Concerns\Orderable;

    protected $guarded = ['position'];
}

如果你想使用不同于position的名称,则需要设置$orderColumn属性

class Article extends Model
{
    use \Baril\Smoothie\Concerns\Orderable;

    protected $orderColumn = 'order';
    protected $guarded = ['order'];
}

基本用法

你可以使用以下方法来更改模型的位置(无需保存,该方法已自动完成)

  • moveToOffset($offset)$offset从0开始,可以是负数,例如$offset = -1是最后一个位置),
  • moveToStart(),
  • moveToEnd(),
  • moveToPosition($position)$position从1开始,必须是一个有效位置),
  • moveUp($positions = 1, $strict = true):将模型向上移动$positions个位置($strict参数控制在尝试将模型移动到“范围之外”时会发生什么:如果设置为false,则模型将被移动到第一个或最后一个位置,否则将抛出PositionException),
  • moveDown($positions = 1, $strict = true),
  • swapWith($anotherModel),
  • moveBefore($anotherModel),
  • moveAfter($anotherModel).
$model = Article::find(1);
$anotherModel = Article::find(10)
$model->moveAfter($anotherModel);
// $model is now positioned after $anotherModel, and both have been saved

此外,此特性

  • create事件上自动定义模型位置,因此无需手动设置position
  • delete事件上自动减少后续模型的位置,以确保没有“间隙”。
$article = new Article();
$article->title = $request->input('title');
$article->body = $request->input('body');
$article->save();

此模型将定位在MAX(position) + 1

要获取有序模型,请使用ordered作用域

$articles = Article::ordered()->get();
$articles = Article::ordered('desc')->get();

(你可以通过调用unordered作用域来取消此作用域的效果。)

可以使用previousnext方法查询前一个和下一个模型

$entity = Article::find(10);
$entity->next(10); // returns a QueryBuilder on the next 10 entities, ordered
$entity->previous(5)->get(); // returns a collection with the previous 5 entities, in reverse order
$entity->next()->first(); // returns the next entity

大量重新排序

上面描述的move*方法不适合大量重新排序,因为

  • 它们会执行许多不必要的查询,
  • 更改模型的位置会影响其他模型的位置,如果不小心可能会产生副作用。

示例

$models = Article::orderBy('publication_date', 'desc')->get();
$models->map(function($model, $key) {
    return $model->moveToOffset($key);
});

上面的示例代码会破坏数据,因为你在更改模型的位置之前需要确保每个模型都是“最新的”。另一方面,以下代码将正常工作

$collection = Article::orderBy('publication_date', 'desc')->get();
$collection->map(function($model, $key) {
    return $model->fresh()->moveToOffset($key);
});

尽管如此,这仍然不是一种好的方法,因为它会执行许多不必要的查询。处理大量重新排序的更好方法是使用集合上的saveOrder方法

$collection = Article::orderBy('publication_date', 'desc')->get();
// $collection is not a regular Eloquent collection object, it's a custom class
// with the following additional method:
$collection->saveOrder();

就这样!现在集合中的项目顺序已应用于数据库的position列。

要显式定义顺序,你可以这样做

$collection = Status::all();
$collection->sortByKeys([2, 1, 5, 3, 4])->saveOrder();

注意:只有集合中的模型之间会重新排序/交换。表中的其他行保持不变。

可排序组/一对一关系

有时,表的数据根据某些列“分组”,你需要单独对每个组进行排序,而不是全局排序。为了实现这一点,你只需要设置$groupColumn属性

class Article extends Model
{
    use \Baril\Smoothie\Concerns\Orderable;

    protected $guarded = ['position'];
    protected $groupColumn = 'section_id';
}

如果组由多个列定义,可以使用数组

protected $groupColumn = ['field_name1', 'field_name2'];

可排序组可用于处理有序一对一关系

class Section extends Model
{
    public function articles()
    {
        return $this->hasMany(Article::class)->ordered();
    }
}

有序多对多关系

如果你需要排序多对多关系,则需要连接表中有一个position列(或某个其他名称)。

让模型使用\Baril\Smoothie\Concerns\HasOrderedRelationships特性(或扩展Baril\Smoothie\Model类)

class Post extends Model
{
    use \Baril\Smoothie\Concerns\HasOrderedRelationships;

    public function tags()
    {
        return $this->belongsToManyOrdered(Tag::class);
    }
}

belongsToManyOrdered 方法的原型与 belongsToMany 类似,增加了第二个参数 $orderColumn

public function belongsToManyOrdered(
        $related,
        $orderColumn = 'position',
        $table = null,
        $foreignPivotKey = null,
        $relatedPivotKey = null,
        $parentKey = null,
        $relatedKey = null,
        $relation = null)

现在所有来自 BelongsToMany 类的常规方法都会设置附加模型的正确位置

$post->tags()->attach($tag->id); // will attach $tag and give it the last position
$post->tags()->sync([$tag1->id, $tag2->id, $tag3->id]) // will keep the provided order
$post->tags()->detach($tag->id); // will decrement the position of subsequent $tags

查询时,关系默认按顺序排序。如果您想按其他字段对相关模型进行排序,您需要先使用 unordered 范围

$post->tags; // ordered by position
$post->tags()->ordered('desc')->get(); // reverse order
$post->tags()->unordered()->get(); // unordered

// Note that orderBy has no effect here since the tags are already ordered by position:
$post->tags()->orderBy('id')->get();

// This is the proper way to do it:
$post->tags()->unordered()->orderBy('id')->get();

当然,如果您不想默认排序,也可以这样定义关系

class Post extends Model
{
    use \Baril\Smoothie\Concerns\HasOrderedRelationships;

    public function tags()
    {
        return $this->belongsToManyOrdered(Tag::class)->unordered();
    }
}

$article->tags; // unordered
$article->tags()->ordered()->get(); // ordered

BelongsToManyOrdered 类具有与 Orderable 特性相同的所有方法,但您需要传递相关 $model 来进行操作

  • moveToOffset($model, $offset),
  • moveToStart($model),
  • moveToEnd($model),
  • moveToPosition($model, $position),
  • moveUp($model, $positions = 1, $strict = true),
  • moveDown($model, $positions = 1, $strict = true),
  • swap($model, $anotherModel),
  • moveBefore($model, $anotherModel) ($model 将被移动到 $anotherModel 之前),
  • moveAfter($model, $anotherModel) ($model 将被移动到 $anotherModel 之后),
  • before($model) (类似于 Orderable 特性中的 previous 方法),
  • after($model) (类似于 next)。
$tag1 = $article->tags()->first();
$tag2 = $article->tags()->last();
$article->tags()->moveBefore($tag1, $tag2);
// now $tag1 is at the second to last position

请注意,如果 $model 不属于该关系,任何这些方法都会抛出 Baril\Smoothie\GroupException

还有一个用于批量重新排序的方法

$article->tags()->setOrder([$id1, $id2, $id3]);

在上面的例子中,具有 ID $id1$id2$id3 的标签现在将位于文章的 tags 集合的开头。任何其他附加到文章的标签将按调用 setOrder 之前的相同顺序出现在其后。

有序的多对多关系

同样,该包定义了一个 MorphToManyOrdered 类型的关系。 morphToManyOrdered 方法的第三个参数是排序列的名称(默认为 position

class Post extends Model
{
    use \Baril\Smoothie\Concerns\HasOrderedRelationships;

    public function tags()
    {
        return $this->morphToManyOrdered('App\Tag', 'taggable', 'tag_order');
    }
}

morphedByManyOrdered 方法相同

class Tag extends Model
{
    use \Baril\Smoothie\Concerns\HasOrderedRelationships;

    public function posts()
    {
        return $this->morphedByManyOrdered('App\Post', 'taggable', 'order');
    }

    public function videos()
    {
        return $this->morphedByManyOrdered('App\Video', 'taggable', 'order');
    }
}

树状结构和闭包表

这是 Laravel 和 SQL 的 "闭包表" 设计模式的实现。这种模式允许对存储在关系数据库中的树形结构进行更快的查询。

设置

您需要在数据库中创建一个闭包表。例如,如果您的主体表是 tags,则需要一个名为 tag_tree 的闭包表(您可以更改此名称——见下文),具有以下列

  • ancestor_id:到您的主表的外键,
  • descendant_id:到您的主表的外键,
  • depth:无符号整数。

当然,您不需要手动编写迁移:该包提供了一个 Artisan 命令(见下文)。

此外,您的主体表需要一个具有自引用外键的 parent_id 列(您也可以更改此名称——见下文)。该列是实际层次结构数据的存储列:闭包只是该信息的复制。

一旦您的数据库准备就绪,让您的模型实现 Baril\Smoothie\Concerns\BelongsToTree 特性。

您可以使用以下属性来配置表和列名称

  • $parentForeignKey:主表中的自引用外键名称(默认为 parent_id),
  • $closureTable:闭包表名称(默认为主模型名称的蛇形名称后缀为 _tree,例如 tag_tree)。
class File extends \Illuminate\Database\Eloquent\Model
{
    use \Baril\Smoothie\Concerns\BelongsToTree;

    protected $parentForeignKey = 'folder_id';
    protected $closureTable = 'file_closures';
}

Artisan 命令

注意:您需要在上述描述中配置您的模型之后才能使用这些命令。

grow-tree 命令将生成闭包表的迁移文件

php artisan smoothie:grow-tree "App\\YourModel"

如果您使用 --migrate 选项,则该命令还会运行迁移。如果您的主体表已包含数据,它还会为现有数据插入闭包。

php artisan smoothie:grow-tree "App\\YourModel" --migrate

⚠️ 注意:如果您使用 --migrate 选项,则会运行任何其他挂起的迁移。

还有一些其他选项:使用 --help 了解更多信息。

如果您需要重新计算闭包,可以使用以下命令

php artisan smoothie:fix-tree "App\\YourModel"

它将截断表并基于主表中的数据重新填充。

最后,show-tree 命令提供了一种快速简便的方法来输出树的内容。它需要一个 label 参数,该参数定义了用作标签的列(或访问器)。可选地,您还可以指定最大深度。

php artisan smoothie:show-tree "App\\YourModel" --label=name --depth=3

基本用法

只需填写模型的 parent_id 并保存模型:闭包表将相应更新。

$tag = Tag::find($tagId);
$tag->parent_id = $parentTagId; // or: $tag->parent()->associate($parentTag);
$tag->save();

在发生冗余错误的情况下(例如,如果 parent_id 对应于模型本身或其子代之一),save 方法将抛出 \Baril\Smoothie\TreeException

当您删除一个模型时,其闭包将被自动删除。如果模型有子代,则 delete 方法将抛出 TreeException。如果您想删除模型及其所有子代,则需要使用 deleteTree 方法。

try {
    $tag->delete();
} catch (\Baril\Smoothie\TreeException $e) {
    // some specific treatment
    // ...
    $tag->deleteTree();
}

关系

该特性定义了以下关系(目前不能重命名)

  • parent:与父代的 BelongsTo 关系
  • children:与子代的 HasMany 关系
  • ancestors:与祖先的 BelongsToMany 关系
  • ancestorsWithSelf:与祖先的 BelongsToMany 关系,包括 $this
  • descendants:与子代的 BelongsToMany 关系
  • descendantsWithSelf:与子代的 BelongsToMany 关系,包括 $this

⚠️ 注意:ancestorsdescendants(以及 -WithSelf)关系是只读的!尝试在这些关系上使用 attachdetach 方法将抛出异常。

可以按深度(即直接父/子代)对 ancestorsdescendants 关系进行排序

$tags->descendants()->orderByDepth()->get();

加载或预加载 descendants 关系将自动加载 children 关系(无需额外查询)。此外,它还将递归地加载所有预加载的子代的 children 关系

$tags = Tag::with('descendants')->limit(10)->get();

// The following code won't execute any new query:
foreach ($tags as $tag) {
    dump($tag->name);
    foreach ($tag->children as $child) {
        dump('-' . $child->name);
        foreach ($child->children as $grandchild) {
            dump('--' . $grandchild->name);
        }
    }
}

当然,ancestorsparent 关系也是如此。

您可以使用此方法检索整个树

$tags = Tag::getTree();

它将返回一个包含根元素的集合,每个元素都将预加载 children 关系,直到叶子节点。

方法

该特性定义了以下方法

  • isRoot():如果项的 parent_idnull,则返回 true
  • isLeaf():检查项是否为叶子(即没有子代)
  • hasChildren()$tag->hasChildren()!$tag->isLeaf() 类似,但更易读
  • isChildOf($item),
  • isParentOf($item),
  • isDescendantOf($item),
  • isAncestorOf($item),
  • isSiblingOf($item),
  • commonAncestorWith($item):返回两个项之间的第一个共同祖先,如果没有共同祖先(这可能在树有多个根时发生),则返回 null
  • distanceTo($item):返回两个项之间的“距离”
  • depth():返回项在树中的深度
  • subtreeDepth():返回项作为根的子树的深度

查询范围

  • withAncestors($depth = null, $constraints = null):是 with('ancestors') 的快捷方式,增加了指定 $depth 限制的能力(例如,$query->withAncestors(1) 将只加载直接父代)。可选地,您可以传递额外的 $constraints
  • withDescendants($depth = null, $constraints = null).
  • withDepth($as = 'depth'):将在您的结果模型上添加一个 depth 列(或您提供的任何别名)
  • whereIsRoot($bool = true):将查询限制为没有父代的项(通过将 $bool 参数设置为 false,可以反转范围的行为)。
  • whereIsLeaf($bool = true).
  • whereHasChildren($bool = true):是 whereIsLeaf 的相反操作。
  • whereIsDescendantOf($ancestorId, $maxDepth = null, $includingSelf = false):限制查询到$ancestorId的后代,可选的$maxDepth。如果将$includingSelf参数设置为true,祖先也将包含在查询结果中。
  • whereIsAncestorOf($descendantId, $maxDepth = null, $includingSelf = false).
  • orderByDepth($direction = 'asc'):此作用域仅在查询ancestorsdescendants关系时才有效(请参阅下面的示例)。
$tag->ancestors()->orderByDepth();
Tag::with(['descendants' => function ($query) {
    $query->orderByDepth('desc');
}]);

有序树

如果您需要明确地为树的每一级排序,可以使用Baril\Smoothie\Concerns\BelongsToOrderedTree特性(而不是BelongsToTree)。

您需要在主表中有一个position列(列名可以通过$orderColumn属性进行配置)。

class Tag extends \Illuminate\Database\Eloquent\Model
{
    use \Baril\Smoothie\Concerns\BelongsToOrderedTree;

    protected $orderColumn = 'order';
}

现在,children关系将进行排序。如果您需要按其他字段排序(或根本不需要对子项进行排序),可以使用unordered作用域。

$children = $this->children()->unordered()->orderBy('name');

此外,所有由上面描述的Orderable特性定义的方法现在都将可用。

$lastChild->moveToPosition(1);

可缓存行为

此包为模型提供Cacheable特性。这不仅仅是一个针对单个项目或查询的缓存,而是一个将整个表内容作为集合存储的缓存系统。因此,它适用于存储不会经常更改的参考数据的小表(如国家或状态列表)。

基本原理

基本原理如下

  • 首次执行“缓存”查询时,表的全部内容将作为具有无限生命期的Eloquent\Collection存储在缓存中。
  • 以下方法在静态调用模型类时始终使用缓存:first及其变体、find及其变体、pluckcountall
  • 默认情况下,其他查询不会被缓存,但可以通过将usingCache方法链接到查询构建器在特定条件下启用缓存(请参阅下面)。
  • 当模型被插入、更新或删除时,其表的缓存将被清除。您还可以使用clearCache静态方法手动清除缓存。

设置

只需在您的模型类上使用Cacheable特性。可选地,您可以通过$cache属性指定要使用的缓存驱动程序。

class Country extends Model
{
    use \Baril\Smoothie\Concerns\Cacheable;

    protected $cache = 'redis';
}

当然,$cache必须引用在cache.php配置文件中定义的缓存存储。

如果您需要更精细地自定义缓存存储(例如设置标签),可以通过重写getCache方法来实现。

class Country extends Model
{
    use \Baril\Smoothie\Concerns\Cacheable;

    public function getCache()
    {
        return app('cache')->store('redis')->tags(['referentials']);
    }
}

默认情况下,将存储在缓存中的是Model::all()的返回值,但可以通过重写loadFromDatabase方法进行自定义,例如,如果您需要加载关系。

class Country extends Model
{
    use \Baril\Smoothie\Concerns\Cacheable;

    protected static function loadFromDatabase()
    {
        return static::with('languages')->get();
    }
}

现在,国家及其languages关系将被加载并存储在缓存中。

缓存查询

可以对特定查询进行缓存,但仅限于非常简单的查询(请参阅下面)。为了启用查询上的缓存,需要将usingCache方法链接到构建器。

Country::where('code', 'fr_FR')->usingCache()->get();

当调用get方法时,将发生以下操作

  1. 从缓存(或从数据库存储并存储在缓存中,如果它之前为空)中检索包含整个表内容的集合。
  2. 将查询的所有whereorderBy子句应用于集合(使用wheresortBy方法)。
  3. 返回过滤和排序后的集合。

步骤2仅在以下条件下才会工作

  • 所有whereorderBy子句都可以转换为对集合的调用方法。这排除了更复杂的子句,例如原始SQL子句、通过OR运算符连接的WHERE子句或使用LIKE运算符的子句。
  • 查询中不得应用其他子句(如havinggroupBywith),因为它们不可翻译。

⚠️ 如果您使用了不可翻译的子句并且仍然启用了缓存,不会抛出异常,但这些子句将被忽略,查询将返回意外的结果。

缓存关系

由于Laravel关系的行为类似于查询构建器,它们也可以使用缓存。当然,相关的模型需要使用Cacheable特性。

class User extends Model
{
    public function country()
    {
        return $this->belongsTo(Country::class)->usingCache();
    }
}

现在,在查询country关系时将始终使用缓存,除非您明确禁用它

$user->country()->usingCache(false)->get();

BelongsToMany(以及BelongsToMultiMany)关系也可以使用缓存,但

  • 对枢纽表的查询仍然会执行,
  • 定义关系的模型需要使用CachesRelationships特性。
class User extends Model
{
    use \Baril\Smoothie\Concerns\CachesRelationships;

    public function groups()
    {
        return $this->belongsToMany(Group::class)->usingCache();
    }
}