Executing Symfony 4 Custom Command in fixtures - symfony

I have a custom command in my symfony project to populate the database with the default data that the application need to work in both dev and prod environments.
For the dev environment I have a fixture script that depends on these default common data.
I'm trying to call my custom Symfony command in the fixture script so that I'm sure to have the required data to properly load my fixtures.
This is my custom command app:db:populate in "pseudo script", just creating a bunch of entities, persit & flush. My custom command works fine when I call it through php bin/console app:db:populate
protected function execute(InputInterface $input, OutputInterface $output)
{
// Creating a bunch of default entities, persist them and flush
$data = new MyDefaultEntity();
// ...
$this->manager->persist($data);
// ...
$this->manager->flush();
}
Then, in my fixture script, I want to call app:db:populate first, because fixtures depends on these data. So I tried to use the Process class to execute my script this way :
public function load(ObjectManager $manager)
{
// Execute the custom command
$cmd = 'php bin/console app:db:populate';
$process = new Process($cmd);
$process->run(function ($type, $buffer) {
if (Process::ERR === $type) {
echo 'ERR > '.$buffer;
} else {
echo 'OUT > '.$buffer;
}
});
// Then load the fixtures !
// ...
}
The custom command seems to execute well until the $this->manager->flush();
I have the following error in my console (Data is obfuscated for the post):
In AbstractMySQLDriver.php line 36:
An exception occurred while executing 'INSERT INTO ....(..., ..., ...) VALUES (?, ?, ?)' with params ["...", "...", "..."]:
SQLSTATE[HY000]: General error: 1205 Lock wait timeout exceeded; try restarting transaction
I don't know what to do regarding this error ... Why the command is working normally when used through a classic console call and why it is not working in a Process?

So, the short answer is
Quoting Symfony documentation :
You may have the need to execute some function that is only available in a console command. Usually, you should refactor the command and move some logic into a service that can be reused in the controller.
I ended up making a service class that handles all the app:db:populate logic (read a json file and insert basic app entities in the database). Then I call this service in both app:db:populate execute methods and AppFixtures load methods.
Hope this will help someone.

Related

Unittesting a Symfony 4.2 process runs infinite loop (than times out), wihout unittest it works fine

Lets say I have the following Symfony 4 command:
class Command1 extends Command {
protected static $defaultName = 'app:command1';
protected function execute(InputInterface $input, OutputInterface $output){
$process = new Process('bin/console list', getcwd());
$process->start(); // or even $process->run() does not matter if its async or not
// ... where we handle if its finished, etc...
}
}
If I simply call bin/console app:command1 it will return the expected command list. Basically works as I expect.
But if I have a phpunit test which uses the Symfony\Component\Console\Application::run() to start this command, I end up in an "infinite loop" (well, actually not, it times out after 60 sec) in the Symfony\Component\Process::wait() in the
do {
$this->checkTimeout();
$running = '\\' === \DIRECTORY_SEPARATOR ? $this->isRunning() : $this->processPipes->areOpen();
$this->readPipes($running, '\\' !== \DIRECTORY_SEPARATOR || !$running);
} while ($running);
where the $this->processPipes->areOpen() will be always open.
It seems to me, if I use any Symfony console command in a Process through phpunit, there will be always two pipes open like these:
1 = {resource} resource id='x' type='stream'
2 = {resource} resource id='z' type='stream'
but I don't know what are these actually. I also saw in htop, that the start()'s proc_open actually starts up a new process, but it just hangs (does absolutely nothing, cant even debug it), until times out. Nothing in error logs (other than the timeout).

Is there a way to prevent multiple executions of controller method in Symfony 4?

I have a service which I use both from a custom command and an HTML page. I want to prevent multiple executions of the the service in parallel. For the command there is the Lock component that does that. But is it possible to achieve the same thing for a controller method ?
The lock component doesn't work if the service is called from a controller:
$store = new FlockStore(sys_get_temp_dir());
$factory = new Factory($store);
$lock = $factory->createLock('MY_SERVICE');
I wanted to avoid calling the command from the controller (that's why I created a service) mainly because the service doesn't have the same output for the HTML page and the CLI.
Inject the lock Factory into your service directly instead of creating the lock in the command AND in the controller.
First you have to install Lock Component:
composer require symfony/lock
Then, for example, you can declare your service like this:
use Symfony\Component\Lock\Factory as LockFactory;
class MyService {
private $lock;
public function __construct(LockFactory $lockFactory) {
$this->lock = $lockFactory->createLock('LOCK_KEY');
}
public function doWork() {
$this->lock->acquire();
try {
// DO THINGS
} finally {
$this->lock->release();
}
}
}
I said:
The lock component doesn't work if the service is called from a controller:
Actually the issue I had was the Symfony built-in dev server which is single-threaded, so requests can't be executed in parallel, while the CLI PHP is multi-threaded. I couldn't run the script in parallel through the dev server, request were queued, service script was never locked.
The lock component is working the same whether it's called from a command or a controller.
Using the lock like this in the service works fine:
use Symfony\Component\Lock\Factory;
use Symfony\Component\Lock\Store\FlockStore;
$store = new FlockStore(sys_get_temp_dir());
$factory = new Factory($store);
$lock = $factory->createLock('LOCK_KEY');
if ($lock->acquire()) {
//some locked code
$lock->release();
}

Symfony - run console command on kernel.terminate

I have configured swiftmailer to spool emails using file type. here is my swiftmailer config
swiftmailer:
transport: "%mailer_transport%"
host: "%mailer_host%"
username: "%mailer_user%"
password: "%mailer_password%"
spool:
type: file
path: "%kernel.root_dir%/../var/spool"
When I send any emails it perfectly spools. I run following command to dispatch emails thereafter.
bin/console swiftmailer:spool:send --env=dev
According to Symfony documentation
the console command should be triggered by a cron job or scheduled task and run at a regular interval.
My problem is, I cannot use crontab because cron can be configured with a minimum of 1 minute interval which I cannot afford. I want to make use of the background process with immediate execution after the response is sent back to browser, hence minimizing execution of spools to bare minimum.
I attempted to solve this problem by creating an event listener class and listening to kernel.terminate, and execute the command using shell_exec or exec function, here is the code for reference.
app.kernel.terminate.listener:
arguments: ["#kernel"]
class: AppBundle\EventListener\KernelTerminateListener
tags:
- { name: kernel.event_listener, event: kernel.terminate }
Here is my EventListener class
<?php
namespace AppBundle\EventListener;
use Symfony\Component\HttpKernel\Event\PostResponseEvent;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
use Cocur\BackgroundProcess\BackgroundProcess;
class KernelTerminateListener
{
protected $kernel;
protected $console;
public function __construct($kernel)
{
$this->kernel = $kernel;
$this->console = $this->kernel->getRootDir().'/../bin/console ';
}
public function onKernelTerminate(PostResponseEvent $event)
{
$command = $this->console.'swiftmailer:spool:send --env='.$this->kernel->getEnvironment();
shell_exec($command);
}
}
What I am trying in here is to run bin/console swiftmailer:spool:send --env=dev on kernel.terminate event, unfortunately this does not work, any hint on how to approach this problem is appreciated.
Thank you.
Please use the memory spool type of swift mailer, it does exactly what you want
When you use spooling to store the emails to memory, they will get sent right before the kernel terminates. This means the email only gets sent if the whole request got executed without any unhandled exception or any errors. To configure swiftmailer with the memory option, use the following configuration:
Instead of using shel_exec make use of process component,which will create a new process and command will be executed after response is sent.
shel_exec or exec will execute under same process which forces kernal to wait for completing request(because once parent process killed,child also terminates). Process component will create a new process,under that command will be executed.
use Symfony\Component\Process\Process;
....
....
....
public function onKernelTerminate(PostResponseEvent $event)
{
$command = $this->console.'swiftmailer:spool:send --env=.'$this->kernel->getEnvironment().'> output.log 2> out.log &';
$process = new Process($command);
$process->run();
}
Perhaps It was an issue with PHP, I am using MAMP and OSX comes pre-installed with PHP, basically, I got two php version installed, and for some reason, when I gave correct PHP path it worked, here is my updated listener class which I renamed to MailerSpoolListener
<?php
namespace AppBundle\EventListener;
use Symfony\Component\HttpKernel\Event\PostResponseEvent;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
use Cocur\BackgroundProcess\BackgroundProcess;
class MailerSpoolListener
{
protected $kernel;
protected $php;
protected $console;
protected $env;
protected $command;
protected $muteOutput;
public function __construct($kernel)
{
$this->kernel = $kernel;
$this->php = PHP_BINDIR.'/php';
$this->command = 'swiftmailer:spool:send';
$this->console = $this->kernel->getRootDir().'/../bin/console';
$this->env = $this->kernel->getEnvironment();
$this->muteOutput = '> /dev/null 2>/dev/null &';
}
public function onKernelTerminate(PostResponseEvent $event)
{
$command = $this->php.' '.$this->console.' '.$this->command.' --env='.$this->env.' '.$this->muteOutput;
$process = shell_exec($command);
}
}

phpunit test passes, page load in browser fails in Symfony 2

I am new to phpunit testing and did a very simple test looking for status code.
The test passes when I run:
bin\phpunit -c app src\AppBundle\Tests\Controller\StarLinX\TravelControllerTest.php
PHPUnit 4.6.10 by Sebastian Bergmann and contributors.
Configuration read from C:\PhpstormProjects\dir\app\phpunit.xml.dist
.
Time: 6.03 seconds, Memory: 20.00Mb
OK (1 test, 1 assertion)
But when I load the page in the browser, an exception is thrown rendering the twig file with status code 500.
I thought maybe this was a cache issue, so I cleared cache in --env=dev, prod and test.
How do I troubleshoot this error?
This is my test file:
namespace AppBundle\Tests\Controller\StarLinX;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
class TravelControllerTest extends WebTestCase {
public function testGET() {
// Create a new client to browse the application
$client = static::createClient();
// get the page
$crawler = $client->request('GET', '/travel/aaaaa');
$this->assertEquals(200, $client->getResponse()->getStatusCode(), "Unexpected HTTP status code for GET /travel/aaaaa");
}
}
This is the error that is thrown when running in the dev environment:
An exception has been thrown during the rendering of a template ("Notice: Array to string conversion")
So after a little more analysis, I find that the error around {{ weatherInfo }} which should be {{ weatherInfo.now }}. This throws an error when running the development environment. In production, twig simply displays Array.
Is that normal behavior?
As it's written your test only check the error status, the test is not specific enough. What if it displayed another page? The test would still pass.
You should add some other tests in order to ensure that the page is properly displayed:
// …
$this->assertEquals(200, $client->getResponse()->getStatusCode(),
"Unexpected HTTP status code for GET /travel/aaaaa");
// Check that the page has a title.
$this->assertSame(
1,
$crawler->filter('title')->count()
);
// Check that the page has a correct title.
$this->assertSame(
'Travel',
$crawler->filter('title')->text()
);
// Check something in the content
$this->assertSame(
'Hello, World!',
$crawler->filter('body > div#content')->text()
);
If you don't have enough information to debug your code, tou can access to the page content which usually contain the error message:
die($this->client->getResponse()->getContent());

Symfony best practice for data export file location

I am writing a console command which generates data files to be used by external services (for example, a Google feed, inventory feed, etc). Should the location of the generated data files be within the Symfony app? I know they can actually be anywhere, I'm just wondering if there is a standard way to do it.
It's up to you, but it is better to have this path in a parameter. For example you can you have a parameter group related to your command. This allows you to have different configurations depending on the current environment:
parameters:
# /app/config.yml
# #see MyExportCommand.php
my_export_command:
base_path: '/data/ftp/export'
other_command_related_param: true
In your command, get and store those parameters in the initialize function:
// MyExportCommand.php
protected function initialize(InputInterface $input, OutputInterface $output)
{
$this->parameters = $this->getContainer()->getParameter('my_export_command');
}
Finally in your execute function, you can use something like this: ($this->fs is an instance of the Symfony2 Filesystem component)
// execute()
// Write the file
$filePath = $this->parameters['base_path']. '/'. $this->fileName;
$this->fs->dumpFile($filePath, $myContent);

Resources