diff --git a/UPGRADE-5.1.md b/UPGRADE-5.1.md index 6a0244b9b8..8dc9fc1a5b 100644 --- a/UPGRADE-5.1.md +++ b/UPGRADE-5.1.md @@ -19,6 +19,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 ------- diff --git a/UPGRADE-6.0.md b/UPGRADE-6.0.md index f23602c7bd..bc1998649b 100644 --- a/UPGRADE-6.0.md +++ b/UPGRADE-6.0.md @@ -19,6 +19,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 ------- diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 9ce51de76b..e653a32320 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -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'])) { diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger.xml index 14117ee8e4..aa5c50e5cd 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger.xml @@ -67,11 +67,11 @@ - + - + diff --git a/src/Symfony/Component/Messenger/Bridge/Amqp/.gitattributes b/src/Symfony/Component/Messenger/Bridge/Amqp/.gitattributes new file mode 100644 index 0000000000..ebb9287043 --- /dev/null +++ b/src/Symfony/Component/Messenger/Bridge/Amqp/.gitattributes @@ -0,0 +1,3 @@ +/Tests export-ignore +/phpunit.xml.dist export-ignore +/.gitignore export-ignore diff --git a/src/Symfony/Component/Messenger/Bridge/Amqp/.gitignore b/src/Symfony/Component/Messenger/Bridge/Amqp/.gitignore new file mode 100644 index 0000000000..c49a5d8df5 --- /dev/null +++ b/src/Symfony/Component/Messenger/Bridge/Amqp/.gitignore @@ -0,0 +1,3 @@ +vendor/ +composer.lock +phpunit.xml diff --git a/src/Symfony/Component/Messenger/Bridge/Amqp/CHANGELOG.md b/src/Symfony/Component/Messenger/Bridge/Amqp/CHANGELOG.md new file mode 100644 index 0000000000..26465c1a31 --- /dev/null +++ b/src/Symfony/Component/Messenger/Bridge/Amqp/CHANGELOG.md @@ -0,0 +1,7 @@ +CHANGELOG +========= + +5.1.0 +----- + + * Introduced the AMQP bridge. diff --git a/src/Symfony/Component/Messenger/Bridge/Amqp/LICENSE b/src/Symfony/Component/Messenger/Bridge/Amqp/LICENSE new file mode 100644 index 0000000000..69d925ba75 --- /dev/null +++ b/src/Symfony/Component/Messenger/Bridge/Amqp/LICENSE @@ -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. diff --git a/src/Symfony/Component/Messenger/Bridge/Amqp/README.md b/src/Symfony/Component/Messenger/Bridge/Amqp/README.md new file mode 100644 index 0000000000..521b99ca91 --- /dev/null +++ b/src/Symfony/Component/Messenger/Bridge/Amqp/README.md @@ -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) diff --git a/src/Symfony/Component/Messenger/Bridge/Amqp/Tests/Fixtures/DummyMessage.php b/src/Symfony/Component/Messenger/Bridge/Amqp/Tests/Fixtures/DummyMessage.php new file mode 100644 index 0000000000..4562b68419 --- /dev/null +++ b/src/Symfony/Component/Messenger/Bridge/Amqp/Tests/Fixtures/DummyMessage.php @@ -0,0 +1,18 @@ +message = $message; + } + + public function getMessage(): string + { + return $this->message; + } +} diff --git a/src/Symfony/Component/Messenger/Tests/Transport/AmqpExt/Fixtures/long_receiver.php b/src/Symfony/Component/Messenger/Bridge/Amqp/Tests/Fixtures/long_receiver.php similarity index 90% rename from src/Symfony/Component/Messenger/Tests/Transport/AmqpExt/Fixtures/long_receiver.php rename to src/Symfony/Component/Messenger/Bridge/Amqp/Tests/Fixtures/long_receiver.php index fc122b7390..0c7740666c 100644 --- a/src/Symfony/Component/Messenger/Tests/Transport/AmqpExt/Fixtures/long_receiver.php +++ b/src/Symfony/Component/Messenger/Bridge/Amqp/Tests/Fixtures/long_receiver.php @@ -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; diff --git a/src/Symfony/Component/Messenger/Tests/Transport/AmqpExt/AmqpExtIntegrationTest.php b/src/Symfony/Component/Messenger/Bridge/Amqp/Tests/Transport/AmqpExtIntegrationTest.php similarity index 91% rename from src/Symfony/Component/Messenger/Tests/Transport/AmqpExt/AmqpExtIntegrationTest.php rename to src/Symfony/Component/Messenger/Bridge/Amqp/Tests/Transport/AmqpExtIntegrationTest.php index 6d1c1598f2..a3a363edb3 100644 --- a/src/Symfony/Component/Messenger/Tests/Transport/AmqpExt/AmqpExtIntegrationTest.php +++ b/src/Symfony/Component/Messenger/Bridge/Amqp/Tests/Transport/AmqpExtIntegrationTest.php @@ -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" ] diff --git a/src/Symfony/Component/Messenger/Tests/Transport/AmqpExt/AmqpReceivedStampTest.php b/src/Symfony/Component/Messenger/Bridge/Amqp/Tests/Transport/AmqpReceivedStampTest.php similarity index 82% rename from src/Symfony/Component/Messenger/Tests/Transport/AmqpExt/AmqpReceivedStampTest.php rename to src/Symfony/Component/Messenger/Bridge/Amqp/Tests/Transport/AmqpReceivedStampTest.php index 79d53feee2..bbb3151bdf 100644 --- a/src/Symfony/Component/Messenger/Tests/Transport/AmqpExt/AmqpReceivedStampTest.php +++ b/src/Symfony/Component/Messenger/Bridge/Amqp/Tests/Transport/AmqpReceivedStampTest.php @@ -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 diff --git a/src/Symfony/Component/Messenger/Tests/Transport/AmqpExt/AmqpReceiverTest.php b/src/Symfony/Component/Messenger/Bridge/Amqp/Tests/Transport/AmqpReceiverTest.php similarity index 91% rename from src/Symfony/Component/Messenger/Tests/Transport/AmqpExt/AmqpReceiverTest.php rename to src/Symfony/Component/Messenger/Bridge/Amqp/Tests/Transport/AmqpReceiverTest.php index 8e9aebbce8..a674c60b47 100644 --- a/src/Symfony/Component/Messenger/Tests/Transport/AmqpExt/AmqpReceiverTest.php +++ b/src/Symfony/Component/Messenger/Bridge/Amqp/Tests/Transport/AmqpReceiverTest.php @@ -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; diff --git a/src/Symfony/Component/Messenger/Tests/Transport/AmqpExt/AmqpSenderTest.php b/src/Symfony/Component/Messenger/Bridge/Amqp/Tests/Transport/AmqpSenderTest.php similarity index 93% rename from src/Symfony/Component/Messenger/Tests/Transport/AmqpExt/AmqpSenderTest.php rename to src/Symfony/Component/Messenger/Bridge/Amqp/Tests/Transport/AmqpSenderTest.php index 002e92b249..ff83cd1c0c 100644 --- a/src/Symfony/Component/Messenger/Tests/Transport/AmqpExt/AmqpSenderTest.php +++ b/src/Symfony/Component/Messenger/Bridge/Amqp/Tests/Transport/AmqpSenderTest.php @@ -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; /** diff --git a/src/Symfony/Component/Messenger/Tests/Transport/AmqpExt/AmqpStampTest.php b/src/Symfony/Component/Messenger/Bridge/Amqp/Tests/Transport/AmqpStampTest.php similarity index 95% rename from src/Symfony/Component/Messenger/Tests/Transport/AmqpExt/AmqpStampTest.php rename to src/Symfony/Component/Messenger/Bridge/Amqp/Tests/Transport/AmqpStampTest.php index 043dfb2e3d..20427b7adb 100644 --- a/src/Symfony/Component/Messenger/Tests/Transport/AmqpExt/AmqpStampTest.php +++ b/src/Symfony/Component/Messenger/Bridge/Amqp/Tests/Transport/AmqpStampTest.php @@ -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 diff --git a/src/Symfony/Component/Messenger/Tests/Transport/AmqpExt/AmqpTransportFactoryTest.php b/src/Symfony/Component/Messenger/Bridge/Amqp/Tests/Transport/AmqpTransportFactoryTest.php similarity index 81% rename from src/Symfony/Component/Messenger/Tests/Transport/AmqpExt/AmqpTransportFactoryTest.php rename to src/Symfony/Component/Messenger/Bridge/Amqp/Tests/Transport/AmqpTransportFactoryTest.php index b3cb7a6dc8..b1f9364c23 100644 --- a/src/Symfony/Component/Messenger/Tests/Transport/AmqpExt/AmqpTransportFactoryTest.php +++ b/src/Symfony/Component/Messenger/Bridge/Amqp/Tests/Transport/AmqpTransportFactoryTest.php @@ -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 diff --git a/src/Symfony/Component/Messenger/Tests/Transport/AmqpExt/AmqpTransportTest.php b/src/Symfony/Component/Messenger/Bridge/Amqp/Tests/Transport/AmqpTransportTest.php similarity index 88% rename from src/Symfony/Component/Messenger/Tests/Transport/AmqpExt/AmqpTransportTest.php rename to src/Symfony/Component/Messenger/Bridge/Amqp/Tests/Transport/AmqpTransportTest.php index 6618d2fc76..c3fc41f976 100644 --- a/src/Symfony/Component/Messenger/Tests/Transport/AmqpExt/AmqpTransportTest.php +++ b/src/Symfony/Component/Messenger/Bridge/Amqp/Tests/Transport/AmqpTransportTest.php @@ -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; diff --git a/src/Symfony/Component/Messenger/Tests/Transport/AmqpExt/ConnectionTest.php b/src/Symfony/Component/Messenger/Bridge/Amqp/Tests/Transport/ConnectionTest.php similarity index 98% rename from src/Symfony/Component/Messenger/Tests/Transport/AmqpExt/ConnectionTest.php rename to src/Symfony/Component/Messenger/Bridge/Amqp/Tests/Transport/ConnectionTest.php index f4df694b60..619bbc28df 100644 --- a/src/Symfony/Component/Messenger/Tests/Transport/AmqpExt/ConnectionTest.php +++ b/src/Symfony/Component/Messenger/Bridge/Amqp/Tests/Transport/ConnectionTest.php @@ -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 diff --git a/src/Symfony/Component/Messenger/Bridge/Amqp/Transport/AmqpFactory.php b/src/Symfony/Component/Messenger/Bridge/Amqp/Transport/AmqpFactory.php new file mode 100644 index 0000000000..6ce1bdc4da --- /dev/null +++ b/src/Symfony/Component/Messenger/Bridge/Amqp/Transport/AmqpFactory.php @@ -0,0 +1,36 @@ + + * + * 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); diff --git a/src/Symfony/Component/Messenger/Bridge/Amqp/Transport/AmqpReceivedStamp.php b/src/Symfony/Component/Messenger/Bridge/Amqp/Transport/AmqpReceivedStamp.php new file mode 100644 index 0000000000..dd4656e809 --- /dev/null +++ b/src/Symfony/Component/Messenger/Bridge/Amqp/Transport/AmqpReceivedStamp.php @@ -0,0 +1,40 @@ + + * + * 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); diff --git a/src/Symfony/Component/Messenger/Bridge/Amqp/Transport/AmqpReceiver.php b/src/Symfony/Component/Messenger/Bridge/Amqp/Transport/AmqpReceiver.php new file mode 100644 index 0000000000..a5c89ef138 --- /dev/null +++ b/src/Symfony/Component/Messenger/Bridge/Amqp/Transport/AmqpReceiver.php @@ -0,0 +1,139 @@ + + * + * 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 + */ +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); diff --git a/src/Symfony/Component/Messenger/Bridge/Amqp/Transport/AmqpSender.php b/src/Symfony/Component/Messenger/Bridge/Amqp/Transport/AmqpSender.php new file mode 100644 index 0000000000..a77268bd05 --- /dev/null +++ b/src/Symfony/Component/Messenger/Bridge/Amqp/Transport/AmqpSender.php @@ -0,0 +1,78 @@ + + * + * 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 + */ +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); diff --git a/src/Symfony/Component/Messenger/Bridge/Amqp/Transport/AmqpStamp.php b/src/Symfony/Component/Messenger/Bridge/Amqp/Transport/AmqpStamp.php new file mode 100644 index 0000000000..9e3f63e0a2 --- /dev/null +++ b/src/Symfony/Component/Messenger/Bridge/Amqp/Transport/AmqpStamp.php @@ -0,0 +1,77 @@ + + * + * 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 + * @author Samuel Roze + */ +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); diff --git a/src/Symfony/Component/Messenger/Bridge/Amqp/Transport/AmqpTransport.php b/src/Symfony/Component/Messenger/Bridge/Amqp/Transport/AmqpTransport.php new file mode 100644 index 0000000000..030046851f --- /dev/null +++ b/src/Symfony/Component/Messenger/Bridge/Amqp/Transport/AmqpTransport.php @@ -0,0 +1,95 @@ + + * + * 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 + */ +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); diff --git a/src/Symfony/Component/Messenger/Bridge/Amqp/Transport/AmqpTransportFactory.php b/src/Symfony/Component/Messenger/Bridge/Amqp/Transport/AmqpTransportFactory.php new file mode 100644 index 0000000000..b9767214f1 --- /dev/null +++ b/src/Symfony/Component/Messenger/Bridge/Amqp/Transport/AmqpTransportFactory.php @@ -0,0 +1,35 @@ + + * + * 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 + */ +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); diff --git a/src/Symfony/Component/Messenger/Bridge/Amqp/Transport/Connection.php b/src/Symfony/Component/Messenger/Bridge/Amqp/Transport/Connection.php new file mode 100644 index 0000000000..a2709946b3 --- /dev/null +++ b/src/Symfony/Component/Messenger/Bridge/Amqp/Transport/Connection.php @@ -0,0 +1,474 @@ + + * + * 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 + * + * @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); diff --git a/src/Symfony/Component/Messenger/Bridge/Amqp/composer.json b/src/Symfony/Component/Messenger/Bridge/Amqp/composer.json new file mode 100644 index 0000000000..0522064e26 --- /dev/null +++ b/src/Symfony/Component/Messenger/Bridge/Amqp/composer.json @@ -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" + } + } +} diff --git a/src/Symfony/Component/Messenger/Bridge/Amqp/phpunit.xml.dist b/src/Symfony/Component/Messenger/Bridge/Amqp/phpunit.xml.dist new file mode 100644 index 0000000000..755a4676f7 --- /dev/null +++ b/src/Symfony/Component/Messenger/Bridge/Amqp/phpunit.xml.dist @@ -0,0 +1,30 @@ + + + + + + + + + + ./Tests/ + + + + + + ./ + + ./Tests + ./vendor + + + + diff --git a/src/Symfony/Component/Messenger/Bridge/Doctrine/.gitattributes b/src/Symfony/Component/Messenger/Bridge/Doctrine/.gitattributes new file mode 100644 index 0000000000..ebb9287043 --- /dev/null +++ b/src/Symfony/Component/Messenger/Bridge/Doctrine/.gitattributes @@ -0,0 +1,3 @@ +/Tests export-ignore +/phpunit.xml.dist export-ignore +/.gitignore export-ignore diff --git a/src/Symfony/Component/Messenger/Bridge/Doctrine/.gitignore b/src/Symfony/Component/Messenger/Bridge/Doctrine/.gitignore new file mode 100644 index 0000000000..c49a5d8df5 --- /dev/null +++ b/src/Symfony/Component/Messenger/Bridge/Doctrine/.gitignore @@ -0,0 +1,3 @@ +vendor/ +composer.lock +phpunit.xml diff --git a/src/Symfony/Component/Messenger/Bridge/Doctrine/CHANGELOG.md b/src/Symfony/Component/Messenger/Bridge/Doctrine/CHANGELOG.md new file mode 100644 index 0000000000..db21756b0c --- /dev/null +++ b/src/Symfony/Component/Messenger/Bridge/Doctrine/CHANGELOG.md @@ -0,0 +1,7 @@ +CHANGELOG +========= + +5.1.0 +----- + + * Introduced the Doctrine bridge. diff --git a/src/Symfony/Component/Messenger/Bridge/Doctrine/LICENSE b/src/Symfony/Component/Messenger/Bridge/Doctrine/LICENSE new file mode 100644 index 0000000000..69d925ba75 --- /dev/null +++ b/src/Symfony/Component/Messenger/Bridge/Doctrine/LICENSE @@ -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. diff --git a/src/Symfony/Component/Messenger/Bridge/Doctrine/README.md b/src/Symfony/Component/Messenger/Bridge/Doctrine/README.md new file mode 100644 index 0000000000..068490d574 --- /dev/null +++ b/src/Symfony/Component/Messenger/Bridge/Doctrine/README.md @@ -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) diff --git a/src/Symfony/Component/Messenger/Bridge/Doctrine/Tests/Fixtures/DummyMessage.php b/src/Symfony/Component/Messenger/Bridge/Doctrine/Tests/Fixtures/DummyMessage.php new file mode 100644 index 0000000000..4ee9f6ef95 --- /dev/null +++ b/src/Symfony/Component/Messenger/Bridge/Doctrine/Tests/Fixtures/DummyMessage.php @@ -0,0 +1,18 @@ +message = $message; + } + + public function getMessage(): string + { + return $this->message; + } +} diff --git a/src/Symfony/Component/Messenger/Tests/Transport/Doctrine/ConnectionTest.php b/src/Symfony/Component/Messenger/Bridge/Doctrine/Tests/Transport/ConnectionTest.php similarity index 98% rename from src/Symfony/Component/Messenger/Tests/Transport/Doctrine/ConnectionTest.php rename to src/Symfony/Component/Messenger/Bridge/Doctrine/Tests/Transport/ConnectionTest.php index bd7fff769b..d685df5100 100644 --- a/src/Symfony/Component/Messenger/Tests/Transport/Doctrine/ConnectionTest.php +++ b/src/Symfony/Component/Messenger/Bridge/Doctrine/Tests/Transport/ConnectionTest.php @@ -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 { diff --git a/src/Symfony/Component/Messenger/Tests/Transport/Doctrine/DoctrineIntegrationTest.php b/src/Symfony/Component/Messenger/Bridge/Doctrine/Tests/Transport/DoctrineIntegrationTest.php similarity index 97% rename from src/Symfony/Component/Messenger/Tests/Transport/Doctrine/DoctrineIntegrationTest.php rename to src/Symfony/Component/Messenger/Bridge/Doctrine/Tests/Transport/DoctrineIntegrationTest.php index a01e68db39..64398d00e4 100644 --- a/src/Symfony/Component/Messenger/Tests/Transport/Doctrine/DoctrineIntegrationTest.php +++ b/src/Symfony/Component/Messenger/Bridge/Doctrine/Tests/Transport/DoctrineIntegrationTest.php @@ -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 diff --git a/src/Symfony/Component/Messenger/Tests/Transport/Doctrine/DoctrineReceiverTest.php b/src/Symfony/Component/Messenger/Bridge/Doctrine/Tests/Transport/DoctrineReceiverTest.php similarity index 93% rename from src/Symfony/Component/Messenger/Tests/Transport/Doctrine/DoctrineReceiverTest.php rename to src/Symfony/Component/Messenger/Bridge/Doctrine/Tests/Transport/DoctrineReceiverTest.php index 45e4dd3b91..cc2c969a28 100644 --- a/src/Symfony/Component/Messenger/Tests/Transport/Doctrine/DoctrineReceiverTest.php +++ b/src/Symfony/Component/Messenger/Bridge/Doctrine/Tests/Transport/DoctrineReceiverTest.php @@ -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; diff --git a/src/Symfony/Component/Messenger/Tests/Transport/Doctrine/DoctrineSenderTest.php b/src/Symfony/Component/Messenger/Bridge/Doctrine/Tests/Transport/DoctrineSenderTest.php similarity index 88% rename from src/Symfony/Component/Messenger/Tests/Transport/Doctrine/DoctrineSenderTest.php rename to src/Symfony/Component/Messenger/Bridge/Doctrine/Tests/Transport/DoctrineSenderTest.php index cb2d194ae1..c2953a524d 100644 --- a/src/Symfony/Component/Messenger/Tests/Transport/Doctrine/DoctrineSenderTest.php +++ b/src/Symfony/Component/Messenger/Bridge/Doctrine/Tests/Transport/DoctrineSenderTest.php @@ -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 diff --git a/src/Symfony/Component/Messenger/Tests/Transport/Doctrine/DoctrineTransportFactoryTest.php b/src/Symfony/Component/Messenger/Bridge/Doctrine/Tests/Transport/DoctrineTransportFactoryTest.php similarity index 89% rename from src/Symfony/Component/Messenger/Tests/Transport/Doctrine/DoctrineTransportFactoryTest.php rename to src/Symfony/Component/Messenger/Bridge/Doctrine/Tests/Transport/DoctrineTransportFactoryTest.php index e124bf94e8..a5ed0d4a2a 100644 --- a/src/Symfony/Component/Messenger/Tests/Transport/Doctrine/DoctrineTransportFactoryTest.php +++ b/src/Symfony/Component/Messenger/Bridge/Doctrine/Tests/Transport/DoctrineTransportFactoryTest.php @@ -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 diff --git a/src/Symfony/Component/Messenger/Tests/Transport/Doctrine/DoctrineTransportTest.php b/src/Symfony/Component/Messenger/Bridge/Doctrine/Tests/Transport/DoctrineTransportTest.php similarity index 85% rename from src/Symfony/Component/Messenger/Tests/Transport/Doctrine/DoctrineTransportTest.php rename to src/Symfony/Component/Messenger/Bridge/Doctrine/Tests/Transport/DoctrineTransportTest.php index 96b5078b3a..f04764dded 100644 --- a/src/Symfony/Component/Messenger/Tests/Transport/Doctrine/DoctrineTransportTest.php +++ b/src/Symfony/Component/Messenger/Bridge/Doctrine/Tests/Transport/DoctrineTransportTest.php @@ -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; diff --git a/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/Connection.php b/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/Connection.php new file mode 100644 index 0000000000..fa5abd6614 --- /dev/null +++ b/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/Connection.php @@ -0,0 +1,347 @@ + + * + * 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 + * + * @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); diff --git a/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/DoctrineReceivedStamp.php b/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/DoctrineReceivedStamp.php new file mode 100644 index 0000000000..6ec3389ab6 --- /dev/null +++ b/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/DoctrineReceivedStamp.php @@ -0,0 +1,33 @@ + + * + * 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 + */ +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); diff --git a/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/DoctrineReceiver.php b/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/DoctrineReceiver.php new file mode 100644 index 0000000000..872e0c9278 --- /dev/null +++ b/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/DoctrineReceiver.php @@ -0,0 +1,173 @@ + + * + * 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 + */ +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); diff --git a/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/DoctrineSender.php b/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/DoctrineSender.php new file mode 100644 index 0000000000..db46afd2b3 --- /dev/null +++ b/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/DoctrineSender.php @@ -0,0 +1,57 @@ + + * + * 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 + */ +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); diff --git a/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/DoctrineTransport.php b/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/DoctrineTransport.php new file mode 100644 index 0000000000..e9695e03a1 --- /dev/null +++ b/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/DoctrineTransport.php @@ -0,0 +1,111 @@ + + * + * 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 + */ +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); diff --git a/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/DoctrineTransportFactory.php b/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/DoctrineTransportFactory.php new file mode 100644 index 0000000000..3cd9089110 --- /dev/null +++ b/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/DoctrineTransportFactory.php @@ -0,0 +1,58 @@ + + * + * 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 + */ +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); diff --git a/src/Symfony/Component/Messenger/Bridge/Doctrine/composer.json b/src/Symfony/Component/Messenger/Bridge/Doctrine/composer.json new file mode 100644 index 0000000000..9b4c738268 --- /dev/null +++ b/src/Symfony/Component/Messenger/Bridge/Doctrine/composer.json @@ -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" + } + } +} diff --git a/src/Symfony/Component/Messenger/Bridge/Doctrine/phpunit.xml.dist b/src/Symfony/Component/Messenger/Bridge/Doctrine/phpunit.xml.dist new file mode 100644 index 0000000000..ed2aa6014f --- /dev/null +++ b/src/Symfony/Component/Messenger/Bridge/Doctrine/phpunit.xml.dist @@ -0,0 +1,30 @@ + + + + + + + + + + ./Tests/ + + + + + + ./ + + ./Tests + ./vendor + + + + diff --git a/src/Symfony/Component/Messenger/Bridge/Redis/.gitattributes b/src/Symfony/Component/Messenger/Bridge/Redis/.gitattributes new file mode 100644 index 0000000000..ebb9287043 --- /dev/null +++ b/src/Symfony/Component/Messenger/Bridge/Redis/.gitattributes @@ -0,0 +1,3 @@ +/Tests export-ignore +/phpunit.xml.dist export-ignore +/.gitignore export-ignore diff --git a/src/Symfony/Component/Messenger/Bridge/Redis/.gitignore b/src/Symfony/Component/Messenger/Bridge/Redis/.gitignore new file mode 100644 index 0000000000..c49a5d8df5 --- /dev/null +++ b/src/Symfony/Component/Messenger/Bridge/Redis/.gitignore @@ -0,0 +1,3 @@ +vendor/ +composer.lock +phpunit.xml diff --git a/src/Symfony/Component/Messenger/Bridge/Redis/CHANGELOG.md b/src/Symfony/Component/Messenger/Bridge/Redis/CHANGELOG.md new file mode 100644 index 0000000000..4ebe764927 --- /dev/null +++ b/src/Symfony/Component/Messenger/Bridge/Redis/CHANGELOG.md @@ -0,0 +1,7 @@ +CHANGELOG +========= + +5.1.0 +----- + + * Introduced the Redis bridge. diff --git a/src/Symfony/Component/Messenger/Bridge/Redis/LICENSE b/src/Symfony/Component/Messenger/Bridge/Redis/LICENSE new file mode 100644 index 0000000000..69d925ba75 --- /dev/null +++ b/src/Symfony/Component/Messenger/Bridge/Redis/LICENSE @@ -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. diff --git a/src/Symfony/Component/Messenger/Bridge/Redis/README.md b/src/Symfony/Component/Messenger/Bridge/Redis/README.md new file mode 100644 index 0000000000..b363ce6198 --- /dev/null +++ b/src/Symfony/Component/Messenger/Bridge/Redis/README.md @@ -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) diff --git a/src/Symfony/Component/Messenger/Bridge/Redis/Tests/Fixtures/DummyMessage.php b/src/Symfony/Component/Messenger/Bridge/Redis/Tests/Fixtures/DummyMessage.php new file mode 100644 index 0000000000..92f8a89c01 --- /dev/null +++ b/src/Symfony/Component/Messenger/Bridge/Redis/Tests/Fixtures/DummyMessage.php @@ -0,0 +1,18 @@ +message = $message; + } + + public function getMessage(): string + { + return $this->message; + } +} diff --git a/src/Symfony/Component/Messenger/Tests/Transport/RedisExt/ConnectionTest.php b/src/Symfony/Component/Messenger/Bridge/Redis/Tests/Transport/ConnectionTest.php similarity index 98% rename from src/Symfony/Component/Messenger/Tests/Transport/RedisExt/ConnectionTest.php rename to src/Symfony/Component/Messenger/Bridge/Redis/Tests/Transport/ConnectionTest.php index 837abaec01..fd7ab71df8 100644 --- a/src/Symfony/Component/Messenger/Tests/Transport/RedisExt/ConnectionTest.php +++ b/src/Symfony/Component/Messenger/Bridge/Redis/Tests/Transport/ConnectionTest.php @@ -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 diff --git a/src/Symfony/Component/Messenger/Tests/Transport/RedisExt/RedisExtIntegrationTest.php b/src/Symfony/Component/Messenger/Bridge/Redis/Tests/Transport/RedisExtIntegrationTest.php similarity index 94% rename from src/Symfony/Component/Messenger/Tests/Transport/RedisExt/RedisExtIntegrationTest.php rename to src/Symfony/Component/Messenger/Bridge/Redis/Tests/Transport/RedisExtIntegrationTest.php index e2375511d6..cb65eddf03 100644 --- a/src/Symfony/Component/Messenger/Tests/Transport/RedisExt/RedisExtIntegrationTest.php +++ b/src/Symfony/Component/Messenger/Bridge/Redis/Tests/Transport/RedisExtIntegrationTest.php @@ -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 diff --git a/src/Symfony/Component/Messenger/Tests/Transport/RedisExt/RedisReceiverTest.php b/src/Symfony/Component/Messenger/Bridge/Redis/Tests/Transport/RedisReceiverTest.php similarity index 89% rename from src/Symfony/Component/Messenger/Tests/Transport/RedisExt/RedisReceiverTest.php rename to src/Symfony/Component/Messenger/Bridge/Redis/Tests/Transport/RedisReceiverTest.php index 0da0e78ff8..ec12e37d5f 100644 --- a/src/Symfony/Component/Messenger/Tests/Transport/RedisExt/RedisReceiverTest.php +++ b/src/Symfony/Component/Messenger/Bridge/Redis/Tests/Transport/RedisReceiverTest.php @@ -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; diff --git a/src/Symfony/Component/Messenger/Tests/Transport/RedisExt/RedisSenderTest.php b/src/Symfony/Component/Messenger/Bridge/Redis/Tests/Transport/RedisSenderTest.php similarity index 80% rename from src/Symfony/Component/Messenger/Tests/Transport/RedisExt/RedisSenderTest.php rename to src/Symfony/Component/Messenger/Bridge/Redis/Tests/Transport/RedisSenderTest.php index 5cbda34e10..26231a1c3e 100644 --- a/src/Symfony/Component/Messenger/Tests/Transport/RedisExt/RedisSenderTest.php +++ b/src/Symfony/Component/Messenger/Bridge/Redis/Tests/Transport/RedisSenderTest.php @@ -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 diff --git a/src/Symfony/Component/Messenger/Tests/Transport/RedisExt/RedisTransportFactoryTest.php b/src/Symfony/Component/Messenger/Bridge/Redis/Tests/Transport/RedisTransportFactoryTest.php similarity index 80% rename from src/Symfony/Component/Messenger/Tests/Transport/RedisExt/RedisTransportFactoryTest.php rename to src/Symfony/Component/Messenger/Bridge/Redis/Tests/Transport/RedisTransportFactoryTest.php index 58b71536cf..07248e05ab 100644 --- a/src/Symfony/Component/Messenger/Tests/Transport/RedisExt/RedisTransportFactoryTest.php +++ b/src/Symfony/Component/Messenger/Bridge/Redis/Tests/Transport/RedisTransportFactoryTest.php @@ -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; /** diff --git a/src/Symfony/Component/Messenger/Tests/Transport/RedisExt/RedisTransportTest.php b/src/Symfony/Component/Messenger/Bridge/Redis/Tests/Transport/RedisTransportTest.php similarity index 87% rename from src/Symfony/Component/Messenger/Tests/Transport/RedisExt/RedisTransportTest.php rename to src/Symfony/Component/Messenger/Bridge/Redis/Tests/Transport/RedisTransportTest.php index 8ca97243ae..16e022f68c 100644 --- a/src/Symfony/Component/Messenger/Tests/Transport/RedisExt/RedisTransportTest.php +++ b/src/Symfony/Component/Messenger/Bridge/Redis/Tests/Transport/RedisTransportTest.php @@ -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; diff --git a/src/Symfony/Component/Messenger/Bridge/Redis/Transport/Connection.php b/src/Symfony/Component/Messenger/Bridge/Redis/Transport/Connection.php new file mode 100644 index 0000000000..d73dc5259a --- /dev/null +++ b/src/Symfony/Component/Messenger/Bridge/Redis/Transport/Connection.php @@ -0,0 +1,329 @@ + + * + * 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 + * @author Antoine Bluchet + * @author Robin Chalas + * + * @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); diff --git a/src/Symfony/Component/Messenger/Bridge/Redis/Transport/RedisReceivedStamp.php b/src/Symfony/Component/Messenger/Bridge/Redis/Transport/RedisReceivedStamp.php new file mode 100644 index 0000000000..486aa58dfd --- /dev/null +++ b/src/Symfony/Component/Messenger/Bridge/Redis/Transport/RedisReceivedStamp.php @@ -0,0 +1,33 @@ + + * + * 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 + */ +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); diff --git a/src/Symfony/Component/Messenger/Bridge/Redis/Transport/RedisReceiver.php b/src/Symfony/Component/Messenger/Bridge/Redis/Transport/RedisReceiver.php new file mode 100644 index 0000000000..0c51d15163 --- /dev/null +++ b/src/Symfony/Component/Messenger/Bridge/Redis/Transport/RedisReceiver.php @@ -0,0 +1,89 @@ + + * + * 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 + * @author Antoine Bluchet + */ +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); diff --git a/src/Symfony/Component/Messenger/Bridge/Redis/Transport/RedisSender.php b/src/Symfony/Component/Messenger/Bridge/Redis/Transport/RedisSender.php new file mode 100644 index 0000000000..38b2e47515 --- /dev/null +++ b/src/Symfony/Component/Messenger/Bridge/Redis/Transport/RedisSender.php @@ -0,0 +1,50 @@ + + * + * 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 + * @author Antoine Bluchet + */ +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); diff --git a/src/Symfony/Component/Messenger/Bridge/Redis/Transport/RedisTransport.php b/src/Symfony/Component/Messenger/Bridge/Redis/Transport/RedisTransport.php new file mode 100644 index 0000000000..d92afdec66 --- /dev/null +++ b/src/Symfony/Component/Messenger/Bridge/Redis/Transport/RedisTransport.php @@ -0,0 +1,87 @@ + + * + * 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 + * @author Antoine Bluchet + */ +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); diff --git a/src/Symfony/Component/Messenger/Bridge/Redis/Transport/RedisTransportFactory.php b/src/Symfony/Component/Messenger/Bridge/Redis/Transport/RedisTransportFactory.php new file mode 100644 index 0000000000..b8a340e84f --- /dev/null +++ b/src/Symfony/Component/Messenger/Bridge/Redis/Transport/RedisTransportFactory.php @@ -0,0 +1,36 @@ + + * + * 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 + * @author Antoine Bluchet + */ +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); diff --git a/src/Symfony/Component/Messenger/Bridge/Redis/composer.json b/src/Symfony/Component/Messenger/Bridge/Redis/composer.json new file mode 100644 index 0000000000..cb456ec44a --- /dev/null +++ b/src/Symfony/Component/Messenger/Bridge/Redis/composer.json @@ -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" + } + } +} diff --git a/src/Symfony/Component/Messenger/Bridge/Redis/phpunit.xml.dist b/src/Symfony/Component/Messenger/Bridge/Redis/phpunit.xml.dist new file mode 100644 index 0000000000..4a59a18553 --- /dev/null +++ b/src/Symfony/Component/Messenger/Bridge/Redis/phpunit.xml.dist @@ -0,0 +1,30 @@ + + + + + + + + + + ./Tests/ + + + + + + ./ + + ./Tests + ./vendor + + + + diff --git a/src/Symfony/Component/Messenger/CHANGELOG.md b/src/Symfony/Component/Messenger/CHANGELOG.md index a62b72a5bc..aab9a173af 100644 --- a/src/Symfony/Component/Messenger/CHANGELOG.md +++ b/src/Symfony/Component/Messenger/CHANGELOG.md @@ -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 ----- diff --git a/src/Symfony/Component/Messenger/Middleware/RejectRedeliveredMessageMiddleware.php b/src/Symfony/Component/Messenger/Middleware/RejectRedeliveredMessageMiddleware.php index d036790ddb..33e5d427fa 100644 --- a/src/Symfony/Component/Messenger/Middleware/RejectRedeliveredMessageMiddleware.php +++ b/src/Symfony/Component/Messenger/Middleware/RejectRedeliveredMessageMiddleware.php @@ -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); } } diff --git a/src/Symfony/Component/Messenger/Tests/DependencyInjection/MessengerPassTest.php b/src/Symfony/Component/Messenger/Tests/DependencyInjection/MessengerPassTest.php index e87cbf18b5..c127163994 100644 --- a/src/Symfony/Component/Messenger/Tests/DependencyInjection/MessengerPassTest.php +++ b/src/Symfony/Component/Messenger/Tests/DependencyInjection/MessengerPassTest.php @@ -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 diff --git a/src/Symfony/Component/Messenger/Transport/AmqpExt/AmqpFactory.php b/src/Symfony/Component/Messenger/Transport/AmqpExt/AmqpFactory.php index 5cbdbdd086..e201d2e80f 100644 --- a/src/Symfony/Component/Messenger/Transport/AmqpExt/AmqpFactory.php +++ b/src/Symfony/Component/Messenger/Transport/AmqpExt/AmqpFactory.php @@ -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); } } diff --git a/src/Symfony/Component/Messenger/Transport/AmqpExt/AmqpReceivedStamp.php b/src/Symfony/Component/Messenger/Transport/AmqpExt/AmqpReceivedStamp.php index e02ecbf3e8..cea910a2c6 100644 --- a/src/Symfony/Component/Messenger/Transport/AmqpExt/AmqpReceivedStamp.php +++ b/src/Symfony/Component/Messenger/Transport/AmqpExt/AmqpReceivedStamp.php @@ -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; } } diff --git a/src/Symfony/Component/Messenger/Transport/AmqpExt/AmqpReceiver.php b/src/Symfony/Component/Messenger/Transport/AmqpExt/AmqpReceiver.php index 068b0cba83..bd11af6cba 100644 --- a/src/Symfony/Component/Messenger/Transport/AmqpExt/AmqpReceiver.php +++ b/src/Symfony/Component/Messenger/Transport/AmqpExt/AmqpReceiver.php @@ -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 - */ -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; } } diff --git a/src/Symfony/Component/Messenger/Transport/AmqpExt/AmqpSender.php b/src/Symfony/Component/Messenger/Transport/AmqpExt/AmqpSender.php index ae99759c49..c247a67f91 100644 --- a/src/Symfony/Component/Messenger/Transport/AmqpExt/AmqpSender.php +++ b/src/Symfony/Component/Messenger/Transport/AmqpExt/AmqpSender.php @@ -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 - */ -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; } } + diff --git a/src/Symfony/Component/Messenger/Transport/AmqpExt/AmqpStamp.php b/src/Symfony/Component/Messenger/Transport/AmqpExt/AmqpStamp.php index 0a4777ccff..242b6f6482 100644 --- a/src/Symfony/Component/Messenger/Transport/AmqpExt/AmqpStamp.php +++ b/src/Symfony/Component/Messenger/Transport/AmqpExt/AmqpStamp.php @@ -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 - * @author Samuel Roze - */ -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) - ); } } diff --git a/src/Symfony/Component/Messenger/Transport/AmqpExt/AmqpTransport.php b/src/Symfony/Component/Messenger/Transport/AmqpExt/AmqpTransport.php index bf536de8a1..653aac1c0e 100644 --- a/src/Symfony/Component/Messenger/Transport/AmqpExt/AmqpTransport.php +++ b/src/Symfony/Component/Messenger/Transport/AmqpExt/AmqpTransport.php @@ -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 - */ -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); } } diff --git a/src/Symfony/Component/Messenger/Transport/AmqpExt/AmqpTransportFactory.php b/src/Symfony/Component/Messenger/Transport/AmqpExt/AmqpTransportFactory.php index 0a366d9a84..3d3dacc54e 100644 --- a/src/Symfony/Component/Messenger/Transport/AmqpExt/AmqpTransportFactory.php +++ b/src/Symfony/Component/Messenger/Transport/AmqpExt/AmqpTransportFactory.php @@ -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 - */ -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://'); } } diff --git a/src/Symfony/Component/Messenger/Transport/AmqpExt/Connection.php b/src/Symfony/Component/Messenger/Transport/AmqpExt/Connection.php index 2540b9d770..acb5f25168 100644 --- a/src/Symfony/Component/Messenger/Transport/AmqpExt/Connection.php +++ b/src/Symfony/Component/Messenger/Transport/AmqpExt/Connection.php @@ -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 - * - * @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(); } } diff --git a/src/Symfony/Component/Messenger/Transport/Doctrine/Connection.php b/src/Symfony/Component/Messenger/Transport/Doctrine/Connection.php index d5d4d74031..30c33bac55 100644 --- a/src/Symfony/Component/Messenger/Transport/Doctrine/Connection.php +++ b/src/Symfony/Component/Messenger/Transport/Doctrine/Connection.php @@ -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 - * - * @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; } } diff --git a/src/Symfony/Component/Messenger/Transport/Doctrine/DoctrineReceivedStamp.php b/src/Symfony/Component/Messenger/Transport/Doctrine/DoctrineReceivedStamp.php index 96cd3eb3f9..f754115b50 100644 --- a/src/Symfony/Component/Messenger/Transport/Doctrine/DoctrineReceivedStamp.php +++ b/src/Symfony/Component/Messenger/Transport/Doctrine/DoctrineReceivedStamp.php @@ -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 - */ -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; } } diff --git a/src/Symfony/Component/Messenger/Transport/Doctrine/DoctrineReceiver.php b/src/Symfony/Component/Messenger/Transport/Doctrine/DoctrineReceiver.php index 071cf2812a..2d36044841 100644 --- a/src/Symfony/Component/Messenger/Transport/Doctrine/DoctrineReceiver.php +++ b/src/Symfony/Component/Messenger/Transport/Doctrine/DoctrineReceiver.php @@ -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 - */ -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']) - ); } } diff --git a/src/Symfony/Component/Messenger/Transport/Doctrine/DoctrineSender.php b/src/Symfony/Component/Messenger/Transport/Doctrine/DoctrineSender.php index ecfb5113e0..b0a645d855 100644 --- a/src/Symfony/Component/Messenger/Transport/Doctrine/DoctrineSender.php +++ b/src/Symfony/Component/Messenger/Transport/Doctrine/DoctrineSender.php @@ -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 - */ -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)); } } diff --git a/src/Symfony/Component/Messenger/Transport/Doctrine/DoctrineTransport.php b/src/Symfony/Component/Messenger/Transport/Doctrine/DoctrineTransport.php index 6ed54e590f..416edb0e81 100644 --- a/src/Symfony/Component/Messenger/Transport/Doctrine/DoctrineTransport.php +++ b/src/Symfony/Component/Messenger/Transport/Doctrine/DoctrineTransport.php @@ -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 - */ -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); } } diff --git a/src/Symfony/Component/Messenger/Transport/Doctrine/DoctrineTransportFactory.php b/src/Symfony/Component/Messenger/Transport/Doctrine/DoctrineTransportFactory.php index b4455e04f1..29df160f2a 100644 --- a/src/Symfony/Component/Messenger/Transport/Doctrine/DoctrineTransportFactory.php +++ b/src/Symfony/Component/Messenger/Transport/Doctrine/DoctrineTransportFactory.php @@ -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 - */ -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://'); } } diff --git a/src/Symfony/Component/Messenger/Transport/RedisExt/Connection.php b/src/Symfony/Component/Messenger/Transport/RedisExt/Connection.php index f4cc3a158e..070ac7e5c9 100644 --- a/src/Symfony/Component/Messenger/Transport/RedisExt/Connection.php +++ b/src/Symfony/Component/Messenger/Transport/RedisExt/Connection.php @@ -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 - * @author Antoine Bluchet - * @author Robin Chalas - * - * @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); } } diff --git a/src/Symfony/Component/Messenger/Transport/RedisExt/RedisReceivedStamp.php b/src/Symfony/Component/Messenger/Transport/RedisExt/RedisReceivedStamp.php index 1f7803394c..3a81152bcc 100644 --- a/src/Symfony/Component/Messenger/Transport/RedisExt/RedisReceivedStamp.php +++ b/src/Symfony/Component/Messenger/Transport/RedisExt/RedisReceivedStamp.php @@ -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 - */ -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; } } diff --git a/src/Symfony/Component/Messenger/Transport/RedisExt/RedisReceiver.php b/src/Symfony/Component/Messenger/Transport/RedisExt/RedisReceiver.php index 5425812de7..bfc1eaf560 100644 --- a/src/Symfony/Component/Messenger/Transport/RedisExt/RedisReceiver.php +++ b/src/Symfony/Component/Messenger/Transport/RedisExt/RedisReceiver.php @@ -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 - * @author Antoine Bluchet - */ -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; } } diff --git a/src/Symfony/Component/Messenger/Transport/RedisExt/RedisSender.php b/src/Symfony/Component/Messenger/Transport/RedisExt/RedisSender.php index beda996870..e5954c9a21 100644 --- a/src/Symfony/Component/Messenger/Transport/RedisExt/RedisSender.php +++ b/src/Symfony/Component/Messenger/Transport/RedisExt/RedisSender.php @@ -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 - * @author Antoine Bluchet - */ -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; } } diff --git a/src/Symfony/Component/Messenger/Transport/RedisExt/RedisTransport.php b/src/Symfony/Component/Messenger/Transport/RedisExt/RedisTransport.php index 61e14822f2..911e905947 100644 --- a/src/Symfony/Component/Messenger/Transport/RedisExt/RedisTransport.php +++ b/src/Symfony/Component/Messenger/Transport/RedisExt/RedisTransport.php @@ -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 - * @author Antoine Bluchet - */ -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); } } diff --git a/src/Symfony/Component/Messenger/Transport/RedisExt/RedisTransportFactory.php b/src/Symfony/Component/Messenger/Transport/RedisExt/RedisTransportFactory.php index 60ea10dca7..a2897c1561 100644 --- a/src/Symfony/Component/Messenger/Transport/RedisExt/RedisTransportFactory.php +++ b/src/Symfony/Component/Messenger/Transport/RedisExt/RedisTransportFactory.php @@ -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 - * @author Antoine Bluchet - */ -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://'); } } diff --git a/src/Symfony/Component/Messenger/Transport/TransportFactory.php b/src/Symfony/Component/Messenger/Transport/TransportFactory.php index 4565efe41b..ae62f7ab9b 100644 --- a/src/Symfony/Component/Messenger/Transport/TransportFactory.php +++ b/src/Symfony/Component/Messenger/Transport/TransportFactory.php @@ -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 diff --git a/src/Symfony/Component/Messenger/composer.json b/src/Symfony/Component/Messenger/composer.json index 510ac0f68c..054836940b 100644 --- a/src/Symfony/Component/Messenger/composer.json +++ b/src/Symfony/Component/Messenger/composer.json @@ -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"