0
0

More than 3 years have passed since last update.

Laravelを知らない中級(中年)プログラマーがマイグレーションファイルの仕組みを調べてみたけど全くわからない!その5 「リポジトリの読込」

Last updated at Posted at 2020-04-22

INDEX

Laravelを知らない中級(中年)プログラマーがマイグレーションファイルの仕組みを調べてみたけど全くわからない!

リポジトリの読込

前回は「Larabelアプリケーションの初期化の流れ」を見てきました。流れでリポジトリの読込について読んでみましょう!
$app['db']が生成される時の流れをもう一度見てみましょう。コマンドラインからartisanコマンドが叩かれると、Illumination/Foundation/Console/Kernel::$bootstrappers[]配列に登録されたサービスがアプリケーションコンテナで読み込まれます。この中にIllumination/Foundation/Bootstrap/RegisterProvidersが含まれています。このRegisterProvidersbootstrap()メソッドには$app->registerConfiguredProviders()とあります。Application::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に以下のように定義されています。

Illuminate/Support/Traits/EnumeratesValues::make()|getArrayableItems()
/**
 * 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;
}
Illuminate/Support/Collection::__construct()
/**
 * 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で定義されています。

Illuminate/Support/Traits/EnumeratesValues::partition()|関連メソッド
/**
 * 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で回します。先程整えたコールバック関数で内容を検証し、truefalseで選別して配列を生成し、各情報を格納した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()メソッドをコールしています。

Illumination/Support/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()メソッドをコールしています。

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.phpprovidersに記述されているサービスを読み込む流れがわかりました。

次回

少しだけ謎が解けてきましたね!いい感じです。ちょっとずつ全体がわかるといいですね!
次回はデータベース接続について読んでみたいと思います。そろそろわかるようになっているかもしれません!

続く☆

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0