mirror of
				https://github.com/yiisoft/yii2.git
				synced 2025-11-04 14:46:19 +08:00 
			
		
		
		
	implement suggestion for unknown command in console application
suggestion is based on two principles: - first suggest commands the begin with the unknown name, to suggest commands after accidentally hitting enter - second find similar commands by computing the levenshtein distance which is a measurement on how many changes need to be made to convert one string into another. This is perfect for finding typos.
This commit is contained in:
		@ -41,6 +41,7 @@ Yii Framework 2 Change Log
 | 
			
		||||
- Enh #12399: Added `ActiveField::addAriaAttributes` property for `aria-required` and `aria-invalid` attributes rendering (Oxyaction, samdark)
 | 
			
		||||
- Enh #12390: Avoid creating queries with false where condition (`0=1`) when fetching relational data (klimov-paul)
 | 
			
		||||
- Enh #12619: Added catch `Throwable` in `yii\base\ErrorHandler::handleException()` (rob006)
 | 
			
		||||
- Enh #12659: Suggest alternatives when console command was not found (mdmunir, cebe)
 | 
			
		||||
- Enh #12726: `yii\base\Application::$version` converted to `yii\base\Module::$version` virtual property, allowing to specify version as a PHP callback (klimov-paul)
 | 
			
		||||
- Enh #12738: Added support for creating protocol-relative URLs in `UrlManager::createAbsoluteUrl()` and `Url` helper methods (rob006)
 | 
			
		||||
- Enh #12748: Added Migration tool automatic generation reference column for foreignKey (MKiselev)
 | 
			
		||||
 | 
			
		||||
@ -32,9 +32,9 @@ class ErrorHandler extends \yii\base\ErrorHandler
 | 
			
		||||
        if ($exception instanceof UnknownCommandException) {
 | 
			
		||||
            // display message and suggest alternatives in case of unknown command
 | 
			
		||||
            $message = $this->formatMessage($exception->getName() . ': ') . $exception->command;
 | 
			
		||||
            $alternatives = $exception->suggestAlternatives();
 | 
			
		||||
            if (count($alternatives) == 1) {
 | 
			
		||||
                $message .= "\n\nDid you mean  " . reset($alternatives) . " ?";
 | 
			
		||||
            $alternatives = $exception->getSuggestedAlternatives();
 | 
			
		||||
            if (count($alternatives) === 1) {
 | 
			
		||||
                $message .= "\n\nDid you mean \"" . reset($alternatives) . "\"?";
 | 
			
		||||
            } elseif (count($alternatives) > 1) {
 | 
			
		||||
                $message .= "\n\nDid you mean one of these?\n    - " . implode("\n    - ", $alternatives);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
@ -6,22 +6,35 @@
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace yii\console;
 | 
			
		||||
 | 
			
		||||
use yii\console\controllers\HelpController;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Exception represents an exception caused by incorrect usage of a console command.
 | 
			
		||||
 * UnknownCommandException represents an exception caused by incorrect usage of a console command.
 | 
			
		||||
 *
 | 
			
		||||
 * @author Carsten Brandt <mail@cebe.cc>
 | 
			
		||||
 * @since 2.0.11
 | 
			
		||||
 */
 | 
			
		||||
class UnknownCommandException extends Exception
 | 
			
		||||
{
 | 
			
		||||
    /**
 | 
			
		||||
     * @var string the name of the command that could not be recognized.
 | 
			
		||||
     */
 | 
			
		||||
    public $command;
 | 
			
		||||
    /**
 | 
			
		||||
     * @var Application
 | 
			
		||||
     */
 | 
			
		||||
    public $application;
 | 
			
		||||
    protected $application;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Construct the exception.
 | 
			
		||||
     *
 | 
			
		||||
     * @param string $route the route of the command that could not be found.
 | 
			
		||||
     * @param Application $application the console application instance involved.
 | 
			
		||||
     * @param int $code the Exception code.
 | 
			
		||||
     * @param \Exception $previous the previous exception used for the exception chaining.
 | 
			
		||||
     */
 | 
			
		||||
    public function __construct($route, $application, $code = 0, \Exception $previous = null)
 | 
			
		||||
    {
 | 
			
		||||
        $this->command = $route;
 | 
			
		||||
@ -37,7 +50,20 @@ class UnknownCommandException extends Exception
 | 
			
		||||
        return 'Unknown command';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function suggestAlternatives()
 | 
			
		||||
    /**
 | 
			
		||||
     * Suggest alternative commands for [[$command]] based on string similarity.
 | 
			
		||||
     *
 | 
			
		||||
     * Alternatives are searched using the following steps:
 | 
			
		||||
     *
 | 
			
		||||
     * - suggest alternatives that begin with `$command`
 | 
			
		||||
     * - find typos by calculating the Levenshtein distance between the unknown command and all
 | 
			
		||||
     *   available commands. The Levenshtein distance is defined as the minimal number of
 | 
			
		||||
     *   characters you have to replace, insert or delete to transform str1 into str2.
 | 
			
		||||
     *
 | 
			
		||||
     * @see http://php.net/manual/en/function.levenshtein.php
 | 
			
		||||
     * @return array a list of suggested alternatives sorted by similarity.
 | 
			
		||||
     */
 | 
			
		||||
    public function getSuggestedAlternatives()
 | 
			
		||||
    {
 | 
			
		||||
        $help = $this->application->createController('help');
 | 
			
		||||
        if ($help === false) {
 | 
			
		||||
@ -67,15 +93,49 @@ class UnknownCommandException extends Exception
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        $availableActions = $this->filterBySimilarity($availableActions);
 | 
			
		||||
 | 
			
		||||
        asort($availableActions);
 | 
			
		||||
        return $availableActions;
 | 
			
		||||
        return $this->filterBySimilarity($availableActions, $this->command);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function filterBySimilarity($actions)
 | 
			
		||||
    /**
 | 
			
		||||
     * Find suggest alternative commands based on string similarity.
 | 
			
		||||
     *
 | 
			
		||||
     * Alternatives are searched using the following steps:
 | 
			
		||||
     *
 | 
			
		||||
     * - suggest alternatives that begin with `$command`
 | 
			
		||||
     * - find typos by calculating the Levenshtein distance between the unknown command and all
 | 
			
		||||
     *   available commands. The Levenshtein distance is defined as the minimal number of
 | 
			
		||||
     *   characters you have to replace, insert or delete to transform str1 into str2.
 | 
			
		||||
     *
 | 
			
		||||
     * @see http://php.net/manual/en/function.levenshtein.php
 | 
			
		||||
     * @param array $actions available command names.
 | 
			
		||||
     * @param string $command the command to compare to.
 | 
			
		||||
     * @return array a list of suggested alternatives sorted by similarity.
 | 
			
		||||
     */
 | 
			
		||||
    private function filterBySimilarity($actions, $command)
 | 
			
		||||
    {
 | 
			
		||||
        // TODO
 | 
			
		||||
        return $actions;
 | 
			
		||||
        $alternatives = [];
 | 
			
		||||
 | 
			
		||||
        // suggest alternatives that begin with $command first
 | 
			
		||||
        foreach ($actions as $action) {
 | 
			
		||||
            if (strpos($action, $command) === 0) {
 | 
			
		||||
                $alternatives[] = $action;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // calculate the Levenshtein distance between the unknown command and all available commands.
 | 
			
		||||
        $distances = array_map(function($action) use ($command) {
 | 
			
		||||
            $action = strlen($action) > 255 ? substr($action, 0, 255) : $action;
 | 
			
		||||
            $command = strlen($command) > 255 ? substr($command, 0, 255) : $command;
 | 
			
		||||
            return levenshtein($action, $command);
 | 
			
		||||
        }, array_combine($actions, $actions));
 | 
			
		||||
 | 
			
		||||
        // we assume a typo if the levensthein distance is no more than 3, i.e. 3 replacements needed
 | 
			
		||||
        $relevantTypos = array_filter($distances, function($distance) {
 | 
			
		||||
            return $distance <= 3;
 | 
			
		||||
        });
 | 
			
		||||
        asort($relevantTypos);
 | 
			
		||||
        $alternatives = array_merge($alternatives, array_flip($relevantTypos));
 | 
			
		||||
 | 
			
		||||
        return array_unique($alternatives);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										70
									
								
								tests/framework/console/UnkownCommandExceptionTest.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								tests/framework/console/UnkownCommandExceptionTest.php
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,70 @@
 | 
			
		||||
<?php
 | 
			
		||||
/**
 | 
			
		||||
 * @link http://www.yiiframework.com/
 | 
			
		||||
 * @copyright Copyright (c) 2008 Yii Software LLC
 | 
			
		||||
 * @license http://www.yiiframework.com/license/
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace yiiunit\framework\console;
 | 
			
		||||
 | 
			
		||||
use Yii;
 | 
			
		||||
use yii\console\Application;
 | 
			
		||||
use yii\console\UnknownCommandException;
 | 
			
		||||
use yiiunit\TestCase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @group console
 | 
			
		||||
 */
 | 
			
		||||
class UnkownCommandExceptionTest extends TestCase
 | 
			
		||||
{
 | 
			
		||||
    public function setUp()
 | 
			
		||||
    {
 | 
			
		||||
        $this->mockApplication([
 | 
			
		||||
            'enableCoreCommands' => false,
 | 
			
		||||
            'controllerMap' => [
 | 
			
		||||
                'cache' => 'yii\console\controllers\CacheController',
 | 
			
		||||
                'migrate' => 'yii\console\controllers\MigrateController',
 | 
			
		||||
                'message' => 'yii\console\controllers\MessageController',
 | 
			
		||||
            ],
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function suggestedCommandsProvider()
 | 
			
		||||
    {
 | 
			
		||||
        return [
 | 
			
		||||
            ['migate', ['migrate']],
 | 
			
		||||
            ['mihate/u', ['migrate/up']],
 | 
			
		||||
            ['mirgte/u', ['migrate/up']],
 | 
			
		||||
            ['mirgte/up', ['migrate/up']],
 | 
			
		||||
            ['mirgte', ['migrate']],
 | 
			
		||||
            ['hlp', ['help']],
 | 
			
		||||
            ['ca', ['cache', 'cache/flush', 'cache/flush-all', 'cache/flush-schema', 'cache/index']],
 | 
			
		||||
            ['cach', ['cache', 'cache/flush', 'cache/flush-all', 'cache/flush-schema', 'cache/index']],
 | 
			
		||||
            ['cach/fush', ['cache/flush']],
 | 
			
		||||
            ['cach/fushall', ['cache/flush-all']],
 | 
			
		||||
            ['what?', []],
 | 
			
		||||
            // test UTF 8 chars
 | 
			
		||||
            ['ёлка', []],
 | 
			
		||||
            // this crashes levenshtein because string is longer than 255 chars
 | 
			
		||||
            [str_repeat('asdw1234', 31), []],
 | 
			
		||||
            [str_repeat('asdw1234', 32), []],
 | 
			
		||||
            [str_repeat('asdw1234', 33), []],
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @dataProvider suggestedCommandsProvider
 | 
			
		||||
     */
 | 
			
		||||
    public function testSuggestCommand($command, $expectedSuggestion)
 | 
			
		||||
    {
 | 
			
		||||
        $exception = new UnknownCommandException($command, Yii::$app);
 | 
			
		||||
        $this->assertEquals($expectedSuggestion, $exception->getSuggestedAlternatives());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function testNameAndConstructor()
 | 
			
		||||
    {
 | 
			
		||||
        $exception = new UnknownCommandException('test', Yii::$app);
 | 
			
		||||
        $this->assertEquals('Unknown command', $exception->getName());
 | 
			
		||||
        $this->assertEquals('test', $exception->command);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user