LoginSignup
6
7

More than 5 years have passed since last update.

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

Last updated at Posted at 2015-05-16

Symfony2ソースコードリーディング (1) app/consoleを読み解く の続きです。

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

app/consoleの実体が Symfony\Component\Console\Application であることは前回説明しました。

入り口はdoRun()メソッド

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だったらバージョン番号を表示して終了、のように読めます。

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

        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

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

次に、

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

これはなんでしょうか。

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

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

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

        $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にはサブコマンドが入るようです。

つぎ。

        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だから別に気にすることないのかも)

つぎ。

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

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

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

    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/consoleapp/console listと等価になります。

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

つぎ。

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

デバグしてみましょう

        $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()メソッドにありました。

    /**
     * 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 です。

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

/**
 * 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()メソッドで、

    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の実体に迫ります。
つづく

6
7
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
6
7