In CakPHP 3.6.0 Console Commands have been added to replace Shells and Tasks long term.
I'm currently designing a cronjob command to execute other commands in different time intervals. So I want to run a command from a Command class like this:
namespace App\Command;
// ...
class CronjobCommand extends Command
{
public function execute(Arguments $args, ConsoleIo $io)
{
// Run other command
}
}
For Shells / Tasks it is possible to use Cake\Console\ShellDispatcher:
$shell = new ShellDispatcher();
$output = $shell->run(['cake', $task]);
but this does not work for Commands. As I did not find any information in the docs, any ideas how to solve this problem?
You can simply instantiate the command and then run it, like this:
try {
$otherCommand = new \App\Command\OtherCommand();
$result = $otherCommand->run(['--foo', 'bar'], $io);
} catch (\Cake\Console\Exception\StopException $e) {
$result = $e->getCode();
}
CakePHP 3.8 will introduce a convenience method that helps with this. Quote from the upcoming docs:
You may need to call other commands from your command. You can use
executeCommand to do that::
// You can pass an array of CLI options and arguments.
$this->executeCommand(OtherCommand::class, ['--verbose', 'deploy']);
// Can pass an instance of the command if it has constructor args
$command = new OtherCommand($otherArgs);
$this->executeCommand($command, ['--verbose', 'deploy']);
See also https://github.com/cakephp/cakephp/pull/13163.
Related
I'm using The Symfony Lock package to check if a class method can be executed
if ($this->lock->acquire()) {
$this->execute();
$this->lock->release();
}
Important: I'm not using the Symfony Framework, only the Lock component
I want to make a test that asserts that the execution is locked when running in multiple threads, but I have not found any documentation on how to achieve this.
Is it a good idea to use pthreads? If not, which is the best way to make this test?
Thank you very much.
Referring to the Lock Component documentation :
https://symfony.com/doc/current/components/lock.html#usage
Information on the CommandTester :
https://symfony.com/doc/current/console.html#testing-commands
Solution for PHPUnit test :
use Symfony\Component\Lock\Factory;
use Symfony\Component\Lock\Store\SemaphoreStore;
public function testLockIsSet()
{
// Create a new Semaphore lock with the same ID as the one that would be
// created if you were running the command / class / process etc.
$store = new SemaphoreStore();
$factory = new Factory($store);
$lock = $factory->createLock('lock-name-used-eg-generate-pdf');
if ($lock->acquire()) {
// In my use case I was running multiple commands to see if the lock
// was working properly
$commandTester = new CommandTester($this->command);
// Try and run the command. The lock should already be set.
$commandTester->execute(
[
'command' => $this->command->getName()
]
);
// You could also use expectException() here for LogicException
$this->assertContains(
'The command is already running in locked mode.',
$commandTester->getDisplay()
);
$lock->release();
}
}
(Symfony3)
I'm toying with the idea of setting up some simple cron tasks to generate security reports for our project managers so that they can schedule upgrade time for developers (vs. me forgetting to run them manually).
As a very basic check, I'll simply run...
php bin/console security:check
...to see what composer has to say about vulnerabilities. Ultimately I'd like to roll this output into an email or post it to a slack channel or basecamp job when the cron is run.
Problem
When I run the command from via terminal it works great. Running the command inside a controller always returns the response Lock file does not exist. I'm assuming this in reference to the composer.lock file at the root of the project. I can confirm that this file does in fact exist.
Following is the controller I'm currently using, which is adapted from this:
http://symfony.com/doc/current/console/command_in_controller.html
<?php
namespace Treetop1500\SecurityReportBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\BufferedOutput;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
class DefaultController extends Controller
{
public function indexAction($key)
{
if ($key != $this->getParameter('easy_cron_key')) {
throw new UnauthorizedHttpException("You are not authorized to access this page.");
}
$kernel = $this->get('kernel');
$application = new Application($kernel);
$application->setAutoExit(false);
$input = new ArrayInput(array(
'command' => 'security:check'
));
// You can use NullOutput() if you don't need the output
$output = new BufferedOutput();
$application->run($input, $output);
// return the output, don't use if you used NullOutput()
$content = $output->fetch();
// return new Response(""), if you used NullOutput()
return new Response($content);
}
}
$content always has the value "Lock file does not exist."
I realize there are probably better tools and ways to do this, however I would really like to understand why this is the generated response from in this controller action. Thank you for taking a look!
Pass absolute path to composer.lock file just like that:
php bin/console security:check /path/to/another/composer.lock
So in your example, that's would be:
$input = new ArrayInput([
'command' => 'security:check',
'lockfile' => '/path/to/another/composer.lock'
]);
Read more: SecurityCheckerCommand from SensioLabs. Optional argument is lockfile, which is checked by SecurityChecker. On line 46, they are looking for composer.lock file (default argument) and throw an exception, when they not found.
P.S. Earlier, I type the wrong parameters to array. I checked in Symfony documentation (How to Call Other Commands) and fixed the answer.
The solution to this is to pass the lockfile argument to the ArrayInput object like this:
$lockfile = $this->get('kernel')->getRootDir()."/../composer.lock";
$input = new ArrayInput(array('command'=>'security:check','lockfile'=>$lockfile));
I just discovered that it exists a ConsoleEvents::TEMINATE event in Symfony.
I want to use it to execute some additional process after the command execution (and not delaying the command).
But the fact is that i want to execute some process when a specific command is finish, not for all the commands (because i think that consoleevent.terminate is fired for all the commands.
I really don't know how to do that.
Regards.
You can access instance of the command from ConsoleTerminateEvent
It's almost copy paste from documentation of Console component. with full symfony registering listener looks a little different but you should get the idea.
$dispatcher->addListener(
ConsoleEvents::TERMINATE,
function(ConsoleTerminateEvent $event) {
$command = $event->getCommand();
// if it's not the command you want
if (!$command instanceof YourDesiredCommand) {
return;
}
// put your logic here
}
);
I know I can unit test console commands by passing arguments and options like so:
$command->run(new ArrayInput($data), new NullOutput);
But what if I want to add a confirmation dialog to my command by using the confirm() method in Laravel?
Have you read the example on Symfony's site?
http://symfony.com/doc/current/components/console/helpers/dialoghelper.html#testing-a-command-which-expects-input
If you have but are still having trouble making it work, let us know.
I forgot to mention I was using PHPSpec for my unit tests.
Finally I figured out how to test confirmations with it.
These are the use statements:
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\NullOutput;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Helper\HelperSet;
And this is a sample test:
public function it_fires_the_command(QuestionHelper $question, HelperSet $helpers)
{
// $data is an array containing arguments and options for the console command
$input = new ArrayInput($data);
$output = new NullOutput;
// $query must be an instance of ConfirmationQuestion
$query = Argument::type('Symfony\Component\Console\Question\ConfirmationQuestion');
// with "willReturn" we can decide whether (TRUE) or not (FALSE) the user confirms
$question->ask($input, $output, $query)->willReturn(true);
// we expect the HelperSet to be invoked returning the mocked QuestionHelper
$helpers->get('question')->willReturn($question);
// finally we set the mocked HelperSet in our console command...
$this->setHelperSet($helpers);
// ...and run it
$this->run($input, $output);
}
I am new on Symfony2 and I got blocked when trying to run an asynchronous command like this:
class MyCommand extends ContainerAwareCommand{
protected function configure()
{
$this
->setName('my:command')
->setDescription('My command')
->addArgument(
'country',
InputArgument::REQUIRED,
'Which country?'
)
;
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$country = $input->getArgument('country');
// Obtain the doctrine manager
$dm = $this->getContainer()->get('doctrine_mongodb.odm.document_manager');
$users = $dm->getRepository('MyBundle:User')
->findBy( array('country'=>$country));
}}
That works perfectly when I call it from my command line:
php app/console my:command uk
But it doesn't work when I call it trowh a Symfony2 Process:
$process = new Process("php ../app/console my:command $country");
$process->start();
I get a database error: "[MongoWriteConcernException] 127.0.0.1:27017: not master"
I think that means that the process is not getting my database configuration...
I just want to run an asynchronous process, is there other way to do it?
Maybe a way to call the Application Command that do not require the answer to keep going ?
Maybe I need to use injection?
PS: My current command is just a test, at the end it should be an 'expensive' operation...
Well, I found out what happened...
I use multiple environments: DEV, TEST and PROD.
And I also use differents servers.
So the DEV environment is my own machine with a simple mongodb configuration.
But the TEST environment is on other server with a replica set configuration...
Now the error get full sense: "[MongoWriteConcernException] 127.0.0.1:27017: not master"
To solve it, I've just added the environment parameter (--env=) to the process and everything worked like a charm:
$process = new Process("php ../app/console my:command $country --env=test");
Actually, to get the correct environment I use this:
$this->get('kernel')->getEnvironment();
Which let's my code as follows:
$process = new Process("php ../app/console my:command $country --env=".$this->get('kernel')->getEnvironment());
Maybe is not a beautifull way to do it, but it works for me :)
Disclamer: This might be a bit overkill for what you're trying to do :)
I would choose an opposite way to do it: pthreads
First, quick examination of StackOverflow showed me a really nice example of using pthreads: Multi-threading is possible in php
Then, knowing that you could invoke your command from another command:
http://www.craftitonline.com/2011/06/calling-commands-within-commands-in-symfony2/
... lets you piece all the parts. It's a bit complicated but it does the job.
In case you want to execute your code completely async in Symfony2/3 there is AsyncServiceCallBundle for that.
You should just call it like this:
$this->get('krlove.async')->call('your_service_id', 'method_name', [$arg1, $arg2]);
Internally it uses this approach to run your code in background.