Eloquent ORM
- 介绍
- 基本用法
- 批量赋值
- 插入、更新、删除
- 软删除
- 时间戳
- 查询范围
- 全局范围
- 关系
- 查询关系
- 预加载
- 插入关联模型
- 更新父级时间戳
- 处理中间表
- 集合
- 访问器和修改器
- 日期修改器
- 属性类型转换
- 模型事件
- 模型观察者
- 模型URL生成
- 转换为数组/JSON
介绍
Laravel自带的Eloquent ORM提供了一种优雅、简单的ActiveRecord实现,用于与数据库交互。每个数据库表都有一个对应的“模型”,用于与该表交互。
在开始之前,请确保在config/database.php
中配置了数据库连接。
基本用法
要开始使用,请创建一个Eloquent模型。模型通常位于app
目录中,但您可以根据composer.json
文件的自动加载规则将它们放置在任何地方。所有Eloquent模型都继承自Illuminate\Database\Eloquent\Model
。
定义一个Eloquent模型
class User extends Model {}
您还可以使用make:model
命令生成Eloquent模型:
php artisan make:model User
注意,我们没有告诉Eloquent我们的User
模型使用哪个表。类名的“蛇形命名法”复数形式将被用作表名,除非显式指定了其他名称。因此,在这种情况下,Eloquent将假定User
模型存储在users
表中。您可以通过在模型上定义一个table
属性来指定自定义表:
class User extends Model {
protected $table = 'my_users';
}
Eloquent还假定每个表都有一个名为id
的主键列。您可以定义一个primaryKey
属性来覆盖此约定。同样,您可以定义一个connection
属性来覆盖使用模型时应使用的数据库连接名称。
一旦定义了模型,您就可以开始在表中检索和创建记录。请注意,默认情况下,您需要在表中放置updated_at
和created_at
列。如果您不希望自动维护这些列,请将模型上的$timestamps
属性设置为false
。
检索所有记录
$users = User::all();
通过主键检索记录
$user = User::find(1);
var_dump($user->name);
查询构建器上可用的所有方法在查询Eloquent模型时也可用。
通过主键检索模型或抛出异常
有时您可能希望在找不到模型时抛出异常。为此,您可以使用firstOrFail
方法:
$model = User::findOrFail(1);
$model = User::where('votes', '>', 100)->firstOrFail();
这样做将允许您捕获异常,以便根据需要记录和显示错误页面。要捕获ModelNotFoundException
,请在app/Exceptions/Handler.php
文件中添加一些逻辑。
use Illuminate\Database\Eloquent\ModelNotFoundException;
class Handler extends ExceptionHandler {
public function render($request, Exception $e)
{
if ($e instanceof ModelNotFoundException)
{
// 自定义逻辑处理模型未找到...
}
return parent::render($request, $e);
}
}
使用Eloquent模型查询
$users = User::where('votes', '>', 100)->take(10)->get();
foreach ($users as $user)
{
var_dump($user->name);
}
Eloquent聚合
当然,您也可以使用查询构建器的聚合函数。
$count = User::where('votes', '>', 100)->count();
如果您无法通过流畅的接口生成所需的查询,请随意使用whereRaw
:
$users = User::whereRaw('age > ? and votes = 100', [25])->get();
分块结果
如果您需要处理大量(成千上万)Eloquent记录,使用chunk
命令将允许您在不耗尽所有RAM的情况下进行处理:
User::chunk(200, function($users)
{
foreach ($users as $user)
{
//
}
});
传递给方法的第一个参数是您希望每个“块”接收的记录数。作为第二个参数传递的闭包将在从数据库中提取的每个块上调用。
指定查询连接
您还可以指定在运行Eloquent查询时应使用哪个数据库连接。只需使用on
方法:
$user = User::on('connection-name')->find(1);
如果您正在使用读/写连接,您可以使用以下方法强制查询使用“写”连接:
$user = User::onWriteConnection()->find(1);
批量赋值
在创建新模型时,您将属性数组传递给模型构造函数。然后,这些属性通过批量赋值分配给模型。这很方便;然而,当盲目地将用户输入传递给模型时,可能会成为一个严重的安全问题。如果用户输入被盲目地传递给模型,用户可以自由修改模型的任何和所有属性。出于这个原因,所有Eloquent模型默认保护批量赋值。
要开始,请在模型上设置fillable
或guarded
属性。
在模型上定义可填充属性
fillable
属性指定哪些属性应该是可批量赋值的。这可以在类或实例级别设置。
class User extends Model {
protected $fillable = ['first_name', 'last_name', 'email'];
}
在此示例中,只有列出的三个属性将是可批量赋值的。
在模型上定义受保护属性
fillable
的反义词是guarded
,它充当“黑名单”而不是“白名单”:
class User extends Model {
protected $guarded = ['id', 'password'];
}
使用guarded
时,您仍然不应将Input::get()
或任何用户控制的原始数组传递给save
或update
方法,因为任何未受保护的列都可能被更新。
阻止所有属性的批量赋值
在上面的示例中,id
和password
属性不能被批量赋值。所有其他属性将是可批量赋值的。您还可以使用guard属性阻止所有属性的批量赋值:
protected $guarded = ['*'];
插入、更新、删除
要从模型中在数据库中创建新记录,只需创建一个新模型实例并调用save
方法。
保存新模型
$user = new User;
$user->name = 'John';
$user->save();
通常,您的Eloquent模型将具有自动递增的键。但是,如果您希望指定自己的键,请将模型上的incrementing
属性设置为false
。
您还可以使用create
方法在一行中保存新模型。插入的模型实例将从方法中返回给您。然而,在这样做之前,您需要在模型上指定fillable
或guarded
属性,因为所有Eloquent模型默认保护批量赋值。
在保存或创建使用自动递增ID的新模型后,您可以通过访问对象的id
属性来检索ID:
$insertedId = $user->id;
在模型上设置受保护属性
class User extends Model {
protected $guarded = ['id', 'account_id'];
}
使用模型创建方法
// 在数据库中创建一个新用户...
$user = User::create(['name' => 'John']);
// 通过属性检索用户,如果不存在则创建它...
$user = User::firstOrCreate(['name' => 'John']);
// 通过属性检索用户,如果不存在则实例化一个新实例...
$user = User::firstOrNew(['name' => 'John']);
更新检索到的模型
要更新模型,您可以检索它,改变一个属性,然后使用save
方法:
$user = User::find(1);
$user->email = 'john@foo.com';
$user->save();
保存模型和关系
有时您可能希望不仅保存模型,还保存其所有关系。为此,您可以使用push
方法:
$user->push();
您还可以对一组模型运行更新查询:
$affectedRows = User::where('votes', '>', 100)->update(['status' => 2]);
在通过Eloquent查询构建器更新一组模型时,不会触发任何模型事件。
删除现有模型
要删除模型,只需在实例上调用delete
方法:
$user = User::find(1);
$user->delete();
通过键删除现有模型
User::destroy(1);
User::destroy([1, 2, 3]);
User::destroy(1, 2, 3);
当然,您也可以对一组模型运行删除查询:
$affectedRows = User::where('votes', '>', 100)->delete();
仅更新模型的时间戳
如果您只想更新模型的时间戳,可以使用touch
方法:
$user->touch();
软删除
在软删除模型时,它实际上并没有从数据库中删除。相反,记录上会设置一个deleted_at
时间戳。要为模型启用软删除,请将SoftDeletes
应用于模型:
use Illuminate\Database\Eloquent\SoftDeletes;
class User extends Model {
use SoftDeletes;
protected $dates = ['deleted_at'];
}
要向表中添加deleted_at
列,您可以使用迁移中的softDeletes
方法:
$table->softDeletes();
现在,当您在模型上调用delete
方法时,deleted_at
列将被设置为当前时间戳。在查询使用软删除的模型时,“已删除”的模型将不会包含在查询结果中。
强制软删除模型进入结果
要强制软删除的模型出现在结果集中,请在查询中使用withTrashed
方法:
$users = User::withTrashed()->where('account_id', 1)->get();
withTrashed
方法可以在定义的关系上使用:
$user->posts()->withTrashed()->get();
如果您希望仅在结果中接收软删除的模型,可以使用onlyTrashed
方法:
$users = User::onlyTrashed()->where('account_id', 1)->get();
要将软删除的模型恢复为活动状态,请使用restore
方法:
$user->restore();
您还可以在查询上使用restore
方法:
User::withTrashed()->where('account_id', 1)->restore();
与withTrashed
一样,restore
方法也可以在关系上使用:
$user->posts()->restore();
如果您希望真正从数据库中删除模型,可以使用forceDelete
方法:
$user->forceDelete();
forceDelete
方法也适用于关系:
$user->posts()->forceDelete();
要确定给定的模型实例是否已被软删除,可以使用trashed
方法:
if ($user->trashed())
{
//
}
时间戳
默认情况下,Eloquent会自动维护数据库表上的created_at
和updated_at
列。只需将这些timestamp
列添加到您的表中,Eloquent将负责其余的工作。如果您不希望Eloquent维护这些列,请在模型中添加以下属性:
禁用自动时间戳
class User extends Model {
protected $table = 'users';
public $timestamps = false;
}
提供自定义时间戳格式
如果您希望自定义时间戳的格式,可以在模型中重写getDateFormat
方法:
class User extends Model {
protected function getDateFormat()
{
return 'U';
}
}
查询范围
定义查询范围
范围允许您轻松地在模型中重用查询逻辑。要定义范围,只需在模型方法前加上scope
前缀:
class User extends Model {
public function scopePopular($query)
{
return $query->where('votes', '>', 100);
}
public function scopeWomen($query)
{
return $query->whereGender('W');
}
}
使用查询范围
$users = User::popular()->women()->orderBy('created_at')->get();
动态范围
有时您可能希望定义一个接受参数的范围。只需将参数添加到范围函数中:
class User extends Model {
public function scopeOfType($query, $type)
{
return $query->whereType($type);
}
}
然后将参数传递给范围调用:
$users = User::ofType('member')->get();
全局范围
有时您可能希望定义一个适用于模型上所有查询的范围。从本质上讲,这就是Eloquent自己的“软删除”功能的工作方式。全局范围是使用PHP特性和Illuminate\Database\Eloquent\ScopeInterface
的实现组合定义的。
首先,让我们定义一个特性。对于这个例子,我们将使用Laravel附带的SoftDeletes
:
trait SoftDeletes {
/**
* 为模型引导软删除特性。
*
* @return void
*/
public static function bootSoftDeletes()
{
static::addGlobalScope(new SoftDeletingScope);
}
}
如果Eloquent模型使用了一个具有与bootNameOfTrait
命名约定匹配的方法的特性,则在Eloquent模型引导时将调用该特性方法,给您一个机会来注册全局范围,或执行其他任何您想要的操作。范围必须实现ScopeInterface
,该接口指定了两个方法:apply
和remove
。
apply
方法接收一个Illuminate\Database\Eloquent\Builder
查询构建器对象和应用的Model
,负责添加范围希望添加的任何附加where
子句。remove
方法也接收一个Builder
对象和Model
,负责逆转apply
所采取的操作。换句话说,remove
应该删除apply
添加的where
子句(或任何其他子句)。因此,对于我们的SoftDeletingScope
,方法看起来像这样:
/**
* 将范围应用于给定的Eloquent查询构建器。
*
* @param \Illuminate\Database\Eloquent\Builder $builder
* @param \Illuminate\Database\Eloquent\Model $model
* @return void
*/
public function apply(Builder $builder, Model $model)
{
$builder->whereNull($model->getQualifiedDeletedAtColumn());
$this->extend($builder);
}
/**
* 从给定的Eloquent查询构建器中移除范围。
*
* @param \Illuminate\Database\Eloquent\Builder $builder
* @param \Illuminate\Database\Eloquent\Model $model
* @return void
*/
public function remove(Builder $builder, Model $model)
{
$column = $model->getQualifiedDeletedAtColumn();
$query = $builder->getQuery();
foreach ((array) $query->wheres as $key => $where)
{
// 如果where子句是软删除日期约束,我们将从查询中移除它并重置wheres上的键。这允许开发人员在懒加载的关系结果集中包含已删除的模型。
if ($this->isSoftDeleteConstraint($where, $column))
{
unset($query->wheres[$key]);
$query->wheres = array_values($query->wheres);
}
}
}
关系
当然,您的数据库表可能彼此相关。例如,博客文章可能有很多评论,或者订单可能与下订单的用户相关。Eloquent使管理和处理这些关系变得容易。Laravel支持多种类型的关系:
一对一
定义一对一关系
一对一关系是非常基本的关系。例如,User
模型可能有一个Phone
。我们可以在Eloquent中定义这种关系:
class User extends Model {
public function phone()
{
return $this->hasOne('App\Phone');
}
}
传递给hasOne
方法的第一个参数是相关模型的名称。一旦定义了关系,我们可以使用Eloquent的动态属性检索它:
$phone = User::find(1)->phone;
此语句执行的SQL如下:
select * from users where id = 1
select * from phones where user_id = 1
请注意,Eloquent根据模型名称假定关系的外键。在这种情况下,Phone
模型被假定使用user_id
外键。如果您希望覆盖此约定,可以将第二个参数传递给hasOne
方法。此外,您可以将第三个参数传递给该方法,以指定用于关联的本地列:
return $this->hasOne('App\Phone', 'foreign_key');
return $this->hasOne('App\Phone', 'foreign_key', 'local_key');
定义关系的反向
要在Phone
模型上定义关系的反向,我们使用belongsTo
方法:
class Phone extends Model {
public function user()
{
return $this->belongsTo('App\User');
}
}
在上面的示例中,Eloquent将在phones
表上查找user_id
列。如果您希望定义不同的外键列,可以将其作为第二个参数传递给belongsTo
方法:
class Phone extends Model {
public function user()
{
return $this->belongsTo('App\User', 'local_key');
}
}
此外,您可以传递第三个参数,指定父表上关联列的名称:
class Phone extends Model {
public function user()
{
return $this->belongsTo('App\User', 'local_key', 'parent_key');
}
}
一对多
一对多关系的一个例子是博客文章“有很多”评论。我们可以这样建模这种关系:
class Post extends Model {
public function comments()
{
return $this->hasMany('App\Comment');
}
}
现在我们可以通过动态属性访问文章的评论:
$comments = Post::find(1)->comments;
如果您需要为检索到的评论添加进一步的约束,可以调用comments
方法并继续链接条件:
$comments = Post::find(1)->comments()->where('title', '=', 'foo')->first();
同样,您可以通过将第二个参数传递给hasMany
方法来覆盖常规外键。并且,像hasOne
关系一样,本地列也可以指定:
return $this->hasMany('App\Comment', 'foreign_key');
return $this->hasMany('App\Comment', 'foreign_key', 'local_key');
定义关系的反向
要在Comment
模型上定义关系的反向,我们使用belongsTo
方法:
class Comment extends Model {
public function post()
{
return $this->belongsTo('App\Post');
}
}
多对多
多对多关系是一种更复杂的关系类型。此类关系的一个例子是具有多个角色的用户,其中角色也由其他用户共享。例如,许多用户可能具有“管理员”角色。此关系需要三个数据库表:users
、roles
和role_user
。role_user
表是从相关模型名称的字母顺序派生的,应该有user_id
和role_id
列。
我们可以使用belongsToMany
方法定义多对多关系:
class User extends Model {
public function roles()
{
return $this->belongsToMany('App\Role');
}
}
现在,我们可以通过User
模型检索角色:
$roles = User::find(1)->roles;
如果您希望为枢纽表使用非常规的表名,可以将其作为第二个参数传递给belongsToMany
方法:
return $this->belongsToMany('App\Role', 'user_roles');
您还可以覆盖常规关联键:
return $this->belongsToMany('App\Role', 'user_roles', 'user_id', 'foo_id');
当然,您还可以在Role
模型上定义关系的反向:
class Role extends Model {
public function users()
{
return $this->belongsToMany('App\User');
}
}
通过关系
“通过关系”提供了一种方便的快捷方式,通过中间关系访问远程关系。例如,Country
模型可能通过User
模型拥有许多Post
。此关系的表如下所示:
countries
id - integer
name - string
users
id - integer
country_id - integer
name - string
posts
id - integer
user_id - integer
title - string
即使posts
表不包含country_id
列,hasManyThrough
关系将允许我们通过$country->posts
访问一个国家的帖子。让我们定义关系:
class Country extends Model {
public function posts()
{
return $this->hasManyThrough('App\Post', 'App\User');
}
}
如果您希望手动指定关系的键,可以将它们作为方法的第三个和第四个参数传递:
class Country extends Model {
public function posts()
{
return $this->hasManyThrough('App\Post', 'App\User', 'country_id', 'user_id');
}
}
多态关系
多态关系允许一个模型属于多个其他模型,基于单个关联。例如,您可能有一个照片模型,它属于员工模型或订单模型。我们可以这样定义这种关系:
class Photo extends Model {
public function imageable()
{
return $this->morphTo();
}
}
class Staff extends Model {
public function photos()
{
return $this->morphMany('App\Photo', 'imageable');
}
}
class Order extends Model {
public function photos()
{
return $this->morphMany('App\Photo', 'imageable');
}
}
检索多态关系
现在,我们可以检索员工或订单的照片:
$staff = Staff::find(1);
foreach ($staff->photos as $photo)
{
//
}
检索多态关系的所有者
然而,真正的“多态”魔法是在您从Photo
模型访问员工或订单时:
$photo = Photo::find(1);
$imageable = $photo->imageable;
Photo
模型上的imageable
关系将返回Staff
或Order
实例,具体取决于哪个类型的模型拥有该照片。
多态关系表结构
为了帮助理解其工作原理,让我们探索多态关系的数据库结构:
staff
id - integer
name - string
orders
id - integer
price - integer
photos
id - integer
path - string
imageable_id - integer
imageable_type - string
这里需要注意的关键字段是photos
表上的imageable_id
和imageable_type
。ID将包含在此示例中拥有的员工或订单的ID值,而类型将包含拥有模型的类名。这就是ORM在访问imageable
关系时确定返回哪种类型的拥有模型的方式。
多对多多态关系
多对多多态关系表结构
除了传统的多态关系,您还可以指定多对多多态关系。例如,博客Post
和Video
模型可以共享与Tag
模型的多态关系。首先,让我们检查表结构:
posts
id - integer
name - string
videos
id - integer
name - string
tags
id - integer
name - string
taggables
tag_id - integer
taggable_id - integer
taggable_type - string
接下来,我们准备在模型上设置关系。Post
和Video
模型都将通过tags
方法具有morphToMany
关系:
class Post extends Model {
public function tags()
{
return $this->morphToMany('App\Tag', 'taggable');
}
}
Tag
模型可以为其每个关系定义一个方法:
class Tag extends Model {
public function posts()
{
return $this->morphedByMany('App\Post', 'taggable');
}
public function videos()
{
return $this->morphedByMany('App\Video', 'taggable');
}
}
查询关系
在选择时查询关系
在访问模型的记录时,您可能希望根据关系的存在限制结果。例如,您希望提取所有至少有一个评论的博客文章。为此,您可以使用has
方法:
$posts = Post::has('comments')->get();
您还可以指定一个运算符和一个计数:
$posts = Post::has('comments', '>=', 3)->get();
嵌套的has
语句也可以使用“点”符号构造:
$posts = Post::has('comments.votes')->get();
如果您需要更强大的功能,可以使用whereHas
和orWhereHas
方法在has
查询上放置“where”条件:
$posts = Post::whereHas('comments', function($q)
{
$q->where('content', 'like', 'foo%');
})->get();
动态属性
Eloquent允许您通过动态属性访问关系。Eloquent会自动为您加载关系,甚至足够智能地知道是调用get
(对于一对多关系)还是first
(对于一对一关系)方法。然后可以通过与关系同名的动态属性访问它。例如,使用以下模型$phone
:
class Phone extends Model {
public function user()
{
return $this->belongsTo('App\User');
}
}
$phone = Phone::find(1);
而不是像这样回显用户的电子邮件:
echo $phone->user()->first()->email;
可以简化为:
echo $phone->user->email;
返回多个结果的关系将返回Illuminate\Database\Eloquent\Collection
类的实例。
预加载
预加载存在是为了缓解N + 1查询问题。例如,考虑一个与Author
相关的Book
模型。关系定义如下:
class Book extends Model {
public function author()
{
return $this->belongsTo('App\Author');
}
}
现在,考虑以下代码:
foreach (Book::all() as $book)
{
echo $book->author->name;
}
此循环将执行1个查询以检索表上的所有书籍,然后为每本书执行另一个查询以检索作者。因此,如果我们有25本书,此循环将运行26个查询。
幸运的是,我们可以使用预加载来大大减少查询的数量。应预加载的关系可以通过with
方法指定:
foreach (Book::with('author')->get() as $book)
{
echo $book->author->name;
}
在上面的循环中,只会执行两个查询:
select * from books
select * from authors where id in (1, 2, 3, 4, 5, ...)
明智地使用预加载可以大大提高应用程序的性能。
当然,您可以一次预加载多个关系:
$books = Book::with('author', 'publisher')->get();
您甚至可以预加载嵌套关系:
$books = Book::with('author.contacts')->get();
在上面的示例中,将预加载author
关系,并且还将加载作者的contacts
关系。
预加载约束
有时您可能希望预加载关系,但也指定预加载的条件。以下是一个示例:
$users = User::with(['posts' => function($query)
{
$query->where('title', 'like', '%first%');
}])->get();
在此示例中,我们正在预加载用户的帖子,但仅当帖子的标题列包含“first”一词时。
当然,预加载闭包不限于“约束”。您还可以应用排序:
$users = User::with(['posts' => function($query)
{
$query->orderBy('created_at', 'desc');
}])->get();
延迟预加载
也可以直接从已经存在的模型集合中预加载相关模型。这在动态决定是否加载相关模型或与缓存结合使用时可能很有用。
$books = Book::all();
$books->load('author', 'publisher');
您还可以传递一个闭包来设置查询的约束:
$books->load(['author' => function($query)
{
$query->orderBy('published_date', 'asc');
}]);
插入关联模型
附加关联模型
您经常需要插入新的关联模型。例如,您可能希望为帖子插入新评论。与其手动设置模型上的post_id
外键,您可以直接从其父Post
模型插入新评论:
$comment = new Comment(['message' => 'A new comment.']);
$post = Post::find(1);
$comment = $post->comments()->save($comment);
在此示例中,post_id
字段将自动设置在插入的评论上。
如果您需要保存多个关联模型:
$comments = [
new Comment(['message' => 'A new comment.']),
new Comment(['message' => 'Another comment.']),
new Comment(['message' => 'The latest comment.'])
];
$post = Post::find(1);
$post->comments()->saveMany($comments);
关联模型(属于)
在更新belongsTo
关系时,您可以使用associate
方法。此方法将在子模型上设置外键:
$account = Account::find(10);
$user->account()->associate($account);
$user->save();
插入关联模型(多对多)
在处理多对多关系时,您还可以插入关联模型。让我们继续使用我们的User
和Role
模型作为示例。我们可以使用attach
方法轻松地将新角色附加到用户:
附加多对多模型
$user = User::find(1);
$user->roles()->attach(1);
您还可以传递一个应存储在关系的枢纽表上的属性数组:
$user->roles()->attach(1, ['expires' => $expires]);
当然,attach
的反义词是detach
:
$user->roles()->detach(1);
attach
和detach
都接受ID数组作为输入:
$user = User::find(1);
$user->roles()->detach([1, 2, 3]);
$user->roles()->attach([1 => ['attribute1' => 'value1'], 2, 3]);
使用Sync附加多对多模型
您还可以使用sync
方法附加关联模型。sync
方法接受一个要放置在枢纽表上的ID数组。此操作完成后,只有数组中的ID将在模型的中间表上:
$user->roles()->sync([1, 2, 3]);
在同步时添加枢纽数据
您还可以将其他枢纽表值与给定的ID关联:
$user->roles()->sync([1 => ['expires' => true]]);
有时您可能希望在单个命令中创建一个新的关联模型并将其附加。对于此操作,您可以使用save
方法:
$role = new Role(['name' => 'Editor']);
User::find(1)->roles()->save($role);
在此示例中,新Role
模型将被保存并附加到用户模型。您还可以传递一个属性数组以放置在此操作的连接表上:
User::find(1)->roles()->save($role, ['expires' => $expires]);
更新父级时间戳
当一个模型belongsTo
另一个模型时,例如一个Comment
属于一个Post
,在更新子模型时更新父级的时间戳通常是有帮助的。例如,当更新Comment
模型时,您可能希望自动触摸拥有的Post
的updated_at
时间戳。Eloquent使这变得容易。只需在子模型中添加一个touches
属性,其中包含关系的名称:
class Comment extends Model {
protected $touches = ['post'];
public function post()
{
return $this->belongsTo('App\Post');
}
}
现在,当您更新Comment
时,拥有的Post
将更新其updated_at
列:
$comment = Comment::find(1);
$comment->text = 'Edit to this comment!';
$comment->save();
处理中间表
正如您已经了解到的,处理多对多关系需要中间表的存在。Eloquent提供了一些非常有用的方法来与此表交互。例如,假设我们的User
对象有许多与之相关的Role
对象。在访问此关系后,我们可以访问模型上的pivot
表:
$user = User::find(1);
foreach ($user->roles as $role)
{
echo $role->pivot->created_at;
}
请注意,我们检索到的每个Role
模型都会自动分配一个pivot
属性。此属性包含一个表示中间表的模型,并可以像任何其他Eloquent模型一样使用。
默认情况下,只有键会出现在pivot
对象上。如果您的枢纽表包含额外的属性,您必须在定义关系时指定它们:
return $this->belongsToMany('App\Role')->withPivot('foo', 'bar');
现在,foo
和bar
属性将在Role
模型的pivot
对象上可访问。
如果您希望枢纽表自动维护created_at
和updated_at
时间戳,请在关系定义中使用withTimestamps
方法:
return $this->belongsToMany('App\Role')->withTimestamps();
删除枢纽表上的记录
要删除模型的枢纽表上的所有记录,可以使用detach
方法:
User::find(1)->roles()->detach();
请注意,此操作不会从roles
表中删除记录,而只会从枢纽表中删除。
更新枢纽表上的记录
有时您可能需要更新枢纽表,但不分离它。如果您希望就地更新枢纽表,可以使用updateExistingPivot
方法,如下所示:
User::find(1)->roles()->updateExistingPivot($roleId, $attributes);
定义自定义枢纽模型
Laravel还允许您定义自定义枢纽模型。要定义自定义模型,首先创建一个扩展Eloquent
的“基础”模型类。在其他Eloquent模型中,扩展此自定义基础模型而不是默认的Eloquent
基础。在基础模型中,添加以下函数以返回自定义枢纽模型的实例:
public function newPivot(Model $parent, array $attributes, $table, $exists)
{
return new YourCustomPivot($parent, $attributes, $table, $exists);
}
集合
所有通过Eloquent返回的多结果集,无论是通过get
方法还是通过relationship
,都将返回一个集合对象。此对象实现了IteratorAggregate
PHP接口,因此可以像数组一样迭代。然而,此对象还具有多种其他有用的方法来处理结果集。
检查集合是否包含键
例如,我们可以使用contains
方法确定结果集中是否包含给定的主键:
$roles = User::find(1)->roles;
if ($roles->contains(2))
{
//
}
集合也可以转换为数组或JSON:
$roles = User::find(1)->roles->toArray();
$roles = User::find(1)->roles->toJson();
如果集合被转换为字符串,它将作为JSON返回:
$roles = (string) User::find(1)->roles;
迭代集合
Eloquent集合还包含一些有用的方法,用于循环和过滤它们包含的项目:
$roles = $user->roles->each(function($role)
{
//
});
过滤集合
在过滤集合时,提供的回调将用作array_filter的回调。
$users = $users->filter(function($user)
{
return $user->isAdmin();
});
在过滤集合并将其转换为JSON时,请尝试先调用values
函数以重置数组的键。
对每个集合对象应用回调
$roles = User::find(1)->roles;
$roles->each(function($role)
{
//
});
按值排序集合
$roles = $roles->sortBy(function($role)
{
return $role->created_at;
});
$roles = $roles->sortByDesc(function($role)
{
return $role->created_at;
});
按值排序集合
$roles = $roles->sortBy('created_at');
$roles = $roles->sortByDesc('created_at');
返回自定义集合类型
有时,您可能希望返回具有自己添加方法的自定义集合对象。您可以通过覆盖Eloquent模型上的newCollection
方法来指定这一点:
class User extends Model {
public function newCollection(array $models = [])
{
return new CustomCollection($models);
}
}
访问器和修改器
定义访问器
Eloquent提供了一种方便的方法来在获取或设置模型属性时转换它们。只需在模型上定义一个getFooAttribute
方法即可声明访问器。请记住,方法应遵循驼峰命名法,即使您的数据库列是蛇形命名法:
class User extends Model {
public function getFirstNameAttribute($value)
{
return ucfirst($value);
}
}
在上面的示例中,first_name
列有一个访问器。请注意,属性的值会传递给访问器。
定义修改器
修改器的声明方式类似:
class User extends Model {
public function setFirstNameAttribute($value)
{
$this->attributes['first_name'] = strtolower($value);
}
}
日期修改器
默认情况下,Eloquent会将created_at
和updated_at
列转换为Carbon的实例,Carbon提供了一系列有用的方法,并扩展了原生PHP的DateTime
类。
您可以自定义哪些字段会自动修改,甚至完全禁用此修改,方法是覆盖模型的getDates
方法:
public function getDates()
{
return ['created_at'];
}
当列被视为日期时,您可以将其值设置为UNIX时间戳、日期字符串(Y-m-d
)、日期时间字符串,当然还有DateTime
/ Carbon
实例。
要完全禁用日期修改,只需从getDates
方法返回一个空数组:
public function getDates()
{
return [];
}
属性类型转换
如果您有一些属性希望始终转换为其他数据类型,可以将属性添加到模型的casts
属性中。否则,您将不得不为每个属性定义一个修改器,这可能会耗费时间。以下是使用casts
属性的示例:
/**
* 应该转换为本机类型的属性。
*
* @var array
*/
protected $casts = [
'is_admin' => 'boolean',
];
现在,即使底层值在数据库中存储为整数,is_admin
属性在访问时也将始终转换为布尔值。其他支持的转换类型有:integer
、real
、float
、double
、string
、boolean
、object
和array
。
array
转换对于处理存储为序列化JSON的列特别有用。例如,如果您的数据库有一个包含序列化JSON的TEXT类型字段,将array
转换添加到该属性将自动在访问Eloquent模型时将属性反序列化为PHP数组:
/**
* 应该转换为本机类型的属性。
*
* @var array
*/
protected $casts = [
'options' => 'array',
];
现在,当您使用Eloquent模型时:
$user = User::find(1);
// $options是一个数组...
$options = $user->options;
// options会自动序列化回JSON...
$user->options = ['foo' => 'bar'];
模型事件
Eloquent 模型会触发多个事件,允许你在模型生命周期的各个点进行挂钩,使用以下方法:creating
、created
、updating
、updated
、saving
、saved
、deleting
、deleted
、restoring
、restored
。
每当一个新项目第一次被保存时,creating
和 created
事件将被触发。如果项目不是新的并且调用了 save
方法,则会触发 updating
/ updated
事件。在这两种情况下,saving
/ saved
事件都会被触发。
通过事件取消保存操作
如果 creating
、updating
、saving
或 deleting
事件返回 false
,操作将被取消:
User::creating(function($user)
{
if ( ! $user->isValid()) return false;
});
在哪里注册事件监听器
你的 EventServiceProvider
是一个方便的地方来注册你的模型事件绑定。例如:
/**
* 为应用程序注册其他事件。
*
* @param \Illuminate\Contracts\Events\Dispatcher $events
* @return void
*/
public function boot(DispatcherContract $events)
{
parent::boot($events);
User::creating(function($user)
{
//
});
}
模型观察者
为了整合模型事件的处理,你可以注册一个模型观察者。观察者类可以有与各种模型事件对应的方法。例如,creating
、updating
、saving
方法可以在观察者中,除此之外还有其他任何模型事件名称。
例如,一个模型观察者可能如下所示:
class UserObserver {
public function saving($model)
{
//
}
public function saved($model)
{
//
}
}
你可以使用 observe
方法注册一个观察者实例:
User::observe(new UserObserver);
模型 URL 生成
当你将一个模型传递给 route
或 action
方法时,它的主键会被插入到生成的 URI 中。例如:
Route::get('user/{user}', 'UserController@show');
action('UserController@show', [$user]);
在这个例子中,$user->id
属性将被插入到生成的 URL 的 {user}
占位符中。然而,如果你想使用 ID 以外的其他属性,你可以在模型上重写 getRouteKey
方法:
public function getRouteKey()
{
return $this->slug;
}
转换为数组 / JSON
将模型转换为数组
在构建 JSON API 时,你可能经常需要将你的模型和关系转换为数组或 JSON。因此,Eloquent 包含了用于执行此操作的方法。要将模型及其加载的关系转换为数组,你可以使用 toArray
方法:
$user = User::with('roles')->first();
return $user->toArray();
注意,整个模型集合也可以转换为数组:
return User::all()->toArray();
将模型转换为 JSON
要将模型转换为 JSON,你可以使用 toJson
方法:
return User::find(1)->toJson();
从路由返回模型
注意,当模型或集合被转换为字符串时,它将被转换为 JSON,这意味着你可以直接从应用程序的路由返回 Eloquent 对象!
Route::get('users', function()
{
return User::all();
});
隐藏数组或 JSON 转换中的属性
有时你可能希望限制模型的数组或 JSON 形式中包含的属性,例如密码。为此,请在模型中添加一个 hidden
属性定义:
class User extends Model {
protected $hidden = ['password'];
}
隐藏关系时,使用关系的方法名称,而不是动态访问器名称。
或者,你可以使用 visible
属性来定义一个白名单:
protected $visible = ['first_name', 'last_name'];
有时,你可能需要添加没有对应数据库列的数组属性。为此,只需为该值定义一个访问器:
public function getIsAdminAttribute()
{
return $this->attributes['admin'] == 'yes';
}
一旦你创建了访问器,只需将该值添加到模型的 appends
属性中:
protected $appends = ['is_admin'];
一旦属性被添加到 appends
列表中,它将在模型的数组和 JSON 形式中被包含。appends
数组中的属性遵循模型上的 visible
和 hidden
配置。