diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 38d37c7fc1990..24f84d3270eaf 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -2926,6 +2926,7 @@ private function registerNotifierConfiguration(array $config, ContainerBuilder $ NotifierBridge\Lox24\Lox24TransportFactory::class => 'notifier.transport_factory.lox24', NotifierBridge\Mailjet\MailjetTransportFactory::class => 'notifier.transport_factory.mailjet', NotifierBridge\Mastodon\MastodonTransportFactory::class => 'notifier.transport_factory.mastodon', + NotifierBridge\Matrix\MatrixTransportFactory::class => 'notifier.transport_factory.matrix', NotifierBridge\Mattermost\MattermostTransportFactory::class => 'notifier.transport_factory.mattermost', NotifierBridge\Mercure\MercureTransportFactory::class => 'notifier.transport_factory.mercure', NotifierBridge\MessageBird\MessageBirdTransportFactory::class => 'notifier.transport_factory.message-bird', diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php index f28007decf81b..d1adcfc370395 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php @@ -36,6 +36,7 @@ 'line-notify' => Bridge\LineNotify\LineNotifyTransportFactory::class, 'linked-in' => Bridge\LinkedIn\LinkedInTransportFactory::class, 'mastodon' => Bridge\Mastodon\MastodonTransportFactory::class, + 'matrix' => Bridge\Matrix\MatrixTransportFactory::class, 'mattermost' => Bridge\Mattermost\MattermostTransportFactory::class, 'mercure' => Bridge\Mercure\MercureTransportFactory::class, 'microsoft-teams' => Bridge\MicrosoftTeams\MicrosoftTeamsTransportFactory::class, diff --git a/src/Symfony/Component/Notifier/Bridge/Matrix/.gitattributes b/src/Symfony/Component/Notifier/Bridge/Matrix/.gitattributes new file mode 100644 index 0000000000000..14c3c35940427 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Matrix/.gitattributes @@ -0,0 +1,3 @@ +/Tests export-ignore +/phpunit.xml.dist export-ignore +/.git* export-ignore diff --git a/src/Symfony/Component/Notifier/Bridge/Matrix/.gitignore b/src/Symfony/Component/Notifier/Bridge/Matrix/.gitignore new file mode 100644 index 0000000000000..c49a5d8df5c65 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Matrix/.gitignore @@ -0,0 +1,3 @@ +vendor/ +composer.lock +phpunit.xml diff --git a/src/Symfony/Component/Notifier/Bridge/Matrix/CHANGELOG.md b/src/Symfony/Component/Notifier/Bridge/Matrix/CHANGELOG.md new file mode 100644 index 0000000000000..c3adba2592600 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Matrix/CHANGELOG.md @@ -0,0 +1,7 @@ +CHANGELOG +========= + +7.3 +--- + + * Add the bridge diff --git a/src/Symfony/Component/Notifier/Bridge/Matrix/LICENSE b/src/Symfony/Component/Notifier/Bridge/Matrix/LICENSE new file mode 100644 index 0000000000000..bc38d714ef697 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Matrix/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2025-present 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/Notifier/Bridge/Matrix/MatrixOptions.php b/src/Symfony/Component/Notifier/Bridge/Matrix/MatrixOptions.php new file mode 100644 index 0000000000000..23a4afc619879 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Matrix/MatrixOptions.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\Notifier\Bridge\Matrix; + +use Symfony\Component\Notifier\Message\MessageOptionsInterface; + +/** + * @author Frank Schulze + */ +final class MatrixOptions implements MessageOptionsInterface, \JsonSerializable +{ + public function __construct( + private array $options = [], + ) { + } + + public function toArray(): array + { + return $this->options; + } + + public function getRecipientId(): ?string + { + return $this->options['recipient_id'] ?? null; + } + + public function jsonSerialize(): mixed + { + return $this->options; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Matrix/MatrixTransport.php b/src/Symfony/Component/Notifier/Bridge/Matrix/MatrixTransport.php new file mode 100644 index 0000000000000..d1290574d0646 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Matrix/MatrixTransport.php @@ -0,0 +1,179 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\Matrix; + +use Symfony\Component\Notifier\Exception\LogicException; +use Symfony\Component\Notifier\Exception\TransportException; +use Symfony\Component\Notifier\Exception\UnsupportedMessageTypeException; +use Symfony\Component\Notifier\Exception\UnsupportedOptionsException; +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\Component\Uid\Uuid; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; +use Symfony\Contracts\HttpClient\HttpClientInterface; +use Symfony\Contracts\HttpClient\ResponseInterface; + +/** + * @author Frank Schulze + */ +final class MatrixTransport extends AbstractTransport +{ + // not all Message Types are supported by Matrix API + private const SUPPORTED_MSG_TYPES_BY_API = ['m.text', 'm.emote', 'm.notice', 'm.image', 'm.file', 'm.audio', 'm.video', 'm.key.verification']; + + public function __construct( + #[\SensitiveParameter] private string $accessToken, + private bool $ssl, + ?HttpClientInterface $client = null, + ?EventDispatcherInterface $dispatcher = null, + ) { + parent::__construct($client, $dispatcher); + } + + public function __toString(): string + { + return \sprintf('matrix://%s', $this->getEndpoint(false)); + } + + public function supports(MessageInterface $message): bool + { + return $message instanceof ChatMessage && (null === $message->getOptions() || $message->getOptions() instanceof MatrixOptions); + } + + protected function doSend(MessageInterface $message): SentMessage + { + if (!$message instanceof ChatMessage) { + throw new UnsupportedMessageTypeException(__CLASS__, ChatMessage::class, $message); + } + + if (($opts = $message->getOptions()) && !$message->getOptions() instanceof MatrixOptions) { + throw new UnsupportedOptionsException(__CLASS__, MatrixOptions::class, $opts); + } + + $options = $opts ? $opts->toArray() : []; + + $options['msgtype'] = $options['msgtype'] ?? 'm.text'; + + if (!\in_array($options['msgtype'], self::SUPPORTED_MSG_TYPES_BY_API, true)) { + throw new LogicException(\sprintf('Unsupported message type: "%s". Only "%s" are supported by Matrix Client-Server API v3.', $options['msgtype'], implode(', ', self::SUPPORTED_MSG_TYPES_BY_API))); + } + + if (null === $message->getRecipientId()) { + throw new LogicException('Recipient id is required.'); + } + + $recipient = match ($message->getRecipientId()[0]) { + '@' => $this->getDirectMessageChannel($message->getRecipientId()), + '!' => $message->getRecipientId(), + '#' => $this->getRoomFromAlias($message->getRecipientId()), + default => throw new LogicException(\sprintf('Only recipients starting with "!","@","#" are supported ("%s" given).', $message->getRecipientId()[0])), + }; + + $options['body'] = $message->getSubject(); + if ('org.matrix.custom.html' === $options['format']) { + $options['formatted_body'] = $message->getSubject(); + $options['body'] = strip_tags($message->getSubject()); + } + + $response = $this->request('PUT', \sprintf('/_matrix/client/v3/rooms/%s/send/%s/%s', $recipient, 'm.room.message', Uuid::v4()), ['json' => $options]); + + $success = $response->toArray(false); + $sentMessage = new SentMessage($message, (string) $this); + $sentMessage->setMessageId($success['event_id']); + + return $sentMessage; + } + + protected function getEndpoint(bool $full = false): string + { + return rtrim(($full ? $this->getScheme().'://' : '').$this->host.($this->port ? ':'.$this->port : ''), '/'); + } + + private function getRoomFromAlias(string $alias): string + { + $response = $this->request('GET', \sprintf('/_matrix/client/v3/directory/room/%s', urlencode($alias))); + + return $response->toArray()['room_id']; + } + + private function createPrivateChannel(string $recipientId): ?array + { + $invites[] = $recipientId; + $response = $this->request('POST', '/_matrix/client/v3/createRoom', ['json' => ['creation_content' => ['m.federate' => false], 'is_direct' => true, 'preset' => 'trusted_private_chat', 'invite' => $invites]]); + + return $response->toArray(); + } + + private function getDirectMessageChannel(string $recipientId): ?string + { + $response = $this->getAccountData($this->getWhoami()['user_id'], 'm.direct'); + if (!isset($response[$recipientId])) { + $roomid = $this->createPrivateChannel($recipientId)['room_id']; + $response[$recipientId] = [$roomid]; + $this->updateAccountData($this->getWhoami()['user_id'], 'm.direct', $response); + + return $roomid; + } + + return $response[$recipientId][0]; + } + + private function updateAccountData(string $userId, string $type, array $data): void + { + $response = $this->request('PUT', \sprintf('/_matrix/client/v3/user/%s/account_data/%s', urlencode($userId), $type), ['json' => $data]); + if ([] !== $response->toArray()) { + throw new TransportException('Unable to update account data.', $response); + } + } + + private function getAccountData(string $userId, string $type): ?array + { + $response = $this->request('GET', \sprintf('/_matrix/client/v3/user/%s/account_data/%s', urlencode($userId), $type)); + + return $response->toArray(); + } + + private function getWhoami(): ?array + { + $response = $this->request('GET', '/_matrix/client/v3/account/whoami'); + + return $response->toArray(); + } + + private function getScheme(): string + { + return $this->ssl ? 'https' : 'http'; + } + + private function request(string $method, string $uri, ?array $options = []): ResponseInterface + { + $options += [ + 'auth_bearer' => $this->accessToken, + ]; + $response = $this->client->request($method, $this->getEndpoint(true).$uri, $options); + + try { + $statusCode = $response->getStatusCode(); + } catch (TransportException $e) { + throw new TransportException('Could not reach the Matrix server.', $response, 0, $e); + } + + if (\in_array($statusCode, [400, 403, 405])) { + $result = $response->toArray(false); + throw new TransportException(\sprintf('Error: Matrix responded with "%s (%s)"', $result['error'], $result['errcode']), $response); + } + + return $response; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Matrix/MatrixTransportFactory.php b/src/Symfony/Component/Notifier/Bridge/Matrix/MatrixTransportFactory.php new file mode 100644 index 0000000000000..33f47626ddfdc --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Matrix/MatrixTransportFactory.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\Matrix; + +use Symfony\Component\Notifier\Exception\UnsupportedSchemeException; +use Symfony\Component\Notifier\Transport\AbstractTransportFactory; +use Symfony\Component\Notifier\Transport\Dsn; + +/** + * @author Frank Schulze + */ +class MatrixTransportFactory extends AbstractTransportFactory +{ + public function create(Dsn $dsn): MatrixTransport + { + if ('matrix' !== $dsn->getScheme()) { + throw new UnsupportedSchemeException($dsn, 'matrix', $this->getSupportedSchemes()); + } + + $token = $dsn->getRequiredOption('accessToken'); + $host = $dsn->getHost(); + $port = $dsn->getPort(); + $ssl = $dsn->getBooleanOption('ssl', true); + + return (new MatrixTransport($token, $ssl, $this->client, $this->dispatcher))->setHost($host)->setPort($port); + } + + protected function getSupportedSchemes(): array + { + return ['matrix']; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Matrix/README.md b/src/Symfony/Component/Notifier/Bridge/Matrix/README.md new file mode 100644 index 0000000000000..52e56111ccaa9 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Matrix/README.md @@ -0,0 +1,29 @@ +Matrix Notifier +=============== + +Provides [Matrix](https://matrix.org) integration for Symfony Notifier. +It uses the [Matrix Client-Server API](https://spec.matrix.org/v1.13/client-server-api/). + +``` +Note: +This Bridge was tested with the official Matrix Synapse Server and the Client-Server API v3 (version v1.13). +But it should work with every Matrix Client-Server API v3 compliant homeserver. +``` + +DSN example +----------- + +``` +MATRIX_DSN=matrix://HOST:PORT/?accessToken=ACCESS_TOKEN&ssl=true +``` +To get started you need an access token. The simplest way to get that is to open Element in a private (incognito) window in your webbrowser or just use your currently open Element. Go to Settings > Help & About > Advanced > Access Token `click to reveal` and copy your access token. + +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) + * [Matrix Playground](https://playground.matrix.org) + * [Matrix Client-Server API](https://spec.matrix.org/latest/client-server-api/) diff --git a/src/Symfony/Component/Notifier/Bridge/Matrix/Tests/MatrixOptionsTest.php b/src/Symfony/Component/Notifier/Bridge/Matrix/Tests/MatrixOptionsTest.php new file mode 100644 index 0000000000000..9c4a09552bc13 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Matrix/Tests/MatrixOptionsTest.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\Matrix\Tests; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Notifier\Bridge\Matrix\MatrixOptions; + +class MatrixOptionsTest extends TestCase +{ + public function testToArray() + { + $options = new MatrixOptions([ + 'recipient_id' => '@testuser:matrix.io', + 'msgtype' => 'm.text', + 'format' => 'org.matrix.custom.html', + ]); + $this->assertSame(['recipient_id' => '@testuser:matrix.io', 'msgtype' => 'm.text', 'format' => 'org.matrix.custom.html'], $options->toArray()); + } + + public function testGetRecipientId() + { + $options = new MatrixOptions([ + 'recipient_id' => '@testuser:matrix.io', + ]); + $this->assertSame('@testuser:matrix.io', $options->getRecipientId()); + } + + public function testJsonSerialize() + { + $options = new MatrixOptions([ + 'recipient_id' => '@testuser:matrix.io', + 'msgtype' => 'm.text', + 'format' => 'org.matrix.custom.html', + ]); + $this->assertSame(['recipient_id' => '@testuser:matrix.io', 'msgtype' => 'm.text', 'format' => 'org.matrix.custom.html'], $options->jsonSerialize()); + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Matrix/Tests/MatrixTransportFactoryTest.php b/src/Symfony/Component/Notifier/Bridge/Matrix/Tests/MatrixTransportFactoryTest.php new file mode 100644 index 0000000000000..3090583e20f3c --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Matrix/Tests/MatrixTransportFactoryTest.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\Matrix\Tests; + +use Symfony\Component\Notifier\Bridge\Matrix\MatrixTransportFactory; +use Symfony\Component\Notifier\Test\AbstractTransportFactoryTestCase; +use Symfony\Component\Notifier\Test\IncompleteDsnTestTrait; + +final class MatrixTransportFactoryTest extends AbstractTransportFactoryTestCase +{ + use IncompleteDsnTestTrait; + + public function createFactory(): MatrixTransportFactory + { + return new MatrixTransportFactory(); + } + + public static function createProvider(): iterable + { + yield [ + 'matrix://host.test', + 'matrix://host.test/?accessToken=1234', + 'matrix://host.test:8008/?accessToken=1234', + 'matrix://host.test:8008/?accessToken=1234&ssl=true', + 'matrix://host.test/?ssl=true', + ]; + } + + public static function incompleteDsnProvider(): iterable + { + yield 'missing api key' => ['matrix://host.test']; + yield 'invalid api key' => ['matrix://host.test/?ssl=true']; + } + + public static function supportsProvider(): iterable + { + yield [true, 'matrix://host.test/?accessToken=1234']; + yield [true, 'matrix://host.test:8008/?accessToken=1234']; + yield [true, 'matrix://host.test:8008/?accessToken=1234&ssl=true']; + yield [false, 'somethingElse://apiKey@default?from=TEST']; + } + + public static function unsupportedSchemeProvider(): iterable + { + yield ['somethingElse://apiKey@default?from=FROM']; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Matrix/Tests/MatrixTransportTest.php b/src/Symfony/Component/Notifier/Bridge/Matrix/Tests/MatrixTransportTest.php new file mode 100644 index 0000000000000..f6e0064357411 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Matrix/Tests/MatrixTransportTest.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\Matrix\Tests; + +use Symfony\Component\HttpClient\MockHttpClient; +use Symfony\Component\Notifier\Bridge\Matrix\MatrixOptions; +use Symfony\Component\Notifier\Bridge\Matrix\MatrixTransport; +use Symfony\Component\Notifier\Exception\LogicException; +use Symfony\Component\Notifier\Message\ChatMessage; +use Symfony\Component\Notifier\Message\SmsMessage; +use Symfony\Component\Notifier\Test\TransportTestCase; +use Symfony\Component\Notifier\Tests\Transport\DummyMessage; +use Symfony\Contracts\HttpClient\HttpClientInterface; + +final class MatrixTransportTest extends TransportTestCase +{ + public static function createTransport(?HttpClientInterface $client = null, ?bool $ssl = false): MatrixTransport + { + return new MatrixTransport('apiKey', $ssl, $client ?? new MockHttpClient()); + } + + public static function toStringProvider(): iterable + { + yield ['matrix://', self::createTransport(null, true)]; + } + + public static function supportedMessagesProvider(): iterable + { + yield [new ChatMessage('Hello!', new MatrixOptions(['recipient_id' => '#testchannelalias:matrix.io', 'format' => 'org.matrix.custom.html']))]; + } + + public static function unsupportedMessagesProvider(): iterable + { + yield [new SmsMessage('0611223344', 'Hello!')]; + yield [new DummyMessage()]; + } + + public function testUnsupportedRecipients() + { + $transport = self::createTransport(); + $this->expectException(LogicException::class); + $this->expectExceptionMessage('Only recipients starting with "!","@","#" are supported ("+" given).'); + $transport->send(new ChatMessage('Hello!', new MatrixOptions(['recipient_id' => '+testchannelalias:matrix.io']))); + } + + public function testUnsupportedMsgType() + { + $transport = self::createTransport(); + $this->expectException(LogicException::class); + $transport->send(new ChatMessage('Hello!', new MatrixOptions(['recipient_id' => '@user:matrix.io', 'msgtype' => 'm.anything']))); + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Matrix/composer.json b/src/Symfony/Component/Notifier/Bridge/Matrix/composer.json new file mode 100644 index 0000000000000..22ce807af3364 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Matrix/composer.json @@ -0,0 +1,33 @@ +{ + "name": "symfony/matrix-notifier", + "type": "symfony-notifier-bridge", + "description": "Symfony Matrix Notifier Bridge", + "keywords": ["matrix", "notifier"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Frank Schulze", + "email": "frank@akiber.de" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=8.2", + "symfony/notifier": "^7.3", + "symfony/uid": "^7.2", + "symfony/http-client": "^6.4|^7.0" + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Notifier\\Bridge\\Matrix\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev" +} diff --git a/src/Symfony/Component/Notifier/Bridge/Matrix/phpunit.xml.dist b/src/Symfony/Component/Notifier/Bridge/Matrix/phpunit.xml.dist new file mode 100644 index 0000000000000..0fdbe6b1304a9 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Matrix/phpunit.xml.dist @@ -0,0 +1,31 @@ + + + + + + + + + + ./Tests/ + + + + + + ./ + + + ./Resources + ./Tests + ./vendor + + + diff --git a/src/Symfony/Component/Notifier/Exception/UnsupportedSchemeException.php b/src/Symfony/Component/Notifier/Exception/UnsupportedSchemeException.php index 95c7a04fed30a..b1b6fbcf1bd36 100644 --- a/src/Symfony/Component/Notifier/Exception/UnsupportedSchemeException.php +++ b/src/Symfony/Component/Notifier/Exception/UnsupportedSchemeException.php @@ -148,6 +148,10 @@ class UnsupportedSchemeException extends LogicException 'class' => Bridge\Mastodon\MastodonTransportFactory::class, 'package' => 'symfony/mastodon-notifier', ], + 'matrix' => [ + 'class' => Bridge\Matrix\MatrixTransportFactory::class, + 'package' => 'symfony/matrix-notifier', + ], 'mattermost' => [ 'class' => Bridge\Mattermost\MattermostTransportFactory::class, 'package' => 'symfony/mattermost-notifier', diff --git a/src/Symfony/Component/Notifier/Tests/Exception/UnsupportedSchemeExceptionTest.php b/src/Symfony/Component/Notifier/Tests/Exception/UnsupportedSchemeExceptionTest.php index f772fbe04227b..7170a3492ca9b 100644 --- a/src/Symfony/Component/Notifier/Tests/Exception/UnsupportedSchemeExceptionTest.php +++ b/src/Symfony/Component/Notifier/Tests/Exception/UnsupportedSchemeExceptionTest.php @@ -58,6 +58,7 @@ public static function setUpBeforeClass(): void Bridge\Lox24\Lox24TransportFactory::class => false, Bridge\Mailjet\MailjetTransportFactory::class => false, Bridge\Mastodon\MastodonTransportFactory::class => false, + Bridge\Matrix\MatrixTransportFactory::class => false, Bridge\Mattermost\MattermostTransportFactory::class => false, Bridge\Mercure\MercureTransportFactory::class => false, Bridge\MessageBird\MessageBirdTransportFactory::class => false, @@ -155,6 +156,7 @@ public static function messageWhereSchemeIsPartOfSchemeToPackageMapProvider(): \ yield ['lox24', 'symfony/lox24-notifier']; yield ['mailjet', 'symfony/mailjet-notifier']; yield ['mastodon', 'symfony/mastodon-notifier']; + yield ['matrix', 'symfony/matrix-notifier']; yield ['mattermost', 'symfony/mattermost-notifier']; yield ['mercure', 'symfony/mercure-notifier']; yield ['messagebird', 'symfony/message-bird-notifier']; diff --git a/src/Symfony/Component/Notifier/Transport.php b/src/Symfony/Component/Notifier/Transport.php index a023ed6654443..c3c8d4a567961 100644 --- a/src/Symfony/Component/Notifier/Transport.php +++ b/src/Symfony/Component/Notifier/Transport.php @@ -60,6 +60,7 @@ final class Transport Bridge\Lox24\Lox24TransportFactory::class, Bridge\Mailjet\MailjetTransportFactory::class, Bridge\Mastodon\MastodonTransportFactory::class, + Bridge\Matrix\MatrixTransportFactory::class, Bridge\Mattermost\MattermostTransportFactory::class, Bridge\Mercure\MercureTransportFactory::class, Bridge\MessageBird\MessageBirdTransportFactory::class,