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!
Related
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);
}
}
I am testing modules using PHPUnit Test cases. All the things working fine but when i use $_SERVER['REMOTE_ADDR'] it gives fatal error and stops execution.
CategoryControllerTest.php
<?php
namespace ProductBundle\Controller\Tests\Controller;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
class CategoryControllerTest extends WebTestCase {
protected function setUp() {
static::$kernel = static::createKernel();
static::$kernel->boot();
$this->container = static::$kernel->getContainer();
$this->em = static::$kernel->getContainer()->get('doctrine')->getManager();
}
public function testCategory() {
$ip_address = $_SERVER['REMOTE_ADDR'];
$client = static::createClient(
array(), array('HTTP_HOST' => static::$kernel->getContainer()->getParameter('test_http_host')
));
$crawler = $client->request('POST', '/category/new');
$client->enableProfiler();
$this->assertEquals('ProductBundle\Controller\CategoryController::addAction', $client->getRequest()->attributes->get('_controller'));
$form = $crawler->selectButton('new_category')->form();
$form['category[name]'] = "Electronics";
$form['category[id]'] = "For US";
$form['category[ip]'] = $ip_address;
$client->submit($form);
$this->assertTrue($client->getResponse()->isRedirect('/category/new')); // check if redirecting properly
$client->followRedirect();
$this->assertEquals(1, $crawler->filter('html:contains("Category Created Successfully.")')->count());
}
}
Error
There was 1 error:
1) ProductBundle\Tests\Controller\CategoryControllerTest::testCategory
Undefined index: REMOTE_ADDR
I have tried to add it in setUp() function but it's not working as well.
Technically, you haven't sent a request to your application yet, so there is no remote address to refer to. That in fact is what your error is telling us too.
To work around this:
move the line to be below:
// Won't work, see comment below
$crawler = $client->request('POST', '/category/new');
Or you could make up an IP address and test with that. Since you're only using the IP to save a model, that will work just as well.
Like #apokryfos mentioned in the comments, it's considered bad practice to access superglobals in test cases. So option 2 is probably your best choice here.
Create service which returns ip address and mock the service in test case.
Here, create controller and service as UserIpAddress. get() will return ip address of user.
service.yml
UserIpAddress:
class: AppBundle\Controller\UserIpAddressController
arguments:
container: "#service_container"
UserIpAddressController.php
class UserIpAddressController
{
public function get()
{
return $_SERVER['REMOTE_ADDR'];
}
}
Create mock of "UserIpAddress" service. It will override existing service. Use 'UserIpAddress' service to get ip address in your project.
CategoryControllerTest.php
$UserIpAddress = $this->getMockBuilder('UserIpAddress')
->disableOriginalConstructor()
->getMock();
$UserIpAddress->expects($this->once())
->method('get')
->willReturn('192.161.1.1'); // Set ip address whatever you want to use
Now, get ip address using $UserIpAddress->get();
You can either create another class that will return the server var then mock it.
Or you can set/unset the server var directly into your test case.
Did it with PHPUnit 6.2.2 :
/**
* Return true if the user agent matches a robot agent
*/
public function testShouldReturnTrueIfRobot()
{
$_SERVER['HTTP_USER_AGENT'] = 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)';
$this->configMock->method('getRobotUserAgents')
->willReturn('bot|crawl|slurp|spider|mediapartner');
$test = $this->robotTest->isBot();
static::assertTrue($test);
}
/**
* Test return false if no user agent
*/
public function testShouldReturnFalseIfNoAgentUser()
{
unset($_SERVER['HTTP_USER_AGENT']);
$test = $this->robotTest->isBot();
static::assertFalse($test);
}
Where tested method is :
/**
* Detect if current user agent matches a robot user agent
*
* #return bool
*/
public function isBot(): bool
{
if (empty($_SERVER['HTTP_USER_AGENT'])) {
return false;
}
$userAgents = $this->config->getRobotUserAgents();
$pattern = '/' . $userAgents . '/i';
return \preg_match($pattern, $_SERVER['HTTP_USER_AGENT']);
}
After a slight modification of my units, I wanted the update with a simple php app/console doctrine: update --force. But no action executed and in addition no response. I then did a php app/check.php meaning me no problems (Your system is ready to run Symfony2 projects). I do not understand and it doesn't provide an error. Here's what I've done:
Command: ********: ***** ProjetSymphony $ php app / console***
Answer (none): ******* **** $ ProjetSymphony***
If someone has an idea.
Screen :
Try with:
php app/console doctrine:schema:update --force
Maybe it's only a syntaxis error.
Also, if anyone tries to run php app/console in a newer symfony version (for example symfony 3.0), you will get an error: no file found because the file was moved to 'bin' folder. Now to run from the console, you have to use php bin/console instead. Just in case this change confused anyone who started to learn symfony and updated to 3.0.
I finally found my mistake. I had a command file that prevented the execution of my order (CreateUserCommand.php)
If someone wants to explain to me why this cosait file an error during the execution of my order ...
Here is the file :
<?php
namespace FP\UserBundle\Command;
use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use FOS\UserBundle\Model\User;
use FOS\UserBundle\Command\CreateUserCommand as BaseCommand;
class CreateUserCommand extends BaseCommand
{
/**
* #see Command
*/
protected function configure()
{
exit;
echo "tes";
parent::configure();
$this
->setName('fp:user:create')
->getDefinition()->addArguments(array(
new InputArgument('age', InputArgument::REQUIRED, 'The age')
))
;
}
/**
* #see Command
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
exit;
echo "tes";
$username = $input->getArgument('username');
$email = $input->getArgument('email');
$password = $input->getArgument('password');
$age = $input->getArgument('age');
$inactive = $input->getOption('inactive');
$superadmin = $input->getOption('super-admin');
$manipulator = $this->getContainer()->get('fos_user.util.user_manipulator');
$manipulator->setAge($age);
$manipulator->create($username, $password, $email, !$inactive, $superadmin);
$output->writeln(sprintf('Created user <comment>%s</comment>', $username));
}
/**
* #see Command
*/
protected function interact(InputInterface $input, OutputInterface $output)
{
exit;
echo "tes";
parent::interact($input, $output);
if (!$input->getArgument('age')) {
$age = $this->getHelper('dialog')->askAndValidate(
$output,
'Please choose a age:',
function($age) {
if (empty($age)) {
throw new \Exception('Lastname can not be empty');
}
return $age;
}
);
$input->setArgument('age', $age);
}
}
}
I am trying to test my Symfony2 Console command using phpunit.
I'm following the Symfony2 cookbook article about this topic:
http://symfony.com/doc/current/components/console/helpers/questionhelper.html#testing-a-command-that-expects-input
However, if I fail to provide an input (a test fail), then phpunit simply sit there doing nothing waiting for input. Here's an example:
// MyCommand.php
class MyCommand extends Command {
// ... configure()
protected function execute(InputInterface $input, OutputInterface $output) {
$qh = $this->getHelper('question');
$q1 = new ConfirmationQuestion('First question, yes or no?', false);
$qh->ask($input, $output, $q);
$q2 = new ConfirmationQuestion('Second question, yes or no?', false);
$qh->ask($input, $output, $q);
}
}
// MyCommandTest.php
class MyCommandTest extends \PHPUnit_Framework_TestCase {
// ... getInputstream()
public function testExecute() {
$app = new Application();
$app->add(new MyCommand());
$cmd = $app->find('askquestions');
$cmdTester = new CommandTester($cmd);
$helper = $cmd->getHelper('question');
$helper->setInputStream($this->getInputStream('y\\n')); // this should be yy\\n
$cmdTester->execute([
'command' => $cmd->getName(),
]);
}
}
Please notice that I have purposefully made my test incorrect, it is only supplying an answer to question 1. Since I wrote that test, I have since added q2 but I forgot to modify my tests. Being a good programmer though I run phpunit to see if there are problems, but phpunit hangs as it expects input from q2!
How do I make it so my test will disregard any further requests for input, fail if it encounters one, and keep going with other tests?
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