Command.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422
  1. <?php
  2. /**
  3. * Copyright (c) 2009 - 2011, RealDolmen
  4. * All rights reserved.
  5. *
  6. * Redistribution and use in source and binary forms, with or without
  7. * modification, are permitted provided that the following conditions are met:
  8. * * Redistributions of source code must retain the above copyright
  9. * notice, this list of conditions and the following disclaimer.
  10. * * Redistributions in binary form must reproduce the above copyright
  11. * notice, this list of conditions and the following disclaimer in the
  12. * documentation and/or other materials provided with the distribution.
  13. * * Neither the name of RealDolmen nor the
  14. * names of its contributors may be used to endorse or promote products
  15. * derived from this software without specific prior written permission.
  16. *
  17. * THIS SOFTWARE IS PROVIDED BY RealDolmen ''AS IS'' AND ANY
  18. * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  19. * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  20. * DISCLAIMED. IN NO EVENT SHALL RealDolmen BE LIABLE FOR ANY
  21. * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  22. * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  23. * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
  24. * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  25. * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  26. * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  27. *
  28. * @category Zend
  29. * @package Zend_Service_Console
  30. * @version $Id$
  31. * @copyright Copyright (c) 2009 - 2011, RealDolmen (http://www.realdolmen.com)
  32. * @license http://phpazure.codeplex.com/license
  33. */
  34. /**
  35. * @category Zend
  36. * @package Zend_Service_Console
  37. * @copyright Copyright (c) 2009 - 2011, RealDolmen (http://www.realdolmen.com)
  38. * @license http://phpazure.codeplex.com/license
  39. */
  40. class Zend_Service_Console_Command
  41. {
  42. /**
  43. * The handler.
  44. *
  45. * @var array
  46. */
  47. protected $_handler;
  48. /**
  49. * Gets the handler.
  50. *
  51. * @return array
  52. */
  53. public function getHandler()
  54. {
  55. return $this->_handler;
  56. }
  57. /**
  58. * Sets the handler.
  59. *
  60. * @param array $handler
  61. * @return Zend_Service_Console_Command
  62. */
  63. public function setHandler($handler)
  64. {
  65. $this->_handler = $handler;
  66. return $this;
  67. }
  68. /**
  69. * Replaces PHP's error handler
  70. *
  71. * @param mixed $errno
  72. * @param mixed $errstr
  73. * @param mixed $errfile
  74. * @param mixed $errline
  75. */
  76. public static function phpstderr($errno, $errstr, $errfile, $errline)
  77. {
  78. self::stderr($errno . ': Error in ' . $errfile . ':' . $errline . ' - ' . $errstr);
  79. }
  80. /**
  81. * Replaces PHP's exception handler
  82. *
  83. * @param Exception $exception
  84. */
  85. public static function phpstdex($exception)
  86. {
  87. self::stderr('Error: ' . $exception->getMessage());
  88. }
  89. /**
  90. * Writes output to STDERR, followed by a newline (optional)
  91. *
  92. * @param string $errorMessage
  93. * @param string $newLine
  94. */
  95. public static function stderr($errorMessage, $newLine = true)
  96. {
  97. if (error_reporting() === 0) {
  98. return;
  99. }
  100. file_put_contents('php://stderr', $errorMessage . ($newLine ? "\r\n" : ''));
  101. }
  102. /**
  103. * Bootstrap the shell command.
  104. *
  105. * @param array $argv PHP argument values.
  106. */
  107. public static function bootstrap($argv)
  108. {
  109. // Abort bootstrapping depending on the MICROSOFT_CONSOLE_COMMAND_HOST constant.
  110. if (defined('MICROSOFT_CONSOLE_COMMAND_HOST') && strtolower(MICROSOFT_CONSOLE_COMMAND_HOST) != 'console') {
  111. return;
  112. }
  113. // Replace error handler
  114. set_error_handler(array('Zend_Service_Console_Command', 'phpstderr'));
  115. set_exception_handler(array('Zend_Service_Console_Command', 'phpstdex'));
  116. // Build the application model
  117. $model = self::_buildModel();
  118. // Find a class that corresponds to the $argv[0] script name
  119. $requiredHandlerName = str_replace('.bat', '', str_replace('.sh', '', str_replace('.php', '', strtolower(basename($argv[0])))));
  120. $handler = null;
  121. foreach ($model as $possibleHandler) {
  122. if ($possibleHandler->handler == strtolower($requiredHandlerName)) {
  123. $handler = $possibleHandler;
  124. break;
  125. }
  126. }
  127. if (is_null($handler)) {
  128. self::stderr("No class found that implements handler '" . $requiredHandlerName . "'. Create a class that is named '" . $requiredHandlerName . "' and extends Zend_Service_Console_Command or is decorated with a docblock comment '@command-handler " . $requiredHandlerName . "'. Make sure it is loaded either through an autoloader or explicitly using require_once().");
  129. die();
  130. }
  131. // Find a method that matches the command name
  132. $command = null;
  133. foreach ($handler->commands as $possibleCommand) {
  134. if (in_array(strtolower(isset($argv[1]) ? $argv[1] : '<default>'), $possibleCommand->aliases)) {
  135. $command = $possibleCommand;
  136. break;
  137. }
  138. }
  139. if (is_null($command)) {
  140. $commandName = (isset($argv[1]) ? $argv[1] : '<default>');
  141. self::stderr("No method found that implements command " . $commandName . ". Create a method in class '" . $handler->class . "' that is named '" . strtolower($commandName) . "Command' or is decorated with a docblock comment '@command-name " . $commandName . "'.");
  142. die();
  143. }
  144. // Parse parameter values
  145. $parameterValues = array();
  146. $missingParameterValues = array();
  147. $parameterInputs = array_splice($argv, 2);
  148. foreach ($command->parameters as $parameter) {
  149. // Default value: null
  150. $value = null;
  151. // Consult value providers for value. First one wins.
  152. foreach ($parameter->valueproviders as $valueProviderName) {
  153. if (!class_exists($valueProviderName)) {
  154. $valueProviderName = 'Zend_Service_Console_Command_ParameterSource_' . $valueProviderName;
  155. }
  156. $valueProvider = new $valueProviderName();
  157. $value = $valueProvider->getValueForParameter($parameter, $parameterInputs);
  158. if (!is_null($value)) {
  159. break;
  160. }
  161. }
  162. if (is_null($value) && $parameter->required) {
  163. $missingParameterValues[] = $parameter->aliases[0];
  164. } else if (is_null($value)) {
  165. $value = $parameter->defaultvalue;
  166. }
  167. // Set value
  168. $parameterValues[] = $value;
  169. $argvValues[$parameter->aliases[0]] = $value;
  170. }
  171. // Mising parameters?
  172. if (count($missingParameterValues) > 0) {
  173. self::stderr("Some parameters are missing:\r\n" . implode("\r\n", $missingParameterValues));
  174. die();
  175. }
  176. // Supply argv in a nice way
  177. $parameterValues['argv'] = $parameterInputs;
  178. // Run the command
  179. $className = $handler->class;
  180. $classInstance = new $className();
  181. $classInstance->setHandler($handler);
  182. call_user_func_array(array($classInstance, $command->method), $parameterValues);
  183. // Restore error handler
  184. restore_exception_handler();
  185. restore_error_handler();
  186. }
  187. /**
  188. * Builds the handler model.
  189. *
  190. * @return array
  191. */
  192. protected static function _buildModel()
  193. {
  194. $model = array();
  195. $classes = get_declared_classes();
  196. foreach ($classes as $class) {
  197. $type = new ReflectionClass($class);
  198. $handlers = self::_findValueForDocComment('@command-handler', $type->getDocComment());
  199. if (count($handlers) == 0 && $type->isSubclassOf('Zend_Service_Console_Command')) {
  200. // Fallback: if the class extends Zend_Service_Console_Command, register it as
  201. // a command handler.
  202. $handlers[] = $class;
  203. }
  204. $handlerDescriptions = self::_findValueForDocComment('@command-handler-description', $type->getDocComment());
  205. $handlerHeaders = self::_findValueForDocComment('@command-handler-header', $type->getDocComment());
  206. $handlerFooters = self::_findValueForDocComment('@command-handler-footer', $type->getDocComment());
  207. for ($hi = 0; $hi < count($handlers); $hi++) {
  208. $handler = $handlers[$hi];
  209. $handlerDescription = isset($handlerDescriptions[$hi]) ? $handlerDescriptions[$hi] : isset($handlerDescriptions[0]) ? $handlerDescriptions[0] : '';
  210. $handlerDescription = str_replace('\r\n', "\r\n", $handlerDescription);
  211. $handlerDescription = str_replace('\n', "\n", $handlerDescription);
  212. $handlerModel = (object)array(
  213. 'handler' => strtolower($handler),
  214. 'description' => $handlerDescription,
  215. 'headers' => $handlerHeaders,
  216. 'footers' => $handlerFooters,
  217. 'class' => $class,
  218. 'commands' => array()
  219. );
  220. $methods = $type->getMethods();
  221. foreach ($methods as $method) {
  222. $commands = self::_findValueForDocComment('@command-name', $method->getDocComment());
  223. if (substr($method->getName(), -7) == 'Command' && !in_array(substr($method->getName(), 0, -7), $commands)) {
  224. // Fallback: if the method is named <commandname>Command,
  225. // register it as a command.
  226. $commands[] = substr($method->getName(), 0, -7);
  227. }
  228. for ($x = 0; $x < count($commands); $x++) {
  229. $commands[$x] = strtolower($commands[$x]);
  230. }
  231. $commands = array_unique($commands);
  232. $commandDescriptions = self::_findValueForDocComment('@command-description', $method->getDocComment());
  233. $commandExamples = self::_findValueForDocComment('@command-example', $method->getDocComment());
  234. if (count($commands) > 0) {
  235. $command = $commands[0];
  236. $commandDescription = isset($commandDescriptions[0]) ? $commandDescriptions[0] : '';
  237. $commandModel = (object)array(
  238. 'command' => $command,
  239. 'aliases' => $commands,
  240. 'description' => $commandDescription,
  241. 'examples' => $commandExamples,
  242. 'class' => $class,
  243. 'method' => $method->getName(),
  244. 'parameters' => array()
  245. );
  246. $parameters = $method->getParameters();
  247. $parametersFor = self::_findValueForDocComment('@command-parameter-for', $method->getDocComment());
  248. for ($pi = 0; $pi < count($parameters); $pi++) {
  249. // Initialize
  250. $parameter = $parameters[$pi];
  251. $parameterFor = null;
  252. $parameterForDefaultValue = null;
  253. // Is it a "catch-all" parameter?
  254. if ($parameter->getName() == 'argv') {
  255. continue;
  256. }
  257. // Find the $parametersFor with the same name defined
  258. foreach ($parametersFor as $possibleParameterFor) {
  259. $possibleParameterFor = explode(' ', $possibleParameterFor, 4);
  260. if ($possibleParameterFor[0] == '$' . $parameter->getName()) {
  261. $parameterFor = $possibleParameterFor;
  262. break;
  263. }
  264. }
  265. if (is_null($parameterFor)) {
  266. die('@command-parameter-for missing for parameter $' . $parameter->getName());
  267. }
  268. if (is_null($parameterForDefaultValue) && $parameter->isOptional()) {
  269. $parameterForDefaultValue = $parameter->getDefaultValue();
  270. }
  271. $parameterModel = (object)array(
  272. 'name' => '$' . $parameter->getName(),
  273. 'defaultvalue' => $parameterForDefaultValue,
  274. 'valueproviders' => explode('|', $parameterFor[1]),
  275. 'aliases' => explode('|', $parameterFor[2]),
  276. 'description' => (isset($parameterFor[3]) ? $parameterFor[3] : ''),
  277. 'required' => (isset($parameterFor[3]) ? strpos(strtolower($parameterFor[3]), 'required') !== false && strpos(strtolower($parameterFor[3]), 'required if') === false : false),
  278. );
  279. // Add to model
  280. $commandModel->parameters[] = $parameterModel;
  281. }
  282. // Add to model
  283. $handlerModel->commands[] = $commandModel;
  284. }
  285. }
  286. // Add to model
  287. $model[] = $handlerModel;
  288. }
  289. }
  290. return $model;
  291. }
  292. /**
  293. * Finds the value for a specific docComment.
  294. *
  295. * @param string $docCommentName Comment name
  296. * @param unknown_type $docComment Comment object
  297. * @return array
  298. */
  299. protected static function _findValueForDocComment($docCommentName, $docComment)
  300. {
  301. $returnValue = array();
  302. $commentLines = explode("\n", $docComment);
  303. foreach ($commentLines as $commentLine) {
  304. if (strpos($commentLine, $docCommentName . ' ') !== false) {
  305. $returnValue[] = trim(substr($commentLine, strpos($commentLine, $docCommentName) + strlen($docCommentName) + 1));
  306. }
  307. }
  308. return $returnValue;
  309. }
  310. /**
  311. * Display information on an object
  312. *
  313. * @param object $object Object
  314. * @param array $propertiesToDump Property names to display
  315. */
  316. protected function _displayObjectInformation($object, $propertiesToDump = array())
  317. {
  318. foreach ($propertiesToDump as $property) {
  319. printf('%-16s: %s' . "\r\n", $property, $object->$property);
  320. }
  321. printf("\r\n");
  322. }
  323. /**
  324. * Displays the help information.
  325. *
  326. * @command-name <default>
  327. * @command-name -h
  328. * @command-name -help
  329. * @command-description Displays the current help information.
  330. */
  331. public function helpCommand() {
  332. $handler = $this->getHandler();
  333. $newline = "\r\n";
  334. if (count($handler->headers) > 0) {
  335. foreach ($handler->headers as $header) {
  336. printf('%s%s', $header, $newline);
  337. }
  338. printf($newline);
  339. }
  340. printf('%s%s', $handler->description, $newline);
  341. printf($newline);
  342. printf('Available commands:%s', $newline);
  343. foreach ($handler->commands as $command) {
  344. $description = str_split($command->description, 50);
  345. printf(' %-25s %s%s', implode(', ', $command->aliases), $description[0], $newline);
  346. for ($di = 1; $di < count($description); $di++) {
  347. printf(' %-25s %s%s', '', $description[$di], $newline);
  348. }
  349. printf($newline);
  350. if (count($command->parameters) > 0) {
  351. foreach ($command->parameters as $parameter) {
  352. $description = str_split($parameter->description, 50);
  353. printf(' %-23s %s%s', implode(', ', $parameter->aliases), $description[0], $newline);
  354. for ($di = 1; $di < count($description); $di++) {
  355. printf(' %-23s %s%s', '', $description[$di], $newline);
  356. }
  357. printf($newline);
  358. }
  359. }
  360. printf($newline);
  361. if (count($command->examples) > 0) {
  362. printf(' Example usage:%s', $newline);
  363. foreach ($command->examples as $example) {
  364. printf(' %s%s', $example, $newline);
  365. }
  366. printf($newline);
  367. }
  368. }
  369. if (count($handler->footers) > 0) {
  370. printf($newline);
  371. foreach ($handler->footers as $footer) {
  372. printf('%s%s', $footer, $newline);
  373. }
  374. printf($newline);
  375. }
  376. }
  377. }