Table of Contents
I read what the CakePHP 3 built-in server is, which is start with php bin/cake.php server.
Environment
- PHP 5.5.9
- cakephp/cakephp 3.0.9
- Ubuntu 14.04.2 LTS
The starting command is php bin/cake.php server, now start reading with cake.php.
cake.php
This is the first file whenever we run CakePHP3 command. There are cake , cake.bat , cake.php in bin directory, but both cake and cake.bat only calls cake.php. cake is shell script and cake.bat is bat file for Windows.
cake.php read config/bootstrap.php and load CakePHP 3 core files. And ShellDispatcher execute command. The last line is important.
|
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)); |
The return value of CakeConsoleShellDispatcher::run($argv) will be command exit status.
Let’s look into 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(); } |
Arguments passed at new ShellDispatcher($argv) , and dispatch() executes the procedure.
The constructor of ShellDispatcher handle several procedures, but the point is $argv is set into instance variable.
dispatch
dispatch focuses on dispatching.
|
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 calls _dispatch. The substantial procedure is in _dispatch, and dispatch returns the result, exit status.
_dispatch
This method find shall code and execute it, according to arguments.
|
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); } |
The point is findShell and rumCommand.
shiftArgs in _dispatch() take the first argument. On launching server, the first argument is 'server'. If there’s no argument or the first one is help, --help or -h, it shows help message. And only if with no argument it returns false and eventually exit status will be 1 (failed).
findShell
This calls substantial shell code. cake.php is called as php bin/cake.php bake or php bin/cake.php migrations, and $shell will be 'bake' or 'migratoins'. On launching server, it will be '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); } |
_shellExists returns class name and _createShell create instance of the returned class.
_shellExists
According to the code, we can know it gets class name by 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; } |
This method returns class name if it exists, otherwise return false.
Now, look into className method in CakeCoreApp.
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; } |
The parameters passed to className are 'server' , 'Shell' and 'Shell'. pluginSplit is defined in cakephp/src/Core/functions.php and returns [null, 'server'] here.
Configure::read read namespase setting in config.php and return it. 'App' is the namespace in default.
The variable $fullname will be 'ShellserverShell'.
Okey, now, let’s see what is returned by _classExistsInBase.
_classExistsInBase
_classExistsInBase is the method to check the class is defined or not. The first parameter is class name and the second one is namespace, both are string.
|
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); } |
This method calls class_exists. If the given class is defined, it returns true, otherwise it returns false.
In className method, _classExistsInBase is called twice. The first call is _classExistsInBase('ShellserverShell', 'App') , and the second one is _classExistsInBase('ShellserverShell', 'Cake').
At the second call, unfortunately, class_exists('CakeShellserverShell') is executed and false is returned. The return value is changed according to OS, but it’s not worrying. After that, _classExistsInBase('ShellServerShell', 'Cake') will be called.
_classExistsInBase returns false false, so _shellExists also returns false and the first if clause in findShell will be executed.
|
1 2 3 4 |
if (!$className) { $shell = $this->_handleAlias($shell); $className = $this->_shellExists($shell); } |
Now, let’s look into _handleAlias($shell).
_handleAlias
_handleAlias compares with registered aliases and if the given parameter is not registered, change it from snake case to camel case and return it. If 'abc_def.ghi' is given , Abc_Def.Ghi will be returned (if it’s not registered).
|
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') returns false. I will read the code around alias some day.
CakeUtilityInflector::camelize' also changes snake case to camel case, with caching.
After _handleAlias, $shell changes to 'Server', and fortunately _shellExists('Server') will be executed. Before, _shellExists('server') returns false because of the lower case first letter of 'server'. But this time, _shellExists returns App::className('Server') which returns CakeShellServerShell, and _createShell('CakeShellServerShell', 'server') will be executed.
So php bin/cake.php Server also can launch server.
_createShell
This method creates instance of given class. The most important is 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 appears again, and set $plugin null this time. $instance->plugin is the instance variable defined in CakeConsoleShell, which is inherited by CakeShellServerShell, and its default value is null.
Now $Shell contains instance of shell class, that is the return value of findShell. dispatch() remains 2 lines.
|
1 2 |
$Shell->initialize(); return $Shell->runCommand($this->args, true); |
$Shell->initialize() calls initialize method in ShellServer.
initialize
In here, set default value into instance variables.
|
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 is defined in path.php read by bootstrap.php. It is the absolute path of webroot directory.
And next, runCommand will be executed, which is defined in Shell class.
runCommand
This method handles given options first, and execute procedure according to options. On launching server, main method of ServerShell class by return call_user_func_array([$this, 'main'], $this->args); line and the server will launch. getOptionParser(), startup() and call_user_func_array([$this, 'main'], $this->args) are the points.
|
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; } |
Take attention to $this->getOptionParser(). The method executed here is not of Shell, but of ServerShell which inherits Shell.
Let’s look into getOptionParser, which handle given options.
getOptionParser
According to the code, we can see what parameters can be used on server launching. The return value is the instance of ConsoleOptionParser class.
|
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 | host name or ip address |
|---|---|
| p, port | port number |
| d, document_root | document root |
The options we can set is initialized in initialize method of ServerShell.
And getOptionParser of ServerShell calls one of the parent class, Shell. And it set options by description and addOption, which is unique for ServerShell.
parse
This method set instance variables according to command arguments. Set parameters into $params variable by calling some methods.
The return value is [$params, $arg] and $arg contains values except option. It is blank array in this time, server launching.
After option setting by parse, it prepares for launching server by startup .
startup
This method applies parameter set by parse method.
|
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(); } |
The code set host, port and document root set by parse method, and calls startup() of parent class, which shows welcome message.
preg_match changes c:docroot to cdocroot. It is for Windows system, I guess.
main
We can reach the code that launch server finally. call_user_func_array calls main method of this class.
|
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); } |
The result is this, php -S host:port -t documentroot documentroot/index.php.
The variable $port seems to be not used ….
According to PHP, -S specifies host and port, -t specifies document root, and last argument specifies router script. Router script can define what procedure should be executed according to request url. As default in CakePHP 3, index.php is the router script and it returns false when existing file is requested for preventing handling by php.




