1: <?php
2:
3: 4: 5: 6: 7: 8: 9: 10:
11:
12: namespace Symfony\Component\Console;
13:
14: use Symfony\Component\Console\Input\InputInterface;
15: use Symfony\Component\Console\Input\ArgvInput;
16: use Symfony\Component\Console\Input\ArrayInput;
17: use Symfony\Component\Console\Input\InputDefinition;
18: use Symfony\Component\Console\Input\InputOption;
19: use Symfony\Component\Console\Input\InputArgument;
20: use Symfony\Component\Console\Output\OutputInterface;
21: use Symfony\Component\Console\Output\Output;
22: use Symfony\Component\Console\Output\ConsoleOutput;
23: use Symfony\Component\Console\Output\ConsoleOutputInterface;
24: use Symfony\Component\Console\Command\Command;
25: use Symfony\Component\Console\Command\HelpCommand;
26: use Symfony\Component\Console\Command\ListCommand;
27: use Symfony\Component\Console\Helper\HelperSet;
28: use Symfony\Component\Console\Helper\FormatterHelper;
29: use Symfony\Component\Console\Helper\DialogHelper;
30:
31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47:
48: class Application
49: {
50: private $commands;
51: private $wantHelps = false;
52: private $runningCommand;
53: private $name;
54: private $version;
55: private $catchExceptions;
56: private $autoExit;
57: private $definition;
58: private $helperSet;
59:
60: 61: 62: 63: 64: 65: 66: 67:
68: public function __construct($name = 'UNKNOWN', $version = 'UNKNOWN')
69: {
70: $this->name = $name;
71: $this->version = $version;
72: $this->catchExceptions = true;
73: $this->autoExit = true;
74: $this->commands = array();
75: $this->helperSet = $this->getDefaultHelperSet();
76: $this->definition = $this->getDefaultInputDefinition();
77:
78: foreach ($this->getDefaultCommands() as $command) {
79: $this->add($command);
80: }
81: }
82:
83: 84: 85: 86: 87: 88: 89: 90: 91: 92: 93: 94:
95: public function run(InputInterface $input = null, OutputInterface $output = null)
96: {
97: if (null === $input) {
98: $input = new ArgvInput();
99: }
100:
101: if (null === $output) {
102: $output = new ConsoleOutput();
103: }
104:
105: try {
106: $statusCode = $this->doRun($input, $output);
107: } catch (\Exception $e) {
108: if (!$this->catchExceptions) {
109: throw $e;
110: }
111:
112: if ($output instanceof ConsoleOutputInterface) {
113: $this->renderException($e, $output->getErrorOutput());
114: } else {
115: $this->renderException($e, $output);
116: }
117: $statusCode = $e->getCode();
118:
119: $statusCode = is_numeric($statusCode) && $statusCode ? $statusCode : 1;
120: }
121:
122: if ($this->autoExit) {
123: if ($statusCode > 255) {
124: $statusCode = 255;
125: }
126:
127: exit($statusCode);
128:
129: }
130:
131: return $statusCode;
132: }
133:
134: 135: 136: 137: 138: 139: 140: 141:
142: public function doRun(InputInterface $input, OutputInterface $output)
143: {
144: $name = $this->getCommandName($input);
145:
146: if (true === $input->hasParameterOption(array('--ansi'))) {
147: $output->setDecorated(true);
148: } elseif (true === $input->hasParameterOption(array('--no-ansi'))) {
149: $output->setDecorated(false);
150: }
151:
152: if (true === $input->hasParameterOption(array('--help', '-h'))) {
153: if (!$name) {
154: $name = 'help';
155: $input = new ArrayInput(array('command' => 'help'));
156: } else {
157: $this->wantHelps = true;
158: }
159: }
160:
161: if (true === $input->hasParameterOption(array('--no-interaction', '-n'))) {
162: $input->setInteractive(false);
163: }
164:
165: if (function_exists('posix_isatty') && $this->getHelperSet()->has('dialog')) {
166: $inputStream = $this->getHelperSet()->get('dialog')->getInputStream();
167: if (!posix_isatty($inputStream)) {
168: $input->setInteractive(false);
169: }
170: }
171:
172: if (true === $input->hasParameterOption(array('--quiet', '-q'))) {
173: $output->setVerbosity(OutputInterface::VERBOSITY_QUIET);
174: } elseif (true === $input->hasParameterOption(array('--verbose', '-v'))) {
175: $output->setVerbosity(OutputInterface::VERBOSITY_VERBOSE);
176: }
177:
178: if (true === $input->hasParameterOption(array('--version', '-V'))) {
179: $output->writeln($this->getLongVersion());
180:
181: return 0;
182: }
183:
184: if (!$name) {
185: $name = 'list';
186: $input = new ArrayInput(array('command' => 'list'));
187: }
188:
189:
190: $command = $this->find($name);
191:
192: $this->runningCommand = $command;
193: $statusCode = $command->run($input, $output);
194: $this->runningCommand = null;
195:
196: return is_numeric($statusCode) ? $statusCode : 0;
197: }
198:
199: 200: 201: 202: 203: 204: 205:
206: public function setHelperSet(HelperSet $helperSet)
207: {
208: $this->helperSet = $helperSet;
209: }
210:
211: 212: 213: 214: 215: 216: 217:
218: public function getHelperSet()
219: {
220: return $this->helperSet;
221: }
222:
223: 224: 225: 226: 227:
228: public function getDefinition()
229: {
230: return $this->definition;
231: }
232:
233: 234: 235: 236: 237:
238: public function getHelp()
239: {
240: $messages = array(
241: $this->getLongVersion(),
242: '',
243: '<comment>Usage:</comment>',
244: sprintf(" [options] command [arguments]\n"),
245: '<comment>Options:</comment>',
246: );
247:
248: foreach ($this->getDefinition()->getOptions() as $option) {
249: $messages[] = sprintf(' %-29s %s %s',
250: '<info>--'.$option->getName().'</info>',
251: $option->getShortcut() ? '<info>-'.$option->getShortcut().'</info>' : ' ',
252: $option->getDescription()
253: );
254: }
255:
256: return implode(PHP_EOL, $messages);
257: }
258:
259: 260: 261: 262: 263: 264: 265:
266: public function setCatchExceptions($boolean)
267: {
268: $this->catchExceptions = (Boolean) $boolean;
269: }
270:
271: 272: 273: 274: 275: 276: 277:
278: public function setAutoExit($boolean)
279: {
280: $this->autoExit = (Boolean) $boolean;
281: }
282:
283: 284: 285: 286: 287: 288: 289:
290: public function getName()
291: {
292: return $this->name;
293: }
294:
295: 296: 297: 298: 299: 300: 301:
302: public function setName($name)
303: {
304: $this->name = $name;
305: }
306:
307: 308: 309: 310: 311: 312: 313:
314: public function getVersion()
315: {
316: return $this->version;
317: }
318:
319: 320: 321: 322: 323: 324: 325:
326: public function setVersion($version)
327: {
328: $this->version = $version;
329: }
330:
331: 332: 333: 334: 335: 336: 337:
338: public function getLongVersion()
339: {
340: if ('UNKNOWN' !== $this->getName() && 'UNKNOWN' !== $this->getVersion()) {
341: return sprintf('<info>%s</info> version <comment>%s</comment>', $this->getName(), $this->getVersion());
342: }
343:
344: return '<info>Console Tool</info>';
345: }
346:
347: 348: 349: 350: 351: 352: 353: 354: 355:
356: public function register($name)
357: {
358: return $this->add(new Command($name));
359: }
360:
361: 362: 363: 364: 365: 366: 367:
368: public function addCommands(array $commands)
369: {
370: foreach ($commands as $command) {
371: $this->add($command);
372: }
373: }
374:
375: 376: 377: 378: 379: 380: 381: 382: 383: 384: 385:
386: public function add(Command $command)
387: {
388: $command->setApplication($this);
389:
390: if (!$command->isEnabled()) {
391: $command->setApplication(null);
392:
393: return;
394: }
395:
396: $this->commands[$command->getName()] = $command;
397:
398: foreach ($command->getAliases() as $alias) {
399: $this->commands[$alias] = $command;
400: }
401:
402: return $command;
403: }
404:
405: 406: 407: 408: 409: 410: 411: 412: 413: 414: 415:
416: public function get($name)
417: {
418: if (!isset($this->commands[$name])) {
419: throw new \InvalidArgumentException(sprintf('The command "%s" does not exist.', $name));
420: }
421:
422: $command = $this->commands[$name];
423:
424: if ($this->wantHelps) {
425: $this->wantHelps = false;
426:
427: $helpCommand = $this->get('help');
428: $helpCommand->setCommand($command);
429:
430: return $helpCommand;
431: }
432:
433: return $command;
434: }
435:
436: 437: 438: 439: 440: 441: 442: 443: 444:
445: public function has($name)
446: {
447: return isset($this->commands[$name]);
448: }
449:
450: 451: 452: 453: 454: 455: 456:
457: public function getNamespaces()
458: {
459: $namespaces = array();
460: foreach ($this->commands as $command) {
461: $namespaces[] = $this->extractNamespace($command->getName());
462:
463: foreach ($command->getAliases() as $alias) {
464: $namespaces[] = $this->extractNamespace($alias);
465: }
466: }
467:
468: return array_values(array_unique(array_filter($namespaces)));
469: }
470:
471: 472: 473: 474: 475: 476: 477: 478: 479:
480: public function findNamespace($namespace)
481: {
482: $allNamespaces = array();
483: foreach ($this->getNamespaces() as $n) {
484: $allNamespaces[$n] = explode(':', $n);
485: }
486:
487: $found = array();
488: foreach (explode(':', $namespace) as $i => $part) {
489: $abbrevs = static::getAbbreviations(array_unique(array_values(array_filter(array_map(function ($p) use ($i) { return isset($p[$i]) ? $p[$i] : ''; }, $allNamespaces)))));
490:
491: if (!isset($abbrevs[$part])) {
492: $message = sprintf('There are no commands defined in the "%s" namespace.', $namespace);
493:
494: if (1 <= $i) {
495: $part = implode(':', $found).':'.$part;
496: }
497:
498: if ($alternatives = $this->findAlternativeNamespace($part, $abbrevs)) {
499: $message .= "\n\nDid you mean one of these?\n ";
500: $message .= implode("\n ", $alternatives);
501: }
502:
503: throw new \InvalidArgumentException($message);
504: }
505:
506: if (count($abbrevs[$part]) > 1) {
507: throw new \InvalidArgumentException(sprintf('The namespace "%s" is ambiguous (%s).', $namespace, $this->getAbbreviationSuggestions($abbrevs[$part])));
508: }
509:
510: $found[] = $abbrevs[$part][0];
511: }
512:
513: return implode(':', $found);
514: }
515:
516: 517: 518: 519: 520: 521: 522: 523: 524: 525: 526: 527: 528: 529:
530: public function find($name)
531: {
532:
533: $namespace = '';
534: $searchName = $name;
535: if (false !== $pos = strrpos($name, ':')) {
536: $namespace = $this->findNamespace(substr($name, 0, $pos));
537: $searchName = $namespace.substr($name, $pos);
538: }
539:
540:
541: $commands = array();
542: foreach ($this->commands as $command) {
543: if ($this->extractNamespace($command->getName()) == $namespace) {
544: $commands[] = $command->getName();
545: }
546: }
547:
548: $abbrevs = static::getAbbreviations(array_unique($commands));
549: if (isset($abbrevs[$searchName]) && 1 == count($abbrevs[$searchName])) {
550: return $this->get($abbrevs[$searchName][0]);
551: }
552:
553: if (isset($abbrevs[$searchName]) && count($abbrevs[$searchName]) > 1) {
554: $suggestions = $this->getAbbreviationSuggestions($abbrevs[$searchName]);
555:
556: throw new \InvalidArgumentException(sprintf('Command "%s" is ambiguous (%s).', $name, $suggestions));
557: }
558:
559:
560: $aliases = array();
561: foreach ($this->commands as $command) {
562: foreach ($command->getAliases() as $alias) {
563: if ($this->extractNamespace($alias) == $namespace) {
564: $aliases[] = $alias;
565: }
566: }
567: }
568:
569: $aliases = static::getAbbreviations(array_unique($aliases));
570: if (!isset($aliases[$searchName])) {
571: $message = sprintf('Command "%s" is not defined.', $name);
572:
573: if ($alternatives = $this->findAlternativeCommands($searchName, $abbrevs)) {
574: $message .= "\n\nDid you mean one of these?\n ";
575: $message .= implode("\n ", $alternatives);
576: }
577:
578: throw new \InvalidArgumentException($message);
579: }
580:
581: if (count($aliases[$searchName]) > 1) {
582: throw new \InvalidArgumentException(sprintf('Command "%s" is ambiguous (%s).', $name, $this->getAbbreviationSuggestions($aliases[$searchName])));
583: }
584:
585: return $this->get($aliases[$searchName][0]);
586: }
587:
588: 589: 590: 591: 592: 593: 594: 595: 596: 597: 598:
599: public function all($namespace = null)
600: {
601: if (null === $namespace) {
602: return $this->commands;
603: }
604:
605: $commands = array();
606: foreach ($this->commands as $name => $command) {
607: if ($namespace === $this->extractNamespace($name, substr_count($namespace, ':') + 1)) {
608: $commands[$name] = $command;
609: }
610: }
611:
612: return $commands;
613: }
614:
615: 616: 617: 618: 619: 620: 621:
622: static public function getAbbreviations($names)
623: {
624: $abbrevs = array();
625: foreach ($names as $name) {
626: for ($len = strlen($name) - 1; $len > 0; --$len) {
627: $abbrev = substr($name, 0, $len);
628: if (!isset($abbrevs[$abbrev])) {
629: $abbrevs[$abbrev] = array($name);
630: } else {
631: $abbrevs[$abbrev][] = $name;
632: }
633: }
634: }
635:
636:
637: foreach ($names as $name) {
638: $abbrevs[$name] = array($name);
639: }
640:
641: return $abbrevs;
642: }
643:
644: 645: 646: 647: 648: 649: 650: 651:
652: public function asText($namespace = null, $raw = false)
653: {
654: $commands = $namespace ? $this->all($this->findNamespace($namespace)) : $this->commands;
655:
656: $width = 0;
657: foreach ($commands as $command) {
658: $width = strlen($command->getName()) > $width ? strlen($command->getName()) : $width;
659: }
660: $width += 2;
661:
662: if ($raw) {
663: $messages = array();
664: foreach ($this->sortCommands($commands) as $space => $commands) {
665: foreach ($commands as $name => $command) {
666: $messages[] = sprintf("%-${width}s %s", $name, $command->getDescription());
667: }
668: }
669:
670: return implode(PHP_EOL, $messages);
671: }
672:
673: $messages = array($this->getHelp(), '');
674: if ($namespace) {
675: $messages[] = sprintf("<comment>Available commands for the \"%s\" namespace:</comment>", $namespace);
676: } else {
677: $messages[] = '<comment>Available commands:</comment>';
678: }
679:
680:
681: foreach ($this->sortCommands($commands) as $space => $commands) {
682: if (!$namespace && '_global' !== $space) {
683: $messages[] = '<comment>'.$space.'</comment>';
684: }
685:
686: foreach ($commands as $name => $command) {
687: $messages[] = sprintf(" <info>%-${width}s</info> %s", $name, $command->getDescription());
688: }
689: }
690:
691: return implode(PHP_EOL, $messages);
692: }
693:
694: 695: 696: 697: 698: 699: 700: 701:
702: public function asXml($namespace = null, $asDom = false)
703: {
704: $commands = $namespace ? $this->all($this->findNamespace($namespace)) : $this->commands;
705:
706: $dom = new \DOMDocument('1.0', 'UTF-8');
707: $dom->formatOutput = true;
708: $dom->appendChild($xml = $dom->createElement('symfony'));
709:
710: $xml->appendChild($commandsXML = $dom->createElement('commands'));
711:
712: if ($namespace) {
713: $commandsXML->setAttribute('namespace', $namespace);
714: } else {
715: $namespacesXML = $dom->createElement('namespaces');
716: $xml->appendChild($namespacesXML);
717: }
718:
719:
720: foreach ($this->sortCommands($commands) as $space => $commands) {
721: if (!$namespace) {
722: $namespaceArrayXML = $dom->createElement('namespace');
723: $namespacesXML->appendChild($namespaceArrayXML);
724: $namespaceArrayXML->setAttribute('id', $space);
725: }
726:
727: foreach ($commands as $name => $command) {
728: if ($name !== $command->getName()) {
729: continue;
730: }
731:
732: if (!$namespace) {
733: $commandXML = $dom->createElement('command');
734: $namespaceArrayXML->appendChild($commandXML);
735: $commandXML->appendChild($dom->createTextNode($name));
736: }
737:
738: $node = $command->asXml(true)->getElementsByTagName('command')->item(0);
739: $node = $dom->importNode($node, true);
740:
741: $commandsXML->appendChild($node);
742: }
743: }
744:
745: return $asDom ? $dom : $dom->saveXml();
746: }
747:
748: 749: 750: 751: 752: 753:
754: public function renderException($e, $output)
755: {
756: $strlen = function ($string) {
757: if (!function_exists('mb_strlen')) {
758: return strlen($string);
759: }
760:
761: if (false === $encoding = mb_detect_encoding($string)) {
762: return strlen($string);
763: }
764:
765: return mb_strlen($string, $encoding);
766: };
767:
768: do {
769: $title = sprintf(' [%s] ', get_class($e));
770: $len = $strlen($title);
771: $lines = array();
772: foreach (explode("\n", $e->getMessage()) as $line) {
773: $lines[] = sprintf(' %s ', $line);
774: $len = max($strlen($line) + 4, $len);
775: }
776:
777: $messages = array(str_repeat(' ', $len), $title.str_repeat(' ', $len - $strlen($title)));
778:
779: foreach ($lines as $line) {
780: $messages[] = $line.str_repeat(' ', $len - $strlen($line));
781: }
782:
783: $messages[] = str_repeat(' ', $len);
784:
785: $output->writeln("");
786: $output->writeln("");
787: foreach ($messages as $message) {
788: $output->writeln('<error>'.$message.'</error>');
789: }
790: $output->writeln("");
791: $output->writeln("");
792:
793: if (OutputInterface::VERBOSITY_VERBOSE === $output->getVerbosity()) {
794: $output->writeln('<comment>Exception trace:</comment>');
795:
796:
797: $trace = $e->getTrace();
798: array_unshift($trace, array(
799: 'function' => '',
800: 'file' => $e->getFile() != null ? $e->getFile() : 'n/a',
801: 'line' => $e->getLine() != null ? $e->getLine() : 'n/a',
802: 'args' => array(),
803: ));
804:
805: for ($i = 0, $count = count($trace); $i < $count; $i++) {
806: $class = isset($trace[$i]['class']) ? $trace[$i]['class'] : '';
807: $type = isset($trace[$i]['type']) ? $trace[$i]['type'] : '';
808: $function = $trace[$i]['function'];
809: $file = isset($trace[$i]['file']) ? $trace[$i]['file'] : 'n/a';
810: $line = isset($trace[$i]['line']) ? $trace[$i]['line'] : 'n/a';
811:
812: $output->writeln(sprintf(' %s%s%s() at <info>%s:%s</info>', $class, $type, $function, $file, $line));
813: }
814:
815: $output->writeln("");
816: $output->writeln("");
817: }
818: } while ($e = $e->getPrevious());
819:
820: if (null !== $this->runningCommand) {
821: $output->writeln(sprintf('<info>%s</info>', sprintf($this->runningCommand->getSynopsis(), $this->getName())));
822: $output->writeln("");
823: $output->writeln("");
824: }
825: }
826:
827: 828: 829: 830: 831: 832: 833:
834: protected function getCommandName(InputInterface $input)
835: {
836: return $input->getFirstArgument('command');
837: }
838:
839: 840: 841: 842: 843:
844: protected function getDefaultInputDefinition()
845: {
846: return new InputDefinition(array(
847: new InputArgument('command', InputArgument::REQUIRED, 'The command to execute'),
848:
849: new InputOption('--help', '-h', InputOption::VALUE_NONE, 'Display this help message.'),
850: new InputOption('--quiet', '-q', InputOption::VALUE_NONE, 'Do not output any message.'),
851: new InputOption('--verbose', '-v', InputOption::VALUE_NONE, 'Increase verbosity of messages.'),
852: new InputOption('--version', '-V', InputOption::VALUE_NONE, 'Display this application version.'),
853: new InputOption('--ansi', '', InputOption::VALUE_NONE, 'Force ANSI output.'),
854: new InputOption('--no-ansi', '', InputOption::VALUE_NONE, 'Disable ANSI output.'),
855: new InputOption('--no-interaction', '-n', InputOption::VALUE_NONE, 'Do not ask any interactive question.'),
856: ));
857: }
858:
859: 860: 861: 862: 863:
864: protected function getDefaultCommands()
865: {
866: return array(new HelpCommand(), new ListCommand());
867: }
868:
869: 870: 871: 872: 873:
874: protected function getDefaultHelperSet()
875: {
876: return new HelperSet(array(
877: new FormatterHelper(),
878: new DialogHelper(),
879: ));
880: }
881:
882: 883: 884: 885: 886: 887: 888:
889: private function sortCommands($commands)
890: {
891: $namespacedCommands = array();
892: foreach ($commands as $name => $command) {
893: $key = $this->extractNamespace($name, 1);
894: if (!$key) {
895: $key = '_global';
896: }
897:
898: $namespacedCommands[$key][$name] = $command;
899: }
900: ksort($namespacedCommands);
901:
902: foreach ($namespacedCommands as &$commands) {
903: ksort($commands);
904: }
905:
906: return $namespacedCommands;
907: }
908:
909: 910: 911: 912: 913: 914: 915:
916: private function getAbbreviationSuggestions($abbrevs)
917: {
918: return sprintf('%s, %s%s', $abbrevs[0], $abbrevs[1], count($abbrevs) > 2 ? sprintf(' and %d more', count($abbrevs) - 2) : '');
919: }
920:
921: 922: 923: 924: 925: 926: 927: 928:
929: private function ($name, $limit = null)
930: {
931: $parts = explode(':', $name);
932: array_pop($parts);
933:
934: return implode(':', null === $limit ? $parts : array_slice($parts, 0, $limit));
935: }
936:
937: 938: 939: 940: 941: 942: 943: 944:
945: private function findAlternativeCommands($name, $abbrevs)
946: {
947: $callback = function($item) {
948: return $item->getName();
949: };
950:
951: return $this->findAlternatives($name, $this->commands, $abbrevs, $callback);
952: }
953:
954: 955: 956: 957: 958: 959: 960: 961:
962: private function findAlternativeNamespace($name, $abbrevs)
963: {
964: return $this->findAlternatives($name, $this->getNamespaces(), $abbrevs);
965: }
966:
967: 968: 969: 970: 971: 972: 973: 974: 975: 976: 977:
978: private function findAlternatives($name, $collection, $abbrevs, $callback = null) {
979: $alternatives = array();
980:
981: foreach ($collection as $item) {
982: if (null !== $callback) {
983: $item = call_user_func($callback, $item);
984: }
985:
986: $lev = levenshtein($name, $item);
987: if ($lev <= strlen($name) / 3 || false !== strpos($item, $name)) {
988: $alternatives[$item] = $lev;
989: }
990: }
991:
992: if (!$alternatives) {
993: foreach ($abbrevs as $key => $values) {
994: $lev = levenshtein($name, $key);
995: if ($lev <= strlen($name) / 3 || false !== strpos($key, $name)) {
996: foreach ($values as $value) {
997: $alternatives[$value] = $lev;
998: }
999: }
1000: }
1001: }
1002:
1003: asort($alternatives);
1004:
1005: return array_keys($alternatives);
1006: }
1007: }
1008: