├── CHANGELOG.md ├── LICENSE ├── MercureOptions.php ├── MercureTransport.php ├── MercureTransportFactory.php ├── README.md └── composer.json /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | ========= 3 | 4 | 7.3 5 | --- 6 | 7 | * Add `content` option 8 | 9 | 5.3 10 | --- 11 | 12 | * Add the bridge 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021-present Fabien Potencier 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /MercureOptions.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Notifier\Bridge\Mercure; 13 | 14 | use Symfony\Component\Notifier\Message\MessageOptionsInterface; 15 | 16 | /** 17 | * @author Mathias Arlaud 18 | */ 19 | final class MercureOptions implements MessageOptionsInterface 20 | { 21 | private ?array $topics; 22 | 23 | /** 24 | * @param string|string[]|null $topics 25 | * @param array{ 26 | * badge?: string, 27 | * body?: string, 28 | * data?: mixed, 29 | * dir?: 'auto'|'ltr'|'rtl', 30 | * icon?: string, 31 | * image?: string, 32 | * lang?: string, 33 | * renotify?: bool, 34 | * requireInteraction?: bool, 35 | * silent?: bool, 36 | * tag?: string, 37 | * timestamp?: int, 38 | * vibrate?: int|list, 39 | * }|null $content 40 | */ 41 | public function __construct( 42 | string|array|null $topics = null, 43 | private bool $private = false, 44 | private ?string $id = null, 45 | private ?string $type = null, 46 | private ?int $retry = null, 47 | private ?array $content = null, 48 | ) { 49 | $this->topics = null !== $topics ? (array) $topics : null; 50 | } 51 | 52 | /** 53 | * @return string[]|null 54 | */ 55 | public function getTopics(): ?array 56 | { 57 | return $this->topics; 58 | } 59 | 60 | public function isPrivate(): bool 61 | { 62 | return $this->private; 63 | } 64 | 65 | public function getId(): ?string 66 | { 67 | return $this->id; 68 | } 69 | 70 | public function getType(): ?string 71 | { 72 | return $this->type; 73 | } 74 | 75 | public function getRetry(): ?int 76 | { 77 | return $this->retry; 78 | } 79 | 80 | /** 81 | * @return array{ 82 | * badge?: string, 83 | * body?: string, 84 | * data?: mixed, 85 | * dir?: 'auto'|'ltr'|'rtl', 86 | * icon?: string, 87 | * image?: string, 88 | * lang?: string, 89 | * renotify?: bool, 90 | * requireInteraction?: bool, 91 | * silent?: bool, 92 | * tag?: string, 93 | * timestamp?: int, 94 | * vibrate?: int|list, 95 | * }|null 96 | */ 97 | public function getContent(): ?array 98 | { 99 | return $this->content; 100 | } 101 | 102 | public function toArray(): array 103 | { 104 | return [ 105 | 'topics' => $this->topics, 106 | 'private' => $this->private, 107 | 'id' => $this->id, 108 | 'type' => $this->type, 109 | 'retry' => $this->retry, 110 | 'content' => $this->content, 111 | ]; 112 | } 113 | 114 | public function getRecipientId(): ?string 115 | { 116 | return null; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /MercureTransport.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Notifier\Bridge\Mercure; 13 | 14 | use Symfony\Component\Mercure\Exception\InvalidArgumentException; 15 | use Symfony\Component\Mercure\Exception\RuntimeException as MercureRuntimeException; 16 | use Symfony\Component\Mercure\HubInterface; 17 | use Symfony\Component\Mercure\Update; 18 | use Symfony\Component\Notifier\Exception\RuntimeException; 19 | use Symfony\Component\Notifier\Exception\UnsupportedMessageTypeException; 20 | use Symfony\Component\Notifier\Exception\UnsupportedOptionsException; 21 | use Symfony\Component\Notifier\Message\ChatMessage; 22 | use Symfony\Component\Notifier\Message\MessageInterface; 23 | use Symfony\Component\Notifier\Message\SentMessage; 24 | use Symfony\Component\Notifier\Transport\AbstractTransport; 25 | use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; 26 | use Symfony\Contracts\HttpClient\HttpClientInterface; 27 | 28 | /** 29 | * @author Mathias Arlaud 30 | */ 31 | final class MercureTransport extends AbstractTransport 32 | { 33 | private string|array $topics; 34 | 35 | /** 36 | * @param string|string[]|null $topics 37 | */ 38 | public function __construct( 39 | private HubInterface $hub, 40 | private string $hubId, 41 | string|array|null $topics = null, 42 | ?HttpClientInterface $client = null, 43 | ?EventDispatcherInterface $dispatcher = null, 44 | ) { 45 | $this->topics = $topics ?? 'https://symfony.com/notifier'; 46 | 47 | parent::__construct($client, $dispatcher); 48 | } 49 | 50 | public function __toString(): string 51 | { 52 | return \sprintf('mercure://%s%s', $this->hubId, '?'.http_build_query(['topic' => $this->topics], '', '&')); 53 | } 54 | 55 | public function supports(MessageInterface $message): bool 56 | { 57 | return $message instanceof ChatMessage && (null === $message->getOptions() || $message->getOptions() instanceof MercureOptions); 58 | } 59 | 60 | /** 61 | * @see https://symfony.com/doc/current/mercure.html#publishing 62 | */ 63 | protected function doSend(MessageInterface $message): SentMessage 64 | { 65 | if (!$message instanceof ChatMessage) { 66 | throw new UnsupportedMessageTypeException(__CLASS__, ChatMessage::class, $message); 67 | } 68 | 69 | if (($options = $message->getOptions()) && !$options instanceof MercureOptions) { 70 | throw new UnsupportedOptionsException(__CLASS__, MercureOptions::class, $options); 71 | } 72 | 73 | $options ??= new MercureOptions($this->topics); 74 | 75 | // @see https://www.w3.org/TR/activitystreams-core/#jsonld 76 | $update = new Update($options->getTopics() ?? $this->topics, json_encode([ 77 | '@context' => 'https://www.w3.org/ns/activitystreams', 78 | 'type' => 'Announce', 79 | 'summary' => $message->getSubject(), 80 | 'mediaType' => 'application/json', 81 | 'content' => $options->getContent(), 82 | ]), $options->isPrivate(), $options->getId(), $options->getType(), $options->getRetry()); 83 | 84 | try { 85 | $messageId = $this->hub->publish($update); 86 | 87 | $sentMessage = new SentMessage($message, (string) $this); 88 | $sentMessage->setMessageId($messageId); 89 | 90 | return $sentMessage; 91 | } catch (MercureRuntimeException|InvalidArgumentException $e) { 92 | throw new RuntimeException('Unable to post the Mercure message: '.$e->getMessage(), $e->getCode(), $e); 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /MercureTransportFactory.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Notifier\Bridge\Mercure; 13 | 14 | use Symfony\Component\Mercure\Exception\InvalidArgumentException; 15 | use Symfony\Component\Mercure\HubRegistry; 16 | use Symfony\Component\Notifier\Exception\IncompleteDsnException; 17 | use Symfony\Component\Notifier\Exception\UnsupportedSchemeException; 18 | use Symfony\Component\Notifier\Transport\AbstractTransportFactory; 19 | use Symfony\Component\Notifier\Transport\Dsn; 20 | use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; 21 | use Symfony\Contracts\HttpClient\HttpClientInterface; 22 | 23 | /** 24 | * @author Mathias Arlaud 25 | */ 26 | final class MercureTransportFactory extends AbstractTransportFactory 27 | { 28 | public function __construct( 29 | private HubRegistry $registry, 30 | ?EventDispatcherInterface $dispatcher = null, 31 | ?HttpClientInterface $client = null, 32 | ) { 33 | parent::__construct($dispatcher, $client); 34 | } 35 | 36 | public function create(Dsn $dsn): MercureTransport 37 | { 38 | if ('mercure' !== $dsn->getScheme()) { 39 | throw new UnsupportedSchemeException($dsn, 'mercure', $this->getSupportedSchemes()); 40 | } 41 | 42 | $hubId = $dsn->getHost(); 43 | $topic = $dsn->getOption('topic'); 44 | 45 | try { 46 | $hub = $this->registry->getHub($hubId); 47 | } catch (InvalidArgumentException) { 48 | throw new IncompleteDsnException(\sprintf('Hub "%s" not found. Did you mean one of: "%s"?', $hubId, implode('", "', array_keys($this->registry->all())))); 49 | } 50 | 51 | return new MercureTransport($hub, $hubId, $topic, $this->client, $this->dispatcher); 52 | } 53 | 54 | protected function getSupportedSchemes(): array 55 | { 56 | return ['mercure']; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Mercure Notifier 2 | ================ 3 | 4 | Provides [Mercure](https://github.com/symfony/mercure) integration for Symfony Notifier. 5 | 6 | DSN example 7 | ----------- 8 | 9 | ``` 10 | MERCURE_DSN=mercure://HUB_ID?topic=TOPIC 11 | ``` 12 | 13 | where: 14 | - `HUB_ID` is the Mercure hub id 15 | - `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`) 16 | 17 | Resources 18 | --------- 19 | 20 | * [Contributing](https://symfony.com/doc/current/contributing/index.html) 21 | * [Report issues](https://github.com/symfony/symfony/issues) and 22 | [send Pull Requests](https://github.com/symfony/symfony/pulls) 23 | in the [main Symfony repository](https://github.com/symfony/symfony) 24 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "symfony/mercure-notifier", 3 | "type": "symfony-notifier-bridge", 4 | "description": "Symfony Mercure Notifier Bridge", 5 | "keywords": ["mercure", "notifier"], 6 | "homepage": "https://symfony.com", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Mathias Arlaud", 11 | "email": "mathias.arlaud@gmail.com" 12 | }, 13 | { 14 | "name": "Symfony Community", 15 | "homepage": "https://symfony.com/contributors" 16 | } 17 | ], 18 | "require": { 19 | "php": ">=8.2", 20 | "symfony/mercure": "^0.5.2|^0.6", 21 | "symfony/notifier": "^7.3", 22 | "symfony/service-contracts": "^2.5|^3" 23 | }, 24 | "autoload": { 25 | "psr-4": { "Symfony\\Component\\Notifier\\Bridge\\Mercure\\": "" }, 26 | "exclude-from-classmap": [ 27 | "/Tests/" 28 | ] 29 | }, 30 | "minimum-stability": "dev" 31 | } 32 | --------------------------------------------------------------------------------