I have a Symfony Console app and I want to use it with autocomplete but autocoplite doesn't work. When I click tab nothing happens.
My app works successfully from the command line if I type like this:
./myapp generate-constants nomenclature
Then it prints
Hello World!, nomenclature
This is my script named myapp
#!/usr/bin/env php
<?php
require 'bootstrap.php';
use MyApp\Core\Console\GenerateConstantsCommand;
use Symfony\Component\Console\Application;
$app = new Application();
$app->add(new GenerateConstantsCommand());
$app->run();
And this is GenerateConstantsCommand.php
<?php
namespace MyApp\Core\Console;
use LogicException;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputArgument;
class GenerateConstantsCommand extends Command
{
protected function configure()
{
$this->setName('generate-constants')
->setDescription('Generate constsants')
->setHelp('Demonstration of custom commands created by Symfony Console component.')
->addArgument('nomenclature', InputArgument::REQUIRED, 'Pass the nomenclature');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$output->writeln(sprintf('Hello World!, %s', $input->getArgument('nomenclature')));
return 0;
}
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
if ($input->mustSuggestOptionValuesFor('nomenclature')) {
$suggestions->suggestValues(['json', 'xml']);
}
}
}
What I am doing wrong? Why autocomplete doesn't work?
Also I have tried with stecman/symfony-console-completion but when I run eval $(./myapp _completion --generate-hook) the cursor goes to new line and stays there forever.
I use Bash 5.0.17(1)-release, Ubuntu 20.04.4 LTS, Symfony Console 5.4.7, and PHP 7.3.26.
As per the documentation:
Make sure to install the bash-completion package: sudo apt-get install -y bash-completion.
Then run the following command ONCE (installing Symfony's bash completion script): php bin/console completion bash | sudo tee /etc/bash_completion.d/console-events-terminate.
Restart your terminal and it should work.
This is the example command that I used for testing and shell auto-completion works as expected:
<?php declare(strict_types = 1);
namespace App\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use function is_string;
use function sprintf;
class TestCommand extends Command
{
protected function configure(): void
{
$this->setName('app:test')
->setDescription('Test command')
->setHidden(false)
->addArgument('format', InputArgument::REQUIRED, 'The format');
}
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
if (true === $input->mustSuggestArgumentValuesFor('format')) {
$suggestions->suggestValues(['json', 'xml']);
}
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$format = $input->getArgument('format');
if (false === is_string($format)) {
$output->writeln('Given format is not a string');
return Command::FAILURE;
}
$output->writeln(sprintf('Format: %s', $format));
return Command::SUCCESS;
}
}
Related
I'm trying to make a custom maker with the Symfony make bundle.
The maker command looks like this:
<?php
namespace App\Maker;
use Doctrine\Common\Annotations\Annotation;
use Symfony\Bundle\MakerBundle\ConsoleStyle;
use Symfony\Bundle\MakerBundle\DependencyBuilder;
use Symfony\Bundle\MakerBundle\Generator;
use Symfony\Bundle\MakerBundle\InputConfiguration;
use Symfony\Bundle\MakerBundle\Maker\AbstractMaker;
use Symfony\Bundle\MakerBundle\Str;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
final class MakeCustomEntity extends AbstractMaker
{
public static function getCommandName(): string
{
return 'make:custom-entity';
}
public static function getCommandDescription(): string
{
return 'Creates a new entity';
}
public function configureCommand(Command $command, InputConfiguration $inputConf)
{
$command
->addArgument('entity-class', InputArgument::OPTIONAL, sprintf('Choose a name for your entity class (e.g. <fg=yellow>%s</>)', Str::asClassName(Str::getRandomTerm())));
}
public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator)
{
}
public function configureDependencies(DependencyBuilder $dependencies)
{
$dependencies->addClassDependency(
Annotation::class,
'doctrine/annotations'
);
}
}
So far so good, the custom maker shows up when listing all commands.
However I would like to write a test for this maker (inspired from the tests I have found on the bundles github):
<?php
namespace Tests\Maker;
use App\Maker\MakeCustomEntity;
use Symfony\Bundle\MakerBundle\Test\MakerTestCase;
use Symfony\Bundle\MakerBundle\Test\MakerTestDetails;
class MakeCustomEntityTest extends MakerTestCase
{
public function getTestDetails()
{
yield 'entity_full_custom_namespace' => [
MakerTestDetails::createTest(
$this->getMakerInstance(MakeCustomEntity::class),
[
// entity class name
'\App\Domain\Entity\Test\Test',
]
)
->assert(function (string $output, string $directory) {
$this->assertStringContainsString('created: src/Domain/Entity/Test/Test.php', $output);
}),
];
}
}
When I try to run this test I get the following warning and test doesn't fail even though it should:
The data provider specified for Tests\Maker\MakeCustomEntityTest::testExecute is invalid.
You have requested a non-existent service "maker.maker.make_custom_entity". Did you mean one of these: "maker.maker.make_authenticator",...
Is this the correct way to testing custom makers? What should I do to avoid this?
I'm following along with this blog to create unit tests that use my data fixtures as a base. The relevant code:
namespace Tests\AppBundle\Repository;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Input\StringInput;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
class DataFixtureTestCase extends WebTestCase
{
protected $client, $container, $em;
protected static $application;
/**
* {#inheritdoc}
*/
protected function setUp()
{
self::runCommand('doctrine:schema:drop --force');
self::runCommand('doctrine:schema:create');
self::runCommand('doctrine:fixtures:load --append --no-interaction --force');
$this->client = static::createClient();
$this->container = $this->client->getContainer();
$this->em = $this->container->get('doctrine')->getManager();
}
protected static function runCommand($command)
{
$command = sprintf('%s --quiet', $command);
try {
return self::getApplication()->run(new StringInput($command));
} catch(\Exception $e) {
echo $e->getMessage();
}
}
protected static function getApplication()
{
if (null === self::$application) {
$client = static::createClient();
self::$application = new Application($client->getKernel());
self::$application->setAutoExit(false);
}
return self::$application;
}
// other methods not relevant to the question
My unit test classes extend DataFixtureTestCase. When setUp() is invoked, I keep getting errors like:
There are no commands defined in the "doctrine:schema" namespace
Any ideas on what's causing this? My research hasn't revealed the cause of the error. Doctrine is correctly installed via Composer, and I can run the console commands manually in a terminal.
Your problem is that you are using the wrong Application class for this.
Have a look at your bin/console php file. github link
Change:
use Symfony\Component\Console\Application;
to
use Symfony\Bundle\FrameworkBundle\Console\Application;
doctrine/doctrine-bundle and doctrine/doctrine-fixtures-bundle should be installed to run doctrine:schema and doctrine:fixtures commands
As suggested by Denis, try adding Doctrine bundles via componser:
composer require doctrine/doctrine-bundle
composer require doctrine/doctrine-fixtures-bundle --dev
I'm trying to spec Symfony command and I want to have formated output with SymfonyStyle
<?php
namespace Acme\AppBundle\Command;
use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
class SomeCommand extends ContainerAwareCommand
{
//....
protected function execute(InputInterface $input, OutputInterface $output)
{
$io = new SymfonyStyle($input, $output);
$io->title('Feed import initiated');
}
}
and spec file:
<?php
namespace spec\Acme\AppBundle\Command;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\BufferedOutput;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
class SomeCommandSpec extends ObjectBehavior
{
//...
function it_fetches_social_feeds(
ContainerInterface $container,
InputInterface $input,
OutputInterface $output,
SymfonyStyle $symfonyStyle
) {
// With options
$input->bind(Argument::any())->shouldBeCalled();
$input->hasArgument('command')->shouldBeCalled();
$input->isInteractive()->shouldBeCalled();
$input->validate()->shouldBeCalled();
$symfonyStyle->title(Argument::any())->shouldBeCalled();
$this->setContainer($container);
$this->run($input, $output);
}
}
but I'm getting this error:
exception [err:Error("__clone method called on non-object")] has been thrown.
0 vendor/symfony/symfony/src/Symfony/Component/Console/Style/SymfonyStyle.php:50
throw new PhpSpec\Exception\ErrorException("__clone method called on ...")
1 vendor/symfony/symfony/src/Symfony/Component/Console/Application.php:866
Symfony\Component\Console\Command\Command->run([obj:Symfony\Component\Console\Input\ArgvInput], [obj:Symfony\Component\Console\Output\ConsoleOutput])
2 vendor/symfony/symfony/src/Symfony/Component/Console/Application.php:193
Symfony\Component\Console\Application->doRunCommand([obj:PhpSpec\Console\Command\RunCommand], [obj:Symfony\Component\Console\Input\ArgvInput], [obj:Symfony\Component\Console\Output\ConsoleOutput])
3 vendor/phpspec/phpspec/src/PhpSpec/Console/Application.php:102
Symfony\Component\Console\Application->doRun([obj:Symfony\Component\Console\Input\ArgvInput], [obj:Symfony\Component\Console\Output\ConsoleOutput])
4 vendor/phpspec/phpspec/bin/phpspec:26
Symfony\Component\Console\Application->run()
5 vendor/phpspec/phpspec/bin/phpspec:28
{closure}("3.2.2")
on line 50 of SymfonyStyle is:
public function __construct(InputInterface $input, OutputInterface $output)
{
$this->input = $input;
/* line 50 */ $this->bufferedOutput = new BufferedOutput($output->getVerbosity(), false, clone $output->getFormatter());
// Windows cmd wraps lines as soon as the terminal width is reached, whether there are following chars or not.
$this->lineLength = min($this->getTerminalWidth() - (int) (DIRECTORY_SEPARATOR === '\\'), self::MAX_LINE_LENGTH);
parent::__construct($output);
}
and phpspec is complainig about
clone $output->getFormatter()
Am I doing something wrong, or am I missing something?
Update
this is my let method:
function let(SymfonyStyle $symfonyStyle, InputInterface $input, OutputInterface $output)
{
$symfonyStyle->beConstructedWith([$input->getWrappedObject(), $output->getWrappedObject()]);
}
You are missing this
function it_fetches_social_feeds(
ContainerInterface $container,
InputInterface $input,
OutputInterface $output,
SymfonyStyle $symfonyStyle
) {
// .... other code
$prophet = new Prophet();
$formatter = $prophet->prophesize(OutputFormatterInterface::class);
$output->getFormatter()->willReturn($formatter);
// .... more code
}
You can place it where you want but before the call is done.
In that way you have created a Stub that is basically a Double with behavior and without expectations. You can see it as a "proxy" that will intercept method calls and returns what you "teach" it to return.
In your example things get broken as your double OutputInterface would return null as it's not a "real" object.
I also suggest to stub the getVerbosity behavior if you need to do different kind of specs.
BTW you can read more about doubles in phpspec guide and prophecy guide
Looking at an example, SymfonyStyle never seems to be passed in the spec class.
The execute function of the command doesn't pass it either, just the input and output classes (the style class it created as a local variable).
I have a command which executes some actions that depend on the entity passed in parameter.
checkAlertCommand.php:
<?php
namespace MDB\PlatformBundle\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class checkAlertCommand extends Command {
protected function configure() {
$this
->setName('platform:checkAlert')
->setDescription('Check the alert in in function of the current advert')
->addArgument(
'postedAdvert'
);
}
protected function execute(InputInterface $input, OutputInterface $output) {
$postedAdvert = $input->getArgument('postedAdvert');
$output->writeln($postedAdvert->getTitre());
}
}
?>
So my questions are:
How to get an entity as argument in the checkAlertCommand.php?
How to call this command from a controller and pass the desired entity as argument?
Thanks.
You can't pass an entity directly to a console command. Instead of you should pass "id" of entity as argument, then use repository and pick up desired entity by its id.
<?php
namespace MDB\PlatformBundle\Command;
use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class checkAlertCommand extends ContainerAwareCommand {
protected function configure() {
$this
->setName('platform:checkAlert')
->setDescription('Check the alert in in function of the current advert')
->addArgument(
'postedAdvertId'
);
}
protected function execute(InputInterface $input, OutputInterface $output) {
$postedAdvertId = $input->getArgument('postedAdvertId');
$em = $this->getContainer()->get('doctrine')->getManager();
$repo = $em->getRepository('MDBPlatformBundle:PostedAdvert');
$postedAdvert = $repo->find($postedAdvertId);
$output->writeln($postedAdvert->getTitre());
}
}
?>
You should use Process component to run the command inside a controller.
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\NullOutput;
use MDB\PlatformBundle\Command\checkAlertCommand;
class MyController extends Controller
{
public function indexAction()
{
// get post $postedAdvertId here
....
$command = new checkAlertCommand();
$command->setContainer($this->container);
$input = new ArrayInput(array('postedAdvertId' => $postedAdvertId));
$output = new NullOutput();
$result = $command->run($input, $output);
...
}
}
Update: Answer to your question
I'm not sure what exactly do you mean "asynchronous", but given example executes the command in synchronous way, so mean the controller will wait till command will be finished and only then will go to next operation. But if you need run it in asynchronous(in background) way,you should use Process component http://symfony.com/doc/current/components/process.html
TL;DR
I'm creating console Garbage collector, which should be able to get services from container.
It's basic, almost straight from the manual:
<?php
namespace SomeBundle\Console\Command;
use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand,
Symfony\Component\Console\Input\InputArgument,
Symfony\Component\Console\Input\InputInterface,
Symfony\Component\Console\Input\InputOption,
Symfony\Component\Console\Output\OutputInterface;
class GarbageCollector extends ContainerAwareCommand
{
protected function configure()
{
$this
->setName('garbage:collect')
->setDescription('Collect garbage')
->addArgument(
'task',
InputArgument::REQUIRED,
'What task to execute?'
)
;
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$task = $input->getArgument('task');
//...
$_container = $this->getContainer();
}
}
Then I'm trying to call it from console via application.php:
#!/usr/bin/env php
<?php
// application.php
require_once __DIR__.'/../vendor/autoload.php';
use SomeBundle\Console\Command\GarbageCollector;
use Symfony\Bundle\FrameworkBundle\Console\Application;
$application = new Application();
$application->add(new GarbageCollector);
$application->run();
Which produces fatal error:
Argument 1 passed to
Symfony\Bundle\FrameworkBundle\Console\Application::__construct() must
implement interface Symfony\Component\HttpKernel\Kernel Interface,
none given
Manual says that only thing I need to do is to extend my class with ContainerAwareCommand, but something missing. I've wrote some crap code to pass Kernel to the Application():
#!/usr/bin/env php
<?php
// application.php
require_once __DIR__.'/../vendor/autoload.php';
require_once __DIR__.'/AppKernel.php';
use SomeBundle\Console\Command\GarbageCollector;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Component\Console\Input\ArgvInput;
$input = new ArgvInput();
$env = $input->getParameterOption(array('--env', '-e'), getenv('SYMFONY_ENV') ?: 'dev');
$debug = getenv('SYMFONY_DEBUG') !== '0' && !$input->hasParameterOption(array('--no-debug', '')) && $env !== 'prod';
$kernel = new AppKernel($env, $debug);
$application = new Application($kernel);
$application->add(new GarbageCollector);
$application->run();
And it works, but feels disgusting.
What do I need to make ContainerAwareCommand implementation of console app? Thanks in advance.
Symfony2 provides via its console runner the ability to call your custom commands and it will handle the injection of the service container. All you need to do is create your command in a registered bundle and it will automatically become available to you. You are initializing your command outside of the framework that is why the container is not available to you unless you manually inject it.
You can reference the cookbook here:
http://symfony.com/doc/current/cookbook/console/console_command.html
You can get a list of all the commands available by running:
php app/console
On a side note, why are you creating a garbage collector in/for PHP? Just curious because to my understanding memory is freed for you after script execution has ended.
As by version 4.1 you can inject the container service at the constructor.
namespace App\Commands;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
class MyCommand extends Command {
protected $container;
public function __construct(ContainerInterface $container) {
$this->container = $container;
parent::__construct();
}
protected function configure() {
// ...
}
protected function execute(InputInterface $input, OutputInterface $output) {
// You must make Some\Service\Class public.
// Documentation at https://symfony.com/doc/current/service_container.html#public-versus-private-services
$some_service = $this->container->get('Some\Service\Class');
}
}