3
* @link https://www.yiiframework.com/
4
* @copyright Copyright (c) 2008 Yii Software LLC
5
* @license https://www.yiiframework.com/license/
10
use yii\console\controllers\HelpController;
13
* UnknownCommandException represents an exception caused by incorrect usage of a console command.
15
* @author Carsten Brandt <mail@cebe.cc>
18
class UnknownCommandException extends Exception
21
* @var string the name of the command that could not be recognized.
28
protected $application;
32
* Construct the exception.
34
* @param string $route the route of the command that could not be found.
35
* @param Application $application the console application instance involved.
36
* @param int $code the Exception code.
37
* @param \Throwable|null $previous the previous exception used for the exception chaining.
39
public function __construct($route, $application, $code = 0, $previous = null)
41
$this->command = $route;
42
$this->application = $application;
43
parent::__construct("Unknown command \"$route\".", $code, $previous);
47
* @return string the user-friendly name of this exception
49
public function getName()
51
return 'Unknown command';
55
* Suggest alternative commands for [[$command]] based on string similarity.
57
* Alternatives are searched using the following steps:
59
* - suggest alternatives that begin with `$command`
60
* - find typos by calculating the Levenshtein distance between the unknown command and all
61
* available commands. The Levenshtein distance is defined as the minimal number of
62
* characters you have to replace, insert or delete to transform str1 into str2.
64
* @see https://www.php.net/manual/en/function.levenshtein.php
65
* @return array a list of suggested alternatives sorted by similarity.
67
public function getSuggestedAlternatives()
69
$help = $this->application->createController('help');
70
if ($help === false || $this->command === '') {
73
/** @var $helpController HelpController */
74
list($helpController, $actionID) = $help;
76
$availableActions = [];
77
foreach ($helpController->getCommands() as $command) {
78
$result = $this->application->createController($command);
79
/** @var $controller Controller */
80
list($controller, $actionID) = $result;
81
if ($controller->createAction($controller->defaultAction) !== null) {
82
// add the command itself (default action)
83
$availableActions[] = $command;
86
// add all actions of this controller
87
$actions = $helpController->getActions($controller);
88
$prefix = $controller->getUniqueId();
89
foreach ($actions as $action) {
90
$availableActions[] = $prefix . '/' . $action;
94
return $this->filterBySimilarity($availableActions, $this->command);
98
* Find suggest alternative commands based on string similarity.
100
* Alternatives are searched using the following steps:
102
* - suggest alternatives that begin with `$command`
103
* - find typos by calculating the Levenshtein distance between the unknown command and all
104
* available commands. The Levenshtein distance is defined as the minimal number of
105
* characters you have to replace, insert or delete to transform str1 into str2.
107
* @see https://www.php.net/manual/en/function.levenshtein.php
108
* @param array $actions available command names.
109
* @param string $command the command to compare to.
110
* @return array a list of suggested alternatives sorted by similarity.
112
private function filterBySimilarity($actions, $command)
116
// suggest alternatives that begin with $command first
117
foreach ($actions as $action) {
118
if (strpos($action, $command) === 0) {
119
$alternatives[] = $action;
123
// calculate the Levenshtein distance between the unknown command and all available commands.
124
$distances = array_map(function ($action) use ($command) {
125
$action = strlen($action) > 255 ? substr($action, 0, 255) : $action;
126
$command = strlen($command) > 255 ? substr($command, 0, 255) : $command;
127
return levenshtein($action, $command);
128
}, array_combine($actions, $actions));
130
// we assume a typo if the levensthein distance is no more than 3, i.e. 3 replacements needed
131
$relevantTypos = array_filter($distances, function ($distance) {
132
return $distance <= 3;
134
asort($relevantTypos);
135
$alternatives = array_merge($alternatives, array_flip($relevantTypos));
137
return array_unique($alternatives);