baril / smoothie
为Laravel的Eloquent添加了一些有趣的特性:模糊日期、双向多对多关系、可排序行为、闭包表。
Requires
- php: >=7.0
- illuminate/cache: ~5.6
- illuminate/console: ~5.6
- illuminate/database: ~5.6
- illuminate/support: ~5.6
Requires (Dev)
- orchestra/database: ~3.6
- orchestra/testbench: ~3.6
- squizlabs/php_codesniffer: ^2.8
Suggests
- rutorika/sortable: An alternative version of the 'Orderable' behavior that inspired Smoothie's implementation.
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_at
和updated_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
(多亏了显式别名)。
注意:您不能为另一个别名(显式或隐式)设置别名。别名仅用于实际的列名。
冲突和优先级
如果别名与实际的列名冲突,则它将具有优先级。这意味着在上述示例中,如果表中实际上有一个名为desc
或description
的列,您将无法再访问它。但是,您仍然可以为此列定义另一个别名。
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
添加到 $users2
的 friends
将会隐式地将 $user2
添加到 $user1
的 friends
中
$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 个关系。
此外,请注意,关系的定义是独立的:这里没有任何东西表明 projects
和 roles
互相关联。魔法只会因为它们被定义为“多对多”关系并且它们使用相同的枢轴表而发生。
查询关系
总的来说,多对多关系的行为与多对多关系完全相同。但是有两个区别。
第一个区别是多对多关系将返回“折叠”的(即去重的)结果。例如,如果$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();
注意:对于非多对多关系或“非约束”的多对多关系,
withAll
是with
的别名
$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
的命名为project
的BelongsTo
关系, - 从连接表到
Role
的命名为role
的BelongsTo
关系。
您可以使用任何常规关系查询这些关系,甚至可以预加载它们。
$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
作用域来取消此作用域的效果。)
可以使用previous
和next
方法查询前一个和下一个模型
$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
关系,包括 $thisdescendants
:与子代的BelongsToMany
关系descendantsWithSelf
:与子代的BelongsToMany
关系,包括 $this
⚠️ 注意:
ancestors
和descendants
(以及-WithSelf
)关系是只读的!尝试在这些关系上使用attach
或detach
方法将抛出异常。
可以按深度(即直接父/子代)对 ancestors
和 descendants
关系进行排序
$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); } } }
当然,ancestors
和 parent
关系也是如此。
您可以使用此方法检索整个树
$tags = Tag::getTree();
它将返回一个包含根元素的集合,每个元素都将预加载 children
关系,直到叶子节点。
方法
该特性定义了以下方法
isRoot()
:如果项的parent_id
为null
,则返回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')
:此作用域仅在查询ancestors
或descendants
关系时才有效(请参阅下面的示例)。
$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
及其变体、pluck
、count
和all
。 - 默认情况下,其他查询不会被缓存,但可以通过将
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
方法时,将发生以下操作
- 从缓存(或从数据库存储并存储在缓存中,如果它之前为空)中检索包含整个表内容的集合。
- 将查询的所有
where
和orderBy
子句应用于集合(使用where
和sortBy
方法)。 - 返回过滤和排序后的集合。
步骤2仅在以下条件下才会工作
- 所有
where
和orderBy
子句都可以转换为对集合的调用方法。这排除了更复杂的子句,例如原始SQL子句、通过OR
运算符连接的WHERE
子句或使用LIKE
运算符的子句。 - 查询中不得应用其他子句(如
having
、groupBy
或with
),因为它们不可翻译。
⚠️ 如果您使用了不可翻译的子句并且仍然启用了缓存,不会抛出异常,但这些子句将被忽略,查询将返回意外的结果。
缓存关系
由于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(); } }