用户模型的 notifications 方法 源码阅读笔记
前言
在《Laravel 消息通知源码阅读笔记》,明白了框架时是如何将消息是如何放到数据库的notifications
表的。那么需要用到的时候,比如要做一个消息列表,是如何获取的呢?laravel提供了一个用户模型的notifications方法。本文将阅读一下大致源码。
前提
首先需要理解以下几点:
- eloquent里面的relationships之间的继承关系: MorphMany->MorphOneOrMany->HasOneOrMany->Relation
- QueryBuilder和EloquentBuilder的联系,二者不是继承关系,而是注入的关系。
源码结构分析
$user->notification()就可以取出某个用户的在notifications
表中的通知列表。
public function notifications()
{
return $this->morphMany(DatabaseNotification::class, 'notifiable')->orderBy('created_at', 'desc');
}
上面这句话的意思是:
- 调用
user
实例的morphMany
方法,生成一个MorphMany
实例,然后通过上面列举的那个继承关系,层层设置相关的多态type,外键,最终返回一个有外键关系的MorphMany
实例。 - 调用
MorphMany
实例的orderBy
方法。由于MorphMany
实例及其祖先类都没有orderBy
方法,最终将通过Relation
类的魔术方法__call
去调用MorphMany
实例中的query属性(也就是一个EloquentBuilder)的orderBy
方法。 - 由于EloquentBuilder本身也没有
orderBy
方法,那么就会通过魔术方法再去调用EloquentBuilder
中的query属性(也就是一个QueryBuilder)的orderBy
方法, - 此时返回的是一个EloquentBuilder,由于对象引用的关系,返回的EloquentBuilder和
MorphMany
实例的query属性进行比较,如果全等,就返回此EloquentBuilder。
\Illuminate\Database\Eloquent\Relations\Relation::__call
public function __call($method, $parameters)
{
if (static::hasMacro($method)) {
return $this->macroCall($method, $parameters);
}
$result = $this->forwardCallTo($this->query, $method, $parameters);
if ($result === $this->query) {
return $this;
}
return $result;
}
有了EloquentBuilder,自然就可以取到我们想要的数据了。比如要取分页数据,就可以$data->paginate(20)
源码详细阅读笔记
生成MorphMany
实例
首先,上面调用的是user
实例的morphMany
方法,此方法做了以下事情:
- 创建了一个指定的
\Illuminate\Notifications\DatabaseNotification
对象(通知对象模板)。 - 根据传入的$name设置后面设置多态关系要用到的
notifiable_type
和notifiable_id
. - 获得通知对象关联的数据库表(默认为
notifications
) - 获得用户模型的主键(作为后面一对多关系中的父模型id)
- 对
DatabaseNotification
对象再创建一个EloquentBuilder,并将二者绑定在一起。这里还进行了一些global scope
和eager load
的工作。 - 将上面的变量作为参数传入
user
实例的newMorphMany
方法。public function morphMany($related, $name, $type = null, $id = null, $localKey = null) { $instance = $this->newRelatedInstance($related); [$type, $id] = $this->getMorphs($name, $type, $id); $table = $instance->getTable(); $localKey = $localKey ?: $this->getKeyName(); return $this->newMorphMany($instance->newQuery(), $this, $table.'.'.$type, $table.'.'.$id, $localKey); }
进入
user
实例的newMorphMany
方法,可以看到是把传入的所有参数用来实例化了一个MorphMany
对象。protected function newMorphMany(Builder $query, Model $parent, $type, $id, $localKey) { return new MorphMany($query, $parent, $type, $id, $localKey); }
由于MorphMany
对象所属类本身没有构造方法,就进入了其父类MorphOneOrMany
的构造方法
public function __construct(Builder $query, Model $parent, $type, $id, $localKey)
{
$this->morphType = $type;
$this->morphClass = $parent->getMorphClass();
parent::__construct($query, $parent, $id, $localKey);
}
此方法完成了如下工作:
- 设置了多态一对多关系中的外键类别morphType(也就是notifications.notifiable_type)
- 设置了多态一对多关系中的父类名morphClass(也就是\App\Models\User)
-
进入父类
HasOneOrMany
的构造方法。public function __construct(Builder $query, Model $parent, $foreignKey, $localKey) { $this->localKey = $localKey; $this->foreignKey = $foreignKey; parent::__construct($query, $parent); }
此方法完成了如下工作:
- 设置一对多关系中的父类的localKey,也就是id
- 设置一对多关系中的父类的foreignKey,也就是notifications.notifiable_id
-
进入父类
Relation
的构造方法。public function __construct(Builder $query, Model $parent) { $this->query = $query; $this->parent = $parent; $this->related = $query->getModel(); $this->addConstraints(); }
此方法完成了如下工作:
- 设置关系中的query属性为我们绑定了
DatabaseNotification
对象的EloquentBuilder
。会大量用到此builder。 - 设置关系中的父类实例为
\App\Models\User
实例。 - 设置关系中的相关模型实例为
DatabaseNotification
对象。 - 然后是对关系添加限制条件(即添加where操作)。
由于这里调用的是MorphMany
对象的addConstraints
,但是MorphMany
对象本身没有此方法,因此会到其父类MorphOneOrMany
的addConstraints方法中。
if (static::$constraints) {
parent::addConstraints();
$this->query->where($this->morphType, $this->morphClass);
}
此方法完成了如下工作:
- 调用父类的
addConstraints
方法。 - 将给当前
EloquentBuilder
添加一个where
操作,指定了notifiable_type
应该是App\Models\User
进入父类HasOneOrMany
的addConstraints
方法:
public function addConstraints()
{
if (static::$constraints) {
$this->query->where($this->foreignKey, '=', $this->getParentKey());
$this->query->whereNotNull($this->foreignKey);
}
}
此方法完成了如下工作:
- 给当前
EloquentBuilder
添加一个where
操作,指定了notifications.notifiable_id
为1(当前用户的id) - 给当前
EloquentBuilder
添加一个where
操作,指定了notifications.notifiable_id
不能为null.
这样,一个设置好了多态关系,外键关系的MorphMany
关系对象就创建好了。
order操作产生EloquentBuilder
上面已经分析了,详细代码就不展开了,都很简单。
加一个分页操作
一般都是分页取数据。\Illuminate\Database\Eloquent\Builder::paginate
public function paginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null)
{
$page = $page ?: Paginator::resolveCurrentPage($pageName);
$perPage = $perPage ?: $this->model->getPerPage();
$results = ($total = $this->toBase()->getCountForPagination())
? $this->forPage($page, $perPage)->get($columns)
: $this->model->newCollection();
return $this->paginator($results, $total, $perPage, $page, [
'path' => Paginator::resolveCurrentPath(),
'pageName' => $pageName,
]);
}
此方法完成了如下工作:
- 设置当前页数,每页显示数。
- 获取分页所需的总数,如果有总数,那么会获取到当前页的所有数据。
- 调用
EloquentBuilder
的paginator
方法,来创建一个length-aware
分页器实例。
获取分页所需的总数
获取分页所需的总数调用了QueryBuilder
的get
方法:
public function get($columns = ['*'])
{
return collect($this->onceWithColumns($columns, function () {
return $this->processor->processSelect($this, $this->runSelect());
}));
}
此方法完成了如下工作:
- 执行select查询
进入\Illuminate\Database\Query\Builder::runSelect
protected function runSelect()
{
return $this->connection->select(
$this->toSql(), $this->getBindings(), ! $this->useWritePdo
);
}
此方法完成了如下工作:
- 自动组装sql语句
public function toSql() { return $this->grammar->compileSelect($this); }
-
获取当前的查询绑定值
public function getBindings() { return Arr::flatten($this->bindings); }
然后就是执行我们熟悉的PDO操作了:
\Illuminate\Database\Connection::select
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(); }); }
准备查询statement,绑定值,执行statement,fetchAll,一切都是那么熟悉。
获取当前页的所有数据
获取当前页的所有数据调用了EloquentBuilder
的get
方法:
public function get($columns = ['*'])
{
$builder = $this->applyScopes();
if (count($models = $builder->getModels($columns)) > 0) {
$models = $builder->eagerLoadRelations($models);
}
return $builder->getModel()->newCollection($models);
}
此方法完成了如下工作:
- 通过
QueryBuilder
获取到了包含当前页所有数据的数组,此数组的元素是stdClass。 - 通过
\Illuminate\Database\Eloquent\Builder::hydrate
方法,创建一个数组元素为DatabaseNotification
的数组。 - 创建一个通知类的集合
\Illuminate\Notifications\DatabaseNotificationCollection
执行的SQL语句
[2018-11-24 11:48:13] local.DEBUG: [670μs] select count(*) as aggregate from `notifications` where `notifications`.`notifiable_id` = '1' and `notifications`.`notifiable_id` is not null and `notifications`.`notifiable_type` = 'App\\Models\\User'
[2018-11-24 11:48:30] local.DEBUG: [980μs] select * from `notifications` where `notifications`.`notifiable_id` = '1' and `notifications`.`notifiable_id` is not null and `notifications`.`notifiable_type` = 'App\\Models\\User' order by `created_at` desc limit 20 offset 0
如果不用框架自己来写,就是这么两句SQL可以获取数据。但框架通过一种语义化的方式给我们自动拼接好了然后还得到了可以链式操作的对象,还附加了很多方便的方法比如分页以及标记已读之类的。
小结
- 看似一个小小的方法'notifications',只是获取一下数据库表中的数据,居然涉及到这么多的操作,而本文仅仅记录了大致的流程,里面一些更详细的操作比如组装sql语句,执行PDO,log记录等等,都没写出来。框架真的是太厚了。
- 基类都是设置共同属性和方法,继承类一级级设置自己的属性和自己的方法。
本作品采用《CC 协议》,转载必须注明作者和本文链接