1
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-04-25

INDEX

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

php artisan migrate

いよいよ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()メソッドをコールしています。これは実行環境を確認しIBMiSeries (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はコマンドラインの入出力を担うクラスということがわかりました。
今回は Laravel というより Symfony のお話でしたね。もしかしたら今後 Symfony のコードを色々と読む必要があるのかもしれません。

次回

次回はKernel::handle()メソッドから追いかけていきたいと思います。

続く☆

1
3
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
1
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?