前回の記事 Symfony2ソースコードリーディング (2) app/consoleを読み解く その2 の続きです。
app/console server:run
が、Symfony\Bundle\FrameworkBundle\Command\ServerRunCommand
クラスを呼び出していることはすでに説明しました。
今回はこのクラスを見てみましょう。
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