Skip to content

Instantly share code, notes, and snippets.

@victorshdev
Created July 22, 2024 02:58
Show Gist options
  • Save victorshdev/0026d1fc741f6da5dc98258962cc7df5 to your computer and use it in GitHub Desktop.
Save victorshdev/0026d1fc741f6da5dc98258962cc7df5 to your computer and use it in GitHub Desktop.
Implementation of OneTime command in Symfony
<?php
namespace App\Entity;
use App\Repository\OneTimeCommandRepository;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: OneTimeCommandRepository::class)]
class OneTimeCommand
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255, unique: true)]
private ?string $name = null;
#[ORM\Column]
private ?\DateTimeImmutable $launchedAt = null;
public function __construct()
{
$this->launchedAt = new \DateTimeImmutable();
}
public function getId(): ?int
{
return $this->id;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(string $name): static
{
$this->name = $name;
return $this;
}
public function getLaunchedAt(): ?\DateTimeImmutable
{
return $this->launchedAt;
}
public function setLaunchedAt(\DateTimeImmutable $launchedAt): static
{
$this->launchedAt = $launchedAt;
return $this;
}
}
<?php
namespace App\EventListener;
use App\Attributes\RunOnlyOnce;
use App\Entity\OneTimeCommand;
use App\Repository\OneTimeCommandRepository;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\ConsoleEvents;
use Symfony\Component\Console\Event\ConsoleCommandEvent;
use Symfony\Component\Console\Event\ConsoleTerminateEvent;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
/**
* Class OneTimeCommandListener
* @package App\EventListener
*/
final class OneTimeCommandListener
{
public function __construct(
private readonly OneTimeCommandRepository $repository,
private readonly EntityManagerInterface $entityManager,
private readonly LoggerInterface $logger
) {
}
/**
* Event fired before command execution. Checks if the command was already run.
* If so, disables the command.
*
* @param ConsoleCommandEvent $event
* @return void
*/
#[AsEventListener(event: ConsoleEvents::COMMAND)]
public function onBeforeRun(ConsoleCommandEvent $event): void
{
// Check if command meets the requirements to be processed by this listener
if (!$this->checkRequirements($event->getCommand())) {
return;
}
$command = $event->getCommand();
// Check if the command was already run
if (!empty($history = $this->repository->findOneBy(['name' => $command->getName()]))) {
$event->disableCommand();
$this->logger->warning('Command {command} was already run at {date}', [
'command' => $command::class,
'listener' => self::class,
'date' => $history->getLaunchedAt()->format('Y-m-d H:i:s')
]);
}
}
/**
* Event fired after command execution. If the command was successful, saves the command to the database.
*
* @param ConsoleTerminateEvent $event
* @return void
*/
#[AsEventListener(event: ConsoleEvents::TERMINATE)]
public function onAfterRun(ConsoleTerminateEvent $event): void
{
// Check if command meets the requirements to be processed by this listener
if (!$this->checkRequirements($event->getCommand())) {
return;
}
$command = $event->getCommand();
if ($event->getExitCode() !== Command::SUCCESS) {
return;
}
$entity = (new OneTimeCommand())->setName($command->getName());
$this->entityManager->persist($entity);
$this->entityManager->flush();
}
/**
* Check if command has RunOnlyOnce attribute. If so, the command should be run only once.
*
* @param Command $command
* @return bool
*/
private function checkRequirements(Command $command): bool
{
return !empty((new \ReflectionClass(get_class($command)))->getAttributes(RunOnlyOnce::class));
}
}
<?php
namespace App\Command\OneTime;
use App\Attributes\RunOnlyOnce;
use App\Entity\Dictionary;
use App\Enum\DictionaryType;
use App\Repository\DictionaryRepository;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand(
name: 'app:one-time-example',
description: 'Example of one time command',
)]
#[RunOnlyOnce]
class OneTimeExampleCommand extends Command {
public function __construct()
{
parent::__construct();
}
protected function configure(): void
{
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
return Command::SUCCESS;
}
}
<?php
namespace App\Attributes;
#[\Attribute(\Attribute::TARGET_CLASS)]
class RunOnlyOnce
{
}
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20240722014725 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SEQUENCE one_time_command_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
$this->addSql('CREATE TABLE one_time_command (id INT NOT NULL, name VARCHAR(255) NOT NULL, launched_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE UNIQUE INDEX UNIQ_734172A35E237E06 ON one_time_command (name)');
$this->addSql('COMMENT ON COLUMN one_time_command.launched_at IS \'(DC2Type:datetime_immutable)\'');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SCHEMA public');
$this->addSql('DROP SEQUENCE one_time_command_id_seq CASCADE');
$this->addSql('DROP TABLE one_time_command');
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment