LEOYANG'S BLOG

Notes and Blogs


  • 首页

  • 归档

  • 分类

  • 标签

  • 关于

  • 搜索
LEOYANG'S BLOG

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

发表于 2017-10-09 | 分类于 laravel , database , php | | 阅读次数

获取模型

get 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 来预加载关系模型。

1
2
3
4
5
6
7
8
public function hydrate(array $items)
{
$instance = $this->newModelInstance();
return $instance->newCollection(array_map(function ($item) use ($instance) {
return $instance->newFromBuilder($item);
}, $items));
}

newModelInstance 函数创建了一个新的数据库模型对象,重要的是这个函数为新的数据库模型对象赋予了 connection:

1
2
3
4
5
6
public function newModelInstance($attributes = [])
{
return $this->model->newInstance($attributes)->setConnection(
$this->query->getConnection()->getName()
);
}

newFromBuilder 函数会将所有数据库数据存入另一个新的 Eloquent Model 的 attributes 中:

1
2
3
4
5
6
7
8
9
10
11
12
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 函数专用于创建新的数据库对象模型:

1
2
3
4
5
6
7
8
9
10
11
12
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,标志着对象的原始状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 函数也可以传入数组,查询多个数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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,在未找到记录的时候会抛出异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 的相关方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 的,需要添加搜索条件才能更加符合预期:

1
2
3
4
public function toBase()
{
return $this->applyScopes()->getQuery();
}

添加和更新模型

save 函数

在 Eloquent Model 中,添加与更新模型可以统一用 save 函数。在添加模型的时候需要事先为 model 属性赋值,可以单个手动赋值,也可以批量赋值。在更新模型的时候,需要事先从数据库中取出模型,然后修改模型属性,最后执行 save 更新操作。官方文档:添加和更新模型

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
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 函数,我们在下面会详细介绍:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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:

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 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 这个字段:

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
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 函数:

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
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 赋值,否则数据库会报错。

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
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 函数:

1
2
3
4
5
6
protected function insertAndSetId(Builder $query, $attributes)
{
$id = $query->insertGetId($attributes, $keyName = $this->getKeyName());
$this->setAttribute($keyName, $id);
}

插入后,会将插入后得到的主键 id 返回,并赋值到模型的属性当中。

如果数据库主键不支持自增,那么我们在数据库类中要设置:

1
public $incrementing = false;

每次进行插入数据的时候,需要手动给主键赋值。

update 函数

save 函数仅仅支持手动的属性赋值,无法批量赋值。laravel 的 Eloquent Model 还有一个函数: update 支持批量属性赋值。有意思的是,Eloquent Builder 也有函数 update,那个是上一小节提到的 performUpdate 所调用的函数。

两个 update 功能一致,只是 Model 的 update 函数比较适用于更新从数据库取回的数据库对象:

1
2
3
$flight = App\Flight::find(1);
$flight->update(['name' => 'New Flight Name','desc' => 'test']);

而 Builder 的 update 适用于多查询条件下的更新:

1
2
3
App\Flight::where('active', 1)
->where('destination', 'San Diego')
->update(['delayed' => 1]);

无论哪一种,都会自动更新 updated_at 字段。

Model 的 update 函数借助 fill 函数与 save 函数:

1
2
3
4
5
6
7
8
public function update(array $attributes = [], array $options = [])
{
if (! $this->exists) {
return false;
}
return $this->fill($attributes)->save($options);
}

make 函数

同样的,save 的插入也仅仅支持手动属性赋值,如果想实现批量属性赋值的插入可以使用 make 函数:

1
2
3
$model = App\Flight::make(['name' => 'New Flight Name','desc' => 'test']);
$model->save();

make 函数实际上仅仅是新建了一个 Eloquent Model,并批量赋予属性值:

1
2
3
4
5
6
7
8
9
10
11
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 函数:

1
App\Flight::create(['name' => 'New Flight Name','desc' => 'test']);

相比较 make 函数,create 函数更进一步调用了 save 函数:

1
2
3
4
5
6
public function create(array $attributes = [])
{
return tap($this->newModelInstance($attributes), function ($instance) {
$instance->save();
});
}

实际上,属性值是否可以批量赋值需要受 fillable 或 guarded 来控制,如果我们想要强制批量赋值可以使用 forceCreate:

1
2
3
4
5
6
public function forceCreate(array $attributes)
{
return $this->model->unguarded(function () use ($attributes) {
return $this->newModelInstance()->create($attributes);
});
}

findOrNew 函数

laravel 提供一种主键查询或者新建数据库对象的函数:findOrNew:

1
2
3
4
5
6
7
8
public function findOrNew($id, $columns = ['*'])
{
if (! is_null($model = $this->find($id, $columns))) {
return $model;
}
return $this->newModelInstance();
}

值得注意的是,当查询失败的时候,会返回一个全新的数据库对象,不含有任何 attributes。

firstOrNew 函数

laravel 提供一种自定义查询或者新建数据库对象的函数:firstOrNew:

1
2
3
4
5
6
7
8
public function firstOrNew(array $attributes, array $values = [])
{
if (! is_null($instance = $this->where($attributes)->first())) {
return $instance;
}
return $this->newModelInstance($attributes + $values);
}

值得注意的是,如果查询失败,会返回一个含有 attributes 和 values 两者合并的属性的数据库对象。

firstOrCreate 函数

类似于 firstOrNew 函数,firstOrCreate 函数也用于自定义查询或者新建数据库对象,不同的是,firstOrCreate 函数还进一步对数据进行了插入操作:

1
2
3
4
5
6
7
8
9
10
public function firstOrCreate(array $attributes, array $values = [])
{
if (! is_null($instance = $this->where($attributes)->first())) {
return $instance;
}
return tap($this->newModelInstance($attributes + $values), function ($instance) {
$instance->save();
});
}

updateOrCreate 函数

在 firstOrCreate 函数基础上,除了对数据进行查询,还会对查询成功的数据利用 value 进行更新:

1
2
3
4
5
6
public function updateOrCreate(array $attributes, array $values = [])
{
return tap($this->firstOrNew($attributes), function ($instance) use ($values) {
$instance->fill($values)->save();
});
}

firstOr 函数

如果想要自定义查找失败后的操作,可以使用 firstOr 函数,该函数可以传入闭包函数,处理找不到数据的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public function firstOr($columns = ['*'], Closure $callback = null)
{
if ($columns instanceof Closure) {
$callback = $columns;
$columns = ['*'];
}
if (! is_null($model = $this->first($columns))) {
return $model;
}
return call_user_func($callback);
}

删除模型

删除模型也会分为两种,一种是针对 Eloquent Model 的删除,这种删除必须是从数据库中取出的对象。还有一种是 Eloquent Builder 的删除,这种删除一般会带有多个查询条件。我们这一小节主要讲 model 的删除:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public function delete()
{
if (is_null($this->getKeyName())) {
throw new Exception('No primary key defined on model.');
}
if (! $this->exists) {
return;
}
if ($this->fireModelEvent('deleting') === false) {
return false;
}
$this->touchOwners();
$this->performDeleteOnModel();
$this->fireModelEvent('deleted', false);
return true;
}

删除模型时,模型对象必然要有主键。performDeleteOnModel 函数执行具体的删除操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
protected function performDeleteOnModel()
{
$this->setKeysForSaveQuery($this->newQueryWithoutScopes())->delete();
$this->exists = false;
}
protected function setKeysForSaveQuery(Builder $query)
{
$query->where($this->getKeyName(), '=', $this->getKeyForSaveQuery());
return $query;
}

所以实际上,Model 调用的也是 builder 的 delete 函数。

软删除

如果想要使用软删除,需要使用 Illuminate\Database\Eloquent\SoftDeletes 这个 trait。并且需要定义软删除字段,默认为 deleted_at,将软删除字段放入 dates 中,具体用法可参考官方文档:软删除

1
2
3
4
5
6
7
8
9
10
11
class Flight extends Model
{
use SoftDeletes;
/**
* 需要被转换成日期的属性。
*
* @var array
*/
protected $dates = ['deleted_at'];
}

我们先看看这个 trait:

1
2
3
4
5
6
7
8
trait SoftDeletes
{
public static function bootSoftDeletes()
{
static::addGlobalScope(new SoftDeletingScope);
}
}

如果使用了软删除,在 model 的启动过程中,就会启动软删除的这个函数。可以看出来,软删除是需要查询作用域来合作发挥作用的。我们看看这个 SoftDeletingScope :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class SoftDeletingScope implements Scope
{
protected $extensions = ['Restore', 'WithTrashed', 'WithoutTrashed', 'OnlyTrashed'];
public function apply(Builder $builder, Model $model)
{
$builder->whereNull($model->getQualifiedDeletedAtColumn());
}
public function extend(Builder $builder)
{
foreach ($this->extensions as $extension) {
$this->{"add{$extension}"}($builder);
}
$builder->onDelete(function (Builder $builder) {
$column = $this->getDeletedAtColumn($builder);
return $builder->update([
$column => $builder->getModel()->freshTimestampString(),
]);
});
}
}

apply 函数是加载全局域调用的函数,每次进行查询的时候,调用 get 函数就会自动加载这个函数,whereNull 这个查询条件会被加载到具体的 where 条件中。deleted_at 字段一般被设置为 null,在执行软删除的时候,该字段会被赋予时间格式的值,标志着被删除的时间。

在加载全局作用域的时候,还会调用 extend 函数,extend 函数为 model 添加了四个函数:

  • WithTrashed
1
2
3
4
5
6
protected function addWithTrashed(Builder $builder)
{
$builder->macro('withTrashed', function (Builder $builder) {
return $builder->withoutGlobalScope($this);
});
}

withTrashed 函数取消了软删除的全局作用域,这样我们查询数据的时候就会查询到正常数据和被软删除的数据。

  • withoutTrashed
1
2
3
4
5
6
7
8
9
10
11
12
protected function addWithoutTrashed(Builder $builder)
{
$builder->macro('withoutTrashed', function (Builder $builder) {
$model = $builder->getModel();
$builder->withoutGlobalScope($this)->whereNull(
$model->getQualifiedDeletedAtColumn()
);
return $builder;
});
}

withTrashed 函数着重强调了不要获取软删除的数据。

  • onlyTrashed
1
2
3
4
5
6
7
8
9
10
11
12
protected function addOnlyTrashed(Builder $builder)
{
$builder->macro('onlyTrashed', function (Builder $builder) {
$model = $builder->getModel();
$builder->withoutGlobalScope($this)->whereNotNull(
$model->getQualifiedDeletedAtColumn()
);
return $builder;
});
}

如果只想获取被软删除的数据,可以使用这个函数 onlyTrashed,可以看到,它使用了 whereNotNull。

  • restore
1
2
3
4
5
6
7
8
protected function addRestore(Builder $builder)
{
$builder->macro('restore', function (Builder $builder) {
$builder->withTrashed();
return $builder->update([$builder->getModel()->getDeletedAtColumn() => null]);
});
}

如果想要恢复被删除的数据,还可以使用 restore,重新将 deleted_at 数据恢复为 null。

performDeleteOnModel

SoftDeletes 这个 trait 会重载 performDeleteOnModel 函数,它将不会调用 Eloquent Builder 的 delete 方法,而是采用更新操作:

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
protected function performDeleteOnModel()
{
if ($this->forceDeleting) {
return $this->newQueryWithoutScopes()->where($this->getKeyName(), $this->getKey())->forceDelete();
}
return $this->runSoftDelete();
}
protected function runSoftDelete()
{
$query = $this->newQueryWithoutScopes()->where($this->getKeyName(), $this->getKey());
$time = $this->freshTimestamp();
$columns = [$this->getDeletedAtColumn() => $this->fromDateTime($time)];
$this->{$this->getDeletedAtColumn()} = $time;
if ($this->timestamps) {
$this->{$this->getUpdatedAtColumn()} = $time;
$columns[$this->getUpdatedAtColumn()] = $this->fromDateTime($time);
}
$query->update($columns);
}

删除操作不仅更新了 deleted_at,还更新了 updated_at 字段。

LEOYANG'S BLOG

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

发表于 2017-10-05 | 分类于 php , database , laravel | | 阅读次数

前言

前面几个博客向大家介绍了查询构造器的原理与源码,然而查询构造器更多是为 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 转换器

接下来,如果该变量被设置为 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
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\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 中还有本地作用域,只有在查询时调用才会生效。

本地作用域是由魔术方法 __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 中选取可以批量赋值的属性。所谓的可以批量赋值的属性,是指被 fillable 或 guarded 成员变量设置的参数。被放入 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();
}
}
LEOYANG'S BLOG

Laravel Database——查询构造器与语法编译器源码分析(下)

发表于 2017-10-02 | 分类于 php , database , laravel | | 阅读次数

insert 语句

insert 语句也是我们经常使用的数据库操作,它的源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public function insert(array $values)
{
if (empty($values)) {
return true;
}
if (! is_array(reset($values))) {
$values = [$values];
}
else {
foreach ($values as $key => $value) {
ksort($value);
$values[$key] = $value;
}
}
return $this->connection->insert(
$this->grammar->compileInsert($this, $values),
$this->cleanBindings(Arr::flatten($values, 1))
);
}

laravel 的 insert 是允许批量插入的,方法如下:

1
DB::table('users')->insert([['email' => 'foo', 'name' => 'taylor'], ['email' => 'bar', 'name' => 'dayle']]);

一个语句可以向数据库插入两条记录。sql 语句为:

1
2
insert into users (`email`,`name`) values ('foo', 'taylor'), ('bar', 'dayle');

因此,laravel 在处理 insert 的时候,首先会判断当前的参数是单条插入还是批量插入。

1
2
3
if (! is_array(reset($values))) {
$values = [$values];
}

reset 会返回 values 的第一个元素。如果是批量插入的话,第一个元素必然也是数组。如果的单条插入的话,第一个元素是列名与列值。因此如果是单条插入的话,会在最外层再套一个数组,统一插入的格式。

如果是批量插入的话,首先需要把插入的各个字段进行排序,保证插入时各个记录的列顺序一致。

compileInsert

对 insert 的编译也是按照批量插入的标准来进行的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public function compileInsert(Builder $query, array $values)
{
$table = $this->wrapTable($query->from);
if (! is_array(reset($values))) {
$values = [$values];
}
$columns = $this->columnize(array_keys(reset($values)));
$parameters = collect($values)->map(function ($record) {
return '('.$this->parameterize($record).')';
})->implode(', ');
return "insert into $table ($columns) values $parameters";
}

首先对插入的列名进行 columnze 函数处理,之后对每个记录的插入都调用 parameterize 函数来对列值进行处理,并用 () 包围起来。

update 语句

1
2
3
4
5
6
7
8
public function update(array $values)
{
$sql = $this->grammar->compileUpdate($this, $values);
return $this->connection->update($sql, $this->cleanBindings(
$this->grammar->prepareBindingsForUpdate($this->bindings, $values)
));
}

与插入语句相比,更新语句更加复杂,因为更新语句必然带有 where 条件,有时还会有 join 条件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public function compileUpdate(Builder $query, $values)
{
$table = $this->wrapTable($query->from);
$columns = collect($values)->map(function ($value, $key) {
return $this->wrap($key).' = '.$this->parameter($value);
})->implode(', ');
$joins = '';
if (isset($query->joins)) {
$joins = ' '.$this->compileJoins($query, $query->joins);
}
$wheres = $this->compileWheres($query);
return trim("update {$table}{$joins} set $columns $wheres");
}

updateOrInsert 语句

updateOrInsert 语句会先根据 attributes 条件查询,如果查询失败,就会合并 attributes 与 values 两个数组,并插入新的记录。如果查询成功,就会利用 values 更新数据。

1
2
3
4
5
6
7
8
public function updateOrInsert(array $attributes, array $values = [])
{
if (! $this->where($attributes)->exists()) {
return $this->insert(array_merge($attributes, $values));
}
return (bool) $this->take(1)->update($values);
}

delete 语句

删除语句比较简单,参数仅仅需要 id 即可,delete 语句会添加 id 的 where 条件:

1
2
3
4
5
6
7
8
9
10
public function delete($id = null)
{
if (! is_null($id)) {
$this->where($this->from.'.id', '=', $id);
}
return $this->connection->delete(
$this->grammar->compileDelete($this), $this->getBindings()
);
}

删除语句的编译需要先编译 where 条件:

1
2
3
4
5
6
public function compileDelete(Builder $query)
{
$wheres = is_array($query->wheres) ? $this->compileWheres($query) : '';
return trim("delete from {$this->wrapTable($query->from)} $wheres");
}

动态 where

laravel 有一个有趣的功能:动态 where。

1
DB::table('users')->whereFooBarAndBazOrQux('corge', 'waldo', 'fred')

这个语句会生成下面的 sql 语句:

1
select * from users where foo_bar = 'corge' and baz = 'waldo' or qux = 'fred';

也就是说,动态 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
public function dynamicWhere($method, $parameters)
{
$finder = substr($method, 5);
$segments = preg_split(
'/(And|Or)(?=[A-Z])/', $finder, -1, PREG_SPLIT_DELIM_CAPTURE
);
$connector = 'and';
$index = 0;
foreach ($segments as $segment) {
if ($segment !== 'And' && $segment !== 'Or') {
$this->addDynamic($segment, $connector, $parameters, $index);
$index++;
}
else {
$connector = $segment;
}
}
return $this;
}
protected function addDynamic($segment, $connector, $parameters, $index)
{
$bool = strtolower($connector);
$this->where(Str::snake($segment), '=', $parameters[$index], $bool);
}
  • 首先,程序会提取函数名 whereFooBarAndBazOrQux,删除前 5 个字符,FooBarAndBazOrQux。

  • 正则判断,根据 And 或 Or 对函数名进行切割:FooBar、And、Baz、Or、Qux。

  • 添加 where 条件,将驼峰命名改为蛇型命名。

LEOYANG'S BLOG

Laravel Database——分页原理与源码分析

发表于 2017-09-30 | 分类于 php , database , laravel | | 阅读次数

paginate 分页

laravel 的分页用起来非常简单,只需要对 query 调用 paginate 函数,把返回的对象扔给前端 blade 文件,在 blade 文件调用函数 render 函数或者 link 函数,就可以得到 上一页、下一页 等等分页特效。

实际上,我们可以简单地把分页服务看作一个前端资源,render 函数或者 link 函数的结果就是分页前端代码。

如果你还对 laravel 的分页不是很熟悉,请先阅读官方文档 : 分页。

分页服务的启动

分页功能也是由一个服务提供者所启动的,PaginationServiceProvider 就是负责注册和启动分页服务的服务提供者:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class PaginationServiceProvider extends ServiceProvider
{
public function register()
{
Paginator::viewFactoryResolver(function () {
return $this->app['view'];
});
Paginator::currentPathResolver(function () {
return $this->app['request']->url();
});
Paginator::currentPageResolver(function ($pageName = 'page') {
$page = $this->app['request']->input($pageName);
if (filter_var($page, FILTER_VALIDATE_INT) !== false && (int) $page >= 1) {
return $page;
}
return 1;
});
}
}

我们看到,服务提供者的注册函数为 Paginator 设置三个闭包函数:

  • viewFactoryResolver 为 Paginator 设置了生成前端资源的类,用于获取分页前端代码。
  • currentPathResolver 为 Paginator 设置了 url 的地址。我们知道, 上一页、下一页 等等都是可以执行翻页的操作,所以实际上这些按钮必然含有链接,而链接的地址就是当前请求的 url 地址,不同的按钮的链接地址只是 page 的参数不同而已。
  • currentPageResolver 为 Paginator 获取了当前的页数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public function boot()
{
$this->loadViewsFrom(__DIR__.'/resources/views', 'pagination');
if ($this->app->runningInConsole()) {
$this->publishes([
__DIR__.'/resources/views' => $this->app->resourcePath('views/vendor/pagination'),
], 'laravel-pagination');
}
}
protected function loadViewsFrom($path, $namespace)
{
if (is_dir($appPath = $this->app->resourcePath().'/views/vendor/'.$namespace)) {
$this->app['view']->addNamespace($namespace, $appPath);
}
$this->app['view']->addNamespace($namespace, $path);
}

服务的启动函数为分页服务设置了默认的前端分页资源。

分页服务 paginator

分页服务 paginator 函数用于 queryBuilder,用于获取分页的数据库数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public function paginate($perPage = 15, $columns = ['*'], $pageName = 'page', $page = null)
{
$page = $page ?: Paginator::resolveCurrentPage($pageName);
$total = $this->getCountForPagination($columns);
$results = $total
? $this->forPage($page, $perPage)->get($columns) : collect();
return $this->paginator($results, $total, $perPage, $page, [
'path' => Paginator::resolveCurrentPath(),
'pageName' => $pageName,
]);
}
protected function paginator($items, $total, $perPage, $currentPage, $options)
{
return Container::getInstance()->makeWith(LengthAwarePaginator::class, compact(
'items', 'total', 'perPage', 'currentPage', 'options'
));
}

也就是说,当我们写下这样的代码时:

1
DB::table('user')->select('*')->where('status',1)->paginator();

我们可以获取到一个 LengthAwarePaginator 类对象,对这个对象调用 render 函数就可以获取分页前端资源。

我们先来研究一下 paginator 函数。

获取当前页

我们可以看到,在这个函数中程序先获取当前页数:

1
2
3
4
5
6
7
8
public static function resolveCurrentPage($pageName = 'page', $default = 1)
{
if (isset(static::$currentPageResolver)) {
return call_user_func(static::$currentPageResolver, $pageName);
}
return $default;
}

currentPageResolver 就是上一节中 currentPageResolver 设置的闭包函数,这个闭包函数从请求参数中获取当前页:

1
$page = $this->app['request']->input($pageName);

获取数据库总记录数

计算数据库符合搜索条件的总记录数理所当然的是使用聚合函数 count :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public function getCountForPagination($columns = ['*'])
{
$results = $this->runPaginationCountQuery($columns);
if (isset($this->groups)) {
return count($results);
} elseif (! isset($results[0])) {
return 0;
} elseif (is_object($results[0])) {
return (int) $results[0]->aggregate;
} else {
return (int) array_change_key_case((array) $results[0])['aggregate'];
}
}
protected function runPaginationCountQuery($columns = ['*'])
{
return $this->cloneWithout(['columns', 'orders', 'limit', 'offset'])
->cloneWithoutBindings(['select', 'order'])
->setAggregate('count', $this->withoutSelectAliases($columns))
->get()->all();
}

获取当前页数据

获取当前页当然是使用 forPage 函数:

1
2
$results = $total
? $this->forPage($page, $perPage)->get($columns) : collect();

初始化 LengthAwarePaginator

paginator 函数利用 Ioc 容器来生成 LengthAwarePaginator 实例:

1
2
3
4
5
6
protected function paginator($items, $total, $perPage, $currentPage, $options)
{
return Container::getInstance()->makeWith(LengthAwarePaginator::class, compact(
'items', 'total', 'perPage', 'currentPage', 'options'
));
}

LengthAwarePaginator 的初始化:

1
2
3
4
5
6
7
8
9
10
11
12
13
public function __construct($items, $total, $perPage, $currentPage = null, array $options = [])
{
foreach ($options as $key => $value) {
$this->{$key} = $value;
}
$this->total = $total;
$this->perPage = $perPage;
$this->lastPage = max((int) ceil($total / $perPage), 1);
$this->path = $this->path !== '/' ? rtrim($this->path, '/') : $this->path;
$this->currentPage = $this->setCurrentPage($currentPage, $this->pageName);
$this->items = $items instanceof Collection ? $items : Collection::make($items);
}

分页资源 render

对 LengthAwarePaginator 调用 render 函数会得到分页所需要的前端资源:

1
2
3
4
5
6
7
public function render($view = null, $data = [])
{
return new HtmlString(static::viewFactory()->make($view ?: static::$defaultView, array_merge($data, [
'paginator' => $this,
'elements' => $this->elements(),
]))->render());
}

当我们使用默认的分页样式的时候,不需要向 render 函数传入 view 参数,此时程序会自动加载默认的前端资源:

1
public static $defaultView = 'pagination::default';

该资源的默认地址是 illuminate\Pagination\resources\views\default.blade.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
30
31
32
33
34
35
36
@if ($paginator->hasPages())
<ul class="pagination">
{{-- Previous Page Link --}}
@if ($paginator->onFirstPage())
<li class="disabled"><span><<</span></li>
@else
<li><a href="{{ $paginator->previousPageUrl() }}" rel="prev"><<</a></li>
@endif
{{-- Pagination Elements --}}
@foreach ($elements as $element)
{{-- "Three Dots" Separator --}}
@if (is_string($element))
<li class="disabled"><span>{{ $element }}</span></li>
@endif
{{-- Array Of Links --}}
@if (is_array($element))
@foreach ($element as $page => $url)
@if ($page == $paginator->currentPage())
<li class="active"><span>{{ $page }}</span></li>
@else
<li><a href="{{ $url }}">{{ $page }}</a></li>
@endif
@endforeach
@endif
@endforeach
{{-- Next Page Link --}}
@if ($paginator->hasMorePages())
<li><a href="{{ $paginator->nextPageUrl() }}" rel="next">>></a></li>
@else
<li class="disabled"><span>>></span></li>
@endif
</ul>
@endif

可以看到,分页效果的代码分为三部分:前一页、后一页、分页元素。

前一页

如果当前页是第一页的话,前一页 按钮需要置灰:

1
2
3
4
public function onFirstPage()
{
return $this->currentPage() <= 1;
}

否则的话,就要为 前一页 按钮赋予链接:

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 previousPageUrl()
{
if ($this->currentPage() > 1) {
return $this->url($this->currentPage() - 1);
}
}
public function url($page)
{
if ($page <= 0) {
$page = 1;
}
$parameters = [$this->pageName => $page];
if (count($this->query) > 0) {
$parameters = array_merge($this->query, $parameters);
}
return $this->path
.(Str::contains($this->path, '?') ? '&' : '?')
.http_build_query($parameters, '', '&')
.$this->buildFragment();
}

如果列表页中存在一些搜索条件,这些搜索条件会被加载到 $this->query 成员变量中,生成 url 的时候,这些搜索添加会被加到 request 的参数中。可以使用 append 方法附加查询参数到分页链接中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public function appends($key, $value = null)
{
if (is_array($key)) {
return $this->appendArray($key);
}
return $this->addQuery($key, $value);
}
protected function appendArray(array $keys)
{
foreach ($keys as $key => $value) {
$this->addQuery($key, $value);
}
return $this;
}

下一页

与 前一页 类似,如果已经在最后一页,那么 下一页 按钮将会被置灰:

1
2
3
4
public function hasMorePages()
{
return $this->currentPage() < $this->lastPage();
}

下一页的链接:

1
2
3
4
5
6
public function nextPageUrl()
{
if ($this->lastPage() > $this->currentPage()) {
return $this->url($this->currentPage() + 1);
}
}

上一页 与 下一页 按钮的功能比较简单,至于中间的分页特效比较复杂,我们由下一节来说。

分页 elements

我们先说一下不同的分页样式:

  • 当我们设置两侧页数为 3 时,当前数据总页数小于 8 页时分页效果:

  • 总页数大于 6 页,且当前页在前 8 页(2 * 3 + 2)时分页效果:

img

  • 当前页在前 6 页与后 6 页之间分页效果:

img

  • 当前页在最后 6 页时分页效果:

img

分页效果样式的关键来源于 UrlWindow,这个类用于根据总页数与当前页的不同来控制不同的分页样式。

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
protected function elements()
{
$window = UrlWindow::make($this);
return array_filter([
$window['first'],
is_array($window['slider']) ? '...' : null,
$window['slider'],
is_array($window['last']) ? '...' : null,
$window['last'],
]);
}
public static function make(PaginatorContract $paginator, $onEachSide = 3)
{
return (new static($paginator))->get($onEachSide);
}
public function get($onEachSide = 3)
{
if ($this->paginator->lastPage() < ($onEachSide * 2) + 6) {
return $this->getSmallSlider();
}
return $this->getUrlSlider($onEachSide);
}

小型分页 getSmallSlider

如果当前总页数小于 ($onEachSide * 2) + 6 的话,就会调用小型分页效果,这种小型分页效果直接将所有页数全部显示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
protected function getSmallSlider()
{
return [
'first' => $this->paginator->getUrlRange(1, $this->lastPage()),
'slider' => null,
'last' => null,
];
}
public function getUrlRange($start, $end)
{
return collect(range($start, $end))->mapWithKeys(function ($page) {
return [$page => $this->url($page)];
})->all();
}

CloseToBeginning 分页效果

当前页数位于前 ($onEachSide * 2) 页时:

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
protected function getUrlSlider($onEachSide)
{
$window = $onEachSide * 2;
if (! $this->hasPages()) {
return ['first' => null, 'slider' => null, 'last' => null];
}
if ($this->currentPage() <= $window) {
return $this->getSliderTooCloseToBeginning($window);
}
elseif ($this->currentPage() > ($this->lastPage() - $window)) {
return $this->getSliderTooCloseToEnding($window);
}
return $this->getFullSlider($onEachSide);
}
protected function getSliderTooCloseToBeginning($window)
{
return [
'first' => $this->paginator->getUrlRange(1, $window + 2),
'slider' => null,
'last' => $this->getFinish(),
];
}
public function getFinish()
{
return $this->paginator->getUrlRange(
$this->lastPage() - 1,
$this->lastPage()
);
}

假设我们设置当前两侧页数为 3,当前页为 5,总页数22,函数 getSliderTooCloseToBeginning 返回结果为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
return [
'first' => [
1 => '/www.example.com/example?page=1',
2 => '/www.example.com/example?page=2'
3 => '/www.example.com/example?page=3'
4 => '/www.example.com/example?page=4'
5 => '/www.example.com/example?page=5'
6 => '/www.example.com/example?page=6'
7 => '/www.example.com/example?page=7'
8 => '/www.example.com/example?page=8'],
'slider' => null,
'last' => [
21 => '/www.example.com/example?page=21',
22 => '/www.example.com/example?page=22'],
];

这个时候 element 函数返回数据:

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
protected function elements()
{
$window = UrlWindow::make($this);
return array_filter([
$window['first'],
is_array($window['slider']) ? '...' : null,
$window['slider'],
is_array($window['last']) ? '...' : null,
$window['last'],
]);
}
//返回结果
[
[
1 => '/www.example.com/example?page=1',
2 => '/www.example.com/example?page=2',
3 => '/www.example.com/example?page=3',
4 => '/www.example.com/example?page=4',
5 => '/www.example.com/example?page=5',
6 => '/www.example.com/example?page=6',
7 => '/www.example.com/example?page=7',
8 => '/www.example.com/example?page=8',
], //$window['first']
‘...’, //is_array($window['last']) ? '...' : null
[
21 => '/www.example.com/example?page=21',
22 => '/www.example.com/example?page=22',
], //$window['last']
]

TooCloseToEnding 分页效果

当前页数位于后 ($onEachSide * 2) 页时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
protected function getSliderTooCloseToEnding($window)
{
$last = $this->paginator->getUrlRange(
$this->lastPage() - ($window + 2),
$this->lastPage()
);
return [
'first' => $this->getStart(),
'slider' => null,
'last' => $last,
];
}
public function getStart()
{
return $this->paginator->getUrlRange(1, 2);
}

假设我们设置当前两侧页数为 3,当前页为 18,总页数22,函数 getSliderTooCloseToEnding 返回结果为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
return [
'first' => [
1 => '/www.example.com/example?page=1',
2 => '/www.example.com/example?page=2'
],
'slider' => null,
'last' => [
15 => '/www.example.com/example?page=15',
16 => '/www.example.com/example?page=16',
17 => '/www.example.com/example?page=17',
18 => '/www.example.com/example?page=18',
19 => '/www.example.com/example?page=19',
20 => '/www.example.com/example?page=20',
21 => '/www.example.com/example?page=21',
22 => '/www.example.com/example?page=22',
],
];

这个时候 element 函数返回数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[
[
1 => '/www.example.com/example?page=1',
2 => '/www.example.com/example?page=2'
],
'...',
[
15 => '/www.example.com/example?page=15',
16 => '/www.example.com/example?page=16',
17 => '/www.example.com/example?page=17',
18 => '/www.example.com/example?page=18',
19 => '/www.example.com/example?page=19',
20 => '/www.example.com/example?page=20',
21 => '/www.example.com/example?page=21',
22 => '/www.example.com/example?page=22',
]
]

FullSlider 分页效果

当前页数位于中间时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
protected function getFullSlider($onEachSide)
{
return [
'first' => $this->getStart(),
'slider' => $this->getAdjacentUrlRange($onEachSide),
'last' => $this->getFinish(),
];
}
public function getAdjacentUrlRange($onEachSide)
{
return $this->paginator->getUrlRange(
$this->currentPage() - $onEachSide,
$this->currentPage() + $onEachSide
);
}

假设我们设置当前两侧页数为 3,当前页为 10,总页数22,函数 getFullSlider 返回结果为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
return [
'first' => [
1 => '/www.example.com/example?page=1',
2 => '/www.example.com/example?page=2'
],
'slider' => [
7 => '/www.example.com/example?page=7',
8 => '/www.example.com/example?page=8',
9 => '/www.example.com/example?page=9',
10 => '/www.example.com/example?page=10',
11 => '/www.example.com/example?page=11',
12 => '/www.example.com/example?page=12',
13 => '/www.example.com/example?page=13',
],
'last' => [
21 => '/www.example.com/example?page=21',
22 => '/www.example.com/example?page=22',
],
];

这个时候 element 函数返回数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[
[
1 => '/www.example.com/example?page=1',
2 => '/www.example.com/example?page=2'
],
'...',
[
7 => '/www.example.com/example?page=7',
8 => '/www.example.com/example?page=8',
9 => '/www.example.com/example?page=9',
10 => '/www.example.com/example?page=10',
11 => '/www.example.com/example?page=11',
12 => '/www.example.com/example?page=12',
13 => '/www.example.com/example?page=13',
],
'...',
[
21 => '/www.example.com/example?page=21',
22 => '/www.example.com/example?page=22',
]
]

simplePaginate 简单分页

简单分页相比以上的功能来说,精简了 elements 的特效:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public function simplePaginate($perPage = 15, $columns = ['*'], $pageName = 'page', $page = null)
{
$page = $page ?: Paginator::resolveCurrentPage($pageName);
$this->skip(($page - 1) * $perPage)->take($perPage + 1);
return $this->simplePaginator($this->get($columns), $perPage, $page, [
'path' => Paginator::resolveCurrentPath(),
'pageName' => $pageName,
]);
}
protected function simplePaginator($items, $perPage, $currentPage, $options)
{
return Container::getInstance()->makeWith(Paginator::class, compact(
'items', 'perPage', 'currentPage', 'options'
));
}

分页服务的类不再使用 LengthAwarePaginator 类,而开始使用 Paginator,这两个类最大的不同在于 render 函数:

1
2
3
4
5
6
7
8
9
10
public static $defaultSimpleView = 'pagination::simple-default';
public function render($view = null, $data = [])
{
return new HtmlString(
static::viewFactory()->make($view ?: static::$defaultSimpleView, array_merge($data, [
'paginator' => $this,
]))->render()
);
}

render 函数调用的前端资源默认地址为 illuminate\Pagination\resources\views\simple-default.blade.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@if ($paginator->hasPages())
<ul class="pagination">
{{-- Previous Page Link --}}
@if ($paginator->onFirstPage())
<li class="disabled"><span>@lang('pagination.previous')</span></li>
@else
<li><a href="{{ $paginator->previousPageUrl() }}" rel="prev">@lang('pagination.previous')</a></li>
@endif
{{-- Next Page Link --}}
@if ($paginator->hasMorePages())
<li><a href="{{ $paginator->nextPageUrl() }}" rel="next">@lang('pagination.next')</a></li>
@else
<li class="disabled"><span>@lang('pagination.next')</span></li>
@endif
</ul>
@endif

可以看到,简单分页只有 上一页、下一页 两个按钮。

LEOYANG'S BLOG

Laravel Database——查询构造器与语法编译器源码分析(中)

发表于 2017-09-26 | 分类于 php , database , laravel | | 阅读次数

join 语句

join 语句对数据库进行连接操作,join 函数的连接条件可以非常简单:

1
DB::table('services')->select('*')->join('translations AS t', 't.item_id', '=', 'services.id');

也可以比较复杂:

1
2
3
4
5
6
7
8
9
10
DB::table('users')->select('*')->join('contacts', function ($j) {
$j->on('users.id', '=', 'contacts.id')->orOn('users.name', '=', 'contacts.name');
});
//select * from "users" inner join "contacts" on "users"."id" = "contacts"."id" or "users"."name" = "contacts"."name"
$builder = $this->getBuilder();
DB::table('users')->select('*')->from('users')->joinWhere('contacts', 'col1', function ($j) {
$j->select('users.col2')->from('users')->where('users.id', '=', 'foo')
});
//select * from "users" inner join "contacts" on "col1" = (select "users"."col2" from "users" where "users"."id" = foo)

还可以更加复杂:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
DB::table('users')->select('*')->leftJoin('contacts', function ($j) {
$j->on('users.id', '=', 'contacts.id')->where(function ($j) {
$j->where('contacts.country', '=', 'US')->orWhere('contacts.is_partner', '=', 1);
});
});
//select * from "users" left join "contacts" on "users"."id" = "contacts"."id" and ("contacts"."country" = 'US' or "contacts"."is_partner" = 1)
DB::table('users')->select('*')->leftJoin('contacts', function ($j) {
$j->on('users.id', '=', 'contacts.id')->where('contacts.is_active', '=', 1)->orOn(function ($j) {
$j->orWhere(function ($j) {
$j->where('contacts.country', '=', 'UK')->orOn('contacts.type', '=', 'users.type');
})->where(function ($j) {
$j->where('contacts.country', '=', 'US')->orWhereNull('contacts.is_partner');
});
});
});
//select * from "users" left join "contacts" on "users"."id" = "contacts"."id" and "contacts"."is_active" = 1 or (("contacts"."country" = 'UK' or "contacts"."type" = "users"."type") and ("contacts"."country" = 'US' or "contacts"."is_partner" is null))

其实 join 语句与 where 语句非常相似,将 join 语句的连接条件看作 where 的查询条件完全可以,接下来我们看看源码。

join 语句

从上面的示例代码可以看出,join 函数的参数多变,第二个参数可以是列名,也有可能是闭包函数。当第二个参数是列名的时候,第三个参数可以是闭包,还可以是符号 =、>=。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public function join($table, $first, $operator = null, $second = null, $type = 'inner', $where = false)
{
$join = new JoinClause($this, $type, $table);
if ($first instanceof Closure) {
call_user_func($first, $join);
$this->joins[] = $join;
$this->addBinding($join->getBindings(), 'join');
}
else {
$method = $where ? 'where' : 'on';
$this->joins[] = $join->$method($first, $operator, $second);
$this->addBinding($join->getBindings(), 'join');
}
return $this;
}

可以看到,程序首先新建了一个 JoinClause 类对象,这个类实际上继承 queryBuilder,也就是说 queryBuilder 上的很多方法它都可以直接用,例如 where、whereNull、whereDate 等等。

1
2
3
class JoinClause extends Builder
{
}

如果第二个参数是闭包函数的话,就会像查询组一样根据查询条件更新 $join。

如果第二个参数是列名,那么就会调用 on 方法或 where 方法。这两个方法的区别是,on 方法只支持 whereColumn方法和 whereNested,也就是说只能写出 join on col1 = col2 这样的语句,而 where 方法可以传递数组、子查询等等.

1
2
3
4
5
6
7
8
9
10
11
12
13
public function on($first, $operator = null, $second = null, $boolean = 'and')
{
if ($first instanceof Closure) {
return $this->whereNested($first, $boolean);
}
return $this->whereColumn($first, $operator, $second, $boolean);
}
public function orOn($first, $operator = null, $second = null)
{
return $this->on($first, $operator, $second, 'or');
}

grammer——compileJoins

接下来我们来看看如何编译 join 语句:

1
2
3
4
5
6
7
8
protected function compileJoins(Builder $query, $joins)
{
return collect($joins)->map(function ($join) use ($query) {
$table = $this->wrapTable($join->table);
return trim("{$join->type} join {$table} {$this->compileWheres($join)}");
})->implode(' ');
}

可以看到,JoinClause 在编译中是作为 queryBuild 对象来看待的。

union 语句

union 用于合并两个或多个 SELECT 语句的结果集。Union 因为要进行重复值扫描,所以效率低。如果合并没有刻意要删除重复行,那么就使用 Union All。

我们在 laravel 中可以这样使用:

1
2
3
4
5
6
7
8
9
10
11
$query = DB::table('users')->select('*')->where('id', '=', 1);
$query->union(DB::table('users')->select('*')->where('id', '=', 2));
//(select * from `users` where `id` = 1) union (select * from `users` where `id` = 2)
```
还可以添加多个 `union` 语句:
```php
$query = DB::table('users')->select('*')->where('id', '=', 1);
$query->union(DB::table('users')->select('*')->where('id', '=', 2));
$query->union(DB::table('users')->select('*')->where('id', '=', 3));
//(select * from "users" where "id" = 1) union (select * from "users" where "id" = 2) union (select * from "users" where "id" = 3)

union 语句可以与 orderBy 相结合:

1
2
3
4
$query = DB::table('users')->select('*')->where('id', '=', 1);
$query->union(DB::table('users')->select('*')->where('id', '=', 2));
$query->orderBy('id', 'desc');
//(select * from `users` where `id` = ?) union (select * from `users` where `id` = ?) order by `id` desc

union 语句可以与 limit、offset 相结合:

1
2
3
4
$query = DB::table('users')->select('*');
$query->union(DB::table('users')->select('*'));
$builder->skip(5)->take(10);
//(select * from `users`) union (select * from `dogs`) limit 10 offset 5

union 函数

union 函数比较简单:

1
2
3
4
5
6
7
8
9
10
11
12
public function union($query, $all = false)
{
if ($query instanceof Closure) {
call_user_func($query, $query = $this->newQuery());
}
$this->unions[] = compact('query', 'all');
$this->addBinding($query->getBindings(), 'union');
return $this;
}

grammer——compileUnions

语法编译器对 union 的处理:

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
public function compileSelect(Builder $query)
{
$sql = parent::compileSelect($query);
if ($query->unions) {
$sql = '('.$sql.') '.$this->compileUnions($query);
}
return $sql;
}
protected function compileUnions(Builder $query)
{
$sql = '';
foreach ($query->unions as $union) {
$sql .= $this->compileUnion($union);
}
if (! empty($query->unionOrders)) {
$sql .= ' '.$this->compileOrders($query, $query->unionOrders);
}
if (isset($query->unionLimit)) {
$sql .= ' '.$this->compileLimit($query, $query->unionLimit);
}
if (isset($query->unionOffset)) {
$sql .= ' '.$this->compileOffset($query, $query->unionOffset);
}
return ltrim($sql);
}
protected function compileUnion(array $union)
{
$conjuction = $union['all'] ? ' union all ' : ' union ';
return $conjuction.'('.$union['query']->toSql().')';
}

可以看出,union 的处理比较简单,都是调用 query->toSql 语句而已。值得注意的是,在处理 union 的时候,要特别处理 order、limit、offset。

orderBy 语句

orderBy 语句用法很简单,可以设置多个排序字段,也可以用原生排序语句:

1
2
3
DB::table('users')->select('*')->orderBy('email')->orderBy('age', 'desc');
DB::table('users')->select('*')->orderBy('email')->orderByRaw('age desc');

orderBy 函数

如果当前查询中有 union 的话,排序的变量会被放入 unionOrders 数组中,这个数组只有在 compileUnions 函数中才会被编译成 sql 语句。否则会被放入 orders 数组中,这时会被 compileOrders 处理:

1
2
3
4
5
6
7
8
9
public function orderBy($column, $direction = 'asc')
{
$this->{$this->unions ? 'unionOrders' : 'orders'}[] = [
'column' => $column,
'direction' => strtolower($direction) == 'asc' ? 'asc' : 'desc',
];
return $this;
}

grammer——compileOrders

orderBy 的编译也很简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
protected function compileOrders(Builder $query, $orders)
{
if (! empty($orders)) {
return 'order by '.implode(', ', $this->compileOrdersToArray($query, $orders));
}
return '';
}
protected function compileOrdersToArray(Builder $query, $orders)
{
return array_map(function ($order) {
return ! isset($order['sql'])
? $this->wrap($order['column']).' '.$order['direction']
: $order['sql'];
}, $orders);
}

limit offset forPage 语句

limit offset 或者 skip take 用法很简单,有趣的是,laravel 考虑了负数的情况:

1
2
3
4
5
6
7
DB::select('*')->from('users')->offset(5)->limit(10);
DB::select('*')->from('users')->skip(5)->take(10);
DB::select('*')->from('users')->skip(-5)->take(-10);
DB::select('*')->from('users')->forPage(5, 10);

limit offset 函数

和 orderBy 一样,如果当前查询中有 union 的话,limit / offset 会被放入 unionLimit / unionOffset 中,在编译 union 的时候解析:

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
public function take($value)
{
return $this->limit($value);
}
public function limit($value)
{
$property = $this->unions ? 'unionLimit' : 'limit';
if ($value >= 0) {
$this->$property = $value;
}
return $this;
}
public function skip($value)
{
return $this->offset($value);
}
public function offset($value)
{
$property = $this->unions ? 'unionOffset' : 'offset';
$this->$property = max(0, $value);
return $this;
}
public function forPage($page, $perPage = 15)
{
return $this->skip(($page - 1) * $perPage)->take($perPage);
}

grammer——compileLimit compileOffset

这个不能再简单了:

1
2
3
4
5
6
7
8
9
protected function compileLimit(Builder $query, $limit)
{
return 'limit '.(int) $limit;
}
protected function compileOffset(Builder $query, $offset)
{
return 'offset '.(int) $offset;
}

group 语句

groupBy 语句的参数形式有多种:

1
2
3
4
5
6
7
DB::select('*')->from('users')->groupBy('email');
DB::select('*')->from('users')->groupBy('id', 'email');
DB::select('*')->from('users')->groupBy(['id', 'email']);
DB::select('*')->from('users')->groupBy(new Raw('DATE(created_at)'));

groupBy 函数很简单,仅仅是为 $this->groups 成员变量合并数组:

1
2
3
4
5
6
7
8
9
10
11
public function groupBy(...$groups)
{
foreach ($groups as $group) {
$this->groups = array_merge(
(array) $this->groups,
Arr::wrap($group)
);
}
return $this;
}

语法编译器的处理:

1
2
3
4
protected function compileGroups(Builder $query, $groups)
{
return 'group by '.$this->columnize($groups);
}

having 语句

having 语句的用法也很简单。大致有 having、orHaving、havingRaw、orHavingRaw 这几个函数:

1
2
3
4
5
6
7
8
9
DB::select('*')->from('users')->having('email', '>', 1);
DB::select('*')->from('users')->groupBy('email')->having('email', '>', 1);
DB::select('*')->from('users')->having('email', 1)->orHaving('email', 2);
DB::select('*')->from('users')->havingRaw('user_foo < user_bar');
DB::select('*')->from('users')->having('baz', '=', 1)->orHavingRaw('user_foo < user_bar');

having 函数

having 函数大致与 whereColumn 相同:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public function having($column, $operator = null, $value = null, $boolean = 'and')
{
$type = 'Basic';
list($value, $operator) = $this->prepareValueAndOperator(
$value, $operator, func_num_args() == 2
);
if ($this->invalidOperator($operator)) {
list($value, $operator) = [$operator, '='];
}
$this->havings[] = compact('type', 'column', 'operator', 'value', 'boolean');
if (! $value instanceof Expression) {
$this->addBinding($value, 'having');
}
return $this;
}

havingRaw 函数:

1
2
3
4
5
6
7
8
9
10
public function havingRaw($sql, array $bindings = [], $boolean = 'and')
{
$type = 'Raw';
$this->havings[] = compact('type', 'sql', 'boolean');
$this->addBinding($bindings, 'having');
return $this;
}

grammer——compileHavings

语法编译器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
protected function compileHavings(Builder $query, $havings)
{
$sql = implode(' ', array_map([$this, 'compileHaving'], $havings));
return 'having '.$this->removeLeadingBoolean($sql);
}
protected function compileHaving(array $having)
{
if ($having['type'] === 'Raw') {
return $having['boolean'].' '.$having['sql'];
}
return $this->compileBasicHaving($having);
}
protected function compileBasicHaving($having)
{
$column = $this->wrap($having['column']);
$parameter = $this->parameter($having['value']);
return $having['boolean'].' '.$column.' '.$having['operator'].' '.$parameter;
}

when / tap / unless 语句

when 语句可以根据条件来判断是否执行查询条件,unless 与 when 相反,第一个参数是 false 才会调用闭包函数执行查询,tap 指定 when 的第一参数永远为真:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$callback = function ($query, $condition) {
$this->assertEquals($condition, 'truthy');
$query->where('id', '=', 1);
};
$default = function ($query, $condition) {
$this->assertEquals($condition, 0);
$query->where('id', '=', 2);
};
DB::select('*')->from('users')->when('truthy', $callback, $default)->where('email', 'foo');
DB::select('*')->from('users')->tap($callback)->where('email', 'foo');
DB::select('*')->from('users')->unless('truthy', $callback, $default)->where('email', 'foo');

when、unless、tap 函数的实现:

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 when($value, $callback, $default = null)
{
if ($value) {
return $callback($this, $value) ?: $this;
} elseif ($default) {
return $default($this, $value) ?: $this;
}
return $this;
}
public function unless($value, $callback, $default = null)
{
if (! $value) {
return $callback($this, $value) ?: $this;
} elseif ($default) {
return $default($this, $value) ?: $this;
}
return $this;
}
public function tap($callback)
{
return $this->when(true, $callback);
}

Aggregate 查询

聚合方法也是 sql 的重要组成部分,laravel 提供 count、max、min、avg、sum、exist 等聚合方法:

1
2
3
4
5
6
7
8
9
DB::table('users')->count();//select count(*) as aggregate from "users"
DB::table('users')->max('id');//select max("id") as aggregate from "users"
DB::table('users')->min('id');//select min("id") as aggregate from "users"
DB::table('users')->sum('id');//select sum("id") as aggregate from "users"
DB::table('users')->exists();//select exists(select * from "users") as "exists"

这些聚合函数实际上都是调用 aggregate 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public function count($columns = '*')
{
return (int) $this->aggregate(__FUNCTION__, Arr::wrap($columns));
}
public function aggregate($function, $columns = ['*'])
{
$results = $this->cloneWithout(['columns'])
->cloneWithoutBindings(['select'])
->setAggregate($function, $columns)
->get($columns);
if (! $results->isEmpty()) {
return array_change_key_case((array) $results[0])['aggregate'];
}
}

可以看出来,aggregate 函数复制了一份 queryBuilder,只是缺少了 select、bingding 成员变量:

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
public function cloneWithout(array $properties)
{
return tap(clone $this, function ($clone) use ($properties) {
foreach ($properties as $property) {
$clone->{$property} = null;
}
});
}
public function cloneWithoutBindings(array $except)
{
return tap(clone $this, function ($clone) use ($except) {
foreach ($except as $type) {
$clone->bindings[$type] = [];
}
});
}
protected function setAggregate($function, $columns)
{
$this->aggregate = compact('function', 'columns');
if (empty($this->groups)) {
$this->orders = null;
$this->bindings['order'] = [];
}
return $this;
}

exist 聚合函数和其他不一样,它的流程与 whereExist 大致相同:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public function exists()
{
$results = $this->connection->select(
$this->grammar->compileExists($this), $this->getBindings(), ! $this->useWritePdo
);
if (isset($results[0])) {
$results = (array) $results[0];
return (bool) $results['exists'];
}
return false;
}

grammer——compileAggregate

laravel 的聚合函数具有独占性,也就是说调用聚合函数后,不能再 select 其他的列:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
protected function compileAggregate(Builder $query, $aggregate)
{
$column = $this->columnize($aggregate['columns']);
if ($query->distinct && $column !== '*') {
$column = 'distinct '.$column;
}
return 'select '.$aggregate['function'].'('.$column.') as aggregate';
}
protected function compileColumns(Builder $query, $columns)
{
if (! is_null($query->aggregate)) {
return;
}
$select = $query->distinct ? 'select distinct ' : 'select ';
return $select.$this->columnize($columns);
}

可以看到,如果存在聚合函数,那么编译 select 的 compileColumns 函数将不会运行。

first / find / value / pluck / implode

从数据库中取出后数据,我们可以使用 laravel 提供给我们的一些函数进行包装处理。

first 函数可以让我们只查询第一条:

1
DB::table('users')->where('id', '=', 1)->first();

find 函数,可以利用数据库表的主键来查询第一条:

1
DB::table('users')->find(1);

pluck 函数可以取查询记录的某一列:

1
DB::table('users')->where('id', '=', 1)->pluck('foo');/['bar', 'baz']

pluck 函数取查询记录的某一列的同时,还可以设置列名的 key:

1
DB::table('users')->where('id', '=', 1)->pluck('foo', 'id');//[1 => 'bar', 10 => 'baz']

value 函数可以取第一条数据的某一列:

1
DB::table('users')->where('id', '=', 1)->value('foo');//bar

implode 函数可以将多条数据的某一列拼成字符串:

1
DB::table('users')->where('id', '=', 1)->implode('foo', ',');//'bar,baz'

first 函数

find 函数,使用了 limit 1 的 sql 语句:

1
2
3
4
public function first($columns = ['*'])
{
return $this->take(1)->get($columns)->first();
}

find 函数

find 函数实际利用主键调用 first 函数:

1
2
3
4
public function find($id, $columns = ['*'])
{
return $this->where('id', '=', $id)->first($columns);
}

pluck 函数

pluck 函数主要对得到的数据调用 pluck 函数:

1
2
3
4
5
6
7
8
9
public function pluck($column, $key = null)
{
$results = $this->get(is_null($key) ? [$column] : [$column, $key]);
return $results->pluck(
$this->stripTableForPluck($column),
$this->stripTableForPluck($key)
);
}

implod 函数

implod 函数对一维数组调用 implod 函数:

1
2
3
4
public function implode($column, $glue = '')
{
return $this->pluck($column)->implode($glue);
}

chunk 语句

如果你需要操作数千条数据库记录,可以考虑使用 chunk 方法。这个方法每次只取出一小块结果,并会将每个块传递给一个闭包处理。

1
2
3
4
5
DB::table('users')->orderBy('id')->chunk(100, function ($users) {
foreach ($users as $user) {
//
}
});

你可以从 闭包 中返回 false,以停止对后续分块的处理:

1
2
3
4
5
6
DB::table('users')->orderBy('id')->chunk(100, function ($users) {
// Process the records...
if (...) {
return false;
}
});

如果不想按照主键 id 来进行分块,我们还可以自定义分块主键:

1
2
3
4
5
DB::table('users')->orderBy('id')->chunkById(100, function ($users) {
foreach ($users as $user) {
//
}
}, 'someIdField');

chunk 函数

chunk 函数的实现实际上是 forPage 函数,当从数据库获得数据后,先判断是否拿到了数据,如果拿到了就会继续执行闭包函数,否则就会中断程序。执行闭包函数后,需要判断返回状态。若取出的数据小于分块的条数,说明数据已经全部获取完毕,结束程序。

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
public function chunk($count, callable $callback)
{
$this->enforceOrderBy();
$page = 1;
do {
$results = $this->forPage($page, $count)->get();
$countResults = $results->count();
if ($countResults == 0) {
break;
}
if ($callback($results, $page) === false) {
return false;
}
unset($results);
$page++;
} while ($countResults == $count);
return true;
}
protected function enforceOrderBy()
{
if (empty($this->query->orders) && empty($this->query->unionOrders)) {
$this->orderBy($this->model->getQualifiedKeyName(), 'asc');
}
}

enforceOrderBy 函数是用于数据按照主键的大小进行排序。

chunkById 函数

chunkById 函数与 chunk 函数唯一不同的是 forPage 函数被换成了 forPageAfterId 函数,目的是替换主键:

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
public function chunkById($count, callable $callback, $column = 'id', $alias = null)
{
$alias = $alias ?: $column;
$lastId = 0;
do {
$clone = clone $this;
$results = $clone->forPageAfterId($count, $lastId, $column)->get();
$countResults = $results->count();
if ($countResults == 0) {
break;
}
if ($callback($results) === false) {
return false;
}
$lastId = $results->last()->{$alias};
unset($results);
} while ($countResults == $count);
return true;
}

forPageAfterId 函数实际上是把 offset 函数删除,并按照自定义的列来排序,每次获取最后一条数据的自定义列的数值,利用 where 条件不断获取下一部分分块数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public function forPageAfterId($perPage = 15, $lastId = 0, $column = 'id')
{
$this->orders = $this->removeExistingOrdersFor($column);
return $this->where($column, '>', $lastId)
->orderBy($column, 'asc')
->take($perPage);
}
protected function removeExistingOrdersFor($column)
{
return Collection::make($this->orders)
->reject(function ($order) use ($column) {
return isset($order['column'])
? $order['column'] === $column : false;
})->values()->all();
}
LEOYANG'S BLOG

Laravel Database——查询构造器与语法编译器源码分析(上)

发表于 2017-09-19 | 分类于 php , database , laravel | | 阅读次数

前言

在前两个文章中,我们分析了数据库的连接启动与数据库底层 CRUD 的原理,底层数据库服务支持原生 sql 的运行。本文以 mysql 为例,向大家讲述支持 Fluent 的查询构造器 query 与语法编译器 grammer 的原理。

DB::table 与 查询构造器

若是不想使用原生的 sql 语句,我们可以使用 DB::table 语句,该语句会返回一个 query 对象:

1
2
3
4
5
6
7
8
9
10
11
public function table($table)
{
return $this->query()->from($table);
}
public function query()
{
return new QueryBuilder(
$this, $this->getQueryGrammar(), $this->getPostProcessor()
);
}

我们可以看到,query 会有两个成员,queryGrammar 与 postProcessor。queryGrammar 负责对 QueryBuilcder 的结果进行 sql 语言的转化,postProcessor 负责查询结果的后处理。

之所以 laravel 推荐我们使用查询构造器,而不是原生的 sql,原因在于可以避免 sql 注入漏洞。当然,我们也可以在使用 DB::select() 函数中手动写 bindings 的值,但是这样的话,我们写 sql 的语句是就必须是这样:

1
DB::select('select * from table where col=?',[1]);

必然会带来很多不便。

有了查询构造器,我们就可以写出 fluent 类型的语句:

1
DB::table('table')->select('*')->where('col', 1);

是不是很方便?

CRUD 与语法编译器

相应于 connection 对象的 CRUD,语法编译器有 compileInsert、compileSelect、compileUpdate、compileDelete。其中最重要的是 compileSelect,因为它不仅负责了 select 语句的语法编译,还负责聚合语句 aggregate、from 语句、join 连接语句、wheres 条件语句、groups 分组语句、havings 条件语句、orders 排序语句、limit 语句、offset 语句、unions 联合语句、lock 语句:

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
47
protected $selectComponents = [
'aggregate',
'columns',
'from',
'joins',
'wheres',
'groups',
'havings',
'orders',
'limit',
'offset',
'unions',
'lock',
];
public function compileSelect(Builder $query)
{
$original = $query->columns;
if (is_null($query->columns)) {
$query->columns = ['*'];
}
$sql = trim($this->concatenate(
$this->compileComponents($query))
);
$query->columns = $original;
return $sql;
}
protected function compileComponents(Builder $query)
{
$sql = [];
foreach ($this->selectComponents as $component) {
if (! is_null($query->$component)) {
$method = 'compile'.ucfirst($component);
$sql[$component] = $this->$method($query, $query->$component);
}
}
return $sql;
}

可以看出来,语法编译器会将上述所有的语句放入 $sql[] 成员中,然后通过 concatenate 函数组装成 sql 语句:

1
2
3
4
5
6
protected function concatenate($segments)
{
return implode(' ', array_filter($segments, function ($value) {
return (string) $value !== '';
}));
}

wrap 函数

若想要了解语法编译器,我们就必须先要了解 grammer 中一个重要的函数 wrap,这个函数专门对表名与列名进行处理,

1
2
3
4
5
6
7
8
9
10
11
12
public function wrap($value, $prefixAlias = false)
{
if ($this->isExpression($value)) {
return $this->getValue($value);
}
if (strpos(strtolower($value), ' as ') !== false) {
return $this->wrapAliasedValue($value, $prefixAlias);
}
return $this->wrapSegments(explode('.', $value));
}

处理的流程:

  • 若是 Expression 对象,利用函数 getValue 直接取出对象值,不对其进行任何处理,用于处理原生 sql。expression 对象的作用是保护原始参数,避免框架解析的一种方式。也就是说,当我们用了 expression 来包装参数的话,laravel 将不会对其进行任何处理,包括库名解析、表名前缀、别名等。
  • 若表名/列名存在 as,则利用函数 wrapAliasedValue 为表名设置别名。
  • 若表名/列名含有 .,则会被分解为 库名/表名,或者 表名/列名,并调用函数 wrapSegments。

wrapAliasedValue 函数

wrapAliasedValue 函数用于处理别名:

1
2
3
4
5
6
7
8
9
10
11
12
protected function wrapAliasedValue($value, $prefixAlias = false)
{
$segments = preg_split('/\s+as\s+/i', $value);
if ($prefixAlias) {
$segments[1] = $this->tablePrefix.$segments[1];
}
return $this->wrap(
$segments[0]).' as '.$this->wrapValue($segments[1]
);
}

可以看到,首先程序会根据 as 将字符串分为两部分,as 前的部分递归调用 wrap 函数,as 后的部分调用 wrapValue 函数.

wrapValue 函数

wrapValue 函数用来处理添加符号 ",例如 table,会被这个函数变为 "table"。需要注意的是 table1"table2 这种情况,假如我们的数据库中存在一个表,名字就叫做: table1"table2,我们在数据库查询的时候,必须将表名转化为 "table1""table2",只有这样,数据库才会有效地转化表名为 "table1"table2",否则数据库就会报告错误:找不到表。

1
2
3
4
5
6
7
8
protected function wrapValue($value)
{
if ($value !== '*') {
return '"'.str_replace('"', '""', $value).'"';
}
return $value;
}

wrapSegments 函数

wrapSegments 函数会判断当前参数,如果是 table.column,会将前一部分 table 调用 wrapTable, column 调用 wrapValue,最后生成 “table”."column"。

1
2
3
4
5
6
7
8
protected function wrapSegments($segments)
{
return collect($segments)->map(function ($segment, $key) use ($segments) {
return $key == 0 && count($segments) > 1
? $this->wrapTable($segment)
: $this->wrapValue($segment);
})->implode('.');
}

wrapTable 函数

wrapTable 函数用于为数据表添加表前缀:

1
2
3
4
5
6
7
8
public function wrapTable($table)
{
if (! $this->isExpression($table)) {
return $this->wrap($this->tablePrefix.$table, true);
}
return $this->getValue($table);
}

wrap 整体流程图如下:

Markdown

from 语句

我们看到 DB::table 实际上是调用了查询构造器的 from 函数。接下来我们就看看,当我们写下了

1
DB::table('table')->get()

时发生了什么。

1
2
3
4
5
6
public function from($table)
{
$this->from = $table;
return $this;
}

我们看到,from 函数极其简单,我们接下来看 get:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public function get($columns = ['*'])
{
$original = $this->columns;
if (is_null($original)) {
$this->columns = $columns;
}
$results = $this->processor->processSelect($this, $this->runSelect());
$this->columns = $original;
return collect($results);
}

laravel 的查询构造器是懒加载的,只有调用了 get 函数才会真正的调用语法编译器,采用调用底层 connection 对象进行数据库查询:

1
2
3
4
5
6
7
8
9
10
11
protected function runSelect()
{
return $this->connection->select(
$this->toSql(), $this->getBindings(), ! $this->useWritePdo
);
}
public function toSql()
{
return $this->grammar->compileSelect($this);
}

compileFrom 函数

首先我们先看看流程图:

Markdown

语法编译器 grammer 对于 from 语句的处理由函数 compileFrom 负责:

1
2
3
4
protected function compileFrom(Builder $query, $table)
{
return 'from '.$this->wrapTable($table);
}

从流程图可以看出,具体流程与 wrap 类似。

我们调用 from 时,可以传递两种参数,一种是字符串,另一种是 expression 对象:

1
2
DB::table('table');
DB::table(new Expression('table'));
  • 传递 expression 对象

当我们传递 expression 对象的时候,grammer 就会调用 getValue 取出原生 sql 语句。

  • 传递字符串

当我们向 from 传递普通的字符串时,laravel 就会对字符串调用 wrap 函数进行处理,处理流程上一个小节已经说明:

  • 为表名加上前缀 $this->tablePrefix
  • 若字符串存在 as,则为表名设置别名。
  • 若字符串含有 .,则会被分解为 库名 与 表名,并进行分别调用 wrapTable 函数与 wrapValue 进行处理。
  • 为表名前后添加 ",例如 t1.t2 会被转化为 "t1"."t2"(不同的数据库添加的字符不同,mysql 就不是 ")

laravel 对 from 处理流程存在一些问题,表名前缀设置功能与数据库名功能公用存在问题,相关 issue 地址是:[Bug] Table prefix added to database name when using database.table,有任何兴趣的同学可以在这个 issue 里面讨论,或者直接向作者提 PR。若作者对此部分有任何修改,我会同步修改这篇文章。

Select 语句

本小节会介绍 select 语句:

1
2
3
4
5
6
public function select($columns = ['*'])
{
$this->columns = is_array($columns) ? $columns : func_get_args();
return $this;
}

queryBuilder 的 select 语句很简单,我们不多讨论.

selectRaw :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public function selectRaw($expression, array $bindings = [])
{
$this->addSelect(new Expression($expression));
if ($bindings) {
$this->addBinding($bindings, 'select');
}
return $this;
}
public function addSelect($column)
{
$column = is_array($column) ? $column : func_get_args();
$this->columns = array_merge((array) $this->columns, $column);
return $this;
}

可以看到, selectRaw 就是将 Expression 对象赋值到 columns 中,我们在前面说到,框架不会对 Expression 进行任何处理(更准确的说是 wrap 函数),这样就保证了原生语句的执行。

我们接着看 grammer .

compileColumns 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
protected function compileColumns(Builder $query, $columns)
{
if (! is_null($query->aggregate)) {
return;
}
$select = $query->distinct ? 'select distinct ' : 'select ';
return $select.$this->columnize($columns);
}
public function columnize(array $columns)
{
return implode(', ', array_map([$this, 'wrap'], $columns));
}

可以看到,grammer 对 select 的语法编译调用 wrap 函数对每个 select 的字段进行处理,处理过程在上面详解过,在此不再赘述。

selectSub 语句

所谓的 select 子查询,就是查询的字段来源于其他数据表。对于这种查询,可以分成两部来理解,首先忽略整个select子查询,查出第一个表中的数据,然后根据第一个表的数据执行子查询,

laravel 的 selectSub 支持闭包函数、queryBuild 对象或者原生 sql 语句,以下是单元测试样例:

1
2
3
4
5
$query = DB::table('one')->select(['foo', 'bar'])->where('key', '=', 'val');
$query->selectSub(function ($query) {
$query->from('two')->select('baz')->where('subkey', '=', 'subval');
}, 'sub');

另一种写法:

1
2
3
4
$query = DB::table('one')->select(['foo', 'bar'])->where('key', '=', 'val');
$query_sub = DB::table('one')->select('baz')->where('subkey', '=', 'subval');
$query->selectSub($query_sub, 'sub');

生成的 sql:

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
select "foo", "bar", (select "baz" from "two" where "subkey" = 'subval') as "sub" from "one" where "key" = 'val'
```
`selectSub` 语句的实现比较简单:
```php
public function selectSub($query, $as)
{
if ($query instanceof Closure) {
$callback = $query;
$callback($query = $this->forSubQuery());
}
list($query, $bindings) = $this->parseSubSelect($query);
return $this->selectRaw(
'('.$query.') as '.$this->grammar->wrap($as), $bindings
);
}
protected function parseSubSelect($query)
{
if ($query instanceof self) {
$query->columns = [$query->columns[0]];
return [$query->toSql(), $query->getBindings()];
} elseif (is_string($query)) {
return [$query, []];
} else {
throw new InvalidArgumentException;
}
}

可以看到,如果 selectSub 的参数是闭包函数,那么就会先执行闭包函数,闭包函数将会为 query 根据查询语句更新对象。

parseSubSelect 函数为子查询解析 sql 语句与 binding 变量。

where 语句总结

在 laravel 文档中,queryBuild 的用法很详尽,但是为了更好的理解源码,我们在这里再次大概的总结一下:

基础用法

1
2
users = DB::table('users')->where('votes', '=', 100)->get();
$users = DB::table('users')->where('votes', 100)->get();

这两个是等价的写法。

where 数组

1
2
3
4
5
6
7
8
9
10
11
12
13
$users = DB::table('users')->where(
['status' => 1, 'subscribed' => 1],
)->get();
$users = DB::table('users')->where([
['status', '1'],
['subscribed', '1'],
])->get();
$users = DB::table('users')->where([
['status', '1'],
['subscribed', '<>', '1'],
])->get();

where 查询组

1
2
3
4
5
6
7
DB::table('users')
->where('name', '=', 'John')
->orWhere(function ($query) {
$query->where('votes', '>', 100)
->where('title', '<>', 'Admin');
})
->get();

这一句的 sql 语句是

1
select * from users where name = 'John' or (votes > 100 and title <> 'Admin')

where 子查询

1
2
3
4
5
6
7
8
public function testFullSubSelects()
{
$builder = $this->getBuilder();
DB::table('users')
->Where('id', '=', function ($q) {
$q->select(new Raw('max(id)'))->from('users')->where('email', '=', 'bar');
});
}

这一句的 sql 语句是

1
select * from "users" where "email" = foo or "id" = (select max(id) from "users" where "email" = bar`

orWhere

1
2
3
4
$users = DB::table('users')
->where('votes', '>', 100)
->orWhere('name', 'John')
->get();

whereDate / whereMonth / whereDay / whereYear / whereTime

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$users = DB::table('users')
->whereDate('created_at', '2016-12-31')
->get();
$users = DB::table('users')
->whereMonth('created_at', '12')
->get();
$users = DB::table('users')
->whereDay('created_at', '31')
->get();
$users = DB::table('users')
->whereYear('created_at', '2016')
->get();
$users = DB::table('users')
->whereTime('created_at', '>=', '22:00')
->get();

whereBetween / whereNotBetween

1
2
3
4
5
6
$users = DB::table('users')
->whereBetween('votes', [1, 100])->get();
$users = DB::table('users')
->whereNotBetween('votes', [1, 100])
->get();

whereRaw / orWhereRaw

1
2
3
4
5
6
7
$users = DB::table('users')
->whereRaw('id = ? or email = ?', [1, 'foo'])
->get();
$users = DB::table('users')
->orWhereRaw('id = ? or email = ?', [1, 'foo'])
->get();

whereIn / whereNotIn / orWhereIn / orWhereNotIn

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$users = DB::table('users')
->whereIn('id', [1, 2, 3])
->get();
$users = DB::table('users')
->whereNotIn('id', [1, 2, 3])
->get();
$users = DB::table('users')
->whereIn('id', function ($q) {
$q->select('id')->from('users')->where('age', '>', 25)->take(3);
});
$users = DB::table('users')
->whereNotIn('id', function ($q) {
$q->select('id')->from('users')->where('age', '>', 25)->take(3);
});
$query = DB::table('users')->select('id')->where('age', '>', 25)->take(3);
$users = DB::table('users')->whereIn('id', $query);
$users = DB::table('users')->whereNotIn('id', $query);

有意思的是,当我们在 whereIn / whereNotIn / orWhereIn / orWhereNotIn 中传入空数组的时候:

1
2
3
4
5
6
7
$users = DB::table('users')
->whereIn('id', [])
->get();
$users = DB::table('users')
->orWhereIn('id', [])
->get();

这个时候,框架自动会生成如下的 sql:

1
2
3
select * from "users" where 0 = 1;
select * from "users" where "id" = ? or 0 = 1

whereColumn

1
2
3
4
5
6
7
8
9
10
11
12
13
$users = DB::table('users')
->whereColumn('first_name', 'last_name')
->get();
$users = DB::table('users')
->whereColumn('updated_at', '>', 'created_at')
->get();
$users = DB::table('users')
->whereColumn([
['first_name', '=', 'last_name'],
['updated_at', '>', 'created_at']
])->get();

whereNull / whereNotNull / orWhereNull / orWhereNotNull

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$users = DB::table('users')
->whereNull('updated_at')
->get();
$users = DB::table('users')
->whereNotNull('updated_at')
->get();
$users = DB::table('users')
->orWhereNull('updated_at')
->get();
$users = DB::table('users')
->orWhereNotNull('updated_at')
->get();

whereExists / whereNotExists / orWhereExists / orWhereNotExists

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
DB::table('users')
->whereExists(function ($query) {
$query->select(DB::raw(1))
->from('orders')
->whereRaw('orders.user_id = users.id');
})
->get();
// select * from users where exists ( select 1 from orders where orders.user_id = users.id)
DB::table('users')
->whereNotExists(function ($query) {
$query->select(DB::raw(1))
->from('orders')
->whereRaw('orders.user_id = users.id');
})
->get();
// select * from users where not exists ( select 1 from orders where orders.user_id = users.id)
DB::table('users')
->orWhereExists(function ($query) {
$query->select(DB::raw(1))
->from('orders')
->whereRaw('orders.user_id = users.id');
})
->get();
// select * from users or exists ( select 1 from orders where orders.user_id = users.id)
DB::table('users')
->orWhereNotExists(function ($query) {
$query->select(DB::raw(1))
->from('orders')
->whereRaw('orders.user_id = users.id');
})
->get();
// select * from users or not exists ( select 1 from orders where orders.user_id = users.id)

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
37
38
39
40
41
42
public function where($column, $operator = null, $value = null, $boolean = 'and')
{
if (is_array($column)) {
return $this->addArrayOfWheres($column, $boolean);
}
list($value, $operator) = $this->prepareValueAndOperator(
$value, $operator, func_num_args() == 2
);
if ($column instanceof Closure) {
return $this->whereNested($column, $boolean);
}
if ($this->invalidOperator($operator)) {
list($value, $operator) = [$operator, '='];
}
if ($value instanceof Closure) {
return $this->whereSub($column, $operator, $value, $boolean);
}
if (is_null($value)) {
return $this->whereNull($column, $boolean, $operator !== '=');
}
if (Str::contains($column, '->') && is_bool($value)) {
$value = new Expression($value ? 'true' : 'false');
}
$type = 'Basic';
$this->wheres[] = compact(
'type', 'column', 'operator', 'value', 'boolean'
);
if (! $value instanceof Expression) {
$this->addBinding($value, 'where');
}
return $this;
}

可以看到,为了支持框架的多种 where 形式,where 的代码中写了很多的条件语句。我们接下来一个个分析。

grammer——compileWheres 函数

在此之前,我们先看看语法编译器对 where 查询的处理:

1
2
3
4
5
6
7
8
9
10
11
12
protected function compileWheres(Builder $query)
{
if (is_null($query->wheres)) {
return '';
}
if (count($sql = $this->compileWheresToArray($query)) > 0) {
return $this->concatenateWhereClauses($query, $sql);
}
return '';
}

compileWheres 函数负责所有 where 查询条件的语法编译工作,compileWheresToArray 函数负责循环编译查询条件,concatenateWhereClauses 函数负责将多个查询条件合并。

1
2
3
4
5
6
7
8
9
10
11
12
13
protected function compileWheresToArray($query)
{
return collect($query->wheres)->map(function ($where) use ($query) {
return $where['boolean'].' '.$this->{"where{$where['type']}"}($query, $where);
})->all();
}
protected function concatenateWhereClauses($query, $sql)
{
$conjunction = $query instanceof JoinClause ? 'on' : 'where';
return $conjunction.' '.$this->removeLeadingBoolean(implode(' ', $sql));
}

compileWheresToArray 函数负责把 $query->wheres 中多个 where 条件循环起来:

  • $where['boolean'] 是多个查询条件的连接,and 或者 or,一般 where 条件默认为 and,各种 orWhere 的连接是 or
  • where{$where['type']} 是查询的类型,laravel 把查询条件分为以下几类:base、raw、in、notIn、inSub、notInSub、null、notNull、between、column、nested、sub、exist、notExist。每种类型的查询条件都有对应的 grammer 方法

concatenateWhereClauses 函数负责连接所有的搜索条件,由于 join 的连接条件也会调用 compileWheres 函数,所以会有判断是否是真正的 where 查询,

where 数组

如果 column 是数组的话,就会调用:

1
2
3
4
5
6
7
8
9
10
11
12
protected function addArrayOfWheres($column, $boolean, $method = 'where')
{
return $this->whereNested(function ($query) use ($column, $method, $boolean) {
foreach ($column as $key => $value) {
if (is_numeric($key) && is_array($value)) {
$query->{$method}(...array_values($value));
} else {
$query->$method($key, '=', $value, $boolean);
}
}
}, $boolean);
}

可以看到,数组分为两类,一种是列名为 key,例如 ['foo' => 1, 'bar' => 2],这个时候就是调用 query->where('foo', '=', '1', ‘and’)。还有一种是 [['foo','1'],['bar','2']],这个时候就会调用 $query->where(['foo','1'])。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public function whereNested(Closure $callback, $boolean = 'and')
{
call_user_func($callback, $query = $this->forNestedWhere());
return $this->addNestedWhereQuery($query, $boolean);
}
public function addNestedWhereQuery($query, $boolean = 'and')
{
if (count($query->wheres)) {
$type = 'Nested';
$this->wheres[] = compact('type', 'query', 'boolean');
$this->addBinding($query->getBindings(), 'where');
}
return $this;
}

grammer——whereNested

语法编译器中负责查询组的函数是 whereNested,它会取出 where 中的 query,递归调用 compileWheres 函数

1
2
3
4
5
6
protected function whereNested(Builder $query, $where)
{
$offset = $query instanceof JoinClause ? 3 : 6;
return '('.substr($this->compileWheres($where['query']), $offset).')';
}

由于 compileWheres 会返回 where ... 或者 on ... 等开头的 sql 语句,所以我们需要把返回结果截取前3个字符或6个字符。

where 查询组

若查询条件是一个闭包函数,也就是第一个参数 column 是个闭包函数,那么就要调用 whereNested 函数,过程和上述过程一致。

whereSub 子查询

如果第二个参数或者第三个参数是一个闭包函数的话,就是 where 子查询语句,这时需要调用 whereSub 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected function whereSub($column, $operator, Closure $callback, $boolean)
{
$type = 'Sub';
call_user_func($callback, $query = $this->forSubQuery());
$this->wheres[] = compact(
'type', 'column', 'operator', 'query', 'boolean'
);
$this->addBinding($query->getBindings(), 'where');
return $this;
}

grammer——whereSub

grammer 中负责子查询的是 whereSub 函数:

1
2
3
4
5
6
protected function whereSub(Builder $query, $where)
{
$select = $this->compileSelect($where['query']);
return $this->wrap($where['column']).' '.$where['operator']." ($select)";
}

因为子查询中可以存在 select 、where 、join 等一切 sql 语句,所以递归的是 compileSelect 这个大的函数,而不是仅仅 compileWheres。

whereNull 语句

whereNull 函数也很简单:

1
2
3
4
5
6
7
8
public function whereNull($column, $boolean = 'and', $not = false)
{
$type = $not ? 'NotNull' : 'Null';
$this->wheres[] = compact('type', 'column', 'boolean');
return $this;
}

grammer——whereNull 函数

1
2
3
4
protected function whereNull(Builder $query, $where)
{
return $this->wrap($where['column']).' is null';
}

whereBasic 语句

如果上述情况都不符合,那么就是最基础的 where 语句,类型是 basic.

1
2
3
4
5
6
7
8
9
$type = 'Basic';
$this->wheres[] = compact(
'type', 'column', 'operator', 'value', 'boolean'
);
if (! $value instanceof Expression) {
$this->addBinding($value, 'where');
}

grammer——whereBasic 函数

grammer 中最基础的 where 语句由 wherebasic 函数负责:

1
2
3
4
5
6
7
8
9
10
11
protected function whereBasic(Builder $query, $where)
{
$value = $this->parameter($where['value']);
return $this->wrap($where['column']).' '.$where['operator'].' '.$value;
}
public function parameter($value)
{
return $this->isExpression($value) ? $this->getValue($value) : '?';
}

wherebasic 函数对参数进行了替换,利用 ? 来替换真正的值。

orWhere 语句

orWhere 函数只是在 where 函数的基础上固定了最后一个参数:

1
2
3
4
public function orWhere($column, $operator = null, $value = null)
{
return $this->where($column, $operator, $value, 'or');
}

whereColumn 语句

whereColumn 函数是简化版的 where 函数,只是 where 类型不是 basic,而是 column:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public function whereColumn($first, $operator = null, $second = null, $boolean = 'and')
{
if (is_array($first)) {
return $this->addArrayOfWheres($first, $boolean, 'whereColumn');
}
if ($this->invalidOperator($operator)) {
list($second, $operator) = [$operator, '='];
}
$type = 'Column';
$this->wheres[] = compact(
'type', 'first', 'operator', 'second', 'boolean'
);
return $this;
}

grammer——whereColumn

可以看到 whereColumn 与 whereBasic 的区别是对 value 的不同处理,whereBasic 实际上是将其看作值,需要用 ? 来替换,参数加载到 binding 中去的。而 whereColumn 是将 second 当做列名来处理,是需要经过表名、别名等处理的:

1
2
3
4
protected function whereColumn(Builder $query, $where)
{
return $this->wrap($where['first']).' '.$where['operator'].' '.$this->wrap($where['second']);
}

whereIn 语句

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
public function whereIn($column, $values, $boolean = 'and', $not = false)
{
$type = $not ? 'NotIn' : 'In';
if ($values instanceof EloquentBuilder) {
$values = $values->getQuery();
}
if ($values instanceof self) {
return $this->whereInExistingQuery(
$column, $values, $boolean, $not
);
}
if ($values instanceof Closure) {
return $this->whereInSub($column, $values, $boolean, $not);
}
if ($values instanceof Arrayable) {
$values = $values->toArray();
}
$this->wheres[] = compact('type', 'column', 'values', 'boolean');
foreach ($values as $value) {
if (! $value instanceof Expression) {
$this->addBinding($value, 'where');
}
}
return $this;
}

可以看出来,whereIn 支持四种参数: EloquentBuilder、queryBuilder、Closure、Arrayable。

whereInExistingQuery 函数

当 whereIn 第二个参数是 queryBuild 时,就会调用 whereInExistingQuery 函数:

1
2
3
4
5
6
7
8
9
10
protected function whereInExistingQuery($column, $query, $boolean, $not)
{
$type = $not ? 'NotInSub' : 'InSub';
$this->wheres[] = compact('type', 'column', 'query', 'boolean');
$this->addBinding($query->getBindings(), 'where');
return $this;
}

可以看出,这个函数添加了一个类型为 InSub 或 NotInSub 类型的 where,我们接着在语法编译器来看:

grammer——whereInSub / whereNotInSub

whereInSub / whereNotInSub 与 whereSub 类似,只是 operator 被固定成 in / not in 而已:

1
2
3
4
5
6
7
8
9
protected function whereInSub(Builder $query, $where)
{
return $this->wrap($where['column']).' in ('.$this->compileSelect($where['query']).')';
}
protected function whereNotInSub(Builder $query, $where)
{
return $this->wrap($where['column']).' not in ('.$this->compileSelect($where['query']).')';
}

whereInSub 函数

当 whereIn 第二个参数是闭包函数的时候,就会调用 whereInSub 函数:

1
2
3
4
5
6
7
8
9
10
11
12
protected function whereInSub($column, Closure $callback, $boolean, $not)
{
$type = $not ? 'NotInSub' : 'InSub';
call_user_func($callback, $query = $this->forSubQuery());
$this->wheres[] = compact('type', 'column', 'query', 'boolean');
$this->addBinding($query->getBindings(), 'where');
return $this;
}

可以看出来,除了闭包函数需要执行获得 query 对象之外,whereInSub 函数与 whereInExistingQuery 函数一致。

In / NotIn 类型 where

如果参数传递了数组,那么就会创建 In 或者 NotIn 类型的 where:

1
2
3
4
5
6
7
$this->wheres[] = compact('type', 'column', 'values', 'boolean');
foreach ($values as $value) {
if (! $value instanceof Expression) {
$this->addBinding($value, 'where');
}
}

grammer——whereIn / whereNotIn

whereIn 或者 whereNotIn 与 whereBasic 函数基本一致,只是参数值需要循环调用 parameter 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
protected function whereIn(Builder $query, $where)
{
if (! empty($where['values'])) {
return $this->wrap($where['column']).' in ('.$this->parameterize($where['values']).')';
}
return '0 = 1';
}
protected function whereNotIn(Builder $query, $where)
{
if (! empty($where['values'])) {
return $this->wrap($where['column']).' not in ('.$this->parameterize($where['values']).')';
}
return '1 = 1';
}
public function parameterize(array $values)
{
return implode(', ', array_map([$this, 'parameter'], $values));
}

whereBetween 语句

类似的 whereBetween 创建了新的 between 类型的 where:

1
2
3
4
5
6
7
8
9
10
public function whereBetween($column, array $values, $boolean = 'and', $not = false)
{
$type = 'between';
$this->wheres[] = compact('column', 'type', 'boolean', 'not');
$this->addBinding($values, 'where');
return $this;
}

grammer——whereBetween

有意思的是,whereBetween 不支持原生的参数值,也就是不支持 expression 对象,所以直接用 ? 来代替参数值:

1
2
3
4
5
6
protected function whereBetween(Builder $query, $where)
{
$between = $where['not'] ? 'not between' : 'between';
return $this->wrap($where['column']).' '.$between.' ? and ?';
}

whereExist 语句

whereExist 只支持闭包函数作为子查询语句,和之前一样,创建了 Exist 或者 NotExist 类型的 where

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public function whereExists(Closure $callback, $boolean = 'and', $not = false)
{
$query = $this->forSubQuery();
call_user_func($callback, $query);
return $this->addWhereExistsQuery($query, $boolean, $not);
}
public function addWhereExistsQuery(Builder $query, $boolean = 'and', $not = false)
{
$type = $not ? 'NotExists' : 'Exists';
$this->wheres[] = compact('type', 'operator', 'query', 'boolean');
$this->addBinding($query->getBindings(), 'where');
return $this;
}

grammer——whereExists / whereNotExists

可以看到,这部分的语法编译器仍然和 whereSub 非常类似:

1
2
3
4
5
6
7
8
9
protected function whereExists(Builder $query, $where)
{
return 'exists ('.$this->compileSelect($where['query']).')';
}
protected function whereNotExists(Builder $query, $where)
{
return 'not exists ('.$this->compileSelect($where['query']).')';
}

whereDate / whereMonth / whereDay / whereYear / whereTime

关于时间的查询条件都由函数 addDateBasedWhere 负责创建新的类型的 where,由于这些函数大致相同,我这里只贴一种:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
protected function addDateBasedWhere($type, $column, $operator, $value, $boolean = 'and')
{
$this->wheres[] = compact('column', 'type', 'boolean', 'operator', 'value');
$this->addBinding($value, 'where');
return $this;
}
public function whereDate($column, $operator, $value = null, $boolean = 'and')
{
list($value, $operator) = $this->prepareValueAndOperator(
$value, $operator, func_num_args() == 2
);
return $this->addDateBasedWhere('Date', $column, $operator, $value, $boolean);
}
...

grammer——dateBasedWhere

时间查询条件都是利用数据库的 date、month、day、year、time 函数实现的:

1
2
3
4
5
6
7
8
9
10
11
protected function whereTime(Builder $query, $where)
{
return $this->dateBasedWhere('time', $query, $where);
}
protected function dateBasedWhere($type, Builder $query, $where)
{
$value = $this->parameter($where['value']);
return $type.'('.$this->wrap($where['column']).') '.$where['operator'].' '.$value;
}

whereRaw 语句

原生的 sql 语句及其简单:

1
2
3
4
5
6
7
8
public function whereRaw($sql, $bindings = [], $boolean = 'and')
{
$this->wheres[] = ['type' => 'raw', 'sql' => $sql, 'boolean' => $boolean];
$this->addBinding((array) $bindings, 'where');
return $this;
}

语法编译器工作:

1
2
3
4
protected function whereRaw(Builder $query, $where)
{
return $where['sql'];
}
LEOYANG'S BLOG

Laravel Database——数据库的 CRUD 操作

发表于 2017-09-16 | 分类于 php , database , laravel | | 阅读次数

前言

当 connection 对象构建初始化完成后,我们就可以利用 DB 来进行数据库的 CRUD ( Create、Retrieve、Update、Delete)操作。本篇文章,我们将会讲述 laravel 如何与 pdo 交互,实现基本数据库服务的原理。

run

laravel 中任何数据库的操作都要经过 run 这个函数,这个函数作用在于重新连接数据库、记录数据库日志、数据库异常处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
protected function run($query, $bindings, Closure $callback)
{
$this->reconnectIfMissingConnection();
$start = microtime(true);
try {
$result = $this->runQueryCallback($query, $bindings, $callback);
} catch (QueryException $e) {
$result = $this->handleQueryException(
$e, $query, $bindings, $callback
);
}
$this->logQuery(
$query, $bindings, $this->getElapsedTime($start)
);
return $result;
}

重新连接数据库 reconnect

如果当期的 pdo 是空,那么就会调用 reconnector 重新与数据库进行连接:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
protected function reconnectIfMissingConnection()
{
if (is_null($this->pdo)) {
$this->reconnect();
}
}
public function reconnect()
{
if (is_callable($this->reconnector)) {
return call_user_func($this->reconnector, $this);
}
throw new LogicException('Lost connection and no reconnector available.');
}

运行数据库操作

数据库的 curd 操作会被包装成为一个闭包函数,作为 runQueryCallback 的一个参数,当运行正常时,会返回结果,如果遇到异常的话,会将异常转化为 QueryException,并且抛出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected function runQueryCallback($query, $bindings, Closure $callback)
{
try {
$result = $callback($query, $bindings);
}
catch (Exception $e) {
throw new QueryException(
$query, $this->prepareBindings($bindings), $e
);
}
return $result;
}

数据库异常处理

当 pdo 查询返回异常的时候,如果当前是事务进行时,那么直接返回异常,让上一层事务来处理。

如果是由于与数据库事情连接导致的异常,那么就要重新与数据库进行连接:

1
2
3
4
5
6
7
8
9
10
protected function handleQueryException($e, $query, $bindings, Closure $callback)
{
if ($this->transactions >= 1) {
throw $e;
}
return $this->tryAgainIfCausedByLostConnection(
$e, $query, $bindings, $callback
);
}

与数据库失去连接:

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
protected function tryAgainIfCausedByLostConnection(QueryException $e, $query, $bindings, Closure $callback)
{
if ($this->causedByLostConnection($e->getPrevious())) {
$this->reconnect();
return $this->runQueryCallback($query, $bindings, $callback);
}
throw $e;
}
protected function causedByLostConnection(Exception $e)
{
$message = $e->getMessage();
return Str::contains($message, [
'server has gone away',
'no connection to the server',
'Lost connection',
'is dead or not enabled',
'Error while sending',
'decryption failed or bad record mac',
'server closed the connection unexpectedly',
'SSL connection has been closed unexpectedly',
'Error writing data to the connection',
'Resource deadlock avoided',
'Transaction() on null',
'child connection forced to terminate due to client_idle_limit',
]);
}

数据库日志

1
2
3
4
5
6
7
8
public function logQuery($query, $bindings, $time = null)
{
$this->event(new QueryExecuted($query, $bindings, $time, $this));
if ($this->loggingQueries) {
$this->queryLog[] = compact('query', 'bindings', 'time');
}
}

想要开启或关闭日志功能:

1
2
3
4
5
6
7
8
9
public function enableQueryLog()
{
$this->loggingQueries = true;
}
public function disableQueryLog()
{
$this->loggingQueries = false;
}

Select 查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public function select($query, $bindings = [], $useReadPdo = true)
{
return $this->run($query, $bindings, function ($query, $bindings) use ($useReadPdo) {
if ($this->pretending()) {
return [];
}
$statement = $this->prepared($this->getPdoForSelect($useReadPdo)
->prepare($query));
$this->bindValues($statement, $this->prepareBindings($bindings));
$statement->execute();
return $statement->fetchAll();
});
}

数据库的查询主要有一下几个步骤:

  • 获取 $this->pdo 成员变量,若当前未连接数据库,则进行数据库连接,获取 pdo 对象。
  • 设置 pdo 数据 fetch 模式
  • pdo 进行 sql 语句预处理,pdo 绑定参数
  • sql 语句执行,并获取数据。

getPdoForSelect 获取 pdo 对象

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
protected function getPdoForSelect($useReadPdo = true)
{
return $useReadPdo ? $this->getReadPdo() : $this->getPdo();
}
public function getPdo()
{
if ($this->pdo instanceof Closure) {
return $this->pdo = call_user_func($this->pdo);
}
return $this->pdo;
}
public function getReadPdo()
{
if ($this->transactions > 0) {
return $this->getPdo();
}
if ($this->getConfig('sticky') && $this->recordsModified) {
return $this->getPdo();
}
if ($this->readPdo instanceof Closure) {
return $this->readPdo = call_user_func($this->readPdo);
}
return $this->readPdo ?: $this->getPdo();
}

getPdo 这里逻辑比较简单,值得我们注意的是 getReadPdo。为了减缓数据库的压力,我们常常对数据库进行读写分离,也就是只要当写数据库这种操作发生时,才会使用写数据库,否则都会用读数据库。这种措施减少了数据库的压力,但是也带来了一些问题,那就是读写两个数据库在一定时间内会出现数据不一致的情况,原因就是写库的数据未能及时推送给读库,造成读库数据延迟的现象。为了在一定程度上解决这类问题,laravel 增添了 sticky 选项,

从程序中我们可以看出,当我们设置选项 sticky 为真,并且的确对数据库进行了写操作后,getReadPdo 会强制返回主库的连接,这样就避免了读写分离造成的延迟问题。

还有一种情况,当数据库在执行事务期间,所有的读取操作也会被强制连接主库。

prepared 设置数据获取方式

1
2
3
4
5
6
7
8
9
10
11
protected $fetchMode = PDO::FETCH_OBJ;
protected function prepared(PDOStatement $statement)
{
$statement->setFetchMode($this->fetchMode);
$this->event(new Events\StatementPrepared(
$this, $statement
));
return $statement;
}

pdo 的 setFetchMode 函数用于为语句设置默认的获取模式,通常模式有一下几种:

  • PDO::FETCH_ASSOC //从结果集中获取以列名为索引的关联数组。
  • PDO::FETCH_NUM //从结果集中获取一个以列在行中的数值偏移量为索引的值数组。
  • PDO::FETCH_BOTH //这是默认值,包含上面两种数组。
  • PDO::FETCH_OBJ //从结果集当前行的记录中获取其属性对应各个列名的一个对象。
  • PDO::FETCH_BOUND //使用fetch()返回TRUE,并将获取的列值赋给在bindParm()方法中指定的相应变量。
  • PDO::FETCH_LAZY //创建关联数组和索引数组,以及包含列属性的一个对象,从而可以在这三种接口中任选一种。

pdo 的 prepare 函数

prepare 函数会为 PDOStatement::execute() 方法准备要执行的 SQL 语句,SQL 语句可以包含零个或多个命名(:name)或问号(?)参数标记,参数在SQL执行时会被替换。

不能在 SQL 语句中同时包含命名(:name)或问号(?)参数标记,只能选择其中一种风格。

预处理 SQL 语句中的参数在使用 PDOStatement::execute() 方法时会传递真实的参数。

之所以使用 prepare 函数,是因为这个函数可以防止 SQL 注入,并且可以加快同一查询语句的速度。关于预处理与参数绑定防止 SQL 漏洞注入的原理可以参考:Web安全之SQL注入攻击技巧与防范.

pdo 的 bindValues 函数

在调用 pdo 的参数绑定函数之前,laravel 对参数值进一步进行了优化,把时间类型的对象利用 grammer 的设置重新格式化,false 也改为0。

pdo 的参数绑定函数 bindValue,对于使用命名占位符的预处理语句,应是类似 :name 形式的参数名。对于使用问号占位符的预处理语句,应是以1开始索引的参数位置。

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 prepareBindings(array $bindings)
{
$grammar = $this->getQueryGrammar();
foreach ($bindings as $key => $value) {
if ($value instanceof DateTimeInterface) {
$bindings[$key] = $value->format($grammar->getDateFormat());
} elseif ($value === false) {
$bindings[$key] = 0;
}
}
return $bindings;
}
public function bindValues($statement, $bindings)
{
foreach ($bindings as $key => $value) {
$statement->bindValue(
is_string($key) ? $key : $key + 1, $value,
is_int($value) ? PDO::PARAM_INT : PDO::PARAM_STR
);
}
}

insert

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public function insert($query, $bindings = [])
{
return $this->statement($query, $bindings);
}
public function statement($query, $bindings = [])
{
return $this->run($query, $bindings, function ($query, $bindings) {
if ($this->pretending()) {
return true;
}
$statement = $this->getPdo()->prepare($query);
$this->bindValues($statement, $this->prepareBindings($bindings));
$this->recordsHaveBeenModified();
return $statement->execute();
});
}

这部分的代码与 select 非常相似,不同之处有一下几个:

  • 直接获取写库的连接,不会考虑读库
  • 由于不需要返回任何数据库数据,因此也不必设置 fetchMode。
  • recordsHaveBeenModified 函数标志当前连接数据库已被写入。
  • 不需要调用函数 fetchAll
1
2
3
4
5
6
public function recordsHaveBeenModified($value = true)
{
if (! $this->recordsModified) {
$this->recordsModified = $value;
}
}

update、delete

affectingStatement 这个函数与上面的 statement 函数一致,只是最后会返回更新、删除影响的行数。

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
public function update($query, $bindings = [])
{
return $this->affectingStatement($query, $bindings);
}
public function delete($query, $bindings = [])
{
return $this->affectingStatement($query, $bindings);
}
public function affectingStatement($query, $bindings = [])
{
return $this->run($query, $bindings, function ($query, $bindings) {
if ($this->pretending()) {
return 0;
}
$statement = $this->getPdo()->prepare($query);
$this->bindValues($statement, $this->prepareBindings($bindings));
$statement->execute();
$this->recordsHaveBeenModified(
($count = $statement->rowCount()) > 0
);
return $count;
});
}

transaction 数据库事务

为保持数据的一致性,对于重要的数据我们经常使用数据库事务,transaction 函数接受一个闭包函数,与一个重复尝试的次数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public function transaction(Closure $callback, $attempts = 1)
{
for ($currentAttempt = 1; $currentAttempt <= $attempts; $currentAttempt++) {
$this->beginTransaction();
try {
return tap($callback($this), function ($result) {
$this->commit();
});
}
catch (Exception $e) {
$this->handleTransactionException(
$e, $currentAttempt, $attempts
);
} catch (Throwable $e) {
$this->rollBack();
throw $e;
}
}
}

开始事务

数据库事务中非常重要的成员变量是 $this->transactions,它标志着当前事务的进程:

1
2
3
4
5
6
7
8
public function beginTransaction()
{
$this->createTransaction();
++$this->transactions;
$this->fireConnectionEvent('beganTransaction');
}

可以看出,当创建事务成功后,就会累加 $this->transactions,并且启动 event,创建事务:

1
2
3
4
5
6
7
8
9
10
11
12
protected function createTransaction()
{
if ($this->transactions == 0) {
try {
$this->getPdo()->beginTransaction();
} catch (Exception $e) {
$this->handleBeginTransactionException($e);
}
} elseif ($this->transactions >= 1 && $this->queryGrammar->supportsSavepoints()) {
$this->createSavepoint();
}
}

如果当前没有任何事务,那么就会调用 pdo 来开启事务。

如果当前已经在事务保护的范围内,那么就会创建 SAVEPOINT,实现数据库嵌套事务:

1
2
3
4
5
6
7
8
9
10
11
protected function createSavepoint()
{
$this->getPdo()->exec(
$this->queryGrammar->compileSavepoint('trans'.($this->transactions + 1))
);
}
public function compileSavepoint($name)
{
return 'SAVEPOINT '.$name;
}

如果创建事务失败,那么就会调用 handleBeginTransactionException:

1
2
3
4
5
6
7
8
9
10
protected function handleBeginTransactionException($e)
{
if ($this->causedByLostConnection($e)) {
$this->reconnect();
$this->pdo->beginTransaction();
} else {
throw $e;
}
}

如果创建事务失败是由于与数据库失去连接的话,那么就会重新连接数据库,否则就要抛出异常。

事务异常

事务的异常处理比较复杂,可以先看一看代码:

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
protected function handleTransactionException($e, $currentAttempt, $maxAttempts)
{
if ($this->causedByDeadlock($e) &&
$this->transactions > 1) {
--$this->transactions;
throw $e;
}
$this->rollBack();
if ($this->causedByDeadlock($e) &&
$currentAttempt < $maxAttempts) {
return;
}
throw $e;
}
protected function causedByDeadlock(Exception $e)
{
$message = $e->getMessage();
return Str::contains($message, [
'Deadlock found when trying to get lock',
'deadlock detected',
'The database file is locked',
'database is locked',
'database table is locked',
'A table in the database is locked',
'has been chosen as the deadlock victim',
'Lock wait timeout exceeded; try restarting transaction',
]);
}

这里可以分为四种情况:

  • 单一事务,非死锁导致的异常

单一事务就是说,此时的事务只有一层,没有嵌套事务的存在。数据库的异常也不是死锁导致的,一般是由于 sql 语句不正确引起的。这个时候,handleTransactionException 会直接回滚事务,并且抛出异常到外层:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
try {
return tap($callback($this), function ($result) {
$this->commit();
});
}
catch (Exception $e) {
$this->handleTransactionException(
$e, $currentAttempt, $attempts
);
} catch (Throwable $e) {
$this->rollBack();
throw $e;
}

接到异常之后,程序会再次回滚,但是由于 $this->transactions 已经为 0,因此回滚直接返回,并未真正执行,之后就会抛出异常。

  • 单一事务,死锁异常

有死锁导致的单一事务异常,一般是由于其他程序同时更改了数据库,这个时候,就要判断当前重复尝试的次数是否大于用户设置的 maxAttempts,如果小于就继续尝试,如果大于,那么就会抛出异常。

  • 嵌套事务,非死锁异常

如果出现嵌套事务,例如:

1
2
3
4
5
6
7
8
\DB::transaction(function(){
...
//directly or indirectly call another transaction:
\DB::transaction(function() {
...
...
}, 2);//attempt twice
}, 2);//attempt twice

如果是非死锁导致的异常,那么就要首先回滚内层的事务,抛出异常到外层事务,再回滚外层事务,抛出异常,让用户来处理。也就是说,对于嵌套事务来说,内部事务异常,一定要回滚整个事务,而不是仅仅回滚内部事务。

  • 嵌套事务,死锁异常

嵌套事务的死锁异常,仍然和嵌套事务非死锁异常一样,内部事务异常,一定要回滚整个事务。

但是,不同的是,mysql 对于嵌套事务的回滚会导致外部事务一并回滚:InnoDB Error Handling,因此这时,我们仅仅将 $this->transactions 减一,并抛出异常,使得外层事务回滚抛出异常即可。

回滚事务

如果事务内的数据库更新操作失败,那么就要进行回滚:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public function rollBack($toLevel = null)
{
$toLevel = is_null($toLevel)
? $this->transactions - 1
: $toLevel;
if ($toLevel < 0 || $toLevel >= $this->transactions) {
return;
}
$this->performRollBack($toLevel);
$this->transactions = $toLevel;
$this->fireConnectionEvent('rollingBack');
}

回滚的第一件事就是要减少 $this->transactions 的值,标志当前事务失败。

回滚的时候仍然要判断当前事务的状态,如果当前处于嵌套事务的话,就要进行回滚到 SAVEPOINT,如果是单一事务的话,才会真正回滚退出事务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
protected function performRollBack($toLevel)
{
if ($toLevel == 0) {
$this->getPdo()->rollBack();
} elseif ($this->queryGrammar->supportsSavepoints()) {
$this->getPdo()->exec(
$this->queryGrammar->compileSavepointRollBack('trans'.($toLevel + 1))
);
}
}
public function compileSavepointRollBack($name)
{
return 'ROLLBACK TO SAVEPOINT '.$name;
}

提交事务

提交事务比较简单,仅仅是调用 pdo 的 commit 即可。需要注意的是对于嵌套事务的事务提交,commit 函数仅仅更新了 $this->transactions,而并没有真正提交事务,原因是内层事务的提交对于 mysql 来说是无效的,只有外部事务的提交才能更新整个事务。

1
2
3
4
5
6
7
8
9
10
public function commit()
{
if ($this->transactions == 1) {
$this->getPdo()->commit();
}
$this->transactions = max(0, $this->transactions - 1);
$this->fireConnectionEvent('committed');
}
LEOYANG'S BLOG

Laravel Database——数据库服务的启动与连接

发表于 2017-09-13 | 分类于 php , database , laravel | | 阅读次数

前言

数据库是 laravel 及其重要的组成部分,大致的讲,laravel 的数据库功能可以分为两部分:数据库 DB、数据库 Eloquent Model。数据库的 Eloquent 是功能十分丰富的 ORM,让我们可以避免写繁杂的 sql 语句。数据库 DB 是比较底层的与 pdo 交互的功能,Eloquent 的底层依赖于 DB。本文将会介绍数据库 DB 中关于数据库服务的启动与连接部分。

在详细讲解数据库各个功能之前,我们先看看支撑着整个 laravel 数据库功能的框架:

Markdown

  • DB 也就是 DatabaseManager,承担着数据库接口的工作,一切数据库相关的操作,例如查询、更新、插入、删除都可以通过 DB 这个接口来完成。但是,具体的调用 pdo API 的工作却不是由该类完成的,它仅仅是一个对外的接口而已。
  • ConnectionFactory 顾名思义专门为 DB 构造初始化 connector、connection 对象,
  • connector 负责数据库的连接功能,为保障程序的高效,laravel 将其包装成为闭包函数,并将闭包函数作为 connection 的一个成员对象,实现懒加载。
  • connection 负责数据库的具体功能,负责底层与 pdo API 的交互。

数据库服务的注册与启动

数据库服务也是一种服务提供者:Illuminate\Database\DatabaseServiceProvider

1
2
3
4
5
6
7
8
9
10
public function register()
{
Model::clearBootedModels();
$this->registerConnectionServices();
$this->registerEloquentFactory();
$this->registerQueueableEntityResolver();
}

我们先来看这个注册函数的第一句: Model::clearBootedModels()。这一句其实是为了 Eloquent 服务的启动做准备。数据库的 Eloquent Model 有一个静态的成员变量数组 $booted,这个静态数组存储了所有已经被初始化的数据库 model ,以便加载数据库模型时更加迅速。因此,在 Eloquent 服务启动之前需要初始化静态成员变量 $booted:

1
2
3
4
5
6
public static function clearBootedModels()
{
static::$booted = [];
static::$globalScopes = [];
}

接下来我们就开始看数据库服务的注册最重要的两部分:ConnectionServices 与 Eloquent.

ConnectionServices 注册

1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected function registerConnectionServices()
{
$this->app->singleton('db.factory', function ($app) {
return new ConnectionFactory($app);
});
$this->app->singleton('db', function ($app) {
return new DatabaseManager($app, $app['db.factory']);
});
$this->app->bind('db.connection', function ($app) {
return $app['db']->connection();
});
}

可以看出,数据库服务向 IOC 容器注册了 db、db.factory 与 db.connection。

  • 最重要的莫过于 db 对象,它有一个 Facade 是 DB, 我们可以利用 DB::connection() 来连接任意数据库,可以利用 DB::select() 来进行数据库的查询,可以说 DB 就是我们操作数据库的接口。
  • db.factory 负责为 DB 创建 connector 提供数据库的底层连接服务,负责为 DB 创建 connection 对象来进行数据库的查询等操作。
  • db.cønnection 是 laravel 用于与数据库 pdo 接口进行交互的底层类,可用于数据库的查询、更新、创建等操作。

Eloquent 注册

1
2
3
4
5
6
7
8
9
10
11
12
protected function registerEloquentFactory()
{
$this->app->singleton(FakerGenerator::class, function () {
return FakerFactory::create();
});
$this->app->singleton(EloquentFactory::class, function ($app) {
return EloquentFactory::construct(
$app->make(FakerGenerator::class), database_path('factories')
);
});
}

EloquentFactory 用于创建 Eloquent Model,用于全局函数 factory() 来创建数据库模型。

数据库服务的启动

1
2
3
4
5
6
public function boot()
{
Model::setConnectionResolver($this->app['db']);
Model::setEventDispatcher($this->app['events']);
}

数据库服务的启动主要设置 Eloquent Model 的 connection resolver,用于数据库模型 model 利用 db 来来连接数据库。
还有设置数据库事件的分发器 dispatcher,用于监听数据库的事件。

DatabaseManager——数据库的接口

如果我们想要使用任何数据库服务,首先要做的事情当然是利用用户名与密码来连接数据库。在 laravel 中,数据库的用户名与密码一般放在 .env 文件中或者放入 nginx 配置中,并且利用数据库的接口 DB 来与 pdo 进行交互,利用 pdo 来连接数据库。

DB 即是类 Illuminate\Database\DatabaseManager,首先我们来看看其构造函数:

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 __construct($app, ConnectionFactory $factory)
{
$this->app = $app;
$this->factory = $factory;
}
```
我们称 `DB` 为一个接口,或者是一个门面模式,是因为数据库操作,例如数据库的连接或者查询、更新等操作均不是 `DB` 的功能,数据库的连接使用类 `Illuminate\Database\Connectors\Connector` 完成,数据库的查询等操作由类 `Illuminate\Database\Connection` 完成,
因此,我们不必直接操作 `connector` 或者 `connection`,仅仅会操作 `DB` 即可。
那么 `DB` 是如何实现 `connector` 或者 `connection` 的功能的呢?关键还是这个 `ConnectionFactory` 类,这个工厂类专门为 `DB` 来生成 `connection` 对象,并将其放入 `DB` 的成员变量数组 `$connections` 中去。`connection` 中会包含 `connector` 对象来实现数据库的连接工作。
```php
class DatabaseManager implements ConnectionResolverInterface
{
protected $app;
protected $factory;
protected $connections = [];
public function __call($method, $parameters)
{
return $this->connection()->$method(...$parameters);
}
}

魔术函数实现了 DB 与 connection 的无缝连接,任何对数据库的操作,例如 DB::select()、DB::table('user')->save(),都会被转移至 connection 中去。

connection 函数——获取数据库连接对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public function connection($name = null)
{
list($database, $type) = $this->parseConnectionName($name);
$name = $name ?: $database;
if (! isset($this->connections[$name])) {
$this->connections[$name] = $this->configure(
$connection = $this->makeConnection($database), $type
);
}
return $this->connections[$name];
}

具体流程如下:

Markdown

DB 的 connection 函数可以传入数据库的名字,也可以不传任何参数,此时会连接默认数据库,默认数据库的设置在 config/database 文件中。

connection 函数流程:

  • 解析数据库名称与数据库类型,例如只读、写
  • 若没有创建过与该数据库的连接,则开始创建数据库连接
  • 返回数据库连接对象 connection
1
2
3
4
5
6
7
8
9
10
11
12
protected function parseConnectionName($name)
{
$name = $name ?: $this->getDefaultConnection();
return Str::endsWith($name, ['::read', '::write'])
? explode('::', $name, 2) : [$name, null];
}
public function getDefaultConnection()
{
return $this->app['config']['database.default'];
}

可以看出,若没有特别指定连接的数据库名称,那么就会利用文件 config/database 文件中设置的 default 数据库名称作为默认连接数据库名称。若数据库支持读写分离,那么还可以指定数据库的读写属性,例如 mysql::read。

makeConnection 函数——创建新的数据库连接对象

当框架从未连接过当前数据库的时候,就要对数据库进行连接操作,首先程序会调用 makeConnection 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected function makeConnection($name)
{
$config = $this->configuration($name);
if (isset($this->extensions[$name])) {
return call_user_func($this->extensions[$name], $config, $name);
}
if (isset($this->extensions[$driver = $config['driver']])) {
return call_user_func($this->extensions[$driver], $config, $name);
}
return $this->factory->make($config, $name);
}

可以看出,连接数据库仅仅需要两个步骤:获取数据库配置、利用 connection factory 获取 connection 对象。

获取数据库配置:

1
2
3
4
5
6
7
8
9
10
11
12
protected function configuration($name)
{
$name = $name ?: $this->getDefaultConnection();
$connections = $this->app['config']['database.connections'];
if (is_null($config = Arr::get($connections, $name))) {
throw new InvalidArgumentException("Database [$name] not configured.");
}
return $config;
}

也是非常简单,直接从配置文件中获取当前数据库的配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
'connections' => [
'mysql' => [
'driver' => 'mysql',
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'forge'),
'username' => env('DB_USERNAME', 'forge'),
'password' => env('DB_PASSWORD', ''),
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
'strict' => true,
'engine' => null,
'read' => [
'database' => env('DB_DATABASE', 'forge'),
],
'write' => [
'database' => env('DB_DATABASE', 'forge'),
],
],
],

$this->factory->make($config, $name) 函数向我们提供了数据库连接对象。

configure——连接对象读写配置

当我们从 connection factory 中获取到连接对象 connection 之后,我们就要根据传入的参数进行读写配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected function configure(Connection $connection, $type)
{
$connection = $this->setPdoForType($connection, $type);
if ($this->app->bound('events')) {
$connection->setEventDispatcher($this->app['events']);
}
$connection->setReconnector(function ($connection) {
$this->reconnect($connection->getName());
});
return $connection;
}

setPdoForType 函数就是根据 type 来设置读写:

当我们需要 read 数据库连接时,我们将 read-pdo 设置为主 pdo。当我们需要 write 数据库连接时,我们将读写 pdo 都设置为 write-pdo:

1
2
3
4
5
6
7
8
9
10
protected function setPdoForType(Connection $connection, $type = null)
{
if ($type == 'read') {
$connection->setPdo($connection->getReadPdo());
} elseif ($type == 'write') {
$connection->setReadPdo($connection->getPdo());
}
return $connection;
}

ConnectionFactory——数据库连接对象工厂

Markdown

make 函数——工厂接口

获取到了数据库的配置参数之后,就要利用 ConnectionFactory 来获取 connection 对象了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public function make(array $config, $name = null)
{
$config = $this->parseConfig($config, $name);
if (isset($config['read'])) {
return $this->createReadWriteConnection($config);
}
return $this->createSingleConnection($config);
}
protected function parseConfig(array $config, $name)
{
return Arr::add(Arr::add($config, 'prefix', ''), 'name', $name);
}

在建立连接之前,要先向配置参数中添加默认的 prefix 属性与 name 属性。

接着,就要判断我们在配置文件中是否设置了读写分离。如果设置了读写分离,那么就会调用 createReadWriteConnection 函数,生成具有读、写两个功能的 connection;否则的话,就会调用 createSingleConnection 函数,生成普通的连接对象。

createSingleConnection 函数——制造数据库连接对象

createSingleConnection 函数是类 ConnectionFactory 的核心,用于生成新的数据库连接对象。

1
2
3
4
5
6
7
8
protected function createSingleConnection(array $config)
{
$pdo = $this->createPdoResolver($config);
return $this->createConnection(
$config['driver'], $pdo, $config['database'], $config['prefix'], $config
);
}

ConnectionFactory 也很简单,只做了两件事情:制造 pdo 连接的闭包函数、构造一个新的 connection 对象。

createPdoResolver——数据库连接器闭包函数

根据配置参数中是否含有 host,创建不同的闭包函数:

1
2
3
4
5
6
protected function createPdoResolver(array $config)
{
return array_key_exists('host', $config)
? $this->createPdoResolverWithHosts($config)
: $this->createPdoResolverWithoutHosts($config);
}

不带有 host 的 pdo 闭包函数:

1
2
3
4
5
6
protected function createPdoResolverWithoutHosts(array $config)
{
return function () use ($config) {
return $this->createConnector($config)->connect($config);
};
}

可以看出,不带有 pdo 的闭包函数非常简单,仅仅创建 connector 对象,利用 connector 对象进行数据库的连接。

带有 host 的 pdo 闭包函数:

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
protected function createPdoResolverWithHosts(array $config)
{
return function () use ($config) {
foreach (Arr::shuffle($hosts = $this->parseHosts($config)) as $key => $host) {
$config['host'] = $host;
try {
return $this->createConnector($config)->connect($config);
} catch (PDOException $e) {
if (count($hosts) - 1 === $key && $this->container->bound(ExceptionHandler::class)) {
$this->container->make(ExceptionHandler::class)->report($e);
}
}
}
throw $e;
};
}
protected function parseHosts(array $config)
{
$hosts = array_wrap($config['host']);
if (empty($hosts)) {
throw new InvalidArgumentException('Database hosts array is empty.');
}
return $hosts;
}

带有 host 的闭包函数相对比较复杂,首先程序会随机选择不同的数据库依次来建立数据库连接,若均失败,就会报告异常。

createConnector——创建连接器

程序会根据配置参数中 driver 的不同来创建不同的连接器,每个连接器都继承自 connector 类,用于连接数据库。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public function createConnector(array $config)
{
if (! isset($config['driver'])) {
throw new InvalidArgumentException('A driver must be specified.');
}
if ($this->container->bound($key = "db.connector.{$config['driver']}")) {
return $this->container->make($key);
}
switch ($config['driver']) {
case 'mysql':
return new MySqlConnector;
case 'pgsql':
return new PostgresConnector;
case 'sqlite':
return new SQLiteConnector;
case 'sqlsrv':
return new SqlServerConnector;
}
throw new InvalidArgumentException("Unsupported driver [{$config['driver']}]");
}

createConnection——创建连接对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
protected function createConnection($driver, $connection, $database, $prefix = '', array $config = [])
{
if ($resolver = Connection::getResolver($driver)) {
return $resolver($connection, $database, $prefix, $config);
}
switch ($driver) {
case 'mysql':
return new MySqlConnection($connection, $database, $prefix, $config);
case 'pgsql':
return new PostgresConnection($connection, $database, $prefix, $config);
case 'sqlite':
return new SQLiteConnection($connection, $database, $prefix, $config);
case 'sqlsrv':
return new SqlServerConnection($connection, $database, $prefix, $config);
}
throw new InvalidArgumentException("Unsupported driver [$driver]");
}

创建 pdo 闭包函数之后,会将该闭包函数放入 connection 对象当中去。以后我们利用 connection 对象进行查询或者更新数据库时,程序便会运行该闭包函数,与数据库进行连接。

createReadWriteConnection——创建读写连接对象

当配置文件中有 read、write 等配置项时,说明用户希望创建一个可以读写分离的数据库连接,此时:

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
protected function createReadWriteConnection(array $config)
{
$connection = $this->createSingleConnection($this->getWriteConfig($config));
return $connection->setReadPdo($this->createReadPdo($config));
}
protected function getWriteConfig(array $config)
{
return $this->mergeReadWriteConfig(
$config, $this->getReadWriteConfig($config, 'write')
);
}
protected function getReadWriteConfig(array $config, $type)
{
return isset($config[$type][0])
? $config[$type][array_rand($config[$type])]
: $config[$type];
}
protected function mergeReadWriteConfig(array $config, array $merge)
{
return Arr::except(array_merge($config, $merge), ['read', 'write']);
}

可以看出,程序先读出关于 write 数据库的配置,之后将其合并到总配置当中,删除关于 read 数据库的配置,然后进行 createSingleConnection 建立新的连接对象。

建立连接对象之后,再根据 read 数据库的配置,生成 read 数据库的 pdo 闭包函数,并调用 setReadPdo 将其设置为读库 pdo。

1
2
3
4
5
6
7
8
9
10
11
protected function createReadPdo(array $config)
{
return $this->createPdoResolver($this->getReadConfig($config));
}
protected function getReadConfig(array $config)
{
return $this->mergeReadWriteConfig(
$config, $this->getReadWriteConfig($config, 'read')
);
}

connector 连接

我们以 mysql 为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class MySqlConnector extends Connector implements ConnectorInterface
{
public function connect(array $config)
{
$dsn = $this->getDsn($config);
$options = $this->getOptions($config);
$connection = $this->createConnection($dsn, $config, $options);
if (! empty($config['database'])) {
$connection->exec("use `{$config['database']}`;");
}
$this->configureEncoding($connection, $config);
$this->configureTimezone($connection, $config);
$this->setModes($connection, $config);
return $connection;
}
}

getDsn——获取数据库连接DSN参数

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
protected function getDsn(array $config)
{
return $this->hasSocket($config)
? $this->getSocketDsn($config)
: $this->getHostDsn($config);
}
protected function hasSocket(array $config)
{
return isset($config['unix_socket']) && ! empty($config['unix_socket']);
}
protected function getSocketDsn(array $config)
{
return "mysql:unix_socket={$config['unix_socket']};dbname={$config['database']}";
}
protected function getHostDsn(array $config)
{
extract($config, EXTR_SKIP);
return isset($port)
? "mysql:host={$host};port={$port};dbname={$database}"
: "mysql:host={$host};dbname={$database}";
}

mysql 数据库的连接有两种:tcp连接与socket连接。

socket 连接更快,但是它要求应用程序与数据库在同一台机器,更普通的是使用 tcp 的方式连接数据库。框架根据配置参数来选择是采用 socket 还是 tcp 的方式连接数据库。

getOptions——pdo 属性设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected $options = [
PDO::ATTR_CASE => PDO::CASE_NATURAL,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL,
PDO::ATTR_STRINGIFY_FETCHES => false,
PDO::ATTR_EMULATE_PREPARES => false,
];
public function getOptions(array $config)
{
$options = Arr::get($config, 'options', []);
return array_diff_key($this->options, $options) + $options;
}

pdo 的属性主要有以下几种:

  • PDO::ATTR_CASE 强制列名为指定的大小写。他的$value可为:
    • PDO::CASE_LOWER:强制列名小写。
    • PDO::CASE_NATURAL:保留数据库驱动返回的列名。
    • PDO::CASE_UPPER:强制列名大写。
  • PDO::ATTR_ERRMODE:错误报告。他的$value可为:
    • PDO::ERRMODE_SILENT: 仅设置错误代码。
    • PDO::ERRMODE_WARNING: 引发 E_WARNING 错误.
    • PDO::ERRMODE_EXCEPTION: 抛出 exceptions 异常。
  • PDO::ATTR_ORACLE_NULLS (在所有驱动中都可用,不仅限于Oracle): 转换 NULL 和空字符串。他的$value可为:
    • PDO::NULL_NATURAL: 不转换。
    • PDO::NULL_EMPTY_STRING: 将空字符串转换成 NULL 。
    • PDO::NULL_TO_STRING: 将 NULL 转换成空字符串。
  • PDO::ATTR_STRINGIFY_FETCHES: 提取的时候将数值转换为字符串。
  • PDO::ATTR_EMULATE_PREPARES 启用或禁用预处理语句的模拟。 有些驱动不支持或有限度地支持本地预处理。使用此设置强制PDO总是模拟预处理语句(如果为 TRUE ),或试着使用本地预处理语句(如果为 FALSE )。如果驱动不能成功预处理当前查询,它将总是回到模拟预处理语句上。 需要 bool 类型。
  • PDO::ATTR_AUTOCOMMIT:设置当前连接 Mysql 服务器的客户端的SQL语句是否自动执行,默认是自动提交.
  • PDO::ATTR_PERSISTENT:当前对Mysql服务器的连接是否是长连接.

createConnection——创建数据库连接

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
public function createConnection($dsn, array $config, array $options)
{
list($username, $password) = [
Arr::get($config, 'username'), Arr::get($config, 'password'),
];
try {
return $this->createPdoConnection(
$dsn, $username, $password, $options
);
} catch (Exception $e) {
return $this->tryAgainIfCausedByLostConnection(
$e, $dsn, $username, $password, $options
);
}
}
protected function createPdoConnection($dsn, $username, $password, $options)
{
if (class_exists(PDOConnection::class) && ! $this->isPersistentConnection($options)) {
return new PDOConnection($dsn, $username, $password, $options);
}
return new PDO($dsn, $username, $password, $options);
}

当 pdo 对象成功的建立起来后,说明我们已经与数据库成功地建立起来了一个连接,接下来我们就可以利用这个 pdo 对象进行查询或者更新等操作。

当创建 pdo 的时候抛出异常时:

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
protected function tryAgainIfCausedByLostConnection(Exception $e, $dsn, $username, $password, $options)
{
if ($this->causedByLostConnection($e)) {
return $this->createPdoConnection($dsn, $username, $password, $options);
}
throw $e;
}
protected function causedByLostConnection(Exception $e)
{
$message = $e->getMessage();
return Str::contains($message, [
'server has gone away',
'no connection to the server',
'Lost connection',
'is dead or not enabled',
'Error while sending',
'decryption failed or bad record mac',
'server closed the connection unexpectedly',
'SSL connection has been closed unexpectedly',
'Error writing data to the connection',
'Resource deadlock avoided',
]);
}

当判断出的异常是上面几种情况时,框架会再次尝试连接数据库。

configureEncoding——设置字符集与校对集

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
protected function configureEncoding($connection, array $config)
{
if (! isset($config['charset'])) {
return $connection;
}
$connection->prepare(
"set names '{$config['charset']}'".$this->getCollation($config)
)->execute();
}
protected function getCollation(array $config)
{
return ! is_null($config['collation']) ? " collate '{$config['collation']}'" : '';
}

如果配置参数中设置了字符集与校对集,程序会利用配置的参数对数据库进行相关设置。

所谓的字符集与校对集设置,可以参考mysql 中 character set 与 collation 的点滴理解

configureTimezone——设置时间区

1
2
3
4
5
6
protected function configureTimezone($connection, array $config)~
{
if (isset($config['timezone'])) {
$connection->prepare('set time_zone="'.$config['timezone'].'"')->execute();
}
}

setModes——设置 SQL_MODE 模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
protected function setModes(PDO $connection, array $config)
{
if (isset($config['modes'])) {
$this->setCustomModes($connection, $config);
} elseif (isset($config['strict'])) {
if ($config['strict']) {
$connection->prepare($this->strictMode())->execute();
} else {
$connection->prepare("set session sql_mode='NO_ENGINE_SUBSTITUTION'")->execute();
}
}
}
protected function setCustomModes(PDO $connection, array $config)
{
$modes = implode(',', $config['modes']);
$connection->prepare("set session sql_mode='{$modes}'")->execute();
}
protected function strictMode()
{
return "set session sql_mode='ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION'";
}

以下内容参考:mysql的sql_mode设置简介:

SQL_MODE 直接理解就是:sql的运作模式。官方的说法是:sql_mode可以影响sql支持的语法以及数据的校验执行,这使得MySQL可以运行在不同的环境中以及和其他数据库一起运作。

想设置sql_mode有三种方式:

  • 在命令行启动MySQL时添加参数 –sql-mode=”modes”
  • 在MySQL的配置文件(my.cnf或者my.ini)中添加一个配置sql-mode=”modes”
  • 运行时修改SQL mode可以通过以下命令之一:
1
2
SET GLOBAL sql_mode = 'modes';
SET SESSION sql_mode = 'modes';

几种常见的mode介绍

  • ONLY_FULL_GROUP_BY: 出现在 select 语句、HAVING 条件和 ORDER BY 语句中的列,必须是 GROUP BY 的列或者依赖于 GROUP BY 列的函数列。

  • NO_AUTO_VALUE_ON_ZERO: 该值影响自增长列的插入。默认设置下,插入0或 NULL 代表生成下一个自增长值。如果用户 希望插入的值为0,而该列又是自增长的,那么这个选项就有用了。

  • STRICT_TRANS_TABLES: 在该模式下,如果一个值不能插入到一个事务表中,则中断当前的操作,对非事务表不做限制

  • NO_ZERO_IN_DATE: 这个模式影响了是否允许日期中的月份和日包含0。如果开启此模式,2016-01-00是不允许的,但是0000-02-01是允许的。它实际的行为受到 strict mode 是否开启的影响1。

  • NO_ZERO_DATE: 设置该值,mysql 数据库不允许插入零日期。它实际的行为受到 strict mode 是否开启的影响2。

  • ERROR_FOR_DIVISION_BY_ZERO: 在 INSERT 或 UPDATE 过程中,如果数据被零除,则产生错误而非警告。如 果未给出该模式,那么数据被零除时 MySQL 返回 NULL

  • NO_AUTO_CREATE_USER: 禁止 GRANT 创建密码为空的用户

  • NO_ENGINE_SUBSTITUTION: 如果需要的存储引擎被禁用或未编译,那么抛出错误。不设置此值时,用默认的存储引擎替代,并抛出一个异常

  • PIPES_AS_CONCAT: 将”||”视为字符串的连接操作符而非或运算符,这和Oracle数据库是一样的,也和字符串的拼接函数Concat相类似

  • ANSI_QUOTES: 启用 ANSI_QUOTES 后,不能用双引号来引用字符串,因为它被解释为识别符

LEOYANG'S BLOG

Laravel Providers——服务提供者的注册与启动源码解析

发表于 2017-08-14 | | 阅读次数

前言

服务提供者是 laravel 框架的重要组成部分,承载着各种服务,自定义的应用以及所有 Laravel 的核心服务都是通过服务提供者启动。本文将会介绍服务提供者的源码分析,关于服务提供者的使用,请参考官方文档 :服务提供者。

服务提供者的注册

服务提供者的启动由类 \Illuminate\Foundation\Bootstrap\RegisterProviders::class 负责,该类用于加载所有服务提供者的 register 函数,并保存延迟加载的服务的信息,以便实现延迟加载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class RegisterProviders
{
public function bootstrap(Application $app)
{
$app->registerConfiguredProviders();
}
}
class Application extends Container implements ApplicationContract, HttpKernelInterface
{
public function registerConfiguredProviders()
{
(new ProviderRepository($this, new Filesystem, $this->getCachedServicesPath()))
->load($this->config['app.providers']);
}
public function getCachedServicesPath()
{
return $this->bootstrapPath().'/cache/services.php';
}
}

以上可以看出,所有服务提供者都在配置文件 app.php 文件的 providers 数组中。类 ProviderRepository 负责所有的服务加载功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class ProviderRepository
{
public function load(array $providers)
{
$manifest = $this->loadManifest();
if ($this->shouldRecompile($manifest, $providers)) {
$manifest = $this->compileManifest($providers);
}
foreach ($manifest['when'] as $provider => $events) {
$this->registerLoadEvents($provider, $events);
}
foreach ($manifest['eager'] as $provider) {
$this->app->register($provider);
}
$this->app->addDeferredServices($manifest['deferred']);
}
}

加载服务缓存文件

laravel 会把所有的服务整理起来,作为缓存写在缓存文件中:

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
return array (
'providers' =>
array (
0 => 'Illuminate\\Auth\\AuthServiceProvider',
1 => 'Illuminate\\Broadcasting\\BroadcastServiceProvider',
...
),
'eager' =>
array (
0 => 'Illuminate\\Auth\\AuthServiceProvider',
1 => 'Illuminate\\Cookie\\CookieServiceProvider',
...
),
'deferred' =>
array (
'Illuminate\\Broadcasting\\BroadcastManager' => 'Illuminate\\Broadcasting\\BroadcastServiceProvider',
'Illuminate\\Contracts\\Broadcasting\\Factory' => 'Illuminate\\Broadcasting\\BroadcastServiceProvider',
...
),
'when' =>
array (
'Illuminate\\Broadcasting\\BroadcastServiceProvider' =>
array (
),
...
),
  • 缓存文件中 providers 放入了所有自定义和框架核心的服务。
  • eager 数组中放入了所有需要立即启动的服务提供者。
  • deferred 数组中放入了所有需要延迟加载的服务提供者。
  • when 放入了延迟加载需要激活的事件。

加载服务提供者缓存文件:

1
2
3
4
5
6
7
8
9
10
public function loadManifest()
{
if ($this->files->exists($this->manifestPath)) {
$manifest = $this->files->getRequire($this->manifestPath);
if ($manifest) {
return array_merge(['when' => []], $manifest);
}
}
}

编译服务提供者

若 laravel 中的服务提供者没有缓存文件或者有变动,那么就会重新生成缓存文件:

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
public function shouldRecompile($manifest, $providers)
{
return is_null($manifest) || $manifest['providers'] != $providers;
}
protected function compileManifest($providers)
{
$manifest = $this->freshManifest($providers);
foreach ($providers as $provider) {
$instance = $this->createProvider($provider);
if ($instance->isDeferred()) {
foreach ($instance->provides() as $service) {
$manifest['deferred'][$service] = $provider;
}
$manifest['when'][$provider] = $instance->when();
}
else {
$manifest['eager'][] = $provider;
}
}
return $this->writeManifest($manifest);
}
protected function freshManifest(array $providers)
{
return ['providers' => $providers, 'eager' => [], 'deferred' => []];
}
  • 如果服务提供者是需要立即注册的,那么将会放入缓存文件中 eager 数组中。
  • 如果服务提供者是延迟加载的,那么其函数 provides() 通常会提供服务别名,这个服务别名通常是向服务容器中注册的别名,别名将会放入缓存文件的 deferred 数组中。
  • 延迟加载若有 event 事件激活,那么可以在 when 函数中写入事件类,并写入缓存文件的 when 数组中。

延迟服务提供者事件注册

延迟服务提供者除了利用 IOC 容器解析服务方式激活,还可以利用 Event 事件来激活:

1
2
3
4
5
6
7
8
9
10
protected function registerLoadEvents($provider, array $events)
{
if (count($events) < 1) {
return;
}
$this->app->make('events')->listen($events, function () use ($provider) {
$this->app->register($provider);
});
}

注册即时启动的服务提供者

服务提供者的注册函数 register() 由类 Application 来调用:

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
47
48
49
50
51
52
53
class Application extends Container implements ApplicationContract, HttpKernelInterface
{
public function register($provider, $options = [], $force = false)
{
if (($registered = $this->getProvider($provider)) && ! $force) {
return $registered;
}
if (is_string($provider)) {
$provider = $this->resolveProvider($provider);
}
if (method_exists($provider, 'register')) {
$provider->register();
}
$this->markAsRegistered($provider);
if ($this->booted) {
$this->bootProvider($provider);
}
return $provider;
}
public function getProvider($provider)
{
$name = is_string($provider) ? $provider : get_class($provider);
return Arr::first($this->serviceProviders, function ($value) use ($name) {
return $value instanceof $name;
});
}
public function resolveProvider($provider)
{
return new $provider($this);
}
protected function markAsRegistered($provider)
{
$this->serviceProviders[] = $provider;
$this->loadedProviders[get_class($provider)] = true;
}
protected function bootProvider(ServiceProvider $provider)
{
if (method_exists($provider, 'boot')) {
return $this->call([$provider, 'boot']);
}
}
}

可以看出,服务提供者的注册过程:

  • 判断当前服务提供者是否被注册过,如注册过直接返回对象
  • 解析服务提供者
  • 调用服务提供者的 register 函数
  • 标记当前服务提供者已经注册完毕
  • 若框架已经加载注册完毕所有的服务容器,那么就启动服务提供者的 boot 函数,该函数由于是 call 调用,所以支持依赖注入。

延迟服务提供者激活与注册

延迟服务提供者首先需要添加到 Application 中:

1
2
3
4
public function addDeferredServices(array $services)
{
$this->deferredServices = array_merge($this->deferredServices, $services);
}

我们之前说过,延迟服务提供者的激活注册有两种方法:事件与服务解析。

当特定的事件被激发后,就会调用 Application 的 register 函数,进而调用服务提供者的 register 函数,实现服务的注册。

当利用 Ioc 容器解析服务名时,例如解析服务名 BroadcastingFactory:

1
2
3
4
5
6
7
8
9
10
11
12
13
class BroadcastServiceProvider extends ServiceProvider
{
protected $defer = true;
public function provides()
{
return [
BroadcastManager::class,
BroadcastingFactory::class,
BroadcasterContract::class,
];
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public function make($abstract)
{
$abstract = $this->getAlias($abstract);
if (isset($this->deferredServices[$abstract])) {
$this->loadDeferredProvider($abstract);
}
return parent::make($abstract);
}
public function loadDeferredProvider($service)
{
if (! isset($this->deferredServices[$service])) {
return;
}
$provider = $this->deferredServices[$service];
if (! isset($this->loadedProviders[$provider])) {
$this->registerDeferredProvider($provider, $service);
}
}

由 deferredServices 数组可以得知,BroadcastingFactory 为延迟服务,接着程序会利用函数 loadDeferredProvider 来加载延迟服务提供者,调用服务提供者的 register 函数,若当前的框架还未注册完全部服务。那么将会放入服务启动的回调函数中,以待服务启动时调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public function registerDeferredProvider($provider, $service = null)
{
if ($service) {
unset($this->deferredServices[$service]);
}
$this->register($instance = new $provider($this));
if (! $this->booted) {
$this->booting(function () use ($instance) {
$this->bootProvider($instance);
});
}
}

关于服务提供者的注册函数:

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
class BroadcastServiceProvider extends ServiceProvider
{
protected $defer = true;
public function register()
{
$this->app->singleton(BroadcastManager::class, function ($app) {
return new BroadcastManager($app);
});
$this->app->singleton(BroadcasterContract::class, function ($app) {
return $app->make(BroadcastManager::class)->connection();
});
$this->app->alias(
BroadcastManager::class, BroadcastingFactory::class
);
}
public function provides()
{
return [
BroadcastManager::class,
BroadcastingFactory::class,
BroadcasterContract::class,
];
}
}

函数 register 为类 BroadcastingFactory 向 Ioc 容器绑定了特定的实现类 BroadcastManager,这样 Ioc 容器中的 make 函数:

1
2
3
4
5
6
7
8
9
10
public function make($abstract)
{
$abstract = $this->getAlias($abstract);
if (isset($this->deferredServices[$abstract])) {
$this->loadDeferredProvider($abstract);
}
return parent::make($abstract);
}

parent::make($abstract) 就会正确的解析服务 BroadcastingFactory。

因此函数 provides() 返回的元素一定都是 register() 向 IOC 容器中绑定的类名或者别名。这样当我们利用服务容器来利用 App::make() 解析这些类名的时候,服务容器才会根据服务提供者的 register() 函数中绑定的实现类,从而正确解析服务功能。

服务容器的启动

服务容器的启动由类 \Illuminate\Foundation\Bootstrap\BootProviders::class 负责:

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
class BootProviders
{
public function bootstrap(Application $app)
{
$app->boot();
}
}
class Application extends Container implements ApplicationContract, HttpKernelInterface
{
public function boot()
{
if ($this->booted) {
return;
}
$this->fireAppCallbacks($this->bootingCallbacks);
array_walk($this->serviceProviders, function ($p) {
$this->bootProvider($p);
});
$this->booted = true;
$this->fireAppCallbacks($this->bootedCallbacks);
}
protected function bootProvider(ServiceProvider $provider)
{
if (method_exists($provider, 'boot')) {
return $this->call([$provider, 'boot']);
}
}
}
LEOYANG'S BLOG

Laravel Exceptions——异常与错误处理

发表于 2017-08-13 | | 阅读次数

前言

对于一个优秀的框架来说,正确的异常处理可以防止暴露自身接口给用户,可以提供快速追溯问题的提示给开发人员。本文会详细的介绍 laravel 异常处理的源码。

PHP 异常处理

本章节参考 PHP错误异常处理详解。

异常处理(又称为错误处理)功能提供了处理程序运行时出现的错误或异常情况的方法。
  
异常处理通常是防止未知错误产生所采取的处理措施。异常处理的好处是你不用再绞尽脑汁去考虑各种错误,这为处理某一类错误提供了一个很有效的方法,使编程效率大大提高。当异常被触发时,通常会发生:

  • 当前代码状态被保存
  • 代码执行被切换到预定义的异常处理器函数
  • 根据情况,处理器也许会从保存的代码状态重新开始执行代码,终止脚本执行,或从代码中另外的位置继续执行脚本

PHP 5 提供了一种新的面向对象的错误处理方法。可以使用检测(try)、抛出(throw)和捕获(catch)异常。即使用try检测有没有抛出(throw)异常,若有异常抛出(throw),使用catch捕获异常。

一个 try 至少要有一个与之对应的 catch。定义多个 catch 可以捕获不同的对象。php 会按这些 catch 被定义的顺序执行,直到完成最后一个为止。而在这些 catch 内,又可以抛出新的异常。

异常的抛出

当一个异常被抛出时,其后的代码将不会继续执行,PHP 会尝试查找匹配的 catch 代码块。如果一个异常没有被捕获,而且又没用使用set_exception_handler() 作相应的处理的话,那么 PHP 将会产生一个严重的错误,并且输出未能捕获异常 (Uncaught Exception ... ) 的提示信息。

抛出异常,但不去捕获它:

1
2
3
4
5
6
ini_set('display_errors', 'On');
error_reporting(E_ALL & ~ E_WARNING);
$error = 'Always throw this error';
throw new Exception($error);
// 继续执行
echo 'Hello World';

上面的代码会获得类似这样的一个致命错误:

1
2
3
4
Fatal error: Uncaught exception 'Exception' with message 'Always throw this error' in E:\sngrep\index.php on line 5
Exception: Always throw this error in E:\sngrep\index.php on line 5
Call Stack:
0.0005 330680 1. {main}() E:\sngrep\index.php:0

Try, throw 和 catch

要避免上面这个致命错误,可以使用try catch捕获掉。

处理处理程序应当包括:

  • Try - 使用异常的函数应该位于 “try” 代码块内。如果没有触发异常,则代码将照常继续执行。但是如果异常被触发,会抛出一个异常。
  • Throw - 这里规定如何触发异常。每一个 “throw” 必须对应至少一个 “catch”
  • Catch - “catch” 代码块会捕获异常,并创建一个包含异常信息的对象

抛出异常并捕获掉,可以继续执行后面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
try {
$error = 'Always throw this error';
throw new Exception($error);
// 从这里开始,tra 代码块内的代码将不会被执行
echo 'Never executed';
} catch (Exception $e) {
echo 'Caught exception: ', $e->getMessage(),'<br>';
}
// 继续执行
echo 'Hello World';

顶层异常处理器 set_exception_handler

在我们实际开发中,异常捕捉仅仅靠 try {} catch () 是远远不够的。set_exception_handler() 函数可设置处理所有未捕获异常的用户定义函数。

1
2
3
4
5
6
7
function myException($exception)
{
echo "<b>Exception:</b> " , $exception->getMessage();
}
set_exception_handler('myException');
throw new Exception('Uncaught Exception occurred');

扩展 PHP 内置的异常处理类

用户可以用自定义的异常处理类来扩展 PHP 内置的异常处理类。以下的代码说明了在内置的异常处理类中,哪些属性和方法在子类中是可访问和可继承的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Exception
{
protected $message = 'Unknown exception'; // 异常信息
protected $code = 0; // 用户自定义异常代码
protected $file; // 发生异常的文件名
protected $line; // 发生异常的代码行号
function __construct($message = null, $code = 0);
final function getMessage(); // 返回异常信息
final function getCode(); // 返回异常代码
final function getFile(); // 返回发生异常的文件名
final function getLine(); // 返回发生异常的代码行号
final function getTrace(); // backtrace() 数组
final function getTraceAsString(); // 已格成化成字符串的 getTrace() 信息
/* 可重载的方法 */
function __toString(); // 可输出的字符串
}

如果使用自定义的类来扩展内置异常处理类,并且要重新定义构造函数的话,建议同时调用 parent::__construct() 来检查所有的变量是否已被赋值。当对象要输出字符串的时候,可以重载 __toString() 并自定义输出的样式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class MyException extends Exception
{
// 重定义构造器使 message 变为必须被指定的属性
public function __construct($message, $code = 0) {
// 自定义的代码
// 确保所有变量都被正确赋值
parent::__construct($message, $code);
}
// 自定义字符串输出的样式 */
public function __toString() {
return __CLASS__ . ": [{$this->code}]: {$this->message}\n";
}
public function customFunction() {
echo "A Custom function for this type of exception\n";
}
}

MyException 类是作为旧的 exception 类的一个扩展来创建的。这样它就继承了旧类的所有属性和方法,我们可以使用 exception 类的方法,比如 getLine() 、 getFile() 以及 getMessage()。

PHP 错误处理

PHP 的错误级别

值 常量 说明
1 E_ERROR 致命的运行时错误。这类错误一般是不可恢复的情况,例如内存分配导致的问题。后果是导致脚本终止不再继续运行。
2 E_WARNING 运行时警告 (非致命错误)。仅给出提示信息,但是脚本不会终止运行。
4 E_PARSE 编译时语法解析错误。解析错误仅仅由分析器产生。
8 E_NOTICE 运行时通知。表示脚本遇到可能会表现为错误的情况,但是在可以正常运行的脚本里面也可能会有类似的通知。
16 E_CORE_ERROR 在PHP初始化启动过程中发生的致命错误。该错误类似 E_ERROR,但是是由PHP引擎核心产生的。
32 E_CORE_WARNING PHP初始化启动过程中发生的警告 (非致命错误) 。类似 E_WARNING,但是是由PHP引擎核心产生的。
64 E_COMPILE_ERROR 致命编译时错误。类似E_ERROR, 但是是由Zend脚本引擎产生的。
128 E_COMPILE_WARNING 编译时警告 (非致命错误)。类似 E_WARNING,但是是由Zend脚本引擎产生的。
256 E_USER_ERROR 用户产生的错误信息。类似 E_ERROR, 但是是由用户自己在代码中使用PHP函数 trigger_error()来产生的。
512 E_USER_WARNING 用户产生的警告信息。类似 E_WARNING, 但是是由用户自己在代码中使用PHP函数 trigger_error()来产生的。
1024 E_USER_NOTICE 用户产生的通知信息。类似 E_NOTICE, 但是是由用户自己在代码中使用PHP函数 trigger_error()来产生的。
2048 E_STRICT 启用 PHP 对代码的修改建议,以确保代码具有最佳的互操作性和向前兼容性。
4096 E_RECOVERABLE_ERROR 可被捕捉的致命错误。 它表示发生了一个可能非常危险的错误,但是还没有导致PHP引擎处于不稳定的状态。 如果该错误没有被用户自定义句柄捕获 (参见 set_error_handler()),将成为一个 E_ERROR 从而脚本会终止运行。
8192 E_DEPRECATED 运行时通知。启用后将会对在未来版本中可能无法正常工作的代码给出警告。
16384 E_USER_DEPRECATED 用户产少的警告信息。 类似 E_DEPRECATED, 但是是由用户自己在代码中使用PHP函数 trigger_error()来产生的。
30719 E_ALL 用户产少的警告信息。 类似 E_DEPRECATED, 但是是由用户自己在代码中使用PHP函数 trigger_error()来产生的。

错误的抛出

除了系统在运行 php 代码抛出的意外错误。我们还可以利用 rigger_error 产生一个自定义的用户级别的 error/warning/notice 错误信息:

1
2
3
if ($divisor == 0) {
trigger_error("Cannot divide by zero", E_USER_ERROR);
}

顶级错误处理器

顶级错误处理器 set_error_handler 一般用于捕捉 E_NOTICE 、E_USER_ERROR、E_USER_WARNING、E_USER_NOTICE 级别的错误,不能捕捉 E_ERROR, E_PARSE, E_CORE_ERROR, E_CORE_WARNING, E_COMPILE_ERROR 和E_COMPILE_WARNING。

register_shutdown_function

register_shutdown_function() 函数可实现当程序执行完成后执行的函数,其功能为可实现程序执行完成的后续操作。程序在运行的时候可能存在执行超时,或强制关闭等情况,但这种情况下默认的提示是非常不友好的,如果使用 register_shutdown_function() 函数捕获异常,就能提供更加友好的错误展示方式,同时可以实现一些功能的后续操作,如执行完成后的临时数据清理,包括临时文件等。

可以这样理解调用条件:

  • 当页面被用户强制停止时
  • 当程序代码运行超时时
  • 当PHP代码执行完成时,代码执行存在异常和错误、警告

我们前面说过,set_error_handler 能够捕捉的错误类型有限,很多致命错误例如解析错误等都无法捕捉,但是这类致命错误发生时,PHP 会调用 register_shutdown_function 所注册的函数,如果结合函数 error_get_last,就会获取错误发生的信息。

Laravel 异常处理

laravel 的异常处理由类 \Illuminate\Foundation\Bootstrap\HandleExceptions::class 完成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class HandleExceptions
{
public function bootstrap(Application $app)
{
$this->app = $app;
error_reporting(-1);
set_error_handler([$this, 'handleError']);
set_exception_handler([$this, 'handleException']);
register_shutdown_function([$this, 'handleShutdown']);
if (! $app->environment('testing')) {
ini_set('display_errors', 'Off');
}
}
}

异常转化

laravel 的异常处理均由函数 handleException 负责。

PHP7 实现了一个全局的 throwable 接口,原来的 Exception 和部分 Error 都实现了这个接口, 以接口的方式定义了异常的继承结构。于是,PHP7 中更多的 Error 变为可捕获的 Exception 返回给开发者,如果不进行捕获则为 Error ,如果捕获就变为一个可在程序内处理的 Exception。这些可被捕获的 Error 通常都是不会对程序造成致命伤害的 Error,例如函数不存在。

PHP7 中,基于 /Error exception,派生了5个新的engine exception:ArithmeticError / AssertionError / DivisionByZeroError / ParseError / TypeError。在 PHP7 里,无论是老的 /Exception 还是新的 /Error ,它们都实现了一个共同的interface: /Throwable。

因此,遇到非 Exception 类型的异常,首先就要将其转化为 FatalThrowableError 类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public function handleException($e)
{
if (! $e instanceof Exception) {
$e = new FatalThrowableError($e);
}
$this->getExceptionHandler()->report($e);
if ($this->app->runningInConsole()) {
$this->renderForConsole($e);
} else {
$this->renderHttpResponse($e);
}
}

FatalThrowableError 是 Symfony 继承 \ErrorException 的错误异常类:

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
class FatalThrowableError extends FatalErrorException
{
public function __construct(\Throwable $e)
{
if ($e instanceof \ParseError) {
$message = 'Parse error: '.$e->getMessage();
$severity = E_PARSE;
} elseif ($e instanceof \TypeError) {
$message = 'Type error: '.$e->getMessage();
$severity = E_RECOVERABLE_ERROR;
} else {
$message = $e->getMessage();
$severity = E_ERROR;
}
\ErrorException::__construct(
$message,
$e->getCode(),
$severity,
$e->getFile(),
$e->getLine()
);
$this->setTrace($e->getTrace());
}
}

异常 Log

当遇到异常情况的时候,laravel 首要做的事情就是记录 log,这个就是 report 函数的作用。

1
2
3
4
protected function getExceptionHandler()
{
return $this->app->make(ExceptionHandler::class);
}

laravel 在 Ioc 容器中默认的异常处理类是 Illuminate\Foundation\Exceptions\Handler:

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
class Handler implements ExceptionHandlerContract
{
public function report(Exception $e)
{
if ($this->shouldntReport($e)) {
return;
}
try {
$logger = $this->container->make(LoggerInterface::class);
} catch (Exception $ex) {
throw $e; // throw the original exception
}
$logger->error($e);
}
protected function shouldntReport(Exception $e)
{
$dontReport = array_merge($this->dontReport, [HttpResponseException::class]);
return ! is_null(collect($dontReport)->first(function ($type) use ($e) {
return $e instanceof $type;
}));
}
}

异常页面展示

记录 log 后,就要将异常转化为页面向开发者展示异常的信息,以便查看问题的来源:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
protected function renderHttpResponse(Exception $e)
{
$this->getExceptionHandler()->render($this->app['request'], $e)->send();
}
class Handler implements ExceptionHandlerContract
{
public function render($request, Exception $e)
{
$e = $this->prepareException($e);
if ($e instanceof HttpResponseException) {
return $e->getResponse();
} elseif ($e instanceof AuthenticationException) {
return $this->unauthenticated($request, $e);
} elseif ($e instanceof ValidationException) {
return $this->convertValidationExceptionToResponse($e, $request);
}
return $this->prepareResponse($request, $e);
}
}

对于不同的异常,laravel 有不同的处理,大致有 HttpException、HttpResponseException、AuthorizationException、ModelNotFoundException、AuthenticationException、ValidationException。由于特定的不同异常带有自身的不同需求,本文不会特别介绍。本文继续介绍最普通的异常 HttpException 的处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
protected function prepareResponse($request, Exception $e)
{
if ($this->isHttpException($e)) {
return $this->toIlluminateResponse($this->renderHttpException($e), $e);
} else {
return $this->toIlluminateResponse($this->convertExceptionToResponse($e), $e);
}
}
protected function renderHttpException(HttpException $e)
{
$status = $e->getStatusCode();
view()->replaceNamespace('errors', [
resource_path('views/errors'),
__DIR__.'/views',
]);
if (view()->exists("errors::{$status}")) {
return response()->view("errors::{$status}", ['exception' => $e], $status, $e->getHeaders());
} else {
return $this->convertExceptionToResponse($e);
}
}

对于 HttpException 来说,会根据其错误的状态码,选取不同的错误页面模板,若不存在相关的模板,则会通过 SymfonyResponse 来构造异常展示页面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
protected function convertExceptionToResponse(Exception $e)
{
$e = FlattenException::create($e);
$handler = new SymfonyExceptionHandler(config('app.debug'));
return SymfonyResponse::create($handler->getHtml($e), $e->getStatusCode(), $e->getHeaders());
}
protected function toIlluminateResponse($response, Exception $e)
{
if ($response instanceof SymfonyRedirectResponse) {
$response = new RedirectResponse($response->getTargetUrl(), $response->getStatusCode(), $response->headers->all());
} else {
$response = new Response($response->getContent(), $response->getStatusCode(), $response->headers->all());
}
return $response->withException($e);
}

laravel 错误处理

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
public function handleError($level, $message, $file = '', $line = 0, $context = [])
{
if (error_reporting() & $level) {
throw new ErrorException($message, 0, $level, $file, $line);
}
}
public function handleShutdown()
{
if (! is_null($error = error_get_last()) && $this->isFatal($error['type'])) {
$this->handleException($this->fatalExceptionFromError($error, 0));
}
}
protected function fatalExceptionFromError(array $error, $traceOffset = null)
{
return new FatalErrorException(
$error['message'], $error['type'], 0, $error['file'], $error['line'], $traceOffset
);
}
protected function isFatal($type)
{
return in_array($type, [E_COMPILE_ERROR, E_CORE_ERROR, E_ERROR, E_PARSE]);
}

对于不致命的错误,例如 notice级别的错误,handleError 即可截取, laravel 将错误转化为了异常,交给了 handleException 去处理。

对于致命错误,例如 E_PARSE 解析错误,handleShutdown 将会启动,并且判断当前脚本结束是否是由于致命错误,如果是致命错误,将会将其转化为 FatalErrorException, 交给了 handleException 作为异常去处理。

123
Leo Yang

Leo Yang

Life and Learn!

30 日志
23 分类
17 标签
GitHub 知乎
© 2017 Leo Yang
由 Hexo 强力驱动
主题 - NexT.Pisces