Eloquent Model 内の処理の委譲
はじめに
Eloquent Model の create メソッ の実装を見てやろうと Laravel Framework のコードを潜ってみたところ、なかなか面白い処理フローになっていたのでメモ
TL;DR
Model::create()
を実行すると処理がBuilder::create()
に委譲される- 処理の委譲は
__call()
メソッドとForwardCalls
トレイトを使って実現される
__call()
https://laravel.com/api/8.x/Illuminate/Database/Eloquent/Model.html を見たらわかると思うが Model クラスには create()
メソッドが存在しない。
また、Model クラスは他のクラスを継承していないし、use しているトレイトにもそれらしいメソッドはない。では、Model::create
を実行したときに一体なにが呼び出されるのか。
実は Model クラスは __call()
メソッドを実装していて、定義されていないメソッドを呼び出すと __call()
メソッドに処理が移る
abstract class Model implements Arrayable, ArrayAccess, Jsonable, JsonSerializable, QueueableEntity, UrlRoutable { use Concerns\HasAttributes, Concerns\HasEvents, Concerns\HasGlobalScopes, Concerns\HasRelationships, Concerns\HasTimestamps, Concerns\HidesAttributes, Concerns\GuardsAttributes, ForwardsCalls; // forwardCallTo() が実装されている ...(snip)... /** * Handle dynamic method calls into the model. * * @param string $method * @param array $parameters * @return mixed */ public function __call($method, $parameters) { if (in_array($method, ['increment', 'decrement'])) { return $this->$method(...$parameters); } // リレーション周りの処理はこの分岐に入るっぽい? if ($resolver = (static::$relationResolvers[get_class($this)][$method] ?? null)) { return $resolver($this); } // Model::create() を呼び出すと通る処理フローはこっち return $this->forwardCallTo($this->newQuery(), $method, $parameters); }
コードを見る限りリレーション以外の処理は最後の行で ForwardsCalls@forwardCallTo()
に処理が移る (要調査)
forwardCallTo()
forwardCallTo()
メソッドの実装は以下のようになっている
/** * Forward a method call to the given object. * * @param mixed $object * @param string $method * @param array $parameters * @return mixed * * @throws \BadMethodCallException */ protected function forwardCallTo($object, $method, $parameters) { try { return $object->{$method}(...$parameters); } catch (Error | BadMethodCallException $e) { ...(snip)... } }
実装の通り、forwardCallTo()
は第 1 引数のオブジェクトに処理を委譲している
newQuery() の戻り値
結局 Model はどこに処理を委譲しているのか。これは newQuery()
の戻り地を見るとわかる
処理フローはコードを見るとわかるが、いかんせん長いので結果だけをいうと最終的には newEloquentBuilder()
メソッド内で
Illuminate\Database\Eloquent\Builder
クラスのインスタンスが生成され、これが返される
Illuminate\Database\Eloquent\Model
クラス内
/** * Get a new query builder for the model's table. * * @return \Illuminate\Database\Eloquent\Builder */ public function newQuery() { return $this->registerGlobalScopes($this->newQueryWithoutScopes()); } /** * Register the global scopes for this builder instance. * * @param \Illuminate\Database\Eloquent\Builder $builder * @return \Illuminate\Database\Eloquent\Builder */ public function registerGlobalScopes($builder) { foreach ($this->getGlobalScopes() as $identifier => $scope) { $builder->withGlobalScope($identifier, $scope); } return $builder; } /** * Get a new query builder that doesn't have any global scopes. * * @return \Illuminate\Database\Eloquent\Builder|static */ public function newQueryWithoutScopes() { return $this->newModelQuery() ->with($this->with) ->withCount($this->withCount); } /** * Get a new query builder that doesn't have any global scopes or eager loading. * * @return \Illuminate\Database\Eloquent\Builder|static */ public function newModelQuery() { return $this->newEloquentBuilder( $this->newBaseQueryBuilder() )->setModel($this); } /** * Create a new Eloquent query builder for the model. * * @param \Illuminate\Database\Query\Builder $query * @return \Illuminate\Database\Eloquent\Builder|static */ public function newEloquentBuilder($query) { return new Builder($query); }
Illuminate\Database\Eloquent\Builder
クラス内
/** * Set a model instance for the model being queried. * * @param \Illuminate\Database\Eloquent\Model $model * @return $this */ public function setModel(Model $model) { ...(snip)... return $this; }
おわりに
Eloquent Model は自身で実装していない処理は ForwardCalls トレイトを使って、Builder クラスに処理を委譲していることがわかった