用户模型的 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_typenotifiable_id.
  • 获得通知对象关联的数据库表(默认为 notifications)
  • 获得用户模型的主键(作为后面一对多关系中的父模型 id)
  • DatabaseNotification 对象再创建一个 EloquentBuilder,并将二者绑定在一起。这里还进行了一些 global scopeeager 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

进入父类 HasOneOrManyaddConstraints 方法:

    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,
        ]);
    }

此方法完成了如下工作:

  • 设置当前页数,每页显示数。
  • 获取分页所需的总数,如果有总数,那么会获取到当前页的所有数据。
  • 调用 EloquentBuilderpaginator 方法,来创建一个 length-aware 分页器实例。

获取分页所需的总数#

获取分页所需的总数调用了 QueryBuilderget 方法:

    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,一切都是那么熟悉。

获取当前页的所有数据#

获取当前页的所有数据调用了 EloquentBuilderget 方法:

    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 协议》,转载必须注明作者和本文链接
日拱一卒
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
未填写
文章
92
粉丝
87
喜欢
152
收藏
121
排名:73
访问:11.3 万
私信
所有博文
社区赞助商