feature #35422 [Messenger] Move Transports to separate packages (Nyholm)
This PR was squashed before being merged into the 5.1-dev branch (closes #35422).
Discussion
----------
[Messenger] Move Transports to separate packages
| Q | A
| ------------- | ---
| Branch? | master
| Bug fix? | no
| New feature? | no
| Deprecations? | no
| Tickets |
| License | MIT
| Doc PR | coming
I think it is a good idea to have the transports in a separate package. The benefits a many:
- It allows us to have usage statistics
- The core messenger package is smaller
- Transports can have dependencies specified in composer.json instead of just suggests
This PR will not break BC but it requires to configure subtree split.
Commits
-------
2990c8f1e7
[Messenger] Move Transports to separate packages
This commit is contained in:
commit
3945a5c80e
|
@ -24,6 +24,13 @@ HttpFoundation
|
|||
`RedirectResponse::create()`, and `StreamedResponse::create()` methods (use
|
||||
`__construct()` instead)
|
||||
|
||||
Messenger
|
||||
---------
|
||||
|
||||
* Deprecated AmqpExt transport. It has moved to a separate package. Run `composer require symfony/amqp-messenger` to use the new classes.
|
||||
* Deprecated Doctrine transport. It has moved to a separate package. Run `composer require symfony/doctrine-messenger` to use the new classes.
|
||||
* Deprecated RedisExt transport. It has moved to a separate package. Run `composer require symfony/redis-messenger` to use the new classes.
|
||||
|
||||
Routing
|
||||
-------
|
||||
|
||||
|
|
|
@ -24,6 +24,13 @@ HttpFoundation
|
|||
`RedirectResponse::create()`, and `StreamedResponse::create()` methods (use
|
||||
`__construct()` instead)
|
||||
|
||||
Messenger
|
||||
---------
|
||||
|
||||
* Removed AmqpExt transport. Run `composer require symfony/amqp-messenger` to keep the transport in your application.
|
||||
* Removed Doctrine transport. Run `composer require symfony/doctrine-messenger` to keep the transport in your application.
|
||||
* Removed RedisExt transport. Run `composer require symfony/redis-messenger` to keep the transport in your application.
|
||||
|
||||
Routing
|
||||
-------
|
||||
|
||||
|
|
|
@ -81,6 +81,8 @@ use Symfony\Component\Mailer\Bridge\Mailgun\Transport\MailgunTransportFactory;
|
|||
use Symfony\Component\Mailer\Bridge\Postmark\Transport\PostmarkTransportFactory;
|
||||
use Symfony\Component\Mailer\Bridge\Sendgrid\Transport\SendgridTransportFactory;
|
||||
use Symfony\Component\Mailer\Mailer;
|
||||
use Symfony\Component\Messenger\Bridge\Amqp\Transport\AmqpTransportFactory;
|
||||
use Symfony\Component\Messenger\Bridge\Redis\Transport\RedisTransportFactory;
|
||||
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
|
||||
use Symfony\Component\Messenger\MessageBus;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
|
@ -312,6 +314,22 @@ class FrameworkExtension extends Extension
|
|||
$container->removeDefinition('console.command.messenger_failed_messages_show');
|
||||
$container->removeDefinition('console.command.messenger_failed_messages_remove');
|
||||
$container->removeDefinition('cache.messenger.restart_workers_signal');
|
||||
|
||||
if ($container->hasDefinition('messenger.transport.amqp.factory') && !class_exists(AmqpTransportFactory::class)) {
|
||||
if (class_exists(\Symfony\Component\Messenger\Transport\AmqpExt\AmqpTransportFactory::class)) {
|
||||
$container->getDefinition('messenger.transport.amqp.factory')->setClass(\Symfony\Component\Messenger\Transport\AmqpExt\AmqpTransportFactory::class);
|
||||
} else {
|
||||
$container->removeDefinition('messenger.transport.amqp.factory');
|
||||
}
|
||||
}
|
||||
|
||||
if ($container->hasDefinition('messenger.transport.redis.factory') && !class_exists(RedisTransportFactory::class)) {
|
||||
if (class_exists(\Symfony\Component\Messenger\Transport\RedisExt\RedisTransportFactory::class)) {
|
||||
$container->getDefinition('messenger.transport.redis.factory')->setClass(\Symfony\Component\Messenger\Transport\RedisExt\RedisTransportFactory::class);
|
||||
} else {
|
||||
$container->removeDefinition('messenger.transport.redis.factory');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->httpClientConfigEnabled = $this->isConfigEnabled($container, $config['http_client'])) {
|
||||
|
|
|
@ -67,11 +67,11 @@
|
|||
<argument type="tagged_iterator" tag="messenger.transport_factory" />
|
||||
</service>
|
||||
|
||||
<service id="messenger.transport.amqp.factory" class="Symfony\Component\Messenger\Transport\AmqpExt\AmqpTransportFactory">
|
||||
<service id="messenger.transport.amqp.factory" class="Symfony\Component\Messenger\Bridge\Amqp\Transport\AmqpTransportFactory">
|
||||
<tag name="messenger.transport_factory" />
|
||||
</service>
|
||||
|
||||
<service id="messenger.transport.redis.factory" class="Symfony\Component\Messenger\Transport\RedisExt\RedisTransportFactory">
|
||||
<service id="messenger.transport.redis.factory" class="Symfony\Component\Messenger\Bridge\Redis\Transport\RedisTransportFactory">
|
||||
<tag name="messenger.transport_factory" />
|
||||
</service>
|
||||
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
/Tests export-ignore
|
||||
/phpunit.xml.dist export-ignore
|
||||
/.gitignore export-ignore
|
|
@ -0,0 +1,3 @@
|
|||
vendor/
|
||||
composer.lock
|
||||
phpunit.xml
|
|
@ -0,0 +1,7 @@
|
|||
CHANGELOG
|
||||
=========
|
||||
|
||||
5.1.0
|
||||
-----
|
||||
|
||||
* Introduced the AMQP bridge.
|
|
@ -0,0 +1,19 @@
|
|||
Copyright (c) 2018-2020 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,18 @@
|
|||
<?php
|
||||
|
||||
namespace Symfony\Component\Messenger\Bridge\Amqp\Tests\Fixtures;
|
||||
|
||||
class DummyMessage
|
||||
{
|
||||
private $message;
|
||||
|
||||
public function __construct(string $message)
|
||||
{
|
||||
$this->message = $message;
|
||||
}
|
||||
|
||||
public function getMessage(): string
|
||||
{
|
||||
return $this->message;
|
||||
}
|
||||
}
|
|
@ -3,7 +3,7 @@
|
|||
$componentRoot = $_SERVER['COMPONENT_ROOT'];
|
||||
|
||||
if (!is_file($autoload = $componentRoot.'/vendor/autoload.php')) {
|
||||
$autoload = $componentRoot.'/../../../../vendor/autoload.php';
|
||||
$autoload = $componentRoot.'/../../../../../../vendor/autoload.php';
|
||||
}
|
||||
|
||||
if (!file_exists($autoload)) {
|
||||
|
@ -17,8 +17,8 @@ use Symfony\Component\Messenger\Envelope;
|
|||
use Symfony\Component\Messenger\EventListener\DispatchPcntlSignalListener;
|
||||
use Symfony\Component\Messenger\EventListener\StopWorkerOnSigtermSignalListener;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
use Symfony\Component\Messenger\Transport\AmqpExt\AmqpReceiver;
|
||||
use Symfony\Component\Messenger\Transport\AmqpExt\Connection;
|
||||
use Symfony\Component\Messenger\Bridge\Amqp\Transport\AmqpReceiver;
|
||||
use Symfony\Component\Messenger\Bridge\Amqp\Transport\Connection;
|
||||
use Symfony\Component\Messenger\Transport\Serialization\Serializer;
|
||||
use Symfony\Component\Messenger\Worker;
|
||||
use Symfony\Component\Serializer as SerializerComponent;
|
|
@ -9,18 +9,18 @@
|
|||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Messenger\Tests\Transport\AmqpExt;
|
||||
namespace Symfony\Component\Messenger\Bridge\Amqp\Tests\Transport;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Messenger\Bridge\Amqp\Tests\Fixtures\DummyMessage;
|
||||
use Symfony\Component\Messenger\Bridge\Amqp\Transport\AmqpReceivedStamp;
|
||||
use Symfony\Component\Messenger\Bridge\Amqp\Transport\AmqpReceiver;
|
||||
use Symfony\Component\Messenger\Bridge\Amqp\Transport\AmqpSender;
|
||||
use Symfony\Component\Messenger\Bridge\Amqp\Transport\AmqpStamp;
|
||||
use Symfony\Component\Messenger\Bridge\Amqp\Transport\Connection;
|
||||
use Symfony\Component\Messenger\Envelope;
|
||||
use Symfony\Component\Messenger\Stamp\DelayStamp;
|
||||
use Symfony\Component\Messenger\Stamp\RedeliveryStamp;
|
||||
use Symfony\Component\Messenger\Tests\Fixtures\DummyMessage;
|
||||
use Symfony\Component\Messenger\Transport\AmqpExt\AmqpReceivedStamp;
|
||||
use Symfony\Component\Messenger\Transport\AmqpExt\AmqpReceiver;
|
||||
use Symfony\Component\Messenger\Transport\AmqpExt\AmqpSender;
|
||||
use Symfony\Component\Messenger\Transport\AmqpExt\AmqpStamp;
|
||||
use Symfony\Component\Messenger\Transport\AmqpExt\Connection;
|
||||
use Symfony\Component\Messenger\Transport\Receiver\ReceiverInterface;
|
||||
use Symfony\Component\Messenger\Transport\Serialization\Serializer;
|
||||
use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface;
|
||||
|
@ -148,8 +148,8 @@ class AmqpExtIntegrationTest extends TestCase
|
|||
|
||||
$amqpReadTimeout = 30;
|
||||
$dsn = getenv('MESSENGER_AMQP_DSN').'?read_timeout='.$amqpReadTimeout;
|
||||
$process = new PhpProcess(file_get_contents(__DIR__.'/Fixtures/long_receiver.php'), null, [
|
||||
'COMPONENT_ROOT' => __DIR__.'/../../../',
|
||||
$process = new PhpProcess(file_get_contents(__DIR__.'/../Fixtures/long_receiver.php'), null, [
|
||||
'COMPONENT_ROOT' => __DIR__.'/../../',
|
||||
'DSN' => $dsn,
|
||||
]);
|
||||
|
||||
|
@ -171,9 +171,9 @@ class AmqpExtIntegrationTest extends TestCase
|
|||
$this->assertFalse($process->isRunning());
|
||||
$this->assertLessThan($amqpReadTimeout, microtime(true) - $signalTime);
|
||||
$this->assertSame($expectedOutput.<<<'TXT'
|
||||
Get envelope with message: Symfony\Component\Messenger\Tests\Fixtures\DummyMessage
|
||||
Get envelope with message: Symfony\Component\Messenger\Bridge\Amqp\Tests\Fixtures\DummyMessage
|
||||
with stamps: [
|
||||
"Symfony\\Component\\Messenger\\Transport\\AmqpExt\\AmqpReceivedStamp",
|
||||
"Symfony\\Component\\Messenger\\Bridge\\Amqp\\Transport\\AmqpReceivedStamp",
|
||||
"Symfony\\Component\\Messenger\\Stamp\\ReceivedStamp",
|
||||
"Symfony\\Component\\Messenger\\Stamp\\ConsumedByWorkerStamp"
|
||||
]
|
|
@ -9,10 +9,10 @@
|
|||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Messenger\Tests\Transport\AmqpExt;
|
||||
namespace Symfony\Component\Messenger\Bridge\Amqp\Tests\Transport;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Messenger\Transport\AmqpExt\AmqpReceivedStamp;
|
||||
use Symfony\Component\Messenger\Bridge\Amqp\Transport\AmqpReceivedStamp;
|
||||
|
||||
/**
|
||||
* @requires extension amqp
|
|
@ -9,14 +9,14 @@
|
|||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Messenger\Tests\Transport\AmqpExt;
|
||||
namespace Symfony\Component\Messenger\Bridge\Amqp\Tests\Transport;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Messenger\Bridge\Amqp\Tests\Fixtures\DummyMessage;
|
||||
use Symfony\Component\Messenger\Bridge\Amqp\Transport\AmqpReceivedStamp;
|
||||
use Symfony\Component\Messenger\Bridge\Amqp\Transport\AmqpReceiver;
|
||||
use Symfony\Component\Messenger\Bridge\Amqp\Transport\Connection;
|
||||
use Symfony\Component\Messenger\Envelope;
|
||||
use Symfony\Component\Messenger\Tests\Fixtures\DummyMessage;
|
||||
use Symfony\Component\Messenger\Transport\AmqpExt\AmqpReceivedStamp;
|
||||
use Symfony\Component\Messenger\Transport\AmqpExt\AmqpReceiver;
|
||||
use Symfony\Component\Messenger\Transport\AmqpExt\Connection;
|
||||
use Symfony\Component\Messenger\Transport\Serialization\Serializer;
|
||||
use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface;
|
||||
use Symfony\Component\Serializer as SerializerComponent;
|
|
@ -9,14 +9,14 @@
|
|||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Messenger\Tests\Transport\AmqpExt;
|
||||
namespace Symfony\Component\Messenger\Bridge\Amqp\Tests\Transport;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Messenger\Bridge\Amqp\Tests\Fixtures\DummyMessage;
|
||||
use Symfony\Component\Messenger\Bridge\Amqp\Transport\AmqpSender;
|
||||
use Symfony\Component\Messenger\Bridge\Amqp\Transport\AmqpStamp;
|
||||
use Symfony\Component\Messenger\Bridge\Amqp\Transport\Connection;
|
||||
use Symfony\Component\Messenger\Envelope;
|
||||
use Symfony\Component\Messenger\Tests\Fixtures\DummyMessage;
|
||||
use Symfony\Component\Messenger\Transport\AmqpExt\AmqpSender;
|
||||
use Symfony\Component\Messenger\Transport\AmqpExt\AmqpStamp;
|
||||
use Symfony\Component\Messenger\Transport\AmqpExt\Connection;
|
||||
use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface;
|
||||
|
||||
/**
|
|
@ -9,10 +9,10 @@
|
|||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Messenger\Tests\Transport\AmqpExt;
|
||||
namespace Symfony\Component\Messenger\Bridge\Amqp\Tests\Transport;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Messenger\Transport\AmqpExt\AmqpStamp;
|
||||
use Symfony\Component\Messenger\Bridge\Amqp\Transport\AmqpStamp;
|
||||
|
||||
/**
|
||||
* @requires extension amqp
|
|
@ -9,12 +9,12 @@
|
|||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Messenger\Tests\Transport\AmqpExt;
|
||||
namespace Symfony\Component\Messenger\Bridge\Amqp\Tests\Transport;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Messenger\Transport\AmqpExt\AmqpTransport;
|
||||
use Symfony\Component\Messenger\Transport\AmqpExt\AmqpTransportFactory;
|
||||
use Symfony\Component\Messenger\Transport\AmqpExt\Connection;
|
||||
use Symfony\Component\Messenger\Bridge\Amqp\Transport\AmqpTransport;
|
||||
use Symfony\Component\Messenger\Bridge\Amqp\Transport\AmqpTransportFactory;
|
||||
use Symfony\Component\Messenger\Bridge\Amqp\Transport\Connection;
|
||||
use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface;
|
||||
|
||||
class AmqpTransportFactoryTest extends TestCase
|
|
@ -9,13 +9,13 @@
|
|||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Messenger\Tests\Transport\AmqpExt;
|
||||
namespace Symfony\Component\Messenger\Bridge\Amqp\Tests\Transport;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Messenger\Bridge\Amqp\Tests\Fixtures\DummyMessage;
|
||||
use Symfony\Component\Messenger\Bridge\Amqp\Transport\AmqpTransport;
|
||||
use Symfony\Component\Messenger\Bridge\Amqp\Transport\Connection;
|
||||
use Symfony\Component\Messenger\Envelope;
|
||||
use Symfony\Component\Messenger\Tests\Fixtures\DummyMessage;
|
||||
use Symfony\Component\Messenger\Transport\AmqpExt\AmqpTransport;
|
||||
use Symfony\Component\Messenger\Transport\AmqpExt\Connection;
|
||||
use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface;
|
||||
use Symfony\Component\Messenger\Transport\TransportInterface;
|
||||
|
|
@ -9,14 +9,14 @@
|
|||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Messenger\Tests\Transport\AmqpExt;
|
||||
namespace Symfony\Component\Messenger\Bridge\Amqp\Tests\Transport;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Messenger\Bridge\Amqp\Tests\Fixtures\DummyMessage;
|
||||
use Symfony\Component\Messenger\Bridge\Amqp\Transport\AmqpFactory;
|
||||
use Symfony\Component\Messenger\Bridge\Amqp\Transport\AmqpStamp;
|
||||
use Symfony\Component\Messenger\Bridge\Amqp\Transport\Connection;
|
||||
use Symfony\Component\Messenger\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Messenger\Tests\Fixtures\DummyMessage;
|
||||
use Symfony\Component\Messenger\Transport\AmqpExt\AmqpFactory;
|
||||
use Symfony\Component\Messenger\Transport\AmqpExt\AmqpStamp;
|
||||
use Symfony\Component\Messenger\Transport\AmqpExt\Connection;
|
||||
|
||||
/**
|
||||
* @requires extension amqp
|
|
@ -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\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);
|
||||
}
|
||||
}
|
||||
class_alias(AmqpFactory::class, \Symfony\Component\Messenger\Transport\AmqpExt\AmqpFactory::class);
|
|
@ -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\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;
|
||||
}
|
||||
}
|
||||
class_alias(AmqpReceivedStamp::class, \Symfony\Component\Messenger\Transport\AmqpExt\AmqpReceivedStamp::class);
|
|
@ -0,0 +1,139 @@
|
|||
<?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\ReceiverInterface;
|
||||
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 ReceiverInterface, 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
|
||||
{
|
||||
foreach ($this->connection->getQueueNames() 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;
|
||||
}
|
||||
}
|
||||
class_alias(AmqpReceiver::class, \Symfony\Component\Messenger\Transport\AmqpExt\AmqpReceiver::class);
|
|
@ -0,0 +1,78 @@
|
|||
<?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\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);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->connection->publish(
|
||||
$encodedMessage['body'],
|
||||
$encodedMessage['headers'] ?? [],
|
||||
$delay,
|
||||
$amqpStamp
|
||||
);
|
||||
} catch (\AMQPException $e) {
|
||||
throw new TransportException($e->getMessage(), 0, $e);
|
||||
}
|
||||
|
||||
return $envelope;
|
||||
}
|
||||
}
|
||||
class_alias(AmqpSender::class, \Symfony\Component\Messenger\Transport\AmqpExt\AmqpSender::class);
|
|
@ -0,0 +1,77 @@
|
|||
<?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;
|
||||
|
||||
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): 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();
|
||||
|
||||
return new self($previousStamp->routingKey ?? $amqpEnvelope->getRoutingKey(), $previousStamp->flags ?? AMQP_NOPARAM, $attr);
|
||||
}
|
||||
|
||||
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)
|
||||
);
|
||||
}
|
||||
}
|
||||
class_alias(AmqpStamp::class, \Symfony\Component\Messenger\Transport\AmqpExt\AmqpStamp::class);
|
|
@ -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\Messenger\Bridge\Amqp\Transport;
|
||||
|
||||
use Symfony\Component\Messenger\Envelope;
|
||||
use Symfony\Component\Messenger\Transport\Receiver\MessageCountAwareInterface;
|
||||
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 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 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);
|
||||
}
|
||||
}
|
||||
class_alias(AmqpTransport::class, \Symfony\Component\Messenger\Transport\AmqpExt\AmqpTransport::class);
|
|
@ -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\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://');
|
||||
}
|
||||
}
|
||||
class_alias(AmqpTransportFactory::class, \Symfony\Component\Messenger\Transport\AmqpExt\AmqpTransportFactory::class);
|
|
@ -0,0 +1,474 @@
|
|||
<?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;
|
||||
|
||||
/**
|
||||
* 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',
|
||||
];
|
||||
|
||||
private $connectionOptions;
|
||||
private $exchangeOptions;
|
||||
private $queuesOptions;
|
||||
private $amqpFactory;
|
||||
|
||||
/**
|
||||
* @var \AMQPChannel|null
|
||||
*/
|
||||
private $amqpChannel;
|
||||
|
||||
/**
|
||||
* @var \AMQPExchange|null
|
||||
*/
|
||||
private $amqpExchange;
|
||||
|
||||
/**
|
||||
* @var \AMQPQueue[]|null
|
||||
*/
|
||||
private $amqpQueues = [];
|
||||
|
||||
/**
|
||||
* @var \AMQPExchange|null
|
||||
*/
|
||||
private $amqpDelayExchange;
|
||||
|
||||
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->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: Username to use to connect the the AMQP service
|
||||
* * password: Password to use the connect to the AMQP service
|
||||
* * 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)
|
||||
* * prefetch_count: set channel prefetch count
|
||||
*/
|
||||
public static function fromDsn(string $dsn, array $options = [], AmqpFactory $amqpFactory = null): self
|
||||
{
|
||||
if (false === $parsedUrl = parse_url($dsn)) {
|
||||
// this is a valid URI that parse_url cannot handle when you want to pass all parameters as options
|
||||
if ('amqp://' !== $dsn) {
|
||||
throw new InvalidArgumentException(sprintf('The given AMQP DSN "%s" is invalid.', $dsn));
|
||||
}
|
||||
|
||||
$parsedUrl = [];
|
||||
}
|
||||
|
||||
$pathParts = isset($parsedUrl['path']) ? explode('/', trim($parsedUrl['path'], '/')) : [];
|
||||
$exchangeName = $pathParts[1] ?? 'messages';
|
||||
parse_str($parsedUrl['query'] ?? '', $parsedQuery);
|
||||
|
||||
$amqpOptions = array_replace_recursive([
|
||||
'host' => $parsedUrl['host'] ?? 'localhost',
|
||||
'port' => $parsedUrl['port'] ?? 5672,
|
||||
'vhost' => isset($pathParts[0]) ? urldecode($pathParts[0]) : '/',
|
||||
'exchange' => [
|
||||
'name' => $exchangeName,
|
||||
],
|
||||
], $options, $parsedQuery);
|
||||
|
||||
if (isset($parsedUrl['user'])) {
|
||||
$amqpOptions['login'] = $parsedUrl['user'];
|
||||
}
|
||||
|
||||
if (isset($parsedUrl['pass'])) {
|
||||
$amqpOptions['password'] = $parsedUrl['pass'];
|
||||
}
|
||||
|
||||
if (!isset($amqpOptions['queues'])) {
|
||||
$amqpOptions['queues'][$exchangeName] = [];
|
||||
}
|
||||
|
||||
$exchangeOptions = $amqpOptions['exchange'];
|
||||
$queuesOptions = $amqpOptions['queues'];
|
||||
unset($amqpOptions['queues'], $amqpOptions['exchange']);
|
||||
|
||||
$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);
|
||||
|
||||
return new self($amqpOptions, $exchangeOptions, $queuesOptions, $amqpFactory);
|
||||
}
|
||||
|
||||
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, \gettype($arguments[$key])));
|
||||
}
|
||||
|
||||
$arguments[$key] = (int) $arguments[$key];
|
||||
}
|
||||
|
||||
return $arguments;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \AMQPException
|
||||
*/
|
||||
public function publish(string $body, array $headers = [], int $delayInMs = 0, AmqpStamp $amqpStamp = null): void
|
||||
{
|
||||
$this->clearWhenDisconnected();
|
||||
|
||||
if (0 !== $delayInMs) {
|
||||
$this->publishWithDelay($body, $headers, $delayInMs, $amqpStamp);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->shouldSetup()) {
|
||||
$this->setupExchangeAndQueues();
|
||||
}
|
||||
|
||||
$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);
|
||||
|
||||
$this->setupDelay($delay, $routingKey);
|
||||
|
||||
$this->publishOnExchange(
|
||||
$this->getDelayExchange(),
|
||||
$body,
|
||||
$this->getRoutingKeyForDelay($delay, $routingKey),
|
||||
$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;
|
||||
|
||||
$exchange->publish(
|
||||
$body,
|
||||
$routingKey,
|
||||
$amqpStamp ? $amqpStamp->getFlags() : AMQP_NOPARAM,
|
||||
$attributes
|
||||
);
|
||||
}
|
||||
|
||||
private function setupDelay(int $delay, ?string $routingKey)
|
||||
{
|
||||
if ($this->shouldSetup()) {
|
||||
$this->setup(); // setup delay exchange and normal exchange for delay queue to DLX messages to
|
||||
}
|
||||
|
||||
$queue = $this->createDelayQueue($delay, $routingKey);
|
||||
$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));
|
||||
}
|
||||
|
||||
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): \AMQPQueue
|
||||
{
|
||||
$queue = $this->amqpFactory->createQueue($this->channel());
|
||||
$queue->setName(str_replace(
|
||||
['%delay%', '%exchange_name%', '%routing_key%'],
|
||||
[$delay, $this->exchangeOptions['name'], $routingKey ?? ''],
|
||||
$this->connectionOptions['delay']['queue_name_pattern']
|
||||
));
|
||||
$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,
|
||||
'x-dead-letter-exchange' => $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): string
|
||||
{
|
||||
return str_replace(
|
||||
['%delay%', '%exchange_name%', '%routing_key%'],
|
||||
[$delay, $this->exchangeOptions['name'], $finalRoutingKey ?? ''],
|
||||
$this->connectionOptions['delay']['queue_name_pattern']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a message from the specified queue.
|
||||
*
|
||||
* @throws \AMQPException
|
||||
*/
|
||||
public function get(string $queueName): ?\AMQPEnvelope
|
||||
{
|
||||
$this->clearWhenDisconnected();
|
||||
|
||||
if ($this->shouldSetup()) {
|
||||
$this->setupExchangeAndQueues();
|
||||
}
|
||||
|
||||
try {
|
||||
if (false !== $message = $this->queue($queueName)->get()) {
|
||||
return $message;
|
||||
}
|
||||
} catch (\AMQPQueueException $e) {
|
||||
if (404 === $e->getCode() && $this->shouldSetup()) {
|
||||
// If we get a 404 for the queue, it means we need to set up the exchange & queue.
|
||||
$this->setupExchangeAndQueues();
|
||||
|
||||
return $this->get();
|
||||
}
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function ack(\AMQPEnvelope $message, string $queueName): bool
|
||||
{
|
||||
return $this->queue($queueName)->ack($message->getDeliveryTag());
|
||||
}
|
||||
|
||||
public function nack(\AMQPEnvelope $message, string $queueName, int $flags = AMQP_NOPARAM): bool
|
||||
{
|
||||
return $this->queue($queueName)->nack($message->getDeliveryTag(), $flags);
|
||||
}
|
||||
|
||||
public function setup(): void
|
||||
{
|
||||
$this->setupExchangeAndQueues();
|
||||
$this->getDelayExchange()->declareExchange();
|
||||
}
|
||||
|
||||
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'] ?? []);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @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) {
|
||||
$credentials = $this->connectionOptions;
|
||||
$credentials['password'] = '********';
|
||||
unset($credentials['delay']);
|
||||
|
||||
throw new \AMQPException(sprintf('Could not connect to the AMQP server. Please verify the provided DSN. (%s)', json_encode($credentials)), 0, $e);
|
||||
}
|
||||
$this->amqpChannel = $this->amqpFactory->createChannel($connection);
|
||||
|
||||
if (isset($this->connectionOptions['prefetch_count'])) {
|
||||
$this->amqpChannel->setPrefetchCount($this->connectionOptions['prefetch_count']);
|
||||
}
|
||||
}
|
||||
|
||||
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->amqpChannel = null;
|
||||
$this->amqpQueues = [];
|
||||
$this->amqpExchange = null;
|
||||
$this->amqpDelayExchange = null;
|
||||
}
|
||||
}
|
||||
|
||||
private function shouldSetup(): bool
|
||||
{
|
||||
if (!\array_key_exists('auto_setup', $this->connectionOptions)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (\in_array($this->connectionOptions['auto_setup'], [false, 'false'], true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
class_alias(Connection::class, \Symfony\Component\Messenger\Transport\AmqpExt\Connection::class);
|
|
@ -0,0 +1,40 @@
|
|||
{
|
||||
"name": "symfony/amqp-messenger",
|
||||
"type": "symfony-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/messenger": "^5.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/property-access": "^4.4|^5.0",
|
||||
"symfony/serializer": "^4.4|^5.0",
|
||||
"symfony/event-dispatcher": "^4.4|^5.0",
|
||||
"symfony/process": "^4.4|^5.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": { "Symfony\\Component\\Messenger\\Bridge\\Amqp\\": "" },
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "5.1-dev"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/5.2/phpunit.xsd"
|
||||
backupGlobals="false"
|
||||
colors="true"
|
||||
bootstrap="vendor/autoload.php"
|
||||
failOnRisky="true"
|
||||
failOnWarning="true"
|
||||
>
|
||||
<php>
|
||||
<ini name="error_reporting" value="-1" />
|
||||
</php>
|
||||
|
||||
<testsuites>
|
||||
<testsuite name="Symfony AQMP Messenger Component Test Suite">
|
||||
<directory>./Tests/</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
|
||||
<filter>
|
||||
<whitelist>
|
||||
<directory>./</directory>
|
||||
<exclude>
|
||||
<directory>./Tests</directory>
|
||||
<directory>./vendor</directory>
|
||||
</exclude>
|
||||
</whitelist>
|
||||
</filter>
|
||||
</phpunit>
|
|
@ -0,0 +1,3 @@
|
|||
/Tests export-ignore
|
||||
/phpunit.xml.dist export-ignore
|
||||
/.gitignore export-ignore
|
|
@ -0,0 +1,3 @@
|
|||
vendor/
|
||||
composer.lock
|
||||
phpunit.xml
|
|
@ -0,0 +1,7 @@
|
|||
CHANGELOG
|
||||
=========
|
||||
|
||||
5.1.0
|
||||
-----
|
||||
|
||||
* Introduced the Doctrine bridge.
|
|
@ -0,0 +1,19 @@
|
|||
Copyright (c) 2018-2020 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,18 @@
|
|||
<?php
|
||||
|
||||
namespace Symfony\Component\Messenger\Bridge\Doctrine\Tests\Fixtures;
|
||||
|
||||
class DummyMessage
|
||||
{
|
||||
private $message;
|
||||
|
||||
public function __construct(string $message)
|
||||
{
|
||||
$this->message = $message;
|
||||
}
|
||||
|
||||
public function getMessage(): string
|
||||
{
|
||||
return $this->message;
|
||||
}
|
||||
}
|
|
@ -9,7 +9,7 @@
|
|||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Messenger\Tests\Transport\Doctrine;
|
||||
namespace Symfony\Component\Messenger\Bridge\Doctrine\Tests\Transport;
|
||||
|
||||
use Doctrine\DBAL\DBALException;
|
||||
use Doctrine\DBAL\Driver\Statement;
|
||||
|
@ -19,8 +19,9 @@ use Doctrine\DBAL\Schema\AbstractSchemaManager;
|
|||
use Doctrine\DBAL\Schema\SchemaConfig;
|
||||
use Doctrine\DBAL\Schema\Synchronizer\SchemaSynchronizer;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Messenger\Tests\Fixtures\DummyMessage;
|
||||
use Symfony\Component\Messenger\Transport\Doctrine\Connection;
|
||||
use Symfony\Component\Messenger\Bridge\Doctrine\Tests\Fixtures\DummyMessage;
|
||||
use Symfony\Component\Messenger\Bridge\Doctrine\Transport\Connection;
|
||||
|
||||
|
||||
class ConnectionTest extends TestCase
|
||||
{
|
|
@ -9,12 +9,12 @@
|
|||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Messenger\Tests\Transport\Doctrine;
|
||||
namespace Symfony\Component\Messenger\Bridge\Doctrine\Tests\Transport;
|
||||
|
||||
use Doctrine\DBAL\DriverManager;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Messenger\Tests\Fixtures\DummyMessage;
|
||||
use Symfony\Component\Messenger\Transport\Doctrine\Connection;
|
||||
use Symfony\Component\Messenger\Bridge\Doctrine\Tests\Fixtures\DummyMessage;
|
||||
use Symfony\Component\Messenger\Bridge\Doctrine\Transport\Connection;
|
||||
|
||||
/**
|
||||
* @requires extension pdo_sqlite
|
|
@ -9,19 +9,19 @@
|
|||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Messenger\Tests\Transport\Doctrine;
|
||||
namespace Symfony\Component\Messenger\Bridge\Doctrine\Tests\Transport;
|
||||
|
||||
use Doctrine\DBAL\Driver\PDOException;
|
||||
use Doctrine\DBAL\Exception\DeadlockException;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Messenger\Bridge\Doctrine\Tests\Fixtures\DummyMessage;
|
||||
use Symfony\Component\Messenger\Envelope;
|
||||
use Symfony\Component\Messenger\Exception\MessageDecodingFailedException;
|
||||
use Symfony\Component\Messenger\Exception\TransportException;
|
||||
use Symfony\Component\Messenger\Stamp\TransportMessageIdStamp;
|
||||
use Symfony\Component\Messenger\Tests\Fixtures\DummyMessage;
|
||||
use Symfony\Component\Messenger\Transport\Doctrine\Connection;
|
||||
use Symfony\Component\Messenger\Transport\Doctrine\DoctrineReceivedStamp;
|
||||
use Symfony\Component\Messenger\Transport\Doctrine\DoctrineReceiver;
|
||||
use Symfony\Component\Messenger\Bridge\Doctrine\Transport\Connection;
|
||||
use Symfony\Component\Messenger\Bridge\Doctrine\Transport\DoctrineReceivedStamp;
|
||||
use Symfony\Component\Messenger\Bridge\Doctrine\Transport\DoctrineReceiver;
|
||||
use Symfony\Component\Messenger\Transport\Serialization\PhpSerializer;
|
||||
use Symfony\Component\Messenger\Transport\Serialization\Serializer;
|
||||
use Symfony\Component\Serializer as SerializerComponent;
|
|
@ -9,15 +9,15 @@
|
|||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Messenger\Tests\Transport\Doctrine;
|
||||
namespace Symfony\Component\Messenger\Bridge\Doctrine\Tests\Transport;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Messenger\Bridge\Doctrine\Tests\Fixtures\DummyMessage;
|
||||
use Symfony\Component\Messenger\Envelope;
|
||||
use Symfony\Component\Messenger\Stamp\DelayStamp;
|
||||
use Symfony\Component\Messenger\Stamp\TransportMessageIdStamp;
|
||||
use Symfony\Component\Messenger\Tests\Fixtures\DummyMessage;
|
||||
use Symfony\Component\Messenger\Transport\Doctrine\Connection;
|
||||
use Symfony\Component\Messenger\Transport\Doctrine\DoctrineSender;
|
||||
use Symfony\Component\Messenger\Bridge\Doctrine\Transport\Connection;
|
||||
use Symfony\Component\Messenger\Bridge\Doctrine\Transport\DoctrineSender;
|
||||
use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface;
|
||||
|
||||
class DoctrineSenderTest extends TestCase
|
|
@ -9,15 +9,15 @@
|
|||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Messenger\Tests\Transport\Doctrine;
|
||||
namespace Symfony\Component\Messenger\Bridge\Doctrine\Tests\Transport;
|
||||
|
||||
use Doctrine\DBAL\Schema\AbstractSchemaManager;
|
||||
use Doctrine\DBAL\Schema\SchemaConfig;
|
||||
use Doctrine\Persistence\ConnectionRegistry;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Messenger\Transport\Doctrine\Connection;
|
||||
use Symfony\Component\Messenger\Transport\Doctrine\DoctrineTransport;
|
||||
use Symfony\Component\Messenger\Transport\Doctrine\DoctrineTransportFactory;
|
||||
use Symfony\Component\Messenger\Bridge\Doctrine\Transport\Connection;
|
||||
use Symfony\Component\Messenger\Bridge\Doctrine\Transport\DoctrineTransport;
|
||||
use Symfony\Component\Messenger\Bridge\Doctrine\Transport\DoctrineTransportFactory;
|
||||
use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface;
|
||||
|
||||
class DoctrineTransportFactoryTest extends TestCase
|
|
@ -9,13 +9,13 @@
|
|||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Messenger\Tests\Transport\Doctrine;
|
||||
namespace Symfony\Component\Messenger\Bridge\Doctrine\Tests\Transport;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Messenger\Bridge\Doctrine\Tests\Fixtures\DummyMessage;
|
||||
use Symfony\Component\Messenger\Envelope;
|
||||
use Symfony\Component\Messenger\Tests\Fixtures\DummyMessage;
|
||||
use Symfony\Component\Messenger\Transport\Doctrine\Connection;
|
||||
use Symfony\Component\Messenger\Transport\Doctrine\DoctrineTransport;
|
||||
use Symfony\Component\Messenger\Bridge\Doctrine\Transport\Connection;
|
||||
use Symfony\Component\Messenger\Bridge\Doctrine\Transport\DoctrineTransport;
|
||||
use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface;
|
||||
use Symfony\Component\Messenger\Transport\TransportInterface;
|
||||
|
|
@ -0,0 +1,347 @@
|
|||
<?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\DBALException;
|
||||
use Doctrine\DBAL\Driver\ResultStatement;
|
||||
use Doctrine\DBAL\Exception\TableNotFoundException;
|
||||
use Doctrine\DBAL\Query\QueryBuilder;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\DBAL\Schema\Synchronizer\SchemaSynchronizer;
|
||||
use Doctrine\DBAL\Schema\Synchronizer\SingleDatabaseSynchronizer;
|
||||
use Doctrine\DBAL\Types\Type;
|
||||
use Symfony\Component\Messenger\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Messenger\Exception\TransportException;
|
||||
|
||||
/**
|
||||
* @author Vincent Touzet <vincent.touzet@gmail.com>
|
||||
*
|
||||
* @final
|
||||
*/
|
||||
class Connection
|
||||
{
|
||||
private 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
|
||||
*/
|
||||
private $configuration = [];
|
||||
private $driverConnection;
|
||||
private $schemaSynchronizer;
|
||||
private $autoSetup;
|
||||
|
||||
public function __construct(array $configuration, DBALConnection $driverConnection, SchemaSynchronizer $schemaSynchronizer = null)
|
||||
{
|
||||
$this->configuration = array_replace_recursive(self::DEFAULT_OPTIONS, $configuration);
|
||||
$this->driverConnection = $driverConnection;
|
||||
$this->schemaSynchronizer = $schemaSynchronizer ?? new SingleDatabaseSynchronizer($this->driverConnection);
|
||||
$this->autoSetup = $this->configuration['auto_setup'];
|
||||
}
|
||||
|
||||
public function getConfiguration(): array
|
||||
{
|
||||
return $this->configuration;
|
||||
}
|
||||
|
||||
public static function buildConfiguration(string $dsn, array $options = []): array
|
||||
{
|
||||
if (false === $components = parse_url($dsn)) {
|
||||
throw new InvalidArgumentException(sprintf('The given Doctrine Messenger DSN "%s" is invalid.', $dsn));
|
||||
}
|
||||
|
||||
$query = [];
|
||||
if (isset($components['query'])) {
|
||||
parse_str($components['query'], $query);
|
||||
}
|
||||
|
||||
$configuration = ['connection' => $components['host']];
|
||||
$configuration += $options + $query + self::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(self::DEFAULT_OPTIONS));
|
||||
if (0 < \count($optionsExtraKeys)) {
|
||||
throw new InvalidArgumentException(sprintf('Unknown option found : [%s]. Allowed options are [%s]', implode(', ', $optionsExtraKeys), implode(', ', self::DEFAULT_OPTIONS)));
|
||||
}
|
||||
|
||||
// check for extra keys in options
|
||||
$queryExtraKeys = array_diff(array_keys($query), array_keys(self::DEFAULT_OPTIONS));
|
||||
if (0 < \count($queryExtraKeys)) {
|
||||
throw new InvalidArgumentException(sprintf('Unknown option found in DSN: [%s]. Allowed options are [%s]', implode(', ', $queryExtraKeys), implode(', ', self::DEFAULT_OPTIONS)));
|
||||
}
|
||||
|
||||
return $configuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $delay The delay in milliseconds
|
||||
*
|
||||
* @return string The inserted id
|
||||
*
|
||||
* @throws \Doctrine\DBAL\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->executeQuery($queryBuilder->getSQL(), [
|
||||
$body,
|
||||
json_encode($headers),
|
||||
$this->configuration['queue_name'],
|
||||
$now,
|
||||
$availableAt,
|
||||
], [
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
Type::DATETIME,
|
||||
Type::DATETIME,
|
||||
]);
|
||||
|
||||
return $this->driverConnection->lastInsertId();
|
||||
}
|
||||
|
||||
public function get(): ?array
|
||||
{
|
||||
get:
|
||||
$this->driverConnection->beginTransaction();
|
||||
try {
|
||||
$query = $this->createAvailableMessagesQueryBuilder()
|
||||
->orderBy('available_at', 'ASC')
|
||||
->setMaxResults(1);
|
||||
|
||||
// use SELECT ... FOR UPDATE to lock table
|
||||
$doctrineEnvelope = $this->executeQuery(
|
||||
$query->getSQL().' '.$this->driverConnection->getDatabasePlatform()->getWriteLockSQL(),
|
||||
$query->getParameters(),
|
||||
$query->getParameterTypes()
|
||||
)->fetch();
|
||||
|
||||
if (false === $doctrineEnvelope) {
|
||||
$this->driverConnection->commit();
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$doctrineEnvelope = $this->decodeEnvelopeHeaders($doctrineEnvelope);
|
||||
|
||||
$queryBuilder = $this->driverConnection->createQueryBuilder()
|
||||
->update($this->configuration['table_name'])
|
||||
->set('delivered_at', '?')
|
||||
->where('id = ?');
|
||||
$now = new \DateTime();
|
||||
$this->executeQuery($queryBuilder->getSQL(), [
|
||||
$now,
|
||||
$doctrineEnvelope['id'],
|
||||
], [
|
||||
Type::DATETIME,
|
||||
]);
|
||||
|
||||
$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 {
|
||||
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 {
|
||||
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();
|
||||
// Since Doctrine 2.9 the getFilterSchemaAssetsExpression is deprecated
|
||||
$hasFilterCallback = method_exists($configuration, 'getSchemaAssetsFilter');
|
||||
|
||||
if ($hasFilterCallback) {
|
||||
$assetFilter = $this->driverConnection->getConfiguration()->getSchemaAssetsFilter();
|
||||
$this->driverConnection->getConfiguration()->setSchemaAssetsFilter(null);
|
||||
} else {
|
||||
$assetFilter = $this->driverConnection->getConfiguration()->getFilterSchemaAssetsExpression();
|
||||
$this->driverConnection->getConfiguration()->setFilterSchemaAssetsExpression(null);
|
||||
}
|
||||
|
||||
$this->schemaSynchronizer->updateSchema($this->getSchema(), true);
|
||||
|
||||
if ($hasFilterCallback) {
|
||||
$this->driverConnection->getConfiguration()->setSchemaAssetsFilter($assetFilter);
|
||||
} else {
|
||||
$this->driverConnection->getConfiguration()->setFilterSchemaAssetsExpression($assetFilter);
|
||||
}
|
||||
|
||||
$this->autoSetup = false;
|
||||
}
|
||||
|
||||
public function getMessageCount(): int
|
||||
{
|
||||
$queryBuilder = $this->createAvailableMessagesQueryBuilder()
|
||||
->select('COUNT(m.id) as message_count')
|
||||
->setMaxResults(1);
|
||||
|
||||
return $this->executeQuery($queryBuilder->getSQL(), $queryBuilder->getParameters(), $queryBuilder->getParameterTypes())->fetchColumn();
|
||||
}
|
||||
|
||||
public function findAll(int $limit = null): array
|
||||
{
|
||||
$queryBuilder = $this->createAvailableMessagesQueryBuilder();
|
||||
if (null !== $limit) {
|
||||
$queryBuilder->setMaxResults($limit);
|
||||
}
|
||||
|
||||
$data = $this->executeQuery($queryBuilder->getSQL(), $queryBuilder->getParameters(), $queryBuilder->getParameterTypes())->fetchAll();
|
||||
|
||||
return array_map(function ($doctrineEnvelope) {
|
||||
return $this->decodeEnvelopeHeaders($doctrineEnvelope);
|
||||
}, $data);
|
||||
}
|
||||
|
||||
public function find($id): ?array
|
||||
{
|
||||
$queryBuilder = $this->createQueryBuilder()
|
||||
->where('m.id = ?');
|
||||
|
||||
$data = $this->executeQuery($queryBuilder->getSQL(), [
|
||||
$id,
|
||||
])->fetch();
|
||||
|
||||
return false === $data ? null : $this->decodeEnvelopeHeaders($data);
|
||||
}
|
||||
|
||||
private function createAvailableMessagesQueryBuilder(): QueryBuilder
|
||||
{
|
||||
$now = new \DateTime();
|
||||
$redeliverLimit = (clone $now)->modify(sprintf('-%d seconds', $this->configuration['redeliver_timeout']));
|
||||
|
||||
return $this->createQueryBuilder()
|
||||
->where('m.delivered_at is null OR m.delivered_at < ?')
|
||||
->andWhere('m.available_at <= ?')
|
||||
->andWhere('m.queue_name = ?')
|
||||
->setParameters([
|
||||
$redeliverLimit,
|
||||
$now,
|
||||
$this->configuration['queue_name'],
|
||||
], [
|
||||
Type::DATETIME,
|
||||
Type::DATETIME,
|
||||
]);
|
||||
}
|
||||
|
||||
private function createQueryBuilder(): QueryBuilder
|
||||
{
|
||||
return $this->driverConnection->createQueryBuilder()
|
||||
->select('m.*')
|
||||
->from($this->configuration['table_name'], 'm');
|
||||
}
|
||||
|
||||
private function executeQuery(string $sql, array $parameters = [], array $types = []): ResultStatement
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
private function getSchema(): Schema
|
||||
{
|
||||
$schema = new Schema([], [], $this->driverConnection->getSchemaManager()->createSchemaConfig());
|
||||
$table = $schema->createTable($this->configuration['table_name']);
|
||||
$table->addColumn('id', Type::BIGINT)
|
||||
->setAutoincrement(true)
|
||||
->setNotnull(true);
|
||||
$table->addColumn('body', Type::TEXT)
|
||||
->setNotnull(true);
|
||||
$table->addColumn('headers', Type::TEXT)
|
||||
->setNotnull(true);
|
||||
$table->addColumn('queue_name', Type::STRING)
|
||||
->setNotnull(true);
|
||||
$table->addColumn('created_at', Type::DATETIME)
|
||||
->setNotnull(true);
|
||||
$table->addColumn('available_at', Type::DATETIME)
|
||||
->setNotnull(true);
|
||||
$table->addColumn('delivered_at', Type::DATETIME)
|
||||
->setNotnull(false);
|
||||
$table->setPrimaryKey(['id']);
|
||||
$table->addIndex(['queue_name']);
|
||||
$table->addIndex(['available_at']);
|
||||
$table->addIndex(['delivered_at']);
|
||||
|
||||
return $schema;
|
||||
}
|
||||
|
||||
private function decodeEnvelopeHeaders(array $doctrineEnvelope): array
|
||||
{
|
||||
$doctrineEnvelope['headers'] = json_decode($doctrineEnvelope['headers'], true);
|
||||
|
||||
return $doctrineEnvelope;
|
||||
}
|
||||
}
|
||||
class_alias(Connection::class, \Symfony\Component\Messenger\Transport\Doctrine\Connection::class);
|
|
@ -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\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;
|
||||
}
|
||||
}
|
||||
class_alias(DoctrineReceivedStamp::class, \Symfony\Component\Messenger\Transport\Doctrine\DoctrineReceivedStamp::class);
|
|
@ -0,0 +1,173 @@
|
|||
<?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\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\Receiver\ReceiverInterface;
|
||||
use Symfony\Component\Messenger\Transport\Serialization\PhpSerializer;
|
||||
use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface;
|
||||
|
||||
/**
|
||||
* @author Vincent Touzet <vincent.touzet@gmail.com>
|
||||
*/
|
||||
class DoctrineReceiver implements ReceiverInterface, MessageCountAwareInterface, ListableReceiverInterface
|
||||
{
|
||||
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'])
|
||||
);
|
||||
}
|
||||
}
|
||||
class_alias(DoctrineReceiver::class, \Symfony\Component\Messenger\Transport\Doctrine\DoctrineReceiver::class);
|
|
@ -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\Messenger\Bridge\Doctrine\Transport;
|
||||
|
||||
use Doctrine\DBAL\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));
|
||||
}
|
||||
}
|
||||
class_alias(DoctrineSender::class, \Symfony\Component\Messenger\Transport\Doctrine\DoctrineSender::class);
|
|
@ -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\Messenger\Bridge\Doctrine\Transport;
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
class_alias(DoctrineTransport::class, \Symfony\Component\Messenger\Transport\Doctrine\DoctrineTransport::class);
|
|
@ -0,0 +1,58 @@
|
|||
<?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\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, \is_object($registry) ? \get_class($registry) : \gettype($registry)));
|
||||
}
|
||||
|
||||
$this->registry = $registry;
|
||||
}
|
||||
|
||||
public function createTransport(string $dsn, array $options, SerializerInterface $serializer): TransportInterface
|
||||
{
|
||||
unset($options['transport_name']);
|
||||
$configuration = Connection::buildConfiguration($dsn, $options);
|
||||
|
||||
try {
|
||||
$driverConnection = $this->registry->getConnection($configuration['connection']);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
throw new TransportException(sprintf('Could not find Doctrine connection from Messenger DSN "%s".', $dsn), 0, $e);
|
||||
}
|
||||
|
||||
$connection = new Connection($configuration, $driverConnection);
|
||||
|
||||
return new DoctrineTransport($connection, $serializer);
|
||||
}
|
||||
|
||||
public function supports(string $dsn, array $options): bool
|
||||
{
|
||||
return 0 === strpos($dsn, 'doctrine://');
|
||||
}
|
||||
}
|
||||
class_alias(DoctrineTransportFactory::class, \Symfony\Component\Messenger\Transport\Doctrine\DoctrineTransportFactory::class);
|
|
@ -0,0 +1,43 @@
|
|||
{
|
||||
"name": "symfony/doctrine-messenger",
|
||||
"type": "symfony-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",
|
||||
"doctrine/dbal": "^2.6",
|
||||
"doctrine/persistence": "^1.3",
|
||||
"symfony/messenger": "^5.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/serializer": "^4.4|^5.0",
|
||||
"symfony/property-access": "^4.4|^5.0"
|
||||
},
|
||||
"conflict": {
|
||||
"doctrine/persistence": "<1.3"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": { "Symfony\\Component\\Messenger\\Bridge\\Doctrine\\": "" },
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "5.1-dev"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/5.2/phpunit.xsd"
|
||||
backupGlobals="false"
|
||||
colors="true"
|
||||
bootstrap="vendor/autoload.php"
|
||||
failOnRisky="true"
|
||||
failOnWarning="true"
|
||||
>
|
||||
<php>
|
||||
<ini name="error_reporting" value="-1" />
|
||||
</php>
|
||||
|
||||
<testsuites>
|
||||
<testsuite name="Symfony Doctrine Messenger Component Test Suite">
|
||||
<directory>./Tests/</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
|
||||
<filter>
|
||||
<whitelist>
|
||||
<directory>./</directory>
|
||||
<exclude>
|
||||
<directory>./Tests</directory>
|
||||
<directory>./vendor</directory>
|
||||
</exclude>
|
||||
</whitelist>
|
||||
</filter>
|
||||
</phpunit>
|
|
@ -0,0 +1,3 @@
|
|||
/Tests export-ignore
|
||||
/phpunit.xml.dist export-ignore
|
||||
/.gitignore export-ignore
|
|
@ -0,0 +1,3 @@
|
|||
vendor/
|
||||
composer.lock
|
||||
phpunit.xml
|
|
@ -0,0 +1,7 @@
|
|||
CHANGELOG
|
||||
=========
|
||||
|
||||
5.1.0
|
||||
-----
|
||||
|
||||
* Introduced the Redis bridge.
|
|
@ -0,0 +1,19 @@
|
|||
Copyright (c) 2018-2020 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 @@
|
|||
Redis Messenger
|
||||
===============
|
||||
|
||||
Provides Redis 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,18 @@
|
|||
<?php
|
||||
|
||||
namespace Symfony\Component\Messenger\Bridge\Redis\Tests\Fixtures;
|
||||
|
||||
class DummyMessage
|
||||
{
|
||||
private $message;
|
||||
|
||||
public function __construct(string $message)
|
||||
{
|
||||
$this->message = $message;
|
||||
}
|
||||
|
||||
public function getMessage(): string
|
||||
{
|
||||
return $this->message;
|
||||
}
|
||||
}
|
|
@ -9,11 +9,11 @@
|
|||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Messenger\Tests\Transport\RedisExt;
|
||||
namespace Symfony\Component\Messenger\Bridge\Redis\Tests\Transport;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Messenger\Exception\TransportException;
|
||||
use Symfony\Component\Messenger\Transport\RedisExt\Connection;
|
||||
use Symfony\Component\Messenger\Bridge\Redis\Transport\Connection;
|
||||
|
||||
/**
|
||||
* @requires extension redis >= 4.3.0
|
|
@ -9,11 +9,11 @@
|
|||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Messenger\Tests\Transport\RedisExt;
|
||||
namespace Symfony\Component\Messenger\Bridge\Redis\Tests\Transport;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Messenger\Tests\Fixtures\DummyMessage;
|
||||
use Symfony\Component\Messenger\Transport\RedisExt\Connection;
|
||||
use Symfony\Component\Messenger\Bridge\Redis\Tests\Fixtures\DummyMessage;
|
||||
use Symfony\Component\Messenger\Bridge\Redis\Transport\Connection;
|
||||
|
||||
/**
|
||||
* @requires extension redis
|
|
@ -9,13 +9,13 @@
|
|||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Messenger\Tests\Transport\RedisExt;
|
||||
namespace Symfony\Component\Messenger\Bridge\Redis\Tests\Transport;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Messenger\Exception\MessageDecodingFailedException;
|
||||
use Symfony\Component\Messenger\Tests\Fixtures\DummyMessage;
|
||||
use Symfony\Component\Messenger\Transport\RedisExt\Connection;
|
||||
use Symfony\Component\Messenger\Transport\RedisExt\RedisReceiver;
|
||||
use Symfony\Component\Messenger\Bridge\Redis\Tests\Fixtures\DummyMessage;
|
||||
use Symfony\Component\Messenger\Bridge\Redis\Transport\Connection;
|
||||
use Symfony\Component\Messenger\Bridge\Redis\Transport\RedisReceiver;
|
||||
use Symfony\Component\Messenger\Transport\Serialization\PhpSerializer;
|
||||
use Symfony\Component\Messenger\Transport\Serialization\Serializer;
|
||||
use Symfony\Component\Serializer as SerializerComponent;
|
|
@ -9,13 +9,13 @@
|
|||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Messenger\Tests\Transport\RedisExt;
|
||||
namespace Symfony\Component\Messenger\Bridge\Redis\Tests\Transport;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Messenger\Envelope;
|
||||
use Symfony\Component\Messenger\Tests\Fixtures\DummyMessage;
|
||||
use Symfony\Component\Messenger\Transport\RedisExt\Connection;
|
||||
use Symfony\Component\Messenger\Transport\RedisExt\RedisSender;
|
||||
use Symfony\Component\Messenger\Bridge\Redis\Tests\Fixtures\DummyMessage;
|
||||
use Symfony\Component\Messenger\Bridge\Redis\Transport\Connection;
|
||||
use Symfony\Component\Messenger\Bridge\Redis\Transport\RedisSender;
|
||||
use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface;
|
||||
|
||||
class RedisSenderTest extends TestCase
|
|
@ -9,12 +9,12 @@
|
|||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Messenger\Tests\Transport\RedisExt;
|
||||
namespace Symfony\Component\Messenger\Bridge\Redis\Tests\Transport;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Messenger\Transport\RedisExt\Connection;
|
||||
use Symfony\Component\Messenger\Transport\RedisExt\RedisTransport;
|
||||
use Symfony\Component\Messenger\Transport\RedisExt\RedisTransportFactory;
|
||||
use Symfony\Component\Messenger\Bridge\Redis\Transport\Connection;
|
||||
use Symfony\Component\Messenger\Bridge\Redis\Transport\RedisTransport;
|
||||
use Symfony\Component\Messenger\Bridge\Redis\Transport\RedisTransportFactory;
|
||||
use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface;
|
||||
|
||||
/**
|
|
@ -9,13 +9,13 @@
|
|||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Messenger\Tests\Transport\RedisExt;
|
||||
namespace Symfony\Component\Messenger\Bridge\Redis\Tests\Transport;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Messenger\Envelope;
|
||||
use Symfony\Component\Messenger\Tests\Fixtures\DummyMessage;
|
||||
use Symfony\Component\Messenger\Transport\RedisExt\Connection;
|
||||
use Symfony\Component\Messenger\Transport\RedisExt\RedisTransport;
|
||||
use Symfony\Component\Messenger\Bridge\Redis\Tests\Fixtures\DummyMessage;
|
||||
use Symfony\Component\Messenger\Bridge\Redis\Transport\Connection;
|
||||
use Symfony\Component\Messenger\Bridge\Redis\Transport\RedisTransport;
|
||||
use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface;
|
||||
use Symfony\Component\Messenger\Transport\TransportInterface;
|
||||
|
|
@ -0,0 +1,329 @@
|
|||
<?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\Redis\Transport;
|
||||
|
||||
use Symfony\Component\Messenger\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Messenger\Exception\LogicException;
|
||||
use Symfony\Component\Messenger\Exception\TransportException;
|
||||
|
||||
/**
|
||||
* A Redis connection.
|
||||
*
|
||||
* @author Alexander Schranz <alexander@sulu.io>
|
||||
* @author Antoine Bluchet <soyuka@gmail.com>
|
||||
* @author Robin Chalas <robin.chalas@gmail.com>
|
||||
*
|
||||
* @internal
|
||||
* @final
|
||||
*/
|
||||
class Connection
|
||||
{
|
||||
private const DEFAULT_OPTIONS = [
|
||||
'stream' => 'messages',
|
||||
'group' => 'symfony',
|
||||
'consumer' => 'consumer',
|
||||
'auto_setup' => true,
|
||||
'stream_max_entries' => 0, // any value higher than 0 defines an approximate maximum number of stream entries
|
||||
'dbindex' => 0,
|
||||
];
|
||||
|
||||
private $connection;
|
||||
private $stream;
|
||||
private $queue;
|
||||
private $group;
|
||||
private $consumer;
|
||||
private $autoSetup;
|
||||
private $maxEntries;
|
||||
private $couldHavePendingMessages = true;
|
||||
|
||||
public function __construct(array $configuration, array $connectionCredentials = [], array $redisOptions = [], \Redis $redis = null)
|
||||
{
|
||||
if (version_compare(phpversion('redis'), '4.3.0', '<')) {
|
||||
throw new LogicException('The redis transport requires php-redis 4.3.0 or higher.');
|
||||
}
|
||||
|
||||
$this->connection = $redis ?: new \Redis();
|
||||
$this->connection->connect($connectionCredentials['host'] ?? '127.0.0.1', $connectionCredentials['port'] ?? 6379);
|
||||
$this->connection->setOption(\Redis::OPT_SERIALIZER, $redisOptions['serializer'] ?? \Redis::SERIALIZER_PHP);
|
||||
|
||||
if (isset($connectionCredentials['auth']) && !$this->connection->auth($connectionCredentials['auth'])) {
|
||||
throw new InvalidArgumentException(sprintf('Redis connection failed: %s', $redis->getLastError()));
|
||||
}
|
||||
|
||||
if (($dbIndex = $configuration['dbindex'] ?? self::DEFAULT_OPTIONS['dbindex']) && !$this->connection->select($dbIndex)) {
|
||||
throw new InvalidArgumentException(sprintf('Redis connection failed: %s', $redis->getLastError()));
|
||||
}
|
||||
|
||||
$this->stream = $configuration['stream'] ?? self::DEFAULT_OPTIONS['stream'];
|
||||
$this->group = $configuration['group'] ?? self::DEFAULT_OPTIONS['group'];
|
||||
$this->consumer = $configuration['consumer'] ?? self::DEFAULT_OPTIONS['consumer'];
|
||||
$this->queue = $this->stream.'__queue';
|
||||
$this->autoSetup = $configuration['auto_setup'] ?? self::DEFAULT_OPTIONS['auto_setup'];
|
||||
$this->maxEntries = $configuration['stream_max_entries'] ?? self::DEFAULT_OPTIONS['stream_max_entries'];
|
||||
}
|
||||
|
||||
public static function fromDsn(string $dsn, array $redisOptions = [], \Redis $redis = null): self
|
||||
{
|
||||
$url = $dsn;
|
||||
|
||||
if (preg_match('#^redis:///([^:@])+$#', $dsn)) {
|
||||
$url = str_replace('redis:', 'file:', $dsn);
|
||||
}
|
||||
|
||||
if (false === $parsedUrl = parse_url($url)) {
|
||||
throw new InvalidArgumentException(sprintf('The given Redis DSN "%s" is invalid.', $dsn));
|
||||
}
|
||||
if (isset($parsedUrl['query'])) {
|
||||
parse_str($parsedUrl['query'], $redisOptions);
|
||||
}
|
||||
|
||||
$autoSetup = null;
|
||||
if (\array_key_exists('auto_setup', $redisOptions)) {
|
||||
$autoSetup = filter_var($redisOptions['auto_setup'], FILTER_VALIDATE_BOOLEAN);
|
||||
unset($redisOptions['auto_setup']);
|
||||
}
|
||||
|
||||
$maxEntries = null;
|
||||
if (\array_key_exists('stream_max_entries', $redisOptions)) {
|
||||
$maxEntries = filter_var($redisOptions['stream_max_entries'], FILTER_VALIDATE_INT);
|
||||
unset($redisOptions['stream_max_entries']);
|
||||
}
|
||||
|
||||
$dbIndex = null;
|
||||
if (\array_key_exists('dbindex', $redisOptions)) {
|
||||
$dbIndex = filter_var($redisOptions['dbindex'], FILTER_VALIDATE_INT);
|
||||
unset($redisOptions['dbindex']);
|
||||
}
|
||||
|
||||
$configuration = [
|
||||
'stream' => $redisOptions['stream'] ?? null,
|
||||
'group' => $redisOptions['group'] ?? null,
|
||||
'consumer' => $redisOptions['consumer'] ?? null,
|
||||
'auto_setup' => $autoSetup,
|
||||
'stream_max_entries' => $maxEntries,
|
||||
'dbindex' => $dbIndex,
|
||||
];
|
||||
|
||||
if (isset($parsedUrl['host'])) {
|
||||
$connectionCredentials = [
|
||||
'host' => $parsedUrl['host'] ?? '127.0.0.1',
|
||||
'port' => $parsedUrl['port'] ?? 6379,
|
||||
'auth' => $parsedUrl['pass'] ?? $parsedUrl['user'] ?? null,
|
||||
];
|
||||
|
||||
$pathParts = explode('/', $parsedUrl['path'] ?? '');
|
||||
|
||||
$configuration['stream'] = $pathParts[1] ?? $configuration['stream'];
|
||||
$configuration['group'] = $pathParts[2] ?? $configuration['group'];
|
||||
$configuration['consumer'] = $pathParts[3] ?? $configuration['consumer'];
|
||||
} else {
|
||||
$connectionCredentials = [
|
||||
'host' => $parsedUrl['path'],
|
||||
'port' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
return new self($configuration, $connectionCredentials, $redisOptions, $redis);
|
||||
}
|
||||
|
||||
public function get(): ?array
|
||||
{
|
||||
if ($this->autoSetup) {
|
||||
$this->setup();
|
||||
}
|
||||
|
||||
try {
|
||||
$queuedMessageCount = $this->connection->zcount($this->queue, 0, $this->getCurrentTimeInMilliseconds());
|
||||
} catch (\RedisException $e) {
|
||||
throw new TransportException($e->getMessage(), 0, $e);
|
||||
}
|
||||
|
||||
if ($queuedMessageCount) {
|
||||
for ($i = 0; $i < $queuedMessageCount; ++$i) {
|
||||
try {
|
||||
$queuedMessages = $this->connection->zpopmin($this->queue, 1);
|
||||
} catch (\RedisException $e) {
|
||||
throw new TransportException($e->getMessage(), 0, $e);
|
||||
}
|
||||
|
||||
foreach ($queuedMessages as $queuedMessage => $time) {
|
||||
$queuedMessage = json_decode($queuedMessage, true);
|
||||
// if a futured placed message is actually popped because of a race condition with
|
||||
// another running message consumer, the message is readded to the queue by add function
|
||||
// else its just added stream and will be available for all stream consumers
|
||||
$this->add(
|
||||
$queuedMessage['body'],
|
||||
$queuedMessage['headers'],
|
||||
$time - $this->getCurrentTimeInMilliseconds()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$messageId = '>'; // will receive new messages
|
||||
|
||||
if ($this->couldHavePendingMessages) {
|
||||
$messageId = '0'; // will receive consumers pending messages
|
||||
}
|
||||
|
||||
try {
|
||||
$messages = $this->connection->xreadgroup(
|
||||
$this->group,
|
||||
$this->consumer,
|
||||
[$this->stream => $messageId],
|
||||
1
|
||||
);
|
||||
} catch (\RedisException $e) {
|
||||
throw new TransportException($e->getMessage(), 0, $e);
|
||||
}
|
||||
|
||||
if (false === $messages) {
|
||||
if ($error = $this->connection->getLastError() ?: null) {
|
||||
$this->connection->clearLastError();
|
||||
}
|
||||
|
||||
throw new TransportException($error ?? 'Could not read messages from the redis stream.');
|
||||
}
|
||||
|
||||
if ($this->couldHavePendingMessages && empty($messages[$this->stream])) {
|
||||
$this->couldHavePendingMessages = false;
|
||||
|
||||
// No pending messages so get a new one
|
||||
return $this->get();
|
||||
}
|
||||
|
||||
foreach ($messages[$this->stream] ?? [] as $key => $message) {
|
||||
$redisEnvelope = json_decode($message['message'], true);
|
||||
|
||||
return [
|
||||
'id' => $key,
|
||||
'body' => $redisEnvelope['body'],
|
||||
'headers' => $redisEnvelope['headers'],
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function ack(string $id): void
|
||||
{
|
||||
try {
|
||||
$acknowledged = $this->connection->xack($this->stream, $this->group, [$id]);
|
||||
} catch (\RedisException $e) {
|
||||
throw new TransportException($e->getMessage(), 0, $e);
|
||||
}
|
||||
|
||||
if (!$acknowledged) {
|
||||
if ($error = $this->connection->getLastError() ?: null) {
|
||||
$this->connection->clearLastError();
|
||||
}
|
||||
throw new TransportException($error ?? sprintf('Could not acknowledge redis message "%s".', $id));
|
||||
}
|
||||
}
|
||||
|
||||
public function reject(string $id): void
|
||||
{
|
||||
try {
|
||||
$deleted = $this->connection->xack($this->stream, $this->group, [$id]);
|
||||
$deleted = $this->connection->xdel($this->stream, [$id]) && $deleted;
|
||||
} catch (\RedisException $e) {
|
||||
throw new TransportException($e->getMessage(), 0, $e);
|
||||
}
|
||||
|
||||
if (!$deleted) {
|
||||
if ($error = $this->connection->getLastError() ?: null) {
|
||||
$this->connection->clearLastError();
|
||||
}
|
||||
throw new TransportException($error ?? sprintf('Could not delete message "%s" from the redis stream.', $id));
|
||||
}
|
||||
}
|
||||
|
||||
public function add(string $body, array $headers, int $delayInMs = 0): void
|
||||
{
|
||||
if ($this->autoSetup) {
|
||||
$this->setup();
|
||||
}
|
||||
|
||||
try {
|
||||
if ($delayInMs > 0) { // the delay could be smaller 0 in a queued message
|
||||
$message = json_encode([
|
||||
'body' => $body,
|
||||
'headers' => $headers,
|
||||
// Entry need to be unique in the sorted set else it would only be added once to the delayed messages queue
|
||||
'uniqid' => uniqid('', true),
|
||||
]);
|
||||
|
||||
if (false === $message) {
|
||||
throw new TransportException(json_last_error_msg());
|
||||
}
|
||||
|
||||
$score = (int) ($this->getCurrentTimeInMilliseconds() + $delayInMs);
|
||||
$added = $this->connection->zadd($this->queue, ['NX'], $score, $message);
|
||||
} else {
|
||||
$message = json_encode([
|
||||
'body' => $body,
|
||||
'headers' => $headers,
|
||||
]);
|
||||
|
||||
if (false === $message) {
|
||||
throw new TransportException(json_last_error_msg());
|
||||
}
|
||||
|
||||
if ($this->maxEntries) {
|
||||
$added = $this->connection->xadd($this->stream, '*', ['message' => $message], $this->maxEntries, true);
|
||||
} else {
|
||||
$added = $this->connection->xadd($this->stream, '*', ['message' => $message]);
|
||||
}
|
||||
}
|
||||
} catch (\RedisException $e) {
|
||||
if ($error = $this->connection->getLastError() ?: null) {
|
||||
$this->connection->clearLastError();
|
||||
}
|
||||
throw new TransportException($error ?? $e->getMessage(), 0, $e);
|
||||
}
|
||||
|
||||
if (!$added) {
|
||||
if ($error = $this->connection->getLastError() ?: null) {
|
||||
$this->connection->clearLastError();
|
||||
}
|
||||
throw new TransportException($error ?? 'Could not add a message to the redis stream.');
|
||||
}
|
||||
}
|
||||
|
||||
public function setup(): void
|
||||
{
|
||||
try {
|
||||
$this->connection->xgroup('CREATE', $this->stream, $this->group, 0, true);
|
||||
} catch (\RedisException $e) {
|
||||
throw new TransportException($e->getMessage(), 0, $e);
|
||||
}
|
||||
|
||||
// group might already exist, ignore
|
||||
if ($this->connection->getLastError()) {
|
||||
$this->connection->clearLastError();
|
||||
}
|
||||
|
||||
$this->autoSetup = false;
|
||||
}
|
||||
|
||||
private function getCurrentTimeInMilliseconds(): int
|
||||
{
|
||||
return (int) (microtime(true) * 1000);
|
||||
}
|
||||
|
||||
public function cleanup(): void
|
||||
{
|
||||
$this->connection->del($this->stream);
|
||||
$this->connection->del($this->queue);
|
||||
}
|
||||
}
|
||||
class_alias(Connection::class, \Symfony\Component\Messenger\Transport\RedisExt\Connection::class);
|
|
@ -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\Messenger\Bridge\Redis\Transport;
|
||||
|
||||
use Symfony\Component\Messenger\Stamp\NonSendableStampInterface;
|
||||
|
||||
/**
|
||||
* @author Alexander Schranz <alexander@sulu.io>
|
||||
*/
|
||||
class RedisReceivedStamp implements NonSendableStampInterface
|
||||
{
|
||||
private $id;
|
||||
|
||||
public function __construct(string $id)
|
||||
{
|
||||
$this->id = $id;
|
||||
}
|
||||
|
||||
public function getId(): string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
}
|
||||
class_alias(RedisReceivedStamp::class, \Symfony\Component\Messenger\Transport\RedisExt\RedisReceivedStamp::class);
|
|
@ -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\Messenger\Bridge\Redis\Transport;
|
||||
|
||||
use Symfony\Component\Messenger\Envelope;
|
||||
use Symfony\Component\Messenger\Exception\LogicException;
|
||||
use Symfony\Component\Messenger\Exception\MessageDecodingFailedException;
|
||||
use Symfony\Component\Messenger\Transport\Receiver\ReceiverInterface;
|
||||
use Symfony\Component\Messenger\Transport\Serialization\PhpSerializer;
|
||||
use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface;
|
||||
|
||||
/**
|
||||
* @author Alexander Schranz <alexander@sulu.io>
|
||||
* @author Antoine Bluchet <soyuka@gmail.com>
|
||||
*/
|
||||
class RedisReceiver implements ReceiverInterface
|
||||
{
|
||||
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
|
||||
{
|
||||
$redisEnvelope = $this->connection->get();
|
||||
|
||||
if (null === $redisEnvelope) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
$envelope = $this->serializer->decode([
|
||||
'body' => $redisEnvelope['body'],
|
||||
'headers' => $redisEnvelope['headers'],
|
||||
]);
|
||||
} catch (MessageDecodingFailedException $exception) {
|
||||
$this->connection->reject($redisEnvelope['id']);
|
||||
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
return [$envelope->with(new RedisReceivedStamp($redisEnvelope['id']))];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function ack(Envelope $envelope): void
|
||||
{
|
||||
$this->connection->ack($this->findRedisReceivedStamp($envelope)->getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function reject(Envelope $envelope): void
|
||||
{
|
||||
$this->connection->reject($this->findRedisReceivedStamp($envelope)->getId());
|
||||
}
|
||||
|
||||
private function findRedisReceivedStamp(Envelope $envelope): RedisReceivedStamp
|
||||
{
|
||||
/** @var RedisReceivedStamp|null $redisReceivedStamp */
|
||||
$redisReceivedStamp = $envelope->last(RedisReceivedStamp::class);
|
||||
|
||||
if (null === $redisReceivedStamp) {
|
||||
throw new LogicException('No RedisReceivedStamp found on the Envelope.');
|
||||
}
|
||||
|
||||
return $redisReceivedStamp;
|
||||
}
|
||||
}
|
||||
class_alias(RedisReceiver::class, \Symfony\Component\Messenger\Transport\RedisExt\RedisReceiver::class);
|
|
@ -0,0 +1,50 @@
|
|||
<?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\Redis\Transport;
|
||||
|
||||
use Symfony\Component\Messenger\Envelope;
|
||||
use Symfony\Component\Messenger\Stamp\DelayStamp;
|
||||
use Symfony\Component\Messenger\Transport\Sender\SenderInterface;
|
||||
use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface;
|
||||
|
||||
/**
|
||||
* @author Alexander Schranz <alexander@sulu.io>
|
||||
* @author Antoine Bluchet <soyuka@gmail.com>
|
||||
*/
|
||||
class RedisSender implements SenderInterface
|
||||
{
|
||||
private $connection;
|
||||
private $serializer;
|
||||
|
||||
public function __construct(Connection $connection, SerializerInterface $serializer)
|
||||
{
|
||||
$this->connection = $connection;
|
||||
$this->serializer = $serializer;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function send(Envelope $envelope): Envelope
|
||||
{
|
||||
$encodedMessage = $this->serializer->encode($envelope);
|
||||
|
||||
/** @var DelayStamp|null $delayStamp */
|
||||
$delayStamp = $envelope->last(DelayStamp::class);
|
||||
$delayInMs = null !== $delayStamp ? $delayStamp->getDelay() : 0;
|
||||
|
||||
$this->connection->add($encodedMessage['body'], $encodedMessage['headers'] ?? [], $delayInMs);
|
||||
|
||||
return $envelope;
|
||||
}
|
||||
}
|
||||
class_alias(RedisSender::class, \Symfony\Component\Messenger\Transport\RedisExt\RedisSender::class);
|
|
@ -0,0 +1,87 @@
|
|||
<?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\Redis\Transport;
|
||||
|
||||
use Symfony\Component\Messenger\Envelope;
|
||||
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 Alexander Schranz <alexander@sulu.io>
|
||||
* @author Antoine Bluchet <soyuka@gmail.com>
|
||||
*/
|
||||
class RedisTransport implements TransportInterface, SetupableTransportInterface
|
||||
{
|
||||
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 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();
|
||||
}
|
||||
|
||||
private function getReceiver(): RedisReceiver
|
||||
{
|
||||
return $this->receiver = new RedisReceiver($this->connection, $this->serializer);
|
||||
}
|
||||
|
||||
private function getSender(): RedisSender
|
||||
{
|
||||
return $this->sender = new RedisSender($this->connection, $this->serializer);
|
||||
}
|
||||
}
|
||||
class_alias(RedisTransport::class, \Symfony\Component\Messenger\Transport\RedisExt\RedisTransport::class);
|
|
@ -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\Redis\Transport;
|
||||
|
||||
use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface;
|
||||
use Symfony\Component\Messenger\Transport\TransportFactoryInterface;
|
||||
use Symfony\Component\Messenger\Transport\TransportInterface;
|
||||
|
||||
/**
|
||||
* @author Alexander Schranz <alexander@suluio>
|
||||
* @author Antoine Bluchet <soyuka@gmail.com>
|
||||
*/
|
||||
class RedisTransportFactory implements TransportFactoryInterface
|
||||
{
|
||||
public function createTransport(string $dsn, array $options, SerializerInterface $serializer): TransportInterface
|
||||
{
|
||||
unset($options['transport_name']);
|
||||
|
||||
return new RedisTransport(Connection::fromDsn($dsn, $options), $serializer);
|
||||
}
|
||||
|
||||
public function supports(string $dsn, array $options): bool
|
||||
{
|
||||
return 0 === strpos($dsn, 'redis://');
|
||||
}
|
||||
}
|
||||
class_alias(RedisTransportFactory::class, \Symfony\Component\Messenger\Transport\RedisExt\RedisTransportFactory::class);
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"name": "symfony/redis-messenger",
|
||||
"type": "symfony-bridge",
|
||||
"description": "Symfony Redis 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/messenger": "^5.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/property-access": "^4.4|^5.0",
|
||||
"symfony/serializer": "^4.4|^5.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": { "Symfony\\Component\\Messenger\\Bridge\\Redis\\": "" },
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "5.1-dev"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/5.2/phpunit.xsd"
|
||||
backupGlobals="false"
|
||||
colors="true"
|
||||
bootstrap="vendor/autoload.php"
|
||||
failOnRisky="true"
|
||||
failOnWarning="true"
|
||||
>
|
||||
<php>
|
||||
<ini name="error_reporting" value="-1" />
|
||||
</php>
|
||||
|
||||
<testsuites>
|
||||
<testsuite name="Symfony Redis Messenger Component Test Suite">
|
||||
<directory>./Tests/</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
|
||||
<filter>
|
||||
<whitelist>
|
||||
<directory>./</directory>
|
||||
<exclude>
|
||||
<directory>./Tests</directory>
|
||||
<directory>./vendor</directory>
|
||||
</exclude>
|
||||
</whitelist>
|
||||
</filter>
|
||||
</phpunit>
|
|
@ -1,6 +1,13 @@
|
|||
CHANGELOG
|
||||
=========
|
||||
|
||||
5.1.0
|
||||
-----
|
||||
|
||||
* Moved AmqpExt transport to package `symfony/amqp-messenger`. All classes in `Symfony\Component\Messenger\Transport\AmqpExt` have been moved to `Symfony\Component\Messenger\Bridge\Amqp\Transport`
|
||||
* Moved Doctrine transport to package `symfony/doctrine-messenger`. All classes in `Symfony\Component\Messenger\Transport\Doctrine` have been moved to `Symfony\Component\Messenger\Bridge\Doctrine\Transport`
|
||||
* Moved RedisExt transport to package `symfony/redis-messenger`. All classes in `Symfony\Component\Messenger\Transport\RedisExt` have been moved to `Symfony\Component\Messenger\Bridge\Redis\Transport`
|
||||
|
||||
5.0.0
|
||||
-----
|
||||
|
||||
|
|
|
@ -11,9 +11,11 @@
|
|||
|
||||
namespace Symfony\Component\Messenger\Middleware;
|
||||
|
||||
|
||||
use Symfony\Component\Messenger\Bridge\Amqp\Transport\AmqpReceivedStamp;
|
||||
use Symfony\Component\Messenger\Envelope;
|
||||
use Symfony\Component\Messenger\Exception\RejectRedeliveredMessageException;
|
||||
use Symfony\Component\Messenger\Transport\AmqpExt\AmqpReceivedStamp;
|
||||
use Symfony\Component\Messenger\Transport\AmqpExt\AmqpReceivedStamp as LegacyAmqpReceivedStamp;
|
||||
|
||||
/**
|
||||
* Middleware that throws a RejectRedeliveredMessageException when a message is detected that has been redelivered by AMQP.
|
||||
|
@ -34,11 +36,16 @@ class RejectRedeliveredMessageMiddleware implements MiddlewareInterface
|
|||
public function handle(Envelope $envelope, StackInterface $stack): Envelope
|
||||
{
|
||||
$amqpReceivedStamp = $envelope->last(AmqpReceivedStamp::class);
|
||||
|
||||
if ($amqpReceivedStamp instanceof AmqpReceivedStamp && $amqpReceivedStamp->getAmqpEnvelope()->isRedelivery()) {
|
||||
throw new RejectRedeliveredMessageException('Redelivered message from AMQP detected that will be rejected and trigger the retry logic.');
|
||||
}
|
||||
|
||||
// Legacy code to support symfony/messenger < 5.1
|
||||
$amqpReceivedStamp = $envelope->last(LegacyAmqpReceivedStamp::class);
|
||||
if ($amqpReceivedStamp instanceof LegacyAmqpReceivedStamp && $amqpReceivedStamp->getAmqpEnvelope()->isRedelivery()) {
|
||||
throw new RejectRedeliveredMessageException('Redelivered message from AMQP detected that will be rejected and trigger the retry logic.');
|
||||
}
|
||||
|
||||
return $stack->next()->handle($envelope, $stack);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,7 +38,7 @@ use Symfony\Component\Messenger\Tests\Fixtures\DummyQueryHandler;
|
|||
use Symfony\Component\Messenger\Tests\Fixtures\MultipleBusesMessage;
|
||||
use Symfony\Component\Messenger\Tests\Fixtures\MultipleBusesMessageHandler;
|
||||
use Symfony\Component\Messenger\Tests\Fixtures\SecondMessage;
|
||||
use Symfony\Component\Messenger\Transport\AmqpExt\AmqpReceiver;
|
||||
use Symfony\Component\Messenger\Bridge\Amqp\Transport\AmqpReceiver;
|
||||
use Symfony\Component\Messenger\Transport\Receiver\ReceiverInterface;
|
||||
|
||||
class MessengerPassTest extends TestCase
|
||||
|
|
|
@ -11,25 +11,17 @@
|
|||
|
||||
namespace Symfony\Component\Messenger\Transport\AmqpExt;
|
||||
|
||||
class AmqpFactory
|
||||
{
|
||||
public function createConnection(array $credentials): \AMQPConnection
|
||||
{
|
||||
return new \AMQPConnection($credentials);
|
||||
}
|
||||
use Symfony\Component\Messenger\Bridge\Amqp\Transport\AmqpFactory as BridgeAmqpFactory;
|
||||
|
||||
public function createChannel(\AMQPConnection $connection): \AMQPChannel
|
||||
{
|
||||
return new \AMQPChannel($connection);
|
||||
}
|
||||
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 5.1, use "%s" instead. The AmqpExt transport has been moved to package "symfony/amqp-messenger" and will not be included by default in 6.0. Run "composer require symfony/amqp-messenger".', AmqpFactory::class, BridgeAmqpFactory::class), E_USER_DEPRECATED);
|
||||
|
||||
public function createQueue(\AMQPChannel $channel): \AMQPQueue
|
||||
{
|
||||
return new \AMQPQueue($channel);
|
||||
}
|
||||
class_exists(BridgeAmqpFactory::class);
|
||||
|
||||
public function createExchange(\AMQPChannel $channel): \AMQPExchange
|
||||
if (false) {
|
||||
/**
|
||||
* @deprecated since Symfony 5.1, to be removed in 6.0. Use symfony/amqp-messenger instead.
|
||||
*/
|
||||
class AmqpFactory
|
||||
{
|
||||
return new \AMQPExchange($channel);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,29 +11,17 @@
|
|||
|
||||
namespace Symfony\Component\Messenger\Transport\AmqpExt;
|
||||
|
||||
use Symfony\Component\Messenger\Stamp\NonSendableStampInterface;
|
||||
use Symfony\Component\Messenger\Bridge\Amqp\Transport\AmqpReceivedStamp as BridgeAmqpReceivedStamp;
|
||||
|
||||
/**
|
||||
* Stamp applied when a message is received from Amqp.
|
||||
*/
|
||||
class AmqpReceivedStamp implements NonSendableStampInterface
|
||||
{
|
||||
private $amqpEnvelope;
|
||||
private $queueName;
|
||||
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 5.1, use "%s" instead. The AmqpExt transport has been moved to package "symfony/amqp-messenger" and will not be included by default in 6.0. Run "composer require symfony/amqp-messenger".', AmqpReceivedStamp::class, BridgeAmqpReceivedStamp::class), E_USER_DEPRECATED);
|
||||
|
||||
public function __construct(\AMQPEnvelope $amqpEnvelope, string $queueName)
|
||||
class_exists(BridgeAmqpReceivedStamp::class);
|
||||
|
||||
if (false) {
|
||||
/**
|
||||
* @deprecated since Symfony 5.1, to be removed in 6.0. Use symfony/amqp-messenger instead.
|
||||
*/
|
||||
class AmqpReceivedStamp
|
||||
{
|
||||
$this->amqpEnvelope = $amqpEnvelope;
|
||||
$this->queueName = $queueName;
|
||||
}
|
||||
|
||||
public function getAmqpEnvelope(): \AMQPEnvelope
|
||||
{
|
||||
return $this->amqpEnvelope;
|
||||
}
|
||||
|
||||
public function getQueueName(): string
|
||||
{
|
||||
return $this->queueName;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,128 +11,17 @@
|
|||
|
||||
namespace Symfony\Component\Messenger\Transport\AmqpExt;
|
||||
|
||||
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\ReceiverInterface;
|
||||
use Symfony\Component\Messenger\Transport\Serialization\PhpSerializer;
|
||||
use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface;
|
||||
use Symfony\Component\Messenger\Bridge\Amqp\Transport\AmqpReceiver as BridgeAmqpReceiver;
|
||||
|
||||
/**
|
||||
* Symfony Messenger receiver to get messages from AMQP brokers using PHP's AMQP extension.
|
||||
*
|
||||
* @author Samuel Roze <samuel.roze@gmail.com>
|
||||
*/
|
||||
class AmqpReceiver implements ReceiverInterface, MessageCountAwareInterface
|
||||
{
|
||||
private $serializer;
|
||||
private $connection;
|
||||
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 5.1, use "%s" instead. The AmqpExt transport has been moved to package "symfony/amqp-messenger" and will not be included by default in 6.0. Run "composer require symfony/amqp-messenger".', AmqpReceiver::class, BridgeAmqpReceiver::class), E_USER_DEPRECATED);
|
||||
|
||||
public function __construct(Connection $connection, SerializerInterface $serializer = null)
|
||||
{
|
||||
$this->connection = $connection;
|
||||
$this->serializer = $serializer ?? new PhpSerializer();
|
||||
}
|
||||
class_exists(BridgeAmqpReceiver::class);
|
||||
|
||||
if (false) {
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @deprecated since Symfony 5.1, to be removed in 6.0. Use symfony/amqp-messenger instead.
|
||||
*/
|
||||
public function get(): iterable
|
||||
class AmqpReceiver
|
||||
{
|
||||
foreach ($this->connection->getQueueNames() 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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,67 +11,18 @@
|
|||
|
||||
namespace Symfony\Component\Messenger\Transport\AmqpExt;
|
||||
|
||||
use Symfony\Component\Messenger\Envelope;
|
||||
use Symfony\Component\Messenger\Exception\TransportException;
|
||||
use Symfony\Component\Messenger\Stamp\DelayStamp;
|
||||
use Symfony\Component\Messenger\Transport\Sender\SenderInterface;
|
||||
use Symfony\Component\Messenger\Transport\Serialization\PhpSerializer;
|
||||
use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface;
|
||||
use Symfony\Component\Messenger\Bridge\Amqp\Transport\AmqpSender as BridgeAmqpSender;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 5.1, use "%s" instead. The AmqpExt transport has been moved to package "symfony/amqp-messenger" and will not be included by default in 6.0. Run "composer require symfony/amqp-messenger".', AmqpSender::class, BridgeAmqpSender::class), E_USER_DEPRECATED);
|
||||
|
||||
public function __construct(Connection $connection, SerializerInterface $serializer = null)
|
||||
{
|
||||
$this->connection = $connection;
|
||||
$this->serializer = $serializer ?? new PhpSerializer();
|
||||
}
|
||||
class_exists(BridgeAmqpSender::class);
|
||||
|
||||
if (false) {
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @deprecated since Symfony 5.1, to be removed in 6.0. Use symfony/amqp-messenger instead.
|
||||
*/
|
||||
public function send(Envelope $envelope): Envelope
|
||||
class AmqpSender
|
||||
{
|
||||
$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);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->connection->publish(
|
||||
$encodedMessage['body'],
|
||||
$encodedMessage['headers'] ?? [],
|
||||
$delay,
|
||||
$amqpStamp
|
||||
);
|
||||
} catch (\AMQPException $e) {
|
||||
throw new TransportException($e->getMessage(), 0, $e);
|
||||
}
|
||||
|
||||
return $envelope;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -11,66 +11,17 @@
|
|||
|
||||
namespace Symfony\Component\Messenger\Transport\AmqpExt;
|
||||
|
||||
use Symfony\Component\Messenger\Stamp\NonSendableStampInterface;
|
||||
use Symfony\Component\Messenger\Bridge\Amqp\Transport\AmqpStamp as BridgeAmqpStamp;
|
||||
|
||||
/**
|
||||
* @author Guillaume Gammelin <ggammelin@gmail.com>
|
||||
* @author Samuel Roze <samuel.roze@gmail.com>
|
||||
*/
|
||||
final class AmqpStamp implements NonSendableStampInterface
|
||||
{
|
||||
private $routingKey;
|
||||
private $flags;
|
||||
private $attributes;
|
||||
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 5.1, use "%s" instead. The AmqpExt transport has been moved to package "symfony/amqp-messenger" and will not be included by default in 6.0. Run "composer require symfony/amqp-messenger".', AmqpStamp::class, BridgeAmqpStamp::class), E_USER_DEPRECATED);
|
||||
|
||||
public function __construct(string $routingKey = null, int $flags = AMQP_NOPARAM, array $attributes = [])
|
||||
class_exists(BridgeAmqpStamp::class);
|
||||
|
||||
if (false) {
|
||||
/**
|
||||
* @deprecated since Symfony 5.1, to be removed in 6.0. Use symfony/amqp-messenger instead.
|
||||
*/
|
||||
class AmqpStamp
|
||||
{
|
||||
$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): 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();
|
||||
|
||||
return new self($previousStamp->routingKey ?? $amqpEnvelope->getRoutingKey(), $previousStamp->flags ?? AMQP_NOPARAM, $attr);
|
||||
}
|
||||
|
||||
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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,84 +11,17 @@
|
|||
|
||||
namespace Symfony\Component\Messenger\Transport\AmqpExt;
|
||||
|
||||
use Symfony\Component\Messenger\Envelope;
|
||||
use Symfony\Component\Messenger\Transport\Receiver\MessageCountAwareInterface;
|
||||
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;
|
||||
use Symfony\Component\Messenger\Bridge\Amqp\Transport\AmqpTransport as BridgeAmqpTransport;
|
||||
|
||||
/**
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*/
|
||||
class AmqpTransport implements TransportInterface, SetupableTransportInterface, MessageCountAwareInterface
|
||||
{
|
||||
private $serializer;
|
||||
private $connection;
|
||||
private $receiver;
|
||||
private $sender;
|
||||
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 5.1, use "%s" instead. The AmqpExt transport has been moved to package "symfony/amqp-messenger" and will not be included by default in 6.0. Run "composer require symfony/amqp-messenger".', AmqpTransport::class, BridgeAmqpTransport::class), E_USER_DEPRECATED);
|
||||
|
||||
public function __construct(Connection $connection, SerializerInterface $serializer = null)
|
||||
{
|
||||
$this->connection = $connection;
|
||||
$this->serializer = $serializer ?? new PhpSerializer();
|
||||
}
|
||||
class_exists(BridgeAmqpTransport::class);
|
||||
|
||||
if (false) {
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @deprecated since Symfony 5.1, to be removed in 6.0. Use symfony/amqp-messenger instead.
|
||||
*/
|
||||
public function get(): iterable
|
||||
class AmqpTransport
|
||||
{
|
||||
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 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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,24 +11,17 @@
|
|||
|
||||
namespace Symfony\Component\Messenger\Transport\AmqpExt;
|
||||
|
||||
use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface;
|
||||
use Symfony\Component\Messenger\Transport\TransportFactoryInterface;
|
||||
use Symfony\Component\Messenger\Transport\TransportInterface;
|
||||
use Symfony\Component\Messenger\Bridge\Amqp\Transport\AmqpTransportFactory as BridgeAmqpTransportFactory;
|
||||
|
||||
/**
|
||||
* @author Samuel Roze <samuel.roze@gmail.com>
|
||||
*/
|
||||
class AmqpTransportFactory implements TransportFactoryInterface
|
||||
{
|
||||
public function createTransport(string $dsn, array $options, SerializerInterface $serializer): TransportInterface
|
||||
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 5.1, use "%s" instead. The AmqpExt transport has been moved to package "symfony/amqp-messenger" and will not be included by default in 6.0. Run "composer require symfony/amqp-messenger".', AmqpTransportFactory::class, BridgeAmqpTransportFactory::class), E_USER_DEPRECATED);
|
||||
|
||||
class_exists(BridgeAmqpTransportFactory::class);
|
||||
|
||||
if (false) {
|
||||
/**
|
||||
* @deprecated since Symfony 5.1, to be removed in 6.0. Use symfony/amqp-messenger instead.
|
||||
*/
|
||||
class AmqpTransportFactory
|
||||
{
|
||||
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://');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,463 +11,17 @@
|
|||
|
||||
namespace Symfony\Component\Messenger\Transport\AmqpExt;
|
||||
|
||||
use Symfony\Component\Messenger\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Messenger\Exception\LogicException;
|
||||
use Symfony\Component\Messenger\Bridge\Amqp\Transport\Connection as BridgeConnection;
|
||||
|
||||
/**
|
||||
* 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',
|
||||
];
|
||||
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 5.1, use "%s" instead. The AmqpExt transport has been moved to package "symfony/amqp-messenger" and will not be included by default in 6.0. Run "composer require symfony/amqp-messenger".', Connection::class, BridgeConnection::class), E_USER_DEPRECATED);
|
||||
|
||||
private $connectionOptions;
|
||||
private $exchangeOptions;
|
||||
private $queuesOptions;
|
||||
private $amqpFactory;
|
||||
class_exists(BridgeConnection::class);
|
||||
|
||||
if (false) {
|
||||
/**
|
||||
* @var \AMQPChannel|null
|
||||
* @deprecated since Symfony 5.1, to be removed in 6.0. Use symfony/amqp-messenger instead.
|
||||
*/
|
||||
private $amqpChannel;
|
||||
|
||||
/**
|
||||
* @var \AMQPExchange|null
|
||||
*/
|
||||
private $amqpExchange;
|
||||
|
||||
/**
|
||||
* @var \AMQPQueue[]|null
|
||||
*/
|
||||
private $amqpQueues = [];
|
||||
|
||||
/**
|
||||
* @var \AMQPExchange|null
|
||||
*/
|
||||
private $amqpDelayExchange;
|
||||
|
||||
public function __construct(array $connectionOptions, array $exchangeOptions, array $queuesOptions, AmqpFactory $amqpFactory = null)
|
||||
class Connection
|
||||
{
|
||||
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->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: Username to use to connect the the AMQP service
|
||||
* * password: Password to use the connect to the AMQP service
|
||||
* * 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)
|
||||
* * prefetch_count: set channel prefetch count
|
||||
*/
|
||||
public static function fromDsn(string $dsn, array $options = [], AmqpFactory $amqpFactory = null): self
|
||||
{
|
||||
if (false === $parsedUrl = parse_url($dsn)) {
|
||||
// this is a valid URI that parse_url cannot handle when you want to pass all parameters as options
|
||||
if ('amqp://' !== $dsn) {
|
||||
throw new InvalidArgumentException(sprintf('The given AMQP DSN "%s" is invalid.', $dsn));
|
||||
}
|
||||
|
||||
$parsedUrl = [];
|
||||
}
|
||||
|
||||
$pathParts = isset($parsedUrl['path']) ? explode('/', trim($parsedUrl['path'], '/')) : [];
|
||||
$exchangeName = $pathParts[1] ?? 'messages';
|
||||
parse_str($parsedUrl['query'] ?? '', $parsedQuery);
|
||||
|
||||
$amqpOptions = array_replace_recursive([
|
||||
'host' => $parsedUrl['host'] ?? 'localhost',
|
||||
'port' => $parsedUrl['port'] ?? 5672,
|
||||
'vhost' => isset($pathParts[0]) ? urldecode($pathParts[0]) : '/',
|
||||
'exchange' => [
|
||||
'name' => $exchangeName,
|
||||
],
|
||||
], $options, $parsedQuery);
|
||||
|
||||
if (isset($parsedUrl['user'])) {
|
||||
$amqpOptions['login'] = $parsedUrl['user'];
|
||||
}
|
||||
|
||||
if (isset($parsedUrl['pass'])) {
|
||||
$amqpOptions['password'] = $parsedUrl['pass'];
|
||||
}
|
||||
|
||||
if (!isset($amqpOptions['queues'])) {
|
||||
$amqpOptions['queues'][$exchangeName] = [];
|
||||
}
|
||||
|
||||
$exchangeOptions = $amqpOptions['exchange'];
|
||||
$queuesOptions = $amqpOptions['queues'];
|
||||
unset($amqpOptions['queues'], $amqpOptions['exchange']);
|
||||
|
||||
$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);
|
||||
|
||||
return new self($amqpOptions, $exchangeOptions, $queuesOptions, $amqpFactory);
|
||||
}
|
||||
|
||||
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, \gettype($arguments[$key])));
|
||||
}
|
||||
|
||||
$arguments[$key] = (int) $arguments[$key];
|
||||
}
|
||||
|
||||
return $arguments;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \AMQPException
|
||||
*/
|
||||
public function publish(string $body, array $headers = [], int $delayInMs = 0, AmqpStamp $amqpStamp = null): void
|
||||
{
|
||||
$this->clearWhenDisconnected();
|
||||
|
||||
if (0 !== $delayInMs) {
|
||||
$this->publishWithDelay($body, $headers, $delayInMs, $amqpStamp);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->shouldSetup()) {
|
||||
$this->setupExchangeAndQueues();
|
||||
}
|
||||
|
||||
$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);
|
||||
|
||||
$this->setupDelay($delay, $routingKey);
|
||||
|
||||
$this->publishOnExchange(
|
||||
$this->getDelayExchange(),
|
||||
$body,
|
||||
$this->getRoutingKeyForDelay($delay, $routingKey),
|
||||
$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;
|
||||
|
||||
$exchange->publish(
|
||||
$body,
|
||||
$routingKey,
|
||||
$amqpStamp ? $amqpStamp->getFlags() : AMQP_NOPARAM,
|
||||
$attributes
|
||||
);
|
||||
}
|
||||
|
||||
private function setupDelay(int $delay, ?string $routingKey)
|
||||
{
|
||||
if ($this->shouldSetup()) {
|
||||
$this->setup(); // setup delay exchange and normal exchange for delay queue to DLX messages to
|
||||
}
|
||||
|
||||
$queue = $this->createDelayQueue($delay, $routingKey);
|
||||
$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));
|
||||
}
|
||||
|
||||
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): \AMQPQueue
|
||||
{
|
||||
$queue = $this->amqpFactory->createQueue($this->channel());
|
||||
$queue->setName(str_replace(
|
||||
['%delay%', '%exchange_name%', '%routing_key%'],
|
||||
[$delay, $this->exchangeOptions['name'], $routingKey ?? ''],
|
||||
$this->connectionOptions['delay']['queue_name_pattern']
|
||||
));
|
||||
$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,
|
||||
'x-dead-letter-exchange' => $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): string
|
||||
{
|
||||
return str_replace(
|
||||
['%delay%', '%exchange_name%', '%routing_key%'],
|
||||
[$delay, $this->exchangeOptions['name'], $finalRoutingKey ?? ''],
|
||||
$this->connectionOptions['delay']['queue_name_pattern']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a message from the specified queue.
|
||||
*
|
||||
* @throws \AMQPException
|
||||
*/
|
||||
public function get(string $queueName): ?\AMQPEnvelope
|
||||
{
|
||||
$this->clearWhenDisconnected();
|
||||
|
||||
if ($this->shouldSetup()) {
|
||||
$this->setupExchangeAndQueues();
|
||||
}
|
||||
|
||||
try {
|
||||
if (false !== $message = $this->queue($queueName)->get()) {
|
||||
return $message;
|
||||
}
|
||||
} catch (\AMQPQueueException $e) {
|
||||
if (404 === $e->getCode() && $this->shouldSetup()) {
|
||||
// If we get a 404 for the queue, it means we need to set up the exchange & queue.
|
||||
$this->setupExchangeAndQueues();
|
||||
|
||||
return $this->get();
|
||||
}
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function ack(\AMQPEnvelope $message, string $queueName): bool
|
||||
{
|
||||
return $this->queue($queueName)->ack($message->getDeliveryTag());
|
||||
}
|
||||
|
||||
public function nack(\AMQPEnvelope $message, string $queueName, int $flags = AMQP_NOPARAM): bool
|
||||
{
|
||||
return $this->queue($queueName)->nack($message->getDeliveryTag(), $flags);
|
||||
}
|
||||
|
||||
public function setup(): void
|
||||
{
|
||||
$this->setupExchangeAndQueues();
|
||||
$this->getDelayExchange()->declareExchange();
|
||||
}
|
||||
|
||||
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'] ?? []);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @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) {
|
||||
$credentials = $this->connectionOptions;
|
||||
$credentials['password'] = '********';
|
||||
unset($credentials['delay']);
|
||||
|
||||
throw new \AMQPException(sprintf('Could not connect to the AMQP server. Please verify the provided DSN. (%s)', json_encode($credentials)), 0, $e);
|
||||
}
|
||||
$this->amqpChannel = $this->amqpFactory->createChannel($connection);
|
||||
|
||||
if (isset($this->connectionOptions['prefetch_count'])) {
|
||||
$this->amqpChannel->setPrefetchCount($this->connectionOptions['prefetch_count']);
|
||||
}
|
||||
}
|
||||
|
||||
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->amqpChannel = null;
|
||||
$this->amqpQueues = [];
|
||||
$this->amqpExchange = null;
|
||||
$this->amqpDelayExchange = null;
|
||||
}
|
||||
}
|
||||
|
||||
private function shouldSetup(): bool
|
||||
{
|
||||
if (!\array_key_exists('auto_setup', $this->connectionOptions)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (\in_array($this->connectionOptions['auto_setup'], [false, 'false'], true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,336 +11,17 @@
|
|||
|
||||
namespace Symfony\Component\Messenger\Transport\Doctrine;
|
||||
|
||||
use Doctrine\DBAL\Connection as DBALConnection;
|
||||
use Doctrine\DBAL\DBALException;
|
||||
use Doctrine\DBAL\Driver\ResultStatement;
|
||||
use Doctrine\DBAL\Exception\TableNotFoundException;
|
||||
use Doctrine\DBAL\Query\QueryBuilder;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\DBAL\Schema\Synchronizer\SchemaSynchronizer;
|
||||
use Doctrine\DBAL\Schema\Synchronizer\SingleDatabaseSynchronizer;
|
||||
use Doctrine\DBAL\Types\Type;
|
||||
use Symfony\Component\Messenger\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Messenger\Exception\TransportException;
|
||||
use Symfony\Component\Messenger\Bridge\Doctrine\Transport\Connection as BridgeConnection;
|
||||
|
||||
/**
|
||||
* @author Vincent Touzet <vincent.touzet@gmail.com>
|
||||
*
|
||||
* @final
|
||||
*/
|
||||
class Connection
|
||||
{
|
||||
private const DEFAULT_OPTIONS = [
|
||||
'table_name' => 'messenger_messages',
|
||||
'queue_name' => 'default',
|
||||
'redeliver_timeout' => 3600,
|
||||
'auto_setup' => true,
|
||||
];
|
||||
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 5.1, use "%s" instead. The Doctrine transport has been moved to package "symfony/doctrine-messenger" and will not be included by default in 6.0. Run "composer require symfony/doctrine-messenger".', Connection::class, BridgeConnection::class), E_USER_DEPRECATED);
|
||||
|
||||
class_exists(BridgeConnection::class);
|
||||
|
||||
if (false) {
|
||||
/**
|
||||
* 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
|
||||
* @deprecated since Symfony 5.1, to be removed in 6.0. Use symfony/doctrine-messenger instead.
|
||||
*/
|
||||
private $configuration = [];
|
||||
private $driverConnection;
|
||||
private $schemaSynchronizer;
|
||||
private $autoSetup;
|
||||
|
||||
public function __construct(array $configuration, DBALConnection $driverConnection, SchemaSynchronizer $schemaSynchronizer = null)
|
||||
class Connection
|
||||
{
|
||||
$this->configuration = array_replace_recursive(self::DEFAULT_OPTIONS, $configuration);
|
||||
$this->driverConnection = $driverConnection;
|
||||
$this->schemaSynchronizer = $schemaSynchronizer ?? new SingleDatabaseSynchronizer($this->driverConnection);
|
||||
$this->autoSetup = $this->configuration['auto_setup'];
|
||||
}
|
||||
|
||||
public function getConfiguration(): array
|
||||
{
|
||||
return $this->configuration;
|
||||
}
|
||||
|
||||
public static function buildConfiguration(string $dsn, array $options = []): array
|
||||
{
|
||||
if (false === $components = parse_url($dsn)) {
|
||||
throw new InvalidArgumentException(sprintf('The given Doctrine Messenger DSN "%s" is invalid.', $dsn));
|
||||
}
|
||||
|
||||
$query = [];
|
||||
if (isset($components['query'])) {
|
||||
parse_str($components['query'], $query);
|
||||
}
|
||||
|
||||
$configuration = ['connection' => $components['host']];
|
||||
$configuration += $options + $query + self::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(self::DEFAULT_OPTIONS));
|
||||
if (0 < \count($optionsExtraKeys)) {
|
||||
throw new InvalidArgumentException(sprintf('Unknown option found : [%s]. Allowed options are [%s]', implode(', ', $optionsExtraKeys), implode(', ', self::DEFAULT_OPTIONS)));
|
||||
}
|
||||
|
||||
// check for extra keys in options
|
||||
$queryExtraKeys = array_diff(array_keys($query), array_keys(self::DEFAULT_OPTIONS));
|
||||
if (0 < \count($queryExtraKeys)) {
|
||||
throw new InvalidArgumentException(sprintf('Unknown option found in DSN: [%s]. Allowed options are [%s]', implode(', ', $queryExtraKeys), implode(', ', self::DEFAULT_OPTIONS)));
|
||||
}
|
||||
|
||||
return $configuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $delay The delay in milliseconds
|
||||
*
|
||||
* @return string The inserted id
|
||||
*
|
||||
* @throws \Doctrine\DBAL\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->executeQuery($queryBuilder->getSQL(), [
|
||||
$body,
|
||||
json_encode($headers),
|
||||
$this->configuration['queue_name'],
|
||||
$now,
|
||||
$availableAt,
|
||||
], [
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
Type::DATETIME,
|
||||
Type::DATETIME,
|
||||
]);
|
||||
|
||||
return $this->driverConnection->lastInsertId();
|
||||
}
|
||||
|
||||
public function get(): ?array
|
||||
{
|
||||
get:
|
||||
$this->driverConnection->beginTransaction();
|
||||
try {
|
||||
$query = $this->createAvailableMessagesQueryBuilder()
|
||||
->orderBy('available_at', 'ASC')
|
||||
->setMaxResults(1);
|
||||
|
||||
// use SELECT ... FOR UPDATE to lock table
|
||||
$doctrineEnvelope = $this->executeQuery(
|
||||
$query->getSQL().' '.$this->driverConnection->getDatabasePlatform()->getWriteLockSQL(),
|
||||
$query->getParameters(),
|
||||
$query->getParameterTypes()
|
||||
)->fetch();
|
||||
|
||||
if (false === $doctrineEnvelope) {
|
||||
$this->driverConnection->commit();
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$doctrineEnvelope = $this->decodeEnvelopeHeaders($doctrineEnvelope);
|
||||
|
||||
$queryBuilder = $this->driverConnection->createQueryBuilder()
|
||||
->update($this->configuration['table_name'])
|
||||
->set('delivered_at', '?')
|
||||
->where('id = ?');
|
||||
$now = new \DateTime();
|
||||
$this->executeQuery($queryBuilder->getSQL(), [
|
||||
$now,
|
||||
$doctrineEnvelope['id'],
|
||||
], [
|
||||
Type::DATETIME,
|
||||
]);
|
||||
|
||||
$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 {
|
||||
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 {
|
||||
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();
|
||||
// Since Doctrine 2.9 the getFilterSchemaAssetsExpression is deprecated
|
||||
$hasFilterCallback = method_exists($configuration, 'getSchemaAssetsFilter');
|
||||
|
||||
if ($hasFilterCallback) {
|
||||
$assetFilter = $this->driverConnection->getConfiguration()->getSchemaAssetsFilter();
|
||||
$this->driverConnection->getConfiguration()->setSchemaAssetsFilter(null);
|
||||
} else {
|
||||
$assetFilter = $this->driverConnection->getConfiguration()->getFilterSchemaAssetsExpression();
|
||||
$this->driverConnection->getConfiguration()->setFilterSchemaAssetsExpression(null);
|
||||
}
|
||||
|
||||
$this->schemaSynchronizer->updateSchema($this->getSchema(), true);
|
||||
|
||||
if ($hasFilterCallback) {
|
||||
$this->driverConnection->getConfiguration()->setSchemaAssetsFilter($assetFilter);
|
||||
} else {
|
||||
$this->driverConnection->getConfiguration()->setFilterSchemaAssetsExpression($assetFilter);
|
||||
}
|
||||
|
||||
$this->autoSetup = false;
|
||||
}
|
||||
|
||||
public function getMessageCount(): int
|
||||
{
|
||||
$queryBuilder = $this->createAvailableMessagesQueryBuilder()
|
||||
->select('COUNT(m.id) as message_count')
|
||||
->setMaxResults(1);
|
||||
|
||||
return $this->executeQuery($queryBuilder->getSQL(), $queryBuilder->getParameters(), $queryBuilder->getParameterTypes())->fetchColumn();
|
||||
}
|
||||
|
||||
public function findAll(int $limit = null): array
|
||||
{
|
||||
$queryBuilder = $this->createAvailableMessagesQueryBuilder();
|
||||
if (null !== $limit) {
|
||||
$queryBuilder->setMaxResults($limit);
|
||||
}
|
||||
|
||||
$data = $this->executeQuery($queryBuilder->getSQL(), $queryBuilder->getParameters(), $queryBuilder->getParameterTypes())->fetchAll();
|
||||
|
||||
return array_map(function ($doctrineEnvelope) {
|
||||
return $this->decodeEnvelopeHeaders($doctrineEnvelope);
|
||||
}, $data);
|
||||
}
|
||||
|
||||
public function find($id): ?array
|
||||
{
|
||||
$queryBuilder = $this->createQueryBuilder()
|
||||
->where('m.id = ?');
|
||||
|
||||
$data = $this->executeQuery($queryBuilder->getSQL(), [
|
||||
$id,
|
||||
])->fetch();
|
||||
|
||||
return false === $data ? null : $this->decodeEnvelopeHeaders($data);
|
||||
}
|
||||
|
||||
private function createAvailableMessagesQueryBuilder(): QueryBuilder
|
||||
{
|
||||
$now = new \DateTime();
|
||||
$redeliverLimit = (clone $now)->modify(sprintf('-%d seconds', $this->configuration['redeliver_timeout']));
|
||||
|
||||
return $this->createQueryBuilder()
|
||||
->where('m.delivered_at is null OR m.delivered_at < ?')
|
||||
->andWhere('m.available_at <= ?')
|
||||
->andWhere('m.queue_name = ?')
|
||||
->setParameters([
|
||||
$redeliverLimit,
|
||||
$now,
|
||||
$this->configuration['queue_name'],
|
||||
], [
|
||||
Type::DATETIME,
|
||||
Type::DATETIME,
|
||||
]);
|
||||
}
|
||||
|
||||
private function createQueryBuilder(): QueryBuilder
|
||||
{
|
||||
return $this->driverConnection->createQueryBuilder()
|
||||
->select('m.*')
|
||||
->from($this->configuration['table_name'], 'm');
|
||||
}
|
||||
|
||||
private function executeQuery(string $sql, array $parameters = [], array $types = []): ResultStatement
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
private function getSchema(): Schema
|
||||
{
|
||||
$schema = new Schema([], [], $this->driverConnection->getSchemaManager()->createSchemaConfig());
|
||||
$table = $schema->createTable($this->configuration['table_name']);
|
||||
$table->addColumn('id', Type::BIGINT)
|
||||
->setAutoincrement(true)
|
||||
->setNotnull(true);
|
||||
$table->addColumn('body', Type::TEXT)
|
||||
->setNotnull(true);
|
||||
$table->addColumn('headers', Type::TEXT)
|
||||
->setNotnull(true);
|
||||
$table->addColumn('queue_name', Type::STRING)
|
||||
->setNotnull(true);
|
||||
$table->addColumn('created_at', Type::DATETIME)
|
||||
->setNotnull(true);
|
||||
$table->addColumn('available_at', Type::DATETIME)
|
||||
->setNotnull(true);
|
||||
$table->addColumn('delivered_at', Type::DATETIME)
|
||||
->setNotnull(false);
|
||||
$table->setPrimaryKey(['id']);
|
||||
$table->addIndex(['queue_name']);
|
||||
$table->addIndex(['available_at']);
|
||||
$table->addIndex(['delivered_at']);
|
||||
|
||||
return $schema;
|
||||
}
|
||||
|
||||
private function decodeEnvelopeHeaders(array $doctrineEnvelope): array
|
||||
{
|
||||
$doctrineEnvelope['headers'] = json_decode($doctrineEnvelope['headers'], true);
|
||||
|
||||
return $doctrineEnvelope;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,22 +11,17 @@
|
|||
|
||||
namespace Symfony\Component\Messenger\Transport\Doctrine;
|
||||
|
||||
use Symfony\Component\Messenger\Stamp\NonSendableStampInterface;
|
||||
use Symfony\Component\Messenger\Bridge\Doctrine\Transport\DoctrineReceivedStamp as BridgeDoctrineReceivedStamp;
|
||||
|
||||
/**
|
||||
* @author Vincent Touzet <vincent.touzet@gmail.com>
|
||||
*/
|
||||
class DoctrineReceivedStamp implements NonSendableStampInterface
|
||||
{
|
||||
private $id;
|
||||
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 5.1, use "%s" instead. The Doctrine transport has been moved to package "symfony/doctrine-messenger" and will not be included by default in 6.0. Run "composer require symfony/doctrine-messenger".', DoctrineReceivedStamp::class, BridgeDoctrineReceivedStamp::class), E_USER_DEPRECATED);
|
||||
|
||||
public function __construct(string $id)
|
||||
class_exists(BridgeDoctrineReceivedStamp::class);
|
||||
|
||||
if (false) {
|
||||
/**
|
||||
* @deprecated since Symfony 5.1, to be removed in 6.0. Use symfony/doctrine-messenger instead.
|
||||
*/
|
||||
class DoctrineReceivedStamp
|
||||
{
|
||||
$this->id = $id;
|
||||
}
|
||||
|
||||
public function getId(): string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,162 +11,17 @@
|
|||
|
||||
namespace Symfony\Component\Messenger\Transport\Doctrine;
|
||||
|
||||
use Doctrine\DBAL\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\Receiver\ReceiverInterface;
|
||||
use Symfony\Component\Messenger\Transport\Serialization\PhpSerializer;
|
||||
use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface;
|
||||
use Symfony\Component\Messenger\Bridge\Doctrine\Transport\DoctrineReceiver as BridgeDoctrineReceiver;
|
||||
|
||||
/**
|
||||
* @author Vincent Touzet <vincent.touzet@gmail.com>
|
||||
*/
|
||||
class DoctrineReceiver implements ReceiverInterface, MessageCountAwareInterface, ListableReceiverInterface
|
||||
{
|
||||
private const MAX_RETRIES = 3;
|
||||
private $retryingSafetyCounter = 0;
|
||||
private $connection;
|
||||
private $serializer;
|
||||
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 5.1, use "%s" instead. The Doctrine transport has been moved to package "symfony/doctrine-messenger" and will not be included by default in 6.0. Run "composer require symfony/doctrine-messenger".', DoctrineReceiver::class, BridgeDoctrineReceiver::class), E_USER_DEPRECATED);
|
||||
|
||||
public function __construct(Connection $connection, SerializerInterface $serializer = null)
|
||||
{
|
||||
$this->connection = $connection;
|
||||
$this->serializer = $serializer ?? new PhpSerializer();
|
||||
}
|
||||
class_exists(BridgeDoctrineReceiver::class);
|
||||
|
||||
if (false) {
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @deprecated since Symfony 5.1, to be removed in 6.0. Use symfony/doctrine-messenger instead.
|
||||
*/
|
||||
public function get(): iterable
|
||||
class DoctrineReceiver
|
||||
{
|
||||
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'])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,46 +11,17 @@
|
|||
|
||||
namespace Symfony\Component\Messenger\Transport\Doctrine;
|
||||
|
||||
use Doctrine\DBAL\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;
|
||||
use Symfony\Component\Messenger\Bridge\Doctrine\Transport\DoctrineSender as BridgeDoctrineSender;
|
||||
|
||||
/**
|
||||
* @author Vincent Touzet <vincent.touzet@gmail.com>
|
||||
*/
|
||||
class DoctrineSender implements SenderInterface
|
||||
{
|
||||
private $connection;
|
||||
private $serializer;
|
||||
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 5.1, use "%s" instead. The Doctrine transport has been moved to package "symfony/doctrine-messenger" and will not be included by default in 6.0. Run "composer require symfony/doctrine-messenger".', DoctrineSender::class, BridgeDoctrineSender::class), E_USER_DEPRECATED);
|
||||
|
||||
public function __construct(Connection $connection, SerializerInterface $serializer = null)
|
||||
{
|
||||
$this->connection = $connection;
|
||||
$this->serializer = $serializer ?? new PhpSerializer();
|
||||
}
|
||||
class_exists(BridgeDoctrineSender::class);
|
||||
|
||||
if (false) {
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @deprecated since Symfony 5.1, to be removed in 6.0. Use symfony/doctrine-messenger instead.
|
||||
*/
|
||||
public function send(Envelope $envelope): Envelope
|
||||
class DoctrineSender
|
||||
{
|
||||
$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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,100 +11,17 @@
|
|||
|
||||
namespace Symfony\Component\Messenger\Transport\Doctrine;
|
||||
|
||||
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;
|
||||
use Symfony\Component\Messenger\Bridge\Doctrine\Transport\DoctrineTransport as BridgeDoctrineTransport;
|
||||
|
||||
/**
|
||||
* @author Vincent Touzet <vincent.touzet@gmail.com>
|
||||
*/
|
||||
class DoctrineTransport implements TransportInterface, SetupableTransportInterface, MessageCountAwareInterface, ListableReceiverInterface
|
||||
{
|
||||
private $connection;
|
||||
private $serializer;
|
||||
private $receiver;
|
||||
private $sender;
|
||||
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 5.1, use "%s" instead. The Doctrine transport has been moved to package "symfony/doctrine-messenger" and will not be included by default in 6.0. Run "composer require symfony/doctrine-messenger".', DoctrineTransport::class, BridgeDoctrineTransport::class), E_USER_DEPRECATED);
|
||||
|
||||
public function __construct(Connection $connection, SerializerInterface $serializer)
|
||||
{
|
||||
$this->connection = $connection;
|
||||
$this->serializer = $serializer;
|
||||
}
|
||||
class_exists(BridgeDoctrineTransport::class);
|
||||
|
||||
if (false) {
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @deprecated since Symfony 5.1, to be removed in 6.0. Use symfony/doctrine-messenger instead.
|
||||
*/
|
||||
public function get(): iterable
|
||||
class DoctrineTransport
|
||||
{
|
||||
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();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,47 +11,17 @@
|
|||
|
||||
namespace Symfony\Component\Messenger\Transport\Doctrine;
|
||||
|
||||
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;
|
||||
use Symfony\Component\Messenger\Bridge\Doctrine\Transport\DoctrineTransportFactory as BridgeDoctrineTransportFactory;
|
||||
|
||||
/**
|
||||
* @author Vincent Touzet <vincent.touzet@gmail.com>
|
||||
*/
|
||||
class DoctrineTransportFactory implements TransportFactoryInterface
|
||||
{
|
||||
private $registry;
|
||||
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 5.1, use "%s" instead. The Doctrine transport has been moved to package "symfony/doctrine-messenger" and will not be included by default in 6.0. Run "composer require symfony/doctrine-messenger".', DoctrineTransportFactory::class, BridgeDoctrineTransportFactory::class), E_USER_DEPRECATED);
|
||||
|
||||
public function __construct($registry)
|
||||
class_exists(BridgeDoctrineTransportFactory::class);
|
||||
|
||||
if (false) {
|
||||
/**
|
||||
* @deprecated since Symfony 5.1, to be removed in 6.0. Use symfony/doctrine-messenger instead.
|
||||
*/
|
||||
class DoctrineTransportFactory
|
||||
{
|
||||
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, \is_object($registry) ? \get_class($registry) : \gettype($registry)));
|
||||
}
|
||||
|
||||
$this->registry = $registry;
|
||||
}
|
||||
|
||||
public function createTransport(string $dsn, array $options, SerializerInterface $serializer): TransportInterface
|
||||
{
|
||||
unset($options['transport_name']);
|
||||
$configuration = Connection::buildConfiguration($dsn, $options);
|
||||
|
||||
try {
|
||||
$driverConnection = $this->registry->getConnection($configuration['connection']);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
throw new TransportException(sprintf('Could not find Doctrine connection from Messenger DSN "%s".', $dsn), 0, $e);
|
||||
}
|
||||
|
||||
$connection = new Connection($configuration, $driverConnection);
|
||||
|
||||
return new DoctrineTransport($connection, $serializer);
|
||||
}
|
||||
|
||||
public function supports(string $dsn, array $options): bool
|
||||
{
|
||||
return 0 === strpos($dsn, 'doctrine://');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,318 +11,17 @@
|
|||
|
||||
namespace Symfony\Component\Messenger\Transport\RedisExt;
|
||||
|
||||
use Symfony\Component\Messenger\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Messenger\Exception\LogicException;
|
||||
use Symfony\Component\Messenger\Exception\TransportException;
|
||||
use Symfony\Component\Messenger\Bridge\Redis\Transport\Connection as BridgeConnection;
|
||||
|
||||
/**
|
||||
* A Redis connection.
|
||||
*
|
||||
* @author Alexander Schranz <alexander@sulu.io>
|
||||
* @author Antoine Bluchet <soyuka@gmail.com>
|
||||
* @author Robin Chalas <robin.chalas@gmail.com>
|
||||
*
|
||||
* @internal
|
||||
* @final
|
||||
*/
|
||||
class Connection
|
||||
{
|
||||
private const DEFAULT_OPTIONS = [
|
||||
'stream' => 'messages',
|
||||
'group' => 'symfony',
|
||||
'consumer' => 'consumer',
|
||||
'auto_setup' => true,
|
||||
'stream_max_entries' => 0, // any value higher than 0 defines an approximate maximum number of stream entries
|
||||
'dbindex' => 0,
|
||||
];
|
||||
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 5.1, use "%s" instead. The RedisExt transport has been moved to package "symfony/redis-messenger" and will not be included by default in 6.0. Run "composer require symfony/redis-messenger".', Connection::class, BridgeConnection::class), E_USER_DEPRECATED);
|
||||
|
||||
private $connection;
|
||||
private $stream;
|
||||
private $queue;
|
||||
private $group;
|
||||
private $consumer;
|
||||
private $autoSetup;
|
||||
private $maxEntries;
|
||||
private $couldHavePendingMessages = true;
|
||||
class_exists(BridgeConnection::class);
|
||||
|
||||
public function __construct(array $configuration, array $connectionCredentials = [], array $redisOptions = [], \Redis $redis = null)
|
||||
if (false) {
|
||||
/**
|
||||
* @deprecated since Symfony 5.1, to be removed in 6.0. Use symfony/redis-messenger instead.
|
||||
*/
|
||||
class Connection
|
||||
{
|
||||
if (version_compare(phpversion('redis'), '4.3.0', '<')) {
|
||||
throw new LogicException('The redis transport requires php-redis 4.3.0 or higher.');
|
||||
}
|
||||
|
||||
$this->connection = $redis ?: new \Redis();
|
||||
$this->connection->connect($connectionCredentials['host'] ?? '127.0.0.1', $connectionCredentials['port'] ?? 6379);
|
||||
$this->connection->setOption(\Redis::OPT_SERIALIZER, $redisOptions['serializer'] ?? \Redis::SERIALIZER_PHP);
|
||||
|
||||
if (isset($connectionCredentials['auth']) && !$this->connection->auth($connectionCredentials['auth'])) {
|
||||
throw new InvalidArgumentException(sprintf('Redis connection failed: %s', $redis->getLastError()));
|
||||
}
|
||||
|
||||
if (($dbIndex = $configuration['dbindex'] ?? self::DEFAULT_OPTIONS['dbindex']) && !$this->connection->select($dbIndex)) {
|
||||
throw new InvalidArgumentException(sprintf('Redis connection failed: %s', $redis->getLastError()));
|
||||
}
|
||||
|
||||
$this->stream = $configuration['stream'] ?? self::DEFAULT_OPTIONS['stream'];
|
||||
$this->group = $configuration['group'] ?? self::DEFAULT_OPTIONS['group'];
|
||||
$this->consumer = $configuration['consumer'] ?? self::DEFAULT_OPTIONS['consumer'];
|
||||
$this->queue = $this->stream.'__queue';
|
||||
$this->autoSetup = $configuration['auto_setup'] ?? self::DEFAULT_OPTIONS['auto_setup'];
|
||||
$this->maxEntries = $configuration['stream_max_entries'] ?? self::DEFAULT_OPTIONS['stream_max_entries'];
|
||||
}
|
||||
|
||||
public static function fromDsn(string $dsn, array $redisOptions = [], \Redis $redis = null): self
|
||||
{
|
||||
$url = $dsn;
|
||||
|
||||
if (preg_match('#^redis:///([^:@])+$#', $dsn)) {
|
||||
$url = str_replace('redis:', 'file:', $dsn);
|
||||
}
|
||||
|
||||
if (false === $parsedUrl = parse_url($url)) {
|
||||
throw new InvalidArgumentException(sprintf('The given Redis DSN "%s" is invalid.', $dsn));
|
||||
}
|
||||
if (isset($parsedUrl['query'])) {
|
||||
parse_str($parsedUrl['query'], $redisOptions);
|
||||
}
|
||||
|
||||
$autoSetup = null;
|
||||
if (\array_key_exists('auto_setup', $redisOptions)) {
|
||||
$autoSetup = filter_var($redisOptions['auto_setup'], FILTER_VALIDATE_BOOLEAN);
|
||||
unset($redisOptions['auto_setup']);
|
||||
}
|
||||
|
||||
$maxEntries = null;
|
||||
if (\array_key_exists('stream_max_entries', $redisOptions)) {
|
||||
$maxEntries = filter_var($redisOptions['stream_max_entries'], FILTER_VALIDATE_INT);
|
||||
unset($redisOptions['stream_max_entries']);
|
||||
}
|
||||
|
||||
$dbIndex = null;
|
||||
if (\array_key_exists('dbindex', $redisOptions)) {
|
||||
$dbIndex = filter_var($redisOptions['dbindex'], FILTER_VALIDATE_INT);
|
||||
unset($redisOptions['dbindex']);
|
||||
}
|
||||
|
||||
$configuration = [
|
||||
'stream' => $redisOptions['stream'] ?? null,
|
||||
'group' => $redisOptions['group'] ?? null,
|
||||
'consumer' => $redisOptions['consumer'] ?? null,
|
||||
'auto_setup' => $autoSetup,
|
||||
'stream_max_entries' => $maxEntries,
|
||||
'dbindex' => $dbIndex,
|
||||
];
|
||||
|
||||
if (isset($parsedUrl['host'])) {
|
||||
$connectionCredentials = [
|
||||
'host' => $parsedUrl['host'] ?? '127.0.0.1',
|
||||
'port' => $parsedUrl['port'] ?? 6379,
|
||||
'auth' => $parsedUrl['pass'] ?? $parsedUrl['user'] ?? null,
|
||||
];
|
||||
|
||||
$pathParts = explode('/', $parsedUrl['path'] ?? '');
|
||||
|
||||
$configuration['stream'] = $pathParts[1] ?? $configuration['stream'];
|
||||
$configuration['group'] = $pathParts[2] ?? $configuration['group'];
|
||||
$configuration['consumer'] = $pathParts[3] ?? $configuration['consumer'];
|
||||
} else {
|
||||
$connectionCredentials = [
|
||||
'host' => $parsedUrl['path'],
|
||||
'port' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
return new self($configuration, $connectionCredentials, $redisOptions, $redis);
|
||||
}
|
||||
|
||||
public function get(): ?array
|
||||
{
|
||||
if ($this->autoSetup) {
|
||||
$this->setup();
|
||||
}
|
||||
|
||||
try {
|
||||
$queuedMessageCount = $this->connection->zcount($this->queue, 0, $this->getCurrentTimeInMilliseconds());
|
||||
} catch (\RedisException $e) {
|
||||
throw new TransportException($e->getMessage(), 0, $e);
|
||||
}
|
||||
|
||||
if ($queuedMessageCount) {
|
||||
for ($i = 0; $i < $queuedMessageCount; ++$i) {
|
||||
try {
|
||||
$queuedMessages = $this->connection->zpopmin($this->queue, 1);
|
||||
} catch (\RedisException $e) {
|
||||
throw new TransportException($e->getMessage(), 0, $e);
|
||||
}
|
||||
|
||||
foreach ($queuedMessages as $queuedMessage => $time) {
|
||||
$queuedMessage = json_decode($queuedMessage, true);
|
||||
// if a futured placed message is actually popped because of a race condition with
|
||||
// another running message consumer, the message is readded to the queue by add function
|
||||
// else its just added stream and will be available for all stream consumers
|
||||
$this->add(
|
||||
$queuedMessage['body'],
|
||||
$queuedMessage['headers'],
|
||||
$time - $this->getCurrentTimeInMilliseconds()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$messageId = '>'; // will receive new messages
|
||||
|
||||
if ($this->couldHavePendingMessages) {
|
||||
$messageId = '0'; // will receive consumers pending messages
|
||||
}
|
||||
|
||||
try {
|
||||
$messages = $this->connection->xreadgroup(
|
||||
$this->group,
|
||||
$this->consumer,
|
||||
[$this->stream => $messageId],
|
||||
1
|
||||
);
|
||||
} catch (\RedisException $e) {
|
||||
throw new TransportException($e->getMessage(), 0, $e);
|
||||
}
|
||||
|
||||
if (false === $messages) {
|
||||
if ($error = $this->connection->getLastError() ?: null) {
|
||||
$this->connection->clearLastError();
|
||||
}
|
||||
|
||||
throw new TransportException($error ?? 'Could not read messages from the redis stream.');
|
||||
}
|
||||
|
||||
if ($this->couldHavePendingMessages && empty($messages[$this->stream])) {
|
||||
$this->couldHavePendingMessages = false;
|
||||
|
||||
// No pending messages so get a new one
|
||||
return $this->get();
|
||||
}
|
||||
|
||||
foreach ($messages[$this->stream] ?? [] as $key => $message) {
|
||||
$redisEnvelope = json_decode($message['message'], true);
|
||||
|
||||
return [
|
||||
'id' => $key,
|
||||
'body' => $redisEnvelope['body'],
|
||||
'headers' => $redisEnvelope['headers'],
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function ack(string $id): void
|
||||
{
|
||||
try {
|
||||
$acknowledged = $this->connection->xack($this->stream, $this->group, [$id]);
|
||||
} catch (\RedisException $e) {
|
||||
throw new TransportException($e->getMessage(), 0, $e);
|
||||
}
|
||||
|
||||
if (!$acknowledged) {
|
||||
if ($error = $this->connection->getLastError() ?: null) {
|
||||
$this->connection->clearLastError();
|
||||
}
|
||||
throw new TransportException($error ?? sprintf('Could not acknowledge redis message "%s".', $id));
|
||||
}
|
||||
}
|
||||
|
||||
public function reject(string $id): void
|
||||
{
|
||||
try {
|
||||
$deleted = $this->connection->xack($this->stream, $this->group, [$id]);
|
||||
$deleted = $this->connection->xdel($this->stream, [$id]) && $deleted;
|
||||
} catch (\RedisException $e) {
|
||||
throw new TransportException($e->getMessage(), 0, $e);
|
||||
}
|
||||
|
||||
if (!$deleted) {
|
||||
if ($error = $this->connection->getLastError() ?: null) {
|
||||
$this->connection->clearLastError();
|
||||
}
|
||||
throw new TransportException($error ?? sprintf('Could not delete message "%s" from the redis stream.', $id));
|
||||
}
|
||||
}
|
||||
|
||||
public function add(string $body, array $headers, int $delayInMs = 0): void
|
||||
{
|
||||
if ($this->autoSetup) {
|
||||
$this->setup();
|
||||
}
|
||||
|
||||
try {
|
||||
if ($delayInMs > 0) { // the delay could be smaller 0 in a queued message
|
||||
$message = json_encode([
|
||||
'body' => $body,
|
||||
'headers' => $headers,
|
||||
// Entry need to be unique in the sorted set else it would only be added once to the delayed messages queue
|
||||
'uniqid' => uniqid('', true),
|
||||
]);
|
||||
|
||||
if (false === $message) {
|
||||
throw new TransportException(json_last_error_msg());
|
||||
}
|
||||
|
||||
$score = (int) ($this->getCurrentTimeInMilliseconds() + $delayInMs);
|
||||
$added = $this->connection->zadd($this->queue, ['NX'], $score, $message);
|
||||
} else {
|
||||
$message = json_encode([
|
||||
'body' => $body,
|
||||
'headers' => $headers,
|
||||
]);
|
||||
|
||||
if (false === $message) {
|
||||
throw new TransportException(json_last_error_msg());
|
||||
}
|
||||
|
||||
if ($this->maxEntries) {
|
||||
$added = $this->connection->xadd($this->stream, '*', ['message' => $message], $this->maxEntries, true);
|
||||
} else {
|
||||
$added = $this->connection->xadd($this->stream, '*', ['message' => $message]);
|
||||
}
|
||||
}
|
||||
} catch (\RedisException $e) {
|
||||
if ($error = $this->connection->getLastError() ?: null) {
|
||||
$this->connection->clearLastError();
|
||||
}
|
||||
throw new TransportException($error ?? $e->getMessage(), 0, $e);
|
||||
}
|
||||
|
||||
if (!$added) {
|
||||
if ($error = $this->connection->getLastError() ?: null) {
|
||||
$this->connection->clearLastError();
|
||||
}
|
||||
throw new TransportException($error ?? 'Could not add a message to the redis stream.');
|
||||
}
|
||||
}
|
||||
|
||||
public function setup(): void
|
||||
{
|
||||
try {
|
||||
$this->connection->xgroup('CREATE', $this->stream, $this->group, 0, true);
|
||||
} catch (\RedisException $e) {
|
||||
throw new TransportException($e->getMessage(), 0, $e);
|
||||
}
|
||||
|
||||
// group might already exist, ignore
|
||||
if ($this->connection->getLastError()) {
|
||||
$this->connection->clearLastError();
|
||||
}
|
||||
|
||||
$this->autoSetup = false;
|
||||
}
|
||||
|
||||
private function getCurrentTimeInMilliseconds(): int
|
||||
{
|
||||
return (int) (microtime(true) * 1000);
|
||||
}
|
||||
|
||||
public function cleanup(): void
|
||||
{
|
||||
$this->connection->del($this->stream);
|
||||
$this->connection->del($this->queue);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,22 +11,17 @@
|
|||
|
||||
namespace Symfony\Component\Messenger\Transport\RedisExt;
|
||||
|
||||
use Symfony\Component\Messenger\Stamp\NonSendableStampInterface;
|
||||
use Symfony\Component\Messenger\Bridge\Redis\Transport\RedisReceivedStamp as BridgeRedisReceivedStamp;
|
||||
|
||||
/**
|
||||
* @author Alexander Schranz <alexander@sulu.io>
|
||||
*/
|
||||
class RedisReceivedStamp implements NonSendableStampInterface
|
||||
{
|
||||
private $id;
|
||||
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 5.1, use "%s" instead. The RedisExt transport has been moved to package "symfony/redis-messenger" and will not be included by default in 6.0. Run "composer require symfony/redis-messenger".', RedisReceivedStamp::class, BridgeRedisReceivedStamp::class), E_USER_DEPRECATED);
|
||||
|
||||
public function __construct(string $id)
|
||||
class_exists(BridgeRedisReceivedStamp::class);
|
||||
|
||||
if (false) {
|
||||
/**
|
||||
* @deprecated since Symfony 5.1, to be removed in 6.0. Use symfony/redis-messenger instead.
|
||||
*/
|
||||
class RedisReceivedStamp
|
||||
{
|
||||
$this->id = $id;
|
||||
}
|
||||
|
||||
public function getId(): string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,78 +11,17 @@
|
|||
|
||||
namespace Symfony\Component\Messenger\Transport\RedisExt;
|
||||
|
||||
use Symfony\Component\Messenger\Envelope;
|
||||
use Symfony\Component\Messenger\Exception\LogicException;
|
||||
use Symfony\Component\Messenger\Exception\MessageDecodingFailedException;
|
||||
use Symfony\Component\Messenger\Transport\Receiver\ReceiverInterface;
|
||||
use Symfony\Component\Messenger\Transport\Serialization\PhpSerializer;
|
||||
use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface;
|
||||
use Symfony\Component\Messenger\Bridge\Redis\Transport\RedisReceiver as BridgeRedisReceiver;
|
||||
|
||||
/**
|
||||
* @author Alexander Schranz <alexander@sulu.io>
|
||||
* @author Antoine Bluchet <soyuka@gmail.com>
|
||||
*/
|
||||
class RedisReceiver implements ReceiverInterface
|
||||
{
|
||||
private $connection;
|
||||
private $serializer;
|
||||
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 5.1, use "%s" instead. The RedisExt transport has been moved to package "symfony/redis-messenger" and will not be included by default in 6.0. Run "composer require symfony/redis-messenger".', RedisReceiver::class, BridgeRedisReceiver::class), E_USER_DEPRECATED);
|
||||
|
||||
public function __construct(Connection $connection, SerializerInterface $serializer = null)
|
||||
{
|
||||
$this->connection = $connection;
|
||||
$this->serializer = $serializer ?? new PhpSerializer();
|
||||
}
|
||||
class_exists(BridgeRedisReceiver::class);
|
||||
|
||||
if (false) {
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @deprecated since Symfony 5.1, to be removed in 6.0. Use symfony/redis-messenger instead.
|
||||
*/
|
||||
public function get(): iterable
|
||||
class RedisReceiver
|
||||
{
|
||||
$redisEnvelope = $this->connection->get();
|
||||
|
||||
if (null === $redisEnvelope) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
$envelope = $this->serializer->decode([
|
||||
'body' => $redisEnvelope['body'],
|
||||
'headers' => $redisEnvelope['headers'],
|
||||
]);
|
||||
} catch (MessageDecodingFailedException $exception) {
|
||||
$this->connection->reject($redisEnvelope['id']);
|
||||
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
return [$envelope->with(new RedisReceivedStamp($redisEnvelope['id']))];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function ack(Envelope $envelope): void
|
||||
{
|
||||
$this->connection->ack($this->findRedisReceivedStamp($envelope)->getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function reject(Envelope $envelope): void
|
||||
{
|
||||
$this->connection->reject($this->findRedisReceivedStamp($envelope)->getId());
|
||||
}
|
||||
|
||||
private function findRedisReceivedStamp(Envelope $envelope): RedisReceivedStamp
|
||||
{
|
||||
/** @var RedisReceivedStamp|null $redisReceivedStamp */
|
||||
$redisReceivedStamp = $envelope->last(RedisReceivedStamp::class);
|
||||
|
||||
if (null === $redisReceivedStamp) {
|
||||
throw new LogicException('No RedisReceivedStamp found on the Envelope.');
|
||||
}
|
||||
|
||||
return $redisReceivedStamp;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,39 +11,17 @@
|
|||
|
||||
namespace Symfony\Component\Messenger\Transport\RedisExt;
|
||||
|
||||
use Symfony\Component\Messenger\Envelope;
|
||||
use Symfony\Component\Messenger\Stamp\DelayStamp;
|
||||
use Symfony\Component\Messenger\Transport\Sender\SenderInterface;
|
||||
use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface;
|
||||
use Symfony\Component\Messenger\Bridge\Redis\Transport\RedisSender as BridgeRedisSender;
|
||||
|
||||
/**
|
||||
* @author Alexander Schranz <alexander@sulu.io>
|
||||
* @author Antoine Bluchet <soyuka@gmail.com>
|
||||
*/
|
||||
class RedisSender implements SenderInterface
|
||||
{
|
||||
private $connection;
|
||||
private $serializer;
|
||||
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 5.1, use "%s" instead. The RedisExt transport has been moved to package "symfony/redis-messenger" and will not be included by default in 6.0. Run "composer require symfony/redis-messenger".', RedisSender::class, BridgeRedisSender::class), E_USER_DEPRECATED);
|
||||
|
||||
public function __construct(Connection $connection, SerializerInterface $serializer)
|
||||
{
|
||||
$this->connection = $connection;
|
||||
$this->serializer = $serializer;
|
||||
}
|
||||
class_exists(BridgeRedisSender::class);
|
||||
|
||||
if (false) {
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @deprecated since Symfony 5.1, to be removed in 6.0. Use symfony/redis-messenger instead.
|
||||
*/
|
||||
public function send(Envelope $envelope): Envelope
|
||||
class RedisSender
|
||||
{
|
||||
$encodedMessage = $this->serializer->encode($envelope);
|
||||
|
||||
/** @var DelayStamp|null $delayStamp */
|
||||
$delayStamp = $envelope->last(DelayStamp::class);
|
||||
$delayInMs = null !== $delayStamp ? $delayStamp->getDelay() : 0;
|
||||
|
||||
$this->connection->add($encodedMessage['body'], $encodedMessage['headers'] ?? [], $delayInMs);
|
||||
|
||||
return $envelope;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,76 +11,17 @@
|
|||
|
||||
namespace Symfony\Component\Messenger\Transport\RedisExt;
|
||||
|
||||
use Symfony\Component\Messenger\Envelope;
|
||||
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;
|
||||
use Symfony\Component\Messenger\Bridge\Redis\Transport\RedisTransport as BridgeRedisTransport;
|
||||
|
||||
/**
|
||||
* @author Alexander Schranz <alexander@sulu.io>
|
||||
* @author Antoine Bluchet <soyuka@gmail.com>
|
||||
*/
|
||||
class RedisTransport implements TransportInterface, SetupableTransportInterface
|
||||
{
|
||||
private $serializer;
|
||||
private $connection;
|
||||
private $receiver;
|
||||
private $sender;
|
||||
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 5.1, use "%s" instead. The RedisExt transport has been moved to package "symfony/redis-messenger" and will not be included by default in 6.0. Run "composer require symfony/redis-messenger".', RedisTransport::class, BridgeRedisTransport::class), E_USER_DEPRECATED);
|
||||
|
||||
public function __construct(Connection $connection, SerializerInterface $serializer = null)
|
||||
{
|
||||
$this->connection = $connection;
|
||||
$this->serializer = $serializer ?? new PhpSerializer();
|
||||
}
|
||||
class_exists(BridgeRedisTransport::class);
|
||||
|
||||
if (false) {
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @deprecated since Symfony 5.1, to be removed in 6.0. Use symfony/redis-messenger instead.
|
||||
*/
|
||||
public function get(): iterable
|
||||
class RedisTransport
|
||||
{
|
||||
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 send(Envelope $envelope): Envelope
|
||||
{
|
||||
return ($this->sender ?? $this->getSender())->send($envelope);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setup(): void
|
||||
{
|
||||
$this->connection->setup();
|
||||
}
|
||||
|
||||
private function getReceiver(): RedisReceiver
|
||||
{
|
||||
return $this->receiver = new RedisReceiver($this->connection, $this->serializer);
|
||||
}
|
||||
|
||||
private function getSender(): RedisSender
|
||||
{
|
||||
return $this->sender = new RedisSender($this->connection, $this->serializer);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,25 +11,17 @@
|
|||
|
||||
namespace Symfony\Component\Messenger\Transport\RedisExt;
|
||||
|
||||
use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface;
|
||||
use Symfony\Component\Messenger\Transport\TransportFactoryInterface;
|
||||
use Symfony\Component\Messenger\Transport\TransportInterface;
|
||||
use Symfony\Component\Messenger\Bridge\Redis\Transport\RedisTransportFactory as BridgeRedisTransportFactory;
|
||||
|
||||
/**
|
||||
* @author Alexander Schranz <alexander@suluio>
|
||||
* @author Antoine Bluchet <soyuka@gmail.com>
|
||||
*/
|
||||
class RedisTransportFactory implements TransportFactoryInterface
|
||||
{
|
||||
public function createTransport(string $dsn, array $options, SerializerInterface $serializer): TransportInterface
|
||||
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 5.1, use "%s" instead. The RedisExt transport has been moved to package "symfony/redis-messenger" and will not be included by default in 6.0. Run "composer require symfony/redis-messenger".', RedisTransportFactory::class, BridgeRedisTransportFactory::class), E_USER_DEPRECATED);
|
||||
|
||||
class_exists(BridgeRedisTransportFactory::class);
|
||||
|
||||
if (false) {
|
||||
/**
|
||||
* @deprecated since Symfony 5.1, to be removed in 6.0. Use symfony/redis-messenger instead.
|
||||
*/
|
||||
class RedisTransportFactory
|
||||
{
|
||||
unset($options['transport_name']);
|
||||
|
||||
return new RedisTransport(Connection::fromDsn($dsn, $options), $serializer);
|
||||
}
|
||||
|
||||
public function supports(string $dsn, array $options): bool
|
||||
{
|
||||
return 0 === strpos($dsn, 'redis://');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,7 +37,17 @@ class TransportFactory implements TransportFactoryInterface
|
|||
}
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException(sprintf('No transport supports the given Messenger DSN "%s".', $dsn));
|
||||
// Help the user to select Symfony packages based on protocol.
|
||||
$packageSuggestion = '';
|
||||
if (substr($dsn, 0, 7) === 'amqp://') {
|
||||
$packageSuggestion = ' Run "composer require symfony/amqp-messenger" to install AMQP transport.';
|
||||
} elseif (substr($dsn, 0, 11) === 'doctrine://') {
|
||||
$packageSuggestion = ' Run "composer require symfony/doctrine-messenger" to install Doctrine transport.';
|
||||
} elseif (substr($dsn, 0, 8) === 'redis://') {
|
||||
$packageSuggestion = ' Run "composer require symfony/redis-messenger" to install Redis transport.';
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException(sprintf('No transport supports the given Messenger DSN "%s".%s', $dsn, $packageSuggestion));
|
||||
}
|
||||
|
||||
public function supports(string $dsn, array $options): bool
|
||||
|
|
|
@ -17,12 +17,13 @@
|
|||
],
|
||||
"require": {
|
||||
"php": "^7.2.5",
|
||||
"psr/log": "~1.0"
|
||||
"psr/log": "~1.0",
|
||||
"symfony/amqp-messenger": "^5.1",
|
||||
"symfony/doctrine-messenger": "^5.1",
|
||||
"symfony/redis-messenger": "^5.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"doctrine/dbal": "^2.6",
|
||||
"psr/cache": "~1.0",
|
||||
"doctrine/persistence": "^1.3",
|
||||
"symfony/console": "^4.4|^5.0",
|
||||
"symfony/dependency-injection": "^4.4|^5.0",
|
||||
"symfony/event-dispatcher": "^4.4|^5.0",
|
||||
|
@ -35,7 +36,6 @@
|
|||
"symfony/validator": "^4.4|^5.0"
|
||||
},
|
||||
"conflict": {
|
||||
"doctrine/persistence": "<1.3",
|
||||
"symfony/event-dispatcher": "<4.4",
|
||||
"symfony/framework-bundle": "<4.4",
|
||||
"symfony/http-kernel": "<4.4"
|
||||
|
|
Reference in New Issue