1. sfCommandApplication.class.php
  2. /** * sfCommandApplication manages the lifecycle of a CLI application. * * @package symfony * @subpackage command * @author Fabien Potencier * @version SVN: $Id: sfCommandApplication.class.php 23218 2009-10-20 20:59:02Z FabianLange $ */
  3. abstract class sfCommandApplication
  4. {
  5. protected
  6. $commandManager = null,
  7. $trace = false,
  8. $verbose = true,
  9. $nowrite = false,
  10. $name = 'UNKNOWN',
  11. $version = 'UNKNOWN',
  12. $tasks = array(),
  13. $currentTask = null,
  14. $dispatcher = null,
  15. $options = array(),
  16. $formatter = null;
  17. /**
  18. * Constructor.
  19. *
  20. * @param sfEventDispatcher $dispatcher A sfEventDispatcher instance
  21. * @param sfFormatter $formatter A sfFormatter instance
  22. * @param array $options An array of options
  23. */
  24. public function __construct(sfEventDispatcher $dispatcher, sfFormatter $formatter = null, $options = array())
  25. {
  26. $this->dispatcher = $dispatcher;
  27. $this->formatter = null === $formatter ? $this->guessBestFormatter(STDOUT) : $formatter;
  28. $this->options = $options;
  29. $this->fixCgi();
  30. $argumentSet = new sfCommandArgumentSet(array(
  31. new sfCommandArgument('task', sfCommandArgument::REQUIRED, 'The task to execute'),
  32. ));
  33. $optionSet = new sfCommandOptionSet(array(
  34. new sfCommandOption('--help', '-H', sfCommandOption::PARAMETER_NONE, 'Display this help message.'),
  35. new sfCommandOption('--quiet', '-q', sfCommandOption::PARAMETER_NONE, 'Do not log messages to standard output.'),
  36. new sfCommandOption('--trace', '-t', sfCommandOption::PARAMETER_NONE, 'Turn on invoke/execute tracing, enable full backtrace.'),
  37. new sfCommandOption('--version', '-V', sfCommandOption::PARAMETER_NONE, 'Display the program version.'),
  38. new sfCommandOption('--color', '', sfCommandOption::PARAMETER_NONE, 'Forces ANSI color output.'),
  39. ));
  40. $this->commandManager = new sfCommandManager($argumentSet, $optionSet);
  41. $this->configure();
  42. $this->registerTasks();
  43. }
  44. /**
  45. * Configures the current command application.
  46. */
  47. abstract public function configure();
  48. /**
  49. * Returns the value of a given option.
  50. *
  51. * @param string $name The option name
  52. *
  53. * @return mixed The option value
  54. */
  55. public function getOption($name)
  56. {
  57. return isset($this->options[$name]) ? $this->options[$name] : null;
  58. }
  59. /**
  60. * Returns the formatter instance.
  61. *
  62. * @return sfFormatter The formatter instance
  63. */
  64. public function getFormatter()
  65. {
  66. return $this->formatter;
  67. }
  68. /**
  69. * Sets the formatter instance.
  70. *
  71. * @param sfFormatter The formatter instance
  72. */
  73. public function setFormatter(sfFormatter $formatter)
  74. {
  75. $this->formatter = $formatter;
  76. foreach ($this->getTasks() as $task)
  77. {
  78. $task->setFormatter($formatter);
  79. }
  80. }
  81. public function clearTasks()
  82. {
  83. $this->tasks = array();
  84. }
  85. /**
  86. * Registers an array of task objects.
  87. *
  88. * If you pass null, this method will register all available tasks.
  89. *
  90. * @param array $tasks An array of tasks
  91. */
  92. public function registerTasks($tasks = null)
  93. {
  94. if (null === $tasks)
  95. {
  96. $tasks = $this->autodiscoverTasks();
  97. }
  98. foreach ($tasks as $task)
  99. {
  100. $this->registerTask($task);
  101. }
  102. }
  103. /**
  104. * Registers a task object.
  105. *
  106. * @param sfTask $task An sfTask object
  107. */
  108. public function registerTask(sfTask $task)
  109. {
  110. if (isset($this->tasks[$task->getFullName()]))
  111. {
  112. throw new sfCommandException(sprintf('The task named "%s" in "%s" task is already registered by the "%s" task.', $task->getFullName(), get_class($task), get_class($this->tasks[$task->getFullName()])));
  113. }
  114. $this->tasks[$task->getFullName()] = $task;
  115. foreach ($task->getAliases() as $alias)
  116. {
  117. if (isset($this->tasks[$alias]))
  118. {
  119. throw new sfCommandException(sprintf('A task named "%s" is already registered.', $alias));
  120. }
  121. $this->tasks[$alias] = $task;
  122. }
  123. }
  124. /**
  125. * Autodiscovers task classes.
  126. *
  127. * @return array An array of tasks instances
  128. */
  129. public function autodiscoverTasks()
  130. {
  131. $tasks = array();
  132. foreach (get_declared_classes() as $class)
  133. {
  134. $r = new ReflectionClass($class);
  135. if ($r->isSubclassOf('sfTask') && !$r->isAbstract())
  136. {
  137. $tasks[] = new $class($this->dispatcher, $this->formatter);
  138. }
  139. }
  140. return $tasks;
  141. }
  142. /**
  143. * Returns all registered tasks.
  144. *
  145. * @return array An array of sfTask objects
  146. */
  147. public function getTasks()
  148. {
  149. return $this->tasks;
  150. }
  151. /**
  152. * Returns a registered task by name or alias.
  153. *
  154. * @param string $name The task name or alias
  155. *
  156. * @return sfTask An sfTask object
  157. */
  158. public function getTask($name)
  159. {
  160. if (!isset($this->tasks[$name]))
  161. {
  162. throw new sfCommandException(sprintf('The task "%s" does not exist.', $name));
  163. }
  164. return $this->tasks[$name];
  165. }
  166. /**
  167. * Runs the current application.
  168. *
  169. * @param mixed $options The command line options
  170. *
  171. * @return integer 0 if everything went fine, or an error code
  172. */
  173. public function run($options = null)
  174. {
  175. $this->handleOptions($options);
  176. $arguments = $this->commandManager->getArgumentValues();
  177. $this->currentTask = $this->getTaskToExecute($arguments['task']);
  178. $ret = $this->currentTask->runFromCLI($this->commandManager, $this->commandOptions);
  179. $this->currentTask = null;
  180. return $ret;
  181. }
  182. /**
  183. * Gets the name of the application.
  184. *
  185. * @return string The application name
  186. */
  187. public function getName()
  188. {
  189. return $this->name;
  190. }
  191. /**
  192. * Sets the application name.
  193. *
  194. * @param string $name The application name
  195. */
  196. public function setName($name)
  197. {
  198. $this->name = $name;
  199. }
  200. /**
  201. * Gets the application version.
  202. *
  203. * @return string The application version
  204. */
  205. public function getVersion()
  206. {
  207. return $this->version;
  208. }
  209. /**
  210. * Sets the application version.
  211. *
  212. * @param string $version The application version
  213. */
  214. public function setVersion($version)
  215. {
  216. $this->version = $version;
  217. }
  218. /**
  219. * Returns the long version of the application.
  220. *
  221. * @return string The long application version
  222. */
  223. public function getLongVersion()
  224. {
  225. return sprintf('%s version %s', $this->getName(), $this->formatter->format($this->getVersion(), 'INFO'))."\n";
  226. }
  227. /**
  228. * Returns whether the application must be verbose.
  229. *
  230. * @return Boolean true if the application must be verbose, false otherwise
  231. */
  232. public function isVerbose()
  233. {
  234. return $this->verbose;
  235. }
  236. /**
  237. * Returns whether the application must activate the trace.
  238. *
  239. * @return Boolean true if the application must activate the trace, false otherwise
  240. */
  241. public function withTrace()
  242. {
  243. return $this->trace;
  244. }
  245. /**
  246. * Outputs a help message for the current application.
  247. */
  248. public function help()
  249. {
  250. $messages = array(
  251. $this->formatter->format('Usage:', 'COMMENT'),
  252. sprintf(" %s [options] task_name [arguments]\n", $this->getName()),
  253. $this->formatter->format('Options:', 'COMMENT'),
  254. );
  255. foreach ($this->commandManager->getOptionSet()->getOptions() as $option)
  256. {
  257. $messages[] = sprintf(' %-24s %s %s',
  258. $this->formatter->format('--'.$option->getName(), 'INFO'),
  259. $option->getShortcut() ? $this->formatter->format('-'.$option->getShortcut(), 'INFO') : ' ',
  260. $option->getHelp()
  261. );
  262. }
  263. $this->dispatcher->notify(new sfEvent($this, 'command.log', $messages));
  264. }
  265. /**
  266. * Parses and handles command line options.
  267. *
  268. * @param mixed $options The command line options
  269. */
  270. protected function handleOptions($options = null)
  271. {
  272. $this->commandManager->process($options);
  273. $this->commandOptions = $options;
  274. // the order of option processing matters
  275. if ($this->commandManager->getOptionSet()->hasOption('color') && false !== $this->commandManager->getOptionValue('color'))
  276. {
  277. $this->setFormatter(new sfAnsiColorFormatter());
  278. }
  279. if ($this->commandManager->getOptionSet()->hasOption('quiet') && false !== $this->commandManager->getOptionValue('quiet'))
  280. {
  281. $this->verbose = false;
  282. }
  283. if ($this->commandManager->getOptionSet()->hasOption('trace') && false !== $this->commandManager->getOptionValue('trace'))
  284. {
  285. $this->verbose = true;
  286. $this->trace = true;
  287. }
  288. if ($this->commandManager->getOptionSet()->hasOption('help') && false !== $this->commandManager->getOptionValue('help'))
  289. {
  290. $this->help();
  291. exit(0);
  292. }
  293. if ($this->commandManager->getOptionSet()->hasOption('version') && false !== $this->commandManager->getOptionValue('version'))
  294. {
  295. echo $this->getLongVersion();
  296. exit(0);
  297. }
  298. }
  299. /**
  300. * Renders an exception.
  301. *
  302. * @param Exception $e An exception object
  303. */
  304. public function renderException($e)
  305. {
  306. $title = sprintf(' [%s] ', get_class($e));
  307. $len = $this->strlen($title);
  308. $lines = array();
  309. foreach (explode("\n", $e->getMessage()) as $line)
  310. {
  311. $lines[] = sprintf(' %s ', $line);
  312. $len = max($this->strlen($line) + 4, $len);
  313. }
  314. $messages = array(str_repeat(' ', $len));
  315. if ($this->trace)
  316. {
  317. $messages[] = $title.str_repeat(' ', $len - $this->strlen($title));
  318. }
  319. foreach ($lines as $line)
  320. {
  321. $messages[] = $line.str_repeat(' ', $len - $this->strlen($line));
  322. }
  323. $messages[] = str_repeat(' ', $len);
  324. fwrite(STDERR, "\n");
  325. foreach ($messages as $message)
  326. {
  327. fwrite(STDERR, $this->formatter->format($message, 'ERROR', STDERR)."\n");
  328. }
  329. fwrite(STDERR, "\n");
  330. if (null !== $this->currentTask && $e instanceof sfCommandArgumentsException)
  331. {
  332. fwrite(STDERR, $this->formatter->format(sprintf($this->currentTask->getSynopsis(), $this->getName()), 'INFO', STDERR)."\n");
  333. fwrite(STDERR, "\n");
  334. }
  335. if ($this->trace)
  336. {
  337. fwrite(STDERR, $this->formatter->format("Exception trace:\n", 'COMMENT'));
  338. // exception related properties
  339. $trace = $e->getTrace();
  340. array_unshift($trace, array(
  341. 'function' => '',
  342. 'file' => $e->getFile() != null ? $e->getFile() : 'n/a',
  343. 'line' => $e->getLine() != null ? $e->getLine() : 'n/a',
  344. 'args' => array(),
  345. ));
  346. for ($i = 0, $count = count($trace); $i < $count; $i++)
  347. {
  348. $class = isset($trace[$i]['class']) ? $trace[$i]['class'] : '';
  349. $type = isset($trace[$i]['type']) ? $trace[$i]['type'] : '';
  350. $function = $trace[$i]['function'];
  351. $file = isset($trace[$i]['file']) ? $trace[$i]['file'] : 'n/a';
  352. $line = isset($trace[$i]['line']) ? $trace[$i]['line'] : 'n/a';
  353. fwrite(STDERR, sprintf(" %s%s%s at %s:%s\n", $class, $type, $function, $this->formatter->format($file, 'INFO', STDERR), $this->formatter->format($line, 'INFO', STDERR)));
  354. }
  355. fwrite(STDERR, "\n");
  356. }
  357. }
  358. /**
  359. * Gets a task from a task name or a shortcut.
  360. *
  361. * @param string $name The task name or a task shortcut
  362. *
  363. * @return sfTask A sfTask object
  364. */
  365. public function getTaskToExecute($name)
  366. {
  367. // namespace
  368. if (false !== $pos = strpos($name, ':'))
  369. {
  370. $namespace = substr($name, 0, $pos);
  371. $name = substr($name, $pos + 1);
  372. $namespaces = array();
  373. foreach ($this->tasks as $task)
  374. {
  375. if ($task->getNamespace() && !in_array($task->getNamespace(), $namespaces))
  376. {
  377. $namespaces[] = $task->getNamespace();
  378. }
  379. }
  380. $abbrev = $this->getAbbreviations($namespaces);
  381. if (!isset($abbrev[$namespace]))
  382. {
  383. throw new sfCommandException(sprintf('There are no tasks defined in the "%s" namespace.', $namespace));
  384. }
  385. else if (count($abbrev[$namespace]) > 1)
  386. {
  387. throw new sfCommandException(sprintf('The namespace "%s" is ambiguous (%s).', $namespace, implode(', ', $abbrev[$namespace])));
  388. }
  389. else
  390. {
  391. $namespace = $abbrev[$namespace][0];
  392. }
  393. }
  394. else
  395. {
  396. $namespace = '';
  397. }
  398. // name
  399. $tasks = array();
  400. foreach ($this->tasks as $taskName => $task)
  401. {
  402. if ($taskName == $task->getFullName() && $task->getNamespace() == $namespace)
  403. {
  404. $tasks[] = $task->getName();
  405. }
  406. }
  407. $abbrev = $this->getAbbreviations($tasks);
  408. if (isset($abbrev[$name]) && count($abbrev[$name]) == 1)
  409. {
  410. return $this->getTask($namespace ? $namespace.':'.$abbrev[$name][0] : $abbrev[$name][0]);
  411. }
  412. // aliases
  413. $aliases = array();
  414. foreach ($this->tasks as $taskName => $task)
  415. {
  416. if ($taskName == $task->getFullName())
  417. {
  418. foreach ($task->getAliases() as $alias)
  419. {
  420. $aliases[] = $alias;
  421. }
  422. }
  423. }
  424. $abbrev = $this->getAbbreviations($aliases);
  425. $fullName = $namespace ? $namespace.':'.$name : $name;
  426. if (!isset($abbrev[$fullName]))
  427. {
  428. throw new sfCommandException(sprintf('Task "%s" is not defined.', $fullName));
  429. }
  430. else if (count($abbrev[$fullName]) > 1)
  431. {
  432. throw new sfCommandException(sprintf('Task "%s" is ambiguous (%s).', $fullName, implode(', ', $abbrev[$fullName])));
  433. }
  434. else
  435. {
  436. return $this->getTask($abbrev[$fullName][0]);
  437. }
  438. }
  439. protected function strlen($string)
  440. {
  441. return function_exists('mb_strlen') ? mb_strlen($string) : strlen($string);
  442. }
  443. /**
  444. * Fixes php behavior if using cgi php.
  445. *
  446. * @see http://www.sitepoint.com/article/php-command-line-1/3
  447. */
  448. protected function fixCgi()
  449. {
  450. // handle output buffering
  451. @ob_end_flush();
  452. ob_implicit_flush(true);
  453. // PHP ini settings
  454. set_time_limit(0);
  455. ini_set('track_errors', true);
  456. ini_set('html_errors', false);
  457. ini_set('magic_quotes_runtime', false);
  458. if (false === strpos(PHP_SAPI, 'cgi'))
  459. {
  460. return;
  461. }
  462. // define stream constants
  463. define('STDIN', fopen('php://stdin', 'r'));
  464. define('STDOUT', fopen('php://stdout', 'w'));
  465. define('STDERR', fopen('php://stderr', 'w'));
  466. // change directory
  467. if (isset($_SERVER['PWD']))
  468. {
  469. chdir($_SERVER['PWD']);
  470. }
  471. // close the streams on script termination
  472. register_shutdown_function(create_function('', 'fclose(STDIN); fclose(STDOUT); fclose(STDERR); return true;'));
  473. }
  474. /**
  475. * Returns an array of possible abbreviations given a set of names.
  476. *
  477. * @see Text::Abbrev perl module for the algorithm
  478. */
  479. protected function getAbbreviations($names)
  480. {
  481. $abbrevs = array();
  482. $table = array();
  483. foreach ($names as $name)
  484. {
  485. for ($len = strlen($name) - 1; $len > 0; --$len)
  486. {
  487. $abbrev = substr($name, 0, $len);
  488. if (!array_key_exists($abbrev, $table))
  489. {
  490. $table[$abbrev] = 1;
  491. }
  492. else
  493. {
  494. ++$table[$abbrev];
  495. }
  496. $seen = $table[$abbrev];
  497. if ($seen == 1)
  498. {
  499. // We're the first word so far to have this abbreviation.
  500. $abbrevs[$abbrev] = array($name);
  501. }
  502. else if ($seen == 2)
  503. {
  504. // We're the second word to have this abbreviation, so we can't use it.
  505. // unset($abbrevs[$abbrev]);
  506. $abbrevs[$abbrev][] = $name;
  507. }
  508. else
  509. {
  510. // We're the third word to have this abbreviation, so skip to the next word.
  511. continue;
  512. }
  513. }
  514. }
  515. // Non-abbreviations always get entered, even if they aren't unique
  516. foreach ($names as $name)
  517. {
  518. $abbrevs[$name] = array($name);
  519. }
  520. return $abbrevs;
  521. }
  522. /**
  523. * Returns true if the stream supports colorization.
  524. *
  525. * Colorization is disabled if not supported by the stream:
  526. *
  527. * - windows without ansicon
  528. * - non tty consoles
  529. *
  530. * @param mixed $stream A stream
  531. *
  532. * @return Boolean true if the stream supports colorization, false otherwise
  533. */
  534. protected function isStreamSupportsColors($stream)
  535. {
  536. if (DIRECTORY_SEPARATOR == '\\')
  537. {
  538. return false !== getenv('ANSICON');
  539. }
  540. else
  541. {
  542. return function_exists('posix_isatty') && @posix_isatty($stream);
  543. }
  544. }
  545. /**
  546. * Guesses the best formatter for the stream.
  547. *
  548. * @param mixed $stream A stream
  549. *
  550. * @return sfFormatter A formatter instance
  551. */
  552. protected function guessBestFormatter($stream)
  553. {
  554. return $this->isStreamSupportsColors($stream) ? new sfAnsiColorFormatter() : new sfFormatter();
  555. }
  556. }

Debug toolbar