用户模型的 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 协议》,转载必须注明作者和本文链接
日拱一卒
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!
未填写
文章
93
粉丝
85
喜欢
153
收藏
121
排名:71
访问:11.4 万
私信
所有博文
社区赞助商