Skip to content

Commit

Permalink
feature #28399 [Messenger] Add a SenderLocator decoupled from Contain…
Browse files Browse the repository at this point in the history
…erInterface (fabpot)

This PR was merged into the 4.2-dev branch.

Discussion
----------

[Messenger] Add a SenderLocator decoupled from ContainerInterface

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | yes
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | n/a
| License       | MIT
| Doc PR        | upcoming

For handler locators, we have a generic `HandlerLocator` class that takes a simple mapping instead of a service locator. The same did not exist for sender locators. So, this PR adds this possibility as well. That allows for something like this:

```php
new MessageBus([
    new SendMessageMiddleware(new SenderLocator([
        Message::class => new AmqpTransport($encoderDecoder, $encoderDecoder, $connection),
    ])),
    new HandleMessageMiddleware(new HandlerLocator([
        Message::class => new MessageHandler(),
    ])),
]);
```

Commits
-------

e658e15 [Messenger] added a SenderLocator decoupled from ContainerInterface
  • Loading branch information
fabpot committed Sep 8, 2018
2 parents 7bbe78b + e658e15 commit e980ce4
Show file tree
Hide file tree
Showing 9 changed files with 215 additions and 93 deletions.
Expand Up @@ -8,7 +8,7 @@
<defaults public="false" />

<!-- Asynchronous -->
<service id="messenger.asynchronous.routing.sender_locator" class="Symfony\Component\Messenger\Asynchronous\Routing\SenderLocator">
<service id="messenger.asynchronous.routing.sender_locator" class="Symfony\Component\Messenger\Asynchronous\Routing\ContainerSenderLocator">
<argument type="service" id="messenger.sender_locator" />
<argument type="collection" /> <!-- Message to sender ID mapping -->
</service>
Expand Down
Expand Up @@ -11,7 +11,7 @@

namespace Symfony\Component\Messenger\Asynchronous\Middleware;

use Symfony\Component\Messenger\Asynchronous\Routing\SenderLocator;
use Symfony\Component\Messenger\Asynchronous\Routing\AbstractSenderLocator;
use Symfony\Component\Messenger\Asynchronous\Routing\SenderLocatorInterface;
use Symfony\Component\Messenger\Asynchronous\Transport\ReceivedMessage;
use Symfony\Component\Messenger\Envelope;
Expand Down Expand Up @@ -60,6 +60,6 @@ public function handle($envelope, callable $next)

private function mustSendAndHandle($message): bool
{
return (bool) SenderLocator::getValueFromMessageRouting($this->messagesToSendAndHandleMapping, $message);
return (bool) AbstractSenderLocator::getValueFromMessageRouting($this->messagesToSendAndHandleMapping, $message);
}
}
@@ -0,0 +1,38 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Messenger\Asynchronous\Routing;

/**
* @author Samuel Roze <samuel.roze@gmail.com>
*
* @internal
*/
abstract class AbstractSenderLocator implements SenderLocatorInterface
{
public static function getValueFromMessageRouting(array $mapping, $message)
{
if (isset($mapping[\get_class($message)])) {
return $mapping[\get_class($message)];
}
if ($parentsMapping = array_intersect_key($mapping, class_parents($message))) {
return current($parentsMapping);
}
if ($interfaceMapping = array_intersect_key($mapping, class_implements($message))) {
return current($interfaceMapping);
}
if (isset($mapping['*'])) {
return $mapping['*'];
}

return null;
}
}
@@ -0,0 +1,40 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Messenger\Asynchronous\Routing;

use Psr\Container\ContainerInterface;
use Symfony\Component\Messenger\Transport\SenderInterface;

/**
* @author Samuel Roze <samuel.roze@gmail.com>
*/
class ContainerSenderLocator extends AbstractSenderLocator
{
private $senderServiceLocator;
private $messageToSenderIdMapping;

public function __construct(ContainerInterface $senderServiceLocator, array $messageToSenderIdMapping)
{
$this->senderServiceLocator = $senderServiceLocator;
$this->messageToSenderIdMapping = $messageToSenderIdMapping;
}

/**
* {@inheritdoc}
*/
public function getSenderForMessage($message): ?SenderInterface
{
$senderId = self::getValueFromMessageRouting($this->messageToSenderIdMapping, $message);

return $senderId ? $this->senderServiceLocator->get($senderId) : null;
}
}
Expand Up @@ -11,56 +11,35 @@

namespace Symfony\Component\Messenger\Asynchronous\Routing;

use Psr\Container\ContainerInterface;
use Symfony\Component\Messenger\Exception\RuntimeException;
use Symfony\Component\Messenger\Transport\SenderInterface;

/**
* @author Samuel Roze <samuel.roze@gmail.com>
* @author Fabien Potencier <fabien@symfony.com>
*/
class SenderLocator implements SenderLocatorInterface
class SenderLocator extends AbstractSenderLocator
{
private $senderServiceLocator;
private $messageToSenderIdMapping;
private $messageToSenderMapping;

public function __construct(ContainerInterface $senderServiceLocator, array $messageToSenderIdMapping)
public function __construct(array $messageToSenderMapping)
{
$this->senderServiceLocator = $senderServiceLocator;
$this->messageToSenderIdMapping = $messageToSenderIdMapping;
$this->messageToSenderMapping = $messageToSenderMapping;
}

/**
* {@inheritdoc}
*/
public function getSenderForMessage($message): ?SenderInterface
{
$senderId = $this->getSenderId($message);

return $senderId ? $this->senderServiceLocator->get($senderId) : null;
}

private function getSenderId($message): ?string
{
return self::getValueFromMessageRouting($this->messageToSenderIdMapping, $message);
}

/**
* @internal
*/
public static function getValueFromMessageRouting(array $mapping, $message)
{
if (isset($mapping[\get_class($message)])) {
return $mapping[\get_class($message)];
$sender = self::getValueFromMessageRouting($this->messageToSenderMapping, $message);
if (null === $sender) {
return null;
}
if ($parentsMapping = array_intersect_key($mapping, class_parents($message))) {
return current($parentsMapping);
}
if ($interfaceMapping = array_intersect_key($mapping, class_implements($message))) {
return current($interfaceMapping);
}
if (isset($mapping['*'])) {
return $mapping['*'];

if (!$sender instanceof SenderInterface) {
throw new RuntimeException(sprintf('The sender instance provided for message "%s" should be of type "%s" but got "%s".', \get_class($message), SenderInterface::class, \is_object($sender) ? \get_class($sender) : \gettype($sender)));
}

return null;
return $sender;
}
}
3 changes: 3 additions & 0 deletions src/Symfony/Component/Messenger/CHANGELOG.md
Expand Up @@ -4,6 +4,9 @@ CHANGELOG
4.2.0
-----

* [BC BREAK] `SenderLocator` has been renamed to `ContainerSenderLocator`
Be careful as there is still a `SenderLocator` class, but it does not rely on a `ContainerInterface` to find senders.
Instead, it accepts the sender instance itself instead of its identifier in the container.
* [BC BREAK] `MessageSubscriberInterface::getHandledMessages()` return value has changed. The value of an array item
needs to be an associative array or the method name.
* `ValidationMiddleware::handle()` and `SendMessageMiddleware::handle()` now require an `Envelope` object
Expand Down
19 changes: 19 additions & 0 deletions src/Symfony/Component/Messenger/Exception/RuntimeException.php
@@ -0,0 +1,19 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Messenger\Exception;

/**
* @author Fabien Potencier <fabien@symfony.com>
*/
class RuntimeException extends \RuntimeException implements ExceptionInterface
{
}
@@ -0,0 +1,91 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Messenger\Tests\Asynchronous\Routing;

use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\Messenger\Asynchronous\Routing\ContainerSenderLocator;
use Symfony\Component\Messenger\Tests\Fixtures\ChildDummyMessage;
use Symfony\Component\Messenger\Tests\Fixtures\DummyMessage;
use Symfony\Component\Messenger\Tests\Fixtures\DummyMessageInterface;
use Symfony\Component\Messenger\Tests\Fixtures\SecondMessage;
use Symfony\Component\Messenger\Transport\SenderInterface;

class ContainerSenderLocatorTest extends TestCase
{
public function testItReturnsTheSenderBasedOnTheMessageClass()
{
$sender = $this->getMockBuilder(SenderInterface::class)->getMock();
$container = new Container();
$container->set('my_amqp_sender', $sender);

$locator = new ContainerSenderLocator($container, array(
DummyMessage::class => 'my_amqp_sender',
));

$this->assertSame($sender, $locator->getSenderForMessage(new DummyMessage('Hello')));
$this->assertNull($locator->getSenderForMessage(new SecondMessage()));
}

public function testItReturnsTheSenderBasedOnTheMessageParentClass()
{
$container = new Container();

$sender = $this->getMockBuilder(SenderInterface::class)->getMock();
$container->set('my_amqp_sender', $sender);

$apiSender = $this->getMockBuilder(SenderInterface::class)->getMock();
$container->set('my_api_sender', $apiSender);

$locator = new ContainerSenderLocator($container, array(
DummyMessageInterface::class => 'my_api_sender',
DummyMessage::class => 'my_amqp_sender',
));

$this->assertSame($sender, $locator->getSenderForMessage(new ChildDummyMessage('Hello')));
$this->assertNull($locator->getSenderForMessage(new SecondMessage()));
}

public function testItReturnsTheSenderBasedOnTheMessageInterface()
{
$container = new Container();

$sender = $this->getMockBuilder(SenderInterface::class)->getMock();
$container->set('my_amqp_sender', $sender);

$locator = new ContainerSenderLocator($container, array(
DummyMessageInterface::class => 'my_amqp_sender',
));

$this->assertSame($sender, $locator->getSenderForMessage(new DummyMessage('Hello')));
$this->assertNull($locator->getSenderForMessage(new SecondMessage()));
}

public function testItSupportsAWildcardInsteadOfTheMessageClass()
{
$container = new Container();

$sender = $this->getMockBuilder(SenderInterface::class)->getMock();
$container->set('my_amqp_sender', $sender);

$apiSender = $this->getMockBuilder(SenderInterface::class)->getMock();
$container->set('my_api_sender', $apiSender);

$locator = new ContainerSenderLocator($container, array(
DummyMessage::class => 'my_amqp_sender',
'*' => 'my_api_sender',
));

$this->assertSame($sender, $locator->getSenderForMessage(new DummyMessage('Hello')));
$this->assertSame($apiSender, $locator->getSenderForMessage(new SecondMessage()));
}
}
Expand Up @@ -12,11 +12,9 @@
namespace Symfony\Component\Messenger\Tests\Asynchronous\Routing;

use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\Messenger\Asynchronous\Routing\SenderLocator;
use Symfony\Component\Messenger\Tests\Fixtures\ChildDummyMessage;
use Symfony\Component\Messenger\Exception\RuntimeException;
use Symfony\Component\Messenger\Tests\Fixtures\DummyMessage;
use Symfony\Component\Messenger\Tests\Fixtures\DummyMessageInterface;
use Symfony\Component\Messenger\Tests\Fixtures\SecondMessage;
use Symfony\Component\Messenger\Transport\SenderInterface;

Expand All @@ -25,67 +23,21 @@ class SenderLocatorTest extends TestCase
public function testItReturnsTheSenderBasedOnTheMessageClass()
{
$sender = $this->getMockBuilder(SenderInterface::class)->getMock();
$container = new Container();
$container->set('my_amqp_sender', $sender);

$locator = new SenderLocator($container, array(
DummyMessage::class => 'my_amqp_sender',
));

$this->assertSame($sender, $locator->getSenderForMessage(new DummyMessage('Hello')));
$this->assertNull($locator->getSenderForMessage(new SecondMessage()));
}

public function testItReturnsTheSenderBasedOnTheMessageParentClass()
{
$container = new Container();

$sender = $this->getMockBuilder(SenderInterface::class)->getMock();
$container->set('my_amqp_sender', $sender);

$apiSender = $this->getMockBuilder(SenderInterface::class)->getMock();
$container->set('my_api_sender', $apiSender);

$locator = new SenderLocator($container, array(
DummyMessageInterface::class => 'my_api_sender',
DummyMessage::class => 'my_amqp_sender',
));

$this->assertSame($sender, $locator->getSenderForMessage(new ChildDummyMessage('Hello')));
$this->assertNull($locator->getSenderForMessage(new SecondMessage()));
}

public function testItReturnsTheSenderBasedOnTheMessageInterface()
{
$container = new Container();

$sender = $this->getMockBuilder(SenderInterface::class)->getMock();
$container->set('my_amqp_sender', $sender);

$locator = new SenderLocator($container, array(
DummyMessageInterface::class => 'my_amqp_sender',
$locator = new SenderLocator(array(
DummyMessage::class => $sender,
));

$this->assertSame($sender, $locator->getSenderForMessage(new DummyMessage('Hello')));
$this->assertNull($locator->getSenderForMessage(new SecondMessage()));
}

public function testItSupportsAWildcardInsteadOfTheMessageClass()
public function testItThrowsExceptionIfConfigurationIsWrong()
{
$container = new Container();

$sender = $this->getMockBuilder(SenderInterface::class)->getMock();
$container->set('my_amqp_sender', $sender);

$apiSender = $this->getMockBuilder(SenderInterface::class)->getMock();
$container->set('my_api_sender', $apiSender);

$locator = new SenderLocator($container, array(
DummyMessage::class => 'my_amqp_sender',
'*' => 'my_api_sender',
$locator = new SenderLocator(array(
DummyMessage::class => 'amqp',
));

$this->assertSame($sender, $locator->getSenderForMessage(new DummyMessage('Hello')));
$this->assertSame($apiSender, $locator->getSenderForMessage(new SecondMessage()));
$this->expectException(RuntimeException::class);
$locator->getSenderForMessage(new DummyMessage('Hello'));
}
}

0 comments on commit e980ce4

Please sign in to comment.