[ Index ] |
PHP Cross Reference of Joomla 4.2.2 documentation |
[Summary view] [Print] [Text view]
1 <?php 2 /** 3 * Part of the Joomla Framework Console Package 4 * 5 * @copyright Copyright (C) 2005 - 2021 Open Source Matters, Inc. All rights reserved. 6 * @license GNU General Public License version 2 or later; see LICENSE 7 */ 8 9 namespace Joomla\Console; 10 11 use Joomla\Application\AbstractApplication; 12 use Joomla\Application\ApplicationEvents; 13 use Joomla\Console\Command\AbstractCommand; 14 use Joomla\Console\Command\HelpCommand; 15 use Joomla\Console\Event\ApplicationErrorEvent; 16 use Joomla\Console\Event\BeforeCommandExecuteEvent; 17 use Joomla\Console\Event\CommandErrorEvent; 18 use Joomla\Console\Event\TerminateEvent; 19 use Joomla\Console\Exception\NamespaceNotFoundException; 20 use Joomla\Registry\Registry; 21 use Joomla\String\StringHelper; 22 use Symfony\Component\Console\Exception\CommandNotFoundException; 23 use Symfony\Component\Console\Exception\ExceptionInterface; 24 use Symfony\Component\Console\Exception\LogicException; 25 use Symfony\Component\Console\Formatter\OutputFormatter; 26 use Symfony\Component\Console\Helper\DebugFormatterHelper; 27 use Symfony\Component\Console\Helper\FormatterHelper; 28 use Symfony\Component\Console\Helper\HelperSet; 29 use Symfony\Component\Console\Helper\ProcessHelper; 30 use Symfony\Component\Console\Helper\QuestionHelper; 31 use Symfony\Component\Console\Input\ArgvInput; 32 use Symfony\Component\Console\Input\ArrayInput; 33 use Symfony\Component\Console\Input\InputArgument; 34 use Symfony\Component\Console\Input\InputAwareInterface; 35 use Symfony\Component\Console\Input\InputDefinition; 36 use Symfony\Component\Console\Input\InputInterface; 37 use Symfony\Component\Console\Input\InputOption; 38 use Symfony\Component\Console\Output\ConsoleOutput; 39 use Symfony\Component\Console\Output\ConsoleOutputInterface; 40 use Symfony\Component\Console\Output\OutputInterface; 41 use Symfony\Component\Console\Style\SymfonyStyle; 42 use Symfony\Component\Console\Terminal; 43 use Symfony\Component\ErrorHandler\ErrorHandler; 44 45 /** 46 * Base application class for a Joomla! command line application. 47 * 48 * @since 2.0.0 49 */ 50 class Application extends AbstractApplication 51 { 52 /** 53 * Flag indicating the application should automatically exit after the command is run. 54 * 55 * @var boolean 56 * @since 2.0.0 57 */ 58 private $autoExit = true; 59 60 /** 61 * Flag indicating the application should catch and handle Throwables. 62 * 63 * @var boolean 64 * @since 2.0.0 65 */ 66 private $catchThrowables = true; 67 68 /** 69 * The available commands. 70 * 71 * @var AbstractCommand[] 72 * @since 2.0.0 73 */ 74 private $commands = []; 75 76 /** 77 * The command loader. 78 * 79 * @var Loader\LoaderInterface|null 80 * @since 2.0.0 81 */ 82 private $commandLoader; 83 84 /** 85 * Console input handler. 86 * 87 * @var InputInterface 88 * @since 2.0.0 89 */ 90 private $consoleInput; 91 92 /** 93 * Console output handler. 94 * 95 * @var OutputInterface 96 * @since 2.0.0 97 */ 98 private $consoleOutput; 99 100 /** 101 * The default command for the application. 102 * 103 * @var string 104 * @since 2.0.0 105 */ 106 private $defaultCommand = 'list'; 107 108 /** 109 * The application input definition. 110 * 111 * @var InputDefinition|null 112 * @since 2.0.0 113 */ 114 private $definition; 115 116 /** 117 * The application helper set. 118 * 119 * @var HelperSet|null 120 * @since 2.0.0 121 */ 122 private $helperSet; 123 124 /** 125 * Internal flag tracking if the command store has been initialised. 126 * 127 * @var boolean 128 * @since 2.0.0 129 */ 130 private $initialised = false; 131 132 /** 133 * The name of the application. 134 * 135 * @var string 136 * @since 2.0.0 137 */ 138 private $name = ''; 139 140 /** 141 * Reference to the currently running command. 142 * 143 * @var AbstractCommand|null 144 * @since 2.0.0 145 */ 146 private $runningCommand; 147 148 /** 149 * The console terminal helper. 150 * 151 * @var Terminal 152 * @since 2.0.0 153 */ 154 private $terminal; 155 156 /** 157 * The version of the application. 158 * 159 * @var string 160 * @since 2.0.0 161 */ 162 private $version = ''; 163 164 /** 165 * Internal flag tracking if the user is seeking help for the given command. 166 * 167 * @var boolean 168 * @since 2.0.0 169 */ 170 private $wantsHelp = false; 171 172 /** 173 * Class constructor. 174 * 175 * @param InputInterface $input An optional argument to provide dependency injection for the application's input object. If the argument is 176 * an InputInterface object that object will become the application's input object, otherwise a default input 177 * object is created. 178 * @param OutputInterface $output An optional argument to provide dependency injection for the application's output object. If the argument 179 * is an OutputInterface object that object will become the application's output object, otherwise a default 180 * output object is created. 181 * @param Registry $config An optional argument to provide dependency injection for the application's config object. If the argument 182 * is a Registry object that object will become the application's config object, otherwise a default config 183 * object is created. 184 * 185 * @since 2.0.0 186 */ 187 public function __construct(?InputInterface $input = null, ?OutputInterface $output = null, ?Registry $config = null) 188 { 189 // Close the application if we are not executed from the command line. 190 if (!\defined('STDOUT') || !\defined('STDIN') || !isset($_SERVER['argv'])) 191 { 192 $this->close(); 193 } 194 195 $this->consoleInput = $input ?: new ArgvInput; 196 $this->consoleOutput = $output ?: new ConsoleOutput; 197 $this->terminal = new Terminal; 198 199 // Call the constructor as late as possible (it runs `initialise`). 200 parent::__construct($config); 201 } 202 203 /** 204 * Adds a command object. 205 * 206 * If a command with the same name already exists, it will be overridden. If the command is not enabled it will not be added. 207 * 208 * @param AbstractCommand $command The command to add to the application. 209 * 210 * @return AbstractCommand 211 * 212 * @since 2.0.0 213 * @throws LogicException 214 */ 215 public function addCommand(AbstractCommand $command): AbstractCommand 216 { 217 $this->initCommands(); 218 219 if (!$command->isEnabled()) 220 { 221 return $command; 222 } 223 224 $command->setApplication($this); 225 226 try 227 { 228 $command->getDefinition(); 229 } 230 catch (\TypeError $exception) 231 { 232 throw new LogicException(sprintf('Command class "%s" is not correctly initialised.', \get_class($command)), 0, $exception); 233 } 234 235 if (!$command->getName()) 236 { 237 throw new LogicException(sprintf('The command class "%s" does not have a name.', \get_class($command))); 238 } 239 240 $this->commands[$command->getName()] = $command; 241 242 foreach ($command->getAliases() as $alias) 243 { 244 $this->commands[$alias] = $command; 245 } 246 247 return $command; 248 } 249 250 /** 251 * Configures the console input and output instances for the process. 252 * 253 * @return void 254 * 255 * @since 2.0.0 256 */ 257 protected function configureIO(): void 258 { 259 if ($this->consoleInput->hasParameterOption(['--ansi'], true)) 260 { 261 $this->consoleOutput->setDecorated(true); 262 } 263 elseif ($this->consoleInput->hasParameterOption(['--no-ansi'], true)) 264 { 265 $this->consoleOutput->setDecorated(false); 266 } 267 268 if ($this->consoleInput->hasParameterOption(['--no-interaction', '-n'], true)) 269 { 270 $this->consoleInput->setInteractive(false); 271 } 272 273 if ($this->consoleInput->hasParameterOption(['--quiet', '-q'], true)) 274 { 275 $this->consoleOutput->setVerbosity(OutputInterface::VERBOSITY_QUIET); 276 $this->consoleInput->setInteractive(false); 277 } 278 else 279 { 280 if ($this->consoleInput->hasParameterOption('-vvv', true) 281 || $this->consoleInput->hasParameterOption('--verbose=3', true) 282 || $this->consoleInput->getParameterOption('--verbose', false, true) === 3 283 ) 284 { 285 $this->consoleOutput->setVerbosity(OutputInterface::VERBOSITY_DEBUG); 286 } 287 elseif ($this->consoleInput->hasParameterOption('-vv', true) 288 || $this->consoleInput->hasParameterOption('--verbose=2', true) 289 || $this->consoleInput->getParameterOption('--verbose', false, true) === 2 290 ) 291 { 292 $this->consoleOutput->setVerbosity(OutputInterface::VERBOSITY_VERY_VERBOSE); 293 } 294 elseif ($this->consoleInput->hasParameterOption('-v', true) 295 || $this->consoleInput->hasParameterOption('--verbose=1', true) 296 || $this->consoleInput->hasParameterOption('--verbose', true) 297 || $this->consoleInput->getParameterOption('--verbose', false, true) 298 ) 299 { 300 $this->consoleOutput->setVerbosity(OutputInterface::VERBOSITY_VERBOSE); 301 } 302 } 303 } 304 305 /** 306 * Method to run the application routines. 307 * 308 * @return integer The exit code for the application 309 * 310 * @since 2.0.0 311 * @throws \Throwable 312 */ 313 protected function doExecute(): int 314 { 315 $input = $this->consoleInput; 316 $output = $this->consoleOutput; 317 318 // If requesting the version, short circuit the application and send the version data 319 if ($input->hasParameterOption(['--version', '-V'], true)) 320 { 321 $output->writeln($this->getLongVersion()); 322 323 return 0; 324 } 325 326 try 327 { 328 // Makes ArgvInput::getFirstArgument() able to distinguish an option from an argument. 329 $input->bind($this->getDefinition()); 330 } 331 catch (ExceptionInterface $e) 332 { 333 // Errors must be ignored, full binding/validation happens later when the command is known. 334 } 335 336 $name = $this->getCommandName($input); 337 338 // Redirect to the help command if requested 339 if ($input->hasParameterOption(['--help', '-h'], true)) 340 { 341 // If no command name was given, use the help command with a minimal input; otherwise flag the request for processing later 342 if (!$name) 343 { 344 $name = 'help'; 345 $input = new ArrayInput(['command_name' => $this->defaultCommand]); 346 } 347 else 348 { 349 $this->wantsHelp = true; 350 } 351 } 352 353 // If we still do not have a command name, then the user has requested the application's default command 354 if (!$name) 355 { 356 $name = $this->defaultCommand; 357 $definition = $this->getDefinition(); 358 359 // Overwrite the default value of the command argument with the default command name 360 $definition->setArguments( 361 array_merge( 362 $definition->getArguments(), 363 [ 364 'command' => new InputArgument( 365 'command', 366 InputArgument::OPTIONAL, 367 $definition->getArgument('command')->getDescription(), 368 $name 369 ), 370 ] 371 ) 372 ); 373 } 374 375 try 376 { 377 $this->runningCommand = null; 378 379 $command = $this->getCommand($name); 380 } 381 catch (\Throwable $e) 382 { 383 if ($e instanceof CommandNotFoundException && !($e instanceof NamespaceNotFoundException)) 384 { 385 (new SymfonyStyle($input, $output))->block(sprintf("\nCommand \"%s\" is not defined.\n", $name), null, 'error'); 386 } 387 388 $event = new CommandErrorEvent($e, $this); 389 390 $this->dispatchEvent(ConsoleEvents::COMMAND_ERROR, $event); 391 392 if ($event->getExitCode() === 0) 393 { 394 return 0; 395 } 396 397 $e = $event->getError(); 398 399 throw $e; 400 } 401 402 $this->runningCommand = $command; 403 $exitCode = $this->runCommand($command, $input, $output); 404 $this->runningCommand = null; 405 406 return $exitCode; 407 } 408 409 /** 410 * Execute the application. 411 * 412 * @return void 413 * 414 * @since 2.0.0 415 * @throws \Throwable 416 */ 417 public function execute() 418 { 419 putenv('LINES=' . $this->terminal->getHeight()); 420 putenv('COLUMNS=' . $this->terminal->getWidth()); 421 422 $this->configureIO(); 423 424 $renderThrowable = function (\Throwable $e) 425 { 426 $this->renderThrowable($e); 427 }; 428 429 if ($phpHandler = set_exception_handler($renderThrowable)) 430 { 431 restore_exception_handler(); 432 433 if (!\is_array($phpHandler) || !$phpHandler[0] instanceof ErrorHandler) 434 { 435 $errorHandler = true; 436 } 437 elseif ($errorHandler = $phpHandler[0]->setExceptionHandler($renderThrowable)) 438 { 439 $phpHandler[0]->setExceptionHandler($errorHandler); 440 } 441 } 442 443 try 444 { 445 $this->dispatchEvent(ApplicationEvents::BEFORE_EXECUTE); 446 447 // Perform application routines. 448 $exitCode = $this->doExecute(); 449 450 $this->dispatchEvent(ApplicationEvents::AFTER_EXECUTE); 451 } 452 catch (\Throwable $throwable) 453 { 454 if (!$this->shouldCatchThrowables()) 455 { 456 throw $throwable; 457 } 458 459 $renderThrowable($throwable); 460 461 $event = new ApplicationErrorEvent($throwable, $this, $this->runningCommand); 462 463 $this->dispatchEvent(ConsoleEvents::APPLICATION_ERROR, $event); 464 465 $exitCode = $event->getExitCode(); 466 467 if (is_numeric($exitCode)) 468 { 469 $exitCode = (int) $exitCode; 470 471 if ($exitCode === 0) 472 { 473 $exitCode = 1; 474 } 475 } 476 else 477 { 478 $exitCode = 1; 479 } 480 } 481 finally 482 { 483 // If the exception handler changed, keep it; otherwise, unregister $renderThrowable 484 if (!$phpHandler) 485 { 486 if (set_exception_handler($renderThrowable) === $renderThrowable) 487 { 488 restore_exception_handler(); 489 } 490 491 restore_exception_handler(); 492 } 493 elseif (!$errorHandler) 494 { 495 $finalHandler = $phpHandler[0]->setExceptionHandler(null); 496 497 if ($finalHandler !== $renderThrowable) 498 { 499 $phpHandler[0]->setExceptionHandler($finalHandler); 500 } 501 } 502 503 if ($this->shouldAutoExit() && isset($exitCode)) 504 { 505 $exitCode = $exitCode > 255 ? 255 : $exitCode; 506 $this->close($exitCode); 507 } 508 } 509 } 510 511 /** 512 * Finds a registered namespace by a name. 513 * 514 * @param string $namespace A namespace to search for 515 * 516 * @return string 517 * 518 * @since 2.0.0 519 * @throws NamespaceNotFoundException When namespace is incorrect or ambiguous 520 */ 521 public function findNamespace(string $namespace): string 522 { 523 $allNamespaces = $this->getNamespaces(); 524 525 $expr = preg_replace_callback( 526 '{([^:]+|)}', 527 function ($matches) 528 { 529 return preg_quote($matches[1]) . '[^:]*'; 530 }, 531 $namespace 532 ); 533 534 $namespaces = preg_grep('{^' . $expr . '}', $allNamespaces); 535 536 if (empty($namespaces)) 537 { 538 throw new NamespaceNotFoundException(sprintf('There are no commands defined in the "%s" namespace.', $namespace)); 539 } 540 541 $exact = \in_array($namespace, $namespaces, true); 542 543 if (\count($namespaces) > 1 && !$exact) 544 { 545 throw new NamespaceNotFoundException(sprintf('The namespace "%s" is ambiguous.', $namespace)); 546 } 547 548 return $exact ? $namespace : reset($namespaces); 549 } 550 551 /** 552 * Gets all commands, including those available through a command loader, optionally filtered on a command namespace. 553 * 554 * @param string $namespace An optional command namespace to filter by. 555 * 556 * @return AbstractCommand[] 557 * 558 * @since 2.0.0 559 */ 560 public function getAllCommands(string $namespace = ''): array 561 { 562 $this->initCommands(); 563 564 if ($namespace === '') 565 { 566 $commands = $this->commands; 567 568 if (!$this->commandLoader) 569 { 570 return $commands; 571 } 572 573 foreach ($this->commandLoader->getNames() as $name) 574 { 575 if (!isset($commands[$name])) 576 { 577 $commands[$name] = $this->getCommand($name); 578 } 579 } 580 581 return $commands; 582 } 583 584 $commands = []; 585 586 foreach ($this->commands as $name => $command) 587 { 588 if ($namespace === $this->extractNamespace($name, substr_count($namespace, ':') + 1)) 589 { 590 $commands[$name] = $command; 591 } 592 } 593 594 if ($this->commandLoader) 595 { 596 foreach ($this->commandLoader->getNames() as $name) 597 { 598 if (!isset($commands[$name]) && $namespace === $this->extractNamespace($name, substr_count($namespace, ':') + 1)) 599 { 600 $commands[$name] = $this->getCommand($name); 601 } 602 } 603 } 604 605 return $commands; 606 } 607 608 /** 609 * Returns a registered command by name or alias. 610 * 611 * @param string $name The command name or alias 612 * 613 * @return AbstractCommand 614 * 615 * @since 2.0.0 616 * @throws CommandNotFoundException 617 */ 618 public function getCommand(string $name): AbstractCommand 619 { 620 $this->initCommands(); 621 622 if (!$this->hasCommand($name)) 623 { 624 throw new CommandNotFoundException(sprintf('The command "%s" does not exist.', $name)); 625 } 626 627 // If the command isn't registered, pull it from the loader if registered 628 if (!isset($this->commands[$name]) && $this->commandLoader) 629 { 630 $this->addCommand($this->commandLoader->get($name)); 631 } 632 633 $command = $this->commands[$name]; 634 635 // If the user requested help, we'll fetch the help command now and inject the user's command into it 636 if ($this->wantsHelp) 637 { 638 $this->wantsHelp = false; 639 640 /** @var HelpCommand $helpCommand */ 641 $helpCommand = $this->getCommand('help'); 642 $helpCommand->setCommand($command); 643 644 return $helpCommand; 645 } 646 647 return $command; 648 } 649 650 /** 651 * Get the name of the command to run. 652 * 653 * @param InputInterface $input The input to read the argument from 654 * 655 * @return string|null 656 * 657 * @since 2.0.0 658 */ 659 protected function getCommandName(InputInterface $input): ?string 660 { 661 return $input->getFirstArgument(); 662 } 663 664 /** 665 * Get the registered commands. 666 * 667 * This method only retrieves commands which have been explicitly registered. To get all commands including those from a 668 * command loader, use the `getAllCommands()` method. 669 * 670 * @return AbstractCommand[] 671 * 672 * @since 2.0.0 673 */ 674 public function getCommands(): array 675 { 676 return $this->commands; 677 } 678 679 /** 680 * Get the console input handler. 681 * 682 * @return InputInterface 683 * 684 * @since 2.0.0 685 */ 686 public function getConsoleInput(): InputInterface 687 { 688 return $this->consoleInput; 689 } 690 691 /** 692 * Get the console output handler. 693 * 694 * @return OutputInterface 695 * 696 * @since 2.0.0 697 */ 698 public function getConsoleOutput(): OutputInterface 699 { 700 return $this->consoleOutput; 701 } 702 703 /** 704 * Get the commands which should be registered by default to the application. 705 * 706 * @return AbstractCommand[] 707 * 708 * @since 2.0.0 709 */ 710 protected function getDefaultCommands(): array 711 { 712 return [ 713 new Command\ListCommand, 714 new Command\HelpCommand, 715 ]; 716 } 717 718 /** 719 * Builds the default input definition. 720 * 721 * @return InputDefinition 722 * 723 * @since 2.0.0 724 */ 725 protected function getDefaultInputDefinition(): InputDefinition 726 { 727 return new InputDefinition( 728 [ 729 new InputArgument('command', InputArgument::REQUIRED, 'The command to execute'), 730 new InputOption('--help', '-h', InputOption::VALUE_NONE, 'Display the help information'), 731 new InputOption('--quiet', '-q', InputOption::VALUE_NONE, 'Flag indicating that all output should be silenced'), 732 new InputOption( 733 '--verbose', 734 '-v|vv|vvv', 735 InputOption::VALUE_NONE, 736 'Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug' 737 ), 738 new InputOption('--version', '-V', InputOption::VALUE_NONE, 'Displays the application version'), 739 new InputOption('--ansi', '', InputOption::VALUE_NONE, 'Force ANSI output'), 740 new InputOption('--no-ansi', '', InputOption::VALUE_NONE, 'Disable ANSI output'), 741 new InputOption('--no-interaction', '-n', InputOption::VALUE_NONE, 'Flag to disable interacting with the user'), 742 ] 743 ); 744 } 745 746 /** 747 * Builds the default helper set. 748 * 749 * @return HelperSet 750 * 751 * @since 2.0.0 752 */ 753 protected function getDefaultHelperSet(): HelperSet 754 { 755 return new HelperSet( 756 [ 757 new FormatterHelper, 758 new DebugFormatterHelper, 759 new ProcessHelper, 760 new QuestionHelper, 761 ] 762 ); 763 } 764 765 /** 766 * Gets the InputDefinition related to this Application. 767 * 768 * @return InputDefinition 769 * 770 * @since 2.0.0 771 */ 772 public function getDefinition(): InputDefinition 773 { 774 if (!$this->definition) 775 { 776 $this->definition = $this->getDefaultInputDefinition(); 777 } 778 779 return $this->definition; 780 } 781 782 /** 783 * Get the helper set associated with the application. 784 * 785 * @return HelperSet 786 */ 787 public function getHelperSet(): HelperSet 788 { 789 if (!$this->helperSet) 790 { 791 $this->helperSet = $this->getDefaultHelperSet(); 792 } 793 794 return $this->helperSet; 795 } 796 797 /** 798 * Get the long version string for the application. 799 * 800 * Typically, this is the application name and version and is used in the application help output. 801 * 802 * @return string 803 * 804 * @since 2.0.0 805 */ 806 public function getLongVersion(): string 807 { 808 $name = $this->getName(); 809 810 if ($name === '') 811 { 812 $name = 'Joomla Console Application'; 813 } 814 815 if ($this->getVersion() !== '') 816 { 817 return sprintf('%s <info>%s</info>', $name, $this->getVersion()); 818 } 819 820 return $name; 821 } 822 823 /** 824 * Get the name of the application. 825 * 826 * @return string 827 * 828 * @since 2.0.0 829 */ 830 public function getName(): string 831 { 832 return $this->name; 833 } 834 835 /** 836 * Returns an array of all unique namespaces used by currently registered commands. 837 * 838 * Note that this does not include the global namespace which always exists. 839 * 840 * @return string[] 841 * 842 * @since 2.0.0 843 */ 844 public function getNamespaces(): array 845 { 846 $namespaces = []; 847 848 foreach ($this->getAllCommands() as $command) 849 { 850 $namespaces = array_merge($namespaces, $this->extractAllNamespaces($command->getName())); 851 852 foreach ($command->getAliases() as $alias) 853 { 854 $namespaces = array_merge($namespaces, $this->extractAllNamespaces($alias)); 855 } 856 } 857 858 return array_values(array_unique(array_filter($namespaces))); 859 } 860 861 /** 862 * Get the version of the application. 863 * 864 * @return string 865 * 866 * @since 2.0.0 867 */ 868 public function getVersion(): string 869 { 870 return $this->version; 871 } 872 873 /** 874 * Check if the application has a command with the given name. 875 * 876 * @param string $name The name of the command to check for existence. 877 * 878 * @return boolean 879 * 880 * @since 2.0.0 881 */ 882 public function hasCommand(string $name): bool 883 { 884 $this->initCommands(); 885 886 // If command is already registered, we're good 887 if (isset($this->commands[$name])) 888 { 889 return true; 890 } 891 892 // If there is no loader, we can't look for a command there 893 if (!$this->commandLoader) 894 { 895 return false; 896 } 897 898 return $this->commandLoader->has($name); 899 } 900 901 /** 902 * Custom initialisation method. 903 * 904 * @return void 905 * 906 * @since 2.0.0 907 */ 908 protected function initialise(): void 909 { 910 // Set the current directory. 911 $this->set('cwd', getcwd()); 912 } 913 914 /** 915 * Renders an error message for a Throwable object 916 * 917 * @param \Throwable $throwable The Throwable object to render the message for. 918 * 919 * @return void 920 * 921 * @since 2.0.0 922 */ 923 public function renderThrowable(\Throwable $throwable): void 924 { 925 $output = $this->consoleOutput instanceof ConsoleOutputInterface ? $this->consoleOutput->getErrorOutput() : $this->consoleOutput; 926 927 $output->writeln('', OutputInterface::VERBOSITY_QUIET); 928 929 $this->doRenderThrowable($throwable, $output); 930 931 if (null !== $this->runningCommand) 932 { 933 $output->writeln( 934 sprintf( 935 '<info>%s</info>', 936 sprintf($this->runningCommand->getSynopsis(), $this->getName()) 937 ), 938 OutputInterface::VERBOSITY_QUIET 939 ); 940 941 $output->writeln('', OutputInterface::VERBOSITY_QUIET); 942 } 943 } 944 945 /** 946 * Handles recursively rendering error messages for a Throwable and all previous Throwables contained within. 947 * 948 * @param \Throwable $throwable The Throwable object to render the message for. 949 * @param OutputInterface $output The output object to send the message to. 950 * 951 * @return void 952 * 953 * @since 2.0.0 954 */ 955 protected function doRenderThrowable(\Throwable $throwable, OutputInterface $output): void 956 { 957 do 958 { 959 $message = trim($throwable->getMessage()); 960 961 if ($message === '' || OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) 962 { 963 $class = \get_class($throwable); 964 965 if ($class[0] === 'c' && strpos($class, "class@anonymous\0") === 0) 966 { 967 $class = get_parent_class($class) ?: key(class_implements($class)); 968 } 969 970 $title = sprintf(' [%s%s] ', $class, ($code = $throwable->getCode()) !== 0 ? ' (' . $code . ')' : ''); 971 $len = StringHelper::strlen($title); 972 } 973 else 974 { 975 $len = 0; 976 } 977 978 if (strpos($message, "class@anonymous\0") !== false) 979 { 980 $message = preg_replace_callback( 981 '/class@anonymous\x00.*?\.php(?:0x?|:[0-9]++\$)[0-9a-fA-F]++/', 982 function ($m) 983 { 984 return class_exists($m[0], false) ? (get_parent_class($m[0]) ?: key(class_implements($m[0]))) . '@anonymous' : $m[0]; 985 }, 986 $message 987 ); 988 } 989 990 $width = $this->terminal->getWidth() ? $this->terminal->getWidth() - 1 : PHP_INT_MAX; 991 $lines = []; 992 993 foreach ($message !== '' ? preg_split('/\r?\n/', $message) : [] as $line) 994 { 995 foreach ($this->splitStringByWidth($line, $width - 4) as $line) 996 { 997 // Pre-format lines to get the right string length 998 $lineLength = StringHelper::strlen($line) + 4; 999 $lines[] = [$line, $lineLength]; 1000 $len = max($lineLength, $len); 1001 } 1002 } 1003 1004 $messages = []; 1005 1006 if (!$throwable instanceof ExceptionInterface || OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) 1007 { 1008 $messages[] = sprintf( 1009 '<comment>%s</comment>', 1010 OutputFormatter::escape( 1011 sprintf( 1012 'In %s line %s:', basename($throwable->getFile()) ?: 'n/a', $throwable->getLine() ?: 'n/a' 1013 ) 1014 ) 1015 ); 1016 } 1017 1018 $messages[] = $emptyLine = sprintf('<error>%s</error>', str_repeat(' ', $len)); 1019 1020 if ($message === '' || OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) 1021 { 1022 $messages[] = sprintf('<error>%s%s</error>', $title, str_repeat(' ', max(0, $len - StringHelper::strlen($title)))); 1023 } 1024 1025 foreach ($lines as $line) 1026 { 1027 $messages[] = sprintf('<error> %s %s</error>', OutputFormatter::escape($line[0]), str_repeat(' ', $len - $line[1])); 1028 } 1029 1030 $messages[] = $emptyLine; 1031 $messages[] = ''; 1032 1033 $output->writeln($messages, OutputInterface::VERBOSITY_QUIET); 1034 1035 if (OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) 1036 { 1037 $output->writeln('<comment>Exception trace:</comment>', OutputInterface::VERBOSITY_QUIET); 1038 1039 // Exception related properties 1040 $trace = $throwable->getTrace(); 1041 array_unshift( 1042 $trace, 1043 [ 1044 'function' => '', 1045 'file' => $throwable->getFile() ?: 'n/a', 1046 'line' => $throwable->getLine() ?: 'n/a', 1047 'args' => [], 1048 ] 1049 ); 1050 1051 for ($i = 0, $count = \count($trace); $i < $count; ++$i) 1052 { 1053 $class = $trace[$i]['class'] ?? ''; 1054 $type = $trace[$i]['type'] ?? ''; 1055 $function = $trace[$i]['function'] ?? ''; 1056 $file = $trace[$i]['file'] ?? 'n/a'; 1057 $line = $trace[$i]['line'] ?? 'n/a'; 1058 1059 $output->writeln( 1060 sprintf( 1061 ' %s%s at <info>%s:%s</info>', $class, $function ? $type . $function . '()' : '', $file, $line 1062 ), 1063 OutputInterface::VERBOSITY_QUIET 1064 ); 1065 } 1066 1067 $output->writeln('', OutputInterface::VERBOSITY_QUIET); 1068 } 1069 } 1070 while ($throwable = $throwable->getPrevious()); 1071 } 1072 1073 /** 1074 * Splits a string for a specified width for use in an output. 1075 * 1076 * @param string $string The string to split. 1077 * @param integer $width The maximum width of the output. 1078 * 1079 * @return string[] 1080 * 1081 * @since 2.0.0 1082 */ 1083 private function splitStringByWidth(string $string, int $width): array 1084 { 1085 /* 1086 * The str_split function is not suitable for multi-byte characters, we should use preg_split to get char array properly. 1087 * Additionally, array_slice() is not enough as some character has doubled width. 1088 * We need a function to split string not by character count but by string width 1089 */ 1090 if (false === $encoding = mb_detect_encoding($string, null, true)) 1091 { 1092 return str_split($string, $width); 1093 } 1094 1095 $utf8String = mb_convert_encoding($string, 'utf8', $encoding); 1096 $lines = []; 1097 $line = ''; 1098 $offset = 0; 1099 1100 while (preg_match('/.{1,10000}/u', $utf8String, $m, 0, $offset)) 1101 { 1102 $offset += \strlen($m[0]); 1103 1104 foreach (preg_split('//u', $m[0]) as $char) 1105 { 1106 // Test if $char could be appended to current line 1107 if (mb_strwidth($line . $char, 'utf8') <= $width) 1108 { 1109 $line .= $char; 1110 1111 continue; 1112 } 1113 1114 // If not, push current line to array and make a new line 1115 $lines[] = str_pad($line, $width); 1116 $line = $char; 1117 } 1118 } 1119 1120 $lines[] = \count($lines) ? str_pad($line, $width) : $line; 1121 mb_convert_variables($encoding, 'utf8', $lines); 1122 1123 return $lines; 1124 } 1125 1126 /** 1127 * Run the given command. 1128 * 1129 * @param AbstractCommand $command The command to run. 1130 * @param InputInterface $input The input to inject into the command. 1131 * @param OutputInterface $output The output to inject into the command. 1132 * 1133 * @return integer 1134 * 1135 * @since 2.0.0 1136 * @throws \Throwable 1137 */ 1138 protected function runCommand(AbstractCommand $command, InputInterface $input, OutputInterface $output): int 1139 { 1140 if ($command->getHelperSet() !== null) 1141 { 1142 foreach ($command->getHelperSet() as $helper) 1143 { 1144 if ($helper instanceof InputAwareInterface) 1145 { 1146 $helper->setInput($input); 1147 } 1148 } 1149 } 1150 1151 // If the application doesn't have an event dispatcher, we can short circuit and just execute the command 1152 try 1153 { 1154 $this->getDispatcher(); 1155 } 1156 catch (\UnexpectedValueException $exception) 1157 { 1158 return $command->execute($input, $output); 1159 } 1160 1161 // Bind before dispatching the event so the listeners have access to input options/arguments 1162 try 1163 { 1164 $command->mergeApplicationDefinition(); 1165 $input->bind($command->getDefinition()); 1166 } 1167 catch (ExceptionInterface $e) 1168 { 1169 // Ignore invalid options/arguments for now 1170 } 1171 1172 $event = new BeforeCommandExecuteEvent($this, $command); 1173 $exception = null; 1174 1175 try 1176 { 1177 $this->dispatchEvent(ConsoleEvents::BEFORE_COMMAND_EXECUTE, $event); 1178 1179 if ($event->isCommandEnabled()) 1180 { 1181 $exitCode = $command->execute($input, $output); 1182 } 1183 else 1184 { 1185 $exitCode = BeforeCommandExecuteEvent::RETURN_CODE_DISABLED; 1186 } 1187 } 1188 catch (\Throwable $exception) 1189 { 1190 $event = new CommandErrorEvent($exception, $this, $command); 1191 1192 $this->dispatchEvent(ConsoleEvents::COMMAND_ERROR, $event); 1193 1194 $exception = $event->getError(); 1195 $exitCode = $event->getExitCode(); 1196 1197 if ($exitCode === 0) 1198 { 1199 $exception = null; 1200 } 1201 } 1202 1203 $event = new TerminateEvent($exitCode, $this, $command); 1204 1205 $this->dispatchEvent(ConsoleEvents::TERMINATE, $event); 1206 1207 if ($exception !== null) 1208 { 1209 throw $exception; 1210 } 1211 1212 return $event->getExitCode(); 1213 } 1214 1215 /** 1216 * Set whether the application should auto exit. 1217 * 1218 * @param boolean $autoExit The auto exit state. 1219 * 1220 * @return void 1221 * 1222 * @since 2.0.0 1223 */ 1224 public function setAutoExit(bool $autoExit): void 1225 { 1226 $this->autoExit = $autoExit; 1227 } 1228 1229 /** 1230 * Set whether the application should catch Throwables. 1231 * 1232 * @param boolean $catchThrowables The catch Throwables state. 1233 * 1234 * @return void 1235 * 1236 * @since 2.0.0 1237 */ 1238 public function setCatchThrowables(bool $catchThrowables): void 1239 { 1240 $this->catchThrowables = $catchThrowables; 1241 } 1242 1243 /** 1244 * Set the command loader. 1245 * 1246 * @param Loader\LoaderInterface $loader The new command loader. 1247 * 1248 * @return void 1249 * 1250 * @since 2.0.0 1251 */ 1252 public function setCommandLoader(Loader\LoaderInterface $loader): void 1253 { 1254 $this->commandLoader = $loader; 1255 } 1256 1257 /** 1258 * Set the application's helper set. 1259 * 1260 * @param HelperSet $helperSet The new HelperSet. 1261 * 1262 * @return void 1263 * 1264 * @since 2.0.0 1265 */ 1266 public function setHelperSet(HelperSet $helperSet): void 1267 { 1268 $this->helperSet = $helperSet; 1269 } 1270 1271 /** 1272 * Set the name of the application. 1273 * 1274 * @param string $name The new application name. 1275 * 1276 * @return void 1277 * 1278 * @since 2.0.0 1279 */ 1280 public function setName(string $name): void 1281 { 1282 $this->name = $name; 1283 } 1284 1285 /** 1286 * Set the version of the application. 1287 * 1288 * @param string $version The new application version. 1289 * 1290 * @return void 1291 * 1292 * @since 2.0.0 1293 */ 1294 public function setVersion(string $version): void 1295 { 1296 $this->version = $version; 1297 } 1298 1299 /** 1300 * Get the application's auto exit state. 1301 * 1302 * @return boolean 1303 * 1304 * @since 2.0.0 1305 */ 1306 public function shouldAutoExit(): bool 1307 { 1308 return $this->autoExit; 1309 } 1310 1311 /** 1312 * Get the application's catch Throwables state. 1313 * 1314 * @return boolean 1315 * 1316 * @since 2.0.0 1317 */ 1318 public function shouldCatchThrowables(): bool 1319 { 1320 return $this->catchThrowables; 1321 } 1322 1323 /** 1324 * Returns all namespaces of the command name. 1325 * 1326 * @param string $name The full name of the command 1327 * 1328 * @return string[] 1329 * 1330 * @since 2.0.0 1331 */ 1332 private function extractAllNamespaces(string $name): array 1333 { 1334 // -1 as third argument is needed to skip the command short name when exploding 1335 $parts = explode(':', $name, -1); 1336 $namespaces = []; 1337 1338 foreach ($parts as $part) 1339 { 1340 if (\count($namespaces)) 1341 { 1342 $namespaces[] = end($namespaces) . ':' . $part; 1343 } 1344 else 1345 { 1346 $namespaces[] = $part; 1347 } 1348 } 1349 1350 return $namespaces; 1351 } 1352 1353 /** 1354 * Returns the namespace part of the command name. 1355 * 1356 * @param string $name The command name to process 1357 * @param integer $limit The maximum number of parts of the namespace 1358 * 1359 * @return string 1360 * 1361 * @since 2.0.0 1362 */ 1363 private function extractNamespace(string $name, ?int $limit = null): string 1364 { 1365 $parts = explode(':', $name); 1366 array_pop($parts); 1367 1368 return implode(':', $limit === null ? $parts : \array_slice($parts, 0, $limit)); 1369 } 1370 1371 /** 1372 * Internal function to initialise the command store, this allows the store to be lazy loaded only when needed. 1373 * 1374 * @return void 1375 * 1376 * @since 2.0.0 1377 */ 1378 private function initCommands(): void 1379 { 1380 if ($this->initialised) 1381 { 1382 return; 1383 } 1384 1385 $this->initialised = true; 1386 1387 foreach ($this->getDefaultCommands() as $command) 1388 { 1389 $this->addCommand($command); 1390 } 1391 } 1392 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Wed Sep 7 05:41:13 2022 | Chilli.vc Blog - For Webmaster,Blog-Writer,System Admin and Domainer |