使用者模型的 notifications 方法 原始碼閱讀筆記

hustnzj發表於2018-11-24

《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
[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 協議》,轉載必須註明作者和本文連結
日拱一卒

相關文章