From 74a77cd9e83b6afdf004d1a2bcb11c9f7697f88d Mon Sep 17 00:00:00 2001 From: Viktor Masicek Date: Thu, 13 Aug 2026 17:42:02 +0200 Subject: [PATCH 1/2] Support nette/mail 4 and Doctrine ORM 3 - composer: allow nette/mail ^3.1 || ^4.0, require PHP ^8.1 - use Nette\Mail\Mailer interface instead of IMailer (exists since mail 3.1 as alias target, the only name left in mail 4) - duplicate entity mapping as PHP attributes next to annotations so it works with both the annotation driver (ORM 2) and the attribute driver (ORM 2.9+/3) - flush via new flushEntry(): ORM 2 keeps single-entity flush() so enqueueing a mail does not write the caller's unrelated pending changes; the full flush() is used only on ORM 3, where single-entity flush was removed (detected via reflection) - fix implicitly nullable parameter in setMessage() (deprecated since PHP 8.4) --- composer.json | 4 ++-- src/Entity/AbstractMailQueueEntry.php | 13 ++++++++++- src/Entity/MailQueueEntry.php | 1 + src/Service/QueueMailer.php | 4 +++- src/Service/QueueService.php | 32 ++++++++++++++++++++++----- src/Traits/Identifier.php | 5 +++++ 6 files changed, 50 insertions(+), 9 deletions(-) diff --git a/composer.json b/composer.json index de7d184..2bc9261 100644 --- a/composer.json +++ b/composer.json @@ -11,8 +11,8 @@ ], "minimum-stability": "stable", "require": { - "php": "^7.1|^8.0", - "nette/mail": "~3.0", + "php": "^8.1", + "nette/mail": "^3.1 || ^4.0", "nette/di": "^2.3 || ~3.0", "tracy/tracy": "^2.3", "adt/background-queue": "^4.22" diff --git a/src/Entity/AbstractMailQueueEntry.php b/src/Entity/AbstractMailQueueEntry.php index 82d7aa1..20f57e5 100644 --- a/src/Entity/AbstractMailQueueEntry.php +++ b/src/Entity/AbstractMailQueueEntry.php @@ -6,6 +6,11 @@ /** + * Mapping is duplicated as annotations and attributes on purpose: annotations + * are read by the annotation driver (ORM 2), attributes by the attribute + * driver (ORM 2.9+ and the only option in ORM 3). Each driver ignores the + * other format, so the class works with both ORM generations. + * * @ORM\MappedSuperclass * @property \DateTime $createdAt * @property \DateTime|NULL $sentAt @@ -13,31 +18,37 @@ * @property string $subject * @property \Nette\Mail\Message|NULL $message */ +#[ORM\MappedSuperclass] abstract class AbstractMailQueueEntry { /** * @ORM\Column(type="datetime") */ + #[ORM\Column(type: 'datetime')] protected $createdAt; /** * @ORM\Column(type="datetime", nullable=true) */ + #[ORM\Column(type: 'datetime', nullable: true)] protected $sentAt; /** * @ORM\Column(type="string", name="`from`") */ + #[ORM\Column(type: 'string', name: '`from`')] protected $from; /** * @ORM\Column(type="text") */ + #[ORM\Column(type: 'text')] protected $subject; /** * @ORM\Column(type="blob", nullable=true) */ + #[ORM\Column(type: 'blob', nullable: true)] protected $message; /** @@ -57,7 +68,7 @@ public function getMessage() { * @param \Nette\Mail\Message|NULL $message * @return $this */ - public function setMessage(\Nette\Mail\Message $message = NULL): self { + public function setMessage(?\Nette\Mail\Message $message = NULL): self { if ($message === NULL) { $this->message = NULL; } else { diff --git a/src/Entity/MailQueueEntry.php b/src/Entity/MailQueueEntry.php index 0a9efb3..dda5874 100644 --- a/src/Entity/MailQueueEntry.php +++ b/src/Entity/MailQueueEntry.php @@ -8,6 +8,7 @@ /** * @ORM\Entity */ +#[ORM\Entity] class MailQueueEntry extends AbstractMailQueueEntry { use \ADT\MailQueue\Traits\Identifier; } diff --git a/src/Service/QueueMailer.php b/src/Service/QueueMailer.php index 0bd9b8a..3471ace 100644 --- a/src/Service/QueueMailer.php +++ b/src/Service/QueueMailer.php @@ -2,7 +2,9 @@ namespace ADT\MailQueue\Service; -class QueueMailer implements \Nette\Mail\IMailer { +// Nette\Mail\Mailer exists since nette/mail 3.1 (IMailer is its alias there) +// and is the only name available in nette/mail 4, so it works for both generations. +class QueueMailer implements \Nette\Mail\Mailer { /** @var QueueService */ protected $queueService; diff --git a/src/Service/QueueService.php b/src/Service/QueueService.php index 60c6683..1c1549b 100644 --- a/src/Service/QueueService.php +++ b/src/Service/QueueService.php @@ -19,7 +19,7 @@ class QueueService { /** @var string */ protected $queueEntryClass; - /** @var \Kdyby\Doctrine\EntityManager */ + /** @var EntityManagerInterface */ protected $em; /** @var string */ @@ -28,7 +28,7 @@ class QueueService { /** @var string */ protected $mutexTimeFile; - /** @var \Nette\Mail\IMailer */ + /** @var \Nette\Mail\Mailer */ protected $mailer; /** @var IMessenger */ @@ -55,6 +55,9 @@ class QueueService { /** @var string */ protected $backgroundQueueCallbackName; + /** @var bool|null Detected lazily via reflection - see flushEntry() */ + protected $emSupportsSingleEntityFlush; + public function __construct($config, EntityManagerInterface $em) { if (! is_dir($config['tempDir'])) { mkdir($config['tempDir']); @@ -73,7 +76,7 @@ public function __construct($config, EntityManagerInterface $em) { $this->backgroundQueueCallbackName = $config['backgroundQueueCallbackName']; } - public function setMailer(\Nette\Mail\IMailer $mailer) { + public function setMailer(\Nette\Mail\Mailer $mailer) { $this->mailer = $mailer; return $this; } @@ -122,7 +125,7 @@ protected function createQueueEntry(\Nette\Mail\Message $message, $custom = []) public function enqueue(\Nette\Mail\Message $message, $custom = [], ?int $backgroundQueuePriority = null) { $entry = $this->createQueueEntry($message, $custom); $this->em->persist($entry); - $this->em->flush($entry); + $this->flushEntry($entry); $this->backgroundQueueService->publish($this->backgroundQueueCallbackName, [$entry->getId()], null, null, false, null, $backgroundQueuePriority); @@ -184,10 +187,29 @@ public function process(int $entryId) { } $this->em->persist($entry); - $this->em->flush($entry); + $this->flushEntry($entry); return TRUE; } + /** + * Flushes the queue entry without touching other pending changes when possible. + * + * ORM 2 supports flushing a single entity, which keeps the caller's unit of work + * untouched - enqueueing a mail must not write unrelated pending changes to DB. + * ORM 3 removed single-entity flush, so there the full flush() is the only option + * and callers should not enqueue mails with an unflushed unit of work. + */ + protected function flushEntry(Entity\AbstractMailQueueEntry $entry): void { + if ($this->emSupportsSingleEntityFlush === null) { + $this->emSupportsSingleEntityFlush = (new \ReflectionMethod($this->em, 'flush'))->getNumberOfParameters() > 0; + } + + if ($this->emSupportsSingleEntityFlush) { + $this->em->flush($entry); + } else { + $this->em->flush(); + } + } } diff --git a/src/Traits/Identifier.php b/src/Traits/Identifier.php index 654bce4..228060e 100644 --- a/src/Traits/Identifier.php +++ b/src/Traits/Identifier.php @@ -2,6 +2,8 @@ namespace ADT\MailQueue\Traits; +use Doctrine\ORM\Mapping as ORM; + trait Identifier { @@ -11,6 +13,9 @@ trait Identifier * @ORM\GeneratedValue * @var integer|null */ + #[ORM\Id] + #[ORM\Column(type: 'integer')] + #[ORM\GeneratedValue] protected $id; /** From 2ac3a7ccea2fd78fc44a6ed0d74384fd5b319c9c Mon Sep 17 00:00:00 2001 From: Viktor Masicek Date: Thu, 13 Aug 2026 18:31:13 +0200 Subject: [PATCH 2/2] Drop Doctrine annotations, keep attributes only Per review: the annotation driver is gone in ORM 3, so keeping the mapping duplicated in both formats only means two places to maintain. Attributes are read by the attribute driver in ORM 2.9+ as well. Projects that still read mapping with the annotation driver must switch to the attribute driver (at least for this package's namespace), so this makes the release a major one. Also declares doctrine/orm in require - it was an undeclared dependency even before this change. --- composer.json | 1 + src/Entity/AbstractMailQueueEntry.php | 22 ++-------------------- src/Entity/MailQueueEntry.php | 3 --- src/Traits/Identifier.php | 7 +------ 4 files changed, 4 insertions(+), 29 deletions(-) diff --git a/composer.json b/composer.json index 2bc9261..c0b7ae4 100644 --- a/composer.json +++ b/composer.json @@ -14,6 +14,7 @@ "php": "^8.1", "nette/mail": "^3.1 || ^4.0", "nette/di": "^2.3 || ~3.0", + "doctrine/orm": "^2.9 || ^3.0", "tracy/tracy": "^2.3", "adt/background-queue": "^4.22" }, diff --git a/src/Entity/AbstractMailQueueEntry.php b/src/Entity/AbstractMailQueueEntry.php index 20f57e5..6274e61 100644 --- a/src/Entity/AbstractMailQueueEntry.php +++ b/src/Entity/AbstractMailQueueEntry.php @@ -6,12 +6,9 @@ /** - * Mapping is duplicated as annotations and attributes on purpose: annotations - * are read by the annotation driver (ORM 2), attributes by the attribute - * driver (ORM 2.9+ and the only option in ORM 3). Each driver ignores the - * other format, so the class works with both ORM generations. + * Mapping is defined as PHP attributes only - the annotation driver was removed + * in ORM 3, so projects using this package need the attribute driver. * - * @ORM\MappedSuperclass * @property \DateTime $createdAt * @property \DateTime|NULL $sentAt * @property string $from @@ -21,33 +18,18 @@ #[ORM\MappedSuperclass] abstract class AbstractMailQueueEntry { - /** - * @ORM\Column(type="datetime") - */ #[ORM\Column(type: 'datetime')] protected $createdAt; - /** - * @ORM\Column(type="datetime", nullable=true) - */ #[ORM\Column(type: 'datetime', nullable: true)] protected $sentAt; - /** - * @ORM\Column(type="string", name="`from`") - */ #[ORM\Column(type: 'string', name: '`from`')] protected $from; - /** - * @ORM\Column(type="text") - */ #[ORM\Column(type: 'text')] protected $subject; - /** - * @ORM\Column(type="blob", nullable=true) - */ #[ORM\Column(type: 'blob', nullable: true)] protected $message; diff --git a/src/Entity/MailQueueEntry.php b/src/Entity/MailQueueEntry.php index dda5874..c1e6455 100644 --- a/src/Entity/MailQueueEntry.php +++ b/src/Entity/MailQueueEntry.php @@ -5,9 +5,6 @@ use Doctrine\ORM\Mapping as ORM; -/** - * @ORM\Entity - */ #[ORM\Entity] class MailQueueEntry extends AbstractMailQueueEntry { use \ADT\MailQueue\Traits\Identifier; diff --git a/src/Traits/Identifier.php b/src/Traits/Identifier.php index 228060e..793ca70 100644 --- a/src/Traits/Identifier.php +++ b/src/Traits/Identifier.php @@ -7,12 +7,7 @@ trait Identifier { - /** - * @ORM\Id - * @ORM\Column(type="integer") - * @ORM\GeneratedValue - * @var integer|null - */ + /** @var integer|null */ #[ORM\Id] #[ORM\Column(type: 'integer')] #[ORM\GeneratedValue]