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.