1<?php 2 3/* 4 * This file is part of the Symfony package. 5 * 6 * (c) Fabien Potencier <fabien@symfony.com> 7 * 8 * For the full copyright and license information, please view the LICENSE 9 * file that was distributed with this source code. 10 */ 11 12namespace Symfony\Component\Mailer\Transport; 13 14use Symfony\Component\Mailer\Envelope; 15use Symfony\Component\Mailer\Exception\TransportException; 16use Symfony\Component\Mailer\Exception\TransportExceptionInterface; 17use Symfony\Component\Mailer\SentMessage; 18use Symfony\Component\Mime\RawMessage; 19 20/** 21 * Uses several Transports using a round robin algorithm. 22 * 23 * @author Fabien Potencier <fabien@symfony.com> 24 */ 25class RoundRobinTransport implements TransportInterface 26{ 27 private $deadTransports; 28 private $transports = []; 29 private $retryPeriod; 30 private $cursor = -1; 31 32 /** 33 * @param TransportInterface[] $transports 34 */ 35 public function __construct(array $transports, int $retryPeriod = 60) 36 { 37 if (!$transports) { 38 throw new TransportException(sprintf('"%s" must have at least one transport configured.', static::class)); 39 } 40 41 $this->transports = $transports; 42 $this->deadTransports = new \SplObjectStorage(); 43 $this->retryPeriod = $retryPeriod; 44 } 45 46 public function send(RawMessage $message, Envelope $envelope = null): ?SentMessage 47 { 48 while ($transport = $this->getNextTransport()) { 49 try { 50 return $transport->send($message, $envelope); 51 } catch (TransportExceptionInterface $e) { 52 $this->deadTransports[$transport] = microtime(true); 53 } 54 } 55 56 throw new TransportException('All transports failed.'); 57 } 58 59 public function __toString(): string 60 { 61 return $this->getNameSymbol().'('.implode(' ', array_map('strval', $this->transports)).')'; 62 } 63 64 /** 65 * Rotates the transport list around and returns the first instance. 66 */ 67 protected function getNextTransport(): ?TransportInterface 68 { 69 if (-1 === $this->cursor) { 70 $this->cursor = $this->getInitialCursor(); 71 } 72 73 $cursor = $this->cursor; 74 while (true) { 75 $transport = $this->transports[$cursor]; 76 77 if (!$this->isTransportDead($transport)) { 78 break; 79 } 80 81 if ((microtime(true) - $this->deadTransports[$transport]) > $this->retryPeriod) { 82 $this->deadTransports->detach($transport); 83 84 break; 85 } 86 87 if ($this->cursor === $cursor = $this->moveCursor($cursor)) { 88 return null; 89 } 90 } 91 92 $this->cursor = $this->moveCursor($cursor); 93 94 return $transport; 95 } 96 97 protected function isTransportDead(TransportInterface $transport): bool 98 { 99 return $this->deadTransports->contains($transport); 100 } 101 102 protected function getInitialCursor(): int 103 { 104 // the cursor initial value is randomized so that 105 // when are not in a daemon, we are still rotating the transports 106 return mt_rand(0, \count($this->transports) - 1); 107 } 108 109 protected function getNameSymbol(): string 110 { 111 return 'roundrobin'; 112 } 113 114 private function moveCursor(int $cursor): int 115 { 116 return ++$cursor >= \count($this->transports) ? 0 : $cursor; 117 } 118} 119