From 04becc238bc7b88bb3e6a690c30df9f87fddf154 Mon Sep 17 00:00:00 2001 From: Serreau Jovann Date: Wed, 18 Mar 2026 21:43:10 +0100 Subject: [PATCH] Add MessengerLog, async mailer, doctrine fixes - MessengerLog entity: store all messenger failures with full details - MessengerFailureSubscriber: log errors + send alert email synchronously - MailerService: dispatch emails via Messenger bus (async) - Makefile: add entity command - Doctrine: enable Second Level Cache in prod, remove deprecated config - Liip Imagine: set twig mode to lazy - Fix app.scss @use/@import Co-Authored-By: Claude Opus 4.6 (1M context) --- Makefile | 4 + assets/app.scss | 2 +- config/packages/doctrine.yaml | 13 +- config/packages/liip_imagine.yaml | 2 + migrations/Version20260318203734.php | 38 ++++++ src/Entity/MessengerLog.php | 125 ++++++++++++++++++ .../MessengerFailureSubscriber.php | 77 +++++++++++ src/Repository/MessengerLogRepository.php | 18 +++ src/Service/MailerService.php | 7 +- 9 files changed, 280 insertions(+), 6 deletions(-) create mode 100644 migrations/Version20260318203734.php create mode 100644 src/Entity/MessengerLog.php create mode 100644 src/EventSubscriber/MessengerFailureSubscriber.php create mode 100644 src/Repository/MessengerLogRepository.php diff --git a/Makefile b/Makefile index 474cc68..83eac10 100644 --- a/Makefile +++ b/Makefile @@ -36,6 +36,10 @@ install_prod: ## Install les dependances et build les assets pour la prod bun install bun run build +## —— Symfony —————————————————————————————————————— +entity: ## Creer ou modifier une entite via Docker dev + docker compose -f docker-compose-dev.yml exec php php bin/console make:entity + ## —— Database —————————————————————————————————————— migration_dev: ## Genere une migration via Docker dev docker compose -f docker-compose-dev.yml exec php php bin/console make:migration diff --git a/assets/app.scss b/assets/app.scss index 9f687da..0de59ac 100644 --- a/assets/app.scss +++ b/assets/app.scss @@ -1,3 +1,3 @@ -@import "tailwindcss"; +@use "tailwindcss"; @import url('https://fonts.googleapis.com/css2?family=Intel+One+Mono:ital,wght@0,300..700;1,300..700&display=swap'); diff --git a/config/packages/doctrine.yaml b/config/packages/doctrine.yaml index 6c57caf..eaebe6b 100644 --- a/config/packages/doctrine.yaml +++ b/config/packages/doctrine.yaml @@ -20,8 +20,6 @@ doctrine: dir: '%kernel.project_dir%/src/Entity' prefix: 'App\Entity' alias: App - controller_resolver: - auto_mapping: false when@test: doctrine: @@ -38,6 +36,17 @@ when@prod: result_cache_driver: type: pool pool: doctrine.result_cache_pool + second_level_cache: + enabled: true + region_cache_driver: + type: pool + pool: doctrine.result_cache_pool + regions: + default: + lifetime: 3600 + cache_driver: + type: pool + pool: doctrine.result_cache_pool framework: cache: diff --git a/config/packages/liip_imagine.yaml b/config/packages/liip_imagine.yaml index 1e7e35c..1b2b6a0 100644 --- a/config/packages/liip_imagine.yaml +++ b/config/packages/liip_imagine.yaml @@ -1,5 +1,7 @@ liip_imagine: driver: imagick + twig: + mode: lazy loaders: flysystem_loader: diff --git a/migrations/Version20260318203734.php b/migrations/Version20260318203734.php new file mode 100644 index 0000000..a5c1d21 --- /dev/null +++ b/migrations/Version20260318203734.php @@ -0,0 +1,38 @@ +addSql('CREATE TABLE email_tracking (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, message_id VARCHAR(64) NOT NULL, recipient VARCHAR(255) NOT NULL, subject VARCHAR(255) NOT NULL, state VARCHAR(10) NOT NULL, sent_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, opened_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY (id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_A31A7D55537A1329 ON email_tracking (message_id)'); + $this->addSql('CREATE TABLE "user" (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, email VARCHAR(180) NOT NULL, first_name VARCHAR(255) NOT NULL, last_name VARCHAR(255) NOT NULL, roles JSON NOT NULL, password VARCHAR(255) NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY (id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_8D93D649E7927C74 ON "user" (email)'); + $this->addSql('CREATE TABLE messenger_messages (id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, body TEXT NOT NULL, headers TEXT NOT NULL, queue_name VARCHAR(190) NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, available_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, delivered_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY (id))'); + $this->addSql('CREATE INDEX IDX_75EA56E0FB7336F0E3BD61CE16BA31DBBF396750 ON messenger_messages (queue_name, available_at, delivered_at, id)'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('DROP TABLE email_tracking'); + $this->addSql('DROP TABLE "user"'); + $this->addSql('DROP TABLE messenger_messages'); + } +} diff --git a/src/Entity/MessengerLog.php b/src/Entity/MessengerLog.php new file mode 100644 index 0000000..baefc9f --- /dev/null +++ b/src/Entity/MessengerLog.php @@ -0,0 +1,125 @@ +messageClass = $messageClass; + $this->messageBody = $messageBody; + $this->status = 'failed'; + $this->errorMessage = $errorMessage; + $this->stackTrace = $stackTrace; + $this->transportName = $transportName; + $this->retryCount = $retryCount; + $this->createdAt = new \DateTimeImmutable(); + $this->failedAt = new \DateTimeImmutable(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getMessageClass(): ?string + { + return $this->messageClass; + } + + public function getMessageBody(): ?string + { + return $this->messageBody; + } + + public function getStatus(): ?string + { + return $this->status; + } + + public function setStatus(string $status): static + { + $this->status = $status; + return $this; + } + + public function getErrorMessage(): ?string + { + return $this->errorMessage; + } + + public function getStackTrace(): ?string + { + return $this->stackTrace; + } + + public function getTransportName(): ?string + { + return $this->transportName; + } + + public function getRetryCount(): ?int + { + return $this->retryCount; + } + + public function getCreatedAt(): ?\DateTimeImmutable + { + return $this->createdAt; + } + + public function getFailedAt(): ?\DateTimeImmutable + { + return $this->failedAt; + } + + public function markAsResolved(): void + { + $this->status = 'resolved'; + } +} diff --git a/src/EventSubscriber/MessengerFailureSubscriber.php b/src/EventSubscriber/MessengerFailureSubscriber.php new file mode 100644 index 0000000..59ca60e --- /dev/null +++ b/src/EventSubscriber/MessengerFailureSubscriber.php @@ -0,0 +1,77 @@ + 'onMessageFailed', + ]; + } + + public function onMessageFailed(WorkerMessageFailedEvent $event): void + { + $envelope = $event->getEnvelope(); + $message = $envelope->getMessage(); + $throwable = $event->getThrowable(); + + $retryCount = 0; + $redeliveryStamp = $envelope->last(RedeliveryStamp::class); + if ($redeliveryStamp instanceof RedeliveryStamp) { + $retryCount = $redeliveryStamp->getRetryCount(); + } + + $messageBody = null; + try { + $messageBody = serialize($message); + } catch (\Throwable) { + $messageBody = get_class($message) . ' (not serializable)'; + } + + $log = new MessengerLog( + messageClass: get_class($message), + messageBody: $messageBody, + errorMessage: $throwable->getMessage(), + stackTrace: $throwable->getTraceAsString(), + transportName: $event->getReceiverName(), + retryCount: $retryCount, + ); + + $this->em->persist($log); + $this->em->flush(); + + try { + $email = (new Email()) + ->from('contact@e-cosplay.fr') + ->to('contact@e-cosplay.fr') + ->subject('Alerte Messenger : Echec de traitement') + ->priority(Email::PRIORITY_HIGH) + ->text( + "Un message Messenger a echoue.\n\n" . + "Message: " . get_class($message) . "\n" . + "Transport: " . $event->getReceiverName() . "\n" . + "Retry: " . $retryCount . "\n" . + "Erreur: " . $throwable->getMessage() . "\n\n" . + "Stack trace:\n" . $throwable->getTraceAsString() + ); + + $this->mailer->send($email); + } catch (\Throwable) { + } + } +} diff --git a/src/Repository/MessengerLogRepository.php b/src/Repository/MessengerLogRepository.php new file mode 100644 index 0000000..c43c6be --- /dev/null +++ b/src/Repository/MessengerLogRepository.php @@ -0,0 +1,18 @@ + + */ +class MessengerLogRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, MessengerLog::class); + } +} diff --git a/src/Service/MailerService.php b/src/Service/MailerService.php index fb50b7c..b86fbcf 100644 --- a/src/Service/MailerService.php +++ b/src/Service/MailerService.php @@ -4,9 +4,10 @@ namespace App\Service; use Symfony\Component\Mime\Crypto\SMimeSigner; use Symfony\Component\Mime\Email; -use Symfony\Component\Mailer\MailerInterface; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\DependencyInjection\Attribute\Autowire; +use Symfony\Component\Messenger\MessageBusInterface; +use Symfony\Component\Mailer\Messenger\SendEmailMessage; use Doctrine\ORM\EntityManagerInterface; class MailerService @@ -16,7 +17,7 @@ class MailerService ]; public function __construct( - private MailerInterface $mailer, + private MessageBusInterface $bus, #[Autowire('%kernel.project_dir%')] private string $projectDir, #[Autowire(env: 'SMIME_PASSPHRASE')] private string $smimePassphrase, private UrlGeneratorInterface $urlGenerator, @@ -40,7 +41,7 @@ class MailerService $email = $signer->sign($email); } - $this->mailer->send($email); + $this->bus->dispatch(new SendEmailMessage($email)); } /**