前言
上一节学习了查询构造器。下面来看下最常用的Model的源码吧
本文主要内容整理自leoyang的系列文章,详情见引用
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;
}
自定义修改器
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修改器
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];
}
}
Eloquent Model 数组转化
还记得CURD里用起来很舒服的toArray么?来看看怎么实现的吧
在使用数据库对象中,我们经常使用 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;
}
我们还可以为数据库对象设置可见元素 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)
);
}
获取模型
get 函数
public function get($columns = ['*'])
{
$builder = $this->applyScopes();
if (count($models = $builder->getModels($columns)) > 0) {
$models = $builder->eagerLoadRelations($models);
}
return $builder->getModel()->newCollection($models);
}
public function getModels($columns = ['*'])
{
return $this->model->hydrate(
$this->query->get($columns)->all()
)->all();
}
get 函数会将 QueryBuilder 所获取的数据进一步包装 hydrate。hydrate 函数会将数据库取回来的数据打包成数据库模型对象 Eloquent Model,如果可以获取到数据,还会利用函数 eagerLoadRelations 来预加载关系模型。
public function hydrate(array $items)
{
$instance = $this->newModelInstance();
return $instance->newCollection(array_map(function ($item) use ($instance) {
return $instance->newFromBuilder($item);
}, $items));
}
newModelInstance 函数创建了一个新的数据库模型对象,重要的是这个函数为新的数据库模型对象赋予了 connection:
public function newModelInstance($attributes = [])
{
return $this->model->newInstance($attributes)->setConnection(
$this->query->getConnection()->getName()
);
}
newFromBuilder 函数会将所有数据库数据存入另一个新的 Eloquent Model 的 attributes 中:
public function newFromBuilder($attributes = [], $connection = null)
{
$model = $this->newInstance([], true);
$model->setRawAttributes((array) $attributes, true);
$model->setConnection($connection ?: $this->getConnectionName());
$model->fireModelEvent('retrieved', false);
return $model;
}
newInstance 函数专用于创建新的数据库对象模型:
public function newInstance($attributes = [], $exists = false)
{
$model = new static((array) $attributes);
$model->exists = $exists;
$model->setConnection(
$this->getConnectionName()
);
return $model;
}
值得注意的是 newInstance 将 exist 设置为 true,意味着当前这个数据库模型对象是从数据库中获取而来,并非是手动新建的,这个 exist 为真,我们才能对这个数据库对象进行 update。
setRawAttributes 函数为新的数据库对象赋予属性值,并且进行 sync,标志着对象的原始状态:
public function setRawAttributes(array $attributes, $sync = false)
{
$this->attributes = $attributes;
if ($sync) {
$this->syncOriginal();
}
return $this;
}
public function syncOriginal()
{
$this->original = $this->attributes;
return $this;
}
这个原始状态的记录十分重要,原因是 save 函数就是利用原始值 original 与属性值 attributes 的差异来决定更新的字段。
find 函数
find 函数用于利用主键 id 来查询数据,find 函数也可以传入数组,查询多个数据
public function find($id, $columns = ['*'])
{
if (is_array($id) || $id instanceof Arrayable) {
return $this->findMany($id, $columns);
}
return $this->whereKey($id)->first($columns);
}
public function findMany($ids, $columns = ['*'])
{
if (empty($ids)) {
return $this->model->newCollection();
}
return $this->whereKey($ids)->get($columns);
}
findOrFail
laravel 还提供 findOrFail 函数,一般用于 controller,在未找到记录的时候会抛出异常。
public function findOrFail($id, $columns = ['*'])
{
$result = $this->find($id, $columns);
if (is_array($id)) {
if (count($result) == count(array_unique($id))) {
return $result;
}
} elseif (! is_null($result)) {
return $result;
}
throw (new ModelNotFoundException)->setModel(
get_class($this->model), $id
);
}
其他查询与数据获取方法
所用 Query Builder 支持的查询方法,例如 select、selectSub、whereDate、whereBetween 等等,都可以直接对 Eloquent Model 直接使用,程序会通过魔术方法调用 Query Builder 的相关方法:
protected $passthru = [
'insert', 'insertGetId', 'getBindings', 'toSql',
'exists', 'count', 'min', 'max', 'avg', 'sum', 'getConnection',
];
public function __call($method, $parameters)
{
...
if (in_array($method, $this->passthru)) {
return $this->toBase()->{$method}(...$parameters);
}
$this->query->{$method}(...$parameters);
return $this;
}
passthru 中的各个函数在调用前需要加载查询作用域,原因是这些操作基本上是 aggregate 的,需要添加搜索条件才能更加符合预期:
public function toBase()
{
return $this->applyScopes()->getQuery();
}
添加和更新模型
save 函数
在 Eloquent Model 中,添加与更新模型可以统一用 save 函数。在添加模型的时候需要事先为 model 属性赋值,可以单个手动赋值,也可以批量赋值。在更新模型的时候,需要事先从数据库中取出模型,然后修改模型属性,最后执行 save 更新操作。官方文档:添加和更新模型
public function save(array $options = [])
{
$query = $this->newQueryWithoutScopes();
if ($this->fireModelEvent('saving') === false) {
return false;
}
if ($this->exists) {
$saved = $this->isDirty() ?
$this->performUpdate($query) : true;
}
else {
$saved = $this->performInsert($query);
if (! $this->getConnectionName() &&
$connection = $query->getConnection()) {
$this->setConnection($connection->getName());
}
}
if ($saved) {
$this->finishSave($options);
}
return $saved;
}
save 函数不会加载全局作用域,原因是凡是利用 save 函数进行的插入或者更新的操作都不会存在 where 条件,仅仅利用自身的主键属性来进行更新。如果需要 where 条件可以使用 query\builder 的 update 函数,我们在下面会详细介绍:
public function newQueryWithoutScopes()
{
$builder = $this->newEloquentBuilder($this->newBaseQueryBuilder());
return $builder->setModel($this)
->with($this->with)
->withCount($this->withCount);
}
protected function newBaseQueryBuilder()
{
$connection = $this->getConnection();
return new QueryBuilder(
$connection, $connection->getQueryGrammar(), $connection->getPostProcessor()
);
}
newQueryWithoutScopes 函数创建新的没有任何其他条件的 Eloquent\builder 类,而 Eloquent\builder 类需要 Query\builder 类作为底层查询构造器。
performUpdate 函数
如果当前的数据库模型对象是从数据库中取出的,也就是直接或间接的调用 get() 函数从数据库中获取到的数据库对象,那么其 exists 必然是 true
public function isDirty($attributes = null)
{
return $this->hasChanges(
$this->getDirty(), is_array($attributes) ? $attributes : func_get_args()
);
}
public function getDirty()
{
$dirty = [];
foreach ($this->getAttributes() as $key => $value) {
if (! $this->originalIsEquivalent($key, $value)) {
$dirty[$key] = $value;
}
}
return $dirty;
}
getDirty 函数可以获取所有与原始值不同的属性值,也就是需要更新的数据库字段。关键函数在于 originalIsEquivalent:
protected function originalIsEquivalent($key, $current)
{
if (! array_key_exists($key, $this->original)) {
return false;
}
$original = $this->getOriginal($key);
if ($current === $original) {
return true;
} elseif (is_null($current)) {
return false;
} elseif ($this->isDateAttribute($key)) {
return $this->fromDateTime($current) ===
$this->fromDateTime($original);
} elseif ($this->hasCast($key)) {
return $this->castAttribute($key, $current) ===
$this->castAttribute($key, $original);
}
return is_numeric($current) && is_numeric($original)
&& strcmp((string) $current, (string) $original) === 0;
}
可以看到,对于数据库可以转化的属性都要先进行转化,然后再开始对比。比较出的结果,就是我们需要 update 的字段。
执行更新的时候,除了 getDirty 函数获得的待更新字段,还会有 UPDATED_AT 这个字段:
protected function performUpdate(Builder $query)
{
if ($this->fireModelEvent('updating') === false) {
return false;
}
if ($this->usesTimestamps()) {
$this->updateTimestamps();
}
$dirty = $this->getDirty();
if (count($dirty) > 0) {
$this->setKeysForSaveQuery($query)->update($dirty);
$this->fireModelEvent('updated', false);
$this->syncChanges();
}
return true;
}
protected function updateTimestamps()
{
$time = $this->freshTimestamp();
if (! is_null(static::UPDATED_AT) && ! $this->isDirty(static::UPDATED_AT)) {
$this->setUpdatedAt($time);
}
if (! $this->exists && ! $this->isDirty(static::CREATED_AT)) {
$this->setCreatedAt($time);
}
}
执行更新的时候,where 条件只有一个,那就是主键 id:
protected function setKeysForSaveQuery(Builder $query)
{
$query->where($this->getKeyName(), '=', $this->getKeyForSaveQuery());
return $query;
}
protected function getKeyForSaveQuery()
{
return $this->original[$this->getKeyName()]
?? $this->getKey();
}
public function getKey()
{
return $this->getAttribute($this->getKeyName());
}
最后会调用 EloquentBuilder 的 update 函数:
public function update(array $values)
{
return $this->toBase()->update($this->addUpdatedAtColumn($values));
}
protected function addUpdatedAtColumn(array $values)
{
if (! $this->model->usesTimestamps()) {
return $values;
}
return Arr::add(
$values, $this->model->getUpdatedAtColumn(),
$this->model->freshTimestampString()
);
}
public function freshTimestampString()
{
return $this->fromDateTime($this->freshTimestamp());
}
public function fromDateTime($value)
{
return is_null($value) ? $value : $this->asDateTime($value)->format(
$this->getDateFormat()
);
}
performInsert 函数
关于数据库对象的插入,如果数据库的主键被设置为 increment,也就是自增的话,程序会调用 insertAndSetId,这个时候不需要给数据库模型对象手动赋值主键 id。若果数据库的主键并不支持自增,那么就需要在插入前,为数据库对象的主键 id 赋值,否则数据库会报错。
protected function performInsert(Builder $query)
{
if ($this->fireModelEvent('creating') === false) {
return false;
}
if ($this->usesTimestamps()) {
$this->updateTimestamps();
}
$attributes = $this->attributes;
if ($this->getIncrementing()) {
$this->insertAndSetId($query, $attributes);
}
else {
if (empty($attributes)) {
return true;
}
$query->insert($attributes);
}
$this->exists = true;
$this->wasRecentlyCreated = true;
$this->fireModelEvent('created', false);
return true;
}
laravel 默认数据库的主键支持自增属性,程序调用的也是函数 insertAndSetId 函数:
protected function insertAndSetId(Builder $query, $attributes)
{
$id = $query->insertGetId($attributes, $keyName = $this->getKeyName());
$this->setAttribute($keyName, $id);
}
插入后,会将插入后得到的主键 id 返回,并赋值到模型的属性当中。
如果数据库主键不支持自增,那么我们在数据库类中要设置:
public $incrementing = false;
每次进行插入数据的时候,需要手动给主键赋值。
update 函数
save 函数仅仅支持手动的属性赋值,无法批量赋值。laravel 的 Eloquent Model 还有一个函数: update 支持批量属性赋值。有意思的是,Eloquent Builder 也有函数 update,那个是上一小节提到的 performUpdate 所调用的函数。
两个 update 功能一致,只是 Model 的 update 函数比较适用于更新从数据库取回的数据库对象:
$flight = App\Flight::find(1);
$flight->update(['name' => 'New Flight Name','desc' => 'test']);
而 Builder 的 update 适用于多查询条件下的更新:
App\Flight::where('active', 1)
->where('destination', 'San Diego')
->update(['delayed' => 1]);
无论哪一种,都会自动更新 updated_at 字段。
Model 的 update 函数借助 fill 函数与 save 函数:
public function update(array $attributes = [], array $options = [])
{
if (! $this->exists) {
return false;
}
return $this->fill($attributes)->save($options);
}
make 函数
同样的,save 的插入也仅仅支持手动属性赋值,如果想实现批量属性赋值的插入可以使用 make 函数:
$model = App\Flight::make(['name' => 'New Flight Name','desc' => 'test']);
$model->save();
make 函数实际上仅仅是新建了一个 Eloquent Model,并批量赋予属性值:
public function make(array $attributes = [])
{
return $this->newModelInstance($attributes);
}
public function newModelInstance($attributes = [])
{
return $this->model->newInstance($attributes)->setConnection(
$this->query->getConnection()->getName()
);
}
create 函数
如果想要一步到位,批量赋值属性与插入一起操作,可以使用 create 函数:
App\Flight::create(['name' => 'New Flight Name','desc' => 'test']);
相比较 make 函数,create 函数更进一步调用了 save 函数:
public function create(array $attributes = [])
{
return tap($this->newModelInstance($attributes), function ($instance) {
$instance->save();
});
}
实际上,属性值是否可以批量赋值需要受 fillable 或 guarded 来控制,如果我们想要强制批量赋值可以使用 forceCreate:
public function forceCreate(array $attributes)
{
return $this->model->unguarded(function () use ($attributes) {
return $this->newModelInstance()->create($attributes);
});
}
参考文献
欢迎大家关注我的公众号
半亩房顶
网友评论