Skip to content

Commit

Permalink
feature #29865 [Console] Added suggestions for missing packages (prze…
Browse files Browse the repository at this point in the history
…myslaw-bogusz)

This PR was squashed before being merged into the 4.3-dev branch (closes #29865).

Discussion
----------

[Console] Added suggestions for missing packages

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| License       | MIT

Currently, when someone runs one of the most common commands, e.g. `server:run`, but does not have a required package installed, they will get a general **'There are no commands defined...'** message.

This commit adds a more useful message, informing the user about a package that might be missing and suggesting a command that should be run in order to install it, e.g. `composer require symfony/web-server-bundle --dev`.

Commits
-------

423a54f [Console] Added suggestions for missing packages
  • Loading branch information
fabpot committed Feb 21, 2019
2 parents cbe8cff + 423a54f commit 3560cfd
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 0 deletions.
@@ -0,0 +1,83 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Bundle\FrameworkBundle\EventListener;

use Symfony\Component\Console\ConsoleEvents;
use Symfony\Component\Console\Event\ConsoleErrorEvent;
use Symfony\Component\Console\Exception\CommandNotFoundException;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

/**
* Suggests a package, that should be installed (via composer),
* if the package is missing, and the input command namespace can be mapped to a Symfony bundle.
*
* @author Przemysław Bogusz <przemyslaw.bogusz@tubotax.pl>
*
* @internal
*/
final class SuggestMissingPackageSubscriber implements EventSubscriberInterface
{
private const PACKAGES = [
'doctrine' => [
'fixtures' => ['DoctrineFixturesBundle', 'doctrine/doctrine-fixtures-bundle --dev'],
'mongodb' => ['DoctrineMongoDBBundle', 'doctrine/mongodb-odm-bundle'],
'_default' => ['Doctrine ORM', 'symfony/orm-pack'],
],
'generate' => [
'_default' => ['SensioGeneratorBundle', 'sensio/generator-bundle'],
],
'make' => [
'_default' => ['MakerBundle', 'symfony/maker-bundle --dev'],
],
'server' => [
'dump' => ['VarDumper Component', 'symfony/var-dumper --dev'],
'_default' => ['WebServerBundle', 'symfony/web-server-bundle --dev'],
],
];

public function onConsoleError(ConsoleErrorEvent $event): void
{
if (!$event->getError() instanceof CommandNotFoundException) {
return;
}

[$namespace, $command] = explode(':', $event->getInput()->getFirstArgument()) + [1 => ''];

if (!isset(self::PACKAGES[$namespace])) {
return;
}

if (isset(self::PACKAGES[$namespace][$command])) {
$suggestion = self::PACKAGES[$namespace][$command];
$exact = true;
} else {
$suggestion = self::PACKAGES[$namespace]['_default'];
$exact = false;
}

$error = $event->getError();

if ($error->getAlternatives() && !$exact) {
return;
}

$message = sprintf("%s\n\nYou may be looking for a command provided by the \"%s\" which is currently not installed. Try running \"composer require %s\".", $error->getMessage(), $suggestion[0], $suggestion[1]);
$event->setError(new CommandNotFoundException($message));
}

public static function getSubscribedEvents(): array
{
return [
ConsoleEvents::ERROR => ['onConsoleError', 0],
];
}
}
Expand Up @@ -13,6 +13,10 @@
<tag name="monolog.logger" channel="console" />
</service>

<service id="console.suggest_missing_package_subscriber" class="Symfony\Bundle\FrameworkBundle\EventListener\SuggestMissingPackageSubscriber">
<tag name="kernel.event_subscriber" />
</service>

<service id="console.command.about" class="Symfony\Bundle\FrameworkBundle\Command\AboutCommand">
<tag name="console.command" command="about" />
</service>
Expand Down
Expand Up @@ -12,8 +12,11 @@
namespace Symfony\Bundle\FrameworkBundle\Tests\Console;

use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Bundle\FrameworkBundle\EventListener\SuggestMissingPackageSubscriber;
use Symfony\Bundle\FrameworkBundle\Tests\TestCase;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Event\ConsoleErrorEvent;
use Symfony\Component\Console\Exception\CommandNotFoundException;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\NullOutput;
Expand Down Expand Up @@ -226,6 +229,34 @@ public function testRunOnlyWarnsOnUnregistrableCommandAtTheEnd()
$this->assertContains(trim('[WARNING] Some commands could not be registered:'), trim($display[1]));
}

public function testSuggestingPackagesWithExactMatch()
{
$result = $this->createEventForSuggestingPackages('server:dump', []);
$this->assertRegExp('/You may be looking for a command provided by/', $result);
}

public function testSuggestingPackagesWithPartialMatchAndNoAlternatives()
{
$result = $this->createEventForSuggestingPackages('server', []);
$this->assertRegExp('/You may be looking for a command provided by/', $result);
}

public function testSuggestingPackagesWithPartialMatchAndAlternatives()
{
$result = $this->createEventForSuggestingPackages('server', ['server:run']);
$this->assertNotRegExp('/You may be looking for a command provided by/', $result);
}

private function createEventForSuggestingPackages(string $command, array $alternatives = []): string
{
$error = new CommandNotFoundException('', $alternatives);
$event = new ConsoleErrorEvent(new ArrayInput([$command]), new NullOutput(), $error);
$subscriber = new SuggestMissingPackageSubscriber();
$subscriber->onConsoleError($event);

return $event->getError()->getMessage();
}

private function getKernel(array $bundles, $useDispatcher = false)
{
$container = $this->getMockBuilder('Symfony\Component\DependencyInjection\ContainerInterface')->getMock();
Expand Down

0 comments on commit 3560cfd

Please sign in to comment.