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) <noreply@anthropic.com>
This commit is contained in:
4
Makefile
4
Makefile
@@ -36,6 +36,10 @@ install_prod: ## Install les dependances et build les assets pour la prod
|
|||||||
bun install
|
bun install
|
||||||
bun run build
|
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 ——————————————————————————————————————
|
## —— Database ——————————————————————————————————————
|
||||||
migration_dev: ## Genere une migration via Docker dev
|
migration_dev: ## Genere une migration via Docker dev
|
||||||
docker compose -f docker-compose-dev.yml exec php php bin/console make:migration
|
docker compose -f docker-compose-dev.yml exec php php bin/console make:migration
|
||||||
|
|||||||
@@ -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');
|
@import url('https://fonts.googleapis.com/css2?family=Intel+One+Mono:ital,wght@0,300..700;1,300..700&display=swap');
|
||||||
|
|
||||||
|
|||||||
@@ -20,8 +20,6 @@ doctrine:
|
|||||||
dir: '%kernel.project_dir%/src/Entity'
|
dir: '%kernel.project_dir%/src/Entity'
|
||||||
prefix: 'App\Entity'
|
prefix: 'App\Entity'
|
||||||
alias: App
|
alias: App
|
||||||
controller_resolver:
|
|
||||||
auto_mapping: false
|
|
||||||
|
|
||||||
when@test:
|
when@test:
|
||||||
doctrine:
|
doctrine:
|
||||||
@@ -38,6 +36,17 @@ when@prod:
|
|||||||
result_cache_driver:
|
result_cache_driver:
|
||||||
type: pool
|
type: pool
|
||||||
pool: doctrine.result_cache_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:
|
framework:
|
||||||
cache:
|
cache:
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
liip_imagine:
|
liip_imagine:
|
||||||
driver: imagick
|
driver: imagick
|
||||||
|
twig:
|
||||||
|
mode: lazy
|
||||||
|
|
||||||
loaders:
|
loaders:
|
||||||
flysystem_loader:
|
flysystem_loader:
|
||||||
|
|||||||
38
migrations/Version20260318203734.php
Normal file
38
migrations/Version20260318203734.php
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<?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 Version20260318203734 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 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
125
src/Entity/MessengerLog.php
Normal file
125
src/Entity/MessengerLog.php
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Entity;
|
||||||
|
|
||||||
|
use App\Repository\MessengerLogRepository;
|
||||||
|
use Doctrine\DBAL\Types\Types;
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
|
||||||
|
#[ORM\Entity(repositoryClass: MessengerLogRepository::class)]
|
||||||
|
#[ORM\Index(columns: ['status'], name: 'idx_messenger_log_status')]
|
||||||
|
#[ORM\Index(columns: ['created_at'], name: 'idx_messenger_log_created_at')]
|
||||||
|
class MessengerLog
|
||||||
|
{
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\GeneratedValue]
|
||||||
|
#[ORM\Column]
|
||||||
|
private ?int $id = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255)]
|
||||||
|
private ?string $messageClass = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::TEXT, nullable: true)]
|
||||||
|
private ?string $messageBody = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 20)]
|
||||||
|
private ?string $status = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::TEXT, nullable: true)]
|
||||||
|
private ?string $errorMessage = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::TEXT, nullable: true)]
|
||||||
|
private ?string $stackTrace = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $transportName = null;
|
||||||
|
|
||||||
|
#[ORM\Column]
|
||||||
|
private ?int $retryCount = null;
|
||||||
|
|
||||||
|
#[ORM\Column]
|
||||||
|
private ?\DateTimeImmutable $createdAt = null;
|
||||||
|
|
||||||
|
#[ORM\Column(nullable: true)]
|
||||||
|
private ?\DateTimeImmutable $failedAt = null;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
string $messageClass,
|
||||||
|
?string $messageBody,
|
||||||
|
string $errorMessage,
|
||||||
|
?string $stackTrace = null,
|
||||||
|
?string $transportName = null,
|
||||||
|
int $retryCount = 0,
|
||||||
|
) {
|
||||||
|
$this->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';
|
||||||
|
}
|
||||||
|
}
|
||||||
77
src/EventSubscriber/MessengerFailureSubscriber.php
Normal file
77
src/EventSubscriber/MessengerFailureSubscriber.php
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\EventSubscriber;
|
||||||
|
|
||||||
|
use App\Entity\MessengerLog;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||||
|
use Symfony\Component\Mailer\MailerInterface;
|
||||||
|
use Symfony\Component\Mime\Email;
|
||||||
|
use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent;
|
||||||
|
use Symfony\Component\Messenger\Stamp\RedeliveryStamp;
|
||||||
|
|
||||||
|
class MessengerFailureSubscriber implements EventSubscriberInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private EntityManagerInterface $em,
|
||||||
|
private MailerInterface $mailer,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public static function getSubscribedEvents(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
WorkerMessageFailedEvent::class => '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) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/Repository/MessengerLogRepository.php
Normal file
18
src/Repository/MessengerLogRepository.php
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Repository;
|
||||||
|
|
||||||
|
use App\Entity\MessengerLog;
|
||||||
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||||
|
use Doctrine\Persistence\ManagerRegistry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends ServiceEntityRepository<MessengerLog>
|
||||||
|
*/
|
||||||
|
class MessengerLogRepository extends ServiceEntityRepository
|
||||||
|
{
|
||||||
|
public function __construct(ManagerRegistry $registry)
|
||||||
|
{
|
||||||
|
parent::__construct($registry, MessengerLog::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,9 +4,10 @@ namespace App\Service;
|
|||||||
|
|
||||||
use Symfony\Component\Mime\Crypto\SMimeSigner;
|
use Symfony\Component\Mime\Crypto\SMimeSigner;
|
||||||
use Symfony\Component\Mime\Email;
|
use Symfony\Component\Mime\Email;
|
||||||
use Symfony\Component\Mailer\MailerInterface;
|
|
||||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||||
|
use Symfony\Component\Messenger\MessageBusInterface;
|
||||||
|
use Symfony\Component\Mailer\Messenger\SendEmailMessage;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
|
||||||
class MailerService
|
class MailerService
|
||||||
@@ -16,7 +17,7 @@ class MailerService
|
|||||||
];
|
];
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private MailerInterface $mailer,
|
private MessageBusInterface $bus,
|
||||||
#[Autowire('%kernel.project_dir%')] private string $projectDir,
|
#[Autowire('%kernel.project_dir%')] private string $projectDir,
|
||||||
#[Autowire(env: 'SMIME_PASSPHRASE')] private string $smimePassphrase,
|
#[Autowire(env: 'SMIME_PASSPHRASE')] private string $smimePassphrase,
|
||||||
private UrlGeneratorInterface $urlGenerator,
|
private UrlGeneratorInterface $urlGenerator,
|
||||||
@@ -40,7 +41,7 @@ class MailerService
|
|||||||
$email = $signer->sign($email);
|
$email = $signer->sign($email);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->mailer->send($email);
|
$this->bus->dispatch(new SendEmailMessage($email));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user