I have a Symfony service that processes a file and does stuff with its information. I call this service from a controller and separately from a command class. The actual service takes a long time to run, and I'd like to show some status output on the command line while it processes the file. What is the best way to accomplish this without adding echo commands in my service?
Edit
This seems to be the solution: http://symfony.com/blog/new-in-symfony-2-4-show-logs-in-console
There are commands like
$output->write('Blah blah blah');
$output->writeLn('Blah blah blah'); // Above with a line break
You can also add colours and progress bars and possibly other stuff that I've never got round to using.
http://symfony.com/doc/current/components/console/introduction.html#coloring-the-output
UPDATE
You could use the EventDisptcher service to update your command on events in your service.
For example...
You command
protected function execute(InputInterface $input, OutputInterface $output)
{
//....
$dispatcher = $this->getContainer->get('event_dispatcher');
$dispatcher->addListener(
'an.event.that.you.have.set.up',
function (GenericEvent $event) use ($output) {
$output->writeLn('<info>This event has happened</info');
}
});
//....
}
Your service
protected $dispatcher;
//....
public function __construct(EventDispatcherInterface $dispatcher, ...)
{
$this->dispatcher = $dispatcher;
//...
}
public function someFunction()
{
//...
$variable = 'something you are using');
$dispatcher->dispatch(
'an.event.that.you.have.set.up',
new GenericEvent($variable)
);
//...
}
Obviously there would be a lot more to both you command your service but this give the basic of how to tie it all together.
An actual use example can be seen here..
Command - https://github.com/Richtermeister/Sylius/blob/subscription-bundle/src/Sylius/Bundle/SubscriptionBundle/Command/ProcessSubscriptionsCommand.php
Service - https://github.com/Richtermeister/Sylius/blob/subscription-bundle/src/Sylius/Bundle/SubscriptionBundle/Processor/SubscriptionProcessor.php
This seems to be the solution: http://symfony.com/blog/new-in-symfony-2-4-show-logs-in-console
Related
I followed this guide: https://symfony.com/doc/current/console/lockable_trait.html and implemented the command lock feature for my one of my commands to see how it works. It worked as described and then I was going to implement it for all of my commands. But the issue is that I have about 50 commands and:
I do not want spent time adding the necessary code to each command
I want to have the centralized management of commands locking. I mean, adding extra option to regular commands so that they will be used by my future management center. For now I will need a pretty simple option protected function isLocked() for a regular command which will help me to manage if a command should have lockable feature.
So, I went to the source of \Symfony\Component\Console\Command\LockableTrait and after some time created the following listener to the event console.command:
use Symfony\Component\Console\Event\ConsoleCommandEvent;
use Symfony\Component\Console\Exception\LogicException;
use Symfony\Component\Lock\Lock;
use Symfony\Component\Lock\LockFactory;
use Symfony\Component\Lock\LockInterface;
use Symfony\Component\Lock\Store\FlockStore;
use Symfony\Component\Lock\Store\SemaphoreStore;
class LockCommandsListener
{
/**
* #var array<string, Lock>
*/
private $commandLocks = [];
private static function init()
{
if (!class_exists(SemaphoreStore::class)) {
throw new LogicException('To enable the locking feature you must install the symfony/lock component.');
}
}
public function onConsoleCommand(ConsoleCommandEvent $event)
{
static::init();
$name = $event->getCommand()->getName();
$this->ensureLockNotPlaced($name);
$lock = $this->createLock($name);
$this->commandLocks[$name] = $lock;
if (!$lock->acquire()) {
$this->disableCommand($event, $name);
}
}
private function disableCommand(ConsoleCommandEvent $event, string $name)
{
unset($this->commandLocks[$name]);
$event->getOutput()->writeln('The command ' . $name . ' is already running');
$event->disableCommand();
$event->getCommand()->setCode()
}
private function createLock(string $name): LockInterface
{
if (SemaphoreStore::isSupported()) {
$store = new SemaphoreStore();
} else {
$store = new FlockStore();
}
return (new LockFactory($store))->createLock($name);
}
private function ensureLockNotPlaced(string $name)
{
if (isset($this->commandLocks[$name])) {
throw new LogicException('A lock is already in place.');
}
}
}
I made some tests and it kind of worked. But I am not sure this is the right way of doing things.
Another problem is that I can not find the proper exit code when I disabled a command. Should I just disable it? But it seems that exit code would be a great feature here. Specially when it comes to this listener testing (PHPUnit testing).
And I also have with testing itself. How can I run commands in parallel in my test class. For now I have this:
class LockCommandTest extends CommandTest
{
public function testOneCommandCanBeRun()
{
$commandTester = new ApplicationTester($this->application);
$commandTester->run([
'command' => 'app:dummy-command'
]);
$output = $commandTester->getDisplay();
dd($output);
}
}
It will allow only to run my commands one by one. But I would like to run them both so after running the first one, the second will fail (with some exit code).
As for me the best way to make background task is doing it via supervisor, create config file, like:
[program:your_service]
command=/usr/local/bin/php /srv/www/bin/console <your:app:command>
priority=1
numprocs=1
# Each 5 min.
startsecs=300
autostart=true
autorestart=true
process_name=%(program_name)s_%(process_num)02d
user=root
this is the best way to be sure that your command will be ran only in one process
I've a Symfony 5 command like this:
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
....
protected function execute(InputInterface $input, OutputInterface $output): int
{
$this->input = $input;
$this->output = $output;
$this->io = new SymfonyStyle($input, $output);
....
}
This command generates A LOT of output with $this->io->caution(...), $this->io->block(....), $this->io->text(....) and so on.
Sometimes (not always: there are some runtime conditions), at the end of the execution, I'd like to access the whole output generated by the command and then send it via email. So.... how can I get back everything that OutputInterface has shown? Is there some kind of $this->output->getBuffer()?
I'd have no problem swapping OutputInterface $output with something else (logger, maybe?) as long as I can still show everything on the stdoutput (my terminal) as I'm doing at the moment.
I don't think there is anything off-the-shelf that accomplish this for you. You may achieve something like this with loggers... but you would have to fiddle a lot with configuration error levels, probably inject more than one logger, the console output would never match the one by SymfonyStyle, etc.
You are better served building your own, which shouldn't be particularly difficult. Just build something that wraps/decorates SymfonyStyle; and captures output.
I'll provide the starting building blocks, it's up to you to finish the implementation:
class LoggingStyle
{
private SymfonyStyle $style;
private array $logEntries = [];
public function __construct(InputInterface $input, OutputInterface $output) {
$this->style = new SymfonyStyle($input, $output);
}
private function addLog(string $type, string $message): void
{
$this->logEntries[] = [
'type' => $type,
'message' => $message,
'time' => date('Y-m-d H:i:s')
];
}
public function flushLog():array
{
$log = $this->logEntries;
$this->logEntries = [];
return $log;
}
public function caution(string $message): void
{
$this->addLog('caution', $message);
$this->style->caution($message);
}
public function text(string $message): void
{
$this->addLog('text', $message);
$this->style->caution($message);
}
public function block(string $message): void
{
$this->addLog('block', $message);
$this->style->caution($message);
}
}
You'd need to implement every part of SymfonyStyle interface you need to use, and decide how to deal with some special behaviour, if at all (e.g. stuff like ask(), table(), or progress bars). But that would be entirely up to your implementation.
And also decide how to format each of the different output styles in your email, since there is logically no way to translate it directly.
You could use this class directly, and if you reach the point where you need the aggregated output you simply call LoggingStyle::flushLog() and get all the entries in array form, for you to process and send accordingly.
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?
I'm trying to run a job queue to create a PDF file using SlmQueueBeanstalkd and DOMPDFModule in ZF".
Here's what I'm doing in my controller:
public function reporteAction()
{
$job = new TareaReporte();
$queueManager = $this->serviceLocator->get('SlmQueue\Queue\QueuePluginManager');
$queue = $queueManager->get('myQueue');
$queue->push($job);
...
}
This is the job:
namespace Application\Job;
use SlmQueue\Job\AbstractJob;
use SlmQueue\Queue\QueueAwareInterface;
use SlmQueue\Queue\QueueInterface;
use DOMPDFModule\View\Model\PdfModel;
class TareaReporte extends AbstractJob implements QueueAwareInterface
{
protected $queue;
public function getQueue()
{
return $this->queue;
}
public function setQueue(QueueInterface $queue)
{
$this->queue = $queue;
}
public function execute()
{
$sm = $this->getQueue()->getJobPluginManager()->getServiceLocator();
$empresaTable = $sm->get('Application\Model\EmpresaTable');
$registros = $empresaTable->listadoCompleto();
$model = new PdfModel(array('registros' => $registros));
$model->setOption('paperSize', 'letter');
$model->setOption('paperOrientation', 'portrait');
$model->setTemplate('empresa/reporte-pdf');
$output = $sm->get('viewPdfrenderer')->render($model);
$filename = "/path/to/pdf/file.pdf";
file_put_contents($filename, $output);
}
}
The first time you run it, the file is created and the work is successful, however, if you run a second time, the task is buried and the file is not created.
It seems that stays in an endless cycle when trying to render the model a second time.
I've had a similar issue and it turned out it was because of the way ZendPdf\PdfDocument reuses it's object factory. Are you using ZendPdf\PdfDocument?
You might need to correctly close factory.
class MyDocument extends PdfDocument
{
public function __destruct()
{
$this->_objFactory->close();
}
}
Try to add this or something similar to the PdfDocument class...
update : it seem you are not using PdfDocument, however I suspect this is the issue is the same. Are you able to regenerate a second PDF in a normal http request? It is your job to make sure the environment is equal on each run.
If you are unable to overcome this problem a short-term quick solution would be to set max_runs configuration for SlmQueue to 1. That way the worker is stopped after each job and this reset to a vanilla state...
Symfony2 enables developers to create their own command-line commands. They can be executed from command line, but also from the controller. According to official Symfony2 documentation, it can be done like that:
protected function execute(InputInterface $input, OutputInterface $output)
{
$command = $this->getApplication()->find('demo:greet');
$arguments = array(
...
);
$input = new ArrayInput($arguments);
$returnCode = $command->run($input, $output);
}
But in this situation we wait for the command to finish it's execution and return the return code.
How can I, from controller, execute command forking it to background without waiting for it to finish execution?
In other words what would be equivalent of
$ nohup php app/console demo:greet &
From the documentation is better use start() instead run() if you want to create a background process. The process_max_time could kill your process if you create it with run()
"Instead of using run() to execute a process, you can start() it: run() is blocking and waits for the process to finish, start() creates a background process."
According to the documentation I don't think there is such an option: http://api.symfony.com/2.1/Symfony/Component/Console/Application.html
But regarding what you are trying to achieve, I think you should use the process component instead:
use Symfony\Component\Process\Process;
$process = new Process('ls -lsa');
$process->run(function ($type, $buffer) {
if ('err' === $type) {
echo 'ERR > '.$buffer;
} else {
echo 'OUT > '.$buffer;
}
});
And as mentioned in the documentation "if you want to be able to get some feedback in real-time, just pass an anonymous function to the run() method".
http://symfony.com/doc/master/components/process.html