LoginSignup
4
4

More than 3 years have passed since last update.

Laravelを知らない中級(中年)プログラマーがマイグレーションファイルの仕組みを調べてみたけど全くわからない!その4:「Larabelアプリケーションの初期化の流れ」

Last updated at Posted at 2020-04-21

INDEX

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

Larabelアプリケーションの初期化の流れ

Laravelをマイグレーションファイルから少しずつ読み始めて、どうやらお作法であろう存在にいくつか出会いました。そろそろ初期化の流れを追ってみようと思います。

artisanが叩かれると、PROJECT_ROOT/bootstrap/app.phpが呼び出されます。その最初で Illuminate\Foundation\Application が生成されます。

PROJECT_ROOT/bootstrap/app.php抜粋
$app = new Illuminate\Foundation\Application(
    $_ENV['APP_BASE_PATH'] ?? dirname(__DIR__)
);

Applicationクラスを見てみましょう。実体はPROJECT_ROOT/vendor/laravel/framework/src/Illuminate/Foundation/Application.phpです。このクラスのコンストラクタと関連する処理の一部は以下です。

Illuminate/Foundation/Application
/**
 * Create a new Illuminate application instance.
 *
 * @param  string|null  $basePath
 * @return void
 */
public function __construct($basePath = null)
{
    if ($basePath) {
        $this->setBasePath($basePath);
    }
    $this->registerBaseBindings();
    $this->registerBaseServiceProviders();
    $this->registerCoreContainerAliases();
}
/**
 * Register the basic bindings into the container.
 *
 * @return void
 */
protected function registerBaseBindings()
{
    static::setInstance($this);
    $this->instance('app', $this);
    $this->instance(Container::class, $this);
    $this->singleton(Mix::class);
    $this->instance(PackageManifest::class, new PackageManifest(
        new Filesystem, $this->basePath(), $this->getCachedPackagesPath()
    ));
}
/**
 * Set the base path for the application.
 *
 * @param  string  $basePath
 * @return $this
 */
public function setBasePath($basePath)
{
    $this->basePath = rtrim($basePath, '\/');
    $this->bindPathsInContainer();
    return $this;
}
/**
 * Bind all of the application paths in the container.
 *
 * @return void
 */
protected function bindPathsInContainer()
{
    $this->instance('path', $this->path());
    $this->instance('path.base', $this->basePath());
    $this->instance('path.lang', $this->langPath());
    $this->instance('path.config', $this->configPath());
    $this->instance('path.public', $this->publicPath());
    $this->instance('path.storage', $this->storagePath());
    $this->instance('path.database', $this->databasePath());
    $this->instance('path.resources', $this->resourcePath());
    $this->instance('path.bootstrap', $this->bootstrapPath());
}
/**
 * Get the path to the application "app" directory.
 *
 * @param  string  $path
 * @return string
 */
public function path($path = '')
{
    $appPath = $this->appPath ?: $this->basePath.DIRECTORY_SEPARATOR.'app';

    return $appPath.($path ? DIRECTORY_SEPARATOR.$path : $path);
}
/**
 * Get the base path of the Laravel installation.
 *
 * @param  string  $path Optionally, a path to append to the base path
 * @return string
 */
public function basePath($path = '')
{
    return $this->basePath.($path ? DIRECTORY_SEPARATOR.$path : $path);
}
/**
 * Get the base path of the Laravel installation.
 *
 * @param  string  $path Optionally, a path to append to the base path
 * @return string
 */
public function basePath($path = '')
{
    return $this->basePath.($path ? DIRECTORY_SEPARATOR.$path : $path);
}
/**
 * Get the path to the language files.
 *
 * @return string
 */
public function langPath()
{
    return $this->resourcePath().DIRECTORY_SEPARATOR.'lang';
}
/**
 * Get the path to the application configuration files.
 *
 * @param  string  $path Optionally, a path to append to the config path
 * @return string
 */
public function configPath($path = '')
{
    return $this->basePath.DIRECTORY_SEPARATOR.'config'.($path ? DIRECTORY_SEPARATOR.$path : $path);
}
/* ========== 略 ========== */

コンストラクタではまず、パス情報の初期化を行っています。アプリケーションコンテナが生成された時に渡された引数は$_ENV['APP_BASE_PATH'] ?? dirname(__DIR__)でした。それを引数としてsetBasePath()メソッドを呼び出しています。setBasePath()メソッドは渡されたパス情報を$this->basePathに格納し、bindPathsInContainer()メソッドを呼び出しています。このメソッドではパスの情報をバインドしているようです。$this->instance()が何をやっているのか、現段階ではまだ良くわかりません。おそらく後々追うことになるでしょう。今追っているconfigpath.configとしてPROJECT_ROOT/configが設定されているようです。

次にregisterBaseBindings()メソッドが呼び出されています。registerBaseBindings()メソッドでは以下の処理が行われています。

Illuminate/Foundation/Application
$this->instance(PackageManifest::class, new PackageManifest(
    new Filesystem, $this->basePath(), $this->getCachedPackagesPath()
));

PackageManifestという名前で第二引数で生成したPackageManifestクラスをアプリケーションコンテナにバインドしているようです。PackageManifestクラスに渡している三つの引数を追ってみましょう。まずFilesystemです。実体はPROJECT_ROOT/vendor/laravel/framework/src/Illuminate/Filesystem/Filesystem.phpです。ファイルを管理するクラスのようです。次に上で初期化された$basePathが渡されます。そして最後にキャッシュパスが渡されます。キャッシュパスについては後ほど追ってみます。PackageManifestクラスを見てみましょう。コンストラクタに以下のような定義がされています。

Illuminate/Foundation/PackageManifest
/**
 * Create a new package manifest instance.
 *
 * @param  \Illuminate\Filesystem\Filesystem  $files
 * @param  string  $basePath
 * @param  string  $manifestPath
 * @return void
 */
public function __construct(Filesystem $files, $basePath, $manifestPath)
{
    $this->files = $files;
    $this->basePath = $basePath;
    $this->manifestPath = $manifestPath;
    $this->vendorPath = $basePath.'/vendor';
}

引数として渡された情報を変数に格納し、追加でvendorPathもハードコーディングで定義しています。

このへんでApplicationクラスはひとまず置いておいて、一番最初に叩かれるartisanファイルを見てみましょう。 次の処理に$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class)とあります。これは、$app = require_once __DIR__.'/bootstrap/app.php'の中で以下のようにアプリケーションコンテナにシングルトンとしてバインドしているようです。生成の流れの詳細はまだわかりません。

PROJECT_ROOT/bootstrap/app.php抜粋
$app->singleton(
    Illuminate\Contracts\Console\Kernel::class,
    App\Console\Kernel::class
);

App\Console\Kernelcomposer/autoload_classmap.php'App\\Console\\Kernel' => $baseDir . '/app/Console/Kernel.php'と定義されています。
つまり実体はPROJECT_ROOT/app/Console/Kernel.phpのようです。このクラスはIlluminate\Foundation\Console\Kernelを継承しています。Illuminate\Foundation\Console\Kernelに以下のような定義がされています。

Illuminate\Foundation\Console\Kernel
/**
 * The bootstrap classes for the application.
 *
 * @var array
 */
protected $bootstrappers = [
    \Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables::class,
    \Illuminate\Foundation\Bootstrap\LoadConfiguration::class,
    \Illuminate\Foundation\Bootstrap\HandleExceptions::class,
    \Illuminate\Foundation\Bootstrap\RegisterFacades::class,
    \Illuminate\Foundation\Bootstrap\SetRequestForConsole::class,
    \Illuminate\Foundation\Bootstrap\RegisterProviders::class,
    \Illuminate\Foundation\Bootstrap\BootProviders::class,
];
/**
 * Run the console application.
 *
 * @param  \Symfony\Component\Console\Input\InputInterface  $input
 * @param  \Symfony\Component\Console\Output\OutputInterface|null  $output
 * @return int
 */
public function handle($input, $output = null)
{
    try {
        $this->bootstrap();
        return $this->getArtisan()->run($input, $output);
    } catch (Throwable $e) {
        $this->reportException($e);
        $this->renderException($output, $e);
        return 1;
    }
}
/**
 * Bootstrap the application for artisan commands.
 *
 * @return void
 */
public function bootstrap()
{
    if (! $this->app->hasBeenBootstrapped()) {
        $this->app->bootstrapWith($this->bootstrappers());
    }
    $this->app->loadDeferredProviders();
    if (! $this->commandsLoaded) {
        $this->commands();
        $this->commandsLoaded = true;
    }
}

$bootstrappersに初期設定用のクラスが配列としてセットされています。LoadConfigurationとそれっぽい記述がありますね。
そして handle() メソッドですが、これは artisan ファイルに make() メソッドの直後に以下のように書かれています。

PROJECT_ROOT/artisan抜粋
$status = $kernel->handle(
    $input = new Symfony\Component\Console\Input\ArgvInput,
    new Symfony\Component\Console\Output\ConsoleOutput
);

コマンド処理開始のトリガのようです。ここから先程のhandle()メソッドが呼ばれます。引数はおそらくコマンドラインから渡されたものと標準出力でしょう。そこは後で追うことになるので今は飛ばします。メイン処理のtryの中に$this->bootstrap()が記述されています。bootstrap()メソッドのはじめで、初期化がされていない場合はアプリケーションコンテナのbootstrapWith()に引数として 先程定義されているのを確認した初期設定用クラスの配列$bootstrappersを渡しています。アプリケーションコンテナのbootstrapWith()メソッドを見てみましょう。

Illuminate/Foundation/Application::bootstrapWith()
/**
 * Run the given array of bootstrap classes.
 *
 * @param  string[]  $bootstrappers
 * @return void
 */
public function bootstrapWith(array $bootstrappers)
{
    $this->hasBeenBootstrapped = true;
    foreach ($bootstrappers as $bootstrapper) {
        $this['events']->dispatch('bootstrapping: '.$bootstrapper, [$this]);
        $this->make($bootstrapper)->bootstrap($this);
        $this['events']->dispatch('bootstrapped: '.$bootstrapper, [$this]);
    }
}

まず、初期化フラグを立てています。先程、初期化がされているか確認するロジックがありましたがそれの判定はここを通過したか否かということのようです。その後に引数で渡された$bootstrappers配列をforeachで回します。配列の中身をeventsサービスのdispatch()メソッドに内容を渡しているようです。eventsサービスは ApplicationクラスのregisterCoreContainerAliases()メソッドでエイリアス登録されていましたね。実体はPROJECT_ROOT/vendor/laravel/framework/src/Illuminate/Events/Dispatcher.phpです。呼び出されているdispatchと関係するメソッドを見てみましょう。

Illuminate/Events/Dispatcher::dispatch|関連メソッド
/**
 * Fire an event and call the listeners.
 *
 * @param  string|object  $event
 * @param  mixed  $payload
 * @param  bool  $halt
 * @return array|null
 */
public function dispatch($event, $payload = [], $halt = false)
{
    // When the given "event" is actually an object we will assume it is an event
    // object and use the class as the event name and this event itself as the
    // payload to the handler, which makes object based events quite simple.
    [$event, $payload] = $this->parseEventAndPayload(
        $event, $payload
    );
    if ($this->shouldBroadcast($payload)) {
        $this->broadcastEvent($payload[0]);
    }
    $responses = [];
    foreach ($this->getListeners($event) as $listener) {
        $response = $listener($event, $payload);
        // If a response is returned from the listener and event halting is enabled
        // we will just return this response, and not call the rest of the event
        // listeners. Otherwise we will add the response on the response list.
        if ($halt && ! is_null($response)) {
            return $response;
        }
        // If a boolean false is returned from a listener, we will stop propagating
        // the event to any further listeners down in the chain, else we keep on
        // looping through the listeners and firing every one in our sequence.
        if ($response === false) {
            break;
        }
        $responses[] = $response;
    }
    return $halt ? null : $responses;
}
/**
 * Parse the given event and payload and prepare them for dispatching.
 *
 * @param  mixed  $event
 * @param  mixed  $payload
 * @return array
 */
protected function parseEventAndPayload($event, $payload)
{
    if (is_object($event)) {
        [$payload, $event] = [[$event], get_class($event)];
    }
    return [$event, Arr::wrap($payload)];
}
/**
 * Determine if the payload has a broadcastable event.
 *
 * @param  array  $payload
 * @return bool
 */
protected function shouldBroadcast(array $payload)
{
    return isset($payload[0]) &&
           $payload[0] instanceof ShouldBroadcast &&
           $this->broadcastWhen($payload[0]);
}
/**
 * Check if event should be broadcasted by condition.
 *
 * @param  mixed  $event
 * @return bool
 */
protected function broadcastWhen($event)
{
    return method_exists($event, 'broadcastWhen')
            ? $event->broadcastWhen() : true;
}
/**
 * Get all of the listeners for a given event name.
 *
 * @param  string  $eventName
 * @return array
 */
public function getListeners($eventName)
{
    $listeners = $this->listeners[$eventName] ?? [];
    $listeners = array_merge(
        $listeners,
        $this->wildcardsCache[$eventName] ?? $this->getWildcardListeners($eventName)
    );
    return class_exists($eventName, false)
                ? $this->addInterfaceListeners($eventName, $listeners)
                : $listeners;
}

まず、受け取った第一引数の初期化クラス名とイベント名(まずは「bootstrapping:」)を連結した文字列と第二引数のアプリケーションコンテナparseEventAndPayload()メソッドに通します。parseEventAndPayload()メソッドは、渡されたものがオブジェクトだった場合の処理や配列でない場合の処理をしています。Arr::wrap()は渡された引数が配列でない場合やnullの場合配列にして返すstaticメソッドです。

次にshouldBroadcast()メソッドでdispatch()メソッドに渡された第二引数の判定をしています。今回の場合はアプリケーションコンテナが対象になります。shouldBroadcast()メソッドは引数として受け取ったものを配列アクセスし、0番目がセットされていてそれがShouldBroadcastクラスのインスタンスで且つ、broadcastWhen()メソッドが存在しない、若しくは戻り値がtrueの場合trueに、そうでない場合falseになります。
判定がtrueの場合、broadcastEvent()メソッドに渡します。 今回処理するのはアプリケーションコンテナなのでこの処理は通りません。次のforeach ($this->getListeners($event) as $listener)の処理はイベントリスナとして登録されているかどうかを実装されたインターフェイスも含め調べそれらをforeachで回して処理をしているようです。イベント登録されている場合はトリガが叩かれる感じでしょうか。今回は主題からそれるので別の機会に追いたいと思います。コール元で戻り値を受け取っていないので次に行きましょう。

アプリケーションコンテナの$this->make($bootstrapper)->bootstrap($this)です。$bootstrappers配列を回して一つずつインスタンスを生成し、bootstrap($this)しているようです。サービスを生成し必ずbootstrap()メソッドが実行されるという仕様はこの部分で実現されているのでしょう。その後にbootstrappedイベントトリガを叩く手順ですね。

LoadConfiguration

Kernelクラスの初期化で$bootstrappers配列に含まれていたIlluminate\Foundation\Bootstrap\LoadConfigurationがサービスとして初期化されました。このクラスのbootstrap()メソッドが叩かれているはずです。見てみましょう。

Illuminate/Foundation/Bootstrap/LoadConfiguration::bootstrap()
/**
 * Bootstrap the given application.
 *
 * @param  \Illuminate\Contracts\Foundation\Application  $app
 * @return void
 */
public function bootstrap(Application $app)
{
    $items = [];
    // First we will see if we have a cache configuration file. If we do, we'll load
    // the configuration items from that file so that it is very quick. Otherwise
    // we will need to spin through every configuration file and load them all.
    if (file_exists($cached = $app->getCachedConfigPath())) {
        $items = require $cached;
        $loadedFromCache = true;
    }
    // Next we will spin through all of the configuration files in the configuration
    // directory and load each one into the repository. This will make all of the
    // options available to the developer for use in various parts of this app.
    $app->instance('config', $config = new Repository($items));
    if (! isset($loadedFromCache)) {
        $this->loadConfigurationFiles($app, $config);
    }
    // Finally, we will set the application's environment based on the configuration
    // values that were loaded. We will pass a callback which will be used to get
    // the environment in a web context where an "--env" switch is not present.
    $app->detectEnvironment(function () use ($config) {
        return $config->get('app.env', 'production');
    });
    date_default_timezone_set($config->get('app.timezone', 'UTC'));
    mb_internal_encoding('UTF-8');
}

bootstrap()メソッドはアプリケーションコンテナを引数として受け取ります。まず、アプリケーションコンテナのgetCachedConfigPath()メソッドを叩き戻り値のファイルが存在確認をしています。Application::getCachedConfigPath()と関連するメソッドは以下のようになっています。

Illuminate\Foundation\Application::Application
/**
 * Get the path to the configuration cache file.
 *
 * @return string
 */
public function getCachedConfigPath()
{
    return $this->normalizeCachePath('APP_CONFIG_CACHE', 'cache/config.php');
}
/**
 * Normalize a relative or absolute path to a cache file.
 *
 * @param  string  $key
 * @param  string  $default
 * @return string
 */
protected function normalizeCachePath($key, $default)
{
    if (is_null($env = Env::get($key))) {
        return $this->bootstrapPath($default);
    }
    return Str::startsWith($env, '/')
            ? $env
            : $this->basePath($env);
}

Application::getCachedConfigPath()APP_CONFIG_CACHEをキーに、デフォルト値にcache/config.phpを引数指定してnormalizeCachePath()メソッドをコールしています。normalizeCachePath()Env::get($key)nullであるか確認しています。
EnvクラスはPROJECT_ROOT/vendor/laravel/framework/src/Illuminate/Support/Env.phpです。get()メソッドを見てみましょう。

Illuminate\Support\Env::get()|getRepository()
/**
 * Get the environment repository instance.
 *
 * @return \Dotenv\Repository\RepositoryInterface
 */
public static function getRepository()
{
    if (static::$repository === null) {
        $adapters = array_merge(
            [new EnvConstAdapter, new ServerConstAdapter],
            static::$putenv ? [new PutenvAdapter] : []
        );
        static::$repository = RepositoryBuilder::create()
            ->withReaders($adapters)
            ->withWriters($adapters)
            ->immutable()
            ->make();
    }
    return static::$repository;
}
/**
 * Gets the value of an environment variable.
 *
 * @param  string  $key
 * @param  mixed  $default
 * @return mixed
 */
public static function get($key, $default = null)
{
    return Option::fromValue(static::getRepository()->get($key))
        ->map(function ($value) {
            switch (strtolower($value)) {
                case 'true':
                case '(true)':
                    return true;
                case 'false':
                case '(false)':
                    return false;
                case 'empty':
                case '(empty)':
                    return '';
                case 'null':
                case '(null)':
                    return;
            }
            if (preg_match('/\A([\'"])(.*)\1\z/', $value, $matches)) {
                return $matches[2];
            }
            return $value;
        })
        ->getOrCall(function () use ($default) {
            return value($default);
        });
}

Option::fromValue()に引数として渡しているstatic::getRepository()->get($key)から見てみます。getRepository()メソッドではstatic::$repositorynullでない場合はそれを返し、nullだった場合はstatic::$repositoryを構築する流れのようです。static::$repository自体はDotenv/Repository/RepositoryInterfaceインターフェイスを実装したインスタンスのようです。処理の中で以下のようなクラスが書かれています。

  • EnvConstAdapter
  • ServerConstAdapter
  • PutenvAdapter
  • RepositoryBuilder

これらは、vlucas/phpdotenvを利用したもののようです。Laravelの環境設定を.envに記述するのはこの仕組を利用するためのようです。Illuminate\Foundation\Console\Kernelで定義した$bootstrappersの一番最初にIlluminate\Foundation\Bootstrap\LoadEnvironmentVariablesがありました。これをKernel::handle()実行時に読み込んでいます。このLoadEnvironmentVariables::bootstrap()メソッドからcreateDotenv()メソッドが呼ばれ、Dotenv::createが実行されています。このメソッドに引数で渡される、環境設定ファイル名 「.env」 とパス情報は アプリケーションコンテナでprotected $environmentFile = '.env'と設定されています。変更したい場合はloadEnvironmentFrom()メソッドで変えられるようです。このphpdotenvでリポジトリを構築することで、Laravel設定ファイル、Apache設定、サーバ環境設定等をにアクセスするインターフェイスを整えてくれるようです。
getRepository()メソッドからRepositoryInterfaceインタフェースを実装した環境設定情報が返されます。そこからget()メソッドが呼ばれ、PhpOption\Optionインターフェイスが実装されたインスタンスが返されます。

PhpOption\Option

PhpOption\Optionとは何でしょうか。これはschmittjoh/php-optionのようです。autoload_classmap.php'PhpOption\\Option' => $vendorDir . '/phpoption/phpoption/src/PhpOption/Option.php'と定義されています。Option::fromValue()と関連するクラスは以下のように定義されています。

PhpOption\Option::fromValue()
abstract class Option implements IteratorAggregate
{
/**
 * Creates an option given a return value.
 *
 * This is intended for consuming existing APIs and allows you to easily
 * convert them to an option. By default, we treat ``null`` as the None
 * case, and everything else as Some.
 *
 * @template S
 *
 * @param S $value     The actual return value.
 * @param S $noneValue The value which should be considered "None"; null by
 *                     default.
 *
 * @return Option<S>
 */
public static function fromValue($value, $noneValue = null)
{
    if ($value === $noneValue) {
        return None::create();
    }
    return new Some($value);
}
/* ========== 略 ========== */
}
PhpOption\None::create()
final class None extends Option
{
/**
 * @return None
 */
public static function create()
{
    if (null === self::$instance) {
        self::$instance = new self();
    }
    return self::$instance;
}
/* ========== 略 ========== */
}
PhpOption\Some::__construct()
final class Some extends Option
{
/** @var T */
private $value;
/**
 * @param T $value
 */
public function __construct($value)
{
    $this->value = $value;
}
public function map($callable)
{
    return new self($callable($this->value));
}
/* ========== 略 ========== */
}

Optionクラスは抽象クラスで、これを静的メソッド呼び出しをしています。Optionクラス自体はIteratorAggregateインターフェイスを実装しています。イテレーターとしてアクセスが可能のようですね。渡された第一引数が第二引数と同じ場合は空のオブジェクトであるNoneインスタンスを、同じでなければ第一引数を内部に持ったSomeインスタンスを返すようです。値はSome->valueの形で格納され、この値に各種コールできるメソッドを実装しています。今回アクセスしたいのはAPP_CONFIG_CACHEなので、$valueAPP_CONFIG_CACHEがセットされたSomeインスタンスが返ってくるはずです。Some::map()メソッドの内容はreturn new self($callable($this->value))とあります。map()に渡された引数にはクロージャーが入っていました。Some::$valueを引数にしたクロージャーの結果を$valueに格納したSomeインスタンスが返ってくる流れです。そして返されたSomeインスタンスのgetOrCall()メソッドを引数としてクロージャーを入れて呼び出します。ただ、Some::getOrCall()return $this->valueしているだけですので、$valueがそのまま返されます。つまり、アプリケーションコンテナのnormalizeCachePathにあるif (is_null($env = Env::get($key)))は各環境設定をまとめたリポジトリからAPP_CONFIG_CACHEが存在するかを判定しています。もし、存在していな場合は第二引数で指定された$defaultつまりcache/config.phpを引数にbootstrapPath()が呼び出されます。このメソッドはbasePathbootstrapディレクトリを追加して引数の文字列を連結したものを返します。結果、PROJECT_ROOT/bootstrap/cache/config.phpという文字列が返されます。APP_CONFIG_CACHEが存在していた場合は、Str::startsWith($env, '/')が判定され文字列加工をします。startsWithJavaなどで使われる関数でPHP関数にないものを独自に定義したもののようです。文字列が引数で指定された文字列で始まるかを判定してtruefalseを返します。LoadConfiguration::bootstrap()にそのパスが返され、file_existsでそのファイルが存在するか判定され、存在した場合はそのキャッシュファイルを読み込み、読み込みフラグ$loadedFromCachetrueがセットされます。存在していた場合は読み込んだキャッシュを、存在していない場合は空の配列をアプリケーションコンテナにconfigの名前でバインドします。もし、存在しなかった場合はloadConfigurationFiles()メソッドを第一引数にアプリケーションコンテナ、第二引数に空の配列で初期化したRepositoryインスタンスを渡してコールします。

Illuminate/Foundation/Bootstrap/LoadConfiguration::loadConfigurationFiles()|関連メソッド
/**
 * Load the configuration items from all of the files.
 *
 * @param  \Illuminate\Contracts\Foundation\Application  $app
 * @param  \Illuminate\Contracts\Config\Repository  $repository
 * @return void
 *
 * @throws \Exception
 */
protected function loadConfigurationFiles(Application $app, RepositoryContract $repository)
{
    $files = $this->getConfigurationFiles($app);
    if (! isset($files['app'])) {
        throw new Exception('Unable to load the "app" configuration file.');
    }
    foreach ($files as $key => $path) {
        $repository->set($key, require $path);
    }
}
/**
 * Get all of the configuration files for the application.
 *
 * @param  \Illuminate\Contracts\Foundation\Application  $app
 * @return array
 */
protected function getConfigurationFiles(Application $app)
{
    $files = [];
    $configPath = realpath($app->configPath());
    foreach (Finder::create()->files()->name('*.php')->in($configPath) as $file) {
        $directory = $this->getNestedDirectory($file, $configPath);
        $files[$directory.basename($file->getRealPath(), '.php')] = $file->getRealPath();
    }
    ksort($files, SORT_NATURAL);
    return $files;
}
/**
 * Get the configuration file nesting path.
 *
 * @param  \SplFileInfo  $file
 * @param  string  $configPath
 * @return string
 */
protected function getNestedDirectory(SplFileInfo $file, $configPath)
{
    $directory = $file->getPath();
    if ($nested = trim(str_replace($configPath, '', $directory), DIRECTORY_SEPARATOR)) {
        $nested = str_replace(DIRECTORY_SEPARATOR, '.', $nested).'.';
    }
    return $nested;
}

loadConfigurationFiles()メソッドのはじめでgetConfigurationFiles()がコールされます。$configPath = realpath($app->configPath())で設定ファイルのパスをセットしています。これはApplication::configPath()return $this->basePath.DIRECTORY_SEPARATOR.'config'.($path ? DIRECTORY_SEPARATOR.$path : $path)と定義されていますので、PROJECT_ROOT/configが代入されます。次にforeachFinderクラスで色々したものを回しています。Finderクラスはautoload_classmap で /symfony/finder/Finder.phpと定義されています。

symfony/finder

symfony/finder/Finderを見てみましょう。
Symfonyのディレクトリやファイルの一覧を取得する便利機能が詰め込まれたコンポーネントのようです。見てみましょう。

symfony/finder/Finder抜粋
/**
 * Finder allows to build rules to find files and directories.
 *
 * It is a thin wrapper around several specialized iterator classes.
 *
 * All rules may be invoked several times.
 *
 * All methods return the current Finder object to allow chaining:
 *
 *     $finder = Finder::create()->files()->name('*.php')->in(__DIR__);
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class Finder implements \IteratorAggregate, \Countable
{
/* ========== 中略 ========== */
private $names = [];
/* ========== 中略 ========== */
/**
 * Creates a new Finder.
 *
 * @return static
 */
public static function create()
{
    return new static();
}
/**
 * Restricts the matching to files only.
 *
 * @return $this
 */
public function files()
{
    $this->mode = Iterator\FileTypeFilterIterator::ONLY_FILES;
    return $this;
}
/**
 * Adds rules that files must match.
 *
 * You can use patterns (delimited with / sign), globs or simple strings.
 *
 *     $finder->name('*.php')
 *     $finder->name('/\.php$/') // same as above
 *     $finder->name('test.php')
 *     $finder->name(['test.py', 'test.php'])
 *
 * @param string|string[] $patterns A pattern (a regexp, a glob, or a string) or an array of patterns
 *
 * @return $this
 *
 * @see FilenameFilterIterator
 */
public function name($patterns)
{
    $this->names = array_merge($this->names, (array) $patterns);
    return $this;
}
/**
 * Searches files and directories which match defined rules.
 *
 * @param string|string[] $dirs A directory path or an array of directories
 *
 * @return $this
 *
 * @throws DirectoryNotFoundException if one of the directories does not exist
 */
public function in($dirs)
{
    $resolvedDirs = [];
    foreach ((array) $dirs as $dir) {
        if (is_dir($dir)) {
            $resolvedDirs[] = $this->normalizeDir($dir);
        } elseif ($glob = glob($dir, (\defined('GLOB_BRACE') ? GLOB_BRACE : 0) | GLOB_ONLYDIR | GLOB_NOSORT)) {
            sort($glob);
            $resolvedDirs = array_merge($resolvedDirs, array_map([$this, 'normalizeDir'], $glob));
        } else {
            throw new DirectoryNotFoundException(sprintf('The "%s" directory does not exist.', $dir));
        }
    }
    $this->dirs = array_merge($this->dirs, $resolvedDirs);
    return $this;
}
}
symfony/finder/Iterator/FileTypeFilterIterator抜粋
/**
 * FileTypeFilterIterator only keeps files, directories, or both.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class FileTypeFilterIterator extends \FilterIterator
{
    const ONLY_FILES = 1;
    const ONLY_DIRECTORIES = 2;
    private $mode;
    /**
     * @param \Iterator $iterator The Iterator to filter
     * @param int       $mode     The mode (self::ONLY_FILES or self::ONLY_DIRECTORIES)
     */
    public function __construct(\Iterator $iterator, int $mode)
    {
        $this->mode = $mode;
        parent::__construct($iterator);
    }
/* ========== 略 ========== */
}

クラスドキュメントを見ると、「ファイルとディレクトリを検索するルールを構築できます」とあります。使用例に

$finder = Finder::create()->files()->name('*.php')->in(__DIR__);

とあります。LoadConfiguration:: getConfigurationFiles()の記述とほぼ同じなので、典型的な利用方法のようです。
まず、create()staticとして自分自身を生成して返します。その後使われるメソッドは基本的に戻り値が$thisになっていて、メソッドチェーンで処理をすすめる前提になっているようです。次にfiles()メソッドがコールされます。$this->modeIterator\FileTypeFilterIterator::ONLY_FILESを代入しています。ONLY_FILESは 1 が定義されています。ファイルだけ一覧にするモード定数なのでしょう。次にname()メソッドに'*.php'が引数として渡されています。name()メソッドではFinder::namesに渡された引数をarray_margeしています。検索条件を配列で蓄える機能と思われます。最後にin()メソッドが設定ファイルのパスを引数としてコールされます。渡された引数をforeachで回し、正規化したパスをFinder::dirsにセットして自身を返します。Finderインスタンスはイテレーターインターフェイスを持っていますので、foreachで回すことができます。つまり、createで生成した後、モードやディレクトリ、その他条件をセットし結果をイテレーターとして提供するコンポーネントですね。ではloadConfigurationFilesに戻りましょう。

PROJECT_ROOT/config/*.php 設定ファイルの読み込み

symfony/finderから受け取るものはPROJECT_ROOT/config/の配下にある.php拡張子のついたファイル一覧だということが先程わかりました。一覧を受け取った後に以下の処理をしています。

Illuminate/Foundation/Bootstrap/LoadConfiguration::getConfigurationFiles()抜粋
$directory = $this->getNestedDirectory($file, $configPath);
$files[$directory.basename($file->getRealPath(), '.php')] = $file->getRealPath();

パスの表記を整えてソートした配列を作り変えしています。loadConfigurationFilesの続きに戻りましょう。

Illuminate/Foundation/Bootstrap/LoadConfiguration::loadConfigurationFiles()抜粋
if (! isset($files['app'])) {
    throw new Exception('Unable to load the "app" configuration file.');
}
foreach ($files as $key => $path) {
    $repository->set($key, require $path);
}

受け取った配列にappをキーとした情報が存在しなければ 「Unable to load the “app” configuration file.」というメッセージを添えた例外を投げています。存在すれば第二引数でうけとったリポジトリインスタンスに設定ファイル名をキーとして、設定ファイルを読み込み、その戻り値の配列をセットしてloadConfigurationFiles()メソッドの処理は終了です。LoadConfiguration::bootstrap()メソッドの続きに戻りましょう。

Illuminate/Foundation/Bootstrap/LoadConfiguration::bootstrap()抜粋
// Finally, we will set the application's environment based on the configuration
// values that were loaded. We will pass a callback which will be used to get
// the environment in a web context where an "--env" switch is not present.
$app->detectEnvironment(function () use ($config) {
    return $config->get('app.env', 'production');
});
Illuminate/Foundation/Application::detectEnvironment()
/**
 * Detect the application's current environment.
 *
 * @param  \Closure  $callback
 * @return string
 */
public function detectEnvironment(Closure $callback)
{
    $args = $_SERVER['argv'] ?? null;
    return $this['env'] = (new EnvironmentDetector)->detect($callback, $args);
}

detectEnvironment()メソッドにクロージャーを引数として渡しています。クロージャー自体の引数は設定リポジトリがuseで指定されています。クロージャーの戻り値は$config->get('app.env', 'production')とあります。ちょうど先程読んだところですね。PROJECT_ROOT/config/app.phpに記述されている配列の 「env」 キーを探し、なければproductionを返します。PRODUCT_ROOT/config/app.phpには'env' => env('APP_ENV', 'production')とあるので、.envファイルなどでAPP_ENVが設定されていればそれを、なければproductionを返します。
detectEnvironment()メソッドを読んでみましょう。EnvironmentDetectorインスタンスを生成して、先程のコールバックとスクリプトへ渡された引数の配列をEnvironmentDetector::detect()メソッドに渡しています。EnvironmentDetectorクラスを読んでみましょう。実体はIlluminate/Foundation/EnvironmentDetectorです。

Illuminate/Foundation/EnvironmentDetector::detect()
/**
 * Detect the application's current environment.
 *
 * @param  \Closure  $callback
 * @param  array|null  $consoleArgs
 * @return string
 */
public function detect(Closure $callback, $consoleArgs = null)
{
    if ($consoleArgs) {
        return $this->detectConsoleEnvironment($callback, $consoleArgs);
    }
    return $this->detectWebEnvironment($callback);
}
/**
 * Set the application environment for a web request.
 *
 * @param  \Closure  $callback
 * @return string
 */
protected function detectWebEnvironment(Closure $callback)
{
    return $callback();
}
/**
 * Set the application environment from command-line arguments.
 *
 * @param  \Closure  $callback
 * @param  array  $args
 * @return string
 */
protected function detectConsoleEnvironment(Closure $callback, array $args)
{
    // First we will check if an environment argument was passed via console arguments
    // and if it was that automatically overrides as the environment. Otherwise, we
    // will check the environment as a "web" request like a typical HTTP request.
    if (! is_null($value = $this->getEnvironmentArgument($args))) {
        return $value;
    }
    return $this->detectWebEnvironment($callback);
}
/**
 * Get the environment argument from the console.
 *
 * @param  array  $args
 * @return string|null
 */
protected function getEnvironmentArgument(array $args)
{
    foreach ($args as $i => $value) {
        if ($value === '--env') {
            return $args[$i + 1] ?? null;
        }
        if (Str::startsWith($value, '--env')) {
            return head(array_slice(explode('=', $value), 1));
        }
    }
}

detect()は受け取った第二引数、スクリプトへ渡された引数、つまりコマンドラインからartisanを実行した時の引数の配列の存在を判定し、あった場合は、detectConsoleEnvironment()メソッドの、なかった場合はdetectWebEnvironment()メソッドの戻り値を返します。detectConsoleEnvironment()メソッドはgetEnvironmentArgument()メソッドにコマンド引数の配列を引数として渡します。getEnvironmentArgument()メソッドはコマンド引数をforeachで回し、--envで指定した値が存在する場合はそれを返します。つまりdetect()artisanコマンドの引数の中に--envがあった場合はenvを上書します。その結果をアプリケーションコンテナのenvに代入します。LoadConfiguration::bootstrap()の残りはタイムゾーンをセットしてエンコードをUTF-8に設定して完了です。
最初の目的より少し読みすぎましたが、LoadConfiguration::loadConfigurationFiles()でリポジトリに設定ファイルを読み込みセットしているところを確認しました。Illuminate/Config/Repository::get()で返されるArr::get($this->items, $key, $default)$this->itemsの正体が明確になりました。

次回

初期化の流れを読んでみてなんとなく、ふんわりやってることがわかってきましたが、まだオブジェクトの生成やイベント周りなどはっきりとつかめていない部分がありますね。徐々に理解していけると良いですが、どうなるでしょうか。次回はこの流れでリポジトリの読込を探っていきたいと思います。

続く☆

4
4
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
4
4