Symfony2ソースコードリーディング (3) server:runの仕組みを読み解く

前回の記事 [Symfony2ソースコードリーディング (2) app/consoleを読み解く その2](http://qiita.com/DQNEO/items/811e9f504605ae61217f) の続きです。

`app/console server:run`が、`Symfony\Bundle\FrameworkBundle\Command\ServerRunCommand`クラスを呼び出していることはすでに説明しました。
今回はこのクラスを見てみましょう。

# Symfony/Bundle/FrameworkBundle/Command/ServerRunCommand.phpを読む

ソースはこちら
https://github.com/symfony/symfony/blob/2.6/src/Symfony/Bundle/FrameworkBundle/Command/ServerRunCommand.php

`configure()`メソッドは飛ばして`execute()`メソッドを読みます。

```php
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $documentRoot = $input->getOption('docroot');
        if (null === $documentRoot) {
            $documentRoot = $this->getContainer()->getParameter('kernel.root_dir').'/../web';
        }
        if (!is_dir($documentRoot)) {
            $output->writeln(sprintf('<error>The given document root directory "%s" does not exist</error>', $documentRoot));
            return 1;
        }
        $env = $this->getContainer()->getParameter('kernel.environment');
        $address = $input->getArgument('address');
        if (false === strpos($address, ':')) {
            $output->writeln('The address has to be of the form <comment>bind-address:port</comment>.');
            return 1;
        }
        if ($this->isOtherServerProcessRunning($address)) {
            $output->writeln(sprintf('<error>A process is already listening on http://%s.</error>', $address));
            return 1;
        }
        if ('prod' === $env) {
            $output->writeln('<error>Running PHP built-in server in production environment is NOT recommended!</error>');
        }
        $output->writeln(sprintf("Server running on <info>http://%s</info>\n", $address));
        $output->writeln('Quit the server with CONTROL-C.');
        if (null === $builder = $this->createPhpProcessBuilder($output, $address, $input->getOption('router'), $env)) {
            return 1;
        }
        $builder->setWorkingDirectory($documentRoot);
        $builder->setTimeout(null);
        $process = $builder->getProcess();
        if (OutputInterface::VERBOSITY_VERBOSE > $output->getVerbosity()) {
            $process->disableOutput();
        }
        $this
            ->getHelper('process')
            ->run($output, $process, null, null, OutputInterface::VERBOSITY_VERBOSE);
        if (!$process->isSuccessful()) {
            $output->writeln('<error>Built-in server terminated unexpectedly</error>');
            if ($process->isOutputDisabled()) {
                $output->writeln('<error>Run the command again with -v option for more details</error>');
            }
        }
        return $process->getExitCode();
    }
```

上から順に見ていきます。

```php
        $documentRoot = $input->getOption('docroot');
        if (null === $documentRoot) {
            $documentRoot = $this->getContainer()->getParameter('kernel.root_dir').'/../web';
        }
```

docrootオプションの指定がなければ、 `web`ディレクトリをDocumentRootとして設定します。
まあ`app/console server:run`するときにいちいちdocrootなんて指定しませんよね。

```php
        if (!is_dir($documentRoot)) {
            $output->writeln(sprintf('<error>The given document root directory "%s" does not exist</error>', $documentRoot));
            return 1;
        }
```

DocumentRootで指定したディレクトリが存在しなければエラーとします。

余談ですが、エラーは例外を投げるとかじゃなくてUnixの終了ステータスをreturnするんですね。
PHPらしくなくてシェルスクリプトっぽさがあります。

この後、

* 引数の `address:port` のチェック
* 既に起動してるサーバプロセスがないかチェック
* 本番環境で実行した場合は警告 (本番で使うなと)
* コンソールにログを表示

をしてから、メインの処理にうつります。

```php
        if (null === $builder = $this->createPhpProcessBuilder($output, $address, $input->getOption('router'), $env)) {
            return 1;
        }
```

PhpProcessBuilderとやらを生成しています。
「プロセスビルダーを作る」とはまたずいぶんと抽象的かつ遠回りな処理ですね。
createPhpProcessBuilder()の定義はすぐ下にあります。

```php
    private function createPhpProcessBuilder(OutputInterface $output, $address, $router, $env)
    {
        $router = $router ?: $this
            ->getContainer()
            ->get('kernel')
            ->locateResource(sprintf('@FrameworkBundle/Resources/config/router_%s.php', $env))
        ;

        if (!file_exists($router)) {
            $output->writeln(sprintf('<error>The given router script "%s" does not exist</error>', $router));

            return;
        }

        $router = realpath($router);
        $finder = new PhpExecutableFinder();

        if (false === $binary = $finder->find()) {
            $output->writeln('<error>Unable to find PHP binary to run server</error>');

            return;
        }

        return new ProcessBuilder(array($binary, '-S', $address, $router));
    }
```

$routerまわりはちょっとよくわかりませんので無視します。

PhpExecutableFinder::find()で PHPのバイナリのパスを取得しているような雰囲気です。

PhpExecutableFinderクラスをみてみましょう。

https://github.com/symfony/symfony/blob/2.6/src/Symfony/Component/Process/PhpExecutableFinder.php#L36

```php
    /**
     * Finds The PHP executable.
     *
     * @param bool $includeArgs Whether or not include command arguments
     *
     * @return string|false The PHP executable path or false if it cannot be found
     */
    public function find($includeArgs = true)
    {
        // HHVM support
        if (defined('HHVM_VERSION')) {
            return (false !== ($hhvm = getenv('PHP_BINARY')) ? $hhvm : PHP_BINARY).($includeArgs ? ' '.implode(' ', $this->findArguments()) : '');
        }
        // PHP_BINARY return the current sapi executable
        if (defined('PHP_BINARY') && PHP_BINARY && in_array(PHP_SAPI, array('cli', 'cli-server')) && is_file(PHP_BINARY)) {
            return PHP_BINARY;
        }
        if ($php = getenv('PHP_PATH')) {
            if (!is_executable($php)) {
                return false;
            }
            return $php;
        }
        if ($php = getenv('PHP_PEAR_PHP_BIN')) {
            if (is_executable($php)) {
                return $php;
            }
        }
        $dirs = array(PHP_BINDIR);
        if ('\\' === DIRECTORY_SEPARATOR) {
            $dirs[] = 'C:\xampp\php\\';
        }
        return $this->executableFinder->find('php', false, $dirs);
    }
```

長々と書いてありますが、HHVMやらWindowsやらの環境でも動くようにするためのhackが書いてあるだけで、
本質的にはPHP CLIのパスを取得しているだけのように見えます。

ちょっと手元で実験してみましょう。

```
$ php -r 'echo PHP_BINARY . "\n";'
/opt/php-5.6.8/bin/php
```

やはりそうでした。単に`PHP_BINARY`定数の値を参照しているだけと考えてよさそうです。

なので、

```
        return new ProcessBuilder(array($binary, '-S', $address, $router));
```

は

```
        return new ProcessBuilder(array("/opt/php-5.6.8/bin/php", '-S', $address, $router));
```

みたいなものだと考えればよいですね。

さて、`ProcessBuilder`クラス を見てみましょう。
https://github.com/symfony/symfony/blob/2.6/src/Symfony/Component/Process/ProcessBuilder.php

このクラスはプロパティとgetter,setterがあるくらいで大した仕事はしてなさそうです。
主な処理はgetProcess()です。

```php
    /**
     * Creates a Process instance and returns it.
     *
     * @return Process
     *
     * @throws LogicException In case no arguments have been provided
     */
    public function getProcess()
    {
        if (0 === count($this->prefix) && 0 === count($this->arguments)) {
            throw new LogicException('You must add() command arguments before calling getProcess().');
        }

        $options = $this->options;

        $arguments = array_merge($this->prefix, $this->arguments);
        $script = implode(' ', array_map(array(__NAMESPACE__.'\\ProcessUtils', 'escapeArgument'), $arguments));

        if ($this->inheritEnv) {
            // include $_ENV for BC purposes
            $env = array_replace($_ENV, $_SERVER, $this->env);
        } else {
            $env = $this->env;
        }

        $process = new Process($script, $this->cwd, $env, $this->input, $this->timeout, $options);

        if ($this->outputDisabled) {
            $process->disableOutput();
        }

        return $process;
    }
```

ひとことで言うとnew Processしてるだけですね。
ここで$script変数を渡しているのですが、これの中身を見てみましょう。

いつものようにvar_dump()をしかけて、

```diff
+       var_dump($script);exit;
        $process = new Process($script, $this->cwd, $env, $this->input, $this->timeout, $options);
```

`app/console run:server`を実行してみます。

```
$ ./app/console server:run
Server running on http://127.0.0.1:8000

Quit the server with CONTROL-C.
string(171) "'/opt/php-5.6.8/bin/php' '-S' '127.0.0.1:8000' '/Users/DQNEO/tmp/2015-05-15/blog/vendor/symfony/symfony/src/Symfony/Bundle/FrameworkBundle/Resources/config/router_dev.php'"
```

ふむふむ。$scriptの中身は、ビルトインサーバを起動するためのコマンドライン文字列でした。
なんとなくやろうとしてることが想像できます。

さて、ここでnewされているProcessクラスを見てみましょう。

https://github.com/symfony/symfony/blob/2.6/src/Symfony/Component/Process/Process.php

クラスレベルコメントにこう書いてあります。

> Process is a thin wrapper around proc_* functions to easily start independent PHP processes.

proc_*関数のラッパーであると。


これのインスタンスが生成されて、冒頭に書いたSymfony\Bundle\FrameworkBundle\Command\ServerRunCommandクラスに戻ってきます。

```php
        $process = $builder->getProcess();
        if (OutputInterface::VERBOSITY_VERBOSE > $output->getVerbosity()) {
            $process->disableOutput();
        }
        $this
            ->getHelper('process')
            ->run($output, $process, null, null, OutputInterface::VERBOSITY_VERBOSE);

```

`$this->->getHelper('process')->run()`に$processが渡されています。

さてこのrun()はProcessHelperというクラスのメソッドのようです。
ちょっとしんどくなってきたw　のでそこは飛ばします。

Processクラスのどこで実際のコマンドが実行されているのでしょうか。

答えはすばりここで、

https://github.com/symfony/symfony/blob/2.6/src/Symfony/Component/Process/Process.php#L289

```php
    /**
     * Starts the process and returns after writing the input to STDIN.
     *
     * This method blocks until all STDIN data is sent to the process then it
     * returns while the process runs in the background.
     *
     * The termination of the process can be awaited with wait().
     *
     * The callback receives the type of output (out or err) and some bytes from
     * the output in real-time while writing the standard input to the process.
     * It allows to have feedback from the independent process during execution.
     * If there is no callback passed, the wait() method can be called
     * with true as a second parameter then the callback will get all data occurred
     * in (and since) the start call.
     *
     * @param callable|null $callback A PHP callback to run whenever there is some
     *                                output available on STDOUT or STDERR
     *
     * @throws RuntimeException When process can't be launched
     * @throws RuntimeException When process is already running
     * @throws LogicException   In case a callback is provided and output has been disabled
     */
    public function start($callback = null)
    {
        if ($this->isRunning()) {
            throw new RuntimeException('Process is already running');
        }
        if ($this->outputDisabled && null !== $callback) {
            throw new LogicException('Output has been disabled, enable it to allow the use of a callback.');
        }
        $this->resetProcessData();
        $this->starttime = $this->lastOutputTime = microtime(true);
        $this->callback = $this->buildCallback($callback);
        $descriptors = $this->getDescriptors();
        $commandline = $this->commandline;
        if ('\\' === DIRECTORY_SEPARATOR && $this->enhanceWindowsCompatibility) {
            $commandline = 'cmd /V:ON /E:ON /C "('.$commandline.')';
            foreach ($this->processPipes->getFiles() as $offset => $filename) {
                $commandline .= ' '.$offset.'>'.ProcessUtils::escapeArgument($filename);
            }
            $commandline .= '"';
            if (!isset($this->options['bypass_shell'])) {
                $this->options['bypass_shell'] = true;
            }
        }
        $this->process = proc_open($commandline, $descriptors, $this->processPipes->pipes, $this->cwd, $this->env, $this->options);
        if (!is_resource($this->process)) {
            throw new RuntimeException('Unable to launch a new process.');
        }
        $this->status = self::STATUS_STARTED;
        if ($this->tty) {
            return;
        }
        $this->updateStatus(false);
        $this->checkTimeout();
    }
```

このproc_open()がサーバを起動している部分です。

```
        $this->process = proc_open($commandline, $descriptors, $this->processPipes->pipes, $this->cwd, $this->env, $this->options);
```

`$commandline`というのは上で説明した`$script`変数と同じもので、ビルトインサーバを実行するコマンドライン文字列です。
実際の値は、`'/opt/php-5.6.8/bin/php' '-S' '127.0.0.1:8000' '/Users/DQNEO/tmp/2015-05-15/blog/vendor/symfony/symfony/src/Symfony/Bundle/FrameworkBundle/Resources/config/router_dev.php'`となっていました。

# まとめ

`app/console server:run`の実体は、`proc_open('php -S 127.0.0.1:8000')`である

ちなみにproc_openは何かというと、プロセス間通信とかパイプとかそういうやつです。

http://php.net/proc_open

参考までに、`app/console server:run`してから`proc_open()`にいたるまでのスタックトレースは下記のようになります。

```
#0  Symfony\Component\Process\Process->start()
#1  Symfony\Component\Process\Process->run()
#2  Symfony\Component\Console\Helper\ProcessHelper->run()
#3  Symfony\Bundle\FrameworkBundle\Command\ServerRunCommand->execute()
#4  Symfony\Component\Console\Command\Command->run()
#5  Symfony\Component\Console\Application->doRunCommand()
#6  Symfony\Component\Console\Application->doRun()
#7  Symfony\Bundle\FrameworkBundle\Console\Application->doRun()
#8  Symfony\Component\Console\Application->run()
```

# 感想

たかが`php -S`でサーバ起動するのに、こんなに大量のクラスをかぶせる必要はあるのだろうか...
解読しながらこの記事を書くのに半日かかりましたw

正直、下記で同じことできるやんと思いました。

```
php -S localhost:8000 -t web/app_dev.php
```