Symfony Console ContainerAwareCommand $this->getContainer() produces Fatal error - php

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');
}
}

Related

Autocomplete doesn't work for a Symfony Console script

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;
}
}

Doctrine Persist Data into Database with Console Command

I created the console command but during the execution I get the following error:
In ControllerTrait.php line 338:
Call to a member function has() on null
DatabaseHelper.php
<?php
namespace App\Controller;
use App\Entity\Currency;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use App\Controller\CurrencyComparison;
class DatabaseHelper extends AbstractController
{
public function insertValues()
{
$curFromAPI = new CurrencyComparison();
$curFromAPI = $curFromAPI->compareCurrencies();
$date = new \DateTime('#'.strtotime('now'));
$entityManager = $this->getDoctrine()->getManager();
$currency = new Currency();
$currency->setName('USD');
$currency->setAsk($curFromAPI->getUSD());
$currency->setLastUpdated($date);
$entityManager->persist($currency);
$entityManager->flush();
}
}
CurrencyComparison.php
<?php
namespace App\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use App\Service\DatabaseHelper;
class CurrencyComparison extends Command
{
private $databaseHelper;
public function __construct(DatabaseHelper $databaseHelper){
$this->$databaseHelper = $databaseHelper;
parent::__construct();
}
protected function configure()
{
$this
->setName('app:currency-comparison')
->setDescription('Inserts currencies into the database.')
->setHelp('Compares currencies from APIs and inserts the lower rates into the database.')
;
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$this->databaseHelper->insertValues();
$output->writeln([
'Currencies are being inserted into the database',
'============',
'',
]);
$output->writeln('Done.');
}
}
When debugging, noticed that I'm getting this error after the following line in DatabaseHelper.php:
$entityManager = $this->getDoctrine()->getManager();
What should I change or should be looking for?
Thanks.
--UPDATE--
I created a Service and tried to inject EntityManager as construct.
App/Service/DatabaseHelper.php
<?php
namespace App\Service;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
class DatabaseHelper extends AbstractController
{
private $entityManager;
public function __construct(EntityManager $entityManager){
$this->entityManager = $entityManager;
}
public function insertValues()
{
$curFromAPI = new CurrencyComparison();
$curFromAPI = $curFromAPI->compareCurrencies();
$date = new \DateTime('#'.strtotime('now'));
$this->entityManager = $this->getDoctrine()->getManager();
return dd($curFromAPI);
$currency = new Currency();
$currency->setName('USD');
$currency->setAsk($curFromAPI->getUSD());
$currency->setLastUpdated($date);
$entityManager->persist($currency);
// actually executes the queries (i.e. the INSERT query)
$entityManager->flush();
}
}
and updated Command\CurrencyComparison.php too. But when I'm trying to call execute function on CurrencyComparison.php, I cannot reach its' $this->databaseHelper->insertValues(); function. Would you mind to give any suggestions?
--UPDATE & Solution--
Removed extends AbstractController from class DatabaseHelper.
Changed __construct of DatabaseHelper and injected as follows: (EntityManagerInterface $entityManager)
removed the following line from insertValues function in DatabaseHelper.php:
$this->entityManager = $this->getDoctrine()->getManager(); and just used persist and flush functions as like above.
As your command has a dependency on DatabaseHelper that has a dependency on the Entity Manager, create the DatabaseHelper class as a service, with the Entity manager injected in via its constructor (autowire it, or specify in its service definition).
then, as per the docs here you can inject your DatabaseHelper service into the command via the constructor as it'll it be autowired by default.
I would also consider not extending the AbstractController as your DatabaseHelper is very tight in scope and wont need the entire container (assuming its only what youve posted). Just inject what you need.

Laravel 5.7: I inject bunch on classes into constructor but League\Csv\Reader is the only one that doesn't work

I have some artisan commands to perform some CLI logic.
class SyncFooCommand extends AbstractBaseSyncCommand
class SyncBarCommand extends AbstractBaseSyncCommand
class SyncBazCommand extends AbstractBaseSyncCommand
Each artisan command extends abstract class AbstractBaseSyncCommand extends Command implements SyncInterface.
Thanks to that inside the abstract parent class I can put some shared logic.
I inject Carbon or FileSystem and both work in child classes like a charm.
<?php
namespace App\Console\Commands\Sync;
use Carbon\Carbon;
use Illuminate\Console\Command;
use Illuminate\Contracts\Filesystem\Factory as Filesystem;
use League\Csv\Reader;
abstract class AbstractBaseSyncCommand extends Command implements SyncInterface
{
protected $carbon;
protected $fileSystem;
protected $reader;
public function __construct(
Carbon $carbon,
FileSystem $fileSystem,
Reader $reader
) {
parent::__construct();
$this->carbon = $carbon; // works like a charm
$this->fileSystem = $fileSystem; // works like a charm
$this->reader = $reader; // fails, why?
}
}
In SyncWhateverCommand I can easily call & use $this->carbon or $this->fileSystem but as soon as it hits the $this->reader I get:
Illuminate\Contracts\Container\BindingResolutionException : Target [League\Csv\Reader] is not instantiable while building [App\Console\Commands\Sync\SyncFooCommand].
at /home/vagrant/code/foo/vendor/laravel/framework/src/Illuminate/Container/Container.php:945
941| } else {
942| $message = "Target [$concrete] is not instantiable.";
943| }
944|
> 945| throw new BindingResolutionException($message);
946| }
947|
948| /**
949| * Throw an exception for an unresolvable primitive.
What's wrong? The installation was easy and didn't require to do any bindings. What am I missing?
To sum up:
$csv = $this->reader->createFromPath($path, 'r'); // fails, but I want to use it this way
$csv = Reader::createFromPath($path, 'r'); // works but I don't want facades because they look ugly
Maybe your app should know how to retrieve it? Like
$this->app->bind('SomeAbstraction', function ($app) {
return new Implementation();
});

Symfony 3.4 - "no commands defined in the “doctrine:schema” namespace" when attempting to run console command in class

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

Pass parameter to command from controller Symfony2

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

Categories