Symfony2ソースコードリーディング (2) app/consoleを読み解く その2

[Symfony2ソースコードリーディング (1) app/consoleを読み解く](http://qiita.com/DQNEO/items/87ea5645acf6646c0bfa) の続きです。

`app/console`の中身を見ていきます。

`app/console`の実体が [Symfony\Component\Console\Application](https://github.com/symfony/symfony/blob/2.6/src/Symfony/Component/Console/Application.php#L113) であることは前回説明しました。

## 入り口はdoRun()メソッド


```php:Symfony/Component/Console/Application.php
    public function doRun(InputInterface $input, OutputInterface $output)
    {
        if (true === $input->hasParameterOption(array('--version', '-V'))) {
            $output->writeln($this->getLongVersion());
            return 0;
        }
        $name = $this->getCommandName($input);
        if (true === $input->hasParameterOption(array('--help', '-h'))) {
            if (!$name) {
                $name = 'help';
                $input = new ArrayInput(array('command' => 'help'));
            } else {
                $this->wantHelps = true;
            }
        }
        if (!$name) {
            $name = $this->defaultCommand;
            $input = new ArrayInput(array('command' => $this->defaultCommand));
        }
        // the command name MUST be the first element of the input
        $command = $this->find($name);
        $this->runningCommand = $command;
        $exitCode = $this->doRunCommand($command, $input, $output);
        $this->runningCommand = null;
        return $exitCode;
    }
```

上から順に見ていきましょう。
まず最初のif文は、引数が`--version`または`-V`だったらバージョン番号を表示して終了、のように読めます。

デバグ用コードを仕込んで確かめてみましょう。

```diff
        if (true === $input->hasParameterOption(array('--version', '-V'))) {
            $output->writeln($this->getLongVersion());
+            die("( ˘ω˘)" . __METHOD__);
            return 0;
        }
````

実行してみます。

```
$ blog  ./app/console --version
Symfony version 2.6.7 - app/dev/debug
( ˘ω˘) Symfony\Component\Console\Application::doRun

$ blog  ./app/console -V
Symfony version 2.6.7 - app/dev/debug
( ˘ω˘) Symfony\Component\Console\Application::doRun
```

確かに、バージョンを表示して終了しました。

次に、

```php
        $name = $this->getCommandName($input);
```

これはなんでしょうか。

getCommandNameメソッドの定義は同じファイル内にあります。

```php
    protected function getCommandName(InputInterface $input)
    {
        return $input->getFirstArgument();
    }
```

一つ目の引数を取得してるようです。つまりサブコマンド名を取得してると思われます。
これを確かめます。

```diff
        $name = $this->getCommandName($input);
+       var_dump($name);exit;
```

```
$  blog  ./app/console help
string(4) "help"

$  blog  ./app/console server:run --help
string(10) "server:run"

$  blog  ./app/console
NULL
```

ビンゴ！$nameにはサブコマンドが入るようです。

つぎ。

```php
        if (true === $input->hasParameterOption(array('--help', '-h'))) {
            if (!$name) {
                $name = 'help';
                $input = new ArrayInput(array('command' => 'help'));
            } else {
                $this->wantHelps = true;
            }
        }
```

これは、`--help`または`-h`オプションをつけた時の分岐です。

* `app/console server:run --help`の場合はサブコマンド`server:run`のヘルプを表示する
* `app/console --help`場合は`app/console help`と解釈しなおして`app/console`自体のヘルプを表示する

となっています。

(余談ですが、関数型プログラミングではImmutableとか言って再代入を避けるのがよいとされていますが、ここでは躊躇なく$inputに再代入しています。まあPHPだから別に気にすることないのかも)

つぎ。

```php
       if (!$name) {
            $name = $this->defaultCommand;
            $input = new ArrayInput(array('command' => $this->defaultCommand));
        }
```

$nameが未定義の場合にデフォルトコマンドを割り当てています。

デフォルトコマンドとは何か？
defaultCommandがどこで定義されてるのか探してみたらコンストラクタにありました。

```php
    public function __construct($name = 'UNKNOWN', $version = 'UNKNOWN')
    {
        $this->name = $name;
        $this->version = $version;
        $this->defaultCommand = 'list';
        $this->helperSet = $this->getDefaultHelperSet();
        $this->definition = $this->getDefaultInputDefinition();

        foreach ($this->getDefaultCommands() as $command) {
            $this->add($command);
        }
    }
```

`list`という値が設定されています。
つまり、
`app/console`は`app/console list`と等価になります。

これどこかで見たことのある挙動だぞー？
と思っていたら、Composerがそうでした。
`composer`と`composer list`は同じ挙動でした。最近の流行りなんでしょうか・・・？

つぎ。

```php
        $command = $this->find($name);
```

デバグしてみましょう

```diff
        $command = $this->find($name);
+       var_dump(get_class($command));exit;
```

```
$  blog  ./app/console list
string(45) "Symfony\Component\Console\Command\ListCommand"

$  blog  ./app/console help
string(45) "Symfony\Component\Console\Command\HelpCommand"

$  blog  ./app/console yaml:lint
string(54) "Symfony\Bundle\FrameworkBundle\Command\YamlLintCommand"

$ blog  ./app/console server:run
string(55) "Symfony\Bundle\FrameworkBundle\Command\ServerRunCommand"

$  blog  ./app/console acme:hello
string(41) "Acme\DemoBundle\Command\HelloWorldCommand"
```

サブコマンドの名前に応じて、コマンドクラスのインスタンスが返ってきています。

注目すべきは、サブコマンドの名前空間がばらばらな点です。
これはどういう探索ロジックになってるんでしょう？探索先の優先順位とかがあるのかな？(その調査は後回しにします)

サブコマンドの文字列`server:run`をオブジェクト`Symfony\Bundle\FrameworkBundle\Command\ServerRunCommand`に変換する処理はfind()メソッドにありました。

```php
    /**
     * 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.
     *
     * @param string $name A command name or a command alias
     *
     * @return Command A Command instance
     *
     * @throws \InvalidArgumentException When command name is incorrect or ambiguous
     *
     * @api
     */
    public function find($name)
    {

        $allCommands = array_keys($this->commands);
        $expr = preg_replace_callback('{([^:]+|)}', function ($matches) { return preg_quote($matches[1]).'[^:]*'; }, $name);
        $commands = preg_grep('{^'.$expr.'}', $allCommands);
```

$this->commandsの中身を調べてみると、

```
array(
    'help' => $helpCommand,
    'server:run' => $serverRunCommand,
    ...
)
```
のようなkey/value構造になっており、valueの部分には各種サブコマンドクラスのインスタンスが保持されています。
サブコマンドが全部で61個(!)もありました。すごい。
あらかじめ61個のサブコマンドを全てnewして配列上に保持した上で、入力されたサブコマンド名にマッチするものを返します。
なんとも富豪的アプローチですw。
まあ`app/console`はそんなに頻繁に起動するものでもないからパフォーマンスを気にする必要はないのでしょう。

## サブコマンドの実体を見てみる

さてサブコマンド`help`の実体は、上でのべたように 
[Symfony\Component\Console\Command\HelpCommand](https://github.com/symfony/symfony/blob/2.6/src/Symfony/Component/Console/Command/HelpCommand.php) です。

中身はこうなっておりました。

```php
/**
 * HelpCommand displays the help for a given command.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class HelpCommand extends Command
{
    private $command;

    /**
     * {@inheritdoc}
     */
    protected function configure()
    {
        $this->ignoreValidationErrors();

        $this
            ->setName('help')
            ->setDefinition(array(
                new InputArgument('command_name', InputArgument::OPTIONAL, 'The command name', 'help'),
                new InputOption('xml', null, InputOption::VALUE_NONE, 'To output help as XML'),
                new InputOption('format', null, InputOption::VALUE_REQUIRED, 'To output help in other formats', 'txt'),
                new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw command help'),
            ))
            ->setDescription('Displays help for a command')
            ->setHelp(<<<EOF
The <info>%command.name%</info> command displays help for a given command:

  <info>php %command.full_name% list</info>

You can also output the help in other formats by using the <comment>--format</comment> option:

  <info>php %command.full_name% --format=xml list</info>

To display the list of available commands, please use the <info>list</info> command.
EOF
            )
        ;
    }
```

ふむふむ。これが`help`サブコマンドの設定ですね。

実行部分はexecute()メソッドで、

```php
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        if (null === $this->command) {
            $this->command = $this->getApplication()->find($input->getArgument('command_name'));
        }

        if ($input->getOption('xml')) {
            $input->setOption('format', 'xml');
        }

        $helper = new DescriptorHelper();
        $helper->describe($output, $this->command, array(
            'format' => $input->getOption('format'),
            'raw_text' => $input->getOption('raw'),
        ));

        $this->command = null;
    }
```

とくに実体は何もなく、説明文を表示して終了という感じです。

次回は、サブコマンド`server:run`の実体に迫ります。
つづく