目次
CakePHP 3 の ビルトインサーバ の実態がなんなのか気になったので 調べました。 php bin/cake.php server
で動くサーバです。
環境
- PHP 5.5.9
- cakephp/cakephp 3.0.9
- Ubuntu 14.04.2 LTS
コマンドは php bin/cake.php server
なので、 cake.php
から順にコードを読み進めます。
cake.php
今回だけでなく、コマンドを実行する時に使う最初のファイルです。 ディレクトリ bin
の中に cake
、cake.bat
、cake.php
がありますが、 cake
もcake.bat
もcake.php
を実行しているだけです。 cake
はシェルスクリプト 、 cake.bat
は Windows 用 bat ファイル です。
cake.php
の中では config/bootstrap.php
を読み込んで実行し、 CakePHP 3 のコアファイルを読み込んでいます。 そして ShellDispatcher
で実際のコマンド内容を処理します。 注目すべきは最後の行です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
#!/usr/bin/php -q <?php /** * Command-line code generation utility to automate programmer chores. * * CakePHP(tm) : Rapid Development Framework (http://cakephp.org) * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org) * * Licensed under The MIT License * For full copyright and license information, please see the LICENSE.txt * Redistributions of files must retain the above copyright notice. * * @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org) * @link http://cakephp.org CakePHP(tm) Project * @since 2.0.0 * @license http://www.opensource.org/licenses/mit-license.php MIT License */ $minVersion = '5.4.16'; if (file_exists('composer.json')) { $composer = json_decode(file_get_contents('composer.json')); if (isset($composer->require->php)) { $minVersion = preg_replace('/([^0-9.])/', '', $composer->require->php); } } if (version_compare(phpversion(), $minVersion, '<')) { fwrite(STDERR, sprintf("Minimum PHP version: %s. You are using: %s.n", $minVersion, phpversion())); exit(-1); } include dirname(__DIR__) . '/config/bootstrap.php'; exit(CakeConsoleShellDispatcher::run($argv)); |
CakeConsoleShellDispatcher::run($argv)
の返り値がコマンドの終了ステータスになります。
CakeConsoleShellDispatcher::run($argv)
の中を見ていきます。
run
1 2 3 4 5 6 7 8 9 10 11 |
/** * Run the dispatcher * * @param array $argv The argv from PHP * @return int The exit code of the shell process. */ public static function run($argv) { $dispatcher = new ShellDispatcher($argv); return $dispatcher->dispatch(); } |
パラメータが new ShellDispatcher($argv)
で渡され、 dispatch()
で処理を実行します。
ShellDispatcher
では コンストラクタで色々処理をやっていますが、 重要なのは $argv
をインスタンス変数に格納していることです。
dispatch
dispatch
という名前だけあって、 振り分けに徹したメソッドとなっています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
/** * Dispatches a CLI request * * Converts a shell command result into an exit code. Null/True * are treated as success. All other return values are an error. * * @return int The cli command exit code. 0 is success. */ public function dispatch() { $result = $this->_dispatch(); if ($result === null || $result === true) { return 0; } return 1; } |
dispatch
は内部で _dispatch
を呼んでいます。 実質的な処理は _dispatch
の中で完結し、 dispatch
は結果(終了ステータス)のみ返します。
_dispatch
パラメータに応じてシェルを探して実行します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
/** * Dispatch a request. * * @return bool * @throws CakeConsoleExceptionMissingShellMethodException */ protected function _dispatch() { $shell = $this->shiftArgs(); if (!$shell) { $this->help(); return false; } if (in_array($shell, ['help', '--help', '-h'])) { $this->help(); return true; } $Shell = $this->findShell($shell); $Shell->initialize(); return $Shell->runCommand($this->args, true); } |
findShell
と rumCommand
がポイントですね。
_dispatch()
の shiftArgs
のところは、 一番最初のパラメータを取り出しています。 今回題材にしている サーバ起動の場合は 'server'
になります。 そして、 パラメータがない または help
、--help
、-h
のいずれかが最初のパラメータになっていた場合は help を表示します。 また、 パラメータがない場合には false
を返し、 最終的に 終了ステータス 1 (失敗) が返ります。
findShell
実際の処理が書かれたシェルを呼び出すところです。 cake.php
は php bin/cake.php bake
や php bin/cake.php migrations
のように呼び出され、 $shell
の中には 'bake'
や 'migratoins'
が入ります。 今回は 'server'
が入ります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
/** * Get shell to use, either plugin shell or application shell * * All paths in the loaded shell paths are searched, handles alias * dereferencing * * @param string $shell Optionally the name of a plugin * @return CakeConsoleShell A shell instance. * @throws CakeConsoleExceptionMissingShellException when errors are encountered. */ public function findShell($shell) { $className = $this->_shellExists($shell); if (!$className) { $shell = $this->_handleAlias($shell); $className = $this->_shellExists($shell); } if (!$className) { throw new MissingShellException([ 'class' => $shell, ]); } return $this->_createShell($className, $shell); } |
$shell
には 'server'
が入っています。 _shellExists
でクラス名を取得して、 _createShell
でインスタンスを作成します。
_shellExists
コードを見ると、 App::className
でクラス名を取得していることがわかります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
/** * Check if a shell class exists for the given name. * * @param string $shell The shell name to look for. * @return string|bool Either the classname or false. */ protected function _shellExists($shell) { $class = App::className($shell, 'Shell', 'Shell'); if (class_exists($class)) { return $class; } return false; } |
クラスが存在すればクラス名を返すようになっています。
CakeCoreApp
の メソッド className
を 見てみます。
App
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
/** * Return the class name namespaced. This method checks if the class is defined on the * application/plugin, otherwise try to load from the CakePHP core * * @param string $class Class name * @param string $type Type of class * @param string $suffix Class name suffix * @return bool|string False if the class is not found or namespaced class name */ public static function className($class, $type = '', $suffix = '') { if (strpos($class, '') !== false) { return $class; } list($plugin, $name) = pluginSplit($class); $base = $plugin ?: Configure::read('App.namespace'); $base = str_replace('/', '', rtrim($base, '')); $fullname = '' . str_replace('/', '', $type . '' . $name) . $suffix; if (static::_classExistsInBase($fullname, $base)) { return $base . $fullname; } if ($plugin) { return false; } if (static::_classExistsInBase($fullname, 'Cake')) { return 'Cake' . $fullname; } return false; } |
className
メソッド に渡されるパラメータは 'server'
、 'Shell'
、 'Shell'
でした。 pluginSplit
は cakephp/src/Core/functions.php
で定義された関数で、 ここでは [null, 'server']
が返ります。
Configure::read
では config.php
に書かれた namespace の値を取得しています。 特に設定していなければ 'App'
になります。
また、 コード内の変数 $fullname
は 'ShellserverShell'
になります。
以上を踏まえ、 _classExistsInBase
で何が返ってくるのかを考えます。
_classExistsInBase
_classExistsInBase
はクラスが定義されているか否かをチェックするメソッドになっており、 1番目の引数がクラス名、 2番目の引数が名前空間になっています。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
/** * _classExistsInBase * * Test isolation wrapper * * @param string $name Class name. * @param string $namespace Namespace. * @return bool */ protected static function _classExistsInBase($name, $namespace) { return class_exists($namespace . $name); } |
内部で PHP の関数 class_exists
を呼び出しています。 クラスが定義されていれば true
、 定義されていなければ false
になります。
className
の中では 2回 _classExistsInBase
を呼び出していました。 1回目は _classExistsInBase('ShellserverShell', 'App')
、 2回目は _classExistsInBase('ShellserverShell', 'Cake')
です。
2回目に呼び出されたときには 惜しくも class_exists('CakeShellserverShell')
が実行されて false
が返ります。 OS によってここで返ってくる値が変わりますが、心配することはありません。 この後 'server'
の 's'
が大文字になって再実行されます。 (クラスのロードは bootstrap.php
を通じて行われていますが、 ここでは深追いしません。)
_classExistsInBase
は false
を返すので、 _shellExists
も false
を返し、 findShell
の 最初の if
節 が実行されます。
1 2 3 4 |
if (!$className) { $shell = $this->_handleAlias($shell); $className = $this->_shellExists($shell); } |
_handleAlias($shell)
が実行されているのでその中を見てみましょう。
_handleAlias
_handleAlias
では 登録されているエイリアスと比較し、 登録されていなければ スネークケースをキャメルケースに変換して返します。 キャメルケースに変換する場合は、 'abc_def.ghi'
という形の文字列が Abc_Def.Ghi
に変換されます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
/** * If the input matches an alias, return the aliased shell name * * @param string $shell Optionally the name of a plugin or alias * @return string Shell name with plugin prefix */ protected function _handleAlias($shell) { $aliased = static::alias($shell); if ($aliased) { $shell = $aliased; } $class = array_map('CakeUtilityInflector::camelize', explode('.', $shell)); return implode('.', $class); } |
static::alias('server')
の返り値は false
になります。 alias
に関するコードは別の機会に読み解くことにします。
CakeUtilityInflector::camelize'
も概要をいえば (キャッシュも使って)スネークケースをキャメルケースにするメソッドです。 ここでは深追いしません。
_handleAlias
を経て、 $shell
の値が 'Server'
になります。 そして、 晴れて _shellExists('Server')
が実行されます。 さっきは _shellExists('server')
が実行されて、 'server'
の先頭が小文字だったために 結果が false
になっていました。 今回は _shellExists
が App::className('Server')
すなわち CakeShellServerShell
を返します。 そして _createShell('CakeShellServerShell', 'server')
が実行されます。
最初から php bin/cake.php Server
とサーバを起動してもよかったことがわかります。
_createShell
ここでは指定されたクラスのインスタンスを作ります。 一番重要なのは new $className()
のところですね。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
/** * Create the given shell name, and set the plugin property * * @param string $className The class name to instantiate * @param string $shortName The plugin-prefixed shell name * @return CakeConsoleShell A shell instance. */ protected function _createShell($className, $shortName) { list($plugin) = pluginSplit($shortName); $instance = new $className(); $instance->plugin = trim($plugin, '.'); return $instance; } |
pluginSplit
は先ほども出てきました。 ここでは $plugin
に null
が入ります。 そして $instance->plugin
というのは CakeShellServerShell
が継承する CakeConsoleShell
で定義されたインスタンス変数で、 初期値は null
です。
findShell
の返り値として $Shell
にインスタンスが代入されました。 dispatch()
はあと2行で終わります。
1 2 |
$Shell->initialize(); return $Shell->runCommand($this->args, true); |
$Shell->initialize()
では ShellServer
の initialize
メソッド が実行されます。
initialize
ここではすべて初期値が設定されます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
/** * Default ServerHost * * @var string */ const DEFAULT_HOST = 'localhost'; /** * Default ListenPort * * @var int */ const DEFAULT_PORT = 8765; /** * Override initialize of the Shell * * @return void */ public function initialize() { $this->_host = self::DEFAULT_HOST; $this->_port = self::DEFAULT_PORT; $this->_documentRoot = WWW_ROOT; } |
WWW_ROOT
は bootstrap.php
の中で読み込まれる path.php
で定義されているもので、 webroot
の絶対パスが設定されています。
そして次は runCommand
が実行されます。 runCommand
は Shell
で定義されています。
runCommand
最初にオプションの処理をして、オプションに応じてメソッドが実行されます。 今回は return call_user_func_array([$this, 'main'], $this->args);
の行で ServerShell
の main
が実行されてサーバが起動します。 getOptionParser()
、startup()
、call_user_func_array([$this, 'main'], $this->args)
がポイントです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
/** * Runs the Shell with the provided argv. * * Delegates calls to Tasks and resolves methods inside the class. Commands are looked * up with the following order: * * - Method on the shell. * - Matching task name. * - `main()` method. * * If a shell implements a `main()` method, all missing method calls will be sent to * `main()` with the original method name in the argv. * * For tasks to be invoked they *must* be exposed as subcommands. If you define any subcommands, * you must define all the subcommands your shell needs, whether they be methods on this class * or methods on tasks. * * @param array $argv Array of arguments to run the shell with. This array should be missing the shell name. * @param bool $autoMethod Set to true to allow any public method to be called even if it * was not defined as a subcommand. This is used by ShellDispatcher to make building simple shells easy. * @return mixed * @link http://book.cakephp.org/3.0/en/console-and-shells.html#the-cakephp-console */ public function runCommand($argv, $autoMethod = false) { $command = isset($argv[0]) ? $argv[0] : null; $this->OptionParser = $this->getOptionParser(); try { list($this->params, $this->args) = $this->OptionParser->parse($argv); } catch (ConsoleException $e) { $this->err('<error>Error: ' . $e->getMessage() . '</error>'); $this->out($this->OptionParser->help($command)); return false; } if (!empty($this->params['quiet'])) { $this->_io->level(ConsoleIo::QUIET); $this->_io->setLoggers(false); } if (!empty($this->params['verbose'])) { $this->_io->level(ConsoleIo::VERBOSE); } if (!empty($this->params['plugin'])) { Plugin::load($this->params['plugin']); } $this->command = $command; if (!empty($this->params['help'])) { return $this->_displayHelp($command); } $subcommands = $this->OptionParser->subcommands(); $method = Inflector::camelize($command); $isMethod = $this->hasMethod($method); if ($isMethod && $autoMethod && count($subcommands) === 0) { array_shift($this->args); $this->startup(); return call_user_func_array([$this, $method], $this->args); } if ($isMethod && isset($subcommands[$command])) { $this->startup(); return call_user_func_array([$this, $method], $this->args); } if ($this->hasTask($command) && isset($subcommands[$command])) { $this->startup(); array_shift($argv); return $this->{$method}->runCommand($argv, false); } if ($this->hasMethod('main')) { $this->startup(); return call_user_func_array([$this, 'main'], $this->args); } $this->out($this->OptionParser->help($command)); return false; } |
$this->getOptionParser()
に注意してください。 ここで実行されるメソッドは、 Shell
のものではなく、 Shell
を継承した ServerShell
のコードです。
まずはオプションを処理する getOptionParser
を見てみます。
getOptionParser
このメソッドを見ると、サーバ起動時にどんなパラメータが設定できるのか推測できます。 メソッドの返り値は、 ConsoleOptionParser
のインスタンスです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
/** * Gets the option parser instance and configures it. * * @return CakeConsoleConsoleOptionParser */ public function getOptionParser() { $parser = parent::getOptionParser(); $parser->description([ 'PHP Built-in Server for CakePHP', '<warning>[WARN] Don't use this at the production environment</warning>', ])->addOption('host', [ 'short' => 'H', 'help' => 'ServerHost' ])->addOption('port', [ 'short' => 'p', 'help' => 'ListenPort' ])->addOption('document_root', [ 'short' => 'd', 'help' => 'DocumentRoot' ]); return $parser; } |
H, host | ホスト名 ・ IPアドレス |
---|---|
p, port | ポート番号 |
d, document_root | ドキュメントルート |
設定できるのは、 ServerShell
の initialize
で初期値に設定されたものです。
ここでは深入りしませんが、 親クラスの Shell
に記述された getOptionParser
が実行されています。 そして description
、addOption
によって、 ServerShell
特有の説明とオプションが設定されています。
parse
このメソッドでは、コマンドで渡されたオプションに基づいてインスタンス変数に値を格納します。 内部で いくつかメソッドを呼び出して、 $params
という変数(連想配列)にパラメータを格納します。
返り値は [$params, $arg]
で $arg
には オプション ではない値が格納されます。 今回は空の配列です。
parse
でオプション設定が終わったら、 次は startup
で起動の準備が行われます。
startup
ここで設定したパラメータが適用されていきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
/** * Starts up the Shell and displays the welcome message. * Allows for checking and configuring prior to command or main execution * * Override this method if you want to remove the welcome information, * or otherwise modify the pre-command flow. * * @return void * @link http://book.cakephp.org/3.0/en/console-and-shells.html#hook-methods */ public function startup() { if (!empty($this->params['host'])) { $this->_host = $this->params['host']; } if (!empty($this->params['port'])) { $this->_port = $this->params['port']; } if (!empty($this->params['document_root'])) { $this->_documentRoot = $this->params['document_root']; } // For Windows if (substr($this->_documentRoot, -1, 1) === DS) { $this->_documentRoot = substr($this->_documentRoot, 0, strlen($this->_documentRoot) - 1); } if (preg_match("/^([a-z]:)[]+(.+)$/i", $this->_documentRoot, $m)) { $this->_documentRoot = $m[1] . '' . $m[2]; } parent::startup(); } |
parse
で設定された ホスト、ポート、ドキュメントルートを設定しています。 そして 親クラスの startup()
を呼び出しています。 親クラスの startup()
は welcome message を表示します。
preg_match
のところは Windows のファイルシステムで c:docroot
などが指定された場合に cdocroot
に変更するものと思われます。
main
ようやく サーバ起動部分のコードに辿り着きました。 call_user_func_array
で main
が呼び出されます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
/** * Override main() to handle action * * @return void */ public function main() { $command = sprintf( "php -S %s:%d -t %s %s", $this->_host, $this->_port, escapeshellarg($this->_documentRoot), escapeshellarg($this->_documentRoot . '/index.php') ); $port = ':' . $this->_port; $this->out(sprintf('built-in server is running in http://%s%s/', $this->_host, $port)); $this->out(sprintf('You can exit with <info>`CTRL-C`</info>')); system($command); } |
結局これです。 php -S host:port -t documentroot documentroot/index.php
が実行されています。
$port
という変数を定義していますが、 定義しなくてもいいような気がします。
PHP のマニュアルを見ると分かりますが、 -S
の直後に指定されているのが待ち受けるホスト名とポート、 -t
の直後に書かれているのが ドキュメントルート、 その後に書かれているのが ルータスクリプトです。 ルータスクリプトは、あるURLで来たリクエストについてどのような処理をするのかをスクリプトとして書くことができます。 通常指定されるルータスクリプト index.php
では、 存在するファイルがリクエストされたら false
を返し、 php では処理しないようになっています。