self :: $core_mode)); } return self :: $core_mode; } /** * On CLI set core mode hook * @param \EesyPHP\HookEvent $event * @return void */ public static function on_cli_set_core_mode($event) { if ($event->enabled) { self :: add_command( 'new_project', array('\\EesyPHP\\Cli', 'cli_new_project'), ___("Create a new project using EesyPHP framework"), null, ___( "This command could be used to easily build the structure of a new project using the ". "EesyPHP framework.") ); } else { self :: add_command( 'serve', array('\\EesyPHP\\Cli', 'cli_serve'), ___("Start the PHP built-in HTTP server to serve the application"), '[-h] [-P -O path] [[A.B.C.D][:port]] [-p http://public.url]', ___( "This command could be used to start the PHP built-in HTTP server to serve the application. Additionnal parameters: -p/--public-url Define the public URL (default: based on listen address) -P/--enable-profiler Enable Xdebug profiler -O/--profiler-output [path] Xdebug profiler output directory path" ) ); } } /** * Registered commands * @var array */ protected static $commands = array(); /** * Current CLI command * @var string|null */ protected static $command = null; /** * Add CLI command * @param string $command The command name * @param callable $handler The command handler * @param string $short_desc Short command description * @param string|null $usage_args Argument usage short message * @param string|array|null $long_desc Long command description * @return bool */ public static function add_command($command, $handler, $short_desc, $usage_args=null, $long_desc=null, $override=false) { if (array_key_exists($command, self :: $commands) && !$override) { Log :: error(I18n::_("The CLI command '%s' already exists."), $command); return False; } if (!is_callable($handler)) { Log :: error(I18n::_("The CLI command '%s' handler is not callable !"), $command); return False; } self :: $commands[$command] = array ( 'handler' => $handler, 'short_desc' => $short_desc, 'usage_args' => $usage_args, 'long_desc' => $long_desc, ); return True; } /** * Show usage message * @param string|false $error Error message to show (optional) * @param array $extra_args Extra arguments to use to compute error message using sprintf * @return never */ public static function usage($error=false, ...$extra_args) { global $argv; // If extra arguments passed, format error message using sprintf if ($extra_args) { $error = call_user_func_array( 'sprintf', array_merge(array($error), $extra_args) ); } if ($error) echo "$error\n\n"; echo(I18n::_("Usage: %s [-h] [-qd] command\n", basename($argv[0]))); echo I18n::_(" -h Show this message\n"); echo I18n::_(" -q / -d Quiet/Debug mode\n"); echo I18n::_(" --trace Trace mode (the most verbose)\n"); echo I18n::_(" -l / --log-file Overwrite log file specified in configuration\n"); echo I18n::_(" -C / --console Enable log on console\n"); echo I18n::_(" command Command to run\n"); echo "\n"; echo I18n::_("Available commands:\n"); foreach (self :: $commands as $command => $info) { if (self :: $command && $command != self :: $command) continue; echo ( " ".str_replace( "\n", "\n ", wordwrap("$command : ".I18n::_($info['short_desc']))) ."\n\n"); echo ( " ".basename($argv[0])." $command ". ($info['usage_args']?I18n::_($info['usage_args']):''). "\n"); if ($info['long_desc']) { if (is_array($info['long_desc'])) { $lines = array(); foreach ($info['long_desc'] as $line) $lines[] = I18n::_($line); $info['long_desc'] = implode("\n", $lines); } else $info['long_desc'] = I18n::_($info['long_desc']); echo "\n ".str_replace("\n", "\n ", wordwrap($info['long_desc']))."\n"; } echo "\n"; } exit(($error?1:0)); } /** * Handle command line arguments * @param array|null $args Command line argurment to handle (optional, default: $argv) * @param bool|null $core_mode Force enable/disable EesyPHP core mode (optional, default: false) * @return never */ public static function handle_args($args=null, $core_mode=false) { global $argv; self :: core_mode($core_mode); Log :: register_fatal_error_handler(array('\\EesyPHP\\Cli', 'fatal_error')); $args = is_array($args)?$args:array_slice($argv, 1); $log_level_set = false; self :: $command = null; $command_args = array(); for ($i=0; $i < count($args); $i++) { if (array_key_exists($args[$i], self :: $commands)) { if (!self :: $command) self :: $command = $args[$i]; else self :: usage(I18n::_("Only one command could be executed !")); } else { switch($args[$i]) { case '-h': case '--help': self :: usage(); case '-d': case '--debug': Log :: set_level('DEBUG'); $log_level_set = true; break; case '-q': case '--quiet': Log :: set_level('WARNING'); $log_level_set = true; break; case '--trace': Log :: set_level('TRACE'); $log_level_set = true; break; case '-l': case '--log-file': $i++; Log :: change_filepath($args[$i]); break; case '-C': case '--console': App :: set('log.cli_console', true); break; default: if (self :: $command) $command_args[] = $args[$i]; else self :: usage( I18n::_( "Invalid parameter \"%s\".\nNote: Command's parameter/argument must be place ". "after the command." ), $args[$i] ); } } } if (!$log_level_set) Log :: set_level('INFO'); if (!self :: $command) self :: usage(); Log :: debug( "Run %s command %s with argument(s) '%s'.", basename($args[0]), self :: $command, implode("', '", $command_args) ); try { $result = call_user_func(self :: $commands[self :: $command]['handler'], $command_args); exit($result?0:1); } catch(Exception $e) { Log :: exception($e, I18n::_("An exception occured running command %s"), self :: $command); exit(1); } } /** * Command to create new project based on EesyPHP framework * * @param array $command_args The command arguments * @return true */ public static function cli_new_project($command_args) { echo "This CLI tool permit to initialize a new project using the EesyPHP framework.\n"; readline('[Press enter to continue]'); echo "\n"; $root_path = null; while (!$root_path) { $root_path = getcwd(); $input = readline("Please enter the root directory of your new project [$root_path]: "); if (empty($input)) break; if (!is_dir($input)) self::fatal_error("Invalid root directory specified: not found or is not a directory"); else if (!is_writeable($input)) self::fatal_error("Invalid root directory specified: not writeable"); else $root_path = $input; } $root_path = rtrim($root_path, "/"); $skel = __DIR__ . "/../skel"; foreach ( $iterator = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator($skel, \RecursiveDirectoryIterator::SKIP_DOTS), \RecursiveIteratorIterator::SELF_FIRST ) as $item ) { $path = "$root_path/".$iterator->getSubPathname(); if ($item->isDir()) { if (!mkdir($path)) self::fatal_error("Fail to create $path directory"); } else { if (!copy($item, $path)) self::fatal_error("Fail to copy file to $path"); } if (!chmod($path, $item->getPerms())) self::fatal_error("Fail to chmod file $path"); } echo "done. Start coding!\n"; return true; } /** * Command to start PHP built-in HTTP server to serve the EesyPHP project * * @param array $command_args The command arguments * @return never */ public static function cli_serve($command_args) { $listen_address = null; $public_url = null; $enable_profiler = false; $profiler_output_dir = realpath(getcwd()); for($i=0; $i < count($command_args); $i++) { switch($command_args[$i]) { case '-p': case '--public-url': $i++; $public_url = $command_args[$i]; break; case '-P': case '--enable-profiler': if (phpversion('xdebug') === false) self :: usage(I18n::_("The PHP XDEBUG extension is missing.")); $enable_profiler = true; break; case '-O': case '--profiler-output': $i++; $profiler_output_dir = $command_args[$i]; if (!is_dir($profiler_output_dir)) self :: usage( I18n::_('Invalid profiler output directory "%s": not found'), $profiler_output_dir ); if (!is_writable($profiler_output_dir)) self :: usage( I18n::_('Invalid profiler output directory "%s": not writeable'), $profiler_output_dir ); break; default: if (is_null($listen_address)) { $listen_address = $command_args[$i]; } else self :: usage(I18n::_('Invalid parameter "%s"', $command_args[$i])); break; } } // Check listen address if (is_null($listen_address)) $listen_address = '127.0.0.1:8000'; $parts = explode(':', $listen_address); if (count($parts) > 2) self :: usage( I18n::_('Invalid listen address specify. Must be in format host:port, host or :port.') ); if (empty($parts[0])) { $parts[0] = '0.0.0.0'; } else if (!Check::ip_address($parts[0])) self :: usage( I18n::_('Invalid listen host specified. Must be an IPv4 or IPv6 address.') ); if (count($parts) < 2) $parts[1] = 8000; else if (!Check::tcp_or_udp_port($parts[1])) self :: usage( I18n::_('Invalid listen port specified. Must be a positive integer between 1 and 65535.') ); $listen_address = implode(':', $parts); if (is_null($public_url)) $public_url = sprintf("http://%s:%s", $parts[0]=='0.0.0.0'?'127.0.0.1':$parts[0], $parts[1]); $public_html = App::get('root_directory_path')."/public_html"; chdir($public_html) or self :: fatal_error( 'Fail to enter in the public_html directory of the application (%s).', $public_html ); $args = array( "-S", $listen_address, ); if ($enable_profiler) { $args = array_merge( $args, array( "-d", "xdebug.mode=profile", "-d", "xdebug.profiler_enable=On", "-d", "xdebug.profiler_output_dir=$profiler_output_dir", ) ); } passthru( "EESYPHP_SERVE_URL=$public_url ".PHP_BINARY." ". implode(' ', array_map('escapeshellarg', $args))." index.php", $exit_code ); exit($exit_code); } /** * Helper method to ask user to enter a password * Note: this method use the bash binary and a fatal error will be trigger if it's not available. * @param string|null $prompt The prompt message (optional) * @return string */ public static function prompt_for_password($prompt=null) { // Check bash is available $command = "/usr/bin/env bash -c 'echo OK'"; if (rtrim(shell_exec($command)) !== 'OK') Log::fatal(I18n::_("Can't invoke bash. Can't ask password prompt.")); $command = "/usr/bin/env bash -c 'read -s -p \""; $command .= addslashes($prompt?_($prompt):I18n::_("Please enter password:")); $command .= "\" mypassword && echo \$mypassword'"; $password = rtrim(shell_exec($command)); echo "\n"; return $password; } /** * Handle a fatal error in a CLI command * @param string $error The error message * @param array $extra_args Extra arguments to use to compute the error message using sprintf * @return never */ public static function fatal_error($error, ...$extra_args) { // If extra arguments passed, format error message using sprintf if ($extra_args) { $error = call_user_func_array( 'sprintf', array_merge(array($error), $extra_args) ); } fwrite(STDERR, "FATAL ERROR : $error\n"); exit(1); } }