Laravel Eloquent 使用总结

直奔主题!

使用技巧

递增和递减

要代替以下实现:

1
2
3
$article = Article::find($article_id);
$article->read_count++;
$article->save();

你可以这样做:

1
2
$article = Article::find($article_id);
$article->increment('read_count');

以下这些方法也可以实现:

1
2
3
Article::find($article_id)->increment('read_count');
Article::find($article_id)->increment('read_count', 10); // +10
Product::find($produce_id)->decrement('stock'); // -1

模型的 boot() 方法

在一个 Eloquent 模型中,有个神奇的地方,叫 boot(),在那里,你可以覆盖默认的行为:

1
2
3
4
5
6
7
8
9
10
11
12
class User extends Model
{
public static function boot()
{
parent::boot();
static::updating(function($model)
{
// 写点日志啥的
// 覆盖一些属性,类似这样 $model->something = transform($something);
});
}
}

在创建模型对象时设置某些字段的值,大概是最受欢迎的例子之一了。 一起来看看在创建模型对象时,你想要生成 UUID 字段 该怎么做。

1
2
3
4
5
6
7
public static function boot()
{
parent::boot();
self::creating(function ($model) {
$model->uuid = (string)Uuid::generate();
});
}

如果你想要 User::all() 总是按照 name 字段来排序呢? 你可以给它分配一个全局作用域。

1
2
3
4
5
6
7
8
9
protected static function boot()
{
parent::boot();

// 按照 name 正序排序
static::addGlobalScope('order', function (Builder $builder) {
$builder->orderBy('name', 'asc');
});
}

带条件与排序的关联关系

定义关联关系的一般方式:

1
2
3
public function users() {
return $this->hasMany('App\User');
}

你知道吗?也可以在上面的基础上增加 where 或者 orderBy
举个例子,如果你想关联某些类型的用户,同时使用 email 字段排序,你可以这样做:

1
2
3
public function approvedUsers() {
return $this->hasMany('App\User')->where('approved', 1)->orderBy('email');
}

WhereX

有一种优雅的方式能将这种代码:

1
$users = User::where('approved', 1)->get();

转换成这种:

1
$users = User::whereApproved(1)->get();

对,你没有看错,使用字段名作为后缀添加到 where 后面,它就能通过魔术方法运行了。

另外,在 Eloquent 里也有些和时间相关的预定义方法:

1
2
3
4
User::whereDate('created_at', date('Y-m-d'));
User::whereDay('created_at', date('d'));
User::whereMonth('created_at', date('m'));
User::whereYear('created_at', date('Y'));

通过关系排序

一个复杂一点的「技巧」。你想对论坛话题按最新发布的帖子来排序?论坛中最新更新的主题在最前面是很常见的需求,对吧?

首先,为主题的最新帖子定义一个单独的关系:

1
2
3
4
public function latestPost()
{
return $this->hasOne(\App\Post::class)->latest();
}

然后,在控制器中,我们可以实现这个「魔法」:

1
$users = Topic::with('latestPost')->get()->sortByDesc('latestPost.created_at');

一对多返回默认模型对象

假设现在有种情况是要显示文章的作者,然后模板代码是:

1
{{ $post->author->name }}

但是如果作者的信息被删除或者因为某些原因没有被设置。代码会返回一个错误,诸如 “property of non-object”。

当然你可以这样处理:

1
{{ $post->author->name ?? '' }}

你可以通过 Eloquent 关系这样做:

1
2
3
4
public function author()
{
return $this->belongsTo('App\Author')->withDefault();
}

在此示例中,如果文字没有作者的信息, author() 会返回一个空的 App\Author 模型对象。

再者,我们也可以给默认的模型对象里面的属性赋默认值。

1
2
3
4
5
6
public function author()
{
return $this->belongsTo('App\Author')->withDefault([
'name' => 'Guest Author'
]);
}

通过赋值函数进行排序

想象一下你有这样的代码:

1
2
3
4
function getFullNameAttribute()
{
return $this->attributes['first_name'] . ' ' . $this->attributes['last_name'];
}

现在,你想要通过 “full_name” 进行排序? 发现是没有效果的:

1
$clients = Client::orderBy('full_name')->get(); //没有效果

解决办法很简单.我们需要在获取结果后对结果进行排序.

1
$clients = Client::get()->sortBy('full_name'); // 成功!

注意的是方法名称是不相同的 – 它不是orderBy,而是sortBy

复制:复制一行的副本

很简单。说明不是很深入,下面是复制数据库实体(一条数据)的最佳方法:

1
2
3
$task = Tasks::find(1);
$newTask = $task->replicate();
$newTask->save();

Chunk() 方法之大块数据

与 Eloquent 不完全相关,它更多的关于 Collection (集合),但是对于处理大数据集合,仍然是很有用的。你可以使用 chunk() 将这些数据分割成小数据块

修改前:

1
2
3
4
$users = User::all();
foreach ($users as $user) {
// ...
}

你可以这样做:

1
2
3
4
5
User::chunk(100, function ($users) {
foreach ($users as $user) {
// ...
}
});

把括号转换成 Eloquent 查询

如果你有个 andor 混合的 SQL 查询,像这样子的:

1
... WHERE (gender = 'Male' and age >= 18) or (gender = 'Female' and age >= 65)

怎么用 Eloquent 来翻译它呢? 下面是一种错误的方式:

1
2
3
4
$q->where('gender', 'Male');
$q->orWhere('age', '>=', 18);
$q->where('gender', 'Female');
$q->orWhere('age', '>=', 65);

顺序就没对。正确的打开方式稍微复杂点,使用闭包作为子查询:

1
2
3
4
5
6
7
$q->where(function ($query) {
$query->where('gender', 'Male')
->where('age', '>=', 18);
})->orWhere(function($query) {
$query->where('gender', 'Female')
->where('age', '>=', 65);
});

复数参数的 orWhere

终于,你可以传递阵列参数给 orWhere()。平常的方式:

1
2
3
$q->where('a', 1);
$q->orWhere('b', 2);
$q->orWhere('c', 3);

你可以这样做:

1
2
$q->where('a', 1);
$q->orWhere(['b' => 2, 'c' => 3]);

Larvel 模型事件

Laravel 模型事件允许你监听模型生命周期内的多个关键点,甚至可以在阻止一个模型的保存或者删除。 Laravel 模型事件文档 概述了如何使用钩子将对应事件与相关的事件类型关联起来,但是本文的主旨是事件与监听器的构建与设置,并额外补充一些细节的说明。

事件概述

  • retrieved
  • creating
  • created
  • updating
  • updated
  • saving
  • saved
  • deleting
  • deleted
  • restoring
  • restored

注册事件

为了在你的模型中关联一个事件,你需要做的第一件事是使用 $dispatchesEvents属性去注册事件对象,这最终将通过 HasEvents::fireCustomModelEvent() 方法触发, 该方法将通过 fireModelEvent() 方法被调用。 fireCustomModelEvent() 方法原始的时候大致是下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 为给定的事件触发一个自定义模型。
*
* @param string $event
* @param string $method
* @return mixed|null
*/
protected function fireCustomModelEvent($event, $method)
{
if (! isset($this->dispatchesEvents[$event])) {
return;
}

$result = static::$dispatcher->$method(new $this->dispatchesEvents[$event]($this));

if (! is_null($result)) {
return $result;
}
}

一些事件,比如 delete, 将进行检测判断是否这个事件会返回 false 然后退出操作。比如,你可以使用这个钩子去做一些检测,也可以防止一个用户被创建或删除。

使用 App\User 模型举例, 这里展示了如何配置你的模型事件

1
2
3
protected $dispatchesEvents = [
'saving' => \App\Events\UserSaving::class,
];

artisan make:event 创建事件、

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php

namespace App\Events;

use App\User;
use Illuminate\Queue\SerializesModels;

class UserSaving
{
use SerializesModels;

public $user;

/**
* 创建一个新的事件实例
*
* @param \App\User $user
*/
public function __construct(User $user)
{
$this->user = $user;
}
}

我们的事件提供了一个公有的 $user 属性以便你能够在 saving事件期间访问 User模型实例。

为了让它工作起来下一步需要做的是为这个事件建立一个实际的监听器。我们设置好模型的触发时机,当 User模型触发 saving 事件,监听器就会被调。

创建事件监听器

现在,我们定义 User 模型并注册一个事件监听器来监听 saving 事件的触发。虽然,我能通过模型观察器快速实现,但是,我想引导你为单个事件触发配置事件监听器。

事件监听器就像 Laravel 其它事件监听一样,handle() 方法将接收 App\Events\UserSaving 事件类的一个实例。

你可以手动创建它,也可以使用 php artisan make:listener 命令。 不管怎么样,你都将创建一个像下面这样子监听类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php

namespace App\Listeners;

use App\Events\UserSaving as UserSavingEvent;

class UserSaving
{
/**
* 处理事件。
*
* @param \App\Events\UserSavingEvent $event
* @return mixed
*/
public function handle(UserSavingEvent $event)
{
app('log')->info($event->user);
}
}

只是添加了一个日志记录调用,以便于检查传递给监听器的模型。为此,我们还需要在 EventServiceProvider::$listen 属性中注册监听器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php

namespace App\Providers;

use Illuminate\Support\Facades\Event;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;

class EventServiceProvider extends ServiceProvider
{
/**
* 应用的事件监听器。
*
* @var array
*/
protected $listen = [
\App\Events\UserSaving::class => [
\App\Listeners\UserSaving::class,
],
];

// ...
}

现在,当模型调用 saving 事件时,我们注册的事件监听器也会被触发并执行。

事件监听

我们可以通过 tinker 会话快速生成事件监听代码:

1
2
3
4
5
6
7
8
9
php artisan tinker
>>> factory(\App\User::class)->create();
=> App\User {#794
name: "Aiden Cremin",
email: "josie05@example.com",
updated_at: "2018-03-15 03:57:18",
created_at: "2018-03-15 03:57:18",
id: 2,
}

如果你已正确注册了事件和监听器,则应该在 laravel.log 文件中可以看到该模型的 JSON 表达形式:

1
[2018-03-15 03:57:18] local.INFO: {"name":"Aiden Cremin","email":"josie05@example.com"}

要注意的一点,此时模型并没有 created_atupdated_at 属性。如果在模型上再次调用 save() ,日志上将会有一个带有时间戳的新记录,因为 saving 事件会在新创建的记录或现在有记录上触发:

1
2
3
4
5
6
7
8
9
10
11
>>> $u = factory(\App\User::class)->create();
=> App\User {#741
name: "Eloisa Hirthe",
email: "gottlieb.itzel@example.com",
updated_at: "2018-03-15 03:59:37",
created_at: "2018-03-15 03:59:37",
id: 3,
}
>>> $u->save();
=> true
>>>

停止一个保存操作

某些模型事件是允许你进行阻止操作的。举个荒谬的例子,假设我们不允许任何一个用户的模型保存其属性 $user->name 的内容为 Paul :

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 处理事件。
*
* @param \App\Events\UserSaving $event
* @return mixed
*/
public function handle(UserSaving $event)
{
if (stripos($event->user->name, 'paul') !== false) {
return false;
}
}

EloquentModel::save() 方法中,会根据事件监听的返回结果判断是否进行停止保存操作:

1
2
3
4
5
6
7
8
9
10
public function save(array $options = [])
{
$query = $this->newQueryWithoutScopes();

// 如果 "saving" 事件返回 false ,我们将退出保存并返回
// false,表示保存失败。这为服务监听者提供了一个机会,
// 当验证失败或者出现其它任何情况,都可以取消保存操作。
if ($this->fireModelEvent('saving') === false) {
return false;
}

这个 save() 是个很好的例子,它告诉了你如何在模型生命周期中自定义事件,以及被动执行日志数据记录或者任务调度。

观察者

如果你正在监听多个事件,那么你可能会发现使用观察者类来按类型分组存放事件会更加方便。这里是一个例子 Eloquent 观察者 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?php

namespace App\Observers;

use App\User;

class UserObserver
{
/**
* 监听 User 创建事件。
*
* @param \App\User $user
* @return void
*/
public function created(User $user)
{
//
}

/**
* 监听 User 删除事件。
*
* @param \App\User $user
* @return void
*/
public function deleting(User $user)
{
//
}
}

在服务提供者 AppServiceProvider 中的 boot() 方法里注册观察者。

1
2
3
4
5
6
7
8
9
/**
* 运行所有应用服务。
*
* @return void
*/
public function boot()
{
User::observe(UserObserver::class);
}

阅读 Laravel 事件文档 去了解有关事件和监听器在整个框架中如何工作。 Eloquent 事件文档 对于可用事件以及如何使用观察者是一个非常好的参考。最后,我建议浏览 Illuminate\Database\Eloquent\Model 类来查找 fireModelEvent() 方法调用的用法去了解事件如何与模型和 HasEvents trait 将这些事件联系在一起。

参考

  1. Laravel 模型事件入门
  2. Laravel Eloquent 必备的实用技巧