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,则返回trueisLeaf():检查项是否为叶子(即没有子代)hasChildren():$tag->hasChildren()与!$tag->isLeaf()类似,但更易读isChildOf($item),isParentOf($item),isDescendantOf($item),isAncestorOf($item),isSiblingOf($item),commonAncestorWith($item):返回两个项之间的第一个共同祖先,如果没有共同祖先(这可能在树有多个根时发生),则返回nulldistanceTo($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(); } }