Symfony - run console command on kernel.terminate - console

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);
}
}

Related

How to configure laragon/sendmail with Symfony

I'm trying to configure a Symfony 6.1 project to send mail through a fresh installed laragon and sendmail, but all my tries were unsuccessful.
I tried with many configuration and this one doesn't trigger error, but I don't receive any mail and any debug message :
#.env
MAILER_DSN=smtp://localhost
#php.ini
[mail function]
; For Win32 only.
; https://php.net/smtp
SMTP = localhost
; https://php.net/smtp-port
smtp_port = 25
; For Win32 only.
; https://php.net/sendmail-from
;sendmail_from = me#example.com
; For Unix only. You may supply arguments as well (default: "sendmail -t -i").
; https://php.net/sendmail-path
;sendmail_path =
; Force the addition of the specified parameters to be passed as extra parameters
; to the sendmail binary. These parameters will always replace the value of
; the 5th parameter to mail().
;mail.force_extra_parameters =
; Add X-PHP-Originating-Script: that will include uid of the script followed by the filename
mail.add_x_header = Off
sendmail_path="C:/laragon/bin/sendmail/sendmail.exe"
; The path to a log file that will log all mail() calls. Log entries include
; the full path of the script, line number, To address and headers.
;mail.log =
; Log mail to syslog (Event Log on Windows).
;mail.log = syslog
<?php
// src/Controller/MailerController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Email;
use Symfony\Component\Routing\Annotation\Route;
class MailerController extends AbstractController
{
#[Route('/email')]
public function sendEmail(MailerInterface $mailer): Response
{
$email = (new Email())
->from('noreply#mydomain.com')
->to('nobody#gmail.com')
->subject('Time for Symfony Mailer!')
->text('Sending emails is fun again!')
->html('<p>See Twig integration for better HTML integration!</p>');
try {
$mailer->send($email);
} catch (TransportExceptionInterface $e) {
die("debug:".$e->getDebug());
}
return new Response('ok');
}
}
The route to test : http://127.0.0.1:8000/email
Result :

Executing Symfony 4 Custom Command in fixtures

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.

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();
}

Clear FileSystemCache in Symfony 3.4

I use Symfony\Component\Cache\Simple\FilesystemCache;
It works when I $cache->set $cache->get $cache->clear() etc
I don't want to use a custom ttl. I want to clear the cache setted only with console.
But when I do php bin/console cache:clear, it doesn't clear cache I have set before with FilesystemCache.
I have tried to clear every pools with console but it doesn't clear $cache either.
1. Why it happens
Symfony's bin/console cache:clear command clears the cache only from kernel cache dir, which is var/cache/{env} by default.
When you create instance of FilesystemCache, you can provide a path where you want to store your cache as a 3rd parameter. Here's a signature of FilesystemCache constructor
public function __construct(string $namespace = '', int $defaultLifetime = 0, string $directory = null)
If you don't provide 3rd parameter it will end up as sys_get_temp_dir().'/symfony-cache', which is /tmp/symfony-cache on Linux.
As you can see it's a different directory and it won't be cleared by cache:clear command.
2. How to fix it
The proper way
You need to create your own data-cache:clear command. It's very simple https://symfony.com/doc/current/console.html
In execute() method of your command you should instantiate your FilesystemCache and call clear() on it. Example:
protected function execute(InputInterface $input, OutputInterface $output)
{
$cache = new FilesystemCache();
$cache->clear();
}
Then you can call php bin/console data-cache:clear from console.
If you decide to switch to some other caching engine in future (Redis, Memcached etc.) you can simply adjust that command to clear that cache.
The wrong way
It will only work if you keep using FilesystemCache and does not
provide fine-grained control of which cache you actually clear.
You can store your cache in kernel.cache_dir by passing a 3rd parameter to FilesystemCache when you instantiate it.
Example:
$cache = new FilesystemCache('', 0, $container->getParameter('kernel.cache_dir').'/data-cache');
or when configured as a service
Symfony\Component\Cache\Simple\FilesystemCache:
arguments:
- ''
- 0
- '%kernel.cache_dir%/data-cache'
This way Symfony's cache:clear command will work for you, but it's not a good idea to store these 2 types of cache in the same place.
If you change some of your project files, you may want to clear only
kernel cache in /var/cache while keeping your data cache intact and
vice versa. That's why I recommend not to use this solution!
It finally works, using AdapterInterface
<?php
namespace Gh\GhBundle\Manager;
use Symfony\Component\Cache\Adapter\AdapterInterface;
class AppManager
{
protected $_rootDir;
protected $_cache;
public function __construct($rootDir, AdapterInterface $cache)
{
$this->_rootDir = $rootDir;
$this->_cache = $cache;
}
/**
*
* Get version of this app
* #return string
*/
public function getVersion()
{
$cache = $this->_cache;
$numVersion = $cache->getItem('stats.num_version');
if (!$numVersion->isHit()) {
$version = !file_exists($this->_rootDir . '/RELEASE.TXT') ? 'dev' : file_get_contents($this->_rootDir . '/RELEASE.TXT');
$numVersion->set($version);
$cache->save($numVersion);
}
return $numVersion->get();
}
/**
*
* Get name of this app
* #return string
*/
public function getName()
{
return 'GH';
}
}

Extract monolog entries

I'm using Symony 3.3 and Monolog as application logger.
All services are using the injected logger. For example:
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
public function work() {
$this->logger->info("Some info");
$this->logger->debug("Some debug");
}
It happens that I use these services from both controllers and Symfony commands. What I'd like is to handle the logs differently if they are executed from commands.
For example: I have some commands whose purpose is to process a record and the business requires that I store the processing history (logs) in the database for each job.
For example, the command
(server)$ bin/console process:record 12345
should save the log content into record's table, inside "processing_logs" field.
The question is: how to I buffer and extract the list of logs? Ideally, without changing the services and without changing the controllers.

Resources