LoginSignup
5
5

More than 5 years have passed since last update.

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

Last updated at Posted at 2015-05-16

前回の記事 Symfony2ソースコードリーディング (2) app/consoleを読み解く その2 の続きです。

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()メソッドを読みます。

    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();
    }

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

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

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

        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 のチェック
  • 既に起動してるサーバプロセスがないかチェック
  • 本番環境で実行した場合は警告 (本番で使うなと)
  • コンソールにログを表示

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

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

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

    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クラスをみてみましょう。

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

    /**
     * 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()をしかけて、

+       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クラスを見てみましょう。

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

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

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

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

        $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クラスのどこで実際のコマンドが実行されているのでしょうか。

答えはすばりここで、

    /**
     * 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は何かというと、プロセス間通信とかパイプとかそういうやつです。

参考までに、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
5
5
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
5
5