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 SymfonyComponentConsoleCommandLockableTrait
and after some time created the following listener to the event console.command
:
use SymfonyComponentConsoleEventConsoleCommandEvent;
use SymfonyComponentConsoleExceptionLogicException;
use SymfonyComponentLockLock;
use SymfonyComponentLockLockFactory;
use SymfonyComponentLockLockInterface;
use SymfonyComponentLockStoreFlockStore;
use SymfonyComponentLockStoreSemaphoreStore;
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).
question from:
https://stackoverflow.com/questions/65861993/implement-locking-for-all-commands-in-my-symfony-app 与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…