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