(Grav GitSync) Automatic Commit from GitSync
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
CHANGELOG
|
||||
=========
|
||||
|
||||
5.3
|
||||
---
|
||||
|
||||
* Deprecated the `prefetch_count` parameter, it has no effect and will be removed in Symfony 6.0.
|
||||
* `AmqpReceiver` implements `QueueReceiverInterface` to fetch messages from a specific set of queues.
|
||||
* Add ability to distinguish retry and delay actions
|
||||
|
||||
5.2.0
|
||||
-----
|
||||
|
||||
* Add option to confirm message delivery
|
||||
* DSN now support AMQPS out-of-the-box.
|
||||
|
||||
5.1.0
|
||||
-----
|
||||
|
||||
* Introduced the AMQP bridge.
|
||||
* Deprecated use of invalid options
|
||||
@@ -0,0 +1,19 @@
|
||||
Copyright (c) 2018-present Fabien Potencier
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is furnished
|
||||
to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
@@ -0,0 +1,12 @@
|
||||
AMQP Messenger
|
||||
==============
|
||||
|
||||
Provides AMQP integration for Symfony Messenger.
|
||||
|
||||
Resources
|
||||
---------
|
||||
|
||||
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
|
||||
* [Report issues](https://github.com/symfony/symfony/issues) and
|
||||
[send Pull Requests](https://github.com/symfony/symfony/pulls)
|
||||
in the [main Symfony repository](https://github.com/symfony/symfony)
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Messenger\Bridge\Amqp\Transport;
|
||||
|
||||
class AmqpFactory
|
||||
{
|
||||
public function createConnection(array $credentials): \AMQPConnection
|
||||
{
|
||||
return new \AMQPConnection($credentials);
|
||||
}
|
||||
|
||||
public function createChannel(\AMQPConnection $connection): \AMQPChannel
|
||||
{
|
||||
return new \AMQPChannel($connection);
|
||||
}
|
||||
|
||||
public function createQueue(\AMQPChannel $channel): \AMQPQueue
|
||||
{
|
||||
return new \AMQPQueue($channel);
|
||||
}
|
||||
|
||||
public function createExchange(\AMQPChannel $channel): \AMQPExchange
|
||||
{
|
||||
return new \AMQPExchange($channel);
|
||||
}
|
||||
}
|
||||
|
||||
if (!class_exists(\Symfony\Component\Messenger\Transport\AmqpExt\AmqpFactory::class, false)) {
|
||||
class_alias(AmqpFactory::class, \Symfony\Component\Messenger\Transport\AmqpExt\AmqpFactory::class);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Messenger\Bridge\Amqp\Transport;
|
||||
|
||||
use Symfony\Component\Messenger\Stamp\NonSendableStampInterface;
|
||||
|
||||
/**
|
||||
* Stamp applied when a message is received from Amqp.
|
||||
*/
|
||||
class AmqpReceivedStamp implements NonSendableStampInterface
|
||||
{
|
||||
private $amqpEnvelope;
|
||||
private $queueName;
|
||||
|
||||
public function __construct(\AMQPEnvelope $amqpEnvelope, string $queueName)
|
||||
{
|
||||
$this->amqpEnvelope = $amqpEnvelope;
|
||||
$this->queueName = $queueName;
|
||||
}
|
||||
|
||||
public function getAmqpEnvelope(): \AMQPEnvelope
|
||||
{
|
||||
return $this->amqpEnvelope;
|
||||
}
|
||||
|
||||
public function getQueueName(): string
|
||||
{
|
||||
return $this->queueName;
|
||||
}
|
||||
}
|
||||
|
||||
if (!class_exists(\Symfony\Component\Messenger\Transport\AmqpExt\AmqpReceivedStamp::class, false)) {
|
||||
class_alias(AmqpReceivedStamp::class, \Symfony\Component\Messenger\Transport\AmqpExt\AmqpReceivedStamp::class);
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Messenger\Bridge\Amqp\Transport;
|
||||
|
||||
use Symfony\Component\Messenger\Envelope;
|
||||
use Symfony\Component\Messenger\Exception\LogicException;
|
||||
use Symfony\Component\Messenger\Exception\MessageDecodingFailedException;
|
||||
use Symfony\Component\Messenger\Exception\TransportException;
|
||||
use Symfony\Component\Messenger\Transport\Receiver\MessageCountAwareInterface;
|
||||
use Symfony\Component\Messenger\Transport\Receiver\QueueReceiverInterface;
|
||||
use Symfony\Component\Messenger\Transport\Serialization\PhpSerializer;
|
||||
use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface;
|
||||
|
||||
/**
|
||||
* Symfony Messenger receiver to get messages from AMQP brokers using PHP's AMQP extension.
|
||||
*
|
||||
* @author Samuel Roze <samuel.roze@gmail.com>
|
||||
*/
|
||||
class AmqpReceiver implements QueueReceiverInterface, MessageCountAwareInterface
|
||||
{
|
||||
private $serializer;
|
||||
private $connection;
|
||||
|
||||
public function __construct(Connection $connection, ?SerializerInterface $serializer = null)
|
||||
{
|
||||
$this->connection = $connection;
|
||||
$this->serializer = $serializer ?? new PhpSerializer();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function get(): iterable
|
||||
{
|
||||
yield from $this->getFromQueues($this->connection->getQueueNames());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFromQueues(array $queueNames): iterable
|
||||
{
|
||||
foreach ($queueNames as $queueName) {
|
||||
yield from $this->getEnvelope($queueName);
|
||||
}
|
||||
}
|
||||
|
||||
private function getEnvelope(string $queueName): iterable
|
||||
{
|
||||
try {
|
||||
$amqpEnvelope = $this->connection->get($queueName);
|
||||
} catch (\AMQPException $exception) {
|
||||
throw new TransportException($exception->getMessage(), 0, $exception);
|
||||
}
|
||||
|
||||
if (null === $amqpEnvelope) {
|
||||
return;
|
||||
}
|
||||
|
||||
$body = $amqpEnvelope->getBody();
|
||||
|
||||
try {
|
||||
$envelope = $this->serializer->decode([
|
||||
'body' => false === $body ? '' : $body, // workaround https://github.com/pdezwart/php-amqp/issues/351
|
||||
'headers' => $amqpEnvelope->getHeaders(),
|
||||
]);
|
||||
} catch (MessageDecodingFailedException $exception) {
|
||||
// invalid message of some type
|
||||
$this->rejectAmqpEnvelope($amqpEnvelope, $queueName);
|
||||
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
yield $envelope->with(new AmqpReceivedStamp($amqpEnvelope, $queueName));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function ack(Envelope $envelope): void
|
||||
{
|
||||
try {
|
||||
$stamp = $this->findAmqpStamp($envelope);
|
||||
|
||||
$this->connection->ack(
|
||||
$stamp->getAmqpEnvelope(),
|
||||
$stamp->getQueueName()
|
||||
);
|
||||
} catch (\AMQPException $exception) {
|
||||
throw new TransportException($exception->getMessage(), 0, $exception);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function reject(Envelope $envelope): void
|
||||
{
|
||||
$stamp = $this->findAmqpStamp($envelope);
|
||||
|
||||
$this->rejectAmqpEnvelope(
|
||||
$stamp->getAmqpEnvelope(),
|
||||
$stamp->getQueueName()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getMessageCount(): int
|
||||
{
|
||||
try {
|
||||
return $this->connection->countMessagesInQueues();
|
||||
} catch (\AMQPException $exception) {
|
||||
throw new TransportException($exception->getMessage(), 0, $exception);
|
||||
}
|
||||
}
|
||||
|
||||
private function rejectAmqpEnvelope(\AMQPEnvelope $amqpEnvelope, string $queueName): void
|
||||
{
|
||||
try {
|
||||
$this->connection->nack($amqpEnvelope, $queueName, \AMQP_NOPARAM);
|
||||
} catch (\AMQPException $exception) {
|
||||
throw new TransportException($exception->getMessage(), 0, $exception);
|
||||
}
|
||||
}
|
||||
|
||||
private function findAmqpStamp(Envelope $envelope): AmqpReceivedStamp
|
||||
{
|
||||
$amqpReceivedStamp = $envelope->last(AmqpReceivedStamp::class);
|
||||
if (null === $amqpReceivedStamp) {
|
||||
throw new LogicException('No "AmqpReceivedStamp" stamp found on the Envelope.');
|
||||
}
|
||||
|
||||
return $amqpReceivedStamp;
|
||||
}
|
||||
}
|
||||
|
||||
if (!class_exists(\Symfony\Component\Messenger\Transport\AmqpExt\AmqpReceiver::class, false)) {
|
||||
class_alias(AmqpReceiver::class, \Symfony\Component\Messenger\Transport\AmqpExt\AmqpReceiver::class);
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Messenger\Bridge\Amqp\Transport;
|
||||
|
||||
use Symfony\Component\Messenger\Envelope;
|
||||
use Symfony\Component\Messenger\Exception\TransportException;
|
||||
use Symfony\Component\Messenger\Stamp\DelayStamp;
|
||||
use Symfony\Component\Messenger\Stamp\RedeliveryStamp;
|
||||
use Symfony\Component\Messenger\Transport\Sender\SenderInterface;
|
||||
use Symfony\Component\Messenger\Transport\Serialization\PhpSerializer;
|
||||
use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface;
|
||||
|
||||
/**
|
||||
* Symfony Messenger sender to send messages to AMQP brokers using PHP's AMQP extension.
|
||||
*
|
||||
* @author Samuel Roze <samuel.roze@gmail.com>
|
||||
*/
|
||||
class AmqpSender implements SenderInterface
|
||||
{
|
||||
private $serializer;
|
||||
private $connection;
|
||||
|
||||
public function __construct(Connection $connection, ?SerializerInterface $serializer = null)
|
||||
{
|
||||
$this->connection = $connection;
|
||||
$this->serializer = $serializer ?? new PhpSerializer();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function send(Envelope $envelope): Envelope
|
||||
{
|
||||
$encodedMessage = $this->serializer->encode($envelope);
|
||||
|
||||
/** @var DelayStamp|null $delayStamp */
|
||||
$delayStamp = $envelope->last(DelayStamp::class);
|
||||
$delay = $delayStamp ? $delayStamp->getDelay() : 0;
|
||||
|
||||
/** @var AmqpStamp|null $amqpStamp */
|
||||
$amqpStamp = $envelope->last(AmqpStamp::class);
|
||||
if (isset($encodedMessage['headers']['Content-Type'])) {
|
||||
$contentType = $encodedMessage['headers']['Content-Type'];
|
||||
unset($encodedMessage['headers']['Content-Type']);
|
||||
|
||||
if (!$amqpStamp || !isset($amqpStamp->getAttributes()['content_type'])) {
|
||||
$amqpStamp = AmqpStamp::createWithAttributes(['content_type' => $contentType], $amqpStamp);
|
||||
}
|
||||
}
|
||||
|
||||
$amqpReceivedStamp = $envelope->last(AmqpReceivedStamp::class);
|
||||
if ($amqpReceivedStamp instanceof AmqpReceivedStamp) {
|
||||
$amqpStamp = AmqpStamp::createFromAmqpEnvelope(
|
||||
$amqpReceivedStamp->getAmqpEnvelope(),
|
||||
$amqpStamp,
|
||||
$envelope->last(RedeliveryStamp::class) ? $amqpReceivedStamp->getQueueName() : null
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->connection->publish(
|
||||
$encodedMessage['body'],
|
||||
$encodedMessage['headers'] ?? [],
|
||||
$delay,
|
||||
$amqpStamp
|
||||
);
|
||||
} catch (\AMQPException $e) {
|
||||
throw new TransportException($e->getMessage(), 0, $e);
|
||||
}
|
||||
|
||||
return $envelope;
|
||||
}
|
||||
}
|
||||
|
||||
if (!class_exists(\Symfony\Component\Messenger\Transport\AmqpExt\AmqpSender::class, false)) {
|
||||
class_alias(AmqpSender::class, \Symfony\Component\Messenger\Transport\AmqpExt\AmqpSender::class);
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Messenger\Bridge\Amqp\Transport;
|
||||
|
||||
use Symfony\Component\Messenger\Stamp\NonSendableStampInterface;
|
||||
|
||||
/**
|
||||
* @author Guillaume Gammelin <ggammelin@gmail.com>
|
||||
* @author Samuel Roze <samuel.roze@gmail.com>
|
||||
*/
|
||||
final class AmqpStamp implements NonSendableStampInterface
|
||||
{
|
||||
private $routingKey;
|
||||
private $flags;
|
||||
private $attributes;
|
||||
private $isRetryAttempt = false;
|
||||
|
||||
public function __construct(?string $routingKey = null, int $flags = \AMQP_NOPARAM, array $attributes = [])
|
||||
{
|
||||
$this->routingKey = $routingKey;
|
||||
$this->flags = $flags;
|
||||
$this->attributes = $attributes;
|
||||
}
|
||||
|
||||
public function getRoutingKey(): ?string
|
||||
{
|
||||
return $this->routingKey;
|
||||
}
|
||||
|
||||
public function getFlags(): int
|
||||
{
|
||||
return $this->flags;
|
||||
}
|
||||
|
||||
public function getAttributes(): array
|
||||
{
|
||||
return $this->attributes;
|
||||
}
|
||||
|
||||
public static function createFromAmqpEnvelope(\AMQPEnvelope $amqpEnvelope, ?self $previousStamp = null, ?string $retryRoutingKey = null): self
|
||||
{
|
||||
$attr = $previousStamp->attributes ?? [];
|
||||
|
||||
$attr['headers'] = $attr['headers'] ?? $amqpEnvelope->getHeaders();
|
||||
$attr['content_type'] = $attr['content_type'] ?? $amqpEnvelope->getContentType();
|
||||
$attr['content_encoding'] = $attr['content_encoding'] ?? $amqpEnvelope->getContentEncoding();
|
||||
$attr['delivery_mode'] = $attr['delivery_mode'] ?? $amqpEnvelope->getDeliveryMode();
|
||||
$attr['priority'] = $attr['priority'] ?? $amqpEnvelope->getPriority();
|
||||
$attr['timestamp'] = $attr['timestamp'] ?? $amqpEnvelope->getTimestamp();
|
||||
$attr['app_id'] = $attr['app_id'] ?? $amqpEnvelope->getAppId();
|
||||
$attr['message_id'] = $attr['message_id'] ?? $amqpEnvelope->getMessageId();
|
||||
$attr['user_id'] = $attr['user_id'] ?? $amqpEnvelope->getUserId();
|
||||
$attr['expiration'] = $attr['expiration'] ?? $amqpEnvelope->getExpiration();
|
||||
$attr['type'] = $attr['type'] ?? $amqpEnvelope->getType();
|
||||
$attr['reply_to'] = $attr['reply_to'] ?? $amqpEnvelope->getReplyTo();
|
||||
$attr['correlation_id'] = $attr['correlation_id'] ?? $amqpEnvelope->getCorrelationId();
|
||||
|
||||
if (null === $retryRoutingKey) {
|
||||
$stamp = new self($previousStamp->routingKey ?? $amqpEnvelope->getRoutingKey(), $previousStamp->flags ?? \AMQP_NOPARAM, $attr);
|
||||
} else {
|
||||
$stamp = new self($retryRoutingKey, $previousStamp->flags ?? \AMQP_NOPARAM, $attr);
|
||||
$stamp->isRetryAttempt = true;
|
||||
}
|
||||
|
||||
return $stamp;
|
||||
}
|
||||
|
||||
public function isRetryAttempt(): bool
|
||||
{
|
||||
return $this->isRetryAttempt;
|
||||
}
|
||||
|
||||
public static function createWithAttributes(array $attributes, ?self $previousStamp = null): self
|
||||
{
|
||||
return new self(
|
||||
$previousStamp->routingKey ?? null,
|
||||
$previousStamp->flags ?? \AMQP_NOPARAM,
|
||||
array_merge($previousStamp->attributes ?? [], $attributes)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!class_exists(\Symfony\Component\Messenger\Transport\AmqpExt\AmqpStamp::class, false)) {
|
||||
class_alias(AmqpStamp::class, \Symfony\Component\Messenger\Transport\AmqpExt\AmqpStamp::class);
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Messenger\Bridge\Amqp\Transport;
|
||||
|
||||
use Symfony\Component\Messenger\Envelope;
|
||||
use Symfony\Component\Messenger\Transport\Receiver\MessageCountAwareInterface;
|
||||
use Symfony\Component\Messenger\Transport\Receiver\QueueReceiverInterface;
|
||||
use Symfony\Component\Messenger\Transport\Serialization\PhpSerializer;
|
||||
use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface;
|
||||
use Symfony\Component\Messenger\Transport\SetupableTransportInterface;
|
||||
use Symfony\Component\Messenger\Transport\TransportInterface;
|
||||
|
||||
/**
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*/
|
||||
class AmqpTransport implements QueueReceiverInterface, TransportInterface, SetupableTransportInterface, MessageCountAwareInterface
|
||||
{
|
||||
private $serializer;
|
||||
private $connection;
|
||||
private $receiver;
|
||||
private $sender;
|
||||
|
||||
public function __construct(Connection $connection, ?SerializerInterface $serializer = null)
|
||||
{
|
||||
$this->connection = $connection;
|
||||
$this->serializer = $serializer ?? new PhpSerializer();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function get(): iterable
|
||||
{
|
||||
return ($this->receiver ?? $this->getReceiver())->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFromQueues(array $queueNames): iterable
|
||||
{
|
||||
return ($this->receiver ?? $this->getReceiver())->getFromQueues($queueNames);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function ack(Envelope $envelope): void
|
||||
{
|
||||
($this->receiver ?? $this->getReceiver())->ack($envelope);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function reject(Envelope $envelope): void
|
||||
{
|
||||
($this->receiver ?? $this->getReceiver())->reject($envelope);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function send(Envelope $envelope): Envelope
|
||||
{
|
||||
return ($this->sender ?? $this->getSender())->send($envelope);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setup(): void
|
||||
{
|
||||
$this->connection->setup();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getMessageCount(): int
|
||||
{
|
||||
return ($this->receiver ?? $this->getReceiver())->getMessageCount();
|
||||
}
|
||||
|
||||
private function getReceiver(): AmqpReceiver
|
||||
{
|
||||
return $this->receiver = new AmqpReceiver($this->connection, $this->serializer);
|
||||
}
|
||||
|
||||
private function getSender(): AmqpSender
|
||||
{
|
||||
return $this->sender = new AmqpSender($this->connection, $this->serializer);
|
||||
}
|
||||
}
|
||||
|
||||
if (!class_exists(\Symfony\Component\Messenger\Transport\AmqpExt\AmqpTransport::class, false)) {
|
||||
class_alias(AmqpTransport::class, \Symfony\Component\Messenger\Transport\AmqpExt\AmqpTransport::class);
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Messenger\Bridge\Amqp\Transport;
|
||||
|
||||
use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface;
|
||||
use Symfony\Component\Messenger\Transport\TransportFactoryInterface;
|
||||
use Symfony\Component\Messenger\Transport\TransportInterface;
|
||||
|
||||
/**
|
||||
* @author Samuel Roze <samuel.roze@gmail.com>
|
||||
*/
|
||||
class AmqpTransportFactory implements TransportFactoryInterface
|
||||
{
|
||||
public function createTransport(string $dsn, array $options, SerializerInterface $serializer): TransportInterface
|
||||
{
|
||||
unset($options['transport_name']);
|
||||
|
||||
return new AmqpTransport(Connection::fromDsn($dsn, $options), $serializer);
|
||||
}
|
||||
|
||||
public function supports(string $dsn, array $options): bool
|
||||
{
|
||||
return 0 === strpos($dsn, 'amqp://') || 0 === strpos($dsn, 'amqps://');
|
||||
}
|
||||
}
|
||||
|
||||
if (!class_exists(\Symfony\Component\Messenger\Transport\AmqpExt\AmqpTransportFactory::class, false)) {
|
||||
class_alias(AmqpTransportFactory::class, \Symfony\Component\Messenger\Transport\AmqpExt\AmqpTransportFactory::class);
|
||||
}
|
||||
@@ -0,0 +1,626 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Messenger\Bridge\Amqp\Transport;
|
||||
|
||||
use Symfony\Component\Messenger\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Messenger\Exception\LogicException;
|
||||
use Symfony\Component\Messenger\Exception\TransportException;
|
||||
|
||||
/**
|
||||
* An AMQP connection.
|
||||
*
|
||||
* @author Samuel Roze <samuel.roze@gmail.com>
|
||||
*
|
||||
* @final
|
||||
*/
|
||||
class Connection
|
||||
{
|
||||
private const ARGUMENTS_AS_INTEGER = [
|
||||
'x-delay',
|
||||
'x-expires',
|
||||
'x-max-length',
|
||||
'x-max-length-bytes',
|
||||
'x-max-priority',
|
||||
'x-message-ttl',
|
||||
];
|
||||
|
||||
/**
|
||||
* @see https://github.com/php-amqp/php-amqp/blob/master/amqp_connection_resource.h
|
||||
*/
|
||||
private const AVAILABLE_OPTIONS = [
|
||||
'host',
|
||||
'port',
|
||||
'vhost',
|
||||
'user',
|
||||
'login',
|
||||
'password',
|
||||
'queues',
|
||||
'exchange',
|
||||
'delay',
|
||||
'auto_setup',
|
||||
'prefetch_count',
|
||||
'retry',
|
||||
'persistent',
|
||||
'frame_max',
|
||||
'channel_max',
|
||||
'heartbeat',
|
||||
'read_timeout',
|
||||
'write_timeout',
|
||||
'confirm_timeout',
|
||||
'connect_timeout',
|
||||
'rpc_timeout',
|
||||
'cacert',
|
||||
'cert',
|
||||
'key',
|
||||
'verify',
|
||||
'sasl_method',
|
||||
];
|
||||
|
||||
private const AVAILABLE_QUEUE_OPTIONS = [
|
||||
'binding_keys',
|
||||
'binding_arguments',
|
||||
'flags',
|
||||
'arguments',
|
||||
];
|
||||
|
||||
private const AVAILABLE_EXCHANGE_OPTIONS = [
|
||||
'name',
|
||||
'type',
|
||||
'default_publish_routing_key',
|
||||
'flags',
|
||||
'arguments',
|
||||
];
|
||||
|
||||
private $connectionOptions;
|
||||
private $exchangeOptions;
|
||||
private $queuesOptions;
|
||||
private $amqpFactory;
|
||||
private $autoSetupExchange;
|
||||
private $autoSetupDelayExchange;
|
||||
|
||||
/**
|
||||
* @var \AMQPChannel|null
|
||||
*/
|
||||
private $amqpChannel;
|
||||
|
||||
/**
|
||||
* @var \AMQPExchange|null
|
||||
*/
|
||||
private $amqpExchange;
|
||||
|
||||
/**
|
||||
* @var \AMQPQueue[]|null
|
||||
*/
|
||||
private $amqpQueues = [];
|
||||
|
||||
/**
|
||||
* @var \AMQPExchange|null
|
||||
*/
|
||||
private $amqpDelayExchange;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $lastActivityTime = 0;
|
||||
|
||||
public function __construct(array $connectionOptions, array $exchangeOptions, array $queuesOptions, ?AmqpFactory $amqpFactory = null)
|
||||
{
|
||||
if (!\extension_loaded('amqp')) {
|
||||
throw new LogicException(sprintf('You cannot use the "%s" as the "amqp" extension is not installed.', __CLASS__));
|
||||
}
|
||||
|
||||
$this->connectionOptions = array_replace_recursive([
|
||||
'delay' => [
|
||||
'exchange_name' => 'delays',
|
||||
'queue_name_pattern' => 'delay_%exchange_name%_%routing_key%_%delay%',
|
||||
],
|
||||
], $connectionOptions);
|
||||
$this->autoSetupExchange = $this->autoSetupDelayExchange = $connectionOptions['auto_setup'] ?? true;
|
||||
$this->exchangeOptions = $exchangeOptions;
|
||||
$this->queuesOptions = $queuesOptions;
|
||||
$this->amqpFactory = $amqpFactory ?? new AmqpFactory();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a connection based on the DSN and options.
|
||||
*
|
||||
* Available options:
|
||||
*
|
||||
* * host: Hostname of the AMQP service
|
||||
* * port: Port of the AMQP service
|
||||
* * vhost: Virtual Host to use with the AMQP service
|
||||
* * user|login: Username to use to connect the AMQP service
|
||||
* * password: Password to use to connect to the AMQP service
|
||||
* * read_timeout: Timeout in for income activity. Note: 0 or greater seconds. May be fractional.
|
||||
* * write_timeout: Timeout in for outcome activity. Note: 0 or greater seconds. May be fractional.
|
||||
* * connect_timeout: Connection timeout. Note: 0 or greater seconds. May be fractional.
|
||||
* * confirm_timeout: Timeout in seconds for confirmation, if none specified transport will not wait for message confirmation. Note: 0 or greater seconds. May be fractional.
|
||||
* * queues[name]: An array of queues, keyed by the name
|
||||
* * binding_keys: The binding keys (if any) to bind to this queue
|
||||
* * binding_arguments: Arguments to be used while binding the queue.
|
||||
* * flags: Queue flags (Default: AMQP_DURABLE)
|
||||
* * arguments: Extra arguments
|
||||
* * exchange:
|
||||
* * name: Name of the exchange
|
||||
* * type: Type of exchange (Default: fanout)
|
||||
* * default_publish_routing_key: Routing key to use when publishing, if none is specified on the message
|
||||
* * flags: Exchange flags (Default: AMQP_DURABLE)
|
||||
* * arguments: Extra arguments
|
||||
* * delay:
|
||||
* * queue_name_pattern: Pattern to use to create the queues (Default: "delay_%exchange_name%_%routing_key%_%delay%")
|
||||
* * exchange_name: Name of the exchange to be used for the delayed/retried messages (Default: "delays")
|
||||
* * auto_setup: Enable or not the auto-setup of queues and exchanges (Default: true)
|
||||
*
|
||||
* * Connection tuning options (see http://www.rabbitmq.com/amqp-0-9-1-reference.html#connection.tune for details):
|
||||
* * channel_max: Specifies highest channel number that the server permits. 0 means standard extension limit
|
||||
* (see PHP_AMQP_MAX_CHANNELS constant)
|
||||
* * frame_max: The largest frame size that the server proposes for the connection, including frame header
|
||||
* and end-byte. 0 means standard extension limit (depends on librabbimq default frame size limit)
|
||||
* * heartbeat: The delay, in seconds, of the connection heartbeat that the server wants.
|
||||
* 0 means the server does not want a heartbeat. Note, librabbitmq has limited heartbeat support,
|
||||
* which means heartbeats checked only during blocking calls.
|
||||
*
|
||||
* TLS support (see https://www.rabbitmq.com/ssl.html for details):
|
||||
* * cacert: Path to the CA cert file in PEM format.
|
||||
* * cert: Path to the client certificate in PEM format.
|
||||
* * key: Path to the client key in PEM format.
|
||||
* * verify: Enable or disable peer verification. If peer verification is enabled then the common name in the
|
||||
* server certificate must match the server name. Peer verification is enabled by default.
|
||||
*/
|
||||
public static function fromDsn(string $dsn, array $options = [], ?AmqpFactory $amqpFactory = null): self
|
||||
{
|
||||
if (false === $params = parse_url($dsn)) {
|
||||
// this is a valid URI that parse_url cannot handle when you want to pass all parameters as options
|
||||
if (!\in_array($dsn, ['amqp://', 'amqps://'])) {
|
||||
throw new InvalidArgumentException('The given AMQP DSN is invalid.');
|
||||
}
|
||||
|
||||
$params = [];
|
||||
}
|
||||
|
||||
$useAmqps = 0 === strpos($dsn, 'amqps://');
|
||||
$pathParts = isset($params['path']) ? explode('/', trim($params['path'], '/')) : [];
|
||||
$exchangeName = $pathParts[1] ?? 'messages';
|
||||
parse_str($params['query'] ?? '', $parsedQuery);
|
||||
$port = $useAmqps ? 5671 : 5672;
|
||||
|
||||
$amqpOptions = array_replace_recursive([
|
||||
'host' => $params['host'] ?? 'localhost',
|
||||
'port' => $params['port'] ?? $port,
|
||||
'vhost' => isset($pathParts[0]) ? urldecode($pathParts[0]) : '/',
|
||||
'exchange' => [
|
||||
'name' => $exchangeName,
|
||||
],
|
||||
], $options, $parsedQuery);
|
||||
|
||||
self::validateOptions($amqpOptions);
|
||||
|
||||
if (isset($params['user'])) {
|
||||
$amqpOptions['login'] = rawurldecode($params['user']);
|
||||
}
|
||||
|
||||
if (isset($params['pass'])) {
|
||||
$amqpOptions['password'] = rawurldecode($params['pass']);
|
||||
}
|
||||
|
||||
if (!isset($amqpOptions['queues'])) {
|
||||
$amqpOptions['queues'][$exchangeName] = [];
|
||||
}
|
||||
|
||||
$exchangeOptions = $amqpOptions['exchange'];
|
||||
$queuesOptions = $amqpOptions['queues'];
|
||||
unset($amqpOptions['queues'], $amqpOptions['exchange']);
|
||||
if (isset($amqpOptions['auto_setup'])) {
|
||||
$amqpOptions['auto_setup'] = filter_var($amqpOptions['auto_setup'], \FILTER_VALIDATE_BOOLEAN);
|
||||
}
|
||||
|
||||
$queuesOptions = array_map(function ($queueOptions) {
|
||||
if (!\is_array($queueOptions)) {
|
||||
$queueOptions = [];
|
||||
}
|
||||
if (\is_array($queueOptions['arguments'] ?? false)) {
|
||||
$queueOptions['arguments'] = self::normalizeQueueArguments($queueOptions['arguments']);
|
||||
}
|
||||
|
||||
return $queueOptions;
|
||||
}, $queuesOptions);
|
||||
|
||||
if (!$useAmqps) {
|
||||
unset($amqpOptions['cacert'], $amqpOptions['cert'], $amqpOptions['key'], $amqpOptions['verify']);
|
||||
}
|
||||
|
||||
if ($useAmqps && !self::hasCaCertConfigured($amqpOptions)) {
|
||||
throw new InvalidArgumentException('No CA certificate has been provided. Set "amqp.cacert" in your php.ini or pass the "cacert" parameter in the DSN to use SSL. Alternatively, you can use amqp:// to use without SSL.');
|
||||
}
|
||||
|
||||
return new self($amqpOptions, $exchangeOptions, $queuesOptions, $amqpFactory);
|
||||
}
|
||||
|
||||
private static function validateOptions(array $options): void
|
||||
{
|
||||
if (0 < \count($invalidOptions = array_diff(array_keys($options), self::AVAILABLE_OPTIONS))) {
|
||||
trigger_deprecation('symfony/messenger', '5.1', 'Invalid option(s) "%s" passed to the AMQP Messenger transport. Passing invalid options is deprecated.', implode('", "', $invalidOptions));
|
||||
}
|
||||
|
||||
if (isset($options['prefetch_count'])) {
|
||||
trigger_deprecation('symfony/messenger', '5.3', 'The "prefetch_count" option passed to the AMQP Messenger transport has no effect and should not be used.');
|
||||
}
|
||||
|
||||
if (\is_array($options['queues'] ?? false)) {
|
||||
foreach ($options['queues'] as $queue) {
|
||||
if (!\is_array($queue)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (0 < \count($invalidQueueOptions = array_diff(array_keys($queue), self::AVAILABLE_QUEUE_OPTIONS))) {
|
||||
trigger_deprecation('symfony/messenger', '5.1', 'Invalid queue option(s) "%s" passed to the AMQP Messenger transport. Passing invalid queue options is deprecated.', implode('", "', $invalidQueueOptions));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (\is_array($options['exchange'] ?? false)
|
||||
&& 0 < \count($invalidExchangeOptions = array_diff(array_keys($options['exchange']), self::AVAILABLE_EXCHANGE_OPTIONS))) {
|
||||
trigger_deprecation('symfony/messenger', '5.1', 'Invalid exchange option(s) "%s" passed to the AMQP Messenger transport. Passing invalid exchange options is deprecated.', implode('", "', $invalidExchangeOptions));
|
||||
}
|
||||
}
|
||||
|
||||
private static function normalizeQueueArguments(array $arguments): array
|
||||
{
|
||||
foreach (self::ARGUMENTS_AS_INTEGER as $key) {
|
||||
if (!\array_key_exists($key, $arguments)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!is_numeric($arguments[$key])) {
|
||||
throw new InvalidArgumentException(sprintf('Integer expected for queue argument "%s", "%s" given.', $key, get_debug_type($arguments[$key])));
|
||||
}
|
||||
|
||||
$arguments[$key] = (int) $arguments[$key];
|
||||
}
|
||||
|
||||
return $arguments;
|
||||
}
|
||||
|
||||
private static function hasCaCertConfigured(array $amqpOptions): bool
|
||||
{
|
||||
return (isset($amqpOptions['cacert']) && '' !== $amqpOptions['cacert']) || '' !== \ini_get('amqp.cacert');
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \AMQPException
|
||||
*/
|
||||
public function publish(string $body, array $headers = [], int $delayInMs = 0, ?AmqpStamp $amqpStamp = null): void
|
||||
{
|
||||
$this->clearWhenDisconnected();
|
||||
|
||||
if ($this->autoSetupExchange) {
|
||||
$this->setupExchangeAndQueues(); // also setup normal exchange for delayed messages so delay queue can DLX messages to it
|
||||
}
|
||||
|
||||
$this->withConnectionExceptionRetry(function () use ($body, $headers, $delayInMs, $amqpStamp) {
|
||||
if (0 !== $delayInMs) {
|
||||
$this->publishWithDelay($body, $headers, $delayInMs, $amqpStamp);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->publishOnExchange(
|
||||
$this->exchange(),
|
||||
$body,
|
||||
$this->getRoutingKeyForMessage($amqpStamp),
|
||||
$headers,
|
||||
$amqpStamp
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an approximate count of the messages in defined queues.
|
||||
*/
|
||||
public function countMessagesInQueues(): int
|
||||
{
|
||||
return array_sum(array_map(function ($queueName) {
|
||||
return $this->queue($queueName)->declareQueue();
|
||||
}, $this->getQueueNames()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \AMQPException
|
||||
*/
|
||||
private function publishWithDelay(string $body, array $headers, int $delay, ?AmqpStamp $amqpStamp = null)
|
||||
{
|
||||
$routingKey = $this->getRoutingKeyForMessage($amqpStamp);
|
||||
$isRetryAttempt = $amqpStamp ? $amqpStamp->isRetryAttempt() : false;
|
||||
|
||||
$this->setupDelay($delay, $routingKey, $isRetryAttempt);
|
||||
|
||||
$this->publishOnExchange(
|
||||
$this->getDelayExchange(),
|
||||
$body,
|
||||
$this->getRoutingKeyForDelay($delay, $routingKey, $isRetryAttempt),
|
||||
$headers,
|
||||
$amqpStamp
|
||||
);
|
||||
}
|
||||
|
||||
private function publishOnExchange(\AMQPExchange $exchange, string $body, ?string $routingKey = null, array $headers = [], ?AmqpStamp $amqpStamp = null)
|
||||
{
|
||||
$attributes = $amqpStamp ? $amqpStamp->getAttributes() : [];
|
||||
$attributes['headers'] = array_merge($attributes['headers'] ?? [], $headers);
|
||||
$attributes['delivery_mode'] = $attributes['delivery_mode'] ?? 2;
|
||||
$attributes['timestamp'] = $attributes['timestamp'] ?? time();
|
||||
|
||||
$this->lastActivityTime = time();
|
||||
|
||||
$exchange->publish(
|
||||
$body,
|
||||
$routingKey,
|
||||
$amqpStamp ? $amqpStamp->getFlags() : \AMQP_NOPARAM,
|
||||
$attributes
|
||||
);
|
||||
|
||||
if ('' !== ($this->connectionOptions['confirm_timeout'] ?? '')) {
|
||||
$this->channel()->waitForConfirm((float) $this->connectionOptions['confirm_timeout']);
|
||||
}
|
||||
}
|
||||
|
||||
private function setupDelay(int $delay, ?string $routingKey, bool $isRetryAttempt)
|
||||
{
|
||||
if ($this->autoSetupDelayExchange) {
|
||||
$this->setupDelayExchange();
|
||||
}
|
||||
|
||||
$queue = $this->createDelayQueue($delay, $routingKey, $isRetryAttempt);
|
||||
$queue->declareQueue(); // the delay queue always need to be declared because the name is dynamic and cannot be declared in advance
|
||||
$queue->bind($this->connectionOptions['delay']['exchange_name'], $this->getRoutingKeyForDelay($delay, $routingKey, $isRetryAttempt));
|
||||
}
|
||||
|
||||
private function getDelayExchange(): \AMQPExchange
|
||||
{
|
||||
if (null === $this->amqpDelayExchange) {
|
||||
$this->amqpDelayExchange = $this->amqpFactory->createExchange($this->channel());
|
||||
$this->amqpDelayExchange->setName($this->connectionOptions['delay']['exchange_name']);
|
||||
$this->amqpDelayExchange->setType(\AMQP_EX_TYPE_DIRECT);
|
||||
$this->amqpDelayExchange->setFlags(\AMQP_DURABLE);
|
||||
}
|
||||
|
||||
return $this->amqpDelayExchange;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a delay queue that will delay for a certain amount of time.
|
||||
*
|
||||
* This works by setting message TTL for the delay and pointing
|
||||
* the dead letter exchange to the original exchange. The result
|
||||
* is that after the TTL, the message is sent to the dead-letter-exchange,
|
||||
* which is the original exchange, resulting on it being put back into
|
||||
* the original queue.
|
||||
*/
|
||||
private function createDelayQueue(int $delay, ?string $routingKey, bool $isRetryAttempt): \AMQPQueue
|
||||
{
|
||||
$queue = $this->amqpFactory->createQueue($this->channel());
|
||||
$queue->setName($this->getRoutingKeyForDelay($delay, $routingKey, $isRetryAttempt));
|
||||
$queue->setFlags(\AMQP_DURABLE);
|
||||
$queue->setArguments([
|
||||
'x-message-ttl' => $delay,
|
||||
// delete the delay queue 10 seconds after the message expires
|
||||
// publishing another message redeclares the queue which renews the lease
|
||||
'x-expires' => $delay + 10000,
|
||||
// message should be broadcast to all consumers during delay, but to only one queue during retry
|
||||
// empty name is default direct exchange
|
||||
'x-dead-letter-exchange' => $isRetryAttempt ? '' : $this->exchangeOptions['name'],
|
||||
// after being released from to DLX, make sure the original routing key will be used
|
||||
// we must use an empty string instead of null for the argument to be picked up
|
||||
'x-dead-letter-routing-key' => $routingKey ?? '',
|
||||
]);
|
||||
|
||||
return $queue;
|
||||
}
|
||||
|
||||
private function getRoutingKeyForDelay(int $delay, ?string $finalRoutingKey, bool $isRetryAttempt): string
|
||||
{
|
||||
$action = $isRetryAttempt ? '_retry' : '_delay';
|
||||
|
||||
return str_replace(
|
||||
['%delay%', '%exchange_name%', '%routing_key%'],
|
||||
[$delay, $this->exchangeOptions['name'], $finalRoutingKey ?? ''],
|
||||
$this->connectionOptions['delay']['queue_name_pattern']
|
||||
).$action;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a message from the specified queue.
|
||||
*
|
||||
* @throws \AMQPException
|
||||
*/
|
||||
public function get(string $queueName): ?\AMQPEnvelope
|
||||
{
|
||||
$this->clearWhenDisconnected();
|
||||
|
||||
if ($this->autoSetupExchange) {
|
||||
$this->setupExchangeAndQueues();
|
||||
}
|
||||
|
||||
if (false !== $message = $this->queue($queueName)->get()) {
|
||||
return $message;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function ack(\AMQPEnvelope $message, string $queueName): bool
|
||||
{
|
||||
return $this->queue($queueName)->ack($message->getDeliveryTag()) ?? true;
|
||||
}
|
||||
|
||||
public function nack(\AMQPEnvelope $message, string $queueName, int $flags = \AMQP_NOPARAM): bool
|
||||
{
|
||||
return $this->queue($queueName)->nack($message->getDeliveryTag(), $flags) ?? true;
|
||||
}
|
||||
|
||||
public function setup(): void
|
||||
{
|
||||
$this->setupExchangeAndQueues();
|
||||
$this->setupDelayExchange();
|
||||
}
|
||||
|
||||
private function setupExchangeAndQueues(): void
|
||||
{
|
||||
$this->exchange()->declareExchange();
|
||||
|
||||
foreach ($this->queuesOptions as $queueName => $queueConfig) {
|
||||
$this->queue($queueName)->declareQueue();
|
||||
foreach ($queueConfig['binding_keys'] ?? [null] as $bindingKey) {
|
||||
$this->queue($queueName)->bind($this->exchangeOptions['name'], $bindingKey, $queueConfig['binding_arguments'] ?? []);
|
||||
}
|
||||
}
|
||||
$this->autoSetupExchange = false;
|
||||
}
|
||||
|
||||
private function setupDelayExchange(): void
|
||||
{
|
||||
$this->getDelayExchange()->declareExchange();
|
||||
$this->autoSetupDelayExchange = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getQueueNames(): array
|
||||
{
|
||||
return array_keys($this->queuesOptions);
|
||||
}
|
||||
|
||||
public function channel(): \AMQPChannel
|
||||
{
|
||||
if (null === $this->amqpChannel) {
|
||||
$connection = $this->amqpFactory->createConnection($this->connectionOptions);
|
||||
$connectMethod = 'true' === ($this->connectionOptions['persistent'] ?? 'false') ? 'pconnect' : 'connect';
|
||||
|
||||
try {
|
||||
$connection->{$connectMethod}();
|
||||
} catch (\AMQPConnectionException $e) {
|
||||
throw new \AMQPException('Could not connect to the AMQP server. Please verify the provided DSN.', 0, $e);
|
||||
}
|
||||
$this->amqpChannel = $this->amqpFactory->createChannel($connection);
|
||||
|
||||
if ('' !== ($this->connectionOptions['confirm_timeout'] ?? '')) {
|
||||
$this->amqpChannel->confirmSelect();
|
||||
$this->amqpChannel->setConfirmCallback(
|
||||
static function (): bool {
|
||||
return false;
|
||||
},
|
||||
static function () {
|
||||
throw new TransportException('Message publication failed due to a negative acknowledgment (nack) from the broker.');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
$this->lastActivityTime = time();
|
||||
} elseif (0 < ($this->connectionOptions['heartbeat'] ?? 0) && time() > $this->lastActivityTime + 2 * $this->connectionOptions['heartbeat']) {
|
||||
$disconnectMethod = 'true' === ($this->connectionOptions['persistent'] ?? 'false') ? 'pdisconnect' : 'disconnect';
|
||||
$this->amqpChannel->getConnection()->{$disconnectMethod}();
|
||||
}
|
||||
|
||||
return $this->amqpChannel;
|
||||
}
|
||||
|
||||
public function queue(string $queueName): \AMQPQueue
|
||||
{
|
||||
if (!isset($this->amqpQueues[$queueName])) {
|
||||
$queueConfig = $this->queuesOptions[$queueName] ?? [];
|
||||
|
||||
$amqpQueue = $this->amqpFactory->createQueue($this->channel());
|
||||
$amqpQueue->setName($queueName);
|
||||
$amqpQueue->setFlags($queueConfig['flags'] ?? \AMQP_DURABLE);
|
||||
|
||||
if (isset($queueConfig['arguments'])) {
|
||||
$amqpQueue->setArguments($queueConfig['arguments']);
|
||||
}
|
||||
|
||||
$this->amqpQueues[$queueName] = $amqpQueue;
|
||||
}
|
||||
|
||||
return $this->amqpQueues[$queueName];
|
||||
}
|
||||
|
||||
public function exchange(): \AMQPExchange
|
||||
{
|
||||
if (null === $this->amqpExchange) {
|
||||
$this->amqpExchange = $this->amqpFactory->createExchange($this->channel());
|
||||
$this->amqpExchange->setName($this->exchangeOptions['name']);
|
||||
$this->amqpExchange->setType($this->exchangeOptions['type'] ?? \AMQP_EX_TYPE_FANOUT);
|
||||
$this->amqpExchange->setFlags($this->exchangeOptions['flags'] ?? \AMQP_DURABLE);
|
||||
|
||||
if (isset($this->exchangeOptions['arguments'])) {
|
||||
$this->amqpExchange->setArguments($this->exchangeOptions['arguments']);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->amqpExchange;
|
||||
}
|
||||
|
||||
private function clearWhenDisconnected(): void
|
||||
{
|
||||
if (!$this->channel()->isConnected()) {
|
||||
$this->clear();
|
||||
}
|
||||
}
|
||||
|
||||
private function clear(): void
|
||||
{
|
||||
$this->amqpChannel = null;
|
||||
$this->amqpQueues = [];
|
||||
$this->amqpExchange = null;
|
||||
$this->amqpDelayExchange = null;
|
||||
}
|
||||
|
||||
private function getDefaultPublishRoutingKey(): ?string
|
||||
{
|
||||
return $this->exchangeOptions['default_publish_routing_key'] ?? null;
|
||||
}
|
||||
|
||||
public function purgeQueues()
|
||||
{
|
||||
foreach ($this->getQueueNames() as $queueName) {
|
||||
$this->queue($queueName)->purge();
|
||||
}
|
||||
}
|
||||
|
||||
private function getRoutingKeyForMessage(?AmqpStamp $amqpStamp): ?string
|
||||
{
|
||||
return (null !== $amqpStamp ? $amqpStamp->getRoutingKey() : null) ?? $this->getDefaultPublishRoutingKey();
|
||||
}
|
||||
|
||||
private function withConnectionExceptionRetry(callable $callable): void
|
||||
{
|
||||
$maxRetries = 3;
|
||||
$retries = 0;
|
||||
|
||||
retry:
|
||||
try {
|
||||
$callable();
|
||||
} catch (\AMQPConnectionException $e) {
|
||||
if (++$retries <= $maxRetries) {
|
||||
$this->clear();
|
||||
|
||||
goto retry;
|
||||
}
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!class_exists(\Symfony\Component\Messenger\Transport\AmqpExt\Connection::class, false)) {
|
||||
class_alias(Connection::class, \Symfony\Component\Messenger\Transport\AmqpExt\Connection::class);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "symfony/amqp-messenger",
|
||||
"type": "symfony-messenger-bridge",
|
||||
"description": "Symfony AMQP extension Messenger Bridge",
|
||||
"keywords": [],
|
||||
"homepage": "https://symfony.com",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=7.2.5",
|
||||
"symfony/deprecation-contracts": "^2.1|^3",
|
||||
"symfony/messenger": "^5.3|^6.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/event-dispatcher": "^4.4|^5.0|^6.0",
|
||||
"symfony/process": "^4.4|^5.0|^6.0",
|
||||
"symfony/property-access": "^4.4|^5.0|^6.0",
|
||||
"symfony/serializer": "^4.4|^5.0|^6.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": { "Symfony\\Component\\Messenger\\Bridge\\Amqp\\": "" },
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"minimum-stability": "dev"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
CHANGELOG
|
||||
=========
|
||||
|
||||
The changelog is maintained for all Symfony contracts at the following URL:
|
||||
https://github.com/symfony/contracts/blob/main/CHANGELOG.md
|
||||
@@ -0,0 +1,19 @@
|
||||
Copyright (c) 2020-present Fabien Potencier
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is furnished
|
||||
to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
@@ -0,0 +1,26 @@
|
||||
Symfony Deprecation Contracts
|
||||
=============================
|
||||
|
||||
A generic function and convention to trigger deprecation notices.
|
||||
|
||||
This package provides a single global function named `trigger_deprecation()` that triggers silenced deprecation notices.
|
||||
|
||||
By using a custom PHP error handler such as the one provided by the Symfony ErrorHandler component,
|
||||
the triggered deprecations can be caught and logged for later discovery, both on dev and prod environments.
|
||||
|
||||
The function requires at least 3 arguments:
|
||||
- the name of the Composer package that is triggering the deprecation
|
||||
- the version of the package that introduced the deprecation
|
||||
- the message of the deprecation
|
||||
- more arguments can be provided: they will be inserted in the message using `printf()` formatting
|
||||
|
||||
Example:
|
||||
```php
|
||||
trigger_deprecation('symfony/blockchain', '8.9', 'Using "%s" is deprecated, use "%s" instead.', 'bitcoin', 'fabcoin');
|
||||
```
|
||||
|
||||
This will generate the following message:
|
||||
`Since symfony/blockchain 8.9: Using "bitcoin" is deprecated, use "fabcoin" instead.`
|
||||
|
||||
While not necessarily recommended, the deprecation notices can be completely ignored by declaring an empty
|
||||
`function trigger_deprecation() {}` in your application.
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "symfony/deprecation-contracts",
|
||||
"type": "library",
|
||||
"description": "A generic function and convention to trigger deprecation notices",
|
||||
"homepage": "https://symfony.com",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nicolas Grekas",
|
||||
"email": "p@tchwork.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=7.1"
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"function.php"
|
||||
]
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "2.5-dev"
|
||||
},
|
||||
"thanks": {
|
||||
"name": "symfony/contracts",
|
||||
"url": "https://github.com/symfony/contracts"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
if (!function_exists('trigger_deprecation')) {
|
||||
/**
|
||||
* Triggers a silenced deprecation notice.
|
||||
*
|
||||
* @param string $package The name of the Composer package that is triggering the deprecation
|
||||
* @param string $version The version of the package that introduced the deprecation
|
||||
* @param string $message The message of the deprecation
|
||||
* @param mixed ...$args Values to insert in the message using printf() formatting
|
||||
*
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*/
|
||||
function trigger_deprecation(string $package, string $version, string $message, ...$args): void
|
||||
{
|
||||
@trigger_error(($package || $version ? "Since $package $version: " : '').($args ? vsprintf($message, $args) : $message), \E_USER_DEPRECATED);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
CHANGELOG
|
||||
=========
|
||||
|
||||
5.1.0
|
||||
-----
|
||||
|
||||
* Introduced the Doctrine bridge.
|
||||
* Added support for PostgreSQL `LISTEN`/`NOTIFY`.
|
||||
@@ -0,0 +1,19 @@
|
||||
Copyright (c) 2018-present Fabien Potencier
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is furnished
|
||||
to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
@@ -0,0 +1,12 @@
|
||||
Doctrine Messenger
|
||||
==================
|
||||
|
||||
Provides Doctrine integration for Symfony Messenger.
|
||||
|
||||
Resources
|
||||
---------
|
||||
|
||||
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
|
||||
* [Report issues](https://github.com/symfony/symfony/issues) and
|
||||
[send Pull Requests](https://github.com/symfony/symfony/pulls)
|
||||
in the [main Symfony repository](https://github.com/symfony/symfony)
|
||||
@@ -0,0 +1,574 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Messenger\Bridge\Doctrine\Transport;
|
||||
|
||||
use Doctrine\DBAL\Connection as DBALConnection;
|
||||
use Doctrine\DBAL\Driver\Exception as DriverException;
|
||||
use Doctrine\DBAL\Driver\Result as DriverResult;
|
||||
use Doctrine\DBAL\Exception as DBALException;
|
||||
use Doctrine\DBAL\Exception\TableNotFoundException;
|
||||
use Doctrine\DBAL\LockMode;
|
||||
use Doctrine\DBAL\Platforms\MySQLPlatform;
|
||||
use Doctrine\DBAL\Platforms\OraclePlatform;
|
||||
use Doctrine\DBAL\Query\QueryBuilder;
|
||||
use Doctrine\DBAL\Result;
|
||||
use Doctrine\DBAL\Schema\AbstractAsset;
|
||||
use Doctrine\DBAL\Schema\AbstractSchemaManager;
|
||||
use Doctrine\DBAL\Schema\Comparator;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\DBAL\Schema\SchemaDiff;
|
||||
use Doctrine\DBAL\Schema\Synchronizer\SchemaSynchronizer;
|
||||
use Doctrine\DBAL\Schema\Table;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Symfony\Component\Messenger\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Messenger\Exception\TransportException;
|
||||
use Symfony\Contracts\Service\ResetInterface;
|
||||
|
||||
/**
|
||||
* @internal since Symfony 5.1
|
||||
*
|
||||
* @author Vincent Touzet <vincent.touzet@gmail.com>
|
||||
* @author Kévin Dunglas <dunglas@gmail.com>
|
||||
*/
|
||||
class Connection implements ResetInterface
|
||||
{
|
||||
protected const TABLE_OPTION_NAME = '_symfony_messenger_table_name';
|
||||
|
||||
protected const DEFAULT_OPTIONS = [
|
||||
'table_name' => 'messenger_messages',
|
||||
'queue_name' => 'default',
|
||||
'redeliver_timeout' => 3600,
|
||||
'auto_setup' => true,
|
||||
];
|
||||
|
||||
/**
|
||||
* Configuration of the connection.
|
||||
*
|
||||
* Available options:
|
||||
*
|
||||
* * table_name: name of the table
|
||||
* * connection: name of the Doctrine's entity manager
|
||||
* * queue_name: name of the queue
|
||||
* * redeliver_timeout: Timeout before redeliver messages still in handling state (i.e: delivered_at is not null and message is still in table). Default: 3600
|
||||
* * auto_setup: Whether the table should be created automatically during send / get. Default: true
|
||||
*/
|
||||
protected $configuration = [];
|
||||
protected $driverConnection;
|
||||
protected $queueEmptiedAt;
|
||||
private $schemaSynchronizer;
|
||||
private $autoSetup;
|
||||
|
||||
public function __construct(array $configuration, DBALConnection $driverConnection, ?SchemaSynchronizer $schemaSynchronizer = null)
|
||||
{
|
||||
$this->configuration = array_replace_recursive(static::DEFAULT_OPTIONS, $configuration);
|
||||
$this->driverConnection = $driverConnection;
|
||||
$this->schemaSynchronizer = $schemaSynchronizer;
|
||||
$this->autoSetup = $this->configuration['auto_setup'];
|
||||
}
|
||||
|
||||
public function reset()
|
||||
{
|
||||
$this->queueEmptiedAt = null;
|
||||
}
|
||||
|
||||
public function getConfiguration(): array
|
||||
{
|
||||
return $this->configuration;
|
||||
}
|
||||
|
||||
public static function buildConfiguration(string $dsn, array $options = []): array
|
||||
{
|
||||
if (false === $params = parse_url($dsn)) {
|
||||
throw new InvalidArgumentException('The given Doctrine Messenger DSN is invalid.');
|
||||
}
|
||||
|
||||
$query = [];
|
||||
if (isset($params['query'])) {
|
||||
parse_str($params['query'], $query);
|
||||
}
|
||||
|
||||
$configuration = ['connection' => $params['host']];
|
||||
$configuration += $query + $options + static::DEFAULT_OPTIONS;
|
||||
|
||||
$configuration['auto_setup'] = filter_var($configuration['auto_setup'], \FILTER_VALIDATE_BOOLEAN);
|
||||
|
||||
// check for extra keys in options
|
||||
$optionsExtraKeys = array_diff(array_keys($options), array_keys(static::DEFAULT_OPTIONS));
|
||||
if (0 < \count($optionsExtraKeys)) {
|
||||
throw new InvalidArgumentException(sprintf('Unknown option found: [%s]. Allowed options are [%s].', implode(', ', $optionsExtraKeys), implode(', ', array_keys(static::DEFAULT_OPTIONS))));
|
||||
}
|
||||
|
||||
// check for extra keys in options
|
||||
$queryExtraKeys = array_diff(array_keys($query), array_keys(static::DEFAULT_OPTIONS));
|
||||
if (0 < \count($queryExtraKeys)) {
|
||||
throw new InvalidArgumentException(sprintf('Unknown option found in DSN: [%s]. Allowed options are [%s].', implode(', ', $queryExtraKeys), implode(', ', array_keys(static::DEFAULT_OPTIONS))));
|
||||
}
|
||||
|
||||
return $configuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $delay The delay in milliseconds
|
||||
*
|
||||
* @return string The inserted id
|
||||
*
|
||||
* @throws DBALException
|
||||
*/
|
||||
public function send(string $body, array $headers, int $delay = 0): string
|
||||
{
|
||||
$now = new \DateTime();
|
||||
$availableAt = (clone $now)->modify(sprintf('%+d seconds', $delay / 1000));
|
||||
|
||||
$queryBuilder = $this->driverConnection->createQueryBuilder()
|
||||
->insert($this->configuration['table_name'])
|
||||
->values([
|
||||
'body' => '?',
|
||||
'headers' => '?',
|
||||
'queue_name' => '?',
|
||||
'created_at' => '?',
|
||||
'available_at' => '?',
|
||||
]);
|
||||
|
||||
$this->executeStatement($queryBuilder->getSQL(), [
|
||||
$body,
|
||||
json_encode($headers),
|
||||
$this->configuration['queue_name'],
|
||||
$now,
|
||||
$availableAt,
|
||||
], [
|
||||
Types::STRING,
|
||||
Types::STRING,
|
||||
Types::STRING,
|
||||
Types::DATETIME_MUTABLE,
|
||||
Types::DATETIME_MUTABLE,
|
||||
]);
|
||||
|
||||
return $this->driverConnection->lastInsertId();
|
||||
}
|
||||
|
||||
public function get(): ?array
|
||||
{
|
||||
if ($this->driverConnection->getDatabasePlatform() instanceof MySQLPlatform) {
|
||||
try {
|
||||
$this->driverConnection->delete($this->configuration['table_name'], ['delivered_at' => '9999-12-31 23:59:59']);
|
||||
} catch (DriverException $e) {
|
||||
// Ignore the exception
|
||||
} catch (TableNotFoundException $e) {
|
||||
if ($this->autoSetup) {
|
||||
$this->setup();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get:
|
||||
$this->driverConnection->beginTransaction();
|
||||
try {
|
||||
$query = $this->createAvailableMessagesQueryBuilder()
|
||||
->orderBy('available_at', 'ASC')
|
||||
->setMaxResults(1);
|
||||
|
||||
if ($this->driverConnection->getDatabasePlatform() instanceof OraclePlatform) {
|
||||
$query->select('m.id');
|
||||
}
|
||||
|
||||
// Append pessimistic write lock to FROM clause if db platform supports it
|
||||
$sql = $query->getSQL();
|
||||
|
||||
// Wrap the rownum query in a sub-query to allow writelocks without ORA-02014 error
|
||||
if ($this->driverConnection->getDatabasePlatform() instanceof OraclePlatform) {
|
||||
$query = $this->createQueryBuilder('w')
|
||||
->where('w.id IN ('.str_replace('SELECT a.* FROM', 'SELECT a.id FROM', $sql).')')
|
||||
->setParameters($query->getParameters(), $query->getParameterTypes());
|
||||
|
||||
if (method_exists(QueryBuilder::class, 'forUpdate')) {
|
||||
$query->forUpdate();
|
||||
}
|
||||
|
||||
$sql = $query->getSQL();
|
||||
} elseif (method_exists(QueryBuilder::class, 'forUpdate')) {
|
||||
$query->forUpdate();
|
||||
try {
|
||||
$sql = $query->getSQL();
|
||||
} catch (DBALException $e) {
|
||||
}
|
||||
} elseif (preg_match('/FROM (.+) WHERE/', (string) $sql, $matches)) {
|
||||
$fromClause = $matches[1];
|
||||
$sql = str_replace(
|
||||
sprintf('FROM %s WHERE', $fromClause),
|
||||
sprintf('FROM %s WHERE', $this->driverConnection->getDatabasePlatform()->appendLockHint($fromClause, LockMode::PESSIMISTIC_WRITE)),
|
||||
$sql
|
||||
);
|
||||
}
|
||||
|
||||
// use SELECT ... FOR UPDATE to lock table
|
||||
if (!method_exists(QueryBuilder::class, 'forUpdate')) {
|
||||
$sql .= ' '.$this->driverConnection->getDatabasePlatform()->getWriteLockSQL();
|
||||
}
|
||||
|
||||
$stmt = $this->executeQuery(
|
||||
$sql,
|
||||
$query->getParameters(),
|
||||
$query->getParameterTypes()
|
||||
);
|
||||
$doctrineEnvelope = $stmt instanceof Result || $stmt instanceof DriverResult ? $stmt->fetchAssociative() : $stmt->fetch();
|
||||
|
||||
if (false === $doctrineEnvelope) {
|
||||
$this->driverConnection->commit();
|
||||
$this->queueEmptiedAt = microtime(true) * 1000;
|
||||
|
||||
return null;
|
||||
}
|
||||
// Postgres can "group" notifications having the same channel and payload
|
||||
// We need to be sure to empty the queue before blocking again
|
||||
$this->queueEmptiedAt = null;
|
||||
|
||||
$doctrineEnvelope = $this->decodeEnvelopeHeaders($doctrineEnvelope);
|
||||
|
||||
$queryBuilder = $this->driverConnection->createQueryBuilder()
|
||||
->update($this->configuration['table_name'])
|
||||
->set('delivered_at', '?')
|
||||
->where('id = ?');
|
||||
$now = new \DateTime();
|
||||
$this->executeStatement($queryBuilder->getSQL(), [
|
||||
$now,
|
||||
$doctrineEnvelope['id'],
|
||||
], [
|
||||
Types::DATETIME_MUTABLE,
|
||||
]);
|
||||
|
||||
$this->driverConnection->commit();
|
||||
|
||||
return $doctrineEnvelope;
|
||||
} catch (\Throwable $e) {
|
||||
$this->driverConnection->rollBack();
|
||||
|
||||
if ($this->autoSetup && $e instanceof TableNotFoundException) {
|
||||
$this->setup();
|
||||
goto get;
|
||||
}
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
public function ack(string $id): bool
|
||||
{
|
||||
try {
|
||||
if ($this->driverConnection->getDatabasePlatform() instanceof MySQLPlatform) {
|
||||
return $this->driverConnection->update($this->configuration['table_name'], ['delivered_at' => '9999-12-31 23:59:59'], ['id' => $id]) > 0;
|
||||
}
|
||||
|
||||
return $this->driverConnection->delete($this->configuration['table_name'], ['id' => $id]) > 0;
|
||||
} catch (DBALException $exception) {
|
||||
throw new TransportException($exception->getMessage(), 0, $exception);
|
||||
}
|
||||
}
|
||||
|
||||
public function reject(string $id): bool
|
||||
{
|
||||
try {
|
||||
if ($this->driverConnection->getDatabasePlatform() instanceof MySQLPlatform) {
|
||||
return $this->driverConnection->update($this->configuration['table_name'], ['delivered_at' => '9999-12-31 23:59:59'], ['id' => $id]) > 0;
|
||||
}
|
||||
|
||||
return $this->driverConnection->delete($this->configuration['table_name'], ['id' => $id]) > 0;
|
||||
} catch (DBALException $exception) {
|
||||
throw new TransportException($exception->getMessage(), 0, $exception);
|
||||
}
|
||||
}
|
||||
|
||||
public function setup(): void
|
||||
{
|
||||
$configuration = $this->driverConnection->getConfiguration();
|
||||
$assetFilter = $configuration->getSchemaAssetsFilter();
|
||||
$configuration->setSchemaAssetsFilter(function ($tableName) {
|
||||
if ($tableName instanceof AbstractAsset) {
|
||||
$tableName = $tableName->getName();
|
||||
}
|
||||
|
||||
if (!\is_string($tableName)) {
|
||||
throw new \TypeError(sprintf('The table name must be an instance of "%s" or a string ("%s" given).', AbstractAsset::class, get_debug_type($tableName)));
|
||||
}
|
||||
|
||||
return $tableName === $this->configuration['table_name'];
|
||||
});
|
||||
$this->updateSchema();
|
||||
$configuration->setSchemaAssetsFilter($assetFilter);
|
||||
$this->autoSetup = false;
|
||||
}
|
||||
|
||||
public function getMessageCount(): int
|
||||
{
|
||||
$queryBuilder = $this->createAvailableMessagesQueryBuilder()
|
||||
->select('COUNT(m.id) AS message_count')
|
||||
->setMaxResults(1);
|
||||
|
||||
$stmt = $this->executeQuery($queryBuilder->getSQL(), $queryBuilder->getParameters(), $queryBuilder->getParameterTypes());
|
||||
|
||||
return $stmt instanceof Result || $stmt instanceof DriverResult ? $stmt->fetchOne() : $stmt->fetchColumn();
|
||||
}
|
||||
|
||||
public function findAll(?int $limit = null): array
|
||||
{
|
||||
$queryBuilder = $this->createAvailableMessagesQueryBuilder();
|
||||
|
||||
if (null !== $limit) {
|
||||
$queryBuilder->setMaxResults($limit);
|
||||
}
|
||||
|
||||
$stmt = $this->executeQuery($queryBuilder->getSQL(), $queryBuilder->getParameters(), $queryBuilder->getParameterTypes());
|
||||
$data = $stmt instanceof Result || $stmt instanceof DriverResult ? $stmt->fetchAllAssociative() : $stmt->fetchAll();
|
||||
|
||||
return array_map(function ($doctrineEnvelope) {
|
||||
return $this->decodeEnvelopeHeaders($doctrineEnvelope);
|
||||
}, $data);
|
||||
}
|
||||
|
||||
public function find($id): ?array
|
||||
{
|
||||
$queryBuilder = $this->createQueryBuilder()
|
||||
->where('m.id = ? and m.queue_name = ?');
|
||||
|
||||
$stmt = $this->executeQuery($queryBuilder->getSQL(), [$id, $this->configuration['queue_name']]);
|
||||
$data = $stmt instanceof Result || $stmt instanceof DriverResult ? $stmt->fetchAssociative() : $stmt->fetch();
|
||||
|
||||
return false === $data ? null : $this->decodeEnvelopeHeaders($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function configureSchema(Schema $schema, DBALConnection $forConnection): void
|
||||
{
|
||||
// only update the schema for this connection
|
||||
if ($forConnection !== $this->driverConnection) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($schema->hasTable($this->configuration['table_name'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->addTableToSchema($schema);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function getExtraSetupSqlForTable(Table $createdTable): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
private function createAvailableMessagesQueryBuilder(): QueryBuilder
|
||||
{
|
||||
$now = new \DateTime();
|
||||
$redeliverLimit = (clone $now)->modify(sprintf('-%d seconds', $this->configuration['redeliver_timeout']));
|
||||
|
||||
return $this->createQueryBuilder()
|
||||
->where('m.queue_name = ?')
|
||||
->andWhere('m.delivered_at is null OR m.delivered_at < ?')
|
||||
->andWhere('m.available_at <= ?')
|
||||
->setParameters([
|
||||
$this->configuration['queue_name'],
|
||||
$redeliverLimit,
|
||||
$now,
|
||||
], [
|
||||
Types::STRING,
|
||||
Types::DATETIME_MUTABLE,
|
||||
Types::DATETIME_MUTABLE,
|
||||
]);
|
||||
}
|
||||
|
||||
private function createQueryBuilder(string $alias = 'm'): QueryBuilder
|
||||
{
|
||||
$queryBuilder = $this->driverConnection->createQueryBuilder()
|
||||
->from($this->configuration['table_name'], $alias);
|
||||
|
||||
$alias .= '.';
|
||||
|
||||
if (!$this->driverConnection->getDatabasePlatform() instanceof OraclePlatform) {
|
||||
return $queryBuilder->select($alias.'*');
|
||||
}
|
||||
|
||||
// Oracle databases use UPPER CASE on tables and column identifiers.
|
||||
// Column alias is added to force the result to be lowercase even when the actual field is all caps.
|
||||
|
||||
return $queryBuilder->select(str_replace(', ', ', '.$alias,
|
||||
$alias.'id AS "id", body AS "body", headers AS "headers", queue_name AS "queue_name", '.
|
||||
'created_at AS "created_at", available_at AS "available_at", '.
|
||||
'delivered_at AS "delivered_at"'
|
||||
));
|
||||
}
|
||||
|
||||
private function executeQuery(string $sql, array $parameters = [], array $types = [])
|
||||
{
|
||||
try {
|
||||
$stmt = $this->driverConnection->executeQuery($sql, $parameters, $types);
|
||||
} catch (TableNotFoundException $e) {
|
||||
if ($this->driverConnection->isTransactionActive()) {
|
||||
throw $e;
|
||||
}
|
||||
|
||||
// create table
|
||||
if ($this->autoSetup) {
|
||||
$this->setup();
|
||||
}
|
||||
$stmt = $this->driverConnection->executeQuery($sql, $parameters, $types);
|
||||
}
|
||||
|
||||
return $stmt;
|
||||
}
|
||||
|
||||
protected function executeStatement(string $sql, array $parameters = [], array $types = [])
|
||||
{
|
||||
try {
|
||||
if (method_exists($this->driverConnection, 'executeStatement')) {
|
||||
$stmt = $this->driverConnection->executeStatement($sql, $parameters, $types);
|
||||
} else {
|
||||
$stmt = $this->driverConnection->executeUpdate($sql, $parameters, $types);
|
||||
}
|
||||
} catch (TableNotFoundException $e) {
|
||||
if ($this->driverConnection->isTransactionActive()) {
|
||||
throw $e;
|
||||
}
|
||||
|
||||
// create table
|
||||
if ($this->autoSetup) {
|
||||
$this->setup();
|
||||
}
|
||||
if (method_exists($this->driverConnection, 'executeStatement')) {
|
||||
$stmt = $this->driverConnection->executeStatement($sql, $parameters, $types);
|
||||
} else {
|
||||
$stmt = $this->driverConnection->executeUpdate($sql, $parameters, $types);
|
||||
}
|
||||
}
|
||||
|
||||
return $stmt;
|
||||
}
|
||||
|
||||
private function getSchema(): Schema
|
||||
{
|
||||
$schema = new Schema([], [], $this->createSchemaManager()->createSchemaConfig());
|
||||
$this->addTableToSchema($schema);
|
||||
|
||||
return $schema;
|
||||
}
|
||||
|
||||
private function addTableToSchema(Schema $schema): void
|
||||
{
|
||||
$table = $schema->createTable($this->configuration['table_name']);
|
||||
// add an internal option to mark that we created this & the non-namespaced table name
|
||||
$table->addOption(self::TABLE_OPTION_NAME, $this->configuration['table_name']);
|
||||
$table->addColumn('id', Types::BIGINT)
|
||||
->setAutoincrement(true)
|
||||
->setNotnull(true);
|
||||
$table->addColumn('body', Types::TEXT)
|
||||
->setNotnull(true);
|
||||
$table->addColumn('headers', Types::TEXT)
|
||||
->setNotnull(true);
|
||||
$table->addColumn('queue_name', Types::STRING)
|
||||
->setLength(190) // MySQL 5.6 only supports 191 characters on an indexed column in utf8mb4 mode
|
||||
->setNotnull(true);
|
||||
$table->addColumn('created_at', Types::DATETIME_MUTABLE)
|
||||
->setNotnull(true);
|
||||
$table->addColumn('available_at', Types::DATETIME_MUTABLE)
|
||||
->setNotnull(true);
|
||||
$table->addColumn('delivered_at', Types::DATETIME_MUTABLE)
|
||||
->setNotnull(false);
|
||||
$table->setPrimaryKey(['id']);
|
||||
$table->addIndex(['queue_name']);
|
||||
$table->addIndex(['available_at']);
|
||||
$table->addIndex(['delivered_at']);
|
||||
}
|
||||
|
||||
private function decodeEnvelopeHeaders(array $doctrineEnvelope): array
|
||||
{
|
||||
$doctrineEnvelope['headers'] = json_decode($doctrineEnvelope['headers'], true);
|
||||
|
||||
return $doctrineEnvelope;
|
||||
}
|
||||
|
||||
private function updateSchema(): void
|
||||
{
|
||||
if (null !== $this->schemaSynchronizer) {
|
||||
$this->schemaSynchronizer->updateSchema($this->getSchema(), true);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$schemaManager = $this->createSchemaManager();
|
||||
$comparator = $this->createComparator($schemaManager);
|
||||
$schemaDiff = $this->compareSchemas($comparator, method_exists($schemaManager, 'introspectSchema') ? $schemaManager->introspectSchema() : $schemaManager->createSchema(), $this->getSchema());
|
||||
$platform = $this->driverConnection->getDatabasePlatform();
|
||||
$exec = method_exists($this->driverConnection, 'executeStatement') ? 'executeStatement' : 'exec';
|
||||
|
||||
if (!method_exists(SchemaDiff::class, 'getCreatedSchemas')) {
|
||||
foreach ($schemaDiff->toSaveSql($platform) as $sql) {
|
||||
$this->driverConnection->$exec($sql);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($platform->supportsSchemas()) {
|
||||
foreach ($schemaDiff->getCreatedSchemas() as $schema) {
|
||||
$this->driverConnection->$exec($platform->getCreateSchemaSQL($schema));
|
||||
}
|
||||
}
|
||||
|
||||
if ($platform->supportsSequences()) {
|
||||
foreach ($schemaDiff->getAlteredSequences() as $sequence) {
|
||||
$this->driverConnection->$exec($platform->getAlterSequenceSQL($sequence));
|
||||
}
|
||||
|
||||
foreach ($schemaDiff->getCreatedSequences() as $sequence) {
|
||||
$this->driverConnection->$exec($platform->getCreateSequenceSQL($sequence));
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($platform->getCreateTablesSQL($schemaDiff->getCreatedTables()) as $sql) {
|
||||
$this->driverConnection->$exec($sql);
|
||||
}
|
||||
|
||||
foreach ($schemaDiff->getAlteredTables() as $tableDiff) {
|
||||
foreach ($platform->getAlterTableSQL($tableDiff) as $sql) {
|
||||
$this->driverConnection->$exec($sql);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function createSchemaManager(): AbstractSchemaManager
|
||||
{
|
||||
return method_exists($this->driverConnection, 'createSchemaManager')
|
||||
? $this->driverConnection->createSchemaManager()
|
||||
: $this->driverConnection->getSchemaManager();
|
||||
}
|
||||
|
||||
private function createComparator(AbstractSchemaManager $schemaManager): Comparator
|
||||
{
|
||||
return method_exists($schemaManager, 'createComparator')
|
||||
? $schemaManager->createComparator()
|
||||
: new Comparator();
|
||||
}
|
||||
|
||||
private function compareSchemas(Comparator $comparator, Schema $from, Schema $to): SchemaDiff
|
||||
{
|
||||
return method_exists($comparator, 'compareSchemas') || method_exists($comparator, 'doCompareSchemas')
|
||||
? $comparator->compareSchemas($from, $to)
|
||||
: $comparator->compare($from, $to);
|
||||
}
|
||||
}
|
||||
|
||||
if (!class_exists(\Symfony\Component\Messenger\Transport\Doctrine\Connection::class, false)) {
|
||||
class_alias(Connection::class, \Symfony\Component\Messenger\Transport\Doctrine\Connection::class);
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Messenger\Bridge\Doctrine\Transport;
|
||||
|
||||
use Symfony\Component\Messenger\Stamp\NonSendableStampInterface;
|
||||
|
||||
/**
|
||||
* @author Vincent Touzet <vincent.touzet@gmail.com>
|
||||
*/
|
||||
class DoctrineReceivedStamp implements NonSendableStampInterface
|
||||
{
|
||||
private $id;
|
||||
|
||||
public function __construct(string $id)
|
||||
{
|
||||
$this->id = $id;
|
||||
}
|
||||
|
||||
public function getId(): string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
}
|
||||
|
||||
if (!class_exists(\Symfony\Component\Messenger\Transport\Doctrine\DoctrineReceivedStamp::class, false)) {
|
||||
class_alias(DoctrineReceivedStamp::class, \Symfony\Component\Messenger\Transport\Doctrine\DoctrineReceivedStamp::class);
|
||||
}
|
||||
+175
@@ -0,0 +1,175 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Messenger\Bridge\Doctrine\Transport;
|
||||
|
||||
use Doctrine\DBAL\Exception as DBALException;
|
||||
use Doctrine\DBAL\Exception\RetryableException;
|
||||
use Symfony\Component\Messenger\Envelope;
|
||||
use Symfony\Component\Messenger\Exception\LogicException;
|
||||
use Symfony\Component\Messenger\Exception\MessageDecodingFailedException;
|
||||
use Symfony\Component\Messenger\Exception\TransportException;
|
||||
use Symfony\Component\Messenger\Stamp\TransportMessageIdStamp;
|
||||
use Symfony\Component\Messenger\Transport\Receiver\ListableReceiverInterface;
|
||||
use Symfony\Component\Messenger\Transport\Receiver\MessageCountAwareInterface;
|
||||
use Symfony\Component\Messenger\Transport\Serialization\PhpSerializer;
|
||||
use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface;
|
||||
|
||||
/**
|
||||
* @author Vincent Touzet <vincent.touzet@gmail.com>
|
||||
*/
|
||||
class DoctrineReceiver implements ListableReceiverInterface, MessageCountAwareInterface
|
||||
{
|
||||
private const MAX_RETRIES = 3;
|
||||
private $retryingSafetyCounter = 0;
|
||||
private $connection;
|
||||
private $serializer;
|
||||
|
||||
public function __construct(Connection $connection, ?SerializerInterface $serializer = null)
|
||||
{
|
||||
$this->connection = $connection;
|
||||
$this->serializer = $serializer ?? new PhpSerializer();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function get(): iterable
|
||||
{
|
||||
try {
|
||||
$doctrineEnvelope = $this->connection->get();
|
||||
$this->retryingSafetyCounter = 0; // reset counter
|
||||
} catch (RetryableException $exception) {
|
||||
// Do nothing when RetryableException occurs less than "MAX_RETRIES"
|
||||
// as it will likely be resolved on the next call to get()
|
||||
// Problem with concurrent consumers and database deadlocks
|
||||
if (++$this->retryingSafetyCounter >= self::MAX_RETRIES) {
|
||||
$this->retryingSafetyCounter = 0; // reset counter
|
||||
throw new TransportException($exception->getMessage(), 0, $exception);
|
||||
}
|
||||
|
||||
return [];
|
||||
} catch (DBALException $exception) {
|
||||
throw new TransportException($exception->getMessage(), 0, $exception);
|
||||
}
|
||||
|
||||
if (null === $doctrineEnvelope) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [$this->createEnvelopeFromData($doctrineEnvelope)];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function ack(Envelope $envelope): void
|
||||
{
|
||||
try {
|
||||
$this->connection->ack($this->findDoctrineReceivedStamp($envelope)->getId());
|
||||
} catch (DBALException $exception) {
|
||||
throw new TransportException($exception->getMessage(), 0, $exception);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function reject(Envelope $envelope): void
|
||||
{
|
||||
try {
|
||||
$this->connection->reject($this->findDoctrineReceivedStamp($envelope)->getId());
|
||||
} catch (DBALException $exception) {
|
||||
throw new TransportException($exception->getMessage(), 0, $exception);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getMessageCount(): int
|
||||
{
|
||||
try {
|
||||
return $this->connection->getMessageCount();
|
||||
} catch (DBALException $exception) {
|
||||
throw new TransportException($exception->getMessage(), 0, $exception);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function all(?int $limit = null): iterable
|
||||
{
|
||||
try {
|
||||
$doctrineEnvelopes = $this->connection->findAll($limit);
|
||||
} catch (DBALException $exception) {
|
||||
throw new TransportException($exception->getMessage(), 0, $exception);
|
||||
}
|
||||
|
||||
foreach ($doctrineEnvelopes as $doctrineEnvelope) {
|
||||
yield $this->createEnvelopeFromData($doctrineEnvelope);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function find($id): ?Envelope
|
||||
{
|
||||
try {
|
||||
$doctrineEnvelope = $this->connection->find($id);
|
||||
} catch (DBALException $exception) {
|
||||
throw new TransportException($exception->getMessage(), 0, $exception);
|
||||
}
|
||||
|
||||
if (null === $doctrineEnvelope) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->createEnvelopeFromData($doctrineEnvelope);
|
||||
}
|
||||
|
||||
private function findDoctrineReceivedStamp(Envelope $envelope): DoctrineReceivedStamp
|
||||
{
|
||||
/** @var DoctrineReceivedStamp|null $doctrineReceivedStamp */
|
||||
$doctrineReceivedStamp = $envelope->last(DoctrineReceivedStamp::class);
|
||||
|
||||
if (null === $doctrineReceivedStamp) {
|
||||
throw new LogicException('No DoctrineReceivedStamp found on the Envelope.');
|
||||
}
|
||||
|
||||
return $doctrineReceivedStamp;
|
||||
}
|
||||
|
||||
private function createEnvelopeFromData(array $data): Envelope
|
||||
{
|
||||
try {
|
||||
$envelope = $this->serializer->decode([
|
||||
'body' => $data['body'],
|
||||
'headers' => $data['headers'],
|
||||
]);
|
||||
} catch (MessageDecodingFailedException $exception) {
|
||||
$this->connection->reject($data['id']);
|
||||
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
return $envelope->with(
|
||||
new DoctrineReceivedStamp($data['id']),
|
||||
new TransportMessageIdStamp($data['id'])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!class_exists(\Symfony\Component\Messenger\Transport\Doctrine\DoctrineReceiver::class, false)) {
|
||||
class_alias(DoctrineReceiver::class, \Symfony\Component\Messenger\Transport\Doctrine\DoctrineReceiver::class);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Messenger\Bridge\Doctrine\Transport;
|
||||
|
||||
use Doctrine\DBAL\Exception as DBALException;
|
||||
use Symfony\Component\Messenger\Envelope;
|
||||
use Symfony\Component\Messenger\Exception\TransportException;
|
||||
use Symfony\Component\Messenger\Stamp\DelayStamp;
|
||||
use Symfony\Component\Messenger\Stamp\TransportMessageIdStamp;
|
||||
use Symfony\Component\Messenger\Transport\Sender\SenderInterface;
|
||||
use Symfony\Component\Messenger\Transport\Serialization\PhpSerializer;
|
||||
use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface;
|
||||
|
||||
/**
|
||||
* @author Vincent Touzet <vincent.touzet@gmail.com>
|
||||
*/
|
||||
class DoctrineSender implements SenderInterface
|
||||
{
|
||||
private $connection;
|
||||
private $serializer;
|
||||
|
||||
public function __construct(Connection $connection, ?SerializerInterface $serializer = null)
|
||||
{
|
||||
$this->connection = $connection;
|
||||
$this->serializer = $serializer ?? new PhpSerializer();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function send(Envelope $envelope): Envelope
|
||||
{
|
||||
$encodedMessage = $this->serializer->encode($envelope);
|
||||
|
||||
/** @var DelayStamp|null $delayStamp */
|
||||
$delayStamp = $envelope->last(DelayStamp::class);
|
||||
$delay = null !== $delayStamp ? $delayStamp->getDelay() : 0;
|
||||
|
||||
try {
|
||||
$id = $this->connection->send($encodedMessage['body'], $encodedMessage['headers'] ?? [], $delay);
|
||||
} catch (DBALException $exception) {
|
||||
throw new TransportException($exception->getMessage(), 0, $exception);
|
||||
}
|
||||
|
||||
return $envelope->with(new TransportMessageIdStamp($id));
|
||||
}
|
||||
}
|
||||
|
||||
if (!class_exists(\Symfony\Component\Messenger\Transport\Doctrine\DoctrineSender::class, false)) {
|
||||
class_alias(DoctrineSender::class, \Symfony\Component\Messenger\Transport\Doctrine\DoctrineSender::class);
|
||||
}
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Messenger\Bridge\Doctrine\Transport;
|
||||
|
||||
use Doctrine\DBAL\Connection as DbalConnection;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\DBAL\Schema\Table;
|
||||
use Symfony\Component\Messenger\Envelope;
|
||||
use Symfony\Component\Messenger\Transport\Receiver\ListableReceiverInterface;
|
||||
use Symfony\Component\Messenger\Transport\Receiver\MessageCountAwareInterface;
|
||||
use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface;
|
||||
use Symfony\Component\Messenger\Transport\SetupableTransportInterface;
|
||||
use Symfony\Component\Messenger\Transport\TransportInterface;
|
||||
|
||||
/**
|
||||
* @author Vincent Touzet <vincent.touzet@gmail.com>
|
||||
*/
|
||||
class DoctrineTransport implements TransportInterface, SetupableTransportInterface, MessageCountAwareInterface, ListableReceiverInterface
|
||||
{
|
||||
private $connection;
|
||||
private $serializer;
|
||||
private $receiver;
|
||||
private $sender;
|
||||
|
||||
public function __construct(Connection $connection, SerializerInterface $serializer)
|
||||
{
|
||||
$this->connection = $connection;
|
||||
$this->serializer = $serializer;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function get(): iterable
|
||||
{
|
||||
return ($this->receiver ?? $this->getReceiver())->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function ack(Envelope $envelope): void
|
||||
{
|
||||
($this->receiver ?? $this->getReceiver())->ack($envelope);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function reject(Envelope $envelope): void
|
||||
{
|
||||
($this->receiver ?? $this->getReceiver())->reject($envelope);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getMessageCount(): int
|
||||
{
|
||||
return ($this->receiver ?? $this->getReceiver())->getMessageCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function all(?int $limit = null): iterable
|
||||
{
|
||||
return ($this->receiver ?? $this->getReceiver())->all($limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function find($id): ?Envelope
|
||||
{
|
||||
return ($this->receiver ?? $this->getReceiver())->find($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function send(Envelope $envelope): Envelope
|
||||
{
|
||||
return ($this->sender ?? $this->getSender())->send($envelope);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setup(): void
|
||||
{
|
||||
$this->connection->setup();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the Table to the Schema if this transport uses this connection.
|
||||
*/
|
||||
public function configureSchema(Schema $schema, DbalConnection $forConnection): void
|
||||
{
|
||||
$this->connection->configureSchema($schema, $forConnection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds extra SQL if the given table was created by the Connection.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function getExtraSetupSqlForTable(Table $createdTable): array
|
||||
{
|
||||
return $this->connection->getExtraSetupSqlForTable($createdTable);
|
||||
}
|
||||
|
||||
private function getReceiver(): DoctrineReceiver
|
||||
{
|
||||
return $this->receiver = new DoctrineReceiver($this->connection, $this->serializer);
|
||||
}
|
||||
|
||||
private function getSender(): DoctrineSender
|
||||
{
|
||||
return $this->sender = new DoctrineSender($this->connection, $this->serializer);
|
||||
}
|
||||
}
|
||||
|
||||
if (!class_exists(\Symfony\Component\Messenger\Transport\Doctrine\DoctrineTransport::class, false)) {
|
||||
class_alias(DoctrineTransport::class, \Symfony\Component\Messenger\Transport\Doctrine\DoctrineTransport::class);
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Messenger\Bridge\Doctrine\Transport;
|
||||
|
||||
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
|
||||
use Doctrine\Persistence\ConnectionRegistry;
|
||||
use Symfony\Bridge\Doctrine\RegistryInterface;
|
||||
use Symfony\Component\Messenger\Exception\TransportException;
|
||||
use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface;
|
||||
use Symfony\Component\Messenger\Transport\TransportFactoryInterface;
|
||||
use Symfony\Component\Messenger\Transport\TransportInterface;
|
||||
|
||||
/**
|
||||
* @author Vincent Touzet <vincent.touzet@gmail.com>
|
||||
*/
|
||||
class DoctrineTransportFactory implements TransportFactoryInterface
|
||||
{
|
||||
private $registry;
|
||||
|
||||
public function __construct($registry)
|
||||
{
|
||||
if (!$registry instanceof RegistryInterface && !$registry instanceof ConnectionRegistry) {
|
||||
throw new \TypeError(sprintf('Expected an instance of "%s" or "%s", but got "%s".', RegistryInterface::class, ConnectionRegistry::class, get_debug_type($registry)));
|
||||
}
|
||||
|
||||
$this->registry = $registry;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $options You can set 'use_notify' to false to not use LISTEN/NOTIFY with postgresql
|
||||
*/
|
||||
public function createTransport(string $dsn, array $options, SerializerInterface $serializer): TransportInterface
|
||||
{
|
||||
$useNotify = ($options['use_notify'] ?? true);
|
||||
unset($options['transport_name'], $options['use_notify']);
|
||||
// Always allow PostgreSQL-specific keys, to be able to transparently fallback to the native driver when LISTEN/NOTIFY isn't available
|
||||
$configuration = PostgreSqlConnection::buildConfiguration($dsn, $options);
|
||||
|
||||
try {
|
||||
$driverConnection = $this->registry->getConnection($configuration['connection']);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
throw new TransportException('Could not find Doctrine connection from Messenger DSN.', 0, $e);
|
||||
}
|
||||
|
||||
if ($useNotify && $driverConnection->getDatabasePlatform() instanceof PostgreSQLPlatform) {
|
||||
$connection = new PostgreSqlConnection($configuration, $driverConnection);
|
||||
} else {
|
||||
$connection = new Connection($configuration, $driverConnection);
|
||||
}
|
||||
|
||||
return new DoctrineTransport($connection, $serializer);
|
||||
}
|
||||
|
||||
public function supports(string $dsn, array $options): bool
|
||||
{
|
||||
return 0 === strpos($dsn, 'doctrine://');
|
||||
}
|
||||
}
|
||||
|
||||
if (!class_exists(\Symfony\Component\Messenger\Transport\Doctrine\DoctrineTransportFactory::class, false)) {
|
||||
class_alias(DoctrineTransportFactory::class, \Symfony\Component\Messenger\Transport\Doctrine\DoctrineTransportFactory::class);
|
||||
}
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Messenger\Bridge\Doctrine\Transport;
|
||||
|
||||
use Doctrine\DBAL\Schema\Table;
|
||||
|
||||
/**
|
||||
* Uses PostgreSQL LISTEN/NOTIFY to push messages to workers.
|
||||
*
|
||||
* If you do not want to use the LISTEN mechanism, set the `use_notify` option to `false` when calling DoctrineTransportFactory::createTransport.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @author Kévin Dunglas <dunglas@gmail.com>
|
||||
*/
|
||||
final class PostgreSqlConnection extends Connection
|
||||
{
|
||||
/**
|
||||
* * check_delayed_interval: The interval to check for delayed messages, in milliseconds. Set to 0 to disable checks. Default: 60000 (1 minute)
|
||||
* * get_notify_timeout: The length of time to wait for a response when calling PDO::pgsqlGetNotify, in milliseconds. Default: 0.
|
||||
*/
|
||||
protected const DEFAULT_OPTIONS = parent::DEFAULT_OPTIONS + [
|
||||
'check_delayed_interval' => 60000,
|
||||
'get_notify_timeout' => 0,
|
||||
];
|
||||
|
||||
public function __sleep(): array
|
||||
{
|
||||
throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
|
||||
}
|
||||
|
||||
public function __wakeup()
|
||||
{
|
||||
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
|
||||
}
|
||||
|
||||
public function __destruct()
|
||||
{
|
||||
$this->unlisten();
|
||||
}
|
||||
|
||||
public function reset()
|
||||
{
|
||||
parent::reset();
|
||||
$this->unlisten();
|
||||
}
|
||||
|
||||
public function get(): ?array
|
||||
{
|
||||
if (null === $this->queueEmptiedAt) {
|
||||
return parent::get();
|
||||
}
|
||||
|
||||
// This is secure because the table name must be a valid identifier:
|
||||
// https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS
|
||||
$this->executeStatement(sprintf('LISTEN "%s"', $this->configuration['table_name']));
|
||||
|
||||
// The condition should be removed once support for DBAL <3.3 is dropped
|
||||
if (method_exists($this->driverConnection, 'getNativeConnection')) {
|
||||
$wrappedConnection = $this->driverConnection->getNativeConnection();
|
||||
} else {
|
||||
$wrappedConnection = $this->driverConnection;
|
||||
while (method_exists($wrappedConnection, 'getWrappedConnection')) {
|
||||
$wrappedConnection = $wrappedConnection->getWrappedConnection();
|
||||
}
|
||||
}
|
||||
|
||||
$notification = $wrappedConnection->pgsqlGetNotify(\PDO::FETCH_ASSOC, $this->configuration['get_notify_timeout']);
|
||||
if (
|
||||
// no notifications, or for another table or queue
|
||||
(false === $notification || $notification['message'] !== $this->configuration['table_name'] || $notification['payload'] !== $this->configuration['queue_name']) &&
|
||||
// delayed messages
|
||||
(microtime(true) * 1000 - $this->queueEmptiedAt < $this->configuration['check_delayed_interval'])
|
||||
) {
|
||||
usleep(1000);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return parent::get();
|
||||
}
|
||||
|
||||
public function setup(): void
|
||||
{
|
||||
parent::setup();
|
||||
|
||||
$this->executeStatement(implode("\n", $this->getTriggerSql()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getExtraSetupSqlForTable(Table $createdTable): array
|
||||
{
|
||||
if (!$createdTable->hasOption(self::TABLE_OPTION_NAME)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if ($createdTable->getOption(self::TABLE_OPTION_NAME) !== $this->configuration['table_name']) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->getTriggerSql();
|
||||
}
|
||||
|
||||
private function getTriggerSql(): array
|
||||
{
|
||||
$functionName = $this->createTriggerFunctionName();
|
||||
|
||||
return [
|
||||
// create trigger function
|
||||
sprintf(<<<'SQL'
|
||||
CREATE OR REPLACE FUNCTION %1$s() RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
PERFORM pg_notify('%2$s', NEW.queue_name::text);
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
SQL
|
||||
, $functionName, $this->configuration['table_name']),
|
||||
// register trigger
|
||||
sprintf('DROP TRIGGER IF EXISTS notify_trigger ON %s;', $this->configuration['table_name']),
|
||||
sprintf('CREATE TRIGGER notify_trigger AFTER INSERT OR UPDATE ON %1$s FOR EACH ROW EXECUTE PROCEDURE %2$s();', $this->configuration['table_name'], $functionName),
|
||||
];
|
||||
}
|
||||
|
||||
private function createTriggerFunctionName(): string
|
||||
{
|
||||
$tableConfig = explode('.', $this->configuration['table_name']);
|
||||
|
||||
if (1 === \count($tableConfig)) {
|
||||
return sprintf('notify_%1$s', $tableConfig[0]);
|
||||
}
|
||||
|
||||
return sprintf('%1$s.notify_%2$s', $tableConfig[0], $tableConfig[1]);
|
||||
}
|
||||
|
||||
private function unlisten()
|
||||
{
|
||||
$this->executeStatement(sprintf('UNLISTEN "%s"', $this->configuration['table_name']));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "symfony/doctrine-messenger",
|
||||
"type": "symfony-messenger-bridge",
|
||||
"description": "Symfony Doctrine Messenger Bridge",
|
||||
"keywords": [],
|
||||
"homepage": "https://symfony.com",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=7.2.5",
|
||||
"symfony/messenger": "^5.1|^6.0",
|
||||
"symfony/service-contracts": "^1.1|^2|^3"
|
||||
},
|
||||
"require-dev": {
|
||||
"doctrine/dbal": "^2.13|^3|^4",
|
||||
"doctrine/persistence": "^1.3|^2|^3",
|
||||
"symfony/property-access": "^4.4|^5.0|^6.0",
|
||||
"symfony/serializer": "^4.4|^5.0|^6.0"
|
||||
},
|
||||
"conflict": {
|
||||
"doctrine/dbal": "<2.13",
|
||||
"doctrine/persistence": "<1.3"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": { "Symfony\\Component\\Messenger\\Bridge\\Doctrine\\": "" },
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"minimum-stability": "dev"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
CHANGELOG
|
||||
=========
|
||||
|
||||
The changelog is maintained for all Symfony contracts at the following URL:
|
||||
https://github.com/symfony/contracts/blob/main/CHANGELOG.md
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Contracts\EventDispatcher;
|
||||
|
||||
use Psr\EventDispatcher\StoppableEventInterface;
|
||||
|
||||
/**
|
||||
* Event is the base class for classes containing event data.
|
||||
*
|
||||
* This class contains no event data. It is used by events that do not pass
|
||||
* state information to an event handler when an event is raised.
|
||||
*
|
||||
* You can call the method stopPropagation() to abort the execution of
|
||||
* further listeners in your event listener.
|
||||
*
|
||||
* @author Guilherme Blanco <guilhermeblanco@hotmail.com>
|
||||
* @author Jonathan Wage <jonwage@gmail.com>
|
||||
* @author Roman Borschel <roman@code-factory.org>
|
||||
* @author Bernhard Schussek <bschussek@gmail.com>
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*/
|
||||
class Event implements StoppableEventInterface
|
||||
{
|
||||
private $propagationStopped = false;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isPropagationStopped(): bool
|
||||
{
|
||||
return $this->propagationStopped;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the propagation of the event to further event listeners.
|
||||
*
|
||||
* If multiple event listeners are connected to the same event, no
|
||||
* further event listener will be triggered once any trigger calls
|
||||
* stopPropagation().
|
||||
*/
|
||||
public function stopPropagation(): void
|
||||
{
|
||||
$this->propagationStopped = true;
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Contracts\EventDispatcher;
|
||||
|
||||
use Psr\EventDispatcher\EventDispatcherInterface as PsrEventDispatcherInterface;
|
||||
|
||||
/**
|
||||
* Allows providing hooks on domain-specific lifecycles by dispatching events.
|
||||
*/
|
||||
interface EventDispatcherInterface extends PsrEventDispatcherInterface
|
||||
{
|
||||
/**
|
||||
* Dispatches an event to all registered listeners.
|
||||
*
|
||||
* @param object $event The event to pass to the event handlers/listeners
|
||||
* @param string|null $eventName The name of the event to dispatch. If not supplied,
|
||||
* the class of $event should be used instead.
|
||||
*
|
||||
* @return object The passed $event MUST be returned
|
||||
*/
|
||||
public function dispatch(object $event, ?string $eventName = null): object;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
Copyright (c) 2018-present Fabien Potencier
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is furnished
|
||||
to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
@@ -0,0 +1,9 @@
|
||||
Symfony EventDispatcher Contracts
|
||||
=================================
|
||||
|
||||
A set of abstractions extracted out of the Symfony components.
|
||||
|
||||
Can be used to build on semantics that the Symfony components proved useful - and
|
||||
that already have battle tested implementations.
|
||||
|
||||
See https://github.com/symfony/contracts/blob/main/README.md for more information.
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "symfony/event-dispatcher-contracts",
|
||||
"type": "library",
|
||||
"description": "Generic abstractions related to dispatching event",
|
||||
"keywords": ["abstractions", "contracts", "decoupling", "interfaces", "interoperability", "standards"],
|
||||
"homepage": "https://symfony.com",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nicolas Grekas",
|
||||
"email": "p@tchwork.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=7.2.5",
|
||||
"psr/event-dispatcher": "^1"
|
||||
},
|
||||
"suggest": {
|
||||
"symfony/event-dispatcher-implementation": ""
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": { "Symfony\\Contracts\\EventDispatcher\\": "" }
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "2.5-dev"
|
||||
},
|
||||
"thanks": {
|
||||
"name": "symfony/contracts",
|
||||
"url": "https://github.com/symfony/contracts"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\EventDispatcher\Attribute;
|
||||
|
||||
/**
|
||||
* Service tag to autoconfigure event listeners.
|
||||
*
|
||||
* @author Alexander M. Turek <me@derrabus.de>
|
||||
*/
|
||||
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
|
||||
class AsEventListener
|
||||
{
|
||||
public function __construct(
|
||||
public ?string $event = null,
|
||||
public ?string $method = null,
|
||||
public int $priority = 0,
|
||||
public ?string $dispatcher = null,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
CHANGELOG
|
||||
=========
|
||||
|
||||
5.4
|
||||
---
|
||||
|
||||
* Allow `#[AsEventListener]` attribute on methods
|
||||
|
||||
5.3
|
||||
---
|
||||
|
||||
* Add `#[AsEventListener]` attribute for declaring listeners on PHP 8
|
||||
|
||||
5.1.0
|
||||
-----
|
||||
|
||||
* The `LegacyEventDispatcherProxy` class has been deprecated.
|
||||
* Added an optional `dispatcher` attribute to the listener and subscriber tags in `RegisterListenerPass`.
|
||||
|
||||
5.0.0
|
||||
-----
|
||||
|
||||
* The signature of the `EventDispatcherInterface::dispatch()` method has been changed to `dispatch($event, string $eventName = null): object`.
|
||||
* The `Event` class has been removed in favor of `Symfony\Contracts\EventDispatcher\Event`.
|
||||
* The `TraceableEventDispatcherInterface` has been removed.
|
||||
* The `WrappedListener` class is now final.
|
||||
|
||||
4.4.0
|
||||
-----
|
||||
|
||||
* `AddEventAliasesPass` has been added, allowing applications and bundles to extend the event alias mapping used by `RegisterListenersPass`.
|
||||
* Made the `event` attribute of the `kernel.event_listener` tag optional for FQCN events.
|
||||
|
||||
4.3.0
|
||||
-----
|
||||
|
||||
* The signature of the `EventDispatcherInterface::dispatch()` method should be updated to `dispatch($event, string $eventName = null)`, not doing so is deprecated
|
||||
* deprecated the `Event` class, use `Symfony\Contracts\EventDispatcher\Event` instead
|
||||
|
||||
4.1.0
|
||||
-----
|
||||
|
||||
* added support for invokable event listeners tagged with `kernel.event_listener` by default
|
||||
* The `TraceableEventDispatcher::getOrphanedEvents()` method has been added.
|
||||
* The `TraceableEventDispatcherInterface` has been deprecated.
|
||||
|
||||
4.0.0
|
||||
-----
|
||||
|
||||
* removed the `ContainerAwareEventDispatcher` class
|
||||
* added the `reset()` method to the `TraceableEventDispatcherInterface`
|
||||
|
||||
3.4.0
|
||||
-----
|
||||
|
||||
* Implementing `TraceableEventDispatcherInterface` without the `reset()` method has been deprecated.
|
||||
|
||||
3.3.0
|
||||
-----
|
||||
|
||||
* The ContainerAwareEventDispatcher class has been deprecated. Use EventDispatcher with closure factories instead.
|
||||
|
||||
3.0.0
|
||||
-----
|
||||
|
||||
* The method `getListenerPriority($eventName, $listener)` has been added to the
|
||||
`EventDispatcherInterface`.
|
||||
* The methods `Event::setDispatcher()`, `Event::getDispatcher()`, `Event::setName()`
|
||||
and `Event::getName()` have been removed.
|
||||
The event dispatcher and the event name are passed to the listener call.
|
||||
|
||||
2.5.0
|
||||
-----
|
||||
|
||||
* added Debug\TraceableEventDispatcher (originally in HttpKernel)
|
||||
* changed Debug\TraceableEventDispatcherInterface to extend EventDispatcherInterface
|
||||
* added RegisterListenersPass (originally in HttpKernel)
|
||||
|
||||
2.1.0
|
||||
-----
|
||||
|
||||
* added TraceableEventDispatcherInterface
|
||||
* added ContainerAwareEventDispatcher
|
||||
* added a reference to the EventDispatcher on the Event
|
||||
* added a reference to the Event name on the event
|
||||
* added fluid interface to the dispatch() method which now returns the Event
|
||||
object
|
||||
* added GenericEvent event class
|
||||
* added the possibility for subscribers to subscribe several times for the
|
||||
same event
|
||||
* added ImmutableEventDispatcher
|
||||
+366
@@ -0,0 +1,366 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\EventDispatcher\Debug;
|
||||
|
||||
use Psr\EventDispatcher\StoppableEventInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\Stopwatch\Stopwatch;
|
||||
use Symfony\Contracts\Service\ResetInterface;
|
||||
|
||||
/**
|
||||
* Collects some data about event listeners.
|
||||
*
|
||||
* This event dispatcher delegates the dispatching to another one.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
class TraceableEventDispatcher implements EventDispatcherInterface, ResetInterface
|
||||
{
|
||||
protected $logger;
|
||||
protected $stopwatch;
|
||||
|
||||
/**
|
||||
* @var \SplObjectStorage<WrappedListener, array{string, string}>
|
||||
*/
|
||||
private $callStack;
|
||||
private $dispatcher;
|
||||
private $wrappedListeners;
|
||||
private $orphanedEvents;
|
||||
private $requestStack;
|
||||
private $currentRequestHash = '';
|
||||
|
||||
public function __construct(EventDispatcherInterface $dispatcher, Stopwatch $stopwatch, ?LoggerInterface $logger = null, ?RequestStack $requestStack = null)
|
||||
{
|
||||
$this->dispatcher = $dispatcher;
|
||||
$this->stopwatch = $stopwatch;
|
||||
$this->logger = $logger;
|
||||
$this->wrappedListeners = [];
|
||||
$this->orphanedEvents = [];
|
||||
$this->requestStack = $requestStack;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function addListener(string $eventName, $listener, int $priority = 0)
|
||||
{
|
||||
$this->dispatcher->addListener($eventName, $listener, $priority);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function addSubscriber(EventSubscriberInterface $subscriber)
|
||||
{
|
||||
$this->dispatcher->addSubscriber($subscriber);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function removeListener(string $eventName, $listener)
|
||||
{
|
||||
if (isset($this->wrappedListeners[$eventName])) {
|
||||
foreach ($this->wrappedListeners[$eventName] as $index => $wrappedListener) {
|
||||
if ($wrappedListener->getWrappedListener() === $listener || ($listener instanceof \Closure && $wrappedListener->getWrappedListener() == $listener)) {
|
||||
$listener = $wrappedListener;
|
||||
unset($this->wrappedListeners[$eventName][$index]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->dispatcher->removeListener($eventName, $listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function removeSubscriber(EventSubscriberInterface $subscriber)
|
||||
{
|
||||
return $this->dispatcher->removeSubscriber($subscriber);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getListeners(?string $eventName = null)
|
||||
{
|
||||
return $this->dispatcher->getListeners($eventName);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getListenerPriority(string $eventName, $listener)
|
||||
{
|
||||
// we might have wrapped listeners for the event (if called while dispatching)
|
||||
// in that case get the priority by wrapper
|
||||
if (isset($this->wrappedListeners[$eventName])) {
|
||||
foreach ($this->wrappedListeners[$eventName] as $wrappedListener) {
|
||||
if ($wrappedListener->getWrappedListener() === $listener || ($listener instanceof \Closure && $wrappedListener->getWrappedListener() == $listener)) {
|
||||
return $this->dispatcher->getListenerPriority($eventName, $wrappedListener);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->dispatcher->getListenerPriority($eventName, $listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function hasListeners(?string $eventName = null)
|
||||
{
|
||||
return $this->dispatcher->hasListeners($eventName);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function dispatch(object $event, ?string $eventName = null): object
|
||||
{
|
||||
$eventName = $eventName ?? \get_class($event);
|
||||
|
||||
if (null === $this->callStack) {
|
||||
$this->callStack = new \SplObjectStorage();
|
||||
}
|
||||
|
||||
$currentRequestHash = $this->currentRequestHash = $this->requestStack && ($request = $this->requestStack->getCurrentRequest()) ? spl_object_hash($request) : '';
|
||||
|
||||
if (null !== $this->logger && $event instanceof StoppableEventInterface && $event->isPropagationStopped()) {
|
||||
$this->logger->debug(sprintf('The "%s" event is already stopped. No listeners have been called.', $eventName));
|
||||
}
|
||||
|
||||
$this->preProcess($eventName);
|
||||
try {
|
||||
$this->beforeDispatch($eventName, $event);
|
||||
try {
|
||||
$e = $this->stopwatch->start($eventName, 'section');
|
||||
try {
|
||||
$this->dispatcher->dispatch($event, $eventName);
|
||||
} finally {
|
||||
if ($e->isStarted()) {
|
||||
$e->stop();
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
$this->afterDispatch($eventName, $event);
|
||||
}
|
||||
} finally {
|
||||
$this->currentRequestHash = $currentRequestHash;
|
||||
$this->postProcess($eventName);
|
||||
}
|
||||
|
||||
return $event;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getCalledListeners(?Request $request = null)
|
||||
{
|
||||
if (null === $this->callStack) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$hash = $request ? spl_object_hash($request) : null;
|
||||
$called = [];
|
||||
foreach ($this->callStack as $listener) {
|
||||
[$eventName, $requestHash] = $this->callStack->getInfo();
|
||||
if (null === $hash || $hash === $requestHash) {
|
||||
$called[] = $listener->getInfo($eventName);
|
||||
}
|
||||
}
|
||||
|
||||
return $called;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getNotCalledListeners(?Request $request = null)
|
||||
{
|
||||
try {
|
||||
$allListeners = $this->getListeners();
|
||||
} catch (\Exception $e) {
|
||||
if (null !== $this->logger) {
|
||||
$this->logger->info('An exception was thrown while getting the uncalled listeners.', ['exception' => $e]);
|
||||
}
|
||||
|
||||
// unable to retrieve the uncalled listeners
|
||||
return [];
|
||||
}
|
||||
|
||||
$hash = $request ? spl_object_hash($request) : null;
|
||||
$calledListeners = [];
|
||||
|
||||
if (null !== $this->callStack) {
|
||||
foreach ($this->callStack as $calledListener) {
|
||||
[, $requestHash] = $this->callStack->getInfo();
|
||||
|
||||
if (null === $hash || $hash === $requestHash) {
|
||||
$calledListeners[] = $calledListener->getWrappedListener();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$notCalled = [];
|
||||
foreach ($allListeners as $eventName => $listeners) {
|
||||
foreach ($listeners as $listener) {
|
||||
if (!\in_array($listener, $calledListeners, true)) {
|
||||
if (!$listener instanceof WrappedListener) {
|
||||
$listener = new WrappedListener($listener, null, $this->stopwatch, $this);
|
||||
}
|
||||
$notCalled[] = $listener->getInfo($eventName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uasort($notCalled, [$this, 'sortNotCalledListeners']);
|
||||
|
||||
return $notCalled;
|
||||
}
|
||||
|
||||
public function getOrphanedEvents(?Request $request = null): array
|
||||
{
|
||||
if ($request) {
|
||||
return $this->orphanedEvents[spl_object_hash($request)] ?? [];
|
||||
}
|
||||
|
||||
if (!$this->orphanedEvents) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_merge(...array_values($this->orphanedEvents));
|
||||
}
|
||||
|
||||
public function reset()
|
||||
{
|
||||
$this->callStack = null;
|
||||
$this->orphanedEvents = [];
|
||||
$this->currentRequestHash = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxies all method calls to the original event dispatcher.
|
||||
*
|
||||
* @param string $method The method name
|
||||
* @param array $arguments The method arguments
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function __call(string $method, array $arguments)
|
||||
{
|
||||
return $this->dispatcher->{$method}(...$arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called before dispatching the event.
|
||||
*/
|
||||
protected function beforeDispatch(string $eventName, object $event)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after dispatching the event.
|
||||
*/
|
||||
protected function afterDispatch(string $eventName, object $event)
|
||||
{
|
||||
}
|
||||
|
||||
private function preProcess(string $eventName): void
|
||||
{
|
||||
if (!$this->dispatcher->hasListeners($eventName)) {
|
||||
$this->orphanedEvents[$this->currentRequestHash][] = $eventName;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->dispatcher->getListeners($eventName) as $listener) {
|
||||
$priority = $this->getListenerPriority($eventName, $listener);
|
||||
$wrappedListener = new WrappedListener($listener instanceof WrappedListener ? $listener->getWrappedListener() : $listener, null, $this->stopwatch, $this);
|
||||
$this->wrappedListeners[$eventName][] = $wrappedListener;
|
||||
$this->dispatcher->removeListener($eventName, $listener);
|
||||
$this->dispatcher->addListener($eventName, $wrappedListener, $priority);
|
||||
$this->callStack->attach($wrappedListener, [$eventName, $this->currentRequestHash]);
|
||||
}
|
||||
}
|
||||
|
||||
private function postProcess(string $eventName): void
|
||||
{
|
||||
unset($this->wrappedListeners[$eventName]);
|
||||
$skipped = false;
|
||||
foreach ($this->dispatcher->getListeners($eventName) as $listener) {
|
||||
if (!$listener instanceof WrappedListener) { // #12845: a new listener was added during dispatch.
|
||||
continue;
|
||||
}
|
||||
// Unwrap listener
|
||||
$priority = $this->getListenerPriority($eventName, $listener);
|
||||
$this->dispatcher->removeListener($eventName, $listener);
|
||||
$this->dispatcher->addListener($eventName, $listener->getWrappedListener(), $priority);
|
||||
|
||||
if (null !== $this->logger) {
|
||||
$context = ['event' => $eventName, 'listener' => $listener->getPretty()];
|
||||
}
|
||||
|
||||
if ($listener->wasCalled()) {
|
||||
if (null !== $this->logger) {
|
||||
$this->logger->debug('Notified event "{event}" to listener "{listener}".', $context);
|
||||
}
|
||||
} else {
|
||||
$this->callStack->detach($listener);
|
||||
}
|
||||
|
||||
if (null !== $this->logger && $skipped) {
|
||||
$this->logger->debug('Listener "{listener}" was not called for event "{event}".', $context);
|
||||
}
|
||||
|
||||
if ($listener->stoppedPropagation()) {
|
||||
if (null !== $this->logger) {
|
||||
$this->logger->debug('Listener "{listener}" stopped propagation of the event "{event}".', $context);
|
||||
}
|
||||
|
||||
$skipped = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function sortNotCalledListeners(array $a, array $b)
|
||||
{
|
||||
if (0 !== $cmp = strcmp($a['event'], $b['event'])) {
|
||||
return $cmp;
|
||||
}
|
||||
|
||||
if (\is_int($a['priority']) && !\is_int($b['priority'])) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!\is_int($a['priority']) && \is_int($b['priority'])) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if ($a['priority'] === $b['priority']) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ($a['priority'] > $b['priority']) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\EventDispatcher\Debug;
|
||||
|
||||
use Psr\EventDispatcher\StoppableEventInterface;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Stopwatch\Stopwatch;
|
||||
use Symfony\Component\VarDumper\Caster\ClassStub;
|
||||
|
||||
/**
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
final class WrappedListener
|
||||
{
|
||||
private $listener;
|
||||
private $optimizedListener;
|
||||
private $name;
|
||||
private $called;
|
||||
private $stoppedPropagation;
|
||||
private $stopwatch;
|
||||
private $dispatcher;
|
||||
private $pretty;
|
||||
private $stub;
|
||||
private $priority;
|
||||
private static $hasClassStub;
|
||||
|
||||
public function __construct($listener, ?string $name, Stopwatch $stopwatch, ?EventDispatcherInterface $dispatcher = null)
|
||||
{
|
||||
$this->listener = $listener;
|
||||
$this->optimizedListener = $listener instanceof \Closure ? $listener : (\is_callable($listener) ? \Closure::fromCallable($listener) : null);
|
||||
$this->stopwatch = $stopwatch;
|
||||
$this->dispatcher = $dispatcher;
|
||||
$this->called = false;
|
||||
$this->stoppedPropagation = false;
|
||||
|
||||
if (\is_array($listener)) {
|
||||
$this->name = \is_object($listener[0]) ? get_debug_type($listener[0]) : $listener[0];
|
||||
$this->pretty = $this->name.'::'.$listener[1];
|
||||
} elseif ($listener instanceof \Closure) {
|
||||
$r = new \ReflectionFunction($listener);
|
||||
if (str_contains($r->name, '{closure')) {
|
||||
$this->pretty = $this->name = 'closure';
|
||||
} elseif ($class = \PHP_VERSION_ID >= 80111 ? $r->getClosureCalledClass() : $r->getClosureScopeClass()) {
|
||||
$this->name = $class->name;
|
||||
$this->pretty = $this->name.'::'.$r->name;
|
||||
} else {
|
||||
$this->pretty = $this->name = $r->name;
|
||||
}
|
||||
} elseif (\is_string($listener)) {
|
||||
$this->pretty = $this->name = $listener;
|
||||
} else {
|
||||
$this->name = get_debug_type($listener);
|
||||
$this->pretty = $this->name.'::__invoke';
|
||||
}
|
||||
|
||||
if (null !== $name) {
|
||||
$this->name = $name;
|
||||
}
|
||||
|
||||
if (null === self::$hasClassStub) {
|
||||
self::$hasClassStub = class_exists(ClassStub::class);
|
||||
}
|
||||
}
|
||||
|
||||
public function getWrappedListener()
|
||||
{
|
||||
return $this->listener;
|
||||
}
|
||||
|
||||
public function wasCalled(): bool
|
||||
{
|
||||
return $this->called;
|
||||
}
|
||||
|
||||
public function stoppedPropagation(): bool
|
||||
{
|
||||
return $this->stoppedPropagation;
|
||||
}
|
||||
|
||||
public function getPretty(): string
|
||||
{
|
||||
return $this->pretty;
|
||||
}
|
||||
|
||||
public function getInfo(string $eventName): array
|
||||
{
|
||||
if (null === $this->stub) {
|
||||
$this->stub = self::$hasClassStub ? new ClassStub($this->pretty.'()', $this->listener) : $this->pretty.'()';
|
||||
}
|
||||
|
||||
return [
|
||||
'event' => $eventName,
|
||||
'priority' => null !== $this->priority ? $this->priority : (null !== $this->dispatcher ? $this->dispatcher->getListenerPriority($eventName, $this->listener) : null),
|
||||
'pretty' => $this->pretty,
|
||||
'stub' => $this->stub,
|
||||
];
|
||||
}
|
||||
|
||||
public function __invoke(object $event, string $eventName, EventDispatcherInterface $dispatcher): void
|
||||
{
|
||||
$dispatcher = $this->dispatcher ?: $dispatcher;
|
||||
|
||||
$this->called = true;
|
||||
$this->priority = $dispatcher->getListenerPriority($eventName, $this->listener);
|
||||
|
||||
$e = $this->stopwatch->start($this->name, 'event_listener');
|
||||
|
||||
try {
|
||||
($this->optimizedListener ?? $this->listener)($event, $eventName, $dispatcher);
|
||||
} finally {
|
||||
if ($e->isStarted()) {
|
||||
$e->stop();
|
||||
}
|
||||
}
|
||||
|
||||
if ($event instanceof StoppableEventInterface && $event->isPropagationStopped()) {
|
||||
$this->stoppedPropagation = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\EventDispatcher\DependencyInjection;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
|
||||
/**
|
||||
* This pass allows bundles to extend the list of event aliases.
|
||||
*
|
||||
* @author Alexander M. Turek <me@derrabus.de>
|
||||
*/
|
||||
class AddEventAliasesPass implements CompilerPassInterface
|
||||
{
|
||||
private $eventAliases;
|
||||
private $eventAliasesParameter;
|
||||
|
||||
public function __construct(array $eventAliases, string $eventAliasesParameter = 'event_dispatcher.event_aliases')
|
||||
{
|
||||
if (1 < \func_num_args()) {
|
||||
trigger_deprecation('symfony/event-dispatcher', '5.3', 'Configuring "%s" is deprecated.', __CLASS__);
|
||||
}
|
||||
|
||||
$this->eventAliases = $eventAliases;
|
||||
$this->eventAliasesParameter = $eventAliasesParameter;
|
||||
}
|
||||
|
||||
public function process(ContainerBuilder $container): void
|
||||
{
|
||||
$eventAliases = $container->hasParameter($this->eventAliasesParameter) ? $container->getParameter($this->eventAliasesParameter) : [];
|
||||
|
||||
$container->setParameter(
|
||||
$this->eventAliasesParameter,
|
||||
array_merge($eventAliases, $this->eventAliases)
|
||||
);
|
||||
}
|
||||
}
|
||||
Vendored
+242
@@ -0,0 +1,242 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\EventDispatcher\DependencyInjection;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
|
||||
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\DependencyInjection\Reference;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcher;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
/**
|
||||
* Compiler pass to register tagged services for an event dispatcher.
|
||||
*/
|
||||
class RegisterListenersPass implements CompilerPassInterface
|
||||
{
|
||||
protected $dispatcherService;
|
||||
protected $listenerTag;
|
||||
protected $subscriberTag;
|
||||
protected $eventAliasesParameter;
|
||||
|
||||
private $hotPathEvents = [];
|
||||
private $hotPathTagName = 'container.hot_path';
|
||||
private $noPreloadEvents = [];
|
||||
private $noPreloadTagName = 'container.no_preload';
|
||||
|
||||
public function __construct(string $dispatcherService = 'event_dispatcher', string $listenerTag = 'kernel.event_listener', string $subscriberTag = 'kernel.event_subscriber', string $eventAliasesParameter = 'event_dispatcher.event_aliases')
|
||||
{
|
||||
if (0 < \func_num_args()) {
|
||||
trigger_deprecation('symfony/event-dispatcher', '5.3', 'Configuring "%s" is deprecated.', __CLASS__);
|
||||
}
|
||||
|
||||
$this->dispatcherService = $dispatcherService;
|
||||
$this->listenerTag = $listenerTag;
|
||||
$this->subscriberTag = $subscriberTag;
|
||||
$this->eventAliasesParameter = $eventAliasesParameter;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setHotPathEvents(array $hotPathEvents)
|
||||
{
|
||||
$this->hotPathEvents = array_flip($hotPathEvents);
|
||||
|
||||
if (1 < \func_num_args()) {
|
||||
trigger_deprecation('symfony/event-dispatcher', '5.4', 'Configuring "$tagName" in "%s" is deprecated.', __METHOD__);
|
||||
$this->hotPathTagName = func_get_arg(1);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setNoPreloadEvents(array $noPreloadEvents): self
|
||||
{
|
||||
$this->noPreloadEvents = array_flip($noPreloadEvents);
|
||||
|
||||
if (1 < \func_num_args()) {
|
||||
trigger_deprecation('symfony/event-dispatcher', '5.4', 'Configuring "$tagName" in "%s" is deprecated.', __METHOD__);
|
||||
$this->noPreloadTagName = func_get_arg(1);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function process(ContainerBuilder $container)
|
||||
{
|
||||
if (!$container->hasDefinition($this->dispatcherService) && !$container->hasAlias($this->dispatcherService)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$aliases = [];
|
||||
|
||||
if ($container->hasParameter($this->eventAliasesParameter)) {
|
||||
$aliases = $container->getParameter($this->eventAliasesParameter);
|
||||
}
|
||||
|
||||
$globalDispatcherDefinition = $container->findDefinition($this->dispatcherService);
|
||||
|
||||
foreach ($container->findTaggedServiceIds($this->listenerTag, true) as $id => $events) {
|
||||
$noPreload = 0;
|
||||
|
||||
foreach ($events as $event) {
|
||||
$priority = $event['priority'] ?? 0;
|
||||
|
||||
if (!isset($event['event'])) {
|
||||
if ($container->getDefinition($id)->hasTag($this->subscriberTag)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$event['method'] = $event['method'] ?? '__invoke';
|
||||
$event['event'] = $this->getEventFromTypeDeclaration($container, $id, $event['method']);
|
||||
}
|
||||
|
||||
$event['event'] = $aliases[$event['event']] ?? $event['event'];
|
||||
|
||||
if (!isset($event['method'])) {
|
||||
$event['method'] = 'on'.preg_replace_callback([
|
||||
'/(?<=\b|_)[a-z]/i',
|
||||
'/[^a-z0-9]/i',
|
||||
], function ($matches) { return strtoupper($matches[0]); }, $event['event']);
|
||||
$event['method'] = preg_replace('/[^a-z0-9]/i', '', $event['method']);
|
||||
|
||||
if (null !== ($class = $container->getDefinition($id)->getClass()) && ($r = $container->getReflectionClass($class, false)) && !$r->hasMethod($event['method'])) {
|
||||
if (!$r->hasMethod('__invoke')) {
|
||||
throw new InvalidArgumentException(sprintf('None of the "%s" or "__invoke" methods exist for the service "%s". Please define the "method" attribute on "%s" tags.', $event['method'], $id, $this->listenerTag));
|
||||
}
|
||||
|
||||
$event['method'] = '__invoke';
|
||||
}
|
||||
}
|
||||
|
||||
$dispatcherDefinition = $globalDispatcherDefinition;
|
||||
if (isset($event['dispatcher'])) {
|
||||
$dispatcherDefinition = $container->findDefinition($event['dispatcher']);
|
||||
}
|
||||
|
||||
$dispatcherDefinition->addMethodCall('addListener', [$event['event'], [new ServiceClosureArgument(new Reference($id)), $event['method']], $priority]);
|
||||
|
||||
if (isset($this->hotPathEvents[$event['event']])) {
|
||||
$container->getDefinition($id)->addTag($this->hotPathTagName);
|
||||
} elseif (isset($this->noPreloadEvents[$event['event']])) {
|
||||
++$noPreload;
|
||||
}
|
||||
}
|
||||
|
||||
if ($noPreload && \count($events) === $noPreload) {
|
||||
$container->getDefinition($id)->addTag($this->noPreloadTagName);
|
||||
}
|
||||
}
|
||||
|
||||
$extractingDispatcher = new ExtractingEventDispatcher();
|
||||
|
||||
foreach ($container->findTaggedServiceIds($this->subscriberTag, true) as $id => $tags) {
|
||||
$def = $container->getDefinition($id);
|
||||
|
||||
// We must assume that the class value has been correctly filled, even if the service is created by a factory
|
||||
$class = $def->getClass();
|
||||
|
||||
if (!$r = $container->getReflectionClass($class)) {
|
||||
throw new InvalidArgumentException(sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id));
|
||||
}
|
||||
if (!$r->isSubclassOf(EventSubscriberInterface::class)) {
|
||||
throw new InvalidArgumentException(sprintf('Service "%s" must implement interface "%s".', $id, EventSubscriberInterface::class));
|
||||
}
|
||||
$class = $r->name;
|
||||
|
||||
$dispatcherDefinitions = [];
|
||||
foreach ($tags as $attributes) {
|
||||
if (!isset($attributes['dispatcher']) || isset($dispatcherDefinitions[$attributes['dispatcher']])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$dispatcherDefinitions[$attributes['dispatcher']] = $container->findDefinition($attributes['dispatcher']);
|
||||
}
|
||||
|
||||
if (!$dispatcherDefinitions) {
|
||||
$dispatcherDefinitions = [$globalDispatcherDefinition];
|
||||
}
|
||||
|
||||
$noPreload = 0;
|
||||
ExtractingEventDispatcher::$aliases = $aliases;
|
||||
ExtractingEventDispatcher::$subscriber = $class;
|
||||
$extractingDispatcher->addSubscriber($extractingDispatcher);
|
||||
foreach ($extractingDispatcher->listeners as $args) {
|
||||
$args[1] = [new ServiceClosureArgument(new Reference($id)), $args[1]];
|
||||
foreach ($dispatcherDefinitions as $dispatcherDefinition) {
|
||||
$dispatcherDefinition->addMethodCall('addListener', $args);
|
||||
}
|
||||
|
||||
if (isset($this->hotPathEvents[$args[0]])) {
|
||||
$container->getDefinition($id)->addTag($this->hotPathTagName);
|
||||
} elseif (isset($this->noPreloadEvents[$args[0]])) {
|
||||
++$noPreload;
|
||||
}
|
||||
}
|
||||
if ($noPreload && \count($extractingDispatcher->listeners) === $noPreload) {
|
||||
$container->getDefinition($id)->addTag($this->noPreloadTagName);
|
||||
}
|
||||
$extractingDispatcher->listeners = [];
|
||||
ExtractingEventDispatcher::$aliases = [];
|
||||
}
|
||||
}
|
||||
|
||||
private function getEventFromTypeDeclaration(ContainerBuilder $container, string $id, string $method): string
|
||||
{
|
||||
if (
|
||||
null === ($class = $container->getDefinition($id)->getClass())
|
||||
|| !($r = $container->getReflectionClass($class, false))
|
||||
|| !$r->hasMethod($method)
|
||||
|| 1 > ($m = $r->getMethod($method))->getNumberOfParameters()
|
||||
|| !($type = $m->getParameters()[0]->getType()) instanceof \ReflectionNamedType
|
||||
|| $type->isBuiltin()
|
||||
|| Event::class === ($name = $type->getName())
|
||||
) {
|
||||
throw new InvalidArgumentException(sprintf('Service "%s" must define the "event" attribute on "%s" tags.', $id, $this->listenerTag));
|
||||
}
|
||||
|
||||
return $name;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class ExtractingEventDispatcher extends EventDispatcher implements EventSubscriberInterface
|
||||
{
|
||||
public $listeners = [];
|
||||
|
||||
public static $aliases = [];
|
||||
public static $subscriber;
|
||||
|
||||
public function addListener(string $eventName, $listener, int $priority = 0)
|
||||
{
|
||||
$this->listeners[] = [$eventName, $listener[1], $priority];
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
$events = [];
|
||||
|
||||
foreach ([self::$subscriber, 'getSubscribedEvents']() as $eventName => $params) {
|
||||
$events[self::$aliases[$eventName] ?? $eventName] = $params;
|
||||
}
|
||||
|
||||
return $events;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\EventDispatcher;
|
||||
|
||||
use Psr\EventDispatcher\StoppableEventInterface;
|
||||
use Symfony\Component\EventDispatcher\Debug\WrappedListener;
|
||||
|
||||
/**
|
||||
* The EventDispatcherInterface is the central point of Symfony's event listener system.
|
||||
*
|
||||
* Listeners are registered on the manager and events are dispatched through the
|
||||
* manager.
|
||||
*
|
||||
* @author Guilherme Blanco <guilhermeblanco@hotmail.com>
|
||||
* @author Jonathan Wage <jonwage@gmail.com>
|
||||
* @author Roman Borschel <roman@code-factory.org>
|
||||
* @author Bernhard Schussek <bschussek@gmail.com>
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
* @author Jordi Boggiano <j.boggiano@seld.be>
|
||||
* @author Jordan Alliot <jordan.alliot@gmail.com>
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*/
|
||||
class EventDispatcher implements EventDispatcherInterface
|
||||
{
|
||||
private $listeners = [];
|
||||
private $sorted = [];
|
||||
private $optimized;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
if (__CLASS__ === static::class) {
|
||||
$this->optimized = [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function dispatch(object $event, ?string $eventName = null): object
|
||||
{
|
||||
$eventName = $eventName ?? \get_class($event);
|
||||
|
||||
if (null !== $this->optimized) {
|
||||
$listeners = $this->optimized[$eventName] ?? (empty($this->listeners[$eventName]) ? [] : $this->optimizeListeners($eventName));
|
||||
} else {
|
||||
$listeners = $this->getListeners($eventName);
|
||||
}
|
||||
|
||||
if ($listeners) {
|
||||
$this->callListeners($listeners, $eventName, $event);
|
||||
}
|
||||
|
||||
return $event;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getListeners(?string $eventName = null)
|
||||
{
|
||||
if (null !== $eventName) {
|
||||
if (empty($this->listeners[$eventName])) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!isset($this->sorted[$eventName])) {
|
||||
$this->sortListeners($eventName);
|
||||
}
|
||||
|
||||
return $this->sorted[$eventName];
|
||||
}
|
||||
|
||||
foreach ($this->listeners as $eventName => $eventListeners) {
|
||||
if (!isset($this->sorted[$eventName])) {
|
||||
$this->sortListeners($eventName);
|
||||
}
|
||||
}
|
||||
|
||||
return array_filter($this->sorted);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getListenerPriority(string $eventName, $listener)
|
||||
{
|
||||
if (empty($this->listeners[$eventName])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (\is_array($listener) && isset($listener[0]) && $listener[0] instanceof \Closure && 2 >= \count($listener)) {
|
||||
$listener[0] = $listener[0]();
|
||||
$listener[1] = $listener[1] ?? '__invoke';
|
||||
}
|
||||
|
||||
foreach ($this->listeners[$eventName] as $priority => &$listeners) {
|
||||
foreach ($listeners as &$v) {
|
||||
if ($v !== $listener && \is_array($v) && isset($v[0]) && $v[0] instanceof \Closure && 2 >= \count($v)) {
|
||||
$v[0] = $v[0]();
|
||||
$v[1] = $v[1] ?? '__invoke';
|
||||
}
|
||||
if ($v === $listener || ($listener instanceof \Closure && $v == $listener)) {
|
||||
return $priority;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function hasListeners(?string $eventName = null)
|
||||
{
|
||||
if (null !== $eventName) {
|
||||
return !empty($this->listeners[$eventName]);
|
||||
}
|
||||
|
||||
foreach ($this->listeners as $eventListeners) {
|
||||
if ($eventListeners) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function addListener(string $eventName, $listener, int $priority = 0)
|
||||
{
|
||||
$this->listeners[$eventName][$priority][] = $listener;
|
||||
unset($this->sorted[$eventName], $this->optimized[$eventName]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function removeListener(string $eventName, $listener)
|
||||
{
|
||||
if (empty($this->listeners[$eventName])) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (\is_array($listener) && isset($listener[0]) && $listener[0] instanceof \Closure && 2 >= \count($listener)) {
|
||||
$listener[0] = $listener[0]();
|
||||
$listener[1] = $listener[1] ?? '__invoke';
|
||||
}
|
||||
|
||||
foreach ($this->listeners[$eventName] as $priority => &$listeners) {
|
||||
foreach ($listeners as $k => &$v) {
|
||||
if ($v !== $listener && \is_array($v) && isset($v[0]) && $v[0] instanceof \Closure && 2 >= \count($v)) {
|
||||
$v[0] = $v[0]();
|
||||
$v[1] = $v[1] ?? '__invoke';
|
||||
}
|
||||
if ($v === $listener || ($listener instanceof \Closure && $v == $listener)) {
|
||||
unset($listeners[$k], $this->sorted[$eventName], $this->optimized[$eventName]);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$listeners) {
|
||||
unset($this->listeners[$eventName][$priority]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function addSubscriber(EventSubscriberInterface $subscriber)
|
||||
{
|
||||
foreach ($subscriber->getSubscribedEvents() as $eventName => $params) {
|
||||
if (\is_string($params)) {
|
||||
$this->addListener($eventName, [$subscriber, $params]);
|
||||
} elseif (\is_string($params[0])) {
|
||||
$this->addListener($eventName, [$subscriber, $params[0]], $params[1] ?? 0);
|
||||
} else {
|
||||
foreach ($params as $listener) {
|
||||
$this->addListener($eventName, [$subscriber, $listener[0]], $listener[1] ?? 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function removeSubscriber(EventSubscriberInterface $subscriber)
|
||||
{
|
||||
foreach ($subscriber->getSubscribedEvents() as $eventName => $params) {
|
||||
if (\is_array($params) && \is_array($params[0])) {
|
||||
foreach ($params as $listener) {
|
||||
$this->removeListener($eventName, [$subscriber, $listener[0]]);
|
||||
}
|
||||
} else {
|
||||
$this->removeListener($eventName, [$subscriber, \is_string($params) ? $params : $params[0]]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers the listeners of an event.
|
||||
*
|
||||
* This method can be overridden to add functionality that is executed
|
||||
* for each listener.
|
||||
*
|
||||
* @param callable[] $listeners The event listeners
|
||||
* @param string $eventName The name of the event to dispatch
|
||||
* @param object $event The event object to pass to the event handlers/listeners
|
||||
*/
|
||||
protected function callListeners(iterable $listeners, string $eventName, object $event)
|
||||
{
|
||||
$stoppable = $event instanceof StoppableEventInterface;
|
||||
|
||||
foreach ($listeners as $listener) {
|
||||
if ($stoppable && $event->isPropagationStopped()) {
|
||||
break;
|
||||
}
|
||||
$listener($event, $eventName, $this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts the internal list of listeners for the given event by priority.
|
||||
*/
|
||||
private function sortListeners(string $eventName)
|
||||
{
|
||||
krsort($this->listeners[$eventName]);
|
||||
$this->sorted[$eventName] = [];
|
||||
|
||||
foreach ($this->listeners[$eventName] as &$listeners) {
|
||||
foreach ($listeners as $k => &$listener) {
|
||||
if (\is_array($listener) && isset($listener[0]) && $listener[0] instanceof \Closure && 2 >= \count($listener)) {
|
||||
$listener[0] = $listener[0]();
|
||||
$listener[1] = $listener[1] ?? '__invoke';
|
||||
}
|
||||
$this->sorted[$eventName][] = $listener;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Optimizes the internal list of listeners for the given event by priority.
|
||||
*/
|
||||
private function optimizeListeners(string $eventName): array
|
||||
{
|
||||
krsort($this->listeners[$eventName]);
|
||||
$this->optimized[$eventName] = [];
|
||||
|
||||
foreach ($this->listeners[$eventName] as &$listeners) {
|
||||
foreach ($listeners as &$listener) {
|
||||
$closure = &$this->optimized[$eventName][];
|
||||
if (\is_array($listener) && isset($listener[0]) && $listener[0] instanceof \Closure && 2 >= \count($listener)) {
|
||||
$closure = static function (...$args) use (&$listener, &$closure) {
|
||||
if ($listener[0] instanceof \Closure) {
|
||||
$listener[0] = $listener[0]();
|
||||
$listener[1] = $listener[1] ?? '__invoke';
|
||||
}
|
||||
($closure = \Closure::fromCallable($listener))(...$args);
|
||||
};
|
||||
} else {
|
||||
$closure = $listener instanceof \Closure || $listener instanceof WrappedListener ? $listener : \Closure::fromCallable($listener);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->optimized[$eventName];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\EventDispatcher;
|
||||
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface as ContractsEventDispatcherInterface;
|
||||
|
||||
/**
|
||||
* The EventDispatcherInterface is the central point of Symfony's event listener system.
|
||||
* Listeners are registered on the manager and events are dispatched through the
|
||||
* manager.
|
||||
*
|
||||
* @author Bernhard Schussek <bschussek@gmail.com>
|
||||
*/
|
||||
interface EventDispatcherInterface extends ContractsEventDispatcherInterface
|
||||
{
|
||||
/**
|
||||
* Adds an event listener that listens on the specified events.
|
||||
*
|
||||
* @param int $priority The higher this value, the earlier an event
|
||||
* listener will be triggered in the chain (defaults to 0)
|
||||
*/
|
||||
public function addListener(string $eventName, callable $listener, int $priority = 0);
|
||||
|
||||
/**
|
||||
* Adds an event subscriber.
|
||||
*
|
||||
* The subscriber is asked for all the events it is
|
||||
* interested in and added as a listener for these events.
|
||||
*/
|
||||
public function addSubscriber(EventSubscriberInterface $subscriber);
|
||||
|
||||
/**
|
||||
* Removes an event listener from the specified events.
|
||||
*/
|
||||
public function removeListener(string $eventName, callable $listener);
|
||||
|
||||
public function removeSubscriber(EventSubscriberInterface $subscriber);
|
||||
|
||||
/**
|
||||
* Gets the listeners of a specific event or all listeners sorted by descending priority.
|
||||
*
|
||||
* @return array<callable[]|callable>
|
||||
*/
|
||||
public function getListeners(?string $eventName = null);
|
||||
|
||||
/**
|
||||
* Gets the listener priority for a specific event.
|
||||
*
|
||||
* Returns null if the event or the listener does not exist.
|
||||
*
|
||||
* @return int|null
|
||||
*/
|
||||
public function getListenerPriority(string $eventName, callable $listener);
|
||||
|
||||
/**
|
||||
* Checks whether an event has any registered listeners.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasListeners(?string $eventName = null);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\EventDispatcher;
|
||||
|
||||
/**
|
||||
* An EventSubscriber knows itself what events it is interested in.
|
||||
* If an EventSubscriber is added to an EventDispatcherInterface, the manager invokes
|
||||
* {@link getSubscribedEvents} and registers the subscriber as a listener for all
|
||||
* returned events.
|
||||
*
|
||||
* @author Guilherme Blanco <guilhermeblanco@hotmail.com>
|
||||
* @author Jonathan Wage <jonwage@gmail.com>
|
||||
* @author Roman Borschel <roman@code-factory.org>
|
||||
* @author Bernhard Schussek <bschussek@gmail.com>
|
||||
*/
|
||||
interface EventSubscriberInterface
|
||||
{
|
||||
/**
|
||||
* Returns an array of event names this subscriber wants to listen to.
|
||||
*
|
||||
* The array keys are event names and the value can be:
|
||||
*
|
||||
* * The method name to call (priority defaults to 0)
|
||||
* * An array composed of the method name to call and the priority
|
||||
* * An array of arrays composed of the method names to call and respective
|
||||
* priorities, or 0 if unset
|
||||
*
|
||||
* For instance:
|
||||
*
|
||||
* * ['eventName' => 'methodName']
|
||||
* * ['eventName' => ['methodName', $priority]]
|
||||
* * ['eventName' => [['methodName1', $priority], ['methodName2']]]
|
||||
*
|
||||
* The code must not depend on runtime state as it will only be called at compile time.
|
||||
* All logic depending on runtime state must be put into the individual methods handling the events.
|
||||
*
|
||||
* @return array<string, string|array{0: string, 1: int}|list<array{0: string, 1?: int}>>
|
||||
*/
|
||||
public static function getSubscribedEvents();
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\EventDispatcher;
|
||||
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
/**
|
||||
* Event encapsulation class.
|
||||
*
|
||||
* Encapsulates events thus decoupling the observer from the subject they encapsulate.
|
||||
*
|
||||
* @author Drak <drak@zikula.org>
|
||||
*
|
||||
* @implements \ArrayAccess<string, mixed>
|
||||
* @implements \IteratorAggregate<string, mixed>
|
||||
*/
|
||||
class GenericEvent extends Event implements \ArrayAccess, \IteratorAggregate
|
||||
{
|
||||
protected $subject;
|
||||
protected $arguments;
|
||||
|
||||
/**
|
||||
* Encapsulate an event with $subject and $arguments.
|
||||
*
|
||||
* @param mixed $subject The subject of the event, usually an object or a callable
|
||||
* @param array $arguments Arguments to store in the event
|
||||
*/
|
||||
public function __construct($subject = null, array $arguments = [])
|
||||
{
|
||||
$this->subject = $subject;
|
||||
$this->arguments = $arguments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter for subject property.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getSubject()
|
||||
{
|
||||
return $this->subject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get argument by key.
|
||||
*
|
||||
* @return mixed
|
||||
*
|
||||
* @throws \InvalidArgumentException if key is not found
|
||||
*/
|
||||
public function getArgument(string $key)
|
||||
{
|
||||
if ($this->hasArgument($key)) {
|
||||
return $this->arguments[$key];
|
||||
}
|
||||
|
||||
throw new \InvalidArgumentException(sprintf('Argument "%s" not found.', $key));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add argument to event.
|
||||
*
|
||||
* @param mixed $value Value
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setArgument(string $key, $value)
|
||||
{
|
||||
$this->arguments[$key] = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter for all arguments.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getArguments()
|
||||
{
|
||||
return $this->arguments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set args property.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setArguments(array $args = [])
|
||||
{
|
||||
$this->arguments = $args;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Has argument.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasArgument(string $key)
|
||||
{
|
||||
return \array_key_exists($key, $this->arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* ArrayAccess for argument getter.
|
||||
*
|
||||
* @param string $key Array key
|
||||
*
|
||||
* @return mixed
|
||||
*
|
||||
* @throws \InvalidArgumentException if key does not exist in $this->args
|
||||
*/
|
||||
#[\ReturnTypeWillChange]
|
||||
public function offsetGet($key)
|
||||
{
|
||||
return $this->getArgument($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* ArrayAccess for argument setter.
|
||||
*
|
||||
* @param string $key Array key to set
|
||||
* @param mixed $value Value
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
#[\ReturnTypeWillChange]
|
||||
public function offsetSet($key, $value)
|
||||
{
|
||||
$this->setArgument($key, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* ArrayAccess for unset argument.
|
||||
*
|
||||
* @param string $key Array key
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
#[\ReturnTypeWillChange]
|
||||
public function offsetUnset($key)
|
||||
{
|
||||
if ($this->hasArgument($key)) {
|
||||
unset($this->arguments[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ArrayAccess has argument.
|
||||
*
|
||||
* @param string $key Array key
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
#[\ReturnTypeWillChange]
|
||||
public function offsetExists($key)
|
||||
{
|
||||
return $this->hasArgument($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* IteratorAggregate for iterating over the object like an array.
|
||||
*
|
||||
* @return \ArrayIterator<string, mixed>
|
||||
*/
|
||||
#[\ReturnTypeWillChange]
|
||||
public function getIterator()
|
||||
{
|
||||
return new \ArrayIterator($this->arguments);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\EventDispatcher;
|
||||
|
||||
/**
|
||||
* A read-only proxy for an event dispatcher.
|
||||
*
|
||||
* @author Bernhard Schussek <bschussek@gmail.com>
|
||||
*/
|
||||
class ImmutableEventDispatcher implements EventDispatcherInterface
|
||||
{
|
||||
private $dispatcher;
|
||||
|
||||
public function __construct(EventDispatcherInterface $dispatcher)
|
||||
{
|
||||
$this->dispatcher = $dispatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function dispatch(object $event, ?string $eventName = null): object
|
||||
{
|
||||
return $this->dispatcher->dispatch($event, $eventName);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function addListener(string $eventName, $listener, int $priority = 0)
|
||||
{
|
||||
throw new \BadMethodCallException('Unmodifiable event dispatchers must not be modified.');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function addSubscriber(EventSubscriberInterface $subscriber)
|
||||
{
|
||||
throw new \BadMethodCallException('Unmodifiable event dispatchers must not be modified.');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function removeListener(string $eventName, $listener)
|
||||
{
|
||||
throw new \BadMethodCallException('Unmodifiable event dispatchers must not be modified.');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function removeSubscriber(EventSubscriberInterface $subscriber)
|
||||
{
|
||||
throw new \BadMethodCallException('Unmodifiable event dispatchers must not be modified.');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getListeners(?string $eventName = null)
|
||||
{
|
||||
return $this->dispatcher->getListeners($eventName);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getListenerPriority(string $eventName, $listener)
|
||||
{
|
||||
return $this->dispatcher->getListenerPriority($eventName, $listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function hasListeners(?string $eventName = null)
|
||||
{
|
||||
return $this->dispatcher->hasListeners($eventName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
Copyright (c) 2004-present Fabien Potencier
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is furnished
|
||||
to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\EventDispatcher;
|
||||
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
trigger_deprecation('symfony/event-dispatcher', '5.1', '%s is deprecated, use the event dispatcher without the proxy.', LegacyEventDispatcherProxy::class);
|
||||
|
||||
/**
|
||||
* A helper class to provide BC/FC with the legacy signature of EventDispatcherInterface::dispatch().
|
||||
*
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*
|
||||
* @deprecated since Symfony 5.1
|
||||
*/
|
||||
final class LegacyEventDispatcherProxy
|
||||
{
|
||||
public static function decorate(?EventDispatcherInterface $dispatcher): ?EventDispatcherInterface
|
||||
{
|
||||
return $dispatcher;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
EventDispatcher Component
|
||||
=========================
|
||||
|
||||
The EventDispatcher component provides tools that allow your application
|
||||
components to communicate with each other by dispatching events and listening to
|
||||
them.
|
||||
|
||||
Resources
|
||||
---------
|
||||
|
||||
* [Documentation](https://symfony.com/doc/current/components/event_dispatcher.html)
|
||||
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
|
||||
* [Report issues](https://github.com/symfony/symfony/issues) and
|
||||
[send Pull Requests](https://github.com/symfony/symfony/pulls)
|
||||
in the [main Symfony repository](https://github.com/symfony/symfony)
|
||||
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"name": "symfony/event-dispatcher",
|
||||
"type": "library",
|
||||
"description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them",
|
||||
"keywords": [],
|
||||
"homepage": "https://symfony.com",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=7.2.5",
|
||||
"symfony/deprecation-contracts": "^2.1|^3",
|
||||
"symfony/event-dispatcher-contracts": "^2|^3",
|
||||
"symfony/polyfill-php80": "^1.16"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/dependency-injection": "^4.4|^5.0|^6.0",
|
||||
"symfony/expression-language": "^4.4|^5.0|^6.0",
|
||||
"symfony/config": "^4.4|^5.0|^6.0",
|
||||
"symfony/error-handler": "^4.4|^5.0|^6.0",
|
||||
"symfony/http-foundation": "^4.4|^5.0|^6.0",
|
||||
"symfony/service-contracts": "^1.1|^2|^3",
|
||||
"symfony/stopwatch": "^4.4|^5.0|^6.0",
|
||||
"psr/log": "^1|^2|^3"
|
||||
},
|
||||
"conflict": {
|
||||
"symfony/dependency-injection": "<4.4"
|
||||
},
|
||||
"provide": {
|
||||
"psr/event-dispatcher-implementation": "1.0",
|
||||
"symfony/event-dispatcher-implementation": "2.0"
|
||||
},
|
||||
"suggest": {
|
||||
"symfony/dependency-injection": "",
|
||||
"symfony/http-kernel": ""
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": { "Symfony\\Component\\EventDispatcher\\": "" },
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"minimum-stability": "dev"
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
CHANGELOG
|
||||
=========
|
||||
|
||||
5.4.21
|
||||
------
|
||||
|
||||
* [BC BREAK] The following data providers for `TransportFactoryTestCase` are now static:
|
||||
`supportsProvider()`, `createProvider()`, `unsupportedSchemeProvider()`and `incompleteDsnProvider()`
|
||||
|
||||
5.4
|
||||
---
|
||||
|
||||
* Enable the mailer to operate on any PSR-14-compatible event dispatcher
|
||||
|
||||
5.3
|
||||
---
|
||||
|
||||
* added the `mailer` monolog channel and set it on all transport definitions
|
||||
|
||||
5.2.0
|
||||
-----
|
||||
|
||||
* added `NativeTransportFactory` to configure a transport based on php.ini settings
|
||||
* added `local_domain`, `restart_threshold`, `restart_threshold_sleep` and `ping_threshold` options for `smtp`
|
||||
* added `command` option for `sendmail`
|
||||
|
||||
4.4.0
|
||||
-----
|
||||
|
||||
* [BC BREAK] changed the `NullTransport` DSN from `smtp://null` to `null://null`
|
||||
* [BC BREAK] renamed `SmtpEnvelope` to `Envelope`, renamed `DelayedSmtpEnvelope` to
|
||||
`DelayedEnvelope`
|
||||
* [BC BREAK] changed the syntax for failover and roundrobin DSNs
|
||||
|
||||
Before:
|
||||
|
||||
dummy://a || dummy://b (for failover)
|
||||
dummy://a && dummy://b (for roundrobin)
|
||||
|
||||
After:
|
||||
|
||||
failover(dummy://a dummy://b)
|
||||
roundrobin(dummy://a dummy://b)
|
||||
|
||||
* added support for multiple transports on a `Mailer` instance
|
||||
* [BC BREAK] removed the `auth_mode` DSN option (it is now always determined automatically)
|
||||
* STARTTLS cannot be enabled anymore (it is used automatically if TLS is disabled and the server supports STARTTLS)
|
||||
* [BC BREAK] Removed the `encryption` DSN option (use `smtps` instead)
|
||||
* Added support for the `smtps` protocol (does the same as using `smtp` and port `465`)
|
||||
* Added PHPUnit constraints
|
||||
* Added `MessageDataCollector`
|
||||
* Added `MessageEvents` and `MessageLoggerListener` to allow collecting sent emails
|
||||
* [BC BREAK] `TransportInterface` has a new `__toString()` method
|
||||
* [BC BREAK] Classes `AbstractApiTransport` and `AbstractHttpTransport` moved under `Transport` sub-namespace.
|
||||
* [BC BREAK] Transports depend on `Symfony\Contracts\EventDispatcher\EventDispatcherInterface`
|
||||
instead of `Symfony\Component\EventDispatcher\EventDispatcherInterface`.
|
||||
* Added possibility to register custom transport for dsn by implementing
|
||||
`Symfony\Component\Mailer\Transport\TransportFactoryInterface` and tagging with `mailer.transport_factory` tag in DI.
|
||||
* Added `Symfony\Component\Mailer\Test\TransportFactoryTestCase` to ease testing custom transport factories.
|
||||
* Added `SentMessage::getDebug()` and `TransportExceptionInterface::getDebug` to help debugging
|
||||
* Made `MessageEvent` final
|
||||
* add DSN parameter `verify_peer` to disable TLS peer verification for SMTP transport
|
||||
|
||||
4.3.0
|
||||
-----
|
||||
|
||||
* Added the component.
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Mailer\DataCollector;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\DataCollector\DataCollector;
|
||||
use Symfony\Component\Mailer\Event\MessageEvents;
|
||||
use Symfony\Component\Mailer\EventListener\MessageLoggerListener;
|
||||
|
||||
/**
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
final class MessageDataCollector extends DataCollector
|
||||
{
|
||||
private $events;
|
||||
|
||||
public function __construct(MessageLoggerListener $logger)
|
||||
{
|
||||
$this->events = $logger->getEvents();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function collect(Request $request, Response $response, ?\Throwable $exception = null)
|
||||
{
|
||||
$this->data['events'] = $this->events;
|
||||
}
|
||||
|
||||
public function getEvents(): MessageEvents
|
||||
{
|
||||
return $this->data['events'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function base64Encode(string $data): string
|
||||
{
|
||||
return base64_encode($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function reset()
|
||||
{
|
||||
$this->data = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return 'mailer';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Mailer;
|
||||
|
||||
use Symfony\Component\Mailer\Exception\LogicException;
|
||||
use Symfony\Component\Mime\Address;
|
||||
use Symfony\Component\Mime\Header\Headers;
|
||||
use Symfony\Component\Mime\Message;
|
||||
|
||||
/**
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class DelayedEnvelope extends Envelope
|
||||
{
|
||||
private $senderSet = false;
|
||||
private $recipientsSet = false;
|
||||
private $message;
|
||||
|
||||
public function __construct(Message $message)
|
||||
{
|
||||
$this->message = $message;
|
||||
}
|
||||
|
||||
public function setSender(Address $sender): void
|
||||
{
|
||||
parent::setSender($sender);
|
||||
|
||||
$this->senderSet = true;
|
||||
}
|
||||
|
||||
public function getSender(): Address
|
||||
{
|
||||
if (!$this->senderSet) {
|
||||
parent::setSender(self::getSenderFromHeaders($this->message->getHeaders()));
|
||||
}
|
||||
|
||||
return parent::getSender();
|
||||
}
|
||||
|
||||
public function setRecipients(array $recipients): void
|
||||
{
|
||||
parent::setRecipients($recipients);
|
||||
|
||||
$this->recipientsSet = parent::getRecipients();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Address[]
|
||||
*/
|
||||
public function getRecipients(): array
|
||||
{
|
||||
if ($this->recipientsSet) {
|
||||
return parent::getRecipients();
|
||||
}
|
||||
|
||||
return self::getRecipientsFromHeaders($this->message->getHeaders());
|
||||
}
|
||||
|
||||
private static function getRecipientsFromHeaders(Headers $headers): array
|
||||
{
|
||||
$recipients = [];
|
||||
foreach (['to', 'cc', 'bcc'] as $name) {
|
||||
foreach ($headers->all($name) as $header) {
|
||||
foreach ($header->getAddresses() as $address) {
|
||||
$recipients[] = $address;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $recipients;
|
||||
}
|
||||
|
||||
private static function getSenderFromHeaders(Headers $headers): Address
|
||||
{
|
||||
if ($sender = $headers->get('Sender')) {
|
||||
return $sender->getAddress();
|
||||
}
|
||||
if ($return = $headers->get('Return-Path')) {
|
||||
return $return->getAddress();
|
||||
}
|
||||
if ($from = $headers->get('From')) {
|
||||
return $from->getAddresses()[0];
|
||||
}
|
||||
|
||||
throw new LogicException('Unable to determine the sender of the message.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Mailer;
|
||||
|
||||
use Symfony\Component\Mailer\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Mailer\Exception\LogicException;
|
||||
use Symfony\Component\Mime\Address;
|
||||
use Symfony\Component\Mime\RawMessage;
|
||||
|
||||
/**
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
class Envelope
|
||||
{
|
||||
private $sender;
|
||||
private $recipients = [];
|
||||
|
||||
/**
|
||||
* @param Address[] $recipients
|
||||
*/
|
||||
public function __construct(Address $sender, array $recipients)
|
||||
{
|
||||
$this->setSender($sender);
|
||||
$this->setRecipients($recipients);
|
||||
}
|
||||
|
||||
public static function create(RawMessage $message): self
|
||||
{
|
||||
if (RawMessage::class === \get_class($message)) {
|
||||
throw new LogicException('Cannot send a RawMessage instance without an explicit Envelope.');
|
||||
}
|
||||
|
||||
return new DelayedEnvelope($message);
|
||||
}
|
||||
|
||||
public function setSender(Address $sender): void
|
||||
{
|
||||
// to ensure deliverability of bounce emails independent of UTF-8 capabilities of SMTP servers
|
||||
if (!preg_match('/^[^@\x80-\xFF]++@/', $sender->getAddress())) {
|
||||
throw new InvalidArgumentException(sprintf('Invalid sender "%s": non-ASCII characters not supported in local-part of email.', $sender->getAddress()));
|
||||
}
|
||||
$this->sender = $sender;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Address Returns a "mailbox" as specified by RFC 2822
|
||||
* Must be converted to an "addr-spec" when used as a "MAIL FROM" value in SMTP (use getAddress())
|
||||
*/
|
||||
public function getSender(): Address
|
||||
{
|
||||
return $this->sender;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Address[] $recipients
|
||||
*/
|
||||
public function setRecipients(array $recipients): void
|
||||
{
|
||||
if (!$recipients) {
|
||||
throw new InvalidArgumentException('An envelope must have at least one recipient.');
|
||||
}
|
||||
|
||||
$this->recipients = [];
|
||||
foreach ($recipients as $recipient) {
|
||||
if (!$recipient instanceof Address) {
|
||||
throw new InvalidArgumentException(sprintf('A recipient must be an instance of "%s" (got "%s").', Address::class, get_debug_type($recipient)));
|
||||
}
|
||||
$this->recipients[] = new Address($recipient->getAddress());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Address[]
|
||||
*/
|
||||
public function getRecipients(): array
|
||||
{
|
||||
return $this->recipients;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Mailer\Event;
|
||||
|
||||
use Symfony\Component\Mailer\Envelope;
|
||||
use Symfony\Component\Mime\RawMessage;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
/**
|
||||
* Allows the transformation of a Message and the Envelope before the email is sent.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
final class MessageEvent extends Event
|
||||
{
|
||||
private $message;
|
||||
private $envelope;
|
||||
private $transport;
|
||||
private $queued;
|
||||
|
||||
public function __construct(RawMessage $message, Envelope $envelope, string $transport, bool $queued = false)
|
||||
{
|
||||
$this->message = $message;
|
||||
$this->envelope = $envelope;
|
||||
$this->transport = $transport;
|
||||
$this->queued = $queued;
|
||||
}
|
||||
|
||||
public function getMessage(): RawMessage
|
||||
{
|
||||
return $this->message;
|
||||
}
|
||||
|
||||
public function setMessage(RawMessage $message): void
|
||||
{
|
||||
$this->message = $message;
|
||||
}
|
||||
|
||||
public function getEnvelope(): Envelope
|
||||
{
|
||||
return $this->envelope;
|
||||
}
|
||||
|
||||
public function setEnvelope(Envelope $envelope): void
|
||||
{
|
||||
$this->envelope = $envelope;
|
||||
}
|
||||
|
||||
public function getTransport(): string
|
||||
{
|
||||
return $this->transport;
|
||||
}
|
||||
|
||||
public function isQueued(): bool
|
||||
{
|
||||
return $this->queued;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Mailer\Event;
|
||||
|
||||
use Symfony\Component\Mime\RawMessage;
|
||||
|
||||
/**
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
class MessageEvents
|
||||
{
|
||||
private $events = [];
|
||||
private $transports = [];
|
||||
|
||||
public function add(MessageEvent $event): void
|
||||
{
|
||||
$this->events[] = $event;
|
||||
$this->transports[$event->getTransport()] = true;
|
||||
}
|
||||
|
||||
public function getTransports(): array
|
||||
{
|
||||
return array_keys($this->transports);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return MessageEvent[]
|
||||
*/
|
||||
public function getEvents(?string $name = null): array
|
||||
{
|
||||
if (null === $name) {
|
||||
return $this->events;
|
||||
}
|
||||
|
||||
$events = [];
|
||||
foreach ($this->events as $event) {
|
||||
if ($name === $event->getTransport()) {
|
||||
$events[] = $event;
|
||||
}
|
||||
}
|
||||
|
||||
return $events;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return RawMessage[]
|
||||
*/
|
||||
public function getMessages(?string $name = null): array
|
||||
{
|
||||
$events = $this->getEvents($name);
|
||||
$messages = [];
|
||||
foreach ($events as $event) {
|
||||
$messages[] = $event->getMessage();
|
||||
}
|
||||
|
||||
return $messages;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Mailer\EventListener;
|
||||
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Mailer\Event\MessageEvent;
|
||||
use Symfony\Component\Mime\Address;
|
||||
use Symfony\Component\Mime\Message;
|
||||
|
||||
/**
|
||||
* Manipulates the Envelope of a Message.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
class EnvelopeListener implements EventSubscriberInterface
|
||||
{
|
||||
private $sender;
|
||||
private $recipients;
|
||||
|
||||
/**
|
||||
* @param Address|string $sender
|
||||
* @param array<Address|string> $recipients
|
||||
*/
|
||||
public function __construct($sender = null, ?array $recipients = null)
|
||||
{
|
||||
if (null !== $sender) {
|
||||
$this->sender = Address::create($sender);
|
||||
}
|
||||
if (null !== $recipients) {
|
||||
$this->recipients = Address::createArray($recipients);
|
||||
}
|
||||
}
|
||||
|
||||
public function onMessage(MessageEvent $event): void
|
||||
{
|
||||
if ($this->sender) {
|
||||
$event->getEnvelope()->setSender($this->sender);
|
||||
|
||||
$message = $event->getMessage();
|
||||
if ($message instanceof Message) {
|
||||
if (!$message->getHeaders()->has('Sender') && !$message->getHeaders()->has('From')) {
|
||||
$message->getHeaders()->addMailboxHeader('Sender', $this->sender);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->recipients) {
|
||||
$event->getEnvelope()->setRecipients($this->recipients);
|
||||
}
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents()
|
||||
{
|
||||
return [
|
||||
// should be the last one to allow header changes by other listeners first
|
||||
MessageEvent::class => ['onMessage', -255],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Mailer\EventListener;
|
||||
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Mailer\Event\MessageEvent;
|
||||
use Symfony\Component\Mailer\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Mailer\Exception\RuntimeException;
|
||||
use Symfony\Component\Mime\BodyRendererInterface;
|
||||
use Symfony\Component\Mime\Header\Headers;
|
||||
use Symfony\Component\Mime\Header\MailboxListHeader;
|
||||
use Symfony\Component\Mime\Message;
|
||||
|
||||
/**
|
||||
* Manipulates the headers and the body of a Message.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
class MessageListener implements EventSubscriberInterface
|
||||
{
|
||||
public const HEADER_SET_IF_EMPTY = 1;
|
||||
public const HEADER_ADD = 2;
|
||||
public const HEADER_REPLACE = 3;
|
||||
public const DEFAULT_RULES = [
|
||||
'from' => self::HEADER_SET_IF_EMPTY,
|
||||
'return-path' => self::HEADER_SET_IF_EMPTY,
|
||||
'reply-to' => self::HEADER_ADD,
|
||||
'to' => self::HEADER_SET_IF_EMPTY,
|
||||
'cc' => self::HEADER_ADD,
|
||||
'bcc' => self::HEADER_ADD,
|
||||
];
|
||||
|
||||
private $headers;
|
||||
private $headerRules = [];
|
||||
private $renderer;
|
||||
|
||||
public function __construct(?Headers $headers = null, ?BodyRendererInterface $renderer = null, array $headerRules = self::DEFAULT_RULES)
|
||||
{
|
||||
$this->headers = $headers;
|
||||
$this->renderer = $renderer;
|
||||
foreach ($headerRules as $headerName => $rule) {
|
||||
$this->addHeaderRule($headerName, $rule);
|
||||
}
|
||||
}
|
||||
|
||||
public function addHeaderRule(string $headerName, int $rule): void
|
||||
{
|
||||
if ($rule < 1 || $rule > 3) {
|
||||
throw new InvalidArgumentException(sprintf('The "%d" rule is not supported.', $rule));
|
||||
}
|
||||
|
||||
$this->headerRules[strtolower($headerName)] = $rule;
|
||||
}
|
||||
|
||||
public function onMessage(MessageEvent $event): void
|
||||
{
|
||||
$message = $event->getMessage();
|
||||
if (!$message instanceof Message) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->setHeaders($message);
|
||||
$this->renderMessage($message);
|
||||
}
|
||||
|
||||
private function setHeaders(Message $message): void
|
||||
{
|
||||
if (!$this->headers) {
|
||||
return;
|
||||
}
|
||||
|
||||
$headers = $message->getHeaders();
|
||||
foreach ($this->headers->all() as $name => $header) {
|
||||
if (!$headers->has($name)) {
|
||||
$headers->add($header);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
switch ($this->headerRules[$name] ?? self::HEADER_SET_IF_EMPTY) {
|
||||
case self::HEADER_SET_IF_EMPTY:
|
||||
break;
|
||||
|
||||
case self::HEADER_REPLACE:
|
||||
$headers->remove($name);
|
||||
$headers->add($header);
|
||||
|
||||
break;
|
||||
|
||||
case self::HEADER_ADD:
|
||||
if (!Headers::isUniqueHeader($name)) {
|
||||
$headers->add($header);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
$h = $headers->get($name);
|
||||
if (!$h instanceof MailboxListHeader) {
|
||||
throw new RuntimeException(sprintf('Unable to set header "%s".', $name));
|
||||
}
|
||||
|
||||
Headers::checkHeaderClass($header);
|
||||
foreach ($header->getAddresses() as $address) {
|
||||
$h->addAddress($address);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function renderMessage(Message $message): void
|
||||
{
|
||||
if (!$this->renderer) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->renderer->render($message);
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents()
|
||||
{
|
||||
return [
|
||||
MessageEvent::class => 'onMessage',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Mailer\EventListener;
|
||||
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Mailer\Event\MessageEvent;
|
||||
use Symfony\Component\Mailer\Event\MessageEvents;
|
||||
use Symfony\Contracts\Service\ResetInterface;
|
||||
|
||||
/**
|
||||
* Logs Messages.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
class MessageLoggerListener implements EventSubscriberInterface, ResetInterface
|
||||
{
|
||||
private $events;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->events = new MessageEvents();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function reset()
|
||||
{
|
||||
$this->events = new MessageEvents();
|
||||
}
|
||||
|
||||
public function onMessage(MessageEvent $event): void
|
||||
{
|
||||
$this->events->add($event);
|
||||
}
|
||||
|
||||
public function getEvents(): MessageEvents
|
||||
{
|
||||
return $this->events;
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents()
|
||||
{
|
||||
return [
|
||||
MessageEvent::class => ['onMessage', -255],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Mailer\Exception;
|
||||
|
||||
/**
|
||||
* Exception interface for all exceptions thrown by the component.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
interface ExceptionInterface extends \Throwable
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Mailer\Exception;
|
||||
|
||||
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||
|
||||
/**
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
class HttpTransportException extends TransportException
|
||||
{
|
||||
private $response;
|
||||
|
||||
public function __construct(?string $message, ResponseInterface $response, int $code = 0, ?\Throwable $previous = null)
|
||||
{
|
||||
if (null === $message) {
|
||||
trigger_deprecation('symfony/mailer', '5.3', 'Passing null as $message to "%s()" is deprecated, pass an empty string instead.', __METHOD__);
|
||||
|
||||
$message = '';
|
||||
}
|
||||
|
||||
parent::__construct($message, $code, $previous);
|
||||
|
||||
$this->response = $response;
|
||||
}
|
||||
|
||||
public function getResponse(): ResponseInterface
|
||||
{
|
||||
return $this->response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Mailer\Exception;
|
||||
|
||||
/**
|
||||
* @author Konstantin Myakshin <molodchick@gmail.com>
|
||||
*/
|
||||
class IncompleteDsnException extends InvalidArgumentException
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Mailer\Exception;
|
||||
|
||||
/**
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Mailer\Exception;
|
||||
|
||||
/**
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
class LogicException extends \LogicException implements ExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Mailer\Exception;
|
||||
|
||||
/**
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
class RuntimeException extends \RuntimeException implements ExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Mailer\Exception;
|
||||
|
||||
/**
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
class TransportException extends RuntimeException implements TransportExceptionInterface
|
||||
{
|
||||
private $debug = '';
|
||||
|
||||
public function getDebug(): string
|
||||
{
|
||||
return $this->debug;
|
||||
}
|
||||
|
||||
public function appendDebug(string $debug): void
|
||||
{
|
||||
$this->debug .= $debug;
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Mailer\Exception;
|
||||
|
||||
/**
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
interface TransportExceptionInterface extends ExceptionInterface
|
||||
{
|
||||
public function getDebug(): string;
|
||||
|
||||
public function appendDebug(string $debug): void;
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Mailer\Exception;
|
||||
|
||||
use Symfony\Component\Mailer\Bridge;
|
||||
use Symfony\Component\Mailer\Transport\Dsn;
|
||||
|
||||
/**
|
||||
* @author Konstantin Myakshin <molodchick@gmail.com>
|
||||
*/
|
||||
class UnsupportedSchemeException extends LogicException
|
||||
{
|
||||
private const SCHEME_TO_PACKAGE_MAP = [
|
||||
'gmail' => [
|
||||
'class' => Bridge\Google\Transport\GmailTransportFactory::class,
|
||||
'package' => 'symfony/google-mailer',
|
||||
],
|
||||
'mailgun' => [
|
||||
'class' => Bridge\Mailgun\Transport\MailgunTransportFactory::class,
|
||||
'package' => 'symfony/mailgun-mailer',
|
||||
],
|
||||
'mailjet' => [
|
||||
'class' => Bridge\Mailjet\Transport\MailjetTransportFactory::class,
|
||||
'package' => 'symfony/mailjet-mailer',
|
||||
],
|
||||
'mandrill' => [
|
||||
'class' => Bridge\Mailchimp\Transport\MandrillTransportFactory::class,
|
||||
'package' => 'symfony/mailchimp-mailer',
|
||||
],
|
||||
'ohmysmtp' => [
|
||||
'class' => Bridge\OhMySmtp\Transport\OhMySmtpTransportFactory::class,
|
||||
'package' => 'symfony/oh-my-smtp-mailer',
|
||||
],
|
||||
'postmark' => [
|
||||
'class' => Bridge\Postmark\Transport\PostmarkTransportFactory::class,
|
||||
'package' => 'symfony/postmark-mailer',
|
||||
],
|
||||
'sendgrid' => [
|
||||
'class' => Bridge\Sendgrid\Transport\SendgridTransportFactory::class,
|
||||
'package' => 'symfony/sendgrid-mailer',
|
||||
],
|
||||
'sendinblue' => [
|
||||
'class' => Bridge\Sendinblue\Transport\SendinblueTransportFactory::class,
|
||||
'package' => 'symfony/sendinblue-mailer',
|
||||
],
|
||||
'ses' => [
|
||||
'class' => Bridge\Amazon\Transport\SesTransportFactory::class,
|
||||
'package' => 'symfony/amazon-mailer',
|
||||
],
|
||||
];
|
||||
|
||||
public function __construct(Dsn $dsn, ?string $name = null, array $supported = [])
|
||||
{
|
||||
$provider = $dsn->getScheme();
|
||||
if (false !== $pos = strpos($provider, '+')) {
|
||||
$provider = substr($provider, 0, $pos);
|
||||
}
|
||||
$package = self::SCHEME_TO_PACKAGE_MAP[$provider] ?? null;
|
||||
if ($package && !class_exists($package['class'])) {
|
||||
parent::__construct(sprintf('Unable to send emails via "%s" as the bridge is not installed; try running "composer require %s".', $provider, $package['package']));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$message = sprintf('The "%s" scheme is not supported', $dsn->getScheme());
|
||||
if ($name && $supported) {
|
||||
$message .= sprintf('; supported schemes for mailer "%s" are: "%s"', $name, implode('", "', $supported));
|
||||
}
|
||||
|
||||
parent::__construct($message.'.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Mailer\Header;
|
||||
|
||||
use Symfony\Component\Mime\Header\UnstructuredHeader;
|
||||
|
||||
/**
|
||||
* @author Kevin Bond <kevinbond@gmail.com>
|
||||
*/
|
||||
final class MetadataHeader extends UnstructuredHeader
|
||||
{
|
||||
private $key;
|
||||
|
||||
public function __construct(string $key, string $value)
|
||||
{
|
||||
$this->key = $key;
|
||||
|
||||
parent::__construct('X-Metadata-'.$key, $value);
|
||||
}
|
||||
|
||||
public function getKey(): string
|
||||
{
|
||||
return $this->key;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Mailer\Header;
|
||||
|
||||
use Symfony\Component\Mime\Header\UnstructuredHeader;
|
||||
|
||||
/**
|
||||
* @author Kevin Bond <kevinbond@gmail.com>
|
||||
*/
|
||||
final class TagHeader extends UnstructuredHeader
|
||||
{
|
||||
public function __construct(string $value)
|
||||
{
|
||||
parent::__construct('X-Tag', $value);
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
Copyright (c) 2019-present Fabien Potencier
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is furnished
|
||||
to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Mailer;
|
||||
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\EventDispatcher\Event;
|
||||
use Symfony\Component\EventDispatcher\LegacyEventDispatcherProxy;
|
||||
use Symfony\Component\Mailer\Event\MessageEvent;
|
||||
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
|
||||
use Symfony\Component\Mailer\Messenger\SendEmailMessage;
|
||||
use Symfony\Component\Mailer\Transport\TransportInterface;
|
||||
use Symfony\Component\Messenger\Exception\HandlerFailedException;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
use Symfony\Component\Mime\RawMessage;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface as SymfonyEventDispatcherInterface;
|
||||
|
||||
/**
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
final class Mailer implements MailerInterface
|
||||
{
|
||||
private $transport;
|
||||
private $bus;
|
||||
private $dispatcher;
|
||||
|
||||
public function __construct(TransportInterface $transport, ?MessageBusInterface $bus = null, ?EventDispatcherInterface $dispatcher = null)
|
||||
{
|
||||
$this->transport = $transport;
|
||||
$this->bus = $bus;
|
||||
$this->dispatcher = class_exists(Event::class) && $dispatcher instanceof SymfonyEventDispatcherInterface ? LegacyEventDispatcherProxy::decorate($dispatcher) : $dispatcher;
|
||||
}
|
||||
|
||||
public function send(RawMessage $message, ?Envelope $envelope = null): void
|
||||
{
|
||||
if (null === $this->bus) {
|
||||
$this->transport->send($message, $envelope);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (null !== $this->dispatcher) {
|
||||
// The dispatched event here has `queued` set to `true`; the goal is NOT to render the message, but to let
|
||||
// listeners do something before a message is sent to the queue.
|
||||
// We are using a cloned message as we still want to dispatch the **original** message, not the one modified by listeners.
|
||||
// That's because the listeners will run again when the email is sent via Messenger by the transport (see `AbstractTransport`).
|
||||
// Listeners should act depending on the `$queued` argument of the `MessageEvent` instance.
|
||||
$clonedMessage = clone $message;
|
||||
$clonedEnvelope = null !== $envelope ? clone $envelope : Envelope::create($clonedMessage);
|
||||
$event = new MessageEvent($clonedMessage, $clonedEnvelope, (string) $this->transport, true);
|
||||
$this->dispatcher->dispatch($event);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->bus->dispatch(new SendEmailMessage($message, $envelope));
|
||||
} catch (HandlerFailedException $e) {
|
||||
foreach ($e->getNestedExceptions() as $nested) {
|
||||
if ($nested instanceof TransportExceptionInterface) {
|
||||
throw $nested;
|
||||
}
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Mailer;
|
||||
|
||||
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
|
||||
use Symfony\Component\Mime\RawMessage;
|
||||
|
||||
/**
|
||||
* Interface for mailers able to send emails synchronously and/or asynchronously.
|
||||
*
|
||||
* Implementations must support synchronous and asynchronous sending.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
interface MailerInterface
|
||||
{
|
||||
/**
|
||||
* @throws TransportExceptionInterface
|
||||
*/
|
||||
public function send(RawMessage $message, ?Envelope $envelope = null): void;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Mailer\Messenger;
|
||||
|
||||
use Symfony\Component\Mailer\SentMessage;
|
||||
use Symfony\Component\Mailer\Transport\TransportInterface;
|
||||
|
||||
/**
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
class MessageHandler
|
||||
{
|
||||
private $transport;
|
||||
|
||||
public function __construct(TransportInterface $transport)
|
||||
{
|
||||
$this->transport = $transport;
|
||||
}
|
||||
|
||||
public function __invoke(SendEmailMessage $message): ?SentMessage
|
||||
{
|
||||
return $this->transport->send($message->getMessage(), $message->getEnvelope());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Mailer\Messenger;
|
||||
|
||||
use Symfony\Component\Mailer\Envelope;
|
||||
use Symfony\Component\Mime\RawMessage;
|
||||
|
||||
/**
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
class SendEmailMessage
|
||||
{
|
||||
private $message;
|
||||
private $envelope;
|
||||
|
||||
public function __construct(RawMessage $message, ?Envelope $envelope = null)
|
||||
{
|
||||
$this->message = $message;
|
||||
$this->envelope = $envelope;
|
||||
}
|
||||
|
||||
public function getMessage(): RawMessage
|
||||
{
|
||||
return $this->message;
|
||||
}
|
||||
|
||||
public function getEnvelope(): ?Envelope
|
||||
{
|
||||
return $this->envelope;
|
||||
}
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
Mailer Component
|
||||
================
|
||||
|
||||
The Mailer component helps sending emails.
|
||||
|
||||
Getting Started
|
||||
---------------
|
||||
|
||||
```
|
||||
$ composer require symfony/mailer
|
||||
```
|
||||
|
||||
```php
|
||||
use Symfony\Component\Mailer\Transport;
|
||||
use Symfony\Component\Mailer\Mailer;
|
||||
use Symfony\Component\Mime\Email;
|
||||
|
||||
$transport = Transport::fromDsn('smtp://localhost');
|
||||
$mailer = new Mailer($transport);
|
||||
|
||||
$email = (new Email())
|
||||
->from('hello@example.com')
|
||||
->to('you@example.com')
|
||||
//->cc('cc@example.com')
|
||||
//->bcc('bcc@example.com')
|
||||
//->replyTo('fabien@example.com')
|
||||
//->priority(Email::PRIORITY_HIGH)
|
||||
->subject('Time for Symfony Mailer!')
|
||||
->text('Sending emails is fun again!')
|
||||
->html('<p>See Twig integration for better HTML integration!</p>');
|
||||
|
||||
$mailer->send($email);
|
||||
```
|
||||
|
||||
To enable the Twig integration of the Mailer, require `symfony/twig-bridge` and
|
||||
set up the `BodyRenderer`:
|
||||
|
||||
```php
|
||||
use Symfony\Bridge\Twig\Mime\BodyRenderer;
|
||||
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcher;
|
||||
use Symfony\Component\Mailer\EventListener\MessageListener;
|
||||
use Symfony\Component\Mailer\Mailer;
|
||||
use Symfony\Component\Mailer\Transport;
|
||||
use Twig\Environment as TwigEnvironment;
|
||||
|
||||
$twig = new TwigEnvironment(...);
|
||||
$messageListener = new MessageListener(null, new BodyRenderer($twig));
|
||||
|
||||
$eventDispatcher = new EventDispatcher();
|
||||
$eventDispatcher->addSubscriber($messageListener);
|
||||
|
||||
$transport = Transport::fromDsn('smtp://localhost', $eventDispatcher);
|
||||
$mailer = new Mailer($transport, null, $eventDispatcher);
|
||||
|
||||
$email = (new TemplatedEmail())
|
||||
// ...
|
||||
->htmlTemplate('emails/signup.html.twig')
|
||||
->context([
|
||||
'expiration_date' => new \DateTime('+7 days'),
|
||||
'username' => 'foo',
|
||||
])
|
||||
;
|
||||
$mailer->send($email);
|
||||
```
|
||||
|
||||
Resources
|
||||
---------
|
||||
|
||||
* [Documentation](https://symfony.com/doc/current/mailer.html)
|
||||
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
|
||||
* [Report issues](https://github.com/symfony/symfony/issues) and
|
||||
[send Pull Requests](https://github.com/symfony/symfony/pulls)
|
||||
in the [main Symfony repository](https://github.com/symfony/symfony)
|
||||
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Mailer;
|
||||
|
||||
use Symfony\Component\Mime\Message;
|
||||
use Symfony\Component\Mime\RawMessage;
|
||||
|
||||
/**
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
class SentMessage
|
||||
{
|
||||
private $original;
|
||||
private $raw;
|
||||
private $envelope;
|
||||
private $messageId;
|
||||
private $debug = '';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function __construct(RawMessage $message, Envelope $envelope)
|
||||
{
|
||||
$message->ensureValidity();
|
||||
|
||||
$this->original = $message;
|
||||
$this->envelope = $envelope;
|
||||
|
||||
if ($message instanceof Message) {
|
||||
$message = clone $message;
|
||||
$headers = $message->getHeaders();
|
||||
if (!$headers->has('Message-ID')) {
|
||||
$headers->addIdHeader('Message-ID', $message->generateMessageId());
|
||||
}
|
||||
$this->messageId = $headers->get('Message-ID')->getId();
|
||||
$this->raw = new RawMessage($message->toIterable());
|
||||
} else {
|
||||
$this->raw = $message;
|
||||
}
|
||||
}
|
||||
|
||||
public function getMessage(): RawMessage
|
||||
{
|
||||
return $this->raw;
|
||||
}
|
||||
|
||||
public function getOriginalMessage(): RawMessage
|
||||
{
|
||||
return $this->original;
|
||||
}
|
||||
|
||||
public function getEnvelope(): Envelope
|
||||
{
|
||||
return $this->envelope;
|
||||
}
|
||||
|
||||
public function setMessageId(string $id): void
|
||||
{
|
||||
$this->messageId = $id;
|
||||
}
|
||||
|
||||
public function getMessageId(): string
|
||||
{
|
||||
return $this->messageId;
|
||||
}
|
||||
|
||||
public function getDebug(): string
|
||||
{
|
||||
return $this->debug;
|
||||
}
|
||||
|
||||
public function appendDebug(string $debug): void
|
||||
{
|
||||
$this->debug .= $debug;
|
||||
}
|
||||
|
||||
public function toString(): string
|
||||
{
|
||||
return $this->raw->toString();
|
||||
}
|
||||
|
||||
public function toIterable(): iterable
|
||||
{
|
||||
return $this->raw->toIterable();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Mailer\Test\Constraint;
|
||||
|
||||
use PHPUnit\Framework\Constraint\Constraint;
|
||||
use Symfony\Component\Mailer\Event\MessageEvents;
|
||||
|
||||
final class EmailCount extends Constraint
|
||||
{
|
||||
private $expectedValue;
|
||||
private $transport;
|
||||
private $queued;
|
||||
|
||||
public function __construct(int $expectedValue, ?string $transport = null, bool $queued = false)
|
||||
{
|
||||
$this->expectedValue = $expectedValue;
|
||||
$this->transport = $transport;
|
||||
$this->queued = $queued;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function toString(): string
|
||||
{
|
||||
return sprintf('%shas %s "%d" emails', $this->transport ? $this->transport.' ' : '', $this->queued ? 'queued' : 'sent', $this->expectedValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param MessageEvents $events
|
||||
*
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function matches($events): bool
|
||||
{
|
||||
return $this->expectedValue === $this->countEmails($events);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param MessageEvents $events
|
||||
*
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function failureDescription($events): string
|
||||
{
|
||||
return sprintf('the Transport %s (%d %s)', $this->toString(), $this->countEmails($events), $this->queued ? 'queued' : 'sent');
|
||||
}
|
||||
|
||||
private function countEmails(MessageEvents $events): int
|
||||
{
|
||||
$count = 0;
|
||||
foreach ($events->getEvents($this->transport) as $event) {
|
||||
if (
|
||||
($this->queued && $event->isQueued())
|
||||
||
|
||||
(!$this->queued && !$event->isQueued())
|
||||
) {
|
||||
++$count;
|
||||
}
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Mailer\Test\Constraint;
|
||||
|
||||
use PHPUnit\Framework\Constraint\Constraint;
|
||||
use Symfony\Component\Mailer\Event\MessageEvent;
|
||||
|
||||
final class EmailIsQueued extends Constraint
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function toString(): string
|
||||
{
|
||||
return 'is queued';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param MessageEvent $event
|
||||
*
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function matches($event): bool
|
||||
{
|
||||
return $event->isQueued();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param MessageEvent $event
|
||||
*
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function failureDescription($event): string
|
||||
{
|
||||
return 'the Email '.$this->toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Mailer\Test;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Mailer\Exception\IncompleteDsnException;
|
||||
use Symfony\Component\Mailer\Exception\UnsupportedSchemeException;
|
||||
use Symfony\Component\Mailer\Transport\Dsn;
|
||||
use Symfony\Component\Mailer\Transport\TransportFactoryInterface;
|
||||
use Symfony\Component\Mailer\Transport\TransportInterface;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
/**
|
||||
* A test case to ease testing Transport Factory.
|
||||
*
|
||||
* @author Konstantin Myakshin <molodchick@gmail.com>
|
||||
*/
|
||||
abstract class TransportFactoryTestCase extends TestCase
|
||||
{
|
||||
protected const USER = 'u$er';
|
||||
protected const PASSWORD = 'pa$s';
|
||||
|
||||
protected $dispatcher;
|
||||
protected $client;
|
||||
protected $logger;
|
||||
|
||||
abstract public function getFactory(): TransportFactoryInterface;
|
||||
|
||||
abstract public static function supportsProvider(): iterable;
|
||||
|
||||
abstract public static function createProvider(): iterable;
|
||||
|
||||
public static function unsupportedSchemeProvider(): iterable
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public static function incompleteDsnProvider(): iterable
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider supportsProvider
|
||||
*/
|
||||
public function testSupports(Dsn $dsn, bool $supports)
|
||||
{
|
||||
$factory = $this->getFactory();
|
||||
|
||||
$this->assertSame($supports, $factory->supports($dsn));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider createProvider
|
||||
*/
|
||||
public function testCreate(Dsn $dsn, TransportInterface $transport)
|
||||
{
|
||||
$factory = $this->getFactory();
|
||||
|
||||
$this->assertEquals($transport, $factory->create($dsn));
|
||||
if (str_contains('smtp', $dsn->getScheme())) {
|
||||
$this->assertStringMatchesFormat($dsn->getScheme().'://%S'.$dsn->getHost().'%S', (string) $transport);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider unsupportedSchemeProvider
|
||||
*/
|
||||
public function testUnsupportedSchemeException(Dsn $dsn, ?string $message = null)
|
||||
{
|
||||
$factory = $this->getFactory();
|
||||
|
||||
$this->expectException(UnsupportedSchemeException::class);
|
||||
if (null !== $message) {
|
||||
$this->expectExceptionMessage($message);
|
||||
}
|
||||
|
||||
$factory->create($dsn);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider incompleteDsnProvider
|
||||
*/
|
||||
public function testIncompleteDsnException(Dsn $dsn)
|
||||
{
|
||||
$factory = $this->getFactory();
|
||||
|
||||
$this->expectException(IncompleteDsnException::class);
|
||||
$factory->create($dsn);
|
||||
}
|
||||
|
||||
protected function getDispatcher(): EventDispatcherInterface
|
||||
{
|
||||
return $this->dispatcher ?? $this->dispatcher = $this->createMock(EventDispatcherInterface::class);
|
||||
}
|
||||
|
||||
protected function getClient(): HttpClientInterface
|
||||
{
|
||||
return $this->client ?? $this->client = $this->createMock(HttpClientInterface::class);
|
||||
}
|
||||
|
||||
protected function getLogger(): LoggerInterface
|
||||
{
|
||||
return $this->logger ?? $this->logger = $this->createMock(LoggerInterface::class);
|
||||
}
|
||||
}
|
||||
+206
@@ -0,0 +1,206 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Mailer;
|
||||
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Mailer\Bridge\Amazon\Transport\SesTransportFactory;
|
||||
use Symfony\Component\Mailer\Bridge\Google\Transport\GmailTransportFactory;
|
||||
use Symfony\Component\Mailer\Bridge\Mailchimp\Transport\MandrillTransportFactory;
|
||||
use Symfony\Component\Mailer\Bridge\Mailgun\Transport\MailgunTransportFactory;
|
||||
use Symfony\Component\Mailer\Bridge\Mailjet\Transport\MailjetTransportFactory;
|
||||
use Symfony\Component\Mailer\Bridge\OhMySmtp\Transport\OhMySmtpTransportFactory;
|
||||
use Symfony\Component\Mailer\Bridge\Postmark\Transport\PostmarkTransportFactory;
|
||||
use Symfony\Component\Mailer\Bridge\Sendgrid\Transport\SendgridTransportFactory;
|
||||
use Symfony\Component\Mailer\Bridge\Sendinblue\Transport\SendinblueTransportFactory;
|
||||
use Symfony\Component\Mailer\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Mailer\Exception\UnsupportedSchemeException;
|
||||
use Symfony\Component\Mailer\Transport\Dsn;
|
||||
use Symfony\Component\Mailer\Transport\FailoverTransport;
|
||||
use Symfony\Component\Mailer\Transport\NativeTransportFactory;
|
||||
use Symfony\Component\Mailer\Transport\NullTransportFactory;
|
||||
use Symfony\Component\Mailer\Transport\RoundRobinTransport;
|
||||
use Symfony\Component\Mailer\Transport\SendmailTransportFactory;
|
||||
use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransportFactory;
|
||||
use Symfony\Component\Mailer\Transport\TransportFactoryInterface;
|
||||
use Symfony\Component\Mailer\Transport\TransportInterface;
|
||||
use Symfony\Component\Mailer\Transport\Transports;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
/**
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
* @author Konstantin Myakshin <molodchick@gmail.com>
|
||||
*
|
||||
* @final since Symfony 5.4
|
||||
*/
|
||||
class Transport
|
||||
{
|
||||
private const FACTORY_CLASSES = [
|
||||
GmailTransportFactory::class,
|
||||
MailgunTransportFactory::class,
|
||||
MailjetTransportFactory::class,
|
||||
MandrillTransportFactory::class,
|
||||
OhMySmtpTransportFactory::class,
|
||||
PostmarkTransportFactory::class,
|
||||
SendgridTransportFactory::class,
|
||||
SendinblueTransportFactory::class,
|
||||
SesTransportFactory::class,
|
||||
];
|
||||
|
||||
private $factories;
|
||||
|
||||
/**
|
||||
* @param EventDispatcherInterface|null $dispatcher
|
||||
* @param HttpClientInterface|null $client
|
||||
* @param LoggerInterface|null $logger
|
||||
*/
|
||||
public static function fromDsn(string $dsn/* , ?EventDispatcherInterface $dispatcher = null, ?HttpClientInterface $client = null, ?LoggerInterface $logger = null */): TransportInterface
|
||||
{
|
||||
$dispatcher = 2 <= \func_num_args() ? func_get_arg(1) : null;
|
||||
$client = 3 <= \func_num_args() ? func_get_arg(2) : null;
|
||||
$logger = 4 <= \func_num_args() ? func_get_arg(3) : null;
|
||||
|
||||
$factory = new self(iterator_to_array(self::getDefaultFactories($dispatcher, $client, $logger)));
|
||||
|
||||
return $factory->fromString($dsn);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param EventDispatcherInterface|null $dispatcher
|
||||
* @param HttpClientInterface|null $client
|
||||
* @param LoggerInterface|null $logger
|
||||
*/
|
||||
public static function fromDsns(array $dsns/* , ?EventDispatcherInterface $dispatcher = null, ?HttpClientInterface $client = null, ?LoggerInterface $logger = null */): TransportInterface
|
||||
{
|
||||
$dispatcher = 2 <= \func_num_args() ? func_get_arg(1) : null;
|
||||
$client = 3 <= \func_num_args() ? func_get_arg(2) : null;
|
||||
$logger = 4 <= \func_num_args() ? func_get_arg(3) : null;
|
||||
|
||||
$factory = new self(iterator_to_array(self::getDefaultFactories($dispatcher, $client, $logger)));
|
||||
|
||||
return $factory->fromStrings($dsns);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TransportFactoryInterface[] $factories
|
||||
*/
|
||||
public function __construct(iterable $factories)
|
||||
{
|
||||
$this->factories = $factories;
|
||||
}
|
||||
|
||||
public function fromStrings(array $dsns): Transports
|
||||
{
|
||||
$transports = [];
|
||||
foreach ($dsns as $name => $dsn) {
|
||||
$transports[$name] = $this->fromString($dsn);
|
||||
}
|
||||
|
||||
return new Transports($transports);
|
||||
}
|
||||
|
||||
public function fromString(string $dsn): TransportInterface
|
||||
{
|
||||
[$transport, $offset] = $this->parseDsn($dsn);
|
||||
if ($offset !== \strlen($dsn)) {
|
||||
throw new InvalidArgumentException('The mailer DSN has some garbage at the end.');
|
||||
}
|
||||
|
||||
return $transport;
|
||||
}
|
||||
|
||||
private function parseDsn(string $dsn, int $offset = 0): array
|
||||
{
|
||||
static $keywords = [
|
||||
'failover' => FailoverTransport::class,
|
||||
'roundrobin' => RoundRobinTransport::class,
|
||||
];
|
||||
|
||||
while (true) {
|
||||
foreach ($keywords as $name => $class) {
|
||||
$name .= '(';
|
||||
if ($name === substr($dsn, $offset, \strlen($name))) {
|
||||
$offset += \strlen($name) - 1;
|
||||
preg_match('{\(([^()]|(?R))*\)}A', $dsn, $matches, 0, $offset);
|
||||
if (!isset($matches[0])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
++$offset;
|
||||
$args = [];
|
||||
while (true) {
|
||||
[$arg, $offset] = $this->parseDsn($dsn, $offset);
|
||||
$args[] = $arg;
|
||||
if (\strlen($dsn) === $offset) {
|
||||
break;
|
||||
}
|
||||
++$offset;
|
||||
if (')' === $dsn[$offset - 1]) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return [new $class($args), $offset];
|
||||
}
|
||||
}
|
||||
|
||||
if (preg_match('{(\w+)\(}A', $dsn, $matches, 0, $offset)) {
|
||||
throw new InvalidArgumentException(sprintf('The "%s" keyword is not valid (valid ones are "%s"), ', $matches[1], implode('", "', array_keys($keywords))));
|
||||
}
|
||||
|
||||
if ($pos = strcspn($dsn, ' )', $offset)) {
|
||||
return [$this->fromDsnObject(Dsn::fromString(substr($dsn, $offset, $pos))), $offset + $pos];
|
||||
}
|
||||
|
||||
return [$this->fromDsnObject(Dsn::fromString(substr($dsn, $offset))), \strlen($dsn)];
|
||||
}
|
||||
}
|
||||
|
||||
public function fromDsnObject(Dsn $dsn): TransportInterface
|
||||
{
|
||||
foreach ($this->factories as $factory) {
|
||||
if ($factory->supports($dsn)) {
|
||||
return $factory->create($dsn);
|
||||
}
|
||||
}
|
||||
|
||||
throw new UnsupportedSchemeException($dsn);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param EventDispatcherInterface|null $dispatcher
|
||||
* @param HttpClientInterface|null $client
|
||||
* @param LoggerInterface|null $logger
|
||||
*
|
||||
* @return \Traversable<int, TransportFactoryInterface>
|
||||
*/
|
||||
public static function getDefaultFactories(/* ?EventDispatcherInterface $dispatcher = null, ?HttpClientInterface $client = null, ?LoggerInterface $logger = null */): iterable
|
||||
{
|
||||
$dispatcher = 1 <= \func_num_args() ? func_get_arg(0) : null;
|
||||
$client = 2 <= \func_num_args() ? func_get_arg(1) : null;
|
||||
$logger = 3 <= \func_num_args() ? func_get_arg(2) : null;
|
||||
|
||||
foreach (self::FACTORY_CLASSES as $factoryClass) {
|
||||
if (class_exists($factoryClass)) {
|
||||
yield new $factoryClass($dispatcher, $client, $logger);
|
||||
}
|
||||
}
|
||||
|
||||
yield new NullTransportFactory($dispatcher, $client, $logger);
|
||||
|
||||
yield new SendmailTransportFactory($dispatcher, $client, $logger);
|
||||
|
||||
yield new EsmtpTransportFactory($dispatcher, $client, $logger);
|
||||
|
||||
yield new NativeTransportFactory($dispatcher, $client, $logger);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Mailer\Transport;
|
||||
|
||||
use Symfony\Component\Mailer\Envelope;
|
||||
use Symfony\Component\Mailer\Exception\RuntimeException;
|
||||
use Symfony\Component\Mailer\SentMessage;
|
||||
use Symfony\Component\Mime\Address;
|
||||
use Symfony\Component\Mime\Email;
|
||||
use Symfony\Component\Mime\MessageConverter;
|
||||
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||
|
||||
/**
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
abstract class AbstractApiTransport extends AbstractHttpTransport
|
||||
{
|
||||
abstract protected function doSendApi(SentMessage $sentMessage, Email $email, Envelope $envelope): ResponseInterface;
|
||||
|
||||
protected function doSendHttp(SentMessage $message): ResponseInterface
|
||||
{
|
||||
try {
|
||||
$email = MessageConverter::toEmail($message->getOriginalMessage());
|
||||
} catch (\Exception $e) {
|
||||
throw new RuntimeException(sprintf('Unable to send message with the "%s" transport: ', __CLASS__).$e->getMessage(), 0, $e);
|
||||
}
|
||||
|
||||
return $this->doSendApi($message, $email, $message->getEnvelope());
|
||||
}
|
||||
|
||||
protected function getRecipients(Email $email, Envelope $envelope): array
|
||||
{
|
||||
return array_filter($envelope->getRecipients(), function (Address $address) use ($email) {
|
||||
return false === \in_array($address, array_merge($email->getCc(), $email->getBcc()), true);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Mailer\Transport;
|
||||
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\HttpClient\HttpClient;
|
||||
use Symfony\Component\Mailer\Exception\HttpTransportException;
|
||||
use Symfony\Component\Mailer\SentMessage;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||
|
||||
/**
|
||||
* @author Victor Bocharsky <victor@symfonycasts.com>
|
||||
*/
|
||||
abstract class AbstractHttpTransport extends AbstractTransport
|
||||
{
|
||||
protected $host;
|
||||
protected $port;
|
||||
protected $client;
|
||||
|
||||
public function __construct(?HttpClientInterface $client = null, ?EventDispatcherInterface $dispatcher = null, ?LoggerInterface $logger = null)
|
||||
{
|
||||
$this->client = $client;
|
||||
if (null === $client) {
|
||||
if (!class_exists(HttpClient::class)) {
|
||||
throw new \LogicException(sprintf('You cannot use "%s" as the HttpClient component is not installed. Try running "composer require symfony/http-client".', __CLASS__));
|
||||
}
|
||||
|
||||
$this->client = HttpClient::create();
|
||||
}
|
||||
|
||||
parent::__construct($dispatcher, $logger);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setHost(?string $host)
|
||||
{
|
||||
$this->host = $host;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setPort(?int $port)
|
||||
{
|
||||
$this->port = $port;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
abstract protected function doSendHttp(SentMessage $message): ResponseInterface;
|
||||
|
||||
protected function doSend(SentMessage $message): void
|
||||
{
|
||||
$response = null;
|
||||
try {
|
||||
$response = $this->doSendHttp($message);
|
||||
$message->appendDebug($response->getInfo('debug') ?? '');
|
||||
} catch (HttpTransportException $e) {
|
||||
$e->appendDebug($e->getResponse()->getInfo('debug') ?? '');
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Mailer\Transport;
|
||||
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Psr\Log\NullLogger;
|
||||
use Symfony\Component\EventDispatcher\Event;
|
||||
use Symfony\Component\EventDispatcher\LegacyEventDispatcherProxy;
|
||||
use Symfony\Component\Mailer\Envelope;
|
||||
use Symfony\Component\Mailer\Event\MessageEvent;
|
||||
use Symfony\Component\Mailer\SentMessage;
|
||||
use Symfony\Component\Mime\Address;
|
||||
use Symfony\Component\Mime\RawMessage;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface as SymfonyEventDispatcherInterface;
|
||||
|
||||
/**
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
abstract class AbstractTransport implements TransportInterface
|
||||
{
|
||||
private $dispatcher;
|
||||
private $logger;
|
||||
private $rate = 0;
|
||||
private $lastSent = 0;
|
||||
|
||||
public function __construct(?EventDispatcherInterface $dispatcher = null, ?LoggerInterface $logger = null)
|
||||
{
|
||||
$this->dispatcher = class_exists(Event::class) && $dispatcher instanceof SymfonyEventDispatcherInterface ? LegacyEventDispatcherProxy::decorate($dispatcher) : $dispatcher;
|
||||
$this->logger = $logger ?? new NullLogger();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the maximum number of messages to send per second (0 to disable).
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setMaxPerSecond(float $rate): self
|
||||
{
|
||||
if (0 >= $rate) {
|
||||
$rate = 0;
|
||||
}
|
||||
|
||||
$this->rate = $rate;
|
||||
$this->lastSent = 0;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function send(RawMessage $message, ?Envelope $envelope = null): ?SentMessage
|
||||
{
|
||||
$message = clone $message;
|
||||
$envelope = null !== $envelope ? clone $envelope : Envelope::create($message);
|
||||
|
||||
if (null !== $this->dispatcher) {
|
||||
$event = new MessageEvent($message, $envelope, (string) $this);
|
||||
$this->dispatcher->dispatch($event);
|
||||
$envelope = $event->getEnvelope();
|
||||
$message = $event->getMessage();
|
||||
}
|
||||
|
||||
$message = new SentMessage($message, $envelope);
|
||||
$this->doSend($message);
|
||||
|
||||
$this->checkThrottling();
|
||||
|
||||
return $message;
|
||||
}
|
||||
|
||||
abstract protected function doSend(SentMessage $message): void;
|
||||
|
||||
/**
|
||||
* @param Address[] $addresses
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
protected function stringifyAddresses(array $addresses): array
|
||||
{
|
||||
return array_map(function (Address $a) {
|
||||
return $a->toString();
|
||||
}, $addresses);
|
||||
}
|
||||
|
||||
protected function getLogger(): LoggerInterface
|
||||
{
|
||||
return $this->logger;
|
||||
}
|
||||
|
||||
private function checkThrottling()
|
||||
{
|
||||
if (0 == $this->rate) {
|
||||
return;
|
||||
}
|
||||
|
||||
$sleep = (1 / $this->rate) - (microtime(true) - $this->lastSent);
|
||||
if (0 < $sleep) {
|
||||
$this->logger->debug(sprintf('Email transport "%s" sleeps for %.2f seconds', __CLASS__, $sleep));
|
||||
usleep((int) ($sleep * 1000000));
|
||||
}
|
||||
$this->lastSent = microtime(true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Mailer\Transport;
|
||||
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Mailer\Exception\IncompleteDsnException;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
/**
|
||||
* @author Konstantin Myakshin <molodchick@gmail.com>
|
||||
*/
|
||||
abstract class AbstractTransportFactory implements TransportFactoryInterface
|
||||
{
|
||||
protected $dispatcher;
|
||||
protected $client;
|
||||
protected $logger;
|
||||
|
||||
public function __construct(?EventDispatcherInterface $dispatcher = null, ?HttpClientInterface $client = null, ?LoggerInterface $logger = null)
|
||||
{
|
||||
$this->dispatcher = $dispatcher;
|
||||
$this->client = $client;
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
public function supports(Dsn $dsn): bool
|
||||
{
|
||||
return \in_array($dsn->getScheme(), $this->getSupportedSchemes());
|
||||
}
|
||||
|
||||
abstract protected function getSupportedSchemes(): array;
|
||||
|
||||
protected function getUser(Dsn $dsn): string
|
||||
{
|
||||
$user = $dsn->getUser();
|
||||
if (null === $user) {
|
||||
throw new IncompleteDsnException('User is not set.');
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
protected function getPassword(Dsn $dsn): string
|
||||
{
|
||||
$password = $dsn->getPassword();
|
||||
if (null === $password) {
|
||||
throw new IncompleteDsnException('Password is not set.');
|
||||
}
|
||||
|
||||
return $password;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Mailer\Transport;
|
||||
|
||||
use Symfony\Component\Mailer\Exception\InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* @author Konstantin Myakshin <molodchick@gmail.com>
|
||||
*/
|
||||
final class Dsn
|
||||
{
|
||||
private $scheme;
|
||||
private $host;
|
||||
private $user;
|
||||
private $password;
|
||||
private $port;
|
||||
private $options;
|
||||
|
||||
public function __construct(string $scheme, string $host, ?string $user = null, ?string $password = null, ?int $port = null, array $options = [])
|
||||
{
|
||||
$this->scheme = $scheme;
|
||||
$this->host = $host;
|
||||
$this->user = $user;
|
||||
$this->password = $password;
|
||||
$this->port = $port;
|
||||
$this->options = $options;
|
||||
}
|
||||
|
||||
public static function fromString(string $dsn): self
|
||||
{
|
||||
if (false === $params = parse_url($dsn)) {
|
||||
throw new InvalidArgumentException('The mailer DSN is invalid.');
|
||||
}
|
||||
|
||||
if (!isset($params['scheme'])) {
|
||||
throw new InvalidArgumentException('The mailer DSN must contain a scheme.');
|
||||
}
|
||||
|
||||
if (!isset($params['host'])) {
|
||||
throw new InvalidArgumentException('The mailer DSN must contain a host (use "default" by default).');
|
||||
}
|
||||
|
||||
$user = '' !== ($params['user'] ?? '') ? rawurldecode($params['user']) : null;
|
||||
$password = '' !== ($params['pass'] ?? '') ? rawurldecode($params['pass']) : null;
|
||||
$port = $params['port'] ?? null;
|
||||
parse_str($params['query'] ?? '', $query);
|
||||
|
||||
return new self($params['scheme'], $params['host'], $user, $password, $port, $query);
|
||||
}
|
||||
|
||||
public function getScheme(): string
|
||||
{
|
||||
return $this->scheme;
|
||||
}
|
||||
|
||||
public function getHost(): string
|
||||
{
|
||||
return $this->host;
|
||||
}
|
||||
|
||||
public function getUser(): ?string
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public function getPassword(): ?string
|
||||
{
|
||||
return $this->password;
|
||||
}
|
||||
|
||||
public function getPort(?int $default = null): ?int
|
||||
{
|
||||
return $this->port ?? $default;
|
||||
}
|
||||
|
||||
public function getOption(string $key, $default = null)
|
||||
{
|
||||
return $this->options[$key] ?? $default;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Mailer\Transport;
|
||||
|
||||
/**
|
||||
* Uses several Transports using a failover algorithm.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
class FailoverTransport extends RoundRobinTransport
|
||||
{
|
||||
private $currentTransport;
|
||||
|
||||
protected function getNextTransport(): ?TransportInterface
|
||||
{
|
||||
if (null === $this->currentTransport || $this->isTransportDead($this->currentTransport)) {
|
||||
$this->currentTransport = parent::getNextTransport();
|
||||
}
|
||||
|
||||
return $this->currentTransport;
|
||||
}
|
||||
|
||||
protected function getInitialCursor(): int
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
protected function getNameSymbol(): string
|
||||
{
|
||||
return 'failover';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Mailer\Transport;
|
||||
|
||||
use Symfony\Component\Mailer\Exception\TransportException;
|
||||
use Symfony\Component\Mailer\Exception\UnsupportedSchemeException;
|
||||
use Symfony\Component\Mailer\Transport\Smtp\SmtpTransport;
|
||||
use Symfony\Component\Mailer\Transport\Smtp\Stream\SocketStream;
|
||||
|
||||
/**
|
||||
* Factory that configures a transport (sendmail or SMTP) based on php.ini settings.
|
||||
*
|
||||
* @author Laurent VOULLEMIER <laurent.voullemier@gmail.com>
|
||||
*/
|
||||
final class NativeTransportFactory extends AbstractTransportFactory
|
||||
{
|
||||
public function create(Dsn $dsn): TransportInterface
|
||||
{
|
||||
if (!\in_array($dsn->getScheme(), $this->getSupportedSchemes(), true)) {
|
||||
throw new UnsupportedSchemeException($dsn, 'native', $this->getSupportedSchemes());
|
||||
}
|
||||
|
||||
if ($sendMailPath = ini_get('sendmail_path')) {
|
||||
return new SendmailTransport($sendMailPath, $this->dispatcher, $this->logger);
|
||||
}
|
||||
|
||||
if ('\\' !== \DIRECTORY_SEPARATOR) {
|
||||
throw new TransportException('sendmail_path is not configured in php.ini.');
|
||||
}
|
||||
|
||||
// Only for windows hosts; at this point non-windows
|
||||
// host have already thrown an exception or returned a transport
|
||||
$host = ini_get('SMTP');
|
||||
$port = (int) ini_get('smtp_port');
|
||||
|
||||
if (!$host || !$port) {
|
||||
throw new TransportException('smtp or smtp_port is not configured in php.ini.');
|
||||
}
|
||||
|
||||
$socketStream = new SocketStream();
|
||||
$socketStream->setHost($host);
|
||||
$socketStream->setPort($port);
|
||||
if (465 !== $port) {
|
||||
$socketStream->disableTls();
|
||||
}
|
||||
|
||||
return new SmtpTransport($socketStream, $this->dispatcher, $this->logger);
|
||||
}
|
||||
|
||||
protected function getSupportedSchemes(): array
|
||||
{
|
||||
return ['native'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Mailer\Transport;
|
||||
|
||||
use Symfony\Component\Mailer\SentMessage;
|
||||
|
||||
/**
|
||||
* Pretends messages have been sent, but just ignores them.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
final class NullTransport extends AbstractTransport
|
||||
{
|
||||
protected function doSend(SentMessage $message): void
|
||||
{
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return 'null://';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Mailer\Transport;
|
||||
|
||||
use Symfony\Component\Mailer\Exception\UnsupportedSchemeException;
|
||||
|
||||
/**
|
||||
* @author Konstantin Myakshin <molodchick@gmail.com>
|
||||
*/
|
||||
final class NullTransportFactory extends AbstractTransportFactory
|
||||
{
|
||||
public function create(Dsn $dsn): TransportInterface
|
||||
{
|
||||
if ('null' === $dsn->getScheme()) {
|
||||
return new NullTransport($this->dispatcher, $this->logger);
|
||||
}
|
||||
|
||||
throw new UnsupportedSchemeException($dsn, 'null', $this->getSupportedSchemes());
|
||||
}
|
||||
|
||||
protected function getSupportedSchemes(): array
|
||||
{
|
||||
return ['null'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Mailer\Transport;
|
||||
|
||||
use Symfony\Component\Mailer\Envelope;
|
||||
use Symfony\Component\Mailer\Exception\TransportException;
|
||||
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
|
||||
use Symfony\Component\Mailer\SentMessage;
|
||||
use Symfony\Component\Mime\RawMessage;
|
||||
|
||||
/**
|
||||
* Uses several Transports using a round robin algorithm.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
class RoundRobinTransport implements TransportInterface
|
||||
{
|
||||
/**
|
||||
* @var \SplObjectStorage<TransportInterface, float>
|
||||
*/
|
||||
private $deadTransports;
|
||||
private $transports = [];
|
||||
private $retryPeriod;
|
||||
private $cursor = -1;
|
||||
|
||||
/**
|
||||
* @param TransportInterface[] $transports
|
||||
*/
|
||||
public function __construct(array $transports, int $retryPeriod = 60)
|
||||
{
|
||||
if (!$transports) {
|
||||
throw new TransportException(sprintf('"%s" must have at least one transport configured.', static::class));
|
||||
}
|
||||
|
||||
$this->transports = $transports;
|
||||
$this->deadTransports = new \SplObjectStorage();
|
||||
$this->retryPeriod = $retryPeriod;
|
||||
}
|
||||
|
||||
public function send(RawMessage $message, ?Envelope $envelope = null): ?SentMessage
|
||||
{
|
||||
$exception = null;
|
||||
|
||||
while ($transport = $this->getNextTransport()) {
|
||||
try {
|
||||
return $transport->send($message, $envelope);
|
||||
} catch (TransportExceptionInterface $e) {
|
||||
$exception = $exception ?? new TransportException('All transports failed.');
|
||||
$exception->appendDebug(sprintf("Transport \"%s\": %s\n", $transport, $e->getDebug()));
|
||||
$this->deadTransports[$transport] = microtime(true);
|
||||
}
|
||||
}
|
||||
|
||||
throw $exception ?? new TransportException('No transports found.');
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->getNameSymbol().'('.implode(' ', array_map('strval', $this->transports)).')';
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotates the transport list around and returns the first instance.
|
||||
*/
|
||||
protected function getNextTransport(): ?TransportInterface
|
||||
{
|
||||
if (-1 === $this->cursor) {
|
||||
$this->cursor = $this->getInitialCursor();
|
||||
}
|
||||
|
||||
$cursor = $this->cursor;
|
||||
while (true) {
|
||||
$transport = $this->transports[$cursor];
|
||||
|
||||
if (!$this->isTransportDead($transport)) {
|
||||
break;
|
||||
}
|
||||
|
||||
if ((microtime(true) - $this->deadTransports[$transport]) > $this->retryPeriod) {
|
||||
$this->deadTransports->detach($transport);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if ($this->cursor === $cursor = $this->moveCursor($cursor)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
$this->cursor = $this->moveCursor($cursor);
|
||||
|
||||
return $transport;
|
||||
}
|
||||
|
||||
protected function isTransportDead(TransportInterface $transport): bool
|
||||
{
|
||||
return $this->deadTransports->contains($transport);
|
||||
}
|
||||
|
||||
protected function getInitialCursor(): int
|
||||
{
|
||||
// the cursor initial value is randomized so that
|
||||
// when are not in a daemon, we are still rotating the transports
|
||||
return mt_rand(0, \count($this->transports) - 1);
|
||||
}
|
||||
|
||||
protected function getNameSymbol(): string
|
||||
{
|
||||
return 'roundrobin';
|
||||
}
|
||||
|
||||
private function moveCursor(int $cursor): int
|
||||
{
|
||||
return ++$cursor >= \count($this->transports) ? 0 : $cursor;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Mailer\Transport;
|
||||
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Mailer\Envelope;
|
||||
use Symfony\Component\Mailer\SentMessage;
|
||||
use Symfony\Component\Mailer\Transport\Smtp\SmtpTransport;
|
||||
use Symfony\Component\Mailer\Transport\Smtp\Stream\AbstractStream;
|
||||
use Symfony\Component\Mailer\Transport\Smtp\Stream\ProcessStream;
|
||||
use Symfony\Component\Mime\RawMessage;
|
||||
|
||||
/**
|
||||
* SendmailTransport for sending mail through a Sendmail/Postfix (etc..) binary.
|
||||
*
|
||||
* Transport can be instantiated through SendmailTransportFactory or NativeTransportFactory:
|
||||
*
|
||||
* - SendmailTransportFactory to use most common sendmail path and recommended options
|
||||
* - NativeTransportFactory when configuration is set via php.ini
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
* @author Chris Corbyn
|
||||
*/
|
||||
class SendmailTransport extends AbstractTransport
|
||||
{
|
||||
private $command = '/usr/sbin/sendmail -bs';
|
||||
private $stream;
|
||||
private $transport;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* Supported modes are -bs and -t, with any additional flags desired.
|
||||
*
|
||||
* The recommended mode is "-bs" since it is interactive and failure notifications are hence possible.
|
||||
* Note that the -t mode does not support error reporting and does not support Bcc properly (the Bcc headers are not removed).
|
||||
*
|
||||
* If using -t mode, you are strongly advised to include -oi or -i in the flags (like /usr/sbin/sendmail -oi -t)
|
||||
*
|
||||
* -f<sender> flag will be appended automatically if one is not present.
|
||||
*/
|
||||
public function __construct(?string $command = null, ?EventDispatcherInterface $dispatcher = null, ?LoggerInterface $logger = null)
|
||||
{
|
||||
parent::__construct($dispatcher, $logger);
|
||||
|
||||
if (null !== $command) {
|
||||
if (!str_contains($command, ' -bs') && !str_contains($command, ' -t')) {
|
||||
throw new \InvalidArgumentException(sprintf('Unsupported sendmail command flags "%s"; must be one of "-bs" or "-t" but can include additional flags.', $command));
|
||||
}
|
||||
|
||||
$this->command = $command;
|
||||
}
|
||||
|
||||
$this->stream = new ProcessStream();
|
||||
if (str_contains($this->command, ' -bs')) {
|
||||
$this->stream->setCommand($this->command);
|
||||
$this->stream->setInteractive(true);
|
||||
$this->transport = new SmtpTransport($this->stream, $dispatcher, $logger);
|
||||
}
|
||||
}
|
||||
|
||||
public function send(RawMessage $message, ?Envelope $envelope = null): ?SentMessage
|
||||
{
|
||||
if ($this->transport) {
|
||||
return $this->transport->send($message, $envelope);
|
||||
}
|
||||
|
||||
return parent::send($message, $envelope);
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
if ($this->transport) {
|
||||
return (string) $this->transport;
|
||||
}
|
||||
|
||||
return 'smtp://sendmail';
|
||||
}
|
||||
|
||||
protected function doSend(SentMessage $message): void
|
||||
{
|
||||
$this->getLogger()->debug(sprintf('Email transport "%s" starting', __CLASS__));
|
||||
|
||||
$command = $this->command;
|
||||
|
||||
if ($recipients = $message->getEnvelope()->getRecipients()) {
|
||||
$command = str_replace(' -t', '', $command);
|
||||
}
|
||||
|
||||
if (!str_contains($command, ' -f')) {
|
||||
$command .= ' -f'.escapeshellarg($message->getEnvelope()->getSender()->getEncodedAddress());
|
||||
}
|
||||
|
||||
$chunks = AbstractStream::replace("\r\n", "\n", $message->toIterable());
|
||||
|
||||
if (!str_contains($command, ' -i') && !str_contains($command, ' -oi')) {
|
||||
$chunks = AbstractStream::replace("\n.", "\n..", $chunks);
|
||||
}
|
||||
|
||||
foreach ($recipients as $recipient) {
|
||||
$command .= ' '.escapeshellarg($recipient->getEncodedAddress());
|
||||
}
|
||||
|
||||
$this->stream->setCommand($command);
|
||||
$this->stream->initialize();
|
||||
foreach ($chunks as $chunk) {
|
||||
$this->stream->write($chunk);
|
||||
}
|
||||
$this->stream->flush();
|
||||
$this->stream->terminate();
|
||||
|
||||
$this->getLogger()->debug(sprintf('Email transport "%s" stopped', __CLASS__));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Mailer\Transport;
|
||||
|
||||
use Symfony\Component\Mailer\Exception\UnsupportedSchemeException;
|
||||
|
||||
/**
|
||||
* @author Konstantin Myakshin <molodchick@gmail.com>
|
||||
*/
|
||||
final class SendmailTransportFactory extends AbstractTransportFactory
|
||||
{
|
||||
public function create(Dsn $dsn): TransportInterface
|
||||
{
|
||||
if ('sendmail+smtp' === $dsn->getScheme() || 'sendmail' === $dsn->getScheme()) {
|
||||
return new SendmailTransport($dsn->getOption('command'), $this->dispatcher, $this->logger);
|
||||
}
|
||||
|
||||
throw new UnsupportedSchemeException($dsn, 'sendmail', $this->getSupportedSchemes());
|
||||
}
|
||||
|
||||
protected function getSupportedSchemes(): array
|
||||
{
|
||||
return ['sendmail', 'sendmail+smtp'];
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Mailer\Transport\Smtp\Auth;
|
||||
|
||||
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
|
||||
use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport;
|
||||
|
||||
/**
|
||||
* An Authentication mechanism.
|
||||
*
|
||||
* @author Chris Corbyn
|
||||
*/
|
||||
interface AuthenticatorInterface
|
||||
{
|
||||
/**
|
||||
* Tries to authenticate the user.
|
||||
*
|
||||
* @throws TransportExceptionInterface
|
||||
*/
|
||||
public function authenticate(EsmtpTransport $client): void;
|
||||
|
||||
/**
|
||||
* Gets the name of the AUTH mechanism this Authenticator handles.
|
||||
*/
|
||||
public function getAuthKeyword(): string;
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Mailer\Transport\Smtp\Auth;
|
||||
|
||||
use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport;
|
||||
|
||||
/**
|
||||
* Handles CRAM-MD5 authentication.
|
||||
*
|
||||
* @author Chris Corbyn
|
||||
*/
|
||||
class CramMd5Authenticator implements AuthenticatorInterface
|
||||
{
|
||||
public function getAuthKeyword(): string
|
||||
{
|
||||
return 'CRAM-MD5';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @see https://www.ietf.org/rfc/rfc4954.txt
|
||||
*/
|
||||
public function authenticate(EsmtpTransport $client): void
|
||||
{
|
||||
$challenge = $client->executeCommand("AUTH CRAM-MD5\r\n", [334]);
|
||||
$challenge = base64_decode(substr($challenge, 4));
|
||||
$message = base64_encode($client->getUsername().' '.$this->getResponse($client->getPassword(), $challenge));
|
||||
$client->executeCommand(sprintf("%s\r\n", $message), [235]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a CRAM-MD5 response from a server challenge.
|
||||
*/
|
||||
private function getResponse(string $secret, string $challenge): string
|
||||
{
|
||||
if (\strlen($secret) > 64) {
|
||||
$secret = pack('H32', md5($secret));
|
||||
}
|
||||
|
||||
if (\strlen($secret) < 64) {
|
||||
$secret = str_pad($secret, 64, \chr(0));
|
||||
}
|
||||
|
||||
$kipad = substr($secret, 0, 64) ^ str_repeat(\chr(0x36), 64);
|
||||
$kopad = substr($secret, 0, 64) ^ str_repeat(\chr(0x5C), 64);
|
||||
|
||||
$inner = pack('H32', md5($kipad.$challenge));
|
||||
$digest = md5($kopad.$inner);
|
||||
|
||||
return $digest;
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Mailer\Transport\Smtp\Auth;
|
||||
|
||||
use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport;
|
||||
|
||||
/**
|
||||
* Handles LOGIN authentication.
|
||||
*
|
||||
* @author Chris Corbyn
|
||||
*/
|
||||
class LoginAuthenticator implements AuthenticatorInterface
|
||||
{
|
||||
public function getAuthKeyword(): string
|
||||
{
|
||||
return 'LOGIN';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @see https://www.ietf.org/rfc/rfc4954.txt
|
||||
*/
|
||||
public function authenticate(EsmtpTransport $client): void
|
||||
{
|
||||
$client->executeCommand("AUTH LOGIN\r\n", [334]);
|
||||
$client->executeCommand(sprintf("%s\r\n", base64_encode($client->getUsername())), [334]);
|
||||
$client->executeCommand(sprintf("%s\r\n", base64_encode($client->getPassword())), [235]);
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Mailer\Transport\Smtp\Auth;
|
||||
|
||||
use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport;
|
||||
|
||||
/**
|
||||
* Handles PLAIN authentication.
|
||||
*
|
||||
* @author Chris Corbyn
|
||||
*/
|
||||
class PlainAuthenticator implements AuthenticatorInterface
|
||||
{
|
||||
public function getAuthKeyword(): string
|
||||
{
|
||||
return 'PLAIN';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @see https://www.ietf.org/rfc/rfc4954.txt
|
||||
*/
|
||||
public function authenticate(EsmtpTransport $client): void
|
||||
{
|
||||
$client->executeCommand(sprintf("AUTH PLAIN %s\r\n", base64_encode($client->getUsername().\chr(0).$client->getUsername().\chr(0).$client->getPassword())), [235]);
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Mailer\Transport\Smtp\Auth;
|
||||
|
||||
use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport;
|
||||
|
||||
/**
|
||||
* Handles XOAUTH2 authentication.
|
||||
*
|
||||
* @author xu.li<AthenaLightenedMyPath@gmail.com>
|
||||
*
|
||||
* @see https://developers.google.com/google-apps/gmail/xoauth2_protocol
|
||||
*/
|
||||
class XOAuth2Authenticator implements AuthenticatorInterface
|
||||
{
|
||||
public function getAuthKeyword(): string
|
||||
{
|
||||
return 'XOAUTH2';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @see https://developers.google.com/google-apps/gmail/xoauth2_protocol#the_sasl_xoauth2_mechanism
|
||||
*/
|
||||
public function authenticate(EsmtpTransport $client): void
|
||||
{
|
||||
$client->executeCommand('AUTH XOAUTH2 '.base64_encode('user='.$client->getUsername()."\1auth=Bearer ".$client->getPassword()."\1\1")."\r\n", [235]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Mailer\Transport\Smtp;
|
||||
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Mailer\Exception\TransportException;
|
||||
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
|
||||
use Symfony\Component\Mailer\Transport\Smtp\Auth\AuthenticatorInterface;
|
||||
use Symfony\Component\Mailer\Transport\Smtp\Stream\SocketStream;
|
||||
|
||||
/**
|
||||
* Sends Emails over SMTP with ESMTP support.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
* @author Chris Corbyn
|
||||
*/
|
||||
class EsmtpTransport extends SmtpTransport
|
||||
{
|
||||
private $authenticators = [];
|
||||
private $username = '';
|
||||
private $password = '';
|
||||
|
||||
public function __construct(string $host = 'localhost', int $port = 0, ?bool $tls = null, ?EventDispatcherInterface $dispatcher = null, ?LoggerInterface $logger = null)
|
||||
{
|
||||
parent::__construct(null, $dispatcher, $logger);
|
||||
|
||||
// order is important here (roughly most secure and popular first)
|
||||
$this->authenticators = [
|
||||
new Auth\CramMd5Authenticator(),
|
||||
new Auth\LoginAuthenticator(),
|
||||
new Auth\PlainAuthenticator(),
|
||||
new Auth\XOAuth2Authenticator(),
|
||||
];
|
||||
|
||||
/** @var SocketStream $stream */
|
||||
$stream = $this->getStream();
|
||||
|
||||
if (null === $tls) {
|
||||
if (465 === $port) {
|
||||
$tls = true;
|
||||
} else {
|
||||
$tls = \defined('OPENSSL_VERSION_NUMBER') && 0 === $port && 'localhost' !== $host;
|
||||
}
|
||||
}
|
||||
if (!$tls) {
|
||||
$stream->disableTls();
|
||||
}
|
||||
if (0 === $port) {
|
||||
$port = $tls ? 465 : 25;
|
||||
}
|
||||
|
||||
$stream->setHost($host);
|
||||
$stream->setPort($port);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setUsername(string $username): self
|
||||
{
|
||||
$this->username = $username;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getUsername(): string
|
||||
{
|
||||
return $this->username;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setPassword(string $password): self
|
||||
{
|
||||
$this->password = $password;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPassword(): string
|
||||
{
|
||||
return $this->password;
|
||||
}
|
||||
|
||||
public function addAuthenticator(AuthenticatorInterface $authenticator): void
|
||||
{
|
||||
$this->authenticators[] = $authenticator;
|
||||
}
|
||||
|
||||
protected function doHeloCommand(): void
|
||||
{
|
||||
if (!$capabilities = $this->callHeloCommand()) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var SocketStream $stream */
|
||||
$stream = $this->getStream();
|
||||
// WARNING: !$stream->isTLS() is right, 100% sure :)
|
||||
// if you think that the ! should be removed, read the code again
|
||||
// if doing so "fixes" your issue then it probably means your SMTP server behaves incorrectly or is wrongly configured
|
||||
if (!$stream->isTLS() && \defined('OPENSSL_VERSION_NUMBER') && \array_key_exists('STARTTLS', $capabilities)) {
|
||||
$this->executeCommand("STARTTLS\r\n", [220]);
|
||||
|
||||
if (!$stream->startTLS()) {
|
||||
throw new TransportException('Unable to connect with STARTTLS.');
|
||||
}
|
||||
|
||||
$capabilities = $this->callHeloCommand();
|
||||
}
|
||||
|
||||
if (\array_key_exists('AUTH', $capabilities)) {
|
||||
$this->handleAuth($capabilities['AUTH']);
|
||||
}
|
||||
}
|
||||
|
||||
private function callHeloCommand(): array
|
||||
{
|
||||
try {
|
||||
$response = $this->executeCommand(sprintf("EHLO %s\r\n", $this->getLocalDomain()), [250]);
|
||||
} catch (TransportExceptionInterface $e) {
|
||||
try {
|
||||
parent::doHeloCommand();
|
||||
|
||||
return [];
|
||||
} catch (TransportExceptionInterface $ex) {
|
||||
if (!$ex->getCode()) {
|
||||
throw $e;
|
||||
}
|
||||
|
||||
throw $ex;
|
||||
}
|
||||
}
|
||||
|
||||
$capabilities = [];
|
||||
$lines = explode("\r\n", trim($response));
|
||||
array_shift($lines);
|
||||
foreach ($lines as $line) {
|
||||
if (preg_match('/^[0-9]{3}[ -]([A-Z0-9-]+)((?:[ =].*)?)$/Di', $line, $matches)) {
|
||||
$value = strtoupper(ltrim($matches[2], ' ='));
|
||||
$capabilities[strtoupper($matches[1])] = $value ? explode(' ', $value) : [];
|
||||
}
|
||||
}
|
||||
|
||||
return $capabilities;
|
||||
}
|
||||
|
||||
private function handleAuth(array $modes): void
|
||||
{
|
||||
if (!$this->username) {
|
||||
return;
|
||||
}
|
||||
|
||||
$authNames = [];
|
||||
$errors = [];
|
||||
$modes = array_map('strtolower', $modes);
|
||||
foreach ($this->authenticators as $authenticator) {
|
||||
if (!\in_array(strtolower($authenticator->getAuthKeyword()), $modes, true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$authNames[] = $authenticator->getAuthKeyword();
|
||||
try {
|
||||
$authenticator->authenticate($this);
|
||||
|
||||
return;
|
||||
} catch (TransportExceptionInterface $e) {
|
||||
try {
|
||||
$this->executeCommand("RSET\r\n", [250]);
|
||||
} catch (TransportExceptionInterface $_) {
|
||||
// ignore this exception as it probably means that the server error was final
|
||||
}
|
||||
|
||||
// keep the error message, but tries the other authenticators
|
||||
$errors[$authenticator->getAuthKeyword()] = $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
if (!$authNames) {
|
||||
throw new TransportException(sprintf('Failed to find an authenticator supported by the SMTP server, which currently supports: "%s".', implode('", "', $modes)));
|
||||
}
|
||||
|
||||
$message = sprintf('Failed to authenticate on SMTP server with username "%s" using the following authenticators: "%s".', $this->username, implode('", "', $authNames));
|
||||
foreach ($errors as $name => $error) {
|
||||
$message .= sprintf(' Authenticator "%s" returned "%s".', $name, $error);
|
||||
}
|
||||
|
||||
throw new TransportException($message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Mailer\Transport\Smtp;
|
||||
|
||||
use Symfony\Component\Mailer\Transport\AbstractTransportFactory;
|
||||
use Symfony\Component\Mailer\Transport\Dsn;
|
||||
use Symfony\Component\Mailer\Transport\Smtp\Stream\SocketStream;
|
||||
use Symfony\Component\Mailer\Transport\TransportInterface;
|
||||
|
||||
/**
|
||||
* @author Konstantin Myakshin <molodchick@gmail.com>
|
||||
*/
|
||||
final class EsmtpTransportFactory extends AbstractTransportFactory
|
||||
{
|
||||
public function create(Dsn $dsn): TransportInterface
|
||||
{
|
||||
$tls = 'smtps' === $dsn->getScheme() ? true : null;
|
||||
$port = $dsn->getPort(0);
|
||||
$host = $dsn->getHost();
|
||||
|
||||
$transport = new EsmtpTransport($host, $port, $tls, $this->dispatcher, $this->logger);
|
||||
|
||||
if ('' !== $dsn->getOption('verify_peer') && !filter_var($dsn->getOption('verify_peer', true), \FILTER_VALIDATE_BOOLEAN)) {
|
||||
/** @var SocketStream $stream */
|
||||
$stream = $transport->getStream();
|
||||
$streamOptions = $stream->getStreamOptions();
|
||||
|
||||
$streamOptions['ssl']['verify_peer'] = false;
|
||||
$streamOptions['ssl']['verify_peer_name'] = false;
|
||||
|
||||
$stream->setStreamOptions($streamOptions);
|
||||
}
|
||||
|
||||
if ($user = $dsn->getUser()) {
|
||||
$transport->setUsername($user);
|
||||
}
|
||||
|
||||
if ($password = $dsn->getPassword()) {
|
||||
$transport->setPassword($password);
|
||||
}
|
||||
|
||||
if (null !== ($localDomain = $dsn->getOption('local_domain'))) {
|
||||
$transport->setLocalDomain($localDomain);
|
||||
}
|
||||
|
||||
if (null !== ($restartThreshold = $dsn->getOption('restart_threshold'))) {
|
||||
$transport->setRestartThreshold((int) $restartThreshold, (int) $dsn->getOption('restart_threshold_sleep', 0));
|
||||
}
|
||||
|
||||
if (null !== ($pingThreshold = $dsn->getOption('ping_threshold'))) {
|
||||
$transport->setPingThreshold((int) $pingThreshold);
|
||||
}
|
||||
|
||||
return $transport;
|
||||
}
|
||||
|
||||
protected function getSupportedSchemes(): array
|
||||
{
|
||||
return ['smtp', 'smtps'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,361 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Mailer\Transport\Smtp;
|
||||
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Mailer\Envelope;
|
||||
use Symfony\Component\Mailer\Exception\LogicException;
|
||||
use Symfony\Component\Mailer\Exception\TransportException;
|
||||
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
|
||||
use Symfony\Component\Mailer\SentMessage;
|
||||
use Symfony\Component\Mailer\Transport\AbstractTransport;
|
||||
use Symfony\Component\Mailer\Transport\Smtp\Stream\AbstractStream;
|
||||
use Symfony\Component\Mailer\Transport\Smtp\Stream\SocketStream;
|
||||
use Symfony\Component\Mime\RawMessage;
|
||||
|
||||
/**
|
||||
* Sends emails over SMTP.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
* @author Chris Corbyn
|
||||
*/
|
||||
class SmtpTransport extends AbstractTransport
|
||||
{
|
||||
private $started = false;
|
||||
private $restartThreshold = 100;
|
||||
private $restartThresholdSleep = 0;
|
||||
private $restartCounter;
|
||||
private $pingThreshold = 100;
|
||||
private $lastMessageTime = 0;
|
||||
private $stream;
|
||||
private $domain = '[127.0.0.1]';
|
||||
|
||||
public function __construct(?AbstractStream $stream = null, ?EventDispatcherInterface $dispatcher = null, ?LoggerInterface $logger = null)
|
||||
{
|
||||
parent::__construct($dispatcher, $logger);
|
||||
|
||||
$this->stream = $stream ?? new SocketStream();
|
||||
}
|
||||
|
||||
public function getStream(): AbstractStream
|
||||
{
|
||||
return $this->stream;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the maximum number of messages to send before re-starting the transport.
|
||||
*
|
||||
* By default, the threshold is set to 100 (and no sleep at restart).
|
||||
*
|
||||
* @param int $threshold The maximum number of messages (0 to disable)
|
||||
* @param int $sleep The number of seconds to sleep between stopping and re-starting the transport
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setRestartThreshold(int $threshold, int $sleep = 0): self
|
||||
{
|
||||
$this->restartThreshold = $threshold;
|
||||
$this->restartThresholdSleep = $sleep;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the minimum number of seconds required between two messages, before the server is pinged.
|
||||
* If the transport wants to send a message and the time since the last message exceeds the specified threshold,
|
||||
* the transport will ping the server first (NOOP command) to check if the connection is still alive.
|
||||
* Otherwise the message will be sent without pinging the server first.
|
||||
*
|
||||
* Do not set the threshold too low, as the SMTP server may drop the connection if there are too many
|
||||
* non-mail commands (like pinging the server with NOOP).
|
||||
*
|
||||
* By default, the threshold is set to 100 seconds.
|
||||
*
|
||||
* @param int $seconds The minimum number of seconds between two messages required to ping the server
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setPingThreshold(int $seconds): self
|
||||
{
|
||||
$this->pingThreshold = $seconds;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the name of the local domain that will be used in HELO.
|
||||
*
|
||||
* This should be a fully-qualified domain name and should be truly the domain
|
||||
* you're using.
|
||||
*
|
||||
* If your server does not have a domain name, use the IP address. This will
|
||||
* automatically be wrapped in square brackets as described in RFC 5321,
|
||||
* section 4.1.3.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setLocalDomain(string $domain): self
|
||||
{
|
||||
if ('' !== $domain && '[' !== $domain[0]) {
|
||||
if (filter_var($domain, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV4)) {
|
||||
$domain = '['.$domain.']';
|
||||
} elseif (filter_var($domain, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6)) {
|
||||
$domain = '[IPv6:'.$domain.']';
|
||||
}
|
||||
}
|
||||
|
||||
$this->domain = $domain;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the name of the domain that will be used in HELO.
|
||||
*
|
||||
* If an IP address was specified, this will be returned wrapped in square
|
||||
* brackets as described in RFC 5321, section 4.1.3.
|
||||
*/
|
||||
public function getLocalDomain(): string
|
||||
{
|
||||
return $this->domain;
|
||||
}
|
||||
|
||||
public function send(RawMessage $message, ?Envelope $envelope = null): ?SentMessage
|
||||
{
|
||||
try {
|
||||
$message = parent::send($message, $envelope);
|
||||
} catch (TransportExceptionInterface $e) {
|
||||
if ($this->started) {
|
||||
try {
|
||||
$this->executeCommand("RSET\r\n", [250]);
|
||||
} catch (TransportExceptionInterface $_) {
|
||||
// ignore this exception as it probably means that the server error was final
|
||||
}
|
||||
}
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$this->checkRestartThreshold();
|
||||
|
||||
return $message;
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
if ($this->stream instanceof SocketStream) {
|
||||
$name = sprintf('smtp%s://%s', ($tls = $this->stream->isTLS()) ? 's' : '', $this->stream->getHost());
|
||||
$port = $this->stream->getPort();
|
||||
if (!(25 === $port || ($tls && 465 === $port))) {
|
||||
$name .= ':'.$port;
|
||||
}
|
||||
|
||||
return $name;
|
||||
}
|
||||
|
||||
return 'smtp://sendmail';
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs a command against the stream, expecting the given response codes.
|
||||
*
|
||||
* @param int[] $codes
|
||||
*
|
||||
* @throws TransportException when an invalid response if received
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function executeCommand(string $command, array $codes): string
|
||||
{
|
||||
$this->stream->write($command);
|
||||
$response = $this->getFullResponse();
|
||||
$this->assertResponseCode($response, $codes);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
protected function doSend(SentMessage $message): void
|
||||
{
|
||||
if (microtime(true) - $this->lastMessageTime > $this->pingThreshold) {
|
||||
$this->ping();
|
||||
}
|
||||
|
||||
if (!$this->started) {
|
||||
$this->start();
|
||||
}
|
||||
|
||||
try {
|
||||
$envelope = $message->getEnvelope();
|
||||
$this->doMailFromCommand($envelope->getSender()->getEncodedAddress());
|
||||
foreach ($envelope->getRecipients() as $recipient) {
|
||||
$this->doRcptToCommand($recipient->getEncodedAddress());
|
||||
}
|
||||
|
||||
$this->executeCommand("DATA\r\n", [354]);
|
||||
try {
|
||||
foreach (AbstractStream::replace("\r\n.", "\r\n..", $message->toIterable()) as $chunk) {
|
||||
$this->stream->write($chunk, false);
|
||||
}
|
||||
$this->stream->flush();
|
||||
} catch (TransportExceptionInterface $e) {
|
||||
throw $e;
|
||||
} catch (\Exception $e) {
|
||||
$this->stream->terminate();
|
||||
$this->started = false;
|
||||
$this->getLogger()->debug(sprintf('Email transport "%s" stopped', __CLASS__));
|
||||
throw $e;
|
||||
}
|
||||
$this->executeCommand("\r\n.\r\n", [250]);
|
||||
$message->appendDebug($this->stream->getDebug());
|
||||
$this->lastMessageTime = microtime(true);
|
||||
} catch (TransportExceptionInterface $e) {
|
||||
$e->appendDebug($this->stream->getDebug());
|
||||
$this->lastMessageTime = 0;
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
protected function doHeloCommand(): void
|
||||
{
|
||||
$this->executeCommand(sprintf("HELO %s\r\n", $this->domain), [250]);
|
||||
}
|
||||
|
||||
private function doMailFromCommand(string $address): void
|
||||
{
|
||||
$this->executeCommand(sprintf("MAIL FROM:<%s>\r\n", $address), [250]);
|
||||
}
|
||||
|
||||
private function doRcptToCommand(string $address): void
|
||||
{
|
||||
$this->executeCommand(sprintf("RCPT TO:<%s>\r\n", $address), [250, 251, 252]);
|
||||
}
|
||||
|
||||
private function start(): void
|
||||
{
|
||||
if ($this->started) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->getLogger()->debug(sprintf('Email transport "%s" starting', __CLASS__));
|
||||
|
||||
$this->stream->initialize();
|
||||
$this->assertResponseCode($this->getFullResponse(), [220]);
|
||||
$this->doHeloCommand();
|
||||
$this->started = true;
|
||||
$this->lastMessageTime = 0;
|
||||
|
||||
$this->getLogger()->debug(sprintf('Email transport "%s" started', __CLASS__));
|
||||
}
|
||||
|
||||
private function stop(): void
|
||||
{
|
||||
if (!$this->started) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->getLogger()->debug(sprintf('Email transport "%s" stopping', __CLASS__));
|
||||
|
||||
try {
|
||||
$this->executeCommand("QUIT\r\n", [221]);
|
||||
} catch (TransportExceptionInterface $e) {
|
||||
} finally {
|
||||
$this->stream->terminate();
|
||||
$this->started = false;
|
||||
$this->getLogger()->debug(sprintf('Email transport "%s" stopped', __CLASS__));
|
||||
}
|
||||
}
|
||||
|
||||
private function ping(): void
|
||||
{
|
||||
if (!$this->started) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->executeCommand("NOOP\r\n", [250]);
|
||||
} catch (TransportExceptionInterface $e) {
|
||||
$this->stop();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws TransportException if a response code is incorrect
|
||||
*/
|
||||
private function assertResponseCode(string $response, array $codes): void
|
||||
{
|
||||
if (!$codes) {
|
||||
throw new LogicException('You must set the expected response code.');
|
||||
}
|
||||
|
||||
[$code] = sscanf($response, '%3d');
|
||||
$valid = \in_array($code, $codes);
|
||||
|
||||
if (!$valid || !$response) {
|
||||
$codeStr = $code ? sprintf('code "%s"', $code) : 'empty code';
|
||||
$responseStr = $response ? sprintf(', with message "%s"', trim($response)) : '';
|
||||
|
||||
throw new TransportException(sprintf('Expected response code "%s" but got ', implode('/', $codes)).$codeStr.$responseStr.'.', $code ?: 0);
|
||||
}
|
||||
}
|
||||
|
||||
private function getFullResponse(): string
|
||||
{
|
||||
$response = '';
|
||||
do {
|
||||
$line = $this->stream->readLine();
|
||||
$response .= $line;
|
||||
} while ($line && isset($line[3]) && ' ' !== $line[3]);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
private function checkRestartThreshold(): void
|
||||
{
|
||||
// when using sendmail via non-interactive mode, the transport is never "started"
|
||||
if (!$this->started) {
|
||||
return;
|
||||
}
|
||||
|
||||
++$this->restartCounter;
|
||||
if ($this->restartCounter < $this->restartThreshold) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->stop();
|
||||
if (0 < $sleep = $this->restartThresholdSleep) {
|
||||
$this->getLogger()->debug(sprintf('Email transport "%s" sleeps for %d seconds after stopping', __CLASS__, $sleep));
|
||||
|
||||
sleep($sleep);
|
||||
}
|
||||
$this->start();
|
||||
$this->restartCounter = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function __sleep()
|
||||
{
|
||||
throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
|
||||
}
|
||||
|
||||
public function __wakeup()
|
||||
{
|
||||
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
|
||||
}
|
||||
|
||||
public function __destruct()
|
||||
{
|
||||
$this->stop();
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user