いよいよ run() の実行です。
実際にコールされるのは Illuminate\Console\Application::run() です。
さっそく見てみましょう。
Illuminate\Console\Application::run()
/**
* {@inheritdoc}
*/
public function run(InputInterface $input = null, OutputInterface $output = null)
{
$commandName = $this->getCommandName(
$input = $input ?: new ArgvInput
);
$this->events->dispatch(
new CommandStarting(
$commandName, $input, $output = $output ?: new ConsoleOutput
)
);
$exitCode = parent::run($input, $output);
$this->events->dispatch(
new CommandFinished($commandName, $input, $output, $exitCode)
);
return $exitCode;
}
Illuminate\Console\Application::run()
Symfony\Component\Console\Input\ArgvInput インスタンスです。第二引数は
Symfony\Component\Console\Output\ConsoleOutput インスタンスです。戻り値は コマンドが成功した場合は 0 問題があった場合はエラーコードが返ります。
まず、getCommandName() メソッドで叩かれたコマンドを調べます。
getCommandName() メソッドは Illuminate\Console\Application のスーパークラスである Symfony\Component\Console\Application に定義されています。見てみましょう。
Symfony\Component\Console\Application::getCommandName()
/**
* Gets the name of the command based on input.
*
* @return string|null
*/
protected function getCommandName(InputInterface $input)
{
return $this->singleCommand ? $this->defaultCommand : $input->getFirstArgument();
}
/**
* Sets the default Command name.
*
* @return self
*/
public function setDefaultCommand(string $commandName, bool $isSingleCommand = false)
{
$this->defaultCommand = $commandName;
if ($isSingleCommand) {
// Ensure the command exist
$this->find($commandName);
$this->singleCommand = true;
}
return $this;
}
Symfony\Component\Console\Application::getCommandName()
Symfony\Component\Console\Input\ArgvInput インスタンスです。戻り値は ストリング若しくは
null です。
$this->singleCommand を検証して true であれば $this->defaultCommand を そうでなければ 第一引数の getFirstArgument() の戻り値を返します。
$this->singleCommand は今回の流れで設定されるところがありませんでした。
$this->singleCommand が変更されるインターフェースは setDefaultCommand() メソッドのみです。
vendor/symfony/var-dumper/Resources/bin/var-dump-server に以下のようなロジックがあります。
setDefaultCommand($command->getName(), true)
setDefaultCommand() メソッドでデフォルトコマンドを設定しコマンドアプリケーションを実行する手段を提供していると推測されます。今回は深追いはしません。
ということで、今回は第一引数の getFirstArgument() の戻り値が返ります。第一引数で渡される ArgvInput を見てみましょう。
Symfony\Component\Console\Input\ArgvInput::__construct | getFirstArgument()
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);
}
/**
* Returns the first argument from the raw parameters (not parsed).
*
* @return string|null The value of the first argument or null otherwise
*/
public function getFirstArgument()
{
$isOption = false;
foreach ($this->tokens as $i => $token) {
if ($token && '-' === $token[0]) {
if (false !== strpos($token, '=') || !isset($this->tokens[$i + 1])) {
continue;
}
// If it's a long option, consider that everything after "--" is the option name.
// Otherwise, use the last char (if it's a short option set, only the last one can take a value with space separator)
$name = '-' === $token[1] ? substr($token, 2) : substr($token, -1);
if (!isset($this->options[$name]) && !$this->definition->hasShortcut($name)) {
// noop
} elseif ((isset($this->options[$name]) || isset($this->options[$name = $this->definition->shortcutToName($name)])) && $this->tokens[$i + 1] === $this->options[$name]) {
$isOption = true;
}
continue;
}
if ($isOption) {
$isOption = false;
continue;
}
return $token;
}
return null;
}
Symfony\Component\Console\Input\ArgvInput::__construct | getFirstArgument()
戻り値は ストリング若しくは
null で最初の引数の値です。。
getFirstArgument() メソッドのはじめで $this->tokens を foreach で回しています。$this->tokens は Symfony\Component\Console\Input\ArgvInput::__construct() でセットしているようです。
ArgvInput インスタンスが生成されたのは、PROJECT_ROOT/artisan の以下部分で引数として渡されたときです。
$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);
$status = $kernel->handle(
$input = new Symfony\Component\Console\Input\ArgvInput,
new Symfony\Component\Console\Output\ConsoleOutput
);
引数は渡されていませんので、 $this->tokens には $_SERVER['argv'] の0番目が array_shift で削られたものが入ります。
$this->token を foreach で回します。
回された引数の先頭が 「-」 で始まる場合は、引数を解析します。
「--」 で始まる場合はその後のすべてがオプション名とし、そうでない場合は最後の文字をオプション名とします。
取得したオプション名が登録されている若しくは定義されている場合は $isOption を true にして foreach を続行します。
回された引数の先頭が 「-」 で始まらない場合は $isOption を判定し、 true であれば false を代入し foreach を続行します。そうでなければその引数を戻します。
この流れでなにをしているかとオプションと設定値を除外した最初の引数を取り出す処理です。
Symfony\Component\Console\Application::getCommandName() の戻り値がわかりました。
続きを見ましょう。
Illuminate\Console\Application::run() の $commandName はコマンドオプションとその値を抜いた最初の引数、つまり今回の場合は migrate が入っています。
続きは以下のように記述されています。
$this->events->dispatch(
new CommandStarting(
$commandName, $input, $output = $output ?: new ConsoleOutput
)
);
$this->event はLaravel のイベントディスパッチャーです。それの dispatch() をコールしています。実際に叩かれるのは Illuminate/Events/Dispatcher::dispatch() ですね。処理の内容はイベント登録処理をしつつ make して生成されたインスタンスの bootstrap() メソッドをコールするというものでした。
では、渡している引数を見てみましょう。
CommandStarting にコマンド名と入出力を渡しています。CommandStarting は Illuminate\Console\Events\CommandStarting です。
Illuminate\Console\Events\CommandStarting::__construct
/**
* Create a new event instance.
*
* @param string $command
* @param \Symfony\Component\Console\Input\InputInterface $input
* @param \Symfony\Component\Console\Output\OutputInterface $output
* @return void
*/
public function __construct($command, InputInterface $input, OutputInterface $output)
{
$this->input = $input;
$this->output = $output;
$this->command = $command;
}
TITLE
第二引数は
InputInterface で入力です。第三引数は
OutputInterface で出力です戻り値はありません。
コンストラクタと変数3つ以外になにもないクラスです。コマンド実行前の状態を記録するために使うのかもしれません。
さらに続きは以下のように記述されています。
$exitCode = parent::run($input, $output);
SymfonyApplication::run() を引数に入出力を渡してコールしています。
見てみましょう。
Symfony\Component\Console\Application::run()
/**
* Runs the current application.
*
* @return int 0 if everything went fine, or an error code
*
* @throws \Exception When running fails. Bypass this when {@link setCatchExceptions()}.
*/
public function run(InputInterface $input = null, OutputInterface $output = null)
{
putenv('LINES='.$this->terminal->getHeight());
putenv('COLUMNS='.$this->terminal->getWidth());
if (null === $input) {
$input = new ArgvInput();
}
if (null === $output) {
$output = new ConsoleOutput();
}
$renderException = function (\Throwable $e) use ($output) {
if ($output instanceof ConsoleOutputInterface) {
$this->renderThrowable($e, $output->getErrorOutput());
} else {
$this->renderThrowable($e, $output);
}
};
if ($phpHandler = set_exception_handler($renderException)) {
restore_exception_handler();
if (!\is_array($phpHandler) || !$phpHandler[0] instanceof ErrorHandler) {
$errorHandler = true;
} elseif ($errorHandler = $phpHandler[0]->setExceptionHandler($renderException)) {
$phpHandler[0]->setExceptionHandler($errorHandler);
}
}
$this->configureIO($input, $output);
try {
$exitCode = $this->doRun($input, $output);
} catch (\Exception $e) {
if (!$this->catchExceptions) {
throw $e;
}
$renderException($e);
$exitCode = $e->getCode();
if (is_numeric($exitCode)) {
$exitCode = (int) $exitCode;
if (0 === $exitCode) {
$exitCode = 1;
}
} else {
$exitCode = 1;
}
} finally {
// if the exception handler changed, keep it
// otherwise, unregister $renderException
if (!$phpHandler) {
if (set_exception_handler($renderException) === $renderException) {
restore_exception_handler();
}
restore_exception_handler();
} elseif (!$errorHandler) {
$finalHandler = $phpHandler[0]->setExceptionHandler(null);
if ($finalHandler !== $renderException) {
$phpHandler[0]->setExceptionHandler($finalHandler);
}
}
}
if ($this->autoExit) {
if ($exitCode > 255) {
$exitCode = 255;
}
exit($exitCode);
}
return $exitCode;
}
public function renderThrowable(\Throwable $e, OutputInterface $output): void
{
$output->writeln('', OutputInterface::VERBOSITY_QUIET);
$this->doRenderThrowable($e, $output);
if (null !== $this->runningCommand) {
$output->writeln(sprintf('%s ', sprintf($this->runningCommand->getSynopsis(), $this->getName())), OutputInterface::VERBOSITY_QUIET);
$output->writeln('', OutputInterface::VERBOSITY_QUIET);
}
}
Symfony\Component\Console\Application::run()
InputInterface で入力です。第二引数は
OutputInterface で出力です戻り値はコマンドが全て正常に実行された場合は 0 そうでない場合はエラーコードです。
ラスボス感がありますね。ようやくここまでたどり着きました。
まず環境変数に端末の幅の文字数、行数を設定します。
引数として渡された入出力が null だった場合は ArgvInput と ConsoleOutput インスタンスを生成して代入します。
$renderException にエラー出力をするクロージャーを代入します。出力インスタンスのクラスによって出力引数を変えています。renderThrowable() メソッドはメッセージの整形し出力するロジックが組まれています。面白そうですが、こちらは別の機会に追いたいと思います。
続いて、ユーザー定義の例外ハンドラ関数を設定し、configureIO() メソッドで入出力のオプション設定をした後、$this->doRun() メソッドをTRYします。
Symfony\Component\Console\Application::doRun()
/**
* Runs the current application.
*
* @return int 0 if everything went fine, or an error code
*/
public function doRun(InputInterface $input, OutputInterface $output)
{
if (true === $input->hasParameterOption(['--version', '-V'], true)) {
$output->writeln($this->getLongVersion());
return 0;
}
try {
// Makes ArgvInput::getFirstArgument() able to distinguish an option from an argument.
$input->bind($this->getDefinition());
} catch (ExceptionInterface $e) {
// Errors must be ignored, full binding/validation happens later when the command is known.
}
$name = $this->getCommandName($input);
if (true === $input->hasParameterOption(['--help', '-h'], true)) {
if (!$name) {
$name = 'help';
$input = new ArrayInput(['command_name' => $this->defaultCommand]);
} else {
$this->wantHelps = true;
}
}
if (!$name) {
$name = $this->defaultCommand;
$definition = $this->getDefinition();
$definition->setArguments(array_merge(
$definition->getArguments(),
[
'command' => new InputArgument('command', InputArgument::OPTIONAL, $definition->getArgument('command')->getDescription(), $name),
]
));
}
try {
$this->runningCommand = null;
// the command name MUST be the first element of the input
$command = $this->find($name);
} catch (\Throwable $e) {
if (!($e instanceof CommandNotFoundException && !$e instanceof NamespaceNotFoundException) || 1 !== \count($alternatives = $e->getAlternatives()) || !$input->isInteractive()) {
if (null !== $this->dispatcher) {
$event = new ConsoleErrorEvent($input, $output, $e);
$this->dispatcher->dispatch($event, ConsoleEvents::ERROR);
if (0 === $event->getExitCode()) {
return 0;
}
$e = $event->getError();
}
throw $e;
}
$alternative = $alternatives[0];
$style = new SymfonyStyle($input, $output);
$style->block(sprintf("\nCommand \"%s\" is not defined.\n", $name), null, 'error');
if (!$style->confirm(sprintf('Do you want to run "%s" instead? ', $alternative), false)) {
if (null !== $this->dispatcher) {
$event = new ConsoleErrorEvent($input, $output, $e);
$this->dispatcher->dispatch($event, ConsoleEvents::ERROR);
return $event->getExitCode();
}
return 1;
}
$command = $this->find($alternative);
}
$this->runningCommand = $command;
$exitCode = $this->doRunCommand($command, $input, $output);
$this->runningCommand = null;
return $exitCode;
}
/**
* Returns the long version of the application.
*
* @return string The long application version
*/
public function getLongVersion()
{
if ('UNKNOWN' !== $this->getName()) {
if ('UNKNOWN' !== $this->getVersion()) {
return sprintf('%s %s ', $this->getName(), $this->getVersion());
}
return $this->getName();
}
return 'Console Tool';
}
/**
* Gets the application version.
*
* @return string The application version
*/
public function getVersion()
{
return $this->version;
}
Symfony\Component\Console\Input\ArgvInput::hasParameterOption()
/**
* Returns true if the raw parameters (not parsed) contain a value.
*
* This method is to be used to introspect the input parameters
* before they have been validated. It must be used carefully.
* Does not necessarily return the correct result for short options
* when multiple flags are combined in the same option.
*
* @param string|array $values The values to look for in the raw parameters (can be an array)
* @param bool $onlyParams Only check real parameters, skip those following an end of options (--) signal
*
* @return bool true if the value is contained in the raw parameters
*/
public function hasParameterOption($values, bool $onlyParams = false)
{
$values = (array) $values;
foreach ($this->tokens as $token) {
if ($onlyParams && '--' === $token) {
return false;
}
foreach ($values as $value) {
// Options with values:
// For long options, test for '--option=' at beginning
// For short options, test for '-o' at beginning
$leading = 0 === strpos($value, '--') ? $value.'=' : $value;
if ($token === $value || '' !== $leading && 0 === strpos($token, $leading)) {
return true;
}
}
}
return false;
}
Symfony\Component\Console\Application::doRun()
InputInterface で入力です。第二引数は
OutputInterface で出力です戻り値はコマンドが全て正常に実行された場合は 0 そうでない場合はエラーコードです。
Symfony\Component\Console\Input\ArgvInput::hasParameterOption()
第二引数はブーリアン型で実際のパラメーターのみをチェックし、オプションの終わりシグナルに続くパラメーターをスキップします。
戻り値はブーリアン型でパラメーターに検索する値が存在してた場合
true、そうでない場合 false です。
hasParameterOption() を引数に配列 ['--version', '-V'] と onlyParams に true を渡しコールします。
hasParameterOption() は、コマンドに入力されたパラメータを検索するものです。配列 ['--version', '-V'] のどちらかが引数に存在する場合 true が戻されます。
true が戻ってきた場合、getLongVersion() でコマンドアプリケーション名、バージョン番号を連結した文字列を取得します。設定がない場合は「Console Tool」が戻されます。今回の場合は、「Laravel Framework」とバージョン番号が連結された文字列が返されます。返された文字列を引数に $output->writeln() で出力し、0 を戻します。
バージョン出力コマンドの場合の処理ですね。
だいたい流れは想像がつきますが、 $output->writeln() を見てみましょう。
Symfony\Component\Console\Output\Output::writeln() | 関連メソッド
/**
* Writes a message to the output and adds a newline at the end.
*
* @param string|iterable $messages The message as an iterable of strings or a single string
* @param int $options A bitmask of options (one of the OUTPUT or VERBOSITY constants), 0 is considered the same as self::OUTPUT_NORMAL | self::VERBOSITY_NORMAL
*/
public function writeln($messages, int $options = self::OUTPUT_NORMAL)
{
$this->write($messages, true, $options);
}
/**
* Writes a message to the output.
*
* @param string|iterable $messages The message as an iterable of strings or a single string
* @param bool $newline Whether to add a newline
* @param int $options A bitmask of options (one of the OUTPUT or VERBOSITY constants), 0 is considered the same as self::OUTPUT_NORMAL | self::VERBOSITY_NORMAL
*/
public function write($messages, bool $newline = false, int $options = self::OUTPUT_NORMAL)
{
if (!is_iterable($messages)) {
$messages = [$messages];
}
$types = self::OUTPUT_NORMAL | self::OUTPUT_RAW | self::OUTPUT_PLAIN;
$type = $types & $options ?: self::OUTPUT_NORMAL;
$verbosities = self::VERBOSITY_QUIET | self::VERBOSITY_NORMAL | self::VERBOSITY_VERBOSE | self::VERBOSITY_VERY_VERBOSE | self::VERBOSITY_DEBUG;
$verbosity = $verbosities & $options ?: self::VERBOSITY_NORMAL;
if ($verbosity > $this->getVerbosity()) {
return;
}
foreach ($messages as $message) {
switch ($type) {
case OutputInterface::OUTPUT_NORMAL:
$message = $this->formatter->format($message);
break;
case OutputInterface::OUTPUT_RAW:
break;
case OutputInterface::OUTPUT_PLAIN:
$message = strip_tags($this->formatter->format($message));
break;
}
$this->doWrite($message, $newline);
}
}
Symfony\Component\Console\Output\StreamOutput::doWrite()
/**
* Writes a message to the output.
*/
protected function doWrite(string $message, bool $newline)
{
if ($newline) {
$message .= PHP_EOL;
}
if (false === @fwrite($this->stream, $message)) {
// should never happen
throw new RuntimeException('Unable to write output.');
}
fflush($this->stream);
}
Symfony\Component\Console\Output\Output::writeln()
第二引数は整数型でオプション定数です。
戻り値はありません。
writeln() は write のラッパーでメッセージの後に改行を出力するものです。
引数をそのまま write に渡します。オプション、冗長性を解決して、受け取ったメッセージを doWrite() で出力します。
$output->writeln() の流れは理解しました。
今回はコマンドに渡されるものは、 「migrate」ですので、以下が実行されます。
$input->bind($this->getDefinition());
getDefinition() メソッドがコールされて戻り値を bind() に渡されています。 getDefinition() を見てみましょう。
Symfony\Component\Console\Application::getDefinition() | 関連メソッド
/**
* Gets the InputDefinition related to this Application.
*
* @return InputDefinition The InputDefinition instance
*/
public function getDefinition()
{
if (!$this->definition) {
$this->definition = $this->getDefaultInputDefinition();
}
if ($this->singleCommand) {
$inputDefinition = $this->definition;
$inputDefinition->setArguments();
return $inputDefinition;
}
return $this->definition;
}
/**
* Gets the default input definition.
*
* @return InputDefinition An InputDefinition instance
*/
protected function getDefaultInputDefinition()
{
return new InputDefinition([
new InputArgument('command', InputArgument::REQUIRED, 'The command to execute'),
new InputOption('--help', '-h', InputOption::VALUE_NONE, 'Display this help message'),
new InputOption('--quiet', '-q', InputOption::VALUE_NONE, 'Do not output any message'),
new InputOption('--verbose', '-v|vv|vvv', InputOption::VALUE_NONE, 'Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug'),
new InputOption('--version', '-V', InputOption::VALUE_NONE, 'Display this application version'),
new InputOption('--ansi', '', InputOption::VALUE_NONE, 'Force ANSI output'),
new InputOption('--no-ansi', '', InputOption::VALUE_NONE, 'Disable ANSI output'),
new InputOption('--no-interaction', '-n', InputOption::VALUE_NONE, 'Do not ask any interactive question'),
]);
}
Symfony\Component\Console\Application::getDefinition()
戻り値は
InputDefinition インスタンスです。
$this->definition を検証しています。
$this->definition を更新するインターフェースは setDefinition() メソッドのみです。これまでの流れで setDefinition() はコールされてませんので、getDefaultInputDefinition() で初期化が行われます。
$this->singleCommand を検証しています。
$this->singleCommand の初期値は false が設定されています。
$this->singleCommand を更新するインターフェースは setDefaultCommand() メソッドのみです。これまでの流れで setDefaultCommand() はコールされてませんので、getDefaultInputDefinition() で初期化された $this->definition が戻されます。
doRun() の続きを見ましょう。
$name = $this->getCommandName($input);
getCommandName() を 引数に $input を渡してコールしています。
Symfony\Component\Console\Application::getCommandName()
/**
* Gets the name of the command based on input.
*
* @return string|null
*/
protected function getCommandName(InputInterface $input)
{
return $this->singleCommand ? $this->defaultCommand : $input->getFirstArgument();
}
Symfony\Component\Console\Application::getCommandName()
戻り値はストリング型のコマンド名若しくは
null です。
$this->singleCommand は false なので、$input->getFirstArgument() つまり「migrate」が戻ります。
$name = $this->getCommandName($input);
if (true === $input->hasParameterOption(['--help', '-h'], true)) {
if (!$name) {
$name = 'help';
$input = new ArrayInput(['command_name' => $this->defaultCommand]);
} else {
$this->wantHelps = true;
}
}
先程のアプリケーション名及びバージョン番号取得の処理と似ています。これはヘルプコマンドが入力された処理です。ロジックはほぼ同じなので見ただけでわかりますね。
$name にコマンド名がセットされていなければデフォルト設定で初期化します。
そして、以下が TRY されます。
$this->runningCommand = null; // the command name MUST be the first element of the input $command = $this->find($name);
find()メソッドを見てみましょう。
その10に続きます。