Create Artisan command with option that must specify a value - php

Laravel's documentation says (emphasis mine):
If the user must specify a value for an option, you should suffix the option name with a = sign…
But then goes on to say:
If the option is not specified when invoking the command, its value will be null…
Which suggests that "must" doesn't mean what I think it means. And indeed that is the case. A simple command with a signature like this:
protected $signature = "mycommand {-t|test=}";
Will run just fine when called like artisan mycommand -t. And what's worse is that if you specify a default value, it isn't applied in this case.
protected $signature = "mycommand {-t|test=42}";
When running artisan mycommand, $this->option('test') will give you a value of 42, but when run as artisan mycommand -t it gives a value of null.
So, is there a way to require that a user must (actually) specify a value for a given option, if it's present on the command line?

Poking around the Laravel code, I confirmed that there is no way to have a truly "required" value. Although Symfony does provide for required values, Laravel doesn't use this capability. Instead the options are all created as optional, so I will have to write my own parser...
This was fairly straightforward; I had to write a custom parser class to override the Illuminate\Console\Parser::parseOption() method, and then override Illuminate\Console\Command::configureUsingFluentDefinition() to use that new class.
I elected to create a new option type, rather than change the behaviour of any existing command options. So now I declare my signature like this when I want to force a value:
<?php
namespace App\Console\Commands;
use App\Console\Command;
class MyCommand extends Command
{
/** #var string The double == means a required value */
protected $signature = "mycommand {--t|test==}";
...
}
Attempting to run artisan mycommand -t will now throw a Symfony\Component\Console\Exception\RuntimeException with a message of "The --test option requires a value." This also works for array options (--t==*) and/or options with default values (--t==42 or --t==*42.)
Here's the code for the new parser class:
<?php
namespace App\Console;
use Illuminate\Console\Parser as BaseParser;
use Symfony\Component\Console\Input\InputOption;
class Parser extends BaseParser
{
protected static function parseOption($token): InputOption
{
[$mytoken, $description] = static::extractDescription($token);
$matches = preg_split("/\\s*\\|\\s*/", $mytoken, 2);
if (isset($matches[1])) {
$shortcut = $matches[0];
$mytoken = $matches[1];
} else {
$shortcut = null;
}
switch (true) {
case str_ends_with($mytoken, "=="):
return new InputOption(
trim($mytoken, "="),
$shortcut,
InputOption::VALUE_REQUIRED,
$description
);
case str_ends_with($mytoken, "==*"):
return new InputOption(
trim($mytoken, "=*"),
$shortcut,
InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
$description
);
case preg_match("/(.+)==\*(.+)/", $mytoken, $matches):
return new InputOption(
$matches[1],
$shortcut,
InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
$description,
preg_split('/,\s?/', $matches[2])
);
case preg_match("/(.+)==(.+)/", $mytoken, $matches):
return new InputOption(
$matches[1],
$shortcut,
InputOption::VALUE_REQUIRED,
$description,
$matches[2]
);
default:
// no == here, fall back to the standard parser
return parent::parseOption($token);
}
}
}
And the new command class:
<?php
namespace App\Console;
use Illuminate\Console\Command as BaseCommand;
class Command extends BaseCommand
{
/**
* Overriding the Laravel parser so we can have required arguments
*
* #inheritdoc
* #throws ReflectionException
*/
protected function configureUsingFluentDefinition(): void
{
// using our parser here
[$name, $arguments, $options] = Parser::parse($this->signature);
// need to call the great-grandparent constructor here; probably
// could have hard-coded to Symfony, but better safe than sorry
$reflectionMethod = new ReflectionMethod(
get_parent_class(BaseCommand::class),
"__construct"
);
$reflectionMethod->invoke($this, $name);
$this->getDefinition()->addArguments($arguments);
$this->getDefinition()->addOptions($options);
}
}

Related

Import massive csv data with symfony command too slow with doctrine

I need to import a lot of data from a csv file (45 Mo) in myqsl database with Symfony. I imported League\Csv\Reader library
I made a command with doctrine.
It works but I is very slow.
How can I accelerate this ?
I tried to :
adding : $this->em->clear() after $this->em->flush();
adding : //Disable SQL Logging: to avoid huge memory loss.
$this->em->getConnection()->getConfiguration()->setSQLLogger(null);
.
namespace App\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use App\Entity\Developer;
use App\Entity\BadgeLabel;
use Doctrine\ORM\EntityManagerInterface;
use League\Csv\Reader;
class CsvImportCommand extends Command
{
public function __construct(EntityManagerInterface $em){
parent::__construct();
$this->em = $em;
}
// the name of the command (the part after "bin/console")
protected static $defaultName = 'app:import-developpers';
protected function configure()
{
$this
// the short description shown while running "php bin/console list"
->setDescription('Import a new developper.')
// the full command description shown when running the command with
// the "--help" option
->setHelp('This command allows you to import a develpper...')
;
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$io = new SymfonyStyle($input, $output);
$io->title('Importation en cours');
$reader = Reader::createFromPath('%kernel.root_dir%/../src/Data/developers_big.csv')
->setHeaderOffset(0)
;
$results = $reader->getrecords();
$io->progressStart(iterator_count($results));
//Disable SQL Logging: to avoid huge memory loss.
$this->em->getConnection()->getConfiguration()->setSQLLogger(null);
foreach ($results as $row) {
$developer = $this->em->getRepository(Developer::class)
->findOneBy([
'firstName' => ($row['FIRSTNAME']),
'lastName'=> ($row['LASTNAME'])
])
;
if (null === $developer) {
$developer = new developer;
$developer
->setFirstName($row['FIRSTNAME'])
->setLastName($row['LASTNAME']);
$this->em->persist($developer);
$this->em->flush();
$this->em->clear();
}
$badgeLabel = $this->em->getRepository(BadgeLabel::class)
->findOneBy([
'name' => ($row['BADGE LABEL']),
'level'=> ($row['BADGE LEVEL'])
])
;
if (null === $badgeLabel) {
$badgeLabel = new BadgeLabel;
$badgeLabel
->setName($row['BADGE LABEL'])
->setLevel($row['BADGE LEVEL']);
$this->em->persist($badgeLabel);
$this->em->flush();
$this->em->clear();
}
$developer
->addBadgeLabel($badgeLabel);
$io->progressAdvance();
}
$this->em->flush();
$this->em->clear();
$io->progressFinish();
$io->success('Importation terminée avec succès');
}
}
The command works put its to slow. After 15 min, only 32% was updload in my Mysql database. I Expected it in 2 minutes max
Method1: (not the best)
When flush method is called, Symfony go throught all listeners. So, you could avoid to flush on each loop. You can replace each flush by this code:
if (0 === ($batchSize++ % $input->getOption('fetch'))) {
$this->entityManager->flush();
$this->entityManager->clear();
}
fetch option can be declared in configure method:
const BATCH_SIZE = 1000; // As example
/**
* Configure the command.
*/
protected function configure()
{
$this
// the short description shown while running "php bin/console list"
->setDescription('Import a new developper.')
//This option helps you to find a good value and use BATCH_SIZE constant as default
->addOption('fetch', 'f', InputArgument::OPTIONAL, 'Number of loop between each flush', self::BATCH_SIZE)
// the full command description shown when running the command with
// the "--help" option
->setHelp('This command allows you to import a develpper...')
;
Method2: More efficient
You can create a command which writes all SQL queries with update or insert in a sql file. Then, you launch a native command that read the files and execute queries.
Method3: Using DBAL
As suggested in comments, youcould use DBAL to avoid unnecessary object hydration with Doctrine.

Mocking a service called by a controller from a WebTestCase

I have an API written using Symfony2 that I'm trying to write post hoc tests for. One of the endpoints uses an email service to send a password reset email to the user. I'd like to mock out this service so that I can check that the right information is sent to the service, and also prevent an email from actually being sent.
Here's the route I'm trying to test:
/**
* #Route("/me/password/resets")
* #Method({"POST"})
*/
public function requestResetAction(Request $request)
{
$userRepository = $this->get('app.repository.user_repository');
$userPasswordResetRepository = $this->get('app.repository.user_password_reset_repository');
$emailService = $this->get('app.service.email_service');
$authenticationLimitsService = $this->get('app.service.authentication_limits_service');
$now = new \DateTime();
$requestParams = $this->getRequestParams($request);
if (empty($requestParams->username)) {
throw new BadRequestHttpException("username parameter is missing");
}
$user = $userRepository->findOneByUsername($requestParams->username);
if ($user) {
if ($authenticationLimitsService->isUserBanned($user, $now)) {
throw new BadRequestHttpException("User temporarily banned because of repeated authentication failures");
}
$userPasswordResetRepository->deleteAllForUser($user);
$reset = $userPasswordResetRepository->createForUser($user);
$userPasswordResetRepository->saveUserPasswordReset($reset);
$authenticationLimitsService->logUserAction($user, UserAuthenticationLog::ACTION_PASSWORD_RESET, $now);
$emailService->sendPasswordResetEmail($user, $reset);
}
// We return 201 Created for every request so that we don't accidently
// leak the existence of usernames
return $this->jsonResponse("Created", $code=201);
}
I then have an ApiTestCase class that extends the Symfony WebTestCase to provide helper methods. This class contains a setup method that tries to mock the email service:
class ApiTestCase extends WebTestCase {
public function setup() {
$this->client = static::createClient(array(
'environment' => 'test'
));
$mockEmailService = $this->getMockBuilder(EmailService::class)
->disableOriginalConstructor()
->getMock();
$this->mockEmailService = $mockEmailService;
}
And then in my actual test cases I'm trying to do something like this:
class CreatePasswordResetTest extends ApiTestCase {
public function testSendsEmail() {
$this->mockEmailService->expects($this->once())
->method('sendPasswordResetEmail');
$this->post(
"/me/password/resets",
array(),
array("username" => $this->user->getUsername())
);
}
}
So now the trick is to get the controller to use the mocked version of the email service. I have read about several different ways to achieve this, so far I've not had much luck.
Method 1: Use container->set()
See How to mock Symfony 2 service in a functional test?
In the setup() method tell the container what it should return when it's asked for the email service:
static::$kernel->getContainer()->set('app.service.email_service', $this->mockEmailService);
# or
$this->client->getContainer()->set('app.service.email_service', $this->mockEmailService);
This does not effect the controller at all. It still calls the original service. Some write ups I've seen mention that the mocked service is 'reset' after a single call. I'm not even seeing my first call mocked out so I'm not certain this issue is affecting me yet.
Is there another container I should be calling set on?
Or am I mocking out the service too late?
Method 2: AppTestKernel
See: http://blog.lyrixx.info/2013/04/12/symfony2-how-to-mock-services-during-functional-tests.html
See: Symfony2 phpunit functional test custom user authentication fails after redirect (session related)
This one pulls me out of my depth when it comes to PHP and Symfony2 stuff (I'm not really a PHP dev).
The goal seems to be to change some kind of foundation class of the website to allow my mock service to be injected very early in the request.
I have a new AppTestKernel:
<?php
// app/AppTestKernel.php
require_once __DIR__.'/AppKernel.php';
class AppTestKernel extends AppKernel
{
private $kernelModifier = null;
public function boot()
{
parent::boot();
if ($kernelModifier = $this->kernelModifier) {
$kernelModifier($this);
$this->kernelModifier = null;
};
}
public function setKernelModifier(\Closure $kernelModifier)
{
$this->kernelModifier = $kernelModifier;
// We force the kernel to shutdown to be sure the next request will boot it
$this->shutdown();
}
}
And a new method in my ApiTestCase:
// https://stackoverflow.com/a/19705215
protected static function getKernelClass(){
$dir = isset($_SERVER['KERNEL_DIR']) ? $_SERVER['KERNEL_DIR'] : static::getPhpUnitXmlDir();
$finder = new Finder();
$finder->name('*TestKernel.php')->depth(0)->in($dir);
$results = iterator_to_array($finder);
if (!count($results)) {
throw new \RuntimeException('Either set KERNEL_DIR in your phpunit.xml according to http://symfony.com/doc/current/book/testing.html#your-first-functional-test or override the WebTestCase::createKernel() method.');
}
$file = current($results);
$class = $file->getBasename('.php');
require_once $file;
return $class;
}
Then I alter my setup() to use the kernel modifier:
public function setup() {
...
$mockEmailService = $this->getMockBuilder(EmailService::class)
->disableOriginalConstructor()
->getMock();
static::$kernel->setKernelModifier(function($kernel) use ($mockEmailService) {
$kernel->getContainer()->set('app.service.email_service', $mockEmailService);
});
$this->mockEmailService = $mockEmailService;
}
This works! However I now can't access the container in my other tests when I'm trying to do something like this:
$c = $this->client->getKernel()->getContainer();
$repo = $c->get('app.repository.user_password_reset_repository');
$resets = $repo->findByUser($user);
The getContainer() method returns null.
Should I be using the container differently?
Do I need to inject the container into the new kernel? It extends the original kernel so I don't really know why/how it's any different when it comes to the container stuff.
Method 3: Replace the service in config_test.yml
See: Symfony/PHPUnit mock services
This method requires that I write a new service class that overrides the email service. Writing a fixed mock class like this seems less useful than a regular dynamic mock. How can I test that certain methods have been called with certain parameters?
Method 4: Setup everything inside the test
Going on #Matteo's suggestion I wrote a test that did this:
public function testSendsEmail() {
$mockEmailService = $this->getMockBuilder(EmailService::class)
->disableOriginalConstructor()
->getMock();
$mockEmailService->expects($this->once())
->method('sendPasswordResetEmail');
static::$kernel->getContainer()->set('app.service.email_service', $mockEmailService);
$this->client->getContainer()->set('app.service.email_service', $mockEmailService);
$this->post(
"/me/password/resets",
array(),
array("username" => $this->user->getUsername())
);
}
This test fails because the expected method sendPasswordResetEmail wasn't called:
There was 1 failure:
1) Tests\Integration\Api\MePassword\CreatePasswordResetTest::testSendsEmail
Expectation failed for method name is equal to <string:sendPasswordResetEmail> when invoked 1 time(s).
Method was expected to be called 1 times, actually called 0 times.
Thanks to Cered's advice I've managed to get something working that can test that the emails I expect to be sent actually are. I haven't been able to actually get the mocking to work so I'm a bit reluctant to mark this as "the" answer.
Here's a test that checks that an email is sent:
public function testSendsEmail() {
$this->client->enableProfiler();
$this->post(
"/me/password/resets",
array(),
array("username" => $this->user->getUsername())
);
$mailCollector = $this->client->getProfile()->getCollector('swiftmailer');
$this->assertEquals(1, $mailCollector->getMessageCount());
$collectedMessages = $mailCollector->getMessages();
$message = $collectedMessages[0];
$this->assertInstanceOf('Swift_Message', $message);
$this->assertEquals('Reset your password', $message->getSubject());
$this->assertEquals('info#example.com', key($message->getFrom()));
$this->assertEquals($this->user->getEmail(), key($message->getTo()));
$this->assertContains(
'This link is valid for 24 hours only.',
$message->getBody()
);
$resets = $this->getResets($this->user);
$this->assertContains(
$resets[0]->getToken(),
$message->getBody()
);
}
It works by enabling the Symfony profiler and inspecting the swiftmailer service. It's documented here: http://symfony.com/doc/current/email/testing.html

How to create a console command in Symfony2 application

I need to create a console command for a Symfony2 application and I read docs here and here though I am not sure what of those I should follow. So this is what I did.
Create a file under /src/PDI/PDOneBundle/Console/PDOneSyncCommand.php
Write this code:
namespace PDI\PDOneBundle\Console\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 PDOneSyncCommand extends Command
{
protected function configure()
{
$this
->setName('pdone:veeva:sync')
->setDescription('Some description');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$name = $input->getArgument('name');
if ($name) {
$text = 'Hello '.$name;
} else {
$text = 'Hello';
}
if ($input->getOption('yell')) {
$text = strtoupper($text);
}
$output->writeln($text);
}
}
Create a file under /bin
Write this code:
! /usr/bin/env php
require __ DIR __ .'/vendor/autoload.php';
use PDI\PDOneBundle\Console\Command\PDOneSyncCommand;
use Symfony\Component\Console\Application;
$application = new Application();
$application->add(new PDOneSyncCommand());
$application->run();
But when I go to console by running php app/console --shell and hit ENTER I can't see the command registered, what I am missing?
NOTE: Can someone with more experience than me format the second piece of code properly?
UPDATE 1
Ok, following suggestions and taking answer as a start point I built this piece of code:
protected function execute(InputInterface $input, OutputInterface $output)
{
$container = $this->getContainer();
$auth_url = $container->get('login_uri')."/services/oauth2/authorize?response_type=code&client_id=".$container->get('client_id')."&redirect_uri=".urlencode($container->get('redirect_uri'));
$token_url = $container->get('login_uri')."/services/oauth2/token";
$revoke_url = $container->get('login_uri')."/services/oauth2/revoke";
$code = $_GET['code'];
if (!isset($code) || $code == "") {
die("Error - code parameter missing from request!");
}
$params = "code=".$code
."&grant_type=".$container->get('grant_type')
."&client_id=".$container->get('client_id')
."&client_secret=".$container->get('client_secret')
."&redirect_uri=".urlencode($container->get('redirect_uri'));
$curl = curl_init($token_url);
curl_setopt($curl, CURLOPT_HEADER, false);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_POST, true);
curl_setopt($curl, CURLOPT_POSTFIELDS, $params);
$json_response = curl_exec($curl);
$status = curl_getinfo($curl, CURLINFO_HTTP_CODE);
if ($status != 200) {
die("Error: call to token URL $token_url failed with status $status, response $json_response, curl_error ".curl_error(
$curl
).", curl_errno ".curl_errno($curl));
}
curl_close($curl);
$response = json_decode($json_response, true);
$access_token = $response['access_token'];
$instance_url = $response['instance_url'];
if (!isset($access_token) || $access_token == "") {
die("Error - access token missing from response!");
}
if (!isset($instance_url) || $instance_url == "") {
die("Error - instance URL missing from response!");
}
$output->writeln('Access Token ' . $access_token);
$output->writeln('Instance Url ' . $instance_url);
}
But any time I invoke the task I got this error:
[Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException]
You have requested a non-existent service "login_uri".
Why? Can't I access paramters on parameter.yml file? Where I am failing?
You are reading article about Console Component. This is slightly different than registering a command in your bundle.
First, your class should live in Namespace Command, and it must include the Command prefix in classname. You've mostly done that. I will show you a sample command to grasp the idea so you can continue working with that as a base.
<?php
namespace AppBundle\Command;
use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
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;
// I am extending ContainerAwareCommand so that you can have access to $container
// which you can see how it's used in method execute
class HelloCommand extends ContainerAwareCommand {
// This method is used to register your command name, also the arguments it requires (if needed)
protected function configure() {
// We register an optional argument here. So more below:
$this->setName('hello:world')
->addArgument('name', InputArgument::OPTIONAL);
}
// This method is called once your command is being called fron console.
// $input - you can access your arguments passed from terminal (if any are given/required)
// $output - use that to show some response in terminal
protected function execute(InputInterface $input, OutputInterface $output) {
// if you want to access your container, this is how its done
$container = $this->getContainer();
$greetLine = $input->getArgument('name')
? sprintf('Hey there %s', $input->getArgument('name'))
: 'Hello world called without arguments passed!'
;
$output->writeln($greetLine);
}
}
Now, running app/console hello:world' you should see a simple Hello world at your terminal.
Hope you got the idea, dont hesitate to ask if you have questions.
Edit
In Commands you cant directly access request, because of scopes. But you can pass arguments when you call your command. In my example I've registered optional argument which leads to two different outputs.
If you call your command like this app/console hello:world you get this output
Hello world called without arguments passed!
but if you provide a name like this app/console hello:world Demo you get the following result:
Hey there Demo
Following Artamiel's answer and the comments below, here what you would need to build a command run as a CRON task (at least, this is how I've done it):
First, declare your SalesforceCommand class:
<?php
class SalesforceCommand extends ContainerAwareCommand
{
protected function configure()
{
$this
->setName('pdone:veeva:sync')
->setDescription('Doing some tasks, whatever...');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$myService = $this->getContainer()->get('my.service');
$returnValue = $myService->whateverAction();
if($returnValue === true)
$output->writeln('Return value of my.service is true');
else
$output->writeln('An error occured!');
}
}
Then, create your controller in whatever bundle you want:
<?php
namespace My\MyBundle\Service;
use Symfony\Component\HttpFoundation\RequestStack;
class ServiceController extends Controller
{
private $_rs;
public function __construct(RequestStack $rs)
{
$this->_rs = $rs;
}
public function whateverAction()
{
$request = $this->_rs->getCurrentRequest();
// do whatever is needed with $request.
return $expectedReturn ? true : false;
}
}
Finally, register your Controller as a Service in app/config/services.yml
services:
my.service:
class: My\MyBundle\Service\ServiceController
arguments: ["#request_stack"]
(as of Symfony 2.4, instead of injecting the request service, you should inject the request_stack service and access the Request by calling the getCurrentRequest() method)
You are finally able to use run it in as a CRON job by adding the following to your crontab (for it to run every minute):
* * * * * /usr/local/bin/php /path/to/your/project/app/console pdone:veeva:sync 1>>/path/to/your/log/std.log 2>>/path/to/your/log/err.log
Hope that helps!

Variable number of options for symfony/console component

How would one configure symfony/console to accept a dynamic list of options?
That said - the names for the options aren't known on development step so I need an application to accept everything and expose it using the standard $input->getOption.
Any chance it can be done easily (without hacking the component in million places)?
My attempts included extending the ArgvInput and InputDefinition classes but they failed due to various reasons (they are objective and symfony/console component implementation-specific). Briefly: the former requires parsing to be invoked multiple times; the latter - is instantiated in multiple places so I just couldn't find a proper way to inject it.
You can create your own ArgvInput that will allow all options.
For example you can see the slightly modified version of ArgvInput here
I have only modified lines : 178
And comment out the lines: 188-199
Then pass instance of your version ArgvInput instead of default one to
$input = new AcceptAllArgvInput();
$kernel = new AppKernel($env, $debug);
$application = new Application($kernel);
$application->run($input);
I have accomplished this in the past using the IS_ARRAY option. Would this not work for your instance as well?
->addArgument('routeParams', InputArgument::IS_ARRAY, "Required Placeholders for route");
My use case was a custom URL generator for a special authentication system. I needed a way to generate URLs for testing. Naturally, each route has a different number of required parameters and I wanted to avoid passing the parameters as a CSV string.
Command examples:
Usage: myGenerateToken user route [variables1] ... [variablesN]
php app/console myGenerateToken 1 productHomePage
php app/console myGenerateToken 1 getProduct 1
php app/console myGenerateToken 1 getProductFile 1 changelog
The variables were delivered to the command in the "routeParams" as an array
$params = $input->getArgument('routeParams');
var_dump($params);
array(2) {
[0] =>
string(1) "1"
[1] =>
string(9) "changelog"
}
I noticed that there is also an "Option" version called InputOption::VALUE_IS_ARRAY, but I did not have success getting it to work. The argument version InputArgument::IS_ARRAY seems to behave as an option anyways, as it does not error if no arguments are specified.
EDIT:
The author's question is seeking "How do i define variable command line options at run time" where my answer is "How do you provide multiple values for a pre-defined option/argument"
Here is how to implement this on PHP 7+ using symfony/console ^3.0:
abstract class CommandWithDynamicOptions extends Command {
/** #var array The list of dynamic options passed to the command */
protected $dynamicOptions = [];
/**
* #inheritdoc
*/
protected function configure() {
$this->setName('custom:command');
$this->setDefinition(new class($this->getDefinition(), $this->dynamicOptions) extends InputDefinition {
protected $dynamicOptions = [];
public function __construct(InputDefinition $definition, array &$dynamicOptions) {
parent::__construct();
$this->setArguments($definition->getArguments());
$this->setOptions($definition->getOptions());
$this->dynamicOptions =& $dynamicOptions;
}
public function getOption($name) {
if (!parent::hasOption($name)) {
$this->addOption(new InputOption($name, null, InputOption::VALUE_OPTIONAL));
$this->dynamicOptions[] = $name;
}
return parent::getOption($name);
}
public function hasOption($name) {
return TRUE;
}
});
}
}
Another approach bypassing Symfony validation and reading from argv directly:
class Foo extends Command {
protected function configure() {
$this->ignoreValidationErrors();
}
protected function execute(InputInterface $input, OutputInterface $output): int {
global $argv;
$customOptions = array_filter($argv, static function ($value) {
return is_string($value) && substr($value, 0, 2) === '--';
});
var_dump($customOptions);
}
}
./foo --foo=bar
Result:
array(1) {
[4]=> string(9) "--foo=bar"
}

Running console command from a Symfony 2 test case

Is there a way to run a console command from a Symfony 2 test case? I want to run the doctrine commands for creating and dropping schemas.
This documentation chapter explains how to run commands from different places. Mind, that using exec() for your needs is quite dirty solution...
The right way of executing console command in Symfony2 is as below:
Option one
use Symfony\Bundle\FrameworkBundle\Console\Application as App;
use Symfony\Component\Console\Tester\CommandTester;
class YourTest extends WebTestCase
{
public function setUp()
{
$kernel = $this->createKernel();
$kernel->boot();
$application = new App($kernel);
$application->add(new YourCommand());
$command = $application->find('your:command:name');
$commandTester = new CommandTester($command);
$commandTester->execute(array('command' => $command->getName()));
}
}
Option two
use Symfony\Component\Console\Input\StringInput;
use Symfony\Bundle\FrameworkBundle\Console\Application;
class YourClass extends WebTestCase
{
protected static $application;
public function setUp()
{
self::runCommand('your:command:name');
// you can also specify an environment:
// self::runCommand('your:command:name --env=test');
}
protected static function runCommand($command)
{
$command = sprintf('%s --quiet', $command);
return self::getApplication()->run(new StringInput($command));
}
protected static function getApplication()
{
if (null === self::$application) {
$client = static::createClient();
self::$application = new Application($client->getKernel());
self::$application->setAutoExit(false);
}
return self::$application;
}
}
P.S. Guys, don't shame Symfony2 with calling exec()...
The docs tell you the suggested way to do it. The example code is pasted below:
protected function execute(InputInterface $input, OutputInterface $output)
{
$command = $this->getApplication()->find('demo:greet');
$arguments = array(
'command' => 'demo:greet',
'name' => 'Fabien',
'--yell' => true,
);
$input = new ArrayInput($arguments);
$returnCode = $command->run($input, $output);
// ...
}
Yes, if your directory structure looks like
/symfony
/app
/src
then you would run
phpunit -c app/phpunit.xml.dist
from your unit tests you can run php commands either by using
passthru("php app/console [...]") (http://php.net/manual/en/function.passthru.php)
exec("php app/console [...]") (http://www.php.net/manual/en/function.exec.php)
or by putting the command in back ticks
php app/consode [...]
If you are running the unit tests from a directory other than symofny, you'll have to adjust the relative path to the app directory for it to work.
To run it from the app:
// the document root should be the web folder
$root = $_SERVER['DOCUMENT_ROOT'];
passthru("php $root/../app/console [...]");
The documentation has been updated since my last answer to reflect the proper Symfony 2 way of calling an existing command:
http://symfony.com/doc/current/components/console/introduction.html#calling-an-existing-command

Categories