当前位置: 首页 > 文档资料 > Laravel 源码详解 >

Laravel Database 数据库 - Laravel Database——Eloquent Model 源码分析(上)

优质
小牛编辑
125浏览
2023-12-01

前言

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

Eloquent Model 修改器

当我们在 Eloquent 模型实例中设置某些属性值的时候,修改器允许对 Eloquent 属性值进行格式化。如果对修改器不熟悉,请参考官方文档:Eloquent: 修改器

下面先看看修改器的原理:

  1. public function offsetSet($offset, $value)
  2. {
  3. $this->setAttribute($offset, $value);
  4. }
  5. public function setAttribute($key, $value)
  6. {
  7. if ($this->hasSetMutator($key)) {
  8. $method = 'set'.Str::studly($key).'Attribute';
  9. return $this->{$method}($value);
  10. }
  11. elseif ($value && $this->isDateAttribute($key)) {
  12. $value = $this->fromDateTime($value);
  13. }
  14. if ($this->isJsonCastable($key) && ! is_null($value)) {
  15. $value = $this->castAttributeAsJson($key, $value);
  16. }
  17. if (Str::contains($key, '->')) {
  18. return $this->fillJsonAttribute($key, $value);
  19. }
  20. $this->attributes[$key] = $value;
  21. return $this;
  22. }

自定义修改器

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

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

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

日期转换器

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

  1. protected function isDateAttribute($key)
  2. {
  3. return in_array($key, $this->getDates()) ||
  4. $this->isDateCastable($key);
  5. }
  6. public function getDates()
  7. {
  8. $defaults = [static::CREATED_AT, static::UPDATED_AT];
  9. return $this->usesTimestamps()
  10. ? array_unique(array_merge($this->dates, $defaults))
  11. : $this->dates;
  12. }
  13. protected function isDateCastable($key)
  14. {
  15. return $this->hasCast($key, ['date', 'datetime']);
  16. }

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

  1. protected $dates = ['date_attr'];

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

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

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

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

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

  1. public function fromDateTime($value)
  2. {
  3. return is_null($value) ? $value : $this->asDateTime($value)->format(
  4. $this->getDateFormat()
  5. );
  6. }
  7. protected function asDateTime($value)
  8. {
  9. if ($value instanceof Carbon) {
  10. return $value;
  11. }
  12. if ($value instanceof DateTimeInterface) {
  13. return new Carbon(
  14. $value->format('Y-m-d H:i:s.u'), $value->getTimezone()
  15. );
  16. }
  17. if (is_numeric($value)) {
  18. return Carbon::createFromTimestamp($value);
  19. }
  20. if ($this->isStandardDateFormat($value)) {
  21. return Carbon::createFromFormat('Y-m-d', $value)->startOfDay();
  22. }
  23. return Carbon::createFromFormat(
  24. $this->getDateFormat(), $value
  25. );
  26. }

json 转换器

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

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

Eloquent Model 访问器

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

访问器的源码:

  1. public function getAttribute($key)
  2. {
  3. if (! $key) {
  4. return;
  5. }
  6. if (array_key_exists($key, $this->attributes) ||
  7. $this->hasGetMutator($key)) {
  8. return $this->getAttributeValue($key);
  9. }
  10. if (method_exists(self::class, $key)) {
  11. return;
  12. }
  13. return $this->getRelationValue($key);
  14. }

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

  1. public function getAttributeValue($key)
  2. {
  3. $value = $this->getAttributeFromArray($key);
  4. if ($this->hasGetMutator($key)) {
  5. return $this->mutateAttribute($key, $value);
  6. }
  7. if ($this->hasCast($key)) {
  8. return $this->castAttribute($key, $value);
  9. }
  10. if (in_array($key, $this->getDates()) &&
  11. ! is_null($value)) {
  12. return $this->asDateTime($value);
  13. }
  14. return $value;
  15. }

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

获取原始值

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

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

自定义访问器

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

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

类型转换

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

  1. public function hasCast($key, $types = null)
  2. {
  3. if (array_key_exists($key, $this->getCasts())) {
  4. return $types ? in_array($this->getCastType($key), (array) $types, true) : true;
  5. }
  6. return false;
  7. }
  8. protected function castAttribute($key, $value)
  9. {
  10. if (is_null($value)) {
  11. return $value;
  12. }
  13. switch ($this->getCastType($key)) {
  14. case 'int':
  15. case 'integer':
  16. return (int) $value;
  17. case 'real':
  18. case 'float':
  19. case 'double':
  20. return (float) $value;
  21. case 'string':
  22. return (string) $value;
  23. case 'bool':
  24. case 'boolean':
  25. return (bool) $value;
  26. case 'object':
  27. return $this->fromJson($value, true);
  28. case 'array':
  29. case 'json':
  30. return $this->fromJson($value);
  31. case 'collection':
  32. return new BaseCollection($this->fromJson($value));
  33. case 'date':
  34. return $this->asDate($value);
  35. case 'datetime':
  36. return $this->asDateTime($value);
  37. case 'timestamp':
  38. return $this->asTimestamp($value);
  39. default:
  40. return $value;
  41. }
  42. }

日期转换

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

Eloquent Model 数组转化

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

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

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

  1. public function attributesToArray()
  2. {
  3. $attributes = $this->addDateAttributesToArray(
  4. $attributes = $this->getArrayableAttributes()
  5. );
  6. $attributes = $this->addMutatedAttributesToArray(
  7. $attributes, $mutatedAttributes = $this->getMutatedAttributes()
  8. );
  9. $attributes = $this->addCastAttributesToArray(
  10. $attributes, $mutatedAttributes
  11. );
  12. foreach ($this->getArrayableAppends() as $key) {
  13. $attributes[$key] = $this->mutateAttributeForArray($key, null);
  14. }
  15. return $attributes;
  16. }

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

getArrayableAttributes 原始值获取

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

  1. protected function getArrayableAttributes()
  2. {
  3. return $this->getArrayableItems($this->attributes);
  4. }
  5. protected function getArrayableItems(array $values)
  6. {
  7. if (count($this->getVisible()) > 0) {
  8. $values = array_intersect_key($values, array_flip($this->getVisible()));
  9. }
  10. if (count($this->getHidden()) > 0) {
  11. $values = array_diff_key($values, array_flip($this->getHidden()));
  12. }
  13. return $values;
  14. }

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

日期转换

  1. protected function addDateAttributesToArray(array $attributes)
  2. {
  3. foreach ($this->getDates() as $key) {
  4. if (! isset($attributes[$key])) {
  5. continue;
  6. }
  7. $attributes[$key] = $this->serializeDate(
  8. $this->asDateTime($attributes[$key])
  9. );
  10. }
  11. return $attributes;
  12. }
  13. protected function serializeDate(DateTimeInterface $date)
  14. {
  15. return $date->format($this->getDateFormat());
  16. }

自定义访问器转换

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

  1. public function getMutatedAttributes()
  2. {
  3. $class = static::class;
  4. if (! isset(static::$mutatorCache[$class])) {
  5. static::cacheMutatedAttributes($class);
  6. }
  7. return static::$mutatorCache[$class];
  8. }
  9. public static function cacheMutatedAttributes($class)
  10. {
  11. static::$mutatorCache[$class] = collect(static::getMutatorMethods($class))->map(function ($match) {
  12. return lcfirst(static::$snakeAttributes ? Str::snake($match) : $match);
  13. })->all();
  14. }
  15. protected static function getMutatorMethods($class)
  16. {
  17. preg_match_all('/(?<=^|;)get([^;]+?)Attribute(;|$)/', implode(';', get_class_methods($class)), $matches);
  18. return $matches[1];
  19. }

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

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

  1. protected function addMutatedAttributesToArray(array $attributes, array $mutatedAttributes)
  2. {
  3. foreach ($mutatedAttributes as $key) {
  4. if (! array_key_exists($key, $attributes)) {
  5. continue;
  6. }
  7. $attributes[$key] = $this->mutateAttributeForArray(
  8. $key, $attributes[$key]
  9. );
  10. }
  11. return $attributes;
  12. }
  13. protected function mutateAttributeForArray($key, $value)
  14. {
  15. $value = $this->mutateAttribute($key, $value);
  16. return $value instanceof Arrayable ? $value->toArray() : $value;
  17. }

cast 类型转换

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

  1. protected function addCastAttributesToArray(array $attributes, array $mutatedAttributes)
  2. {
  3. foreach ($this->getCasts() as $key => $value) {
  4. if (! array_key_exists($key, $attributes) || in_array($key, $mutatedAttributes)) {
  5. continue;
  6. }
  7. $attributes[$key] = $this->castAttribute(
  8. $key, $attributes[$key]
  9. );
  10. if ($attributes[$key] &&
  11. ($value === 'date' || $value === 'datetime')) {
  12. $attributes[$key] = $this->serializeDate($attributes[$key]);
  13. }
  14. }
  15. return $attributes;
  16. }

appends 额外属性添加

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

  1. protected function getArrayableAppends()
  2. {
  3. if (! count($this->appends)) {
  4. return [];
  5. }
  6. return $this->getArrayableItems(
  7. array_combine($this->appends, $this->appends)
  8. );
  9. }

查询作用域

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

全局作用域

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

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

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

我们先看看源码:

  1. public static function addGlobalScope($scope, Closure $implementation = null)
  2. {
  3. if (is_string($scope) && ! is_null($implementation)) {
  4. return static::$globalScopes[static::class][$scope] = $implementation;
  5. } elseif ($scope instanceof Closure) {
  6. return static::$globalScopes[static::class][spl_object_hash($scope)] = $scope;
  7. } elseif ($scope instanceof Scope) {
  8. return static::$globalScopes[static::class][get_class($scope)] = $scope;
  9. }
  10. throw new InvalidArgumentException('Global scope must be an instance of Closure or Scope.');
  11. }

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

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

  1. public function __call($method, $parameters)
  2. {
  3. if (in_array($method, ['increment', 'decrement'])) {
  4. return $this->$method(...$parameters);
  5. }
  6. try {
  7. return $this->newQuery()->$method(...$parameters);
  8. } catch (BadMethodCallException $e) {
  9. throw new BadMethodCallException(
  10. sprintf('Call to undefined method %s::%s()', get_class($this), $method)
  11. );
  12. }
  13. }

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

  1. public function newQuery()
  2. {
  3. $builder = $this->newQueryWithoutScopes();
  4. foreach ($this->getGlobalScopes() as $identifier => $scope) {
  5. $builder->withGlobalScope($identifier, $scope);
  6. }
  7. return $builder;
  8. }
  9. public function getGlobalScopes()
  10. {
  11. return Arr::get(static::$globalScopes, static::class, []);
  12. }
  13. public function withGlobalScope($identifier, $scope)
  14. {
  15. $this->scopes[$identifier] = $scope;
  16. if (method_exists($scope, 'extend')) {
  17. $scope->extend($this);
  18. }
  19. return $this;
  20. }

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

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

  1. public function get($columns = ['*'])
  2. {
  3. $builder = $this->applyScopes();
  4. if (count($models = $builder->getModels($columns)) > 0) {
  5. $models = $builder->eagerLoadRelations($models);
  6. }
  7. return $builder->getModel()->newCollection($models);
  8. }

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

  1. public function applyScopes()
  2. {
  3. if (! $this->scopes) {
  4. return $this;
  5. }
  6. $builder = clone $this;
  7. foreach ($this->scopes as $identifier => $scope) {
  8. if (! isset($builder->scopes[$identifier])) {
  9. continue;
  10. }
  11. $builder->callScope(function (Builder $builder) use ($scope) {
  12. if ($scope instanceof Closure) {
  13. $scope($builder);
  14. }
  15. if ($scope instanceof Scope) {
  16. $scope->apply($builder, $this->getModel());
  17. }
  18. });
  19. }
  20. return $builder;
  21. }

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

  1. protected function callScope(callable $scope, $parameters = [])
  2. {
  3. array_unshift($parameters, $this);
  4. $query = $this->getQuery();
  5. $originalWhereCount = is_null($query->wheres)
  6. ? 0 : count($query->wheres);
  7. $result = $scope(...array_values($parameters)) ?? $this;
  8. if (count((array) $query->wheres) > $originalWhereCount) {
  9. $this->addNewWheresWithinGroup($query, $originalWhereCount);
  10. }
  11. return $result;
  12. }

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

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

  1. protected function addNewWheresWithinGroup(QueryBuilder $query, $originalWhereCount)
  2. {
  3. $allWheres = $query->wheres;
  4. $query->wheres = [];
  5. $this->groupWhereSliceForScope(
  6. $query, array_slice($allWheres, 0, $originalWhereCount)
  7. );
  8. $this->groupWhereSliceForScope(
  9. $query, array_slice($allWheres, $originalWhereCount)
  10. );
  11. }
  12. protected function groupWhereSliceForScope(QueryBuilder $query, $whereSlice)
  13. {
  14. $whereBooleans = collect($whereSlice)->pluck('boolean');
  15. if ($whereBooleans->contains('or')) {
  16. $query->wheres[] = $this->createNestedWhere(
  17. $whereSlice, $whereBooleans->first()
  18. );
  19. } else {
  20. $query->wheres = array_merge($query->wheres, $whereSlice);
  21. }
  22. }
  23. protected function createNestedWhere($whereSlice, $boolean = 'and')
  24. {
  25. $whereGroup = $this->getQuery()->forNestedWhere();
  26. $whereGroup->wheres = $whereSlice;
  27. return ['type' => 'Nested', 'query' => $whereGroup, 'boolean' => $boolean];
  28. }

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

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

本地作用域

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

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

  1. public function __call($method, $parameters)
  2. {
  3. ...
  4. if (method_exists($this->model, $scope = 'scope'.ucfirst($method))) {
  5. return $this->callScope([$this->model, $scope], $parameters);
  6. }
  7. if (in_array($method, $this->passthru)) {
  8. return $this->toBase()->{$method}(...$parameters);
  9. }
  10. $this->query->{$method}(...$parameters);
  11. return $this;
  12. }

批量调用本地作用域

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

  1. $scopes = [
  2. 'published',
  3. 'category' => 'Laravel',
  4. 'framework' => ['Laravel', '5.3'],
  5. ];
  6. (new EloquentModelStub)->scopes($scopes);

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

  1. public function scopes(array $scopes)
  2. {
  3. $builder = $this;
  4. foreach ($scopes as $scope => $parameters) {
  5. if (is_int($scope)) {
  6. list($scope, $parameters) = [$parameters, []];
  7. }
  8. $builder = $builder->callScope(
  9. [$this->model, 'scope'.ucfirst($scope)],
  10. (array) $parameters
  11. );
  12. }
  13. return $builder;
  14. }

fill 批量赋值

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

fill 函数

  1. public function fill(array $attributes)
  2. {
  3. $totallyGuarded = $this->totallyGuarded();
  4. foreach ($this->fillableFromArray($attributes) as $key => $value) {
  5. $key = $this->removeTableFromKey($key);
  6. if ($this->isFillable($key)) {
  7. $this->setAttribute($key, $value);
  8. } elseif ($totallyGuarded) {
  9. throw new MassAssignmentException($key);
  10. }
  11. }
  12. return $this;
  13. }

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

获取可批量赋值的属性:

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

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

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

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

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

  1. public function isFillable($key)
  2. {
  3. if (static::$unguarded) {
  4. return true;
  5. }
  6. if (in_array($key, $this->getFillable())) {
  7. return true;
  8. }
  9. if ($this->isGuarded($key)) {
  10. return false;
  11. }
  12. return empty($this->getFillable()) &&
  13. ! Str::startsWith($key, '_');
  14. }

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

forceFill

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

  1. public function forceFill(array $attributes)
  2. {
  3. return static::unguarded(function () use ($attributes) {
  4. return $this->fill($attributes);
  5. });
  6. }
  7. public static function unguarded(callable $callback)
  8. {
  9. if (static::$unguarded) {
  10. return $callback();
  11. }
  12. static::unguard();
  13. try {
  14. return $callback();
  15. } finally {
  16. static::reguard();
  17. }
  18. }