diff --git a/composer.json b/composer.json index de7d184..c0b7ae4 100644 --- a/composer.json +++ b/composer.json @@ -11,9 +11,10 @@ ], "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", + "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 82d7aa1..6274e61 100644 --- a/src/Entity/AbstractMailQueueEntry.php +++ b/src/Entity/AbstractMailQueueEntry.php @@ -6,38 +6,31 @@ /** - * @ORM\MappedSuperclass + * Mapping is defined as PHP attributes only - the annotation driver was removed + * in ORM 3, so projects using this package need the attribute driver. + * * @property \DateTime $createdAt * @property \DateTime|NULL $sentAt * @property string $from * @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 +50,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..c1e6455 100644 --- a/src/Entity/MailQueueEntry.php +++ b/src/Entity/MailQueueEntry.php @@ -5,9 +5,7 @@ use Doctrine\ORM\Mapping as ORM; -/** - * @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..793ca70 100644 --- a/src/Traits/Identifier.php +++ b/src/Traits/Identifier.php @@ -2,15 +2,15 @@ namespace ADT\MailQueue\Traits; +use Doctrine\ORM\Mapping as ORM; + trait Identifier { - /** - * @ORM\Id - * @ORM\Column(type="integer") - * @ORM\GeneratedValue - * @var integer|null - */ + /** @var integer|null */ + #[ORM\Id] + #[ORM\Column(type: 'integer')] + #[ORM\GeneratedValue] protected $id; /**