Symfony\Component\Console\Application::find() メソッドからコマンド名が返ってきました。
TRY でコールしていましたので、例外が出た場合のロジックも書いてあります。
「定義されていません」「もしかしてこれのこと?」のような感じのメッセージをセットしています。
ざっと斜めよみしたら次に行きましょう。
$this->runningCommand に実行コマンドを代入しています。状態管理でしょうか。
次に doRunCommand() がコールされています。
$exitCode = $this->doRunCommand($command, $input, $output);
コマンドと入出力が引数として渡されてます。
見てみましょう。
Symfony\Component\Console\Application::doRunCommand()
/**
* Runs the current command.
*
* If an event dispatcher has been attached to the application,
* events are also dispatched during the life-cycle of the command.
*
* @return int 0 if everything went fine, or an error code
*/
protected function doRunCommand(Command $command, InputInterface $input, OutputInterface $output)
{
foreach ($command->getHelperSet() as $helper) {
if ($helper instanceof InputAwareInterface) {
$helper->setInput($input);
}
}
if (null === $this->dispatcher) {
return $command->run($input, $output);
}
// bind before the console.command event, so the listeners have access to input options/arguments
try {
$command->mergeApplicationDefinition();
$input->bind($command->getDefinition());
} catch (ExceptionInterface $e) {
// ignore invalid options/arguments for now, to allow the event listeners to customize the InputDefinition
}
$event = new ConsoleCommandEvent($command, $input, $output);
$e = null;
try {
$this->dispatcher->dispatch($event, ConsoleEvents::COMMAND);
if ($event->commandShouldRun()) {
$exitCode = $command->run($input, $output);
} else {
$exitCode = ConsoleCommandEvent::RETURN_CODE_DISABLED;
}
} catch (\Throwable $e) {
$event = new ConsoleErrorEvent($input, $output, $e, $command);
$this->dispatcher->dispatch($event, ConsoleEvents::ERROR);
$e = $event->getError();
if (0 === $exitCode = $event->getExitCode()) {
$e = null;
}
}
$event = new ConsoleTerminateEvent($command, $input, $output, $exitCode);
$this->dispatcher->dispatch($event, ConsoleEvents::TERMINATE);
if (null !== $e) {
throw $e;
}
return $event->getExitCode();
}
Symfony\Component\Console\Application::doRunCommand()
Command インスタンスです。第二引数は
InputInterface インスタンスで入力です。第三引数は
OutputInterface インスタンスで出力です。戻り値は コマンドが全て問題なく処理できた場合は 0 例外が起きた場合はエラーコードです。
$command->getHelperSet() を foreach で回します。おそらく今回はフォーマッタ、デバグフォーマッタ、プロセス、クエスチョンのヘルパが入っていると思います。回したインスタンスが InputAwareInterface だった場合、 setInput() に入力を渡しています。おそらく入力対応インターフェースを持ったヘルパで使うのでしょう。
$this->dispatcher を検証し null ならば $command->run() します。
$this->dispatcher は setDispatcher() で設定され、 EventDispatcherInterface インスタンスが代入されます。
イベント登録されている場合の処理が後に続きます。
今回はイベント登録されていませんのでここは通りません。
$command に入っているのは以下です。
function ($app) {
return new MigrateCommand($app['migrator']);
}
見てみましょう。
Illuminate\Database\Console\Migrations\MigrateCommand
/**
* Create a new migration command instance.
*
* @param \Illuminate\Database\Migrations\Migrator $migrator
* @return void
*/
public function __construct(Migrator $migrator)
{
parent::__construct();
$this->migrator = $migrator;
}
Illuminate\Database\Console\Migrations\BaseCommand
class BaseCommand extends Command
{
}
Illuminate\Console\Command
/**
* Create a new console command instance.
*
* @return void
*/
public function __construct()
{
// We will go ahead and set the name, description, and parameters on console
// commands just to make things a little easier on the developer. This is
// so they don't have to all be manually specified in the constructors.
if (isset($this->signature)) {
$this->configureUsingFluentDefinition();
} else {
parent::__construct($this->name);
}
// Once we have constructed the command, we'll set the description and other
// related properties of the command. If a signature wasn't used to build
// the command we'll set the arguments and the options on this command.
$this->setDescription((string) $this->description);
$this->setHelp((string) $this->help);
$this->setHidden($this->isHidden());
if (! isset($this->signature)) {
$this->specifyParameters();
}
}
/**
* Run the console command.
*
* @param \Symfony\Component\Console\Input\InputInterface $input
* @param \Symfony\Component\Console\Output\OutputInterface $output
* @return int
*/
public function run(InputInterface $input, OutputInterface $output)
{
$this->output = $this->laravel->make(
OutputStyle::class, ['input' => $input, 'output' => $output]
);
return parent::run(
$this->input = $input, $this->output
);
}
Symfony\Component\Console\Command\Command::__construct() | run() | 関連メソッド
/**
* @param string|null $name The name of the command; passing null means it must be set in configure()
*
* @throws LogicException When the command name is empty
*/
public function __construct(string $name = null)
{
$this->definition = new InputDefinition();
if (null !== $name || null !== $name = static::getDefaultName()) {
$this->setName($name);
}
$this->configure();
}
/**
* Configures the current command.
*/
protected function configure()
{
}
Symfony\Component\Console\Command\Command::run()
InputInterface インスタンスで入力です。第三引数は
OutputInterface インスタンスで出力です。戻り値は整数でステータスコードです。
MigrateCommand インスタンスは生成される時に渡される引数 $app['migrator'] を変数 $this->migrator に代入します。 スーパークラスのインストラクタをコールします。
MigrateCommand クラスは BaseCommand を継承しています。しかし、 BaseCommand にはコンストラクタがありません。BaseCommand は Command クラスを継承していますのでこのクラスのコンストラクタがコールされます。
まず、$this->signature を検証しています。この部分は前回のその10で読みましたね。署名に設定されたストリングからコマンド名とオプションの解説をパースして登録します。そして、スーパークラスの Symfony\Component\Console\Command\Command のコンストラクタがコールされ、説明文とヘルプと非表示フラグをセットします。
Symfony\Component\Console\Command\Command はクラス変数 $definition に InputDefinition インスタンスを生成し代入します。自身の configure() メソッドをコールしますが、中身は空です。
$command の中には MigrateCommand が生成されて入っているのはわかりました。
MigrateCommand::run() メソッドがコールされます。
MigrateCommand クラス自体に run() メソッドはありません。継承している BaseCommand クラスにもありません。さらに継承している Command に定義してあります。内容は以下です。
Illuminate\Console\Command::run()
/**
* Run the console command.
*
* @param \Symfony\Component\Console\Input\InputInterface $input
* @param \Symfony\Component\Console\Output\OutputInterface $output
* @return int
*/
public function run(InputInterface $input, OutputInterface $output)
{
$this->output = $this->laravel->make(
OutputStyle::class, ['input' => $input, 'output' => $output]
);
return parent::run(
$this->input = $input, $this->output
);
}
Illuminate\Console\OutputStyle::__construct()
/**
* Create a new Console OutputStyle instance.
*
* @param \Symfony\Component\Console\Input\InputInterface $input
* @param \Symfony\Component\Console\Output\OutputInterface $output
* @return void
*/
public function __construct(InputInterface $input, OutputInterface $output)
{
$this->output = $output;
parent::__construct($input, $output);
}
Illuminate\Console\Command::run()
Symfony\Component\Console\Input\InputInterface で入力です。第二引数は
Symfony\Component\Console\Output\OutputInterface で出力です。戻り値はありません。
OutputStyle としてコマンドアプリケーションに入出力を配列で登録し、$this->output に代入します。
OutputStyle は Symfony\Component\Console\Style\SymfonyStyle を継承するクラスで、出力装飾に関するクラスのようです。
スーパークラスの run() メソッドを入出力を渡してコールします。
見てみましょう。
Symfony\Component\Console\Command\Command::run()
/**
* Runs the command.
*
* The code to execute is either defined directly with the
* setCode() method or by overriding the execute() method
* in a sub-class.
*
* @return int The command exit code
*
* @throws \Exception When binding input fails. Bypass this by calling {@link ignoreValidationErrors()}.
*
* @see setCode()
* @see execute()
*/
public function run(InputInterface $input, OutputInterface $output)
{
// force the creation of the synopsis before the merge with the app definition
$this->getSynopsis(true);
$this->getSynopsis(false);
// add the application arguments and options
$this->mergeApplicationDefinition();
// bind the input against the command specific arguments/options
try {
$input->bind($this->definition);
} catch (ExceptionInterface $e) {
if (!$this->ignoreValidationErrors) {
throw $e;
}
}
$this->initialize($input, $output);
if (null !== $this->processTitle) {
if (\function_exists('cli_set_process_title')) {
if (!@cli_set_process_title($this->processTitle)) {
if ('Darwin' === PHP_OS) {
$output->writeln('Running "cli_set_process_title" as an unprivileged user is not supported on MacOS. ', OutputInterface::VERBOSITY_VERY_VERBOSE);
} else {
cli_set_process_title($this->processTitle);
}
}
} elseif (\function_exists('setproctitle')) {
setproctitle($this->processTitle);
} elseif (OutputInterface::VERBOSITY_VERY_VERBOSE === $output->getVerbosity()) {
$output->writeln('Install the proctitle PECL to be able to change the process title. ');
}
}
if ($input->isInteractive()) {
$this->interact($input, $output);
}
// The command name argument is often omitted when a command is executed directly with its run() method.
// It would fail the validation if we didn't make sure the command argument is present,
// since it's required by the application.
if ($input->hasArgument('command') && null === $input->getArgument('command')) {
$input->setArgument('command', $this->getName());
}
$input->validate();
if ($this->code) {
$statusCode = ($this->code)($input, $output);
} else {
$statusCode = $this->execute($input, $output);
if (!\is_int($statusCode)) {
throw new \TypeError(sprintf('Return value of "%s::execute()" must be of the type int, %s returned.', static::class, \gettype($statusCode)));
}
}
return is_numeric($statusCode) ? (int) $statusCode : 0;
}
/**
* Returns the synopsis for the command.
*
* @param bool $short Whether to show the short version of the synopsis (with options folded) or not
*
* @return string The synopsis
*/
public function getSynopsis(bool $short = false)
{
$key = $short ? 'short' : 'long';
if (!isset($this->synopsis[$key])) {
$this->synopsis[$key] = trim(sprintf('%s %s', $this->name, $this->definition->getSynopsis($short)));
}
return $this->synopsis[$key];
}
Symfony\Component\Console\Command\Command::run()
Symfony\Component\Console\Input\InputInterface で入力です。第二引数は
Symfony\Component\Console\Output\OutputInterface で出力です。戻り値は整数です。
getSynopsis() を引数に true を入れてコールします。
getSynopsis() を引数に false を入れてコールします。
true と false を1回ずつ叩くことで概要の略と詳細を確実に生成しておきます。
mergeApplicationDefinition() メソッドが叩かれています。
見てみましょう。
Symfony\Component\Console\Command\Command::mergeApplicationDefinition()
/**
* Merges the application definition with the command definition.
*
* This method is not part of public API and should not be used directly.
*
* @param bool $mergeArgs Whether to merge or not the Application definition arguments to Command definition arguments
*/
public function mergeApplicationDefinition(bool $mergeArgs = true)
{
if (null === $this->application || (true === $this->applicationDefinitionMerged && ($this->applicationDefinitionMergedWithArgs || !$mergeArgs))) {
return;
}
$this->definition->addOptions($this->application->getDefinition()->getOptions());
$this->applicationDefinitionMerged = true;
if ($mergeArgs) {
$currentArguments = $this->definition->getArguments();
$this->definition->setArguments($this->application->getDefinition()->getArguments());
$this->definition->addArguments($currentArguments);
$this->applicationDefinitionMergedWithArgs = true;
}
}
Symfony\Component\Console\Command\Command::mergeApplicationDefinition()
戻り値はありません。
コマンドアプリケーションが null 若しくはマージ済み、若しくはマージするかどうかの引数が false の場合、そのまま戻します。
そうでない場合はコマンド定義にアプリケーション定義と引数をマージし、マージ済みフラグを true に設定します。
次は以下が実行されます。
$input->bind($this->definition);
見てみましょう。
Symfony\Component\Console\Input\bind()
/**
* Binds the current Input instance with the given arguments and options.
*
* @throws RuntimeException
*/
public function bind(InputDefinition $definition)
{
$this->arguments = [];
$this->options = [];
$this->definition = $definition;
$this->parse();
}
Symfony\Component\Console\ArgvInput\parse()
/**
* Processes command line arguments.
*/
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\ArgvInput\parse()
InputDefinition インスタンスです。戻り値はありません。
引数とオプションを空の配列で初期化して、 $this->definition に引数として渡された定義を代入します。
今回の場合は $command->getDefinition() で取得したコマンド定義が入ります。
その後 parse() で解析し引数、オプションを登録していきます。
次に以下が実行されています。
$this->initialize($input, $output);
Symfony\Component\Console\Command\Command::initialize() メソッドは中身が空になっています。オーバーライドされてもいないようです。初期化が必要なコマンドを作成する場合はここを拡張して利用するのでしょう。
次に $this->processTitle が null かどうか検証しています。
$this->processTitle を設定できるインターフェイスは setProcessTitle() でそのコードドキュメントには 「この機能は、長いプロセスコマンドを作成する場合にのみ使用してください。」とあります。デーモンのようなコマンドを作成する時にプロセス名をセットするためのもののようです。今回は null なのでここは通りません。ざっとみるとプロセス名を設定出来なかった場合の例外処理やメッセージなどのロジックのようです。次を見ましょう。isInteractive() メソッドがコールされています。
if ($input->isInteractive()) {
$this->interact($input, $output);
}
Symfony\Component\Console\Input\Input::interact()
/**
* Interacts with the user.
*
* This method is executed before the InputDefinition is validated.
* This means that this is the only place where the command can
* interactively ask for values of missing required arguments.
*/
protected function interact(InputInterface $input, OutputInterface $output)
{
}
Symfony\Component\Console\Input\Input::isInteractive()
protected $interactive = true;
/**
* Is this input means interactive?
*
* @return bool
*/
public function isInteractive()
{
return $this->interactive;
}
/**
* {@inheritdoc}
*/
public function setInteractive(bool $interactive)
{
$this->interactive = $interactive;
}
isInteractive() メソッドは $this->interactive をそのまま戻しています。 $this->interactive はインスタンス生成時に true に設定されています。変更するインターフェースは setInteractive() メソッドです。このメソッドをコールしているところは特に見当たりません。Input::interact() メソッドのコードドキュメントに書いてある内容から推測すると、実行される前に会話式のインターフェースで実行内容を変更するタイプのコマンドを作成したい時に interact() メソッドをオーバーライドして使うのかもしれません。
// The command name argument is often omitted when a command is executed directly with its run() method.
// It would fail the validation if we didn't make sure the command argument is present,
// since it's required by the application.
if ($input->hasArgument('command') && null === $input->getArgument('command')) {
$input->setArgument('command', $this->getName());
}
コマンド定義に引数「’command’」があり、コマンドラインから受け取った引数「’command’」がなかった場合、コマンド名を設定しています。
コードドキュメントによると、コマンドがrun()メソッドで直接実行される場合、コマンド名の引数が省略されることがあるため、だそうです。
今回はここは通らなそうです。
$input->validate();
Symfony\Component\Console\Input\Input::validate()
/**
* Validates the input.
*
* @throws RuntimeException When not enough arguments are given
*/
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)));
}
}
コマンドラインから受け取った引数とコマンド定義を照らし合わせ足りない引数があった場合は 「sprintf('Not enough arguments (missing: "%s").', implode(', ', $missingArguments))」 とメッセージを添えて例外 RuntimeException を投げています。引数チェックですね。
続きです。
Symfony\Component\Console\Command\Command::run() 抜粋
if ($this->code) {
$statusCode = ($this->code)($input, $output);
} else {
$statusCode = $this->execute($input, $output);
if (!\is_int($statusCode)) {
throw new \TypeError(sprintf('Return value of "%s::execute()" must be of the type int, %s returned.', static::class, \gettype($statusCode)));
}
}
return is_numeric($statusCode) ? (int) $statusCode : 0;
Symfony\Component\Console\Command\Command::execute() | setCode()
/**
* Executes the current command.
*
* This method is not abstract because you can use this class
* as a concrete class. In this case, instead of defining the
* execute() method, you set the code to execute by passing
* a Closure to the setCode() method.
*
* @return int 0 if everything went fine, or an exit code
*
* @throws LogicException When this abstract method is not implemented
*
* @see setCode()
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
throw new LogicException('You must override the execute() method in the concrete command class.');
}
/**
* Sets the code to execute when running this command.
*
* If this method is used, it overrides the code defined
* in the execute() method.
*
* @param callable $code A callable(InputInterface $input, OutputInterface $output)
*
* @return $this
*
* @throws InvalidArgumentException
*
* @see execute()
*/
public function setCode(callable $code)
{
if ($code instanceof \Closure) {
$r = new \ReflectionFunction($code);
if (null === $r->getClosureThis()) {
$code = \Closure::bind($code, $this);
}
}
$this->code = $code;
return $this;
}
$this->code が設定されている場合は、それを、されていない場合は、$this->execute() をコールし、戻り値を $statusCode に代入します。 $statusCode が整数型でなかった場合、「sprintf('Return value of "%s::execute()" must be of the type int, %s returned.', static::class, \gettype($statusCode))」のメッセージを添えて例外 TypeError をスローします。
$this->code は setCode() で設定できます。
Symfony\Component\Console\Command\Command を継承し、execute() メソッドをオーバーライドしてコマンド処理を記述する方法の他に、Symfony\Component\Console\Command\Command 自体に setCode() にクロージャーを渡す方法もあるそうです。今回は Illuminate/Console/Command で execute() をオーバーライドしています。
Illuminate/Console/Command::execute() は次の回で読みます。
$statusCode が整数ならばそれを、そうでなければ 0 を戻します。
その12に続きます。