feature #39342 [Notifier] Add mercure bridge (mtarld)
This PR was merged into the 5.3-dev branch.
Discussion
----------
[Notifier] Add mercure bridge
| Q | A
| ------------- | ---
| Branch? | 5.x
| Bug fix? | no
| New feature? | yes
| Deprecations? | no
| Tickets | Fix #36481
| License | MIT
| Doc PR | https://github.com/symfony/symfony-docs/pull/14840
Add a Notifier bridge for Mercure.
In this PR, Mercure is considered as a chatter (I'm still wondering if it's the most appropriate type).
The first approach for the DSN is `mercure://jwtToken@host:port/hubPath?topic=/foo/1&secure=false` with:
- `topic` optional (defaults to `null`)
- `secure` optional (defaults to `true`)
I'm not sure about the current way to deal with http/https. Maybe we can just replace the `mercure` scheme by `http|https`?
The notification representation is following [Activity Streams](https://www.w3.org/TR/activitystreams-core/#jsonld)
#SymfonyHackday
Commits
-------
19c6544f42
[Notifier] Add mercure bridge
This commit is contained in:
commit
c6100bc386
@ -130,6 +130,7 @@
|
||||
"psr/http-client": "^1.0",
|
||||
"psr/simple-cache": "^1.0",
|
||||
"egulias/email-validator": "^2.1.10",
|
||||
"symfony/mercure-bundle": "^0.2",
|
||||
"symfony/phpunit-bridge": "^5.2",
|
||||
"symfony/security-acl": "~2.8|~3.0",
|
||||
"phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0",
|
||||
|
@ -25,6 +25,7 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Bundle\FrameworkBundle\Routing\AnnotatedRouteControllerLoader;
|
||||
use Symfony\Bundle\FrameworkBundle\Routing\RouteLoaderInterface;
|
||||
use Symfony\Bundle\FullStack;
|
||||
use Symfony\Bundle\MercureBundle\MercureBundle;
|
||||
use Symfony\Component\Asset\PackageInterface;
|
||||
use Symfony\Component\BrowserKit\AbstractBrowser;
|
||||
use Symfony\Component\Cache\Adapter\AdapterInterface;
|
||||
@ -43,6 +44,8 @@ use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\DependencyInjection\Alias;
|
||||
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
|
||||
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
|
||||
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
|
||||
use Symfony\Component\DependencyInjection\ChildDefinition;
|
||||
use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
@ -112,6 +115,7 @@ use Symfony\Component\Notifier\Bridge\Infobip\InfobipTransportFactory;
|
||||
use Symfony\Component\Notifier\Bridge\Iqsms\IqsmsTransportFactory;
|
||||
use Symfony\Component\Notifier\Bridge\LinkedIn\LinkedInTransportFactory;
|
||||
use Symfony\Component\Notifier\Bridge\Mattermost\MattermostTransportFactory;
|
||||
use Symfony\Component\Notifier\Bridge\Mercure\MercureTransportFactory;
|
||||
use Symfony\Component\Notifier\Bridge\Mobyt\MobytTransportFactory;
|
||||
use Symfony\Component\Notifier\Bridge\Nexmo\NexmoTransportFactory;
|
||||
use Symfony\Component\Notifier\Bridge\Octopush\OctopushTransportFactory;
|
||||
@ -2242,6 +2246,7 @@ class FrameworkExtension extends Extension
|
||||
LinkedInTransportFactory::class => 'notifier.transport_factory.linkedin',
|
||||
GatewayApiTransportFactory::class => 'notifier.transport_factory.gatewayapi',
|
||||
OctopushTransportFactory::class => 'notifier.transport_factory.octopush',
|
||||
MercureTransportFactory::class => 'notifier.transport_factory.mercure',
|
||||
];
|
||||
|
||||
foreach ($classToServices as $class => $service) {
|
||||
@ -2250,6 +2255,15 @@ class FrameworkExtension extends Extension
|
||||
}
|
||||
}
|
||||
|
||||
if (class_exists(MercureTransportFactory::class)) {
|
||||
if (!class_exists(MercureBundle::class)) {
|
||||
throw new \LogicException('The MercureBundle is not registered in your application. Try running "composer require symfony/mercure-bundle".');
|
||||
}
|
||||
|
||||
$container->getDefinition($classToServices[MercureTransportFactory::class])
|
||||
->replaceArgument('$publisherLocator', new ServiceLocatorArgument(new TaggedIteratorArgument('mercure.publisher', null, null, true)));
|
||||
}
|
||||
|
||||
if (isset($config['admin_recipients'])) {
|
||||
$notifier = $container->getDefinition('notifier');
|
||||
foreach ($config['admin_recipients'] as $i => $recipient) {
|
||||
|
@ -22,6 +22,7 @@ use Symfony\Component\Notifier\Bridge\Infobip\InfobipTransportFactory;
|
||||
use Symfony\Component\Notifier\Bridge\Iqsms\IqsmsTransportFactory;
|
||||
use Symfony\Component\Notifier\Bridge\LinkedIn\LinkedInTransportFactory;
|
||||
use Symfony\Component\Notifier\Bridge\Mattermost\MattermostTransportFactory;
|
||||
use Symfony\Component\Notifier\Bridge\Mercure\MercureTransportFactory;
|
||||
use Symfony\Component\Notifier\Bridge\Mobyt\MobytTransportFactory;
|
||||
use Symfony\Component\Notifier\Bridge\Nexmo\NexmoTransportFactory;
|
||||
use Symfony\Component\Notifier\Bridge\Octopush\OctopushTransportFactory;
|
||||
@ -135,6 +136,10 @@ return static function (ContainerConfigurator $container) {
|
||||
->parent('notifier.transport_factory.abstract')
|
||||
->tag('texter.transport_factory')
|
||||
|
||||
->set('notifier.transport_factory.mercure', MercureTransportFactory::class)
|
||||
->parent('notifier.transport_factory.abstract')
|
||||
->tag('chatter.transport_factory')
|
||||
|
||||
->set('notifier.transport_factory.null', NullTransportFactory::class)
|
||||
->parent('notifier.transport_factory.abstract')
|
||||
->tag('chatter.transport_factory')
|
||||
|
4
src/Symfony/Component/Notifier/Bridge/Mercure/.gitattributes
vendored
Normal file
4
src/Symfony/Component/Notifier/Bridge/Mercure/.gitattributes
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
/Tests export-ignore
|
||||
/phpunit.xml.dist export-ignore
|
||||
/.gitattributes export-ignore
|
||||
/.gitignore export-ignore
|
@ -0,0 +1,7 @@
|
||||
CHANGELOG
|
||||
=========
|
||||
|
||||
5.3
|
||||
---
|
||||
|
||||
* Add the bridge
|
19
src/Symfony/Component/Notifier/Bridge/Mercure/LICENSE
Normal file
19
src/Symfony/Component/Notifier/Bridge/Mercure/LICENSE
Normal file
@ -0,0 +1,19 @@
|
||||
Copyright (c) 2021 Fabien Potencier
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is furnished
|
||||
to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Notifier\Bridge\Mercure;
|
||||
|
||||
use Symfony\Component\Notifier\Message\MessageOptionsInterface;
|
||||
|
||||
/**
|
||||
* @author Mathias Arlaud <mathias.arlaud@gmail.com>
|
||||
*/
|
||||
final class MercureOptions implements MessageOptionsInterface
|
||||
{
|
||||
private $topics;
|
||||
private $private;
|
||||
private $id;
|
||||
private $type;
|
||||
private $retry;
|
||||
|
||||
/**
|
||||
* @param string|string[]|null $topics
|
||||
*/
|
||||
public function __construct($topics = null, bool $private = false, ?string $id = null, ?string $type = null, ?int $retry = null)
|
||||
{
|
||||
if (null !== $topics && !\is_array($topics) && !\is_string($topics)) {
|
||||
throw new \TypeError(sprintf('"%s()" expects parameter 1 to be an array of strings, a string or null, "%s" given.', __METHOD__, get_debug_type($topics)));
|
||||
}
|
||||
|
||||
$this->topics = null !== $topics ? (array) $topics : null;
|
||||
$this->private = $private;
|
||||
$this->id = $id;
|
||||
$this->type = $type;
|
||||
$this->retry = $retry;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]|null
|
||||
*/
|
||||
public function getTopics(): ?array
|
||||
{
|
||||
return $this->topics;
|
||||
}
|
||||
|
||||
public function isPrivate(): bool
|
||||
{
|
||||
return $this->private;
|
||||
}
|
||||
|
||||
public function getId(): ?string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getType(): ?string
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
public function getRetry(): ?int
|
||||
{
|
||||
return $this->retry;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'topics' => $this->topics,
|
||||
'private' => $this->private,
|
||||
'id' => $this->id,
|
||||
'type' => $this->type,
|
||||
'retry' => $this->retry,
|
||||
];
|
||||
}
|
||||
|
||||
public function getRecipientId(): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,100 @@
|
||||
<?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\Notifier\Bridge\Mercure;
|
||||
|
||||
use Symfony\Component\Mercure\PublisherInterface;
|
||||
use Symfony\Component\Mercure\Update;
|
||||
use Symfony\Component\Notifier\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Notifier\Exception\LogicException;
|
||||
use Symfony\Component\Notifier\Exception\TransportException;
|
||||
use Symfony\Component\Notifier\Exception\UnsupportedMessageTypeException;
|
||||
use Symfony\Component\Notifier\Message\ChatMessage;
|
||||
use Symfony\Component\Notifier\Message\MessageInterface;
|
||||
use Symfony\Component\Notifier\Message\SentMessage;
|
||||
use Symfony\Component\Notifier\Transport\AbstractTransport;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Contracts\HttpClient\Exception\ExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
/**
|
||||
* @author Mathias Arlaud <mathias.arlaud@gmail.com>
|
||||
*/
|
||||
final class MercureTransport extends AbstractTransport
|
||||
{
|
||||
private $publisher;
|
||||
private $publisherId;
|
||||
private $topics;
|
||||
|
||||
/**
|
||||
* @param string|string[]|null $topics
|
||||
*/
|
||||
public function __construct(PublisherInterface $publisher, string $publisherId, $topics = null, ?HttpClientInterface $client = null, ?EventDispatcherInterface $dispatcher = null)
|
||||
{
|
||||
if (null !== $topics && !\is_array($topics) && !\is_string($topics)) {
|
||||
throw new \TypeError(sprintf('"%s()" expects parameter 3 to be an array of strings, a string or null, "%s" given.', __METHOD__, get_debug_type($topics)));
|
||||
}
|
||||
|
||||
$this->publisher = $publisher;
|
||||
$this->publisherId = $publisherId;
|
||||
$this->topics = $topics ?? 'https://symfony.com/notifier';
|
||||
|
||||
parent::__construct($client, $dispatcher);
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return sprintf('mercure://%s?%s', $this->publisherId, http_build_query(['topic' => $this->topics]));
|
||||
}
|
||||
|
||||
public function supports(MessageInterface $message): bool
|
||||
{
|
||||
return $message instanceof ChatMessage && (null === $message->getOptions() || $message->getOptions() instanceof MercureOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://symfony.com/doc/current/mercure.html#publishing
|
||||
*/
|
||||
protected function doSend(MessageInterface $message): SentMessage
|
||||
{
|
||||
if (!$message instanceof ChatMessage) {
|
||||
throw new UnsupportedMessageTypeException(__CLASS__, ChatMessage::class, $message);
|
||||
}
|
||||
|
||||
if (($options = $message->getOptions()) && !$options instanceof MercureOptions) {
|
||||
throw new LogicException(sprintf('The "%s" transport only supports instances of "%s" for options.', __CLASS__, MercureOptions::class));
|
||||
}
|
||||
|
||||
if (null === $options) {
|
||||
$options = new MercureOptions($this->topics);
|
||||
}
|
||||
|
||||
// @see https://www.w3.org/TR/activitystreams-core/#jsonld
|
||||
$update = new Update($options->getTopics() ?? $this->topics, json_encode([
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'type' => 'Announce',
|
||||
'summary' => $message->getSubject(),
|
||||
]), $options->isPrivate(), $options->getId(), $options->getType(), $options->getRetry());
|
||||
|
||||
try {
|
||||
$messageId = ($this->publisher)($update);
|
||||
|
||||
$sentMessage = new SentMessage($message, (string) $this);
|
||||
$sentMessage->setMessageId($messageId);
|
||||
|
||||
return $sentMessage;
|
||||
} catch (ExceptionInterface $e) {
|
||||
throw new TransportException(sprintf('Unable to post the Mercure message: "%s".', $e->getResponse()->getContent(false)), $e->getResponse(), $e->getCode(), $e);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
throw new InvalidArgumentException(sprintf('Unable to post the Mercure message: "%s".', $e->getMessage()), $e->getCode(), $e);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Notifier\Bridge\Mercure;
|
||||
|
||||
use Symfony\Component\Mercure\PublisherInterface;
|
||||
use Symfony\Component\Notifier\Exception\LogicException;
|
||||
use Symfony\Component\Notifier\Exception\UnsupportedSchemeException;
|
||||
use Symfony\Component\Notifier\Transport\AbstractTransportFactory;
|
||||
use Symfony\Component\Notifier\Transport\Dsn;
|
||||
use Symfony\Component\Notifier\Transport\TransportInterface;
|
||||
use Symfony\Contracts\Service\ServiceProviderInterface;
|
||||
|
||||
/**
|
||||
* @author Mathias Arlaud <mathias.arlaud@gmail.com>
|
||||
*/
|
||||
final class MercureTransportFactory extends AbstractTransportFactory
|
||||
{
|
||||
private $publisherLocator;
|
||||
|
||||
/**
|
||||
* @param ServiceProviderInterface $publisherLocator A container that holds {@see PublisherInterface} instances
|
||||
*/
|
||||
public function __construct(ServiceProviderInterface $publisherLocator)
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$this->publisherLocator = $publisherLocator;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return MercureTransport
|
||||
*/
|
||||
public function create(Dsn $dsn): TransportInterface
|
||||
{
|
||||
if ('mercure' !== $dsn->getScheme()) {
|
||||
throw new UnsupportedSchemeException($dsn, 'mercure', $this->getSupportedSchemes());
|
||||
}
|
||||
|
||||
$publisherId = $dsn->getHost();
|
||||
if (!$this->publisherLocator->has($publisherId)) {
|
||||
throw new LogicException(sprintf('"%s" not found. Did you mean one of: %s?', $publisherId, implode(', ', array_keys($this->publisherLocator->getProvidedServices()))));
|
||||
}
|
||||
|
||||
$topic = $dsn->getOption('topic');
|
||||
|
||||
return new MercureTransport($this->publisherLocator->get($publisherId), $publisherId, $topic);
|
||||
}
|
||||
|
||||
protected function getSupportedSchemes(): array
|
||||
{
|
||||
return ['mercure'];
|
||||
}
|
||||
}
|
23
src/Symfony/Component/Notifier/Bridge/Mercure/README.md
Normal file
23
src/Symfony/Component/Notifier/Bridge/Mercure/README.md
Normal file
@ -0,0 +1,23 @@
|
||||
Mercure Notifier
|
||||
================
|
||||
|
||||
Provides [Mercure](https://github.com/symfony/mercure) integration for Symfony Notifier.
|
||||
|
||||
DSN example
|
||||
-----------
|
||||
|
||||
```
|
||||
MERCURE_DSN=mercure://PUBLISHER_SERVICE_ID?topic=TOPIC
|
||||
```
|
||||
|
||||
where:
|
||||
- `PUBLISHER_SERVICE_ID` is the Mercure publisher service id
|
||||
- `TOPIC` is the topic IRI (optional, default: `https://symfony.com/notifier`. Could be either a single topic: `topic=https://foo` or multiple topics: `topic[]=/foo/1&topic[]=https://bar`)
|
||||
|
||||
Resources
|
||||
---------
|
||||
|
||||
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
|
||||
* [Report issues](https://github.com/symfony/symfony/issues) and
|
||||
[send Pull Requests](https://github.com/symfony/symfony/pulls)
|
||||
in the [main Symfony repository](https://github.com/symfony/symfony)
|
@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Notifier\Bridge\Mercure\Tests;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Notifier\Bridge\Mercure\MercureOptions;
|
||||
use TypeError;
|
||||
|
||||
final class MercureOptionsTest extends TestCase
|
||||
{
|
||||
public function testConstructWithDefaults()
|
||||
{
|
||||
$this->assertSame((new MercureOptions())->toArray(), [
|
||||
'topics' => null,
|
||||
'private' => false,
|
||||
'id' => null,
|
||||
'type' => null,
|
||||
'retry' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
public function testConstructWithParameters()
|
||||
{
|
||||
$options = (new MercureOptions('/topic/1', true, 'id', 'type', 1));
|
||||
|
||||
$this->assertSame($options->toArray(), [
|
||||
'topics' => ['/topic/1'],
|
||||
'private' => true,
|
||||
'id' => 'id',
|
||||
'type' => 'type',
|
||||
'retry' => 1,
|
||||
]);
|
||||
}
|
||||
|
||||
public function testConstructWithWrongTopicsThrows()
|
||||
{
|
||||
$this->expectException(TypeError::class);
|
||||
new MercureOptions(1);
|
||||
}
|
||||
}
|
@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Notifier\Bridge\Mercure\Tests;
|
||||
|
||||
use LogicException;
|
||||
use Symfony\Component\Mercure\PublisherInterface;
|
||||
use Symfony\Component\Notifier\Bridge\Mercure\MercureTransportFactory;
|
||||
use Symfony\Component\Notifier\Tests\TransportFactoryTestCase;
|
||||
use Symfony\Component\Notifier\Transport\Dsn;
|
||||
use Symfony\Component\Notifier\Transport\TransportFactoryInterface;
|
||||
use Symfony\Contracts\Service\ServiceProviderInterface;
|
||||
|
||||
/**
|
||||
* @author Mathias Arlaud <mathias.arlaud@gmail.com>
|
||||
*/
|
||||
final class MercureTransportFactoryTest extends TransportFactoryTestCase
|
||||
{
|
||||
public function createFactory(): TransportFactoryInterface
|
||||
{
|
||||
$publisherLocator = $this->createMock(ServiceProviderInterface::class);
|
||||
$publisherLocator->method('has')->willReturn(true);
|
||||
$publisherLocator->method('get')->willReturn($this->createMock(PublisherInterface::class));
|
||||
|
||||
return new MercureTransportFactory($publisherLocator);
|
||||
}
|
||||
|
||||
public function supportsProvider(): iterable
|
||||
{
|
||||
yield [true, 'mercure://publisherId?topic=topic'];
|
||||
yield [false, 'somethingElse://publisherId?topic=topic'];
|
||||
}
|
||||
|
||||
public function createProvider(): iterable
|
||||
{
|
||||
yield [
|
||||
'mercure://publisherId?topic=%2Ftopic%2F1',
|
||||
'mercure://publisherId?topic=/topic/1',
|
||||
];
|
||||
|
||||
yield [
|
||||
'mercure://publisherId?topic%5B0%5D=%2Ftopic%2F1&topic%5B1%5D=%2Ftopic%2F2',
|
||||
'mercure://publisherId?topic[]=/topic/1&topic[]=/topic/2',
|
||||
];
|
||||
|
||||
yield [
|
||||
'mercure://publisherId?topic=https%3A%2F%2Fsymfony.com%2Fnotifier',
|
||||
'mercure://publisherId',
|
||||
];
|
||||
}
|
||||
|
||||
public function unsupportedSchemeProvider(): iterable
|
||||
{
|
||||
yield ['somethingElse://publisherId?topic=topic'];
|
||||
}
|
||||
|
||||
public function testNotFoundPublisherThrows()
|
||||
{
|
||||
$publisherLocator = $this->createMock(ServiceProviderInterface::class);
|
||||
$publisherLocator->method('has')->willReturn(false);
|
||||
$publisherLocator->method('getProvidedServices')->willReturn(['fooPublisher' => 'fooFqcn', 'barPublisher' => 'barFqcn']);
|
||||
|
||||
$factory = new MercureTransportFactory($publisherLocator);
|
||||
|
||||
$this->expectException(LogicException::class);
|
||||
$this->expectExceptionMessage('"publisherId" not found. Did you mean one of: fooPublisher, barPublisher?');
|
||||
$factory->create(new Dsn('mercure://publisherId'));
|
||||
}
|
||||
}
|
@ -0,0 +1,163 @@
|
||||
<?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\Notifier\Bridge\Mercure\Tests;
|
||||
|
||||
use Symfony\Component\Mercure\PublisherInterface;
|
||||
use Symfony\Component\Mercure\Update;
|
||||
use Symfony\Component\Notifier\Bridge\Mercure\MercureOptions;
|
||||
use Symfony\Component\Notifier\Bridge\Mercure\MercureTransport;
|
||||
use Symfony\Component\Notifier\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Notifier\Exception\LogicException;
|
||||
use Symfony\Component\Notifier\Exception\TransportException;
|
||||
use Symfony\Component\Notifier\Message\ChatMessage;
|
||||
use Symfony\Component\Notifier\Message\MessageInterface;
|
||||
use Symfony\Component\Notifier\Message\MessageOptionsInterface;
|
||||
use Symfony\Component\Notifier\Message\SmsMessage;
|
||||
use Symfony\Component\Notifier\Tests\TransportTestCase;
|
||||
use Symfony\Component\Notifier\Transport\TransportInterface;
|
||||
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||
use TypeError;
|
||||
|
||||
/**
|
||||
* @author Mathias Arlaud <mathias.arlaud@gmail.com>
|
||||
*/
|
||||
final class MercureTransportTest extends TransportTestCase
|
||||
{
|
||||
public function createTransport(?HttpClientInterface $client = null, ?PublisherInterface $publisher = null, string $publisherId = 'publisherId', $topics = null): TransportInterface
|
||||
{
|
||||
$publisher = $publisher ?? $this->createMock(PublisherInterface::class);
|
||||
|
||||
return new MercureTransport($publisher, $publisherId, $topics);
|
||||
}
|
||||
|
||||
public function toStringProvider(): iterable
|
||||
{
|
||||
yield ['mercure://publisherId?topic=https%3A%2F%2Fsymfony.com%2Fnotifier', $this->createTransport()];
|
||||
yield ['mercure://customPublisherId?topic=%2Ftopic', $this->createTransport(null, null, 'customPublisherId', '/topic')];
|
||||
yield ['mercure://customPublisherId?topic%5B0%5D=%2Ftopic%2F1&topic%5B1%5D%5B0%5D=%2Ftopic%2F2', $this->createTransport(null, null, 'customPublisherId', ['/topic/1', ['/topic/2']])];
|
||||
}
|
||||
|
||||
public function supportedMessagesProvider(): iterable
|
||||
{
|
||||
yield [new ChatMessage('Hello!')];
|
||||
}
|
||||
|
||||
public function unsupportedMessagesProvider(): iterable
|
||||
{
|
||||
yield [new SmsMessage('0611223344', 'Hello!')];
|
||||
yield [$this->createMock(MessageInterface::class)];
|
||||
}
|
||||
|
||||
public function testCanSetCustomPort()
|
||||
{
|
||||
$this->markTestSkipped("Mercure transport doesn't use a regular HTTP Dsn");
|
||||
}
|
||||
|
||||
public function testCanSetCustomHost()
|
||||
{
|
||||
$this->markTestSkipped("Mercure transport doesn't use a regular HTTP Dsn");
|
||||
}
|
||||
|
||||
public function testCanSetCustomHostAndPort()
|
||||
{
|
||||
$this->markTestSkipped("Mercure transport doesn't use a regular HTTP Dsn");
|
||||
}
|
||||
|
||||
public function testConstructWithWrongTopicsThrows()
|
||||
{
|
||||
$this->expectException(TypeError::class);
|
||||
$this->createTransport(null, null, 'publisherId', 1);
|
||||
}
|
||||
|
||||
public function testSendWithNonMercureOptionsThrows()
|
||||
{
|
||||
$this->expectException(LogicException::class);
|
||||
$this->createTransport()->send(new ChatMessage('testMessage', $this->createMock(MessageOptionsInterface::class)));
|
||||
}
|
||||
|
||||
public function testSendWithWrongResponseThrows()
|
||||
{
|
||||
$response = $this->createMock(ResponseInterface::class);
|
||||
$response->method('getContent')->willReturn('Service Unavailable');
|
||||
|
||||
$httpException = $this->createMock(ServerExceptionInterface::class);
|
||||
$httpException->method('getResponse')->willReturn($response);
|
||||
|
||||
$publisher = $this->createMock(PublisherInterface::class);
|
||||
$publisher->method('__invoke')->willThrowException($httpException);
|
||||
|
||||
$this->expectException(TransportException::class);
|
||||
$this->expectExceptionMessage('Unable to post the Mercure message: "Service Unavailable".');
|
||||
|
||||
$this->createTransport(null, $publisher)->send(new ChatMessage('subject'));
|
||||
}
|
||||
|
||||
public function testSendWithWrongTokenThrows()
|
||||
{
|
||||
$publisher = $this->createMock(PublisherInterface::class);
|
||||
$publisher->method('__invoke')->willThrowException(new \InvalidArgumentException('The provided JWT is not valid'));
|
||||
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('Unable to post the Mercure message: "The provided JWT is not valid".');
|
||||
|
||||
$this->createTransport(null, $publisher)->send(new ChatMessage('subject'));
|
||||
}
|
||||
|
||||
public function testSendWithMercureOptions()
|
||||
{
|
||||
$publisher = $this->createMock(PublisherInterface::class);
|
||||
$publisher
|
||||
->expects($this->once())
|
||||
->method('__invoke')
|
||||
->with(new Update(['/topic/1', '/topic/2'], '{"@context":"https:\/\/www.w3.org\/ns\/activitystreams","type":"Announce","summary":"subject"}', true, 'id', 'type', 1))
|
||||
;
|
||||
|
||||
$this->createTransport(null, $publisher)->send(new ChatMessage('subject', new MercureOptions(['/topic/1', '/topic/2'], true, 'id', 'type', 1)));
|
||||
}
|
||||
|
||||
public function testSendWithMercureOptionsButWithoutOptionTopic()
|
||||
{
|
||||
$publisher = $this->createMock(PublisherInterface::class);
|
||||
$publisher
|
||||
->expects($this->once())
|
||||
->method('__invoke')
|
||||
->with(new Update(['https://symfony.com/notifier'], '{"@context":"https:\/\/www.w3.org\/ns\/activitystreams","type":"Announce","summary":"subject"}', true, 'id', 'type', 1))
|
||||
;
|
||||
|
||||
$this->createTransport(null, $publisher)->send(new ChatMessage('subject', new MercureOptions(null, true, 'id', 'type', 1)));
|
||||
}
|
||||
|
||||
public function testSendWithoutMercureOptions()
|
||||
{
|
||||
$publisher = $this->createMock(PublisherInterface::class);
|
||||
$publisher
|
||||
->expects($this->once())
|
||||
->method('__invoke')
|
||||
->with(new Update(['https://symfony.com/notifier'], '{"@context":"https:\/\/www.w3.org\/ns\/activitystreams","type":"Announce","summary":"subject"}'))
|
||||
;
|
||||
|
||||
$this->createTransport(null, $publisher)->send(new ChatMessage('subject'));
|
||||
}
|
||||
|
||||
public function testSendSuccessfully()
|
||||
{
|
||||
$messageId = 'urn:uuid:a7045be0-a75d-4d40-8bd2-29fa4e5dd10b';
|
||||
|
||||
$publisher = $this->createMock(PublisherInterface::class);
|
||||
$publisher->method('__invoke')->willReturn($messageId);
|
||||
|
||||
$sentMessage = $this->createTransport(null, $publisher)->send(new ChatMessage('subject'));
|
||||
$this->assertSame($messageId, $sentMessage->getMessageId());
|
||||
}
|
||||
}
|
32
src/Symfony/Component/Notifier/Bridge/Mercure/composer.json
Normal file
32
src/Symfony/Component/Notifier/Bridge/Mercure/composer.json
Normal file
@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "symfony/mercure-notifier",
|
||||
"type": "symfony-bridge",
|
||||
"description": "Symfony Mercure Notifier Bridge",
|
||||
"keywords": ["mercure", "notifier"],
|
||||
"homepage": "https://symfony.com",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Mathias Arlaud",
|
||||
"email": "mathias.arlaud@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=7.2.5",
|
||||
"ext-json": "*",
|
||||
"symfony/mercure": "^0.4",
|
||||
"symfony/notifier": "^5.3",
|
||||
"symfony/service-contracts": "^1.10|^2"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\Mercure\\": "" },
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"minimum-stability": "dev"
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/5.2/phpunit.xsd"
|
||||
backupGlobals="false"
|
||||
colors="true"
|
||||
bootstrap="vendor/autoload.php"
|
||||
failOnRisky="true"
|
||||
failOnWarning="true"
|
||||
>
|
||||
<php>
|
||||
<ini name="error_reporting" value="-1" />
|
||||
</php>
|
||||
|
||||
<testsuites>
|
||||
<testsuite name="Symfony Mercure Notifier Bridge Test Suite">
|
||||
<directory>./Tests/</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
|
||||
<filter>
|
||||
<whitelist>
|
||||
<directory>./</directory>
|
||||
<exclude>
|
||||
<directory>./Resources</directory>
|
||||
<directory>./Tests</directory>
|
||||
<directory>./vendor</directory>
|
||||
</exclude>
|
||||
</whitelist>
|
||||
</filter>
|
||||
</phpunit>
|
@ -100,6 +100,10 @@ class UnsupportedSchemeException extends LogicException
|
||||
'class' => Bridge\Octopush\OctopushTransportFactory::class,
|
||||
'package' => 'symfony/octopush-notifier',
|
||||
],
|
||||
'mercure' => [
|
||||
'class' => Bridge\Mercure\MercureTransportFactory::class,
|
||||
'package' => 'symfony/mercure-notifier',
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
|
Reference in New Issue
Block a user