INDEX
Laravelを知らない中級(中年)プログラマーがマイグレーションファイルの仕組みを調べてみたけど全くわからない!
- その1「マイグレーションファイルを見てみよう」
- その2「$app['db']って何者?」
- その3「Repositoryクラス」
- その4「Larabelアプリケーションの初期化の流れ」
- その5「リポジトリの読込」
- その6「データベース接続」
- その7「スキーマビルダー」
- その8「migrate コマンドの実行」
- その9「Kernel::handle()」
Larabelアプリケーションの初期化の流れ
Laravelをマイグレーションファイルから少しずつ読み始めて、どうやらお作法であろう存在にいくつか出会いました。そろそろ初期化の流れを追ってみようと思います。
artisan
が叩かれると、PROJECT_ROOT/bootstrap/app.php
が呼び出されます。その最初で Illuminate\Foundation\Application が生成されます。
$app = new Illuminate\Foundation\Application(
$_ENV['APP_BASE_PATH'] ?? dirname(__DIR__)
);
Application
クラスを見てみましょう。実体はPROJECT_ROOT/vendor/laravel/framework/src/Illuminate/Foundation/Application.php
です。このクラスのコンストラクタと関連する処理の一部は以下です。
/**
* 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()
が何をやっているのか、現段階ではまだ良くわかりません。おそらく後々追うことになるでしょう。今追っているconfig
はpath.config
としてPROJECT_ROOT/config
が設定されているようです。
次にregisterBaseBindings()
メソッドが呼び出されています。registerBaseBindings()
メソッドでは以下の処理が行われています。
$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
クラスを見てみましょう。コンストラクタに以下のような定義がされています。
/**
* 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'
の中で以下のようにアプリケーションコンテナにシングルトンとしてバインドしているようです。生成の流れの詳細はまだわかりません。
$app->singleton(
Illuminate\Contracts\Console\Kernel::class,
App\Console\Kernel::class
);
App\Console\Kernel
はcomposer/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
に以下のような定義がされています。
/**
* 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() メソッドの直後に以下のように書かれています。
$status = $kernel->handle(
$input = new Symfony\Component\Console\Input\ArgvInput,
new Symfony\Component\Console\Output\ConsoleOutput
);
コマンド処理開始のトリガのようです。ここから先程のhandle()
メソッドが呼ばれます。引数はおそらくコマンドラインから渡されたものと標準出力でしょう。そこは後で追うことになるので今は飛ばします。メイン処理のtry
の中に$this->bootstrap()
が記述されています。bootstrap()
メソッドのはじめで、初期化がされていない場合はアプリケーションコンテナのbootstrapWith()
に引数として 先程定義されているのを確認した初期設定用クラスの配列$bootstrappers
を渡しています。アプリケーションコンテナの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
と関係するメソッドを見てみましょう。
/**
* 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()
メソッドが叩かれているはずです。見てみましょう。
/**
* 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()
と関連するメソッドは以下のようになっています。
/**
* 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()
メソッドを見てみましょう。
/**
* 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::$repository
がnull
でない場合はそれを返し、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()
と関連するクラスは以下のように定義されています。
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);
}
/* ========== 略 ========== */
}
final class None extends Option
{
/**
* @return None
*/
public static function create()
{
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
/* ========== 略 ========== */
}
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
なので、$value
にAPP_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()
が呼び出されます。このメソッドはbasePath
にbootstrap
ディレクトリを追加して引数の文字列を連結したものを返します。結果、PROJECT_ROOT/bootstrap/cache/config.php
という文字列が返されます。APP_CONFIG_CACHE
が存在していた場合は、Str::startsWith($env, '/')
が判定され文字列加工をします。startsWith
はJava
などで使われる関数でPHP
関数にないものを独自に定義したもののようです。文字列が引数で指定された文字列で始まるかを判定してtrue
かfalse
を返します。LoadConfiguration::bootstrap()
にそのパスが返され、file_exists
でそのファイルが存在するか判定され、存在した場合はそのキャッシュファイルを読み込み、読み込みフラグ$loadedFromCache
にtrue
がセットされます。存在していた場合は読み込んだキャッシュを、存在していない場合は空の配列をアプリケーションコンテナにconfig
の名前でバインドします。もし、存在しなかった場合はloadConfigurationFiles()
メソッドを第一引数にアプリケーションコンテナ、第二引数に空の配列で初期化したRepository
インスタンスを渡してコールします。
/**
* 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
が代入されます。次にforeach
でFinder
クラスで色々したものを回しています。Finder
クラスはautoload_classmap で /symfony/finder/Finder.php
と定義されています。
symfony/finder
symfony/finder/Finder
を見てみましょう。
Symfony
のディレクトリやファイルの一覧を取得する便利機能が詰め込まれたコンポーネントのようです。見てみましょう。
/**
* 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;
}
}
/**
* 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->mode
にIterator\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
拡張子のついたファイル一覧だということが先程わかりました。一覧を受け取った後に以下の処理をしています。
$directory = $this->getNestedDirectory($file, $configPath);
$files[$directory.basename($file->getRealPath(), '.php')] = $file->getRealPath();
パスの表記を整えてソートした配列を作り変えしています。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()
メソッドの続きに戻りましょう。
// 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');
});
/**
* 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
です。
/**
* 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
の正体が明確になりました。
次回
初期化の流れを読んでみてなんとなく、ふんわりやってることがわかってきましたが、まだオブジェクトの生成やイベント周りなどはっきりとつかめていない部分がありますね。徐々に理解していけると良いですが、どうなるでしょうか。次回はこの流れでリポジトリの読込を探っていきたいと思います。
続く☆