UnknownCommandException.php 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  1. <?php
  2. /**
  3. * @link http://www.yiiframework.com/
  4. * @copyright Copyright (c) 2008 Yii Software LLC
  5. * @license http://www.yiiframework.com/license/
  6. */
  7. namespace yii\console;
  8. use yii\console\controllers\HelpController;
  9. /**
  10. * UnknownCommandException represents an exception caused by incorrect usage of a console command.
  11. *
  12. * @author Carsten Brandt <mail@cebe.cc>
  13. * @since 2.0.11
  14. */
  15. class UnknownCommandException extends Exception
  16. {
  17. /**
  18. * @var string the name of the command that could not be recognized.
  19. */
  20. public $command;
  21. /**
  22. * @var Application
  23. */
  24. protected $application;
  25. /**
  26. * Construct the exception.
  27. *
  28. * @param string $route the route of the command that could not be found.
  29. * @param Application $application the console application instance involved.
  30. * @param int $code the Exception code.
  31. * @param \Exception $previous the previous exception used for the exception chaining.
  32. */
  33. public function __construct($route, $application, $code = 0, \Exception $previous = null)
  34. {
  35. $this->command = $route;
  36. $this->application = $application;
  37. parent::__construct("Unknown command \"$route\".", $code, $previous);
  38. }
  39. /**
  40. * @return string the user-friendly name of this exception
  41. */
  42. public function getName()
  43. {
  44. return 'Unknown command';
  45. }
  46. /**
  47. * Suggest alternative commands for [[$command]] based on string similarity.
  48. *
  49. * Alternatives are searched using the following steps:
  50. *
  51. * - suggest alternatives that begin with `$command`
  52. * - find typos by calculating the Levenshtein distance between the unknown command and all
  53. * available commands. The Levenshtein distance is defined as the minimal number of
  54. * characters you have to replace, insert or delete to transform str1 into str2.
  55. *
  56. * @see https://secure.php.net/manual/en/function.levenshtein.php
  57. * @return array a list of suggested alternatives sorted by similarity.
  58. */
  59. public function getSuggestedAlternatives()
  60. {
  61. $help = $this->application->createController('help');
  62. if ($help === false || $this->command === '') {
  63. return [];
  64. }
  65. /** @var $helpController HelpController */
  66. list($helpController, $actionID) = $help;
  67. $availableActions = [];
  68. foreach ($helpController->getCommands() as $command) {
  69. $result = $this->application->createController($command);
  70. if ($result === false) {
  71. continue;
  72. }
  73. // add the command itself (default action)
  74. $availableActions[] = $command;
  75. // add all actions of this controller
  76. /** @var $controller Controller */
  77. list($controller, $actionID) = $result;
  78. $actions = $helpController->getActions($controller);
  79. if (!empty($actions)) {
  80. $prefix = $controller->getUniqueId();
  81. foreach ($actions as $action) {
  82. $availableActions[] = $prefix . '/' . $action;
  83. }
  84. }
  85. }
  86. return $this->filterBySimilarity($availableActions, $this->command);
  87. }
  88. /**
  89. * Find suggest alternative commands based on string similarity.
  90. *
  91. * Alternatives are searched using the following steps:
  92. *
  93. * - suggest alternatives that begin with `$command`
  94. * - find typos by calculating the Levenshtein distance between the unknown command and all
  95. * available commands. The Levenshtein distance is defined as the minimal number of
  96. * characters you have to replace, insert or delete to transform str1 into str2.
  97. *
  98. * @see https://secure.php.net/manual/en/function.levenshtein.php
  99. * @param array $actions available command names.
  100. * @param string $command the command to compare to.
  101. * @return array a list of suggested alternatives sorted by similarity.
  102. */
  103. private function filterBySimilarity($actions, $command)
  104. {
  105. $alternatives = [];
  106. // suggest alternatives that begin with $command first
  107. foreach ($actions as $action) {
  108. if (strpos($action, $command) === 0) {
  109. $alternatives[] = $action;
  110. }
  111. }
  112. // calculate the Levenshtein distance between the unknown command and all available commands.
  113. $distances = array_map(function ($action) use ($command) {
  114. $action = strlen($action) > 255 ? substr($action, 0, 255) : $action;
  115. $command = strlen($command) > 255 ? substr($command, 0, 255) : $command;
  116. return levenshtein($action, $command);
  117. }, array_combine($actions, $actions));
  118. // we assume a typo if the levensthein distance is no more than 3, i.e. 3 replacements needed
  119. $relevantTypos = array_filter($distances, function ($distance) {
  120. return $distance <= 3;
  121. });
  122. asort($relevantTypos);
  123. $alternatives = array_merge($alternatives, array_flip($relevantTypos));
  124. return array_unique($alternatives);
  125. }
  126. }