INDEX
Laravelを知らない中級(中年)プログラマーがマイグレーションファイルの仕組みを調べてみたけど全くわからない!
- その1「マイグレーションファイルを見てみよう」
- その2「$app['db']って何者?」
- その3「Repositoryクラス」
- その4「Larabelアプリケーションの初期化の流れ」
- その5「リポジトリの読込」
- その6「データベース接続」
- その7「スキーマビルダー」
- その8「migrate コマンドの実行」
- その9「Kernel::handle()」
リポジトリの読込
前回は「Larabelアプリケーションの初期化の流れ」を見てきました。流れでリポジトリの読込について読んでみましょう!
$app['db']
が生成される時の流れをもう一度見てみましょう。コマンドラインからartisan
コマンドが叩かれると、Illumination/Foundation/Console/Kernel::$bootstrappers[]
配列に登録されたサービスがアプリケーションコンテナで読み込まれます。この中にIllumination/Foundation/Bootstrap/RegisterProviders
が含まれています。このRegisterProviders
のbootstrap()
メソッドには$app->registerConfiguredProviders()
とあります。Application::registerConfiguredProviders()
は以下が定義されています。
/**
* Register all of the configured providers.
*
* @return void
*/
public function registerConfiguredProviders()
{
$providers = Collection::make($this->config['app.providers'])
->partition(function ($provider) {
return strpos($provider, 'Illuminate\\') === 0;
});
$providers->splice(1, 0, [$this->make(PackageManifest::class)->providers()]);
(new ProviderRepository($this, new Filesystem, $this->getCachedServicesPath()))
->load($providers->collapse()->toArray());
}
Collection::make()
に$this->config['app.providers']
つまりPROJECT_ROOT/config/app.php
に記述されている配列のproviders
を渡しています。Collection
クラスはIlluminate/Support/Collection
ですが、このクラス自体にはmake()
は実装されていません。このメソッドはuse EnumeratesValues
されているトレイトのIlluminate/Support/Traits/EnumeratesValues
に以下のように定義されています。
/**
* Create a new collection instance if the value isn't one already.
*
* @param mixed $items
* @return static
*/
public static function make($items = [])
{
return new static($items);
}
/**
* Results array of items from Collection or Arrayable.
*
* @param mixed $items
* @return array
*/
protected function getArrayableItems($items)
{
if (is_array($items)) {
return $items;
} elseif ($items instanceof Enumerable) {
return $items->all();
} elseif ($items instanceof Arrayable) {
return $items->toArray();
} elseif ($items instanceof Jsonable) {
return json_decode($items->toJson(), true);
} elseif ($items instanceof JsonSerializable) {
return (array) $items->jsonSerialize();
} elseif ($items instanceof Traversable) {
return iterator_to_array($items);
}
return (array) $items;
}
/**
* Create a new collection.
*
* @param mixed $items
* @return void
*/
public function __construct($items = [])
{
$this->items = $this->getArrayableItems($items);
}
渡されたconfig['app.providers']
は配列でした。これをCollection::__construct()
に引数として渡し生成したインスタンスを戻しています。コンストラクタは受け取った引数を$this->items に getArrayableItems
メソッドを通して格納しています。このCollection
クラスはEnumerable
インターフェイスを継承しています。Illuminate/Support/Enumerable
クラスは複数のインターフェイスを実装しています。
interface Enumerable extends Arrayable, Countable, IteratorAggregate, Jsonable, JsonSerializable
様々な形でデータを扱えるようです。その情報を配列の形に変換する役割を担うのがEnumeratesValues::getArrayableItems
です。
Application::registerConfiguredProviders()
の続きです。Collection::make()
の戻り値にメソッドチェーンでpartition()
メソッドをクロージャーを引数にコールしています。これはトレイトのIlluminate/Support/Traits/EnumeratesValues
で定義されています。
/**
* Partition the collection into two arrays using the given callback or key.
*
* @param callable|string $key
* @param mixed $operator
* @param mixed $value
* @return static
*/
public function partition($key, $operator = null, $value = null)
{
$passed = [];
$failed = [];
$callback = func_num_args() === 1
? $this->valueRetriever($key)
: $this->operatorForWhere(...func_get_args());
foreach ($this as $key => $item) {
if ($callback($item, $key)) {
$passed[$key] = $item;
} else {
$failed[$key] = $item;
}
}
return new static([new static($passed), new static($failed)]);
}
/**
* Determine if the given value is callable, but not a string.
*
* @param mixed $value
* @return bool
*/
protected function useAsCallable($value)
{
return ! is_string($value) && is_callable($value);
}
/**
* Get a value retrieving callback.
*
* @param callable|string|null $value
* @return callable
*/
protected function valueRetriever($value)
{
if ($this->useAsCallable($value)) {
return $value;
}
return function ($item) use ($value) {
return data_get($item, $value);
};
}
/**
* Get an operator checker callback.
*
* @param string $key
* @param string|null $operator
* @param mixed $value
* @return \Closure
*/
protected function operatorForWhere($key, $operator = null, $value = null)
{
if (func_num_args() === 1) {
$value = true;
$operator = '=';
}
if (func_num_args() === 2) {
$value = $operator;
$operator = '=';
}
return function ($item) use ($key, $operator, $value) {
$retrieved = data_get($item, $key);
$strings = array_filter([$retrieved, $value], function ($value) {
return is_string($value) || (is_object($value) && method_exists($value, '__toString'));
});
if (count($strings) < 2 && count(array_filter([$retrieved, $value], 'is_object')) == 1) {
return in_array($operator, ['!=', '<>', '!==']);
}
switch ($operator) {
default:
case '=':
case '==': return $retrieved == $value;
case '!=':
case '<>': return $retrieved != $value;
case '<': return $retrieved < $value;
case '>': return $retrieved > $value;
case '<=': return $retrieved <= $value;
case '>=': return $retrieved >= $value;
case '===': return $retrieved === $value;
case '!==': return $retrieved !== $value;
}
};
}
partition()
に渡した第一引数はクロージャーでした。メソッドのPHPDocはストリングも受け入ると定義されています。おそらくこれを解決するロジックが挟まれるでしょう。メソッドの引数の数を検証し、1ならばvalueRetriever()
をコールしています。コール先では、渡された引数が使用可能か検証しています。問題なく使用可能ならそのまま戻します。使用できない場合(おそらくストリング型)はdata_get()
を通して戻しています。このdata_get()
ですが、今までのコードで定義を見かけていません。これは、Laravel が用意している「ヘルパ」関数です。実体はIlluminate/Support/helpers.php
です。プロジェクト内でどこからでも使える便利関数的なものでしょう。こちらはまた別の機会に追ってみたいと思います。この関数自体の挙動は、「ドット」記法を使用し、ネストした配列やオブジェクトから値を取得するものです。Collection
インスタンスの情報にドットシンタックスでアクセスするためのものと推測できます。次はpartition
の引数の数が1出なかった場合です。operatorForWhere()
をpartition()
に渡された引数をsplat演算子で引数としてコールしています。operatorForWhere()
を見てみましょう。
引数が1つ若しくは2つだった時の挙動を整理して、引数で渡されたキーと演算子と値でフィルタした結果を返すクロージャーを戻します。
partition()
の続きに戻ります。Collection
クラスはIteratorAggregate
を継承しているのでforeach
で回すことができます。自身をforeach
で回します。先程整えたコールバック関数で内容を検証し、true
とfalse
で選別して配列を生成し、各情報を格納したCollection
クラスを配列として格納したCollection
クラスを戻します。
Application::registerConfiguredProviders()
の続きです。
$providers = Collection::make($this->config['app.providers'])
->partition(function ($provider) {
return strpos($provider, 'Illuminate\\') === 0;
});
PROJECT_ROOT/config/app.php
に記述された配列のproviders
キーの内容の配列をforeach
で回し、strpos($provider, 'Illuminate\\') === 0
を判定して結果ごとにCollection
を生成して$providers
に代入するということのようです。
次に行きましょう。
$providers->splice(1, 0, [$this->make(PackageManifest::class)->providers()]);
先程の結果で生成されたCollection
クラスのsplice()
メソッドをコールしています。
/**
* Splice a portion of the underlying collection array.
*
* @param int $offset
* @param int|null $length
* @param mixed $replacement
* @return static
*/
public function splice($offset, $length = null, $replacement = [])
{
if (func_num_args() === 1) {
return new static(array_splice($this->items, $offset));
}
return new static(array_splice($this->items, $offset, $length, $replacement));
}
引数が1つの場合、$offset
で指定された場所以降の情報を削除した情報をもつCollection
インスタンスを返します。
一つ以上の場合はarray_splice
関数どおりの処理で生成された配列情報を持つCollection
インスタンスを返します。
今回は、PROJECT_ROOT/config/app.php
に記述された配列のproviders
キーの内容で、Illuminate\\
で始まるもの以外を削除し、$this->make(PackageManifest::class)->providers()
を追加したCollection
インスタンスを返します。アプリケーションコンテナにバインドされたPackageManifest::providers()
の戻り値はreturn $this->config('providers')
となっています。リポジトリに登録されたproviders
の設定値を追加しています。
(new ProviderRepository($this, new Filesystem, $this->getCachedServicesPath()))->load($providers->collapse()->toArray());
Illuminate/Foundation/ProviderRepository
を生成してload()
メソッドをコールしています。
/**
* Register the application service providers.
*
* @param array $providers
* @return void
*/
public function load(array $providers)
{
$manifest = $this->loadManifest();
// First we will load the service manifest, which contains information on all
// service providers registered with the application and which services it
// provides. This is used to know which services are "deferred" loaders.
if ($this->shouldRecompile($manifest, $providers)) {
$manifest = $this->compileManifest($providers);
}
// Next, we will register events to load the providers for each of the events
// that it has requested. This allows the service provider to defer itself
// while still getting automatically loaded when a certain event occurs.
foreach ($manifest['when'] as $provider => $events) {
$this->registerLoadEvents($provider, $events);
}
// We will go ahead and register all of the eagerly loaded providers with the
// application so their services can be registered with the application as
// a provided service. Then we will set the deferred service list on it.
foreach ($manifest['eager'] as $provider) {
$this->app->register($provider);
}
$this->app->addDeferredServices($manifest['deferred']);
}
/**
* Load the service provider manifest JSON file.
*
* @return array|null
*/
public function loadManifest()
{
// The service manifest is a file containing a JSON representation of every
// service provided by the application and whether its provider is using
// deferred loading or should be eagerly loaded on each request to us.
if ($this->files->exists($this->manifestPath)) {
$manifest = $this->files->getRequire($this->manifestPath);
if ($manifest) {
return array_merge(['when' => []], $manifest);
}
}
}
/**
* Determine if the manifest should be compiled.
*
* @param array $manifest
* @param array $providers
* @return bool
*/
public function shouldRecompile($manifest, $providers)
{
return is_null($manifest) || $manifest['providers'] != $providers;
}
/**
* Register the load events for the given provider.
*
* @param string $provider
* @param array $events
* @return void
*/
protected function registerLoadEvents($provider, array $events)
{
if (count($events) < 1) {
return;
}
$this->app->make('events')->listen($events, function () use ($provider) {
$this->app->register($provider);
});
}
/**
* Compile the application service manifest file.
*
* @param array $providers
* @return array
*/
protected function compileManifest($providers)
{
// The service manifest should contain a list of all of the providers for
// the application so we can compare it on each request to the service
// and determine if the manifest should be recompiled or is current.
$manifest = $this->freshManifest($providers);
foreach ($providers as $provider) {
$instance = $this->createProvider($provider);
// When recompiling the service manifest, we will spin through each of the
// providers and check if it's a deferred provider or not. If so we'll
// add it's provided services to the manifest and note the provider.
if ($instance->isDeferred()) {
foreach ($instance->provides() as $service) {
$manifest['deferred'][$service] = $provider;
}
$manifest['when'][$provider] = $instance->when();
}
// If the service providers are not deferred, we will simply add it to an
// array of eagerly loaded providers that will get registered on every
// request to this application instead of "lazy" loading every time.
else {
$manifest['eager'][] = $provider;
}
}
return $this->writeManifest($manifest);
}
/**
* Create a fresh service manifest data structure.
*
* @param array $providers
* @return array
*/
protected function freshManifest(array $providers)
{
return ['providers' => $providers, 'eager' => [], 'deferred' => []];
}
/**
* Write the service manifest file to disk.
*
* @param array $manifest
* @return array
*
* @throws \Exception
*/
public function writeManifest($manifest)
{
if (! is_writable($dirname = dirname($this->manifestPath))) {
throw new Exception("The {$dirname} directory must be present and writable.");
}
$this->files->replace(
$this->manifestPath, '<!--?php return '.var_export($manifest, true).';'
);
return array_merge(['when' =--> []], $manifest);
}
/**
* Create a new provider instance.
*
* @param string $provider
* @return \Illuminate\Support\ServiceProvider
*/
public function createProvider($provider)
{
return new $provider($this->app);
}
まず、__construct()
の第三引数として渡された$this->getCachedServicesPath()
のパスにキャッシュファイルがあるかloadManifest()
で判定し、存在していた場合読み込みます。
shouldRecompile()
はキャッシュの正当性を判定します。キャッシュが存在しない、若しくはload()
に引数として渡されたプロバイダーがキャッシュと等価でない場合は、compileManifest()
で$manifest
を構築します。
compileManifest()
はfreshManifest
で$manifest
を初期化して、引数として受け取ったプロバイダー情報をforeach
で回し、一つずつcreateProvider()
でインスタンス生成し、そのインスタンスが遅延ロードか否かを$manifest
に登録します。そして構築できたものをwriteManifest()
でファイルとして書き出します。おそらく構築したものをキャッシュしてファイルへのアクセス回数を減らす高速化のための処理でしょう。
次に$manifest['when']
をforeach
で回し、registerLoadEvents()
をコールしてプロバイダがどのタイミングでApplication::register()
に渡されるのか登録します。
そして、$manifest['eager']
をforeach
で回して、初期化時に必要なプロバイダーを登録します。
最後にアプリケーションコンテナに遅延ロードするプロバイダーを登録します。
このproviders
に登録されたサービスは、実際にmake
される時は、Application::registerCoreContainerAliases()
でエイリアス登録されたものを、Container::getAlias()
で名前解決しビルドされます。
以上の処理で、PROJECT_ROOT/config/app.php
のproviders
に記述されているサービスを読み込む流れがわかりました。
次回
少しだけ謎が解けてきましたね!いい感じです。ちょっとずつ全体がわかるといいですね!
次回はデータベース接続について読んでみたいと思います。そろそろわかるようになっているかもしれません!
続く☆