LEOYANG'S BLOG

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


前言

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

Eloquent Model 修改器

当我们在 Eloquent 模型实例中设置某些属性值的时候,修改器允许对 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 转换器

接下来,如果该变量被设置为 arrayjson 等属性,那么其将会转化为 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 访问器

相比较修改器来说,访问器的适用情景会更加多。例如,我们经常把一些关于类型的字段设置为 123 等等,例如用户数据表中用户性别字段,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_ATUPDATED_AT 或者被存入成员变量 dates 中,那么就要进行日期转换。日期转换函数 asDateTime 可以查看上一节中的内容。

Eloquent Model 数组转化

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

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 toArray()
{
return array_merge($this->attributesToArray(), $this->relationsToArray());
}
```
本文中只介绍属性转化为数组的部分:
```php
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
protected function getArrayableAppends()
{
if (! count($this->appends)) {
return [];
}
return $this->getArrayableItems(
array_combine($this->appends, $this->appends)
);
}

查询作用域

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

全局作用域

一般全局作用域需要定义一个实现 Illuminate\Database\Eloquent\Scope 接口的类,该接口要求你实现一个方法:apply。需要的话可以在 apply 方法中添加 where 条件到查询。

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

另外,我们还可以向 addGlobalScope 中添加匿名函数实现匿名全局作用域。

我们先看看源码:

1
2
3
4
5
6
7
8
9
10
11
12
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
public function __call($method, $parameters)
{
if (in_array($method, ['increment', 'decrement'])) {
return $this->$method(...$parameters);
}
try {
return $this->newQuery()->$method(...$parameters);
} catch (BadMethodCallException $e) {
throw new BadMethodCallException(
sprintf('Call to undefined method %s::%s()', get_class($this), $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\builderscopes 成员变量中。

当我们使用 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\bulidwhere 条件。

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 中。

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

本地作用域

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

本地作用域是由魔术方法 __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
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 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 中选取可以批量赋值的属性。所谓的可以批量赋值的属性,是指被 fillableguarded 成员变量设置的参数。被放入 fillable 的属性允许批量赋值的属性,被放入 guarded 的属性禁止批量赋值。

获取可批量赋值的属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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` 函数来去除数据库前缀:
```php
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();
}
}