Console\Application::doRun() の処理の流れで find() メソッドがコールされているところまで読みました。では、find() を見てみましょう。
Symfony\Component\Console\Application::find() | 関連メソッド
/**
* Finds a command by name or alias.
*
* Contrary to get, this command tries to find the best
* match if you give it an abbreviation of a name or alias.
*
* @return Command A Command instance
*
* @throws CommandNotFoundException When command name is incorrect or ambiguous
*/
public function find(string $name)
{
$this->init();
$aliases = [];
foreach ($this->commands as $command) {
foreach ($command->getAliases() as $alias) {
if (!$this->has($alias)) {
$this->commands[$alias] = $command;
}
}
}
if ($this->has($name)) {
return $this->get($name);
}
$allCommands = $this->commandLoader ? array_merge($this->commandLoader->getNames(), array_keys($this->commands)) : array_keys($this->commands);
$expr = preg_replace_callback('{([^:]+|)}', function ($matches) { return preg_quote($matches[1]).'[^:]*'; }, $name);
$commands = preg_grep('{^'.$expr.'}', $allCommands);
if (empty($commands)) {
$commands = preg_grep('{^'.$expr.'}i', $allCommands);
}
// if no commands matched or we just matched namespaces
if (empty($commands) || \count(preg_grep('{^'.$expr.'$}i', $commands)) findNamespace(substr($name, 0, $pos));
}
$message = sprintf('Command "%s" is not defined.', $name);
if ($alternatives = $this->findAlternatives($name, $allCommands)) {
// remove hidden commands
$alternatives = array_filter($alternatives, function ($name) {
return !$this->get($name)->isHidden();
});
if (1 == \count($alternatives)) {
$message .= "\n\nDid you mean this?\n ";
} else {
$message .= "\n\nDid you mean one of these?\n ";
}
$message .= implode("\n ", $alternatives);
}
throw new CommandNotFoundException($message, array_values($alternatives));
}
// filter out aliases for commands which are already on the list
if (\count($commands) > 1) {
$commandList = $this->commandLoader ? array_merge(array_flip($this->commandLoader->getNames()), $this->commands) : $this->commands;
$commands = array_unique(array_filter($commands, function ($nameOrAlias) use (&$commandList, $commands, &$aliases) {
if (!$commandList[$nameOrAlias] instanceof Command) {
$commandList[$nameOrAlias] = $this->commandLoader->get($nameOrAlias);
}
$commandName = $commandList[$nameOrAlias]->getName();
$aliases[$nameOrAlias] = $commandName;
return $commandName === $nameOrAlias || !\in_array($commandName, $commands);
}));
}
if (\count($commands) > 1) {
$usableWidth = $this->terminal->getWidth() - 10;
$abbrevs = array_values($commands);
$maxLen = 0;
foreach ($abbrevs as $abbrev) {
$maxLen = max(Helper::strlen($abbrev), $maxLen);
}
$abbrevs = array_map(function ($cmd) use ($commandList, $usableWidth, $maxLen, &$commands) {
if ($commandList[$cmd]->isHidden()) {
unset($commands[array_search($cmd, $commands)]);
return false;
}
$abbrev = str_pad($cmd, $maxLen, ' ').' '.$commandList[$cmd]->getDescription();
return Helper::strlen($abbrev) > $usableWidth ? Helper::substr($abbrev, 0, $usableWidth - 3).'...' : $abbrev;
}, array_values($commands));
if (\count($commands) > 1) {
$suggestions = $this->getAbbreviationSuggestions(array_filter($abbrevs));
throw new CommandNotFoundException(sprintf("Command \"%s\" is ambiguous.\nDid you mean one of these?\n%s", $name, $suggestions), array_values($commands));
}
}
$command = $this->get(reset($commands));
if ($command->isHidden()) {
throw new CommandNotFoundException(sprintf('The command "%s" does not exist.', $name));
}
return $command;
}
private function init()
{
if ($this->initialized) {
return;
}
$this->initialized = true;
foreach ($this->getDefaultCommands() as $command) {
$this->add($command);
}
}
/**
* Adds a command object.
*
* If a command with the same name already exists, it will be overridden.
* If the command is not enabled it will not be added.
*
* @return Command|null The registered command if enabled or null
*/
public function add(Command $command)
{
$this->init();
$command->setApplication($this);
if (!$command->isEnabled()) {
$command->setApplication(null);
return null;
}
// Will throw if the command is not correctly initialized.
$command->getDefinition();
if (!$command->getName()) {
throw new LogicException(sprintf('The command defined in "%s" cannot have an empty name.', \get_class($command)));
}
$this->commands[$command->getName()] = $command;
foreach ($command->getAliases() as $alias) {
$this->commands[$alias] = $command;
}
return $command;
}
Symfony\Component\Console\Application::find()
InputInterface で入力です。第二引数は
OutputInterface で出力です戻り値はコマンドが全て正常に実行された場合は 0 そうでない場合はエラーコードです。
まず、init() メソッドがコールされます。
init() メソッドでは、$this->initialized が検証され true であればそのまま戻されます。
これまでの流れで $this->initialized にtrue を代入した箇所はありませんでした。そのまま飛ばします。
次に $this->initialized に true を代入します。初期化フラグですね。
foreach で $this->getDefaultCommands() を回し、中身を add() メソッドに渡しています。
$this->getDefaultCommands() の戻り値は以下です。
return [new HelpCommand(), new ListCommand()];
つまり、ヘルプと一覧コマンドが登録されるわけですね。add() メソッドの中でも $this->init() メソッドがコールされています。一度 init() メソッドがコールされれば $this->initialized は true になるのでそのまま戻されるのですが、少し冗長な印象です。どこからコールされてもヘルプと一覧だけは登録したいということなのでしょう。もう少し違う設計もありそうに思いますが、Symfonyの思想的なものなのでしょうか。
次に $command->setApplication() を引数に自身を渡してコールしています。
見てみましょう。
Symfony\Component\Console\Command\Command::setApplication()
public function setApplication(Application $application = null)
{
$this->application = $application;
if ($application) {
$this->setHelperSet($application->getHelperSet());
} else {
$this->helperSet = null;
}
}
public function setHelperSet(HelperSet $helperSet)
{
$this->helperSet = $helperSet;
}
Symfony\Component\Console\Application::getHelperSet() | 関連メソッド
/**
* Get the helper set associated with the command.
*
* @return HelperSet The HelperSet instance associated with this command
*/
public function getHelperSet()
{
if (!$this->helperSet) {
$this->helperSet = $this->getDefaultHelperSet();
}
return $this->helperSet;
}
/**
* Gets the default helper set with the helpers that should always be available.
*
* @return HelperSet A HelperSet instance
*/
protected function getDefaultHelperSet()
{
return new HelperSet([
new FormatterHelper(),
new DebugFormatterHelper(),
new ProcessHelper(),
new QuestionHelper(),
]);
}
Command インスタンスの変数にアプリケーションコンテナを代入し、setHelperSet() メソッドをコールしています。引数として $application->getHelperSet() メソッドをコールした戻り値を渡しています。 setHelperSet() は受け取った引数をそのまま $this->helperSet に代入するだけです。
getHelperSet() は $this->helperSet つまりコマンドアプリケーションコンテナの helperSet がセットされていなければ、 getDefaultHelperSet() をコールし戻り値をそれに代入してさらにその値を戻します。つまり Command インスタンスにも同じものが共有されます。内容は、フォーマッタ、デバグフォーマッタ、プロセス、クエスチョンのヘルパです。
ちなみに、ヘルパは setHelperSet() で変更することができます。
次に $command->getDefinition() メソッドをコールしています。
Symfony\Component\Console\Command\Command::getDefinition()
/**
* Gets the InputDefinition attached to this Command.
*
* @return InputDefinition An InputDefinition instance
*/
public function getDefinition()
{
if (null === $this->definition) {
throw new LogicException(sprintf('Command class "%s" is not correctly initialized. You probably forgot to call the parent constructor.', static::class));
}
return $this->definition;
}
$this->definition が null だった場合「Command class “%s” is not correctly initialized. You probably forgot to call the parent constructor.」というメッセージを添えて例外 LogicException をスローします。$this->definition は Command インスタンスが生成される時にコンストラクタで InputDefinition インスタンスを生成したものが代入されるはずですので、ここで例外がスローされるケースがちょっとわかりません。思想でしょうか。問題なければそのまま this->definition を戻します。コール元は引数を受け取っていません。つまり初期化が済んでいなかったら例外をスローするという処理です。
次に $command->getName() を検証しています。これは 各コマンドが Command クラスを継承して configure() メソッドで設定を構築する時に setName() メソッドで設定するコマンド名です。ない場合は「The command defined in “%s” cannot have an empty name.」というメッセージを添えて例外 LogicException をスローします。
コマンド名があれば $this->commands[] 配列にコマンド名をキーに Command インスタンスを追加します。
コマンドにコマンドエイリアスが登録されていた場合、コマンドアプリケーションにもエイリアス登録します。
そのまま Command インスタンスを戻します。
コール元の init() の foreach では戻り値を受け取っていません。 init の処理完了です。
その8で、Symfony\Component\Console\Application::add() までたどり着いたところで一旦引き返しました。
その時コールされた add() の挙動は先程理解しました。
$this->commands[$alias] = $command;の処理でコマンド群が登録されていますので、ここからコマンドラインから指示されたコマンドを検索して実行すると推測されます。
その8で
ArtisanServiceProvider インスタンスの $commands と $devCommands を array_merge したものを登録していました。ただ、この中には migrate コマンドらしきものはありません。あてが外れたようです。Illuminate\Foundation\Providers\ConsoleSupportServiceProvider の $providers には以下の記述がされていました。
protected $providers = [
ArtisanServiceProvider::class,
MigrationServiceProvider::class,
ComposerServiceProvider::class,
];
そうです。ArtisanServiceProvider ではなく、 MigrationServiceProvider を読むべきでした。読んでみましょう。
Illuminate\Database\MigrationServiceProvider 抜粋
/**
* The commands to be registered.
*
* @var array
*/
protected $commands = [
'Migrate' => 'command.migrate',
'MigrateFresh' => 'command.migrate.fresh',
'MigrateInstall' => 'command.migrate.install',
'MigrateRefresh' => 'command.migrate.refresh',
'MigrateReset' => 'command.migrate.reset',
'MigrateRollback' => 'command.migrate.rollback',
'MigrateStatus' => 'command.migrate.status',
'MigrateMake' => 'command.migrate.make',
];
/**
* Register the service provider.
*
* @return void
*/
public function register()
{
$this->registerRepository();
$this->registerMigrator();
$this->registerCreator();
$this->registerCommands($this->commands);
}
/**
* Register the migration repository service.
*
* @return void
*/
protected function registerRepository()
{
$this->app->singleton('migration.repository', function ($app) {
$table = $app['config']['database.migrations'];
return new DatabaseMigrationRepository($app['db'], $table);
});
}
/**
* Register the migrator service.
*
* @return void
*/
protected function registerMigrator()
{
// The migrator is responsible for actually running and rollback the migration
// files in the application. We'll pass in our database connection resolver
// so the migrator can resolve any of these connections when it needs to.
$this->app->singleton('migrator', function ($app) {
$repository = $app['migration.repository'];
return new Migrator($repository, $app['db'], $app['files'], $app['events']);
});
}
/**
* Register the migration creator.
*
* @return void
*/
protected function registerCreator()
{
$this->app->singleton('migration.creator', function ($app) {
return new MigrationCreator($app['files'], $app->basePath('stubs'));
});
}
/**
* Register the given commands.
*
* @param array $commands
* @return void
*/
protected function registerCommands(array $commands)
{
foreach (array_keys($commands) as $command) {
call_user_func_array([$this, "register{$command}Command"], []);
}
$this->commands(array_values($commands));
}
/**
* Register the command.
*
* @return void
*/
protected function registerMigrateCommand()
{
$this->app->singleton('command.migrate', function ($app) {
return new MigrateCommand($app['migrator']);
});
}
やはり、そのものズバリの 'Migrate' => 'command.migrate' がありました。
構造は以前読んだ ArtisanServiceProvider とほぼ同じですね。違う点は migrator コマンドを登録する際に database.migrations を migration.repository として Migrator インスタンスに渡しているところ、さらに migration.creator として MigrationCreator インスタンスをアプリケーションコンテナに登録したファイルシステムを引数に渡して生成しています。おそらくコマンドラインからマイグレーションファイルを生成する時にファイルストリームを使うのでしょう。
これで、$this->commands['migrate'] に MigrageCommand を生成するクロージャーが入っているのが推測されます。
一つ疑問があります。Symfony\Component\Console\Application::add() の中で以下の処理によって $this->commands[] にコマンドを登録していると思います。
$this->commands[$command->getName()] = $command;
この $command->getName() で取得するコマンド名が migrate の場合どこで定義しているのでしょうか。
Illuminate\Database\Console\Migrations\MigrateCommand クラスの定義を見てみましょう。
Illuminate\Database\Console\Migrations\MigrateCommand 抜粋
use ConfirmableTrait;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'migrate {--database= : The database connection to use}
{--force : Force the operation to run when in production}
{--path=* : The path(s) to the migrations files to be executed}
{--realpath : Indicate any provided migration file paths are pre-resolved absolute paths}
{--pretend : Dump the SQL queries that would be run}
{--seed : Indicates if the seed task should be re-run}
{--step : Force the migrations to be run so they can be rolled back individually}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Run the database migrations';
/**
* The migrator instance.
*
* @var \Illuminate\Database\Migrations\Migrator
*/
protected $migrator;
/**
* Create a new migration command instance.
*
* @param \Illuminate\Database\Migrations\Migrator $migrator
* @return void
*/
public function __construct(Migrator $migrator)
{
parent::__construct();
$this->migrator = $migrator;
}
registerMigrateCommand() メソッドで MigrateCommand インスタンスを生成しています。MigrateCommand クラスは $signature 変数にコマンド名と署名が定義してあります。
MigrateCommand クラスのスーパークラスのスーパークラス、Illuminate\Console\Command のコンストラクタに以下のようなロジックが入っています。
Illuminate\Console\Command::__construct()
/**
* 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();
}
}
/**
* Configure the console command using a fluent definition.
*
* @return void
*/
protected function configureUsingFluentDefinition()
{
[$name, $arguments, $options] = Parser::parse($this->signature);
parent::__construct($this->name = $name);
// After parsing the signature we will spin through the arguments and options
// and set them on this command. These will already be changed into proper
// instances of these "InputArgument" and "InputOption" Symfony classes.
$this->getDefinition()->addArguments($arguments);
$this->getDefinition()->addOptions($options);
}
$this->signature を検証し設定されていた場合、configureUsingFluentDefinition() メソッドがコールされます。configureUsingFluentDefinition() メソッドは Parser::parse($this->signature) の静的メソッドをコールして戻り値を $this->name に代入しています。
Parser::parse() を見てみましょう。
Illuminate\Console\Parser::parse()
/**
* Parse the given console command definition into an array.
*
* @param string $expression
* @return array
*
* @throws \InvalidArgumentException
*/
public static function parse($expression)
{
$name = static::name($expression);
if (preg_match_all('/\{\s*(.*?)\s*\}/', $expression, $matches)) {
if (count($matches[1])) {
return array_merge([$name], static::parameters($matches[1]));
}
}
return [$name, [], []];
}
/**
* Extract the name of the command from the expression.
*
* @param string $expression
* @return string
*
* @throws \InvalidArgumentException
*/
protected static function name($expression)
{
if (! preg_match('/[^\s]+/', $expression, $matches)) {
throw new InvalidArgumentException('Unable to determine command name from signature.');
}
return $matches[0];
}
Illuminate\Console\Parser::parse()
戻り値は配列型でコマンド名、パラメーターです。
受け取った引数のはじめからスペースまでの文字列をコマンド名に、パラメーターの説明として「{}」で囲まれた文字列を全て取得して配列型として戻されます。
これで $this->commands['migrate'] に MigrageCommand を生成するクロージャーが入っていることがはっきり確認できました。スッキリしましたね。
続きを読みましょう。
find() の続きにもどります。
$this->commands[] に登録されているコマンドに登録されているエイリアスで、$this->commands[] に登録されていないものを登録します。
has() メソッドで実行したいコマンドが登録されているか確認します。
Symfony\Component\Console\Application::has()
/**
* Returns true if the command exists, false otherwise.
*
* @return bool true if the command exists, false otherwise
*/
public function has(string $name)
{
$this->init();
return isset($this->commands[$name]) || ($this->commandLoader && $this->commandLoader->has($name) && $this->add($this->commandLoader->get($name)));
}
Symfony\Component\Console\Application::has()
戻り値はブーリアンでコマンド名の登録確認の結果です。
init() メソッドの後に $this->commands[$name] がセットされているか確認しています。今回はもちろん true が戻されます。
その後に書いてある commandLoader はなんでしょうか。コマンドアプリケーションの $commandLoader に代入されている CommandLoaderInterface インターフェースを実装したクラスのようです。
Symfony\Component\Console\CommandLoader\CommandLoaderInterface
interface CommandLoaderInterface
{
/**
* Loads a command.
*
* @return Command
*
* @throws CommandNotFoundException
*/
public function get(string $name);
/**
* Checks if a command exists.
*
* @return bool
*/
public function has(string $name);
/**
* @return string[] All registered command names
*/
public function getNames();
}
get() と has() と getNames() のメソッドが定義されているのみです。コードドキュメントも特にありません。推測ですが、この作りだとコマンドの遅延ロードをしたい時に使うのかもしれません。本筋に戻りましょう。
$this->has() の返り値は true ですので、 $this->get() がコールされます。
見てみましょう。
Symfony\Component\Console\Application::get()
/**
* Returns a registered command by name or alias.
*
* @return Command A Command object
*
* @throws CommandNotFoundException When given command name does not exist
*/
public function get(string $name)
{
$this->init();
if (!$this->has($name)) {
throw new CommandNotFoundException(sprintf('The command "%s" does not exist.', $name));
}
$command = $this->commands[$name];
if ($this->wantHelps) {
$this->wantHelps = false;
$helpCommand = $this->get('help');
$helpCommand->setCommand($command);
return $helpCommand;
}
return $command;
}
Symfony\Component\Console\Application::get()
戻り値は
Command インスタンスです。
まず初期化をします。そこら中に書いてありますね。そういう思想なのでしょう。
もし、渡されたコマンド名が登録されていなかった場合、「sprintf('The command "%s" does not exist.', $name)」とメッセージを添えて例外 CommandNotFoundException をスローします。
登録されていた場合、そのコマンドを取得して戻します。
ヘルプ表示フラグが立っていた場合はヘルプコマンドを優先して返します。
コマンドを受け取った find() メソッドはそれをそのまま返します。
今回のケースでは find() メソッドはここまで読めば良いのですが、
そのあとを少し覗いてみましょう。
いろいろ判定していますが、
「そんなコマンドはないけどもしかしてこれの事かい?」というようなメッセージをセットしていたりします。
コマンドが見つからなかった時の救済処置や例外などを色々書いているのでしょう。今回はこれ以上深追いしないことにします。
find() メソッドの中身はほぼ理解しました。続きを読みましょう。
その11に続く。