前言
本文 GitBook 地址: https://www.gitbook.com/book/leoyang90/lar...
前面幾個部落格向大家介紹了查詢構造器的原理與原始碼,然而查詢構造器更多是為 Eloquent Model
服務的,我們對資料庫操作更加方便的是使用 Eloquent Model
。 本篇文章將會大家介紹 Model
的一些特性原理。
Eloquent Model 修改器
當我們在 Eloquent
模型例項中設定某些屬性值的時候,修改器允許對 Eloquent
屬性值進行格式化。如果對修改器不熟悉,請參考官方文件:Eloquent: 修改器
下面先看看修改器的原理:
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
函式,在這個函式中第一個檢查的就是是否存在預處理函式:
public function hasSetMutator($key)
{
return method_exists($this, 'set'.Str::studly($key).'Attribute');
}
如果存在該函式,就會直接呼叫自定義修改器。
日期轉換器
接著如果沒有自定義修改器的話,還會檢查當前更新的成員變數是否是日期屬性:
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
屬性:
protected $dates = ['date_attr'];
還有一種方法是設定 cast
陣列:
protected $casts = ['date_attr' => 'date'];
只要是時間屬性的欄位,無論是什麼型別的值,laravel
都會自動將其轉化為資料庫的時間格式。資料庫的時間格式設定是 dateFormat
成員變數,不設定的時候,預設的時間格式為 `Y-m-d H:i:s':
protected $dateFormat = ['U'];
protected $dateFormat = ['Y-m-d H:i:s'];
當資料庫對應的欄位是時間型別時,為其賦值就可以非常靈活。我們可以賦值 Carbon
型別、DateTime
型別、數字型別、字串等等:
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
型別。
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
代表女,很多時候我們取出這些值之後必然要經過轉換,然後再顯示出來。這時候就需要定義訪問器。
訪問器的原始碼:
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);
}
可以看到,當我們訪問資料庫物件的成員變數的時候,大致可以分為兩類:屬性值與關係物件。關係物件我們以後再詳細來說,本文中先說關於屬性的訪問。
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
。
protected function getAttributeFromArray($key)
{
if (isset($this->attributes[$key])) {
return $this->attributes[$key];
}
}
自定義訪問器
如果定義了訪問器,那麼就會呼叫訪問器,獲取返回值:
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
陣列中為屬性定義了型別轉換,那麼就要進行型別轉換:
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
函式,它可以將從資料庫中取出的所有屬性和關係模型轉化為陣列:
public function toArray()
{
return array_merge($this->attributesToArray(), $this->relationsToArray());
}
本文中只介紹屬性轉化為陣列的部分:
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
陣列中獲取原始值:
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
可轉化的元素屬性。
日期轉換
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());
}
自定義訪問器轉換
定義了自定義訪問器的屬性,會呼叫訪問器函式來覆蓋原有的屬性值,首先我們需要獲取所有的自定義訪問器變數:
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
中。
接著將會利用自定義訪問器變數替換原始值:
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
,如果是時間型別,還要按照時間格式來轉換:
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
成員變數陣列中的屬性需要有自定義訪問器函式:
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
中新增匿名函式實現匿名全域性作用域。
我們先看看原始碼:
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
類。
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
函式:
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
函式:
public function get($columns = ['*'])
{
$builder = $this->applyScopes();
if (count($models = $builder->getModels($columns)) > 0) {
$models = $builder->eagerLoadRelations($models);
}
return $builder->getModel()->newCollection($models);
}
呼叫 applyScopes
函式載入所有的全域性作用域:
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
載入全域性作用域的查詢條件。
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
條件:
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
中還有本地作用域,只有在查詢時呼叫才會生效。
本地作用域是由魔術方法 __call
實現的:
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
還提供一個方法可以一次性呼叫多個本地作用域:
$scopes = [
'published',
'category' => 'Laravel',
'framework' => ['Laravel', '5.3'],
];
(new EloquentModelStub)->scopes($scopes);
上面的寫法會呼叫三個本地作用域,它們的引數是 $scopes
的值。
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 函式
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
的屬性禁止批量賦值。
獲取可批量賦值的屬性:
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
函式來去除資料庫字首:
protected function removeTableFromKey($key)
{
return Str::contains($key, '.') ? last(explode('.', $key)) : $key;
}
下一步,還要進一步驗證屬性的 fillable
:
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
強制來批量賦值。
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();
}
}
本作品採用《CC 協議》,轉載必須註明作者和本文連結