XIONCC

【フレームワークを読む】Laravelのマイグレーションファイルは何をしているのか? :その7:migrate コマンドの実行

php artisan migrate

マイグレーションを実行する際、コマンドラインから以下のコマンドを叩きます。

$ php artisan migrate

コマンドが叩かれると、PROJECT_ROOT/artisan が実行されます。もう一度内容を見てみましょう。

PROJECT_ROOT/artisan

#!/usr/bin/env php
<?php

define('LARAVEL_START', microtime(true));

/*
|--------------------------------------------------------------------------
| Register The Auto Loader
|--------------------------------------------------------------------------
|
| Composer provides a convenient, automatically generated class loader
| for our application. We just need to utilize it! We'll require it
| into the script here so that we do not have to worry about the
| loading of any our classes "manually". Feels great to relax.
|
*/

require __DIR__.'/vendor/autoload.php';

$app = require_once __DIR__.'/bootstrap/app.php';

/*
|--------------------------------------------------------------------------
| Run The Artisan Application
|--------------------------------------------------------------------------
|
| When we run the console application, the current CLI command will be
| executed in this console and the response sent back to a terminal
| or another output device for the developers. Here goes nothing!
|
*/

$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);

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

/*
|--------------------------------------------------------------------------
| Shutdown The Application
|--------------------------------------------------------------------------
|
| Once Artisan has finished running, we will fire off the shutdown events
| so that any final work may be done by the application before we shut
| down the process. This is the last thing to happen to the request.
|
*/

$kernel->terminate($input, $status);

exit($status);

実行開始時間が定義され、composerのオートロードが読み込まれ、PROJECT_ROOT/bootstrap/app.php が読み込まれ、Kernel サービスがコンテナに登録されます。ここまで長いこと読んできたのでそこまではもう問題なく理解できますね。いよいよメインディッシュです。

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

Kernel::handle() メソッドがコールされます。コマンドの実行です。
引数に Symfony\Component\Console\Input\ArgvInputSymfony\Component\Console\Output\ConsoleOutput が指定されています。
見てみましょう。

Symfony\Component\Console\Input\ArgvInput

autoload_classmap によると、実体は /symfony/console/Input/ArgvInput.php のようです。

Symfony\Component\Console\Input\ArgvInput::__construct()

    public function __construct(array $argv = null, InputDefinition $definition = null)
    {
        if (null === $argv) {
            $argv = $_SERVER['argv'];
        }

        // strip the application name
        array_shift($argv);

        $this->tokens = $argv;

        parent::__construct($definition);
    }
    /**
     * {@inheritdoc}
     */
    protected function parse()
    {
        $parseOptions = true;
        $this->parsed = $this->tokens;
        while (null !== $token = array_shift($this->parsed)) {
            if ($parseOptions && '' == $token) {
                $this->parseArgument($token);
            } elseif ($parseOptions && '--' == $token) {
                $parseOptions = false;
            } elseif ($parseOptions && 0 === strpos($token, '--')) {
                $this->parseLongOption($token);
            } elseif ($parseOptions && '-' === $token[0] && '-' !== $token) {
                $this->parseShortOption($token);
            } else {
                $this->parseArgument($token);
            }
        }
    }

Symfony\Component\Console\Input\Input::__construct()

    public function __construct(InputDefinition $definition = null)
    {
        if (null === $definition) {
            $this->definition = new InputDefinition();
        } else {
            $this->bind($definition);
            $this->validate();
        }
    }
    /**
     * {@inheritdoc}
     */
    public function bind(InputDefinition $definition)
    {
        $this->arguments = [];
        $this->options = [];
        $this->definition = $definition;

        $this->parse();
    }
    /**
     * Processes command line arguments.
     */
    abstract protected function parse();

    /**
     * {@inheritdoc}
     */
    public function validate()
    {
        $definition = $this->definition;
        $givenArguments = $this->arguments;

        $missingArguments = array_filter(array_keys($definition->getArguments()), function ($argument) use ($definition, $givenArguments) {
            return !\array_key_exists($argument, $givenArguments) && $definition->getArgument($argument)->isRequired();
        });

        if (\count($missingArguments) > 0) {
            throw new RuntimeException(sprintf('Not enough arguments (missing: "%s").', implode(', ', $missingArguments)));
        }
    }
ArgvInput::__construct()
第一引数は配列型でコマンドラインの引数です。
第二引数はInputDefinition インスタンスです。
戻り値はありません。

ArgvInput クラスはコマンドラインから受け取る引数を解析する仕組みを持ちます。
第一引数があればその配列を、なければ$_SERVER['argv']$tokens に代入します。
コマンドラインの引数を代入した $token 配列の0番目、つまりコマンド名を削除します。今回の場合は 0番目に「artisan」 というストリングが入っていますのでそれを削除します。
その後、第二引数に InputDefinition インスタンスが渡されていればそれを、なければnull を引数にし、継承したInput クラスのコンストラクタをコールします。

Input::__construct() は引数として InputDefinition インスタンスが渡されていた場合、それをバインドし、validate() メソッドをコールします。
渡されていなければ InputDefinition インスタンスを生成し、$definition に代入します。
InputDefinition クラスは、コマンドオプションの記述ルールを定義するクラスのようです。コードドキュメントに以下のような記述があります。

 * A InputDefinition represents a set of valid command line arguments and options.
 *
 * Usage:
 *
 *     $definition = new InputDefinition([
 *         new InputArgument('name', InputArgument::REQUIRED),
 *         new InputOption('foo', 'f', InputOption::VALUE_REQUIRED),
 *     ]);
引数に InputOption 型を格納した配列を受け取り、引数とオプションを管理するもののようです。
これ以上は Symfony の話になりますので詳細をリーディングは別の機会にしますが、bind() で 引数、オプション、オプション定義をセットし、オーバーライドしたArgvInput::parse() で解析し、validate() で処理をする流れと読み解けます。

Symfony\Component\Console\Output\ConsoleOutput

ConsoleOutput クラスの方も見てみましょう。
autoload_classmap によると、実体は /symfony/console/Output/ConsoleOutput.php のようです。

ConsoleOutput::__construct() | 関連メソッド

    /**
     * @param int                           $verbosity The verbosity level (one of the VERBOSITY constants in OutputInterface)
     * @param bool|null                     $decorated Whether to decorate messages (null for auto-guessing)
     * @param OutputFormatterInterface|null $formatter Output formatter instance (null to use default OutputFormatter)
     */
    public function __construct(int $verbosity = self::VERBOSITY_NORMAL, bool $decorated = null, OutputFormatterInterface $formatter = null)
    {
        parent::__construct($this->openOutputStream(), $verbosity, $decorated, $formatter);

        $actualDecorated = $this->isDecorated();
        $this->stderr = new StreamOutput($this->openErrorStream(), $verbosity, $decorated, $this->getFormatter());

        if (null === $decorated) {
            $this->setDecorated($actualDecorated && $this->stderr->isDecorated());
        }
    }

    /**
     * @return resource
     */
    private function openOutputStream()
    {
        if (!$this->hasStdoutSupport()) {
            return fopen('php://output', 'w');
        }

        return @fopen('php://stdout', 'w') ?: fopen('php://output', 'w');
    }
    /**
     * Returns true if current environment supports writing console output to
     * STDERR.
     *
     * @return bool
     */
    protected function hasStderrSupport()
    {
        return false === $this->isRunningOS400();
    }

    /**
     * Checks if current executing environment is IBM iSeries (OS400), which
     * doesn't properly convert character-encodings between ASCII to EBCDIC.
     */
    private function isRunningOS400(): bool
    {
        $checks = [
            \function_exists('php_uname') ? php_uname('s') : '',
            getenv('OSTYPE'),
            PHP_OS,
        ];

        return false !== stripos(implode(';', $checks), 'OS400');
    }

ConsoleOutput::__construct()
第一引数は整数型で冗長レベル指定定数です。
第二引数はブーリアン型若しくは null でメッセージを装飾するかを決めるパラメータです。
第三引数は OutputFormatterInterface インスタンスで出力用のフォーマッタです。nullの場合はデフォルトのフォーマッタが使われます。
戻り値はありません。

ConsoleOutput クラスはコマンドラインインターフェースでの出力を担うクラスです。
第一引数は冗長性レベル定数の指定とあります。ConsoleOutput クラスは多段階継承をしていて、冗長性レベルの定数はその基底クラスで定義されています。
処理の最初にいきなりスーパークラスのコンストラクタをコールしています。その第一引数に openOutputStream() メソッドをコールしています。これは実行環境を確認しIBMのiSeries (OS400)だった場合、文字エンコードをASCIIからEBCDICに正しく変換しないため、出力ストリームを php://output でオープンし、そうでない場合は php://stdout で出力ストリームをオープンできるか試し、出来ない場合は php://output でオープンしたものを返すメソッドです。とても面白そうですが、脱線がすぎるので今回は深追いしません。
他はほぼ受け取った引数をそのままスーパークラスに渡しています。
StreamOutput::__construct()を見てみましょう。
渡された第一引数がストリームかどうか検証しています。ストリームでない場合は「The StreamOutput class needs a stream as its first argument.」というメッセージを添えて例外 InvalidArgumentException をスローします。
function_existsis_resource の前にバックスラッシュがついています。明示しないとかぶるファンクションのようなものがあるのでしょうか。なぜ必要なのかちょっと思い当たりません。

StreamOutput::__construct()

    /**
     * @param resource                      $stream    A stream resource
     * @param int                           $verbosity The verbosity level (one of the VERBOSITY constants in OutputInterface)
     * @param bool|null                     $decorated Whether to decorate messages (null for auto-guessing)
     * @param OutputFormatterInterface|null $formatter Output formatter instance (null to use default OutputFormatter)
     *
     * @throws InvalidArgumentException When first argument is not a real stream
     */
    public function __construct($stream, int $verbosity = self::VERBOSITY_NORMAL, bool $decorated = null, OutputFormatterInterface $formatter = null)
    {
        if (!\is_resource($stream) || 'stream' !== get_resource_type($stream)) {
            throw new InvalidArgumentException('The StreamOutput class needs a stream as its first argument.');
        }

        $this->stream = $stream;

        if (null === $decorated) {
            $decorated = $this->hasColorSupport();
        }

        parent::__construct($verbosity, $decorated, $formatter);
    }
    /**
     * Returns true if the stream supports colorization.
     *
     * Colorization is disabled if not supported by the stream:
     *
     * This is tricky on Windows, because Cygwin, Msys2 etc emulate pseudo
     * terminals via named pipes, so we can only check the environment.
     *
     * Reference: Composer\XdebugHandler\Process::supportsColor
     * https://github.com/composer/xdebug-handler
     *
     * @return bool true if the stream supports colorization, false otherwise
     */
    protected function hasColorSupport()
    {
        // Follow https://no-color.org/
        if (isset($_SERVER['NO_COLOR']) || false !== getenv('NO_COLOR')) {
            return false;
        }

        if ('Hyper' === getenv('TERM_PROGRAM')) {
            return true;
        }

        if (\DIRECTORY_SEPARATOR === '\') {
            return (\function_exists('sapi_windows_vt100_support')
                && @sapi_windows_vt100_support($this->stream))
                || false !== getenv('ANSICON')
                || 'ON' === getenv('ConEmuANSI')
                || 'xterm' === getenv('TERM');
        }

        return stream_isatty($this->stream);
    }

StreamOutput::__construct()
第一引数はリソース型でストリームリソースです。
第二引数は整数型で冗長レベル定数です。
第三引数はブーリアン型若しくは null でメッセージを装飾するかを決めるパラメータです。
第四引数は OutputFormatterInterface インスタンスで出力用のフォーマッタです。nullの場合はデフォルトのフォーマッタが使われます。
戻り値はありません。

ConsoleOutput::__construct() からそのままの引数に加えてストリームリソースを渡されます。
受け取った第一引数がストリームリソースでない場合「The StreamOutput class needs a stream as its first argument.」とメッセージを添えて例外 InvalidArgumentException をスローします。
受け取ったストリームリソースを $this->stream に代入します。
第三引数はメッセージ装飾をするか否かのステータスです。これに null が渡された場合、hasColorSupport() メソッドがコールされ、自動で判定されます。
判定ロジックは以下です。
サーバ変数若しくはgetenv()の「NO_COLOR」が true にセットされていた場合、装飾しません。
getenv() の「TERM_PROGRAM」が Hyper にセットされていた場合、装飾します。
ディレクトリの区切り文字が「\\」で且つ、Windows コンソールの出力バッファに関連付けられたストリームのVT100サポートしていて、getenv() で確認しそれが利用できる設定だった場合、装飾します。
上記以外の場合でストリームがTTYに対応している場合装飾します。

非常に面白そうですね。機会があったら掘り下げたいところです。

Output::__construct()

    /**
     * @param int                           $verbosity The verbosity level (one of the VERBOSITY constants in OutputInterface)
     * @param bool                          $decorated Whether to decorate messages
     * @param OutputFormatterInterface|null $formatter Output formatter instance (null to use default OutputFormatter)
     */
    public function __construct(?int $verbosity = self::VERBOSITY_NORMAL, bool $decorated = false, OutputFormatterInterface $formatter = null)
    {
        $this->verbosity = null === $verbosity ? self::VERBOSITY_NORMAL : $verbosity;
        $this->formatter = $formatter ?: new OutputFormatter();
        $this->formatter->setDecorated($decorated);
    }
ConsoleOutput::__construct()
第一引数は整数型で冗長レベル指定定数です。
第二引数はブーリアン型若しくは null でメッセージを装飾するかを決めるパラメータです。
第三引数は OutputFormatterInterface インスタンスで出力用のフォーマッタです。nullの場合はデフォルトのフォーマッタが使われます。
戻り値はありません。

受け取った引数から、冗長レベルとフォーマッタを設定しフォーマッタに装飾ステータスをセットします。

OutputInterface::const

    const VERBOSITY_QUIET = 16;
    const VERBOSITY_NORMAL = 32;
    const VERBOSITY_VERBOSE = 64;
    const VERBOSITY_VERY_VERBOSE = 128;
    const VERBOSITY_DEBUG = 256;

    const OUTPUT_NORMAL = 1;
    const OUTPUT_RAW = 2;
    const OUTPUT_PLAIN = 4;
冗長レベル定数は上記のように定義されています。

Symfony\Component\Console\Input\ArgvInputSymfony\Component\Console\Output\ConsoleOutput はコマンドラインの入出力を担うクラスということがわかりました。
Kernel::handle() メソッドを見てみましょう。