Laravel Database-Eloquent Model源码分析(上)

思考并回答以下问题:

前言

前面几个博客向大家介绍了查询构造器的原理与源码,然而查询构造器更多是为Eloquent Model服务的,我们对数据库操作更加方便的是使用Eloquent Model。本篇文章将会大家介绍Model的一些特性原理。

Eloquent Model修改器

简介

当你在Eloquent模型实例中获取或设置某些属性值的时候,访问器和修改器允许你对Eloquent属性值进行格式化。例如,你可能需要使用Laravel加密器来加密保存在数据库中的值,而在使用Eloquent模型访问该属性的时候自动进行解密其值。

除了自定义访问器和修改器外,Eloquent也会自动将日期字段类型转换为Carbon实例,或将文本类型转换为JSON。

访问器&修改器

定义一个访问器

若要定义一个访问器,则需在模型上创建一个getFooAttribute方法,要访问的Foo字段需使用「驼峰式」命名。在这个示例中,我们将为first_name属性定义一个访问器。当Eloquent尝试获取first_name属性时,将自动调用此访问器:

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

namespace App;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
/**
* 获取用户的姓名.
*
* @param string $value
* @return string
*/
public function getFirstNameAttribute($value)
{
return ucfirst($value);
}
}

如你所见,字段的原始值被传递到访问器中,允许你对它进行处理并返回结果。如果想获取被修改后的值,你可以在模型实例上访问first_name属性:

1
2
3
$user = App\User::find(1);

$firstName = $user->first_name;

当然,你也可以通过已有的属性值,使用访问器返回新的计算值:

1
2
3
4
5
6
7
8
9
/**
* 获取用户的姓名.
*
* @return string
*/
public function getFullNameAttribute()
{
return "{$this->first_name} {$this->last_name}";
}

Tip:如果你需要将这些计算值添加到模型的数组/JSON中,你需要追加它们。

定义一个修改器

若要定义一个修改器,则需在模型上面定义setFooAttribute方法。要访问的Foo字段使用「驼峰式」命名。让我们再来定义一个first_name属性的修改器。当我们尝试在模型上设置first_name属性值时,该修改器将被自动调用:

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

namespace App;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
/**
* 设置用户的姓名.
*
* @param string $value
* @return void
*/
public function setFirstNameAttribute($value)
{
$this->attributes['first_name'] = strtolower($value);
}
}

修改器会获取属性已经被设置的值,允许你修改并且将其值设置到Eloquent模型内部的$attributes属性上。举个例子,如果我们尝试将first_name属性的值设置为Sally:

1
2
3
$user = App\User::find(1);

$user->first_name = 'Sally';

在这个例子中,setFirstNameAttribute方法在调用的时候接受Sally这个值作为参数。接着修改器会应用strtolower函数并将处理的结果设置到内部的$attributes数组。

日期转换器

默认情况下,Eloquent会将created_at和updated_at字段转换为Carbon实例,它继承了PHP原生的DateTime类并提供了各种有用的方法。你可以通过设置模型的$dates属性来添加其他日期属性:

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

namespace App;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
/**
* 应该转换为日期格式的属性.
*
* @var array
*/
protected $dates = [
'seen_at',
];
}

Tip:你可以通过将模型的公有属性$timestamps值设置为false来禁用默认的created_at和updated_at时间戳。

当某个字段是日期格式时,你可以将值设置为一个UNIX时间戳,日期时间(Y-m-d)字符串,或者DateTime/Carbon实例。日期值会被正确格式化并保存到你的数据库中:

1
2
3
4
5
$user = App\User::find(1);

$user->deleted_at = now();

$user->save();

就如上面所说,当获取到的属性包含在$dates属性中时,都会自动转换为Carbon实例,允许你在属性上使用任意的Carbon方法:

1
2
3
$user = App\User::find(1);

return $user->deleted_at->getTimestamp();

日期格式

默认情况下,时间戳都将以’Y-m-d Hs’形式格式化。如果你需要自定义时间戳格式,可在模型中设置$dateFormat属性。这个属性决定了日期属性将以何种形式保存在数据库中,以及当模型序列化成数组或JSON时的格式:

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

namespace App;

use Illuminate\Database\Eloquent\Model;

class Flight extends Model
{
/**
* 模型日期字段的保存格式.
*
* @var string
*/
protected $dateFormat = 'U';
}

属性类型转换

模型中的$casts属性提供了一个便利的方法来将属性转换为常见的数据类型。$casts属性应是一个数组,且数组的键是那些需要被转换的属性名称,值则是你希望转换的数据类型。支持转换的数据类型有:integer,real,float,double,decimal:\,string,boolean,object,array,collection,date,datetime和timestamp。当需要转换为decimal类型时,你需要定义小数位的个数,如:decimal:2。

示例,让我们把以整数(0或1)形式存储在数据库中的is_admin属性转成布尔值:

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

namespace App;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
/**
* 这个属性应该被转换为原生类型.
*
* @var array
*/
protected $casts = [
'is_admin' => 'boolean',
];
}

现在当你访问is_admin属性时,虽然保存在数据库里的值是一个整数类型,但是返回值总是会被转换成布尔值类型:

1
2
3
4
5
$user = App\User::find(1);

if ($user->is_admin) {
//
}

数组&JSON转换

当你在数据库存储序列化的JSON的数据时,array类型的转换非常有用。比如:如果你的数据库具有被序列化为JSON的JSON或TEXT字段类型,并且在Eloquent模型中加入了array类型转换,那么当你访问的时候就会自动被转换为PHP数组:

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
    <?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
/**
* 这个属性应该被转换为原生类型.
*
* @var array
*/
protected $casts = [
'options' => 'array',
];
}
```
一旦定义了转换,你访问options属性时他会自动从JSON类型反序列化为PHP数组。当你设置了options属性的值时,给定的数组也会自动序列化为JSON类型存储:
```php
$user = App\User::find(1);

$options = $user->options;

$options['key'] = 'value';

$user->options = $options;

$user->save();

Date转换

当使用date或datetime属性时,可以指定日期的格式。这种格式会被用在模型序列化为数组或者JSON:

1
2
3
4
5
6
7
8
/**
* 这个属性应该被转化为原生类型.
*
* @var array
*/
protected $casts = [
'created_at' => 'datetime:Y-m-d',
];

源码

当我们在Eloquent模型实例中设置某些属性值的时候,修改器允许对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
public function offsetSet($offset, $value)
{
$this->setAttribute($offset, $value);
}

public function setAttribute($key, $value)
{
if ($this->hasSetMutator($key)) {
$method = 'set'.Str::studly($key).'Attribute';

return $this->{$method}($value);
}

elseif ($value && $this->isDateAttribute($key)) {
$value = $this->fromDateTime($value);
}

if ($this->isJsonCastable($key) && ! is_null($value)) {
$value = $this->castAttributeAsJson($key, $value);
}

if (Str::contains($key, '->')) {
return $this->fillJsonAttribute($key, $value);
}

$this->attributes[$key] = $value;

return $this;
}

自定义修改器

当我们为model的成员变量赋值的时候,就会调用offsetSet函数,进而运行setAttribute函数,在这个函数中第一个检查的就是是否存在预处理函数:

1
2
3
4
public function hasSetMutator($key)
{
return method_exists($this, 'set'.Str::studly($key).'Attribute');
}

如果存在该函数,就会直接调用自定义修改器。

日期转换器

接着如果没有自定义修改器的话,还会检查当前更新的成员变量是否是日期属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
protected function isDateAttribute($key)
{
return in_array($key, $this->getDates()) ||
$this->isDateCastable($key);
}

public function getDates()
{
$defaults = [static::CREATED_AT, static::UPDATED_AT];

return $this->usesTimestamps()
? array_unique(array_merge($this->dates, $defaults))
: $this->dates;
}

protected function isDateCastable($key)
{
return $this->hasCast($key, ['date', 'datetime']);
}

字段的时间属性有两种设置方法,一种是设置$dates属性:

1
protected $dates = ['date_attr'];

还有一种方法是设置 cast 数组:

1
protected $casts = ['date_attr' => 'date'];

只要是时间属性的字段,无论是什么类型的值,Laravel都会自动将其转化为数据库的时间格式。数据库的时间格式设置是dateFormat成员变量,不设置的时候,默认的时间格式为’Y-m-d H:i:s’:

1
2
3
protected $dateFormat = ['U'];

protected $dateFormat = ['Y-m-d H:i:s'];

当数据库对应的字段是时间类型时,为其赋值就可以非常灵活。我们可以赋值Carbon类型、DateTime类型、数字类型、字符串等等:

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
31
public function fromDateTime($value)
{
return is_null($value) ? $value : $this->asDateTime($value)->format(
$this->getDateFormat()
);
}

protected function asDateTime($value)
{
if ($value instanceof Carbon) {
return $value;
}

if ($value instanceof DateTimeInterface) {
return new Carbon(
$value->format('Y-m-d H:i:s.u'), $value->getTimezone()
);
}

if (is_numeric($value)) {
return Carbon::createFromTimestamp($value);
}

if ($this->isStandardDateFormat($value)) {
return Carbon::createFromFormat('Y-m-d', $value)->startOfDay();
}

return Carbon::createFromFormat(
$this->getDateFormat(), $value
);
}

json转换器

接下来,如果该变量被设置为array、json等属性,那么其将会转化为json类型。

1
2
3
4
5
6
7
8
9
protected function isJsonCastable($key)
{
return $this->hasCast($key, ['array', 'json', 'object', 'collection']);
}

protected function asJson($value)
{
return json_encode($value);
}

Eloquent Model 访问器

相比较修改器来说,访问器的适用情景会更加多。例如,我们经常把一些关于类型的字段设置为 1、2、3等等,例如用户数据表中用户性别字段,1代表男,2代表女,很多时候我们取出这些值之后必然要经过转换,然后再显示出来。这时候就需要定义访问器。

访问器的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public function getAttribute($key)
{
if (! $key) {
return;
}

if (array_key_exists($key, $this->attributes) ||
$this->hasGetMutator($key)) {
return $this->getAttributeValue($key);
}

if (method_exists(self::class, $key)) {
return;
}

return $this->getRelationValue($key);
}

可以看到,当我们访问数据库对象的成员变量的时候,大致可以分为两类:属性值与关系对象。关系对象我们以后再详细来说,本文中先说关于属性的访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public function getAttributeValue($key)
{
$value = $this->getAttributeFromArray($key);

if ($this->hasGetMutator($key)) {
return $this->mutateAttribute($key, $value);
}

if ($this->hasCast($key)) {
return $this->castAttribute($key, $value);
}

if (in_array($key, $this->getDates()) &&
! is_null($value)) {
return $this->asDateTime($value);
}

return $value;
}

与修改器类似,访问器也由三部分构成:自定义访问器、日期访问器、类型访问器。

获取原始值

访问器的第一步就是从成员变量attributes中获取原始的字段值,一般指的是存在数据库的值。有的时候,我们要取的属性并不在attributes中,这时候就会返回null。

1
2
3
4
5
6
protected function getAttributeFromArray($key)
{
if (isset($this->attributes[$key])) {
return $this->attributes[$key];
}
}

自定义访问器

如果定义了访问器,那么就会调用访问器,获取返回值:

1
2
3
4
5
6
7
8
9
public function hasGetMutator($key)
{
return method_exists($this, 'get'.Str::studly($key).'Attribute');
}

protected function mutateAttribute($key, $value)
{
return $this->{'get'.Str::studly($key).'Attribute'}($value);
}

类型转换

若我们在成员变量 $casts 数组中为属性定义了类型转换,那么就要进行类型转换:

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public function hasCast($key, $types = null)
{
if (array_key_exists($key, $this->getCasts())) {
return $types ? in_array($this->getCastType($key), (array) $types, true) : true;
}

return false;
}

protected function castAttribute($key, $value)
{
if (is_null($value)) {
return $value;
}

switch ($this->getCastType($key)) {
case 'int':
case 'integer':
return (int) $value;
case 'real':
case 'float':
case 'double':
return (float) $value;
case 'string':
return (string) $value;
case 'bool':
case 'boolean':
return (bool) $value;
case 'object':
return $this->fromJson($value, true);
case 'array':
case 'json':
return $this->fromJson($value);
case 'collection':
return new BaseCollection($this->fromJson($value));
case 'date':
return $this->asDate($value);
case 'datetime':
return $this->asDateTime($value);
case 'timestamp':
return $this->asTimestamp($value);
default:
return $value;
}
}

日期转换

若当前属性是CREATED_AT、UPDATED_AT或者被存入成员变量dates中,那么就要进行日期转换。日期转换函数asDateTime可以查看上一节中的内容。

Eloquent Model数组转化

在使用数据库对象中,我们经常使用toArray函数,它可以将从数据库中取出的所有属性和关系模型转化为数组:

1
2
3
4
public function toArray()
{
return array_merge($this->attributesToArray(), $this->relationsToArray());
}

本文中只介绍属性转化为数组的部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public function attributesToArray()
{
$attributes = $this->addDateAttributesToArray(
$attributes = $this->getArrayableAttributes()
);

$attributes = $this->addMutatedAttributesToArray(
$attributes, $mutatedAttributes = $this->getMutatedAttributes()
);

$attributes = $this->addCastAttributesToArray(
$attributes, $mutatedAttributes
);

foreach ($this->getArrayableAppends() as $key) {
$attributes[$key] = $this->mutateAttributeForArray($key, null);
}

return $attributes;
}

与访问器与修改器类似,需要转为数组的元素有日期类型、自定义访问器、类型转换,我们接下来一个个看:

getArrayableAttributes 原始值获取

首先我们要从成员变量 attributes 数组中获取原始值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
protected function getArrayableAttributes()
{
return $this->getArrayableItems($this->attributes);
}

protected function getArrayableItems(array $values)
{
if (count($this->getVisible()) > 0) {
$values = array_intersect_key($values, array_flip($this->getVisible()));
}

if (count($this->getHidden()) > 0) {
$values = array_diff_key($values, array_flip($this->getHidden()));
}

return $values;
}

我们还可以为数据库对象设置可见元素$visible与隐藏元素$hidden,这两个变量会控制toArray可转化的元素属性。

日期转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
protected function addDateAttributesToArray(array $attributes)
{
foreach ($this->getDates() as $key) {
if (! isset($attributes[$key])) {
continue;
}

$attributes[$key] = $this->serializeDate(
$this->asDateTime($attributes[$key])
);
}

return $attributes;
}

protected function serializeDate(DateTimeInterface $date)
{
return $date->format($this->getDateFormat());
}

自定义访问器转换

定义了自定义访问器的属性,会调用访问器函数来覆盖原有的属性值,首先我们需要获取所有的自定义访问器变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public function getMutatedAttributes()
{
$class = static::class;

if (! isset(static::$mutatorCache[$class])) {
static::cacheMutatedAttributes($class);
}

return static::$mutatorCache[$class];
}

public static function cacheMutatedAttributes($class)
{
static::$mutatorCache[$class] = collect(static::getMutatorMethods($class))->map(function ($match) {
return lcfirst(static::$snakeAttributes ? Str::snake($match) : $match);
})->all();
}

protected static function getMutatorMethods($class)
{
preg_match_all('/(?<=^|;)get([^;]+?)Attribute(;|$)/', implode(';', get_class_methods($class)), $matches);

return $matches[1];
}

可以看到,函数用get_class_methods获取类内所有的函数,并筛选出符合get…Attribute的函数,获得自定义的访问器变量,并缓存到mutatorCache中。

接着将会利用自定义访问器变量替换原始值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
protected function addMutatedAttributesToArray(array $attributes, array $mutatedAttributes)
{
foreach ($mutatedAttributes as $key) {
if (! array_key_exists($key, $attributes)) {
continue;
}

$attributes[$key] = $this->mutateAttributeForArray(
$key, $attributes[$key]
);
}

return $attributes;
}

protected function mutateAttributeForArray($key, $value)
{
$value = $this->mutateAttribute($key, $value);

return $value instanceof Arrayable ? $value->toArray() : $value;
}

cast 类型转换

被定义在cast数组中的变量也要进行数组转换,调用的方法和访问器相同,也是castAttribute,如果是时间类型,还要按照时间格式来转换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
protected function addCastAttributesToArray(array $attributes, array $mutatedAttributes)
{
foreach ($this->getCasts() as $key => $value) {
if (! array_key_exists($key, $attributes) || in_array($key, $mutatedAttributes)) {
continue;
}

$attributes[$key] = $this->castAttribute(
$key, $attributes[$key]
);

if ($attributes[$key] &&
($value === 'date' || $value === 'datetime')) {
$attributes[$key] = $this->serializeDate($attributes[$key]);
}
}

return $attributes;
}

appends 额外属性添加

toArray()还会将我们定义在appends变量中的属性一起进行数组转换,但是注意被放入appends成员变量数组中的属性需要有自定义访问器函数:

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
protected function getArrayableAppends()
{
if (! count($this->appends)) {
return [];
}

return $this->getArrayableItems(
array_combine($this->appends, $this->appends)
);
}
``` 

## 查询作用域

查询作用域分为全局作用域与本地作用域。全局作用域不需要手动调用,由程序在每次的查询中自动加载,本地作用域需要在查询的时候进行手动调用。

### 全局作用域

全局作用域可以给模型的查询都添加上约束。Laravel的软删除功能就是利用此特性从数据库中获取「未删除」的模型。你可以编写你自己的全局作用域,很简单、方便的为每个模型查询都加上约束条件:

**编写全局作用域**

编写全局作用域很简单。定义一个实现Illuminate\Database\Eloquent\Scope接口的类,并实现apply这个方法。根据你的需求,在apply方法中加入查询的where条件:
```php
<?php

namespace App\Scopes;

use Illuminate\Database\Eloquent\Scope;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;

class AgeScope implements Scope
{
/**
* 把约束加到Eloquent查询构造中。
*
* @param \Illuminate\Database\Eloquent\Builder $builder
* @param \Illuminate\Database\Eloquent\Model $model
* @return void
*/
public function apply(Builder $builder, Model $model)
{
$builder->where('age', '>', 200);
}
}

提示:如果你需要在select语句里添加字段,应使用addSelect方法,而不是select方法。这将有效防止无意中替换现有select语句的情况。

应用全局作用域

要将全局作用域分配给模型,需要重写模型的boot方法并使用addGlobalScope方法:

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

namespace App;

use App\Scopes\AgeScope;
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
/**
* 模型的「启动」方法
*
* @return void
*/
protected static function boot()
{
parent::boot();

static::addGlobalScope(new AgeScope);
}
}

添加作用域后,对User::all()的查询会生成以下SQL查询语句:

1
select * from `users` where `age` > 200

匿名全局作用域

Eloquent同样允许使用闭包定义全局作用域,这样就不需要为一个简单的作用域而编写一个单独的类:

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;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;

class User extends Model
{
/**
* 模型的「启动」方法
*
* @return void
*/
protected static function boot()
{
parent::boot();

static::addGlobalScope('age', function (Builder $builder) {
$builder->where('age', '>', 200);
});
}
}

取消全局作用域

如果需要对当前查询取消全局作用域,需要使用withoutGlobalScope方法。该方法仅接受全局作用域类名作为它唯一的参数:

1
User::withoutGlobalScope(AgeScope::class)->get();

或者,如果使用闭包定义全局作用域的话:

1
User::withoutGlobalScope('age')->get();

如果你需要取消部分或者全部的全局作用域的话,需要使用withoutGlobalScopes方法:

1
2
3
4
5
6
7
// 取消所有的全局作用域...
User::withoutGlobalScopes()->get();

// 取消部分全局作用域...
User::withoutGlobalScopes([
FirstScope::class, SecondScope::class
])->get();

我们先看看源码:

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
31
32
33
<?php

namespace Illuminate\Database\Eloquent\Concerns;

use Closure;
use Illuminate\Database\Eloquent\Scope;
use Illuminate\Support\Arr;
use InvalidArgumentException;

trait HasGlobalScopes
{
/**
* Register a new global scope on the model.
*
* @param \Illuminate\Database\Eloquent\Scope|\Closure|string $scope
* @param \Closure|null $implementation
* @return mixed
*
* @throws \InvalidArgumentException
*/
public static function addGlobalScope($scope, Closure $implementation = null)
{
if (is_string($scope) && ! is_null($implementation)) {
return static::$globalScopes[static::class][$scope] = $implementation;
} elseif ($scope instanceof Closure) {
return static::$globalScopes[static::class][spl_object_hash($scope)] = $scope;
} elseif ($scope instanceof Scope) {
return static::$globalScopes[static::class][get_class($scope)] = $scope;
}

throw new InvalidArgumentException('Global scope must be an instance of Closure or Scope.');
}
}

可以看到,全局作用域使用的是全局的静态变量globalScopes,该变量保存着所有数据库对象的全局作用域。

Eloquent\Model类并不负责查询功能,相关功能由Eloquent\Builder负责,因此每次查询都会间接调用Eloquent\Builder类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
abstract class Model
{
use ForwardsCalls;

/**
* Handle dynamic method calls into the model.
*
* @param string $method
* @param array $parameters
* @return mixed
*/
public function __call($method, $parameters)
{
if (in_array($method, ['increment', 'decrement'])) {
return $this->$method(...$parameters);
}

return $this->forwardCallTo($this->newQuery(), $method, $parameters);
}
}

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
31
32
33
34
35
36
37
38
39
<?php

namespace Illuminate\Support\Traits;

use BadMethodCallException;
use Error;

trait ForwardsCalls
{
/**
* Forward a method call to the given object.
*
* @param mixed $object
* @param string $method
* @param array $parameters
* @return mixed
*
* @throws \BadMethodCallException
*/
protected function forwardCallTo($object, $method, $parameters)
{
try {
return $object->{$method}(...$parameters);
} catch (Error | BadMethodCallException $e) {
$pattern = '~^Call to undefined method (?P<class>[^:]+)::(?P<method>[^\(]+)\(\)$~';

if (! preg_match($pattern, $e->getMessage(), $matches)) {
throw $e;
}

if ($matches['class'] != get_class($object) ||
$matches['method'] != $method) {
throw $e;
}

static::throwBadMethodCallException($method);
}
}
}

创建新的Eloquent\Builder类需要newQuery函数:

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
public function newQuery()
{
$builder = $this->newQueryWithoutScopes();

foreach ($this->getGlobalScopes() as $identifier => $scope) {
$builder->withGlobalScope($identifier, $scope);
}

return $builder;
}

public function getGlobalScopes()
{
return Arr::get(static::$globalScopes, static::class, []);
}

public function withGlobalScope($identifier, $scope)
{
$this->scopes[$identifier] = $scope;

if (method_exists($scope, 'extend')) {
$scope->extend($this);
}

return $this;
}

newQuery函数为Eloquent\builder加载全局作用域,这样静态变量globalScopes的值就会被赋到Eloquent\builder的scopes成员变量中。

当我们使用get()函数获取数据库数据的时候,也需要借助魔术方法调用Illuminate\Database\Eloquent\Builder类的get函数:

1
2
3
4
5
6
7
8
9
10
public function get($columns = ['*'])
{
$builder = $this->applyScopes();

if (count($models = $builder->getModels($columns)) > 0) {
$models = $builder->eagerLoadRelations($models);
}

return $builder->getModel()->newCollection($models);
}

调用applyScopes函数加载所有的全局作用域:

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
public function applyScopes()
{
if (! $this->scopes) {
return $this;
}

$builder = clone $this;

foreach ($this->scopes as $identifier => $scope) {
if (! isset($builder->scopes[$identifier])) {
continue;
}

$builder->callScope(function (Builder $builder) use ($scope) {
if ($scope instanceof Closure) {
$scope($builder);
}

if ($scope instanceof Scope) {
$scope->apply($builder, $this->getModel());
}
});
}

return $builder;
}

可以看到,builder查询类会通过callScope加载全局作用域的查询条件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
protected function callScope(callable $scope, $parameters = [])
{
array_unshift($parameters, $this);

$query = $this->getQuery();

$originalWhereCount = is_null($query->wheres)
? 0 : count($query->wheres);

$result = $scope(...array_values($parameters)) ?? $this;

if (count((array) $query->wheres) > $originalWhereCount) {
$this->addNewWheresWithinGroup($query, $originalWhereCount);
}

return $result;
}

callScope函数首先会获取更加底层的Query\builder,更新query\bulid的where条件。

addNewWheresWithinGroup这个函数很重要,它为Query\builder提供nest类型的where条件:

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
31
32
33
34
35
36
protected function addNewWheresWithinGroup(QueryBuilder $query, $originalWhereCount)
{
$allWheres = $query->wheres;

$query->wheres = [];

$this->groupWhereSliceForScope(
$query, array_slice($allWheres, 0, $originalWhereCount)
);

$this->groupWhereSliceForScope(
$query, array_slice($allWheres, $originalWhereCount)
);
}

protected function groupWhereSliceForScope(QueryBuilder $query, $whereSlice)
{
$whereBooleans = collect($whereSlice)->pluck('boolean');

if ($whereBooleans->contains('or')) {
$query->wheres[] = $this->createNestedWhere(
$whereSlice, $whereBooleans->first()
);
} else {
$query->wheres = array_merge($query->wheres, $whereSlice);
}
}

protected function createNestedWhere($whereSlice, $boolean = 'and')
{
$whereGroup = $this->getQuery()->forNestedWhere();

$whereGroup->wheres = $whereSlice;

return ['type' => 'Nested', 'query' => $whereGroup, 'boolean' => $boolean];
}

当我们在查询作用域中,所有的查询条件连接符都是and的时候,可以直接合并到where中。

如果我们在查询作用域中或者原查询条件写下了orWhere、orWhereColumn等等连接符为or的查询条件,那么就会利用createNestedWhere函数创建nest类型的where条件。这个where条件会包含查询作用域的所有查询条件,或者原查询的所有查询条件。

本地作用域

全局作用域会自动加载到所有的查询条件当中,Laravel中还有本地作用域,只有在查询时调用才会生效。

本地作用域允许定义通用的约束集合以便在应用程序中重复使用。例如,你可能经常需要获取所有「流行」的用户。要定义这样一个范围,只需要在对应的Eloquent模型方法前添加scope前缀。

作用域总是返回一个查询构造器实例:

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;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
/**
* 只查询受欢迎的用户的作用域
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopePopular($query)
{
return $query->where('votes', '>', 100);
}

/**
* 只查询active用户的作用域
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeActive($query)
{
return $query->where('active', 1);
}
}

使用本地作用域

一旦定义了作用域,就可以在查询该模型时调用作用域方法。不过,在调用这些方法时不必包含scope前缀。甚至可以链式调用多个作用域,例如:

1
$users = App\User::popular()->active()->orderBy('created_at')->get();

借助or查询运行符整合多个Eloquent模型,可能需要使用闭包回调:

1
2
3
$users = App\User::popular()->orWhere(function (Builder $query) {
$query->active();
})->get();

因为这样可能会有点麻烦,Laravel提供了「更高阶的」orWhere方法,它允许你在链式调用作用域时不使用闭包:

1
$users = App\User::popular()->orWhere->active()->get();

动态作用域

有时可能地希望定义一个可以接受参数的作用域。把额外参数传递给作用域就可以达到此目的。作用域参数要放在$query参数之后:

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

namespace App;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
/**
* 将查询作用域限制为仅包含给定类型的用户
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param mixed $type
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeOfType($query, $type)
{
return $query->where('type', $type);
}
}

这样就可以在调用作用域时传递参数了:

1
$users = App\User::ofType('admin')->get();

本地作用域是由魔术方法__call实现的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public function __call($method, $parameters)
{
...

if (method_exists($this->model, $scope = 'scope'.ucfirst($method))) {
return $this->callScope([$this->model, $scope], $parameters);
}

if (in_array($method, $this->passthru)) {
return $this->toBase()->{$method}(...$parameters);
}

$this->query->{$method}(...$parameters);

return $this;
}

批量调用本地作用域

Laravel还提供一个方法可以一次性调用多个本地作用域:

1
2
3
4
5
6
7
$scopes = [
'published',
'category' => 'Laravel',
'framework' => ['Laravel', '5.3'],
];

(new EloquentModelStub)->scopes($scopes);

上面的写法会调用三个本地作用域,它们的参数是$scopes的值。

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
31
32
33
34
35
36
37
38
39
40
41
public function scopes(array $scopes)
{
$builder = $this;

foreach ($scopes as $scope => $parameters) {
if (is_int($scope)) {
list($scope, $parameters) = [$parameters, []];
}

$builder = $builder->callScope(
[$this->model, 'scope'.ucfirst($scope)],
(array) $parameters
);
}

return $builder;
}
``` 

fill批量赋值

Eloquent Model默认只能一个一个的设置数据库对象的属性,这是为了保护数据库。但是有的时候,字段过多会造成代码很繁琐。因此,Laravel提供属性批量赋值的功能,fill函数,相关的官方文档:批量赋值

fill函数
```php
public function fill(array $attributes)
{
$totallyGuarded = $this->totallyGuarded();

foreach ($this->fillableFromArray($attributes) as $key => $value) {
$key = $this->removeTableFromKey($key);

if ($this->isFillable($key)) {
$this->setAttribute($key, $value);
} elseif ($totallyGuarded) {
throw new MassAssignmentException($key);
}
}

return $this;
}

fill函数会从参数attributes中选取可以批量赋值的属性。所谓的可以批量赋值的属性,是指被fillable或guarded成员变量设置的参数。被放入fillable的属性允许批量赋值的属性,被放入guarded的属性禁止批量赋值。

获取可批量赋值的属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
protected function fillableFromArray(array $attributes)
{
if (count($this->getFillable()) > 0 && ! static::$unguarded) {
return array_intersect_key($attributes, array_flip($this->getFillable()));
}

return $attributes;
}

public function getFillable()
{
return $this->fillable;
}

可以看到,若想要实现批量赋值,需要将属性设置在fillable成员数组中。

在Laravel中,有一种数据库对象关系是morph,也就是多态关系,这种关系也会调用fill函数,这个时候传入的参数attributes会带有数据库前缀。接下来,就要调用removeTableFromKey函数来去除数据库前缀:

1
2
3
4
protected function removeTableFromKey($key)
{
return Str::contains($key, '.') ? last(explode('.', $key)) : $key;
}

下一步,还要进一步验证属性的fillable:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public function isFillable($key)
{
if (static::$unguarded) {
return true;
}

if (in_array($key, $this->getFillable())) {
return true;
}

if ($this->isGuarded($key)) {
return false;
}

return empty($this->getFillable()) &&
! Str::startsWith($key, '_');
}

如果当前unguarded开启,也就是不会保护任何属性,那么直接返回true。如果当前属性在fillable中,也会返回true。如果当前属性在guarded中,返回false。最后,如果fillable是空数组,也会返回true。

forceFill

如果不想受fillable或者guarded等的影响,还可以使用forceFill强制来批量赋值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public function forceFill(array $attributes)
{
return static::unguarded(function () use ($attributes) {
return $this->fill($attributes);
});
}

public static function unguarded(callable $callback)
{
if (static::$unguarded) {
return $callback();
}

static::unguard();

try {
return $callback();
} finally {
static::reguard();
}
}

0%