diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 3a780a8eaa35c..a5d1286f6e3db 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -2741,6 +2741,7 @@ private function registerNotifierConfiguration(array $config, ContainerBuilder $ NotifierBridge\MessageMedia\MessageMediaTransportFactory::class => 'notifier.transport_factory.message-media', NotifierBridge\MicrosoftTeams\MicrosoftTeamsTransportFactory::class => 'notifier.transport_factory.microsoft-teams', NotifierBridge\Mobyt\MobytTransportFactory::class => 'notifier.transport_factory.mobyt', + NotifierBridge\Novu\NovuTransportFactory::class => 'notifier.transport_factory.novu', NotifierBridge\Octopush\OctopushTransportFactory::class => 'notifier.transport_factory.octopush', NotifierBridge\OneSignal\OneSignalTransportFactory::class => 'notifier.transport_factory.one-signal', NotifierBridge\OrangeSms\OrangeSmsTransportFactory::class => 'notifier.transport_factory.orange-sms', diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php index 1573aa0bea0a4..5dc43096c0ba6 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php @@ -279,13 +279,17 @@ ->set('notifier.transport_factory.simple-textin', Bridge\SimpleTextin\SimpleTextinTransportFactory::class) ->parent('notifier.transport_factory.abstract') ->tag('texter.transport_factory') - + ->set('notifier.transport_factory.click-send', Bridge\ClickSend\ClickSendTransportFactory::class) ->parent('notifier.transport_factory.abstract') ->tag('texter.transport_factory') - + ->set('notifier.transport_factory.smsmode', Bridge\Smsmode\SmsmodeTransportFactory::class) ->parent('notifier.transport_factory.abstract') ->tag('texter.transport_factory') + + ->set('notifier.transport_factory.novu', Bridge\Novu\NovuTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('texter.transport_factory') ; }; diff --git a/src/Symfony/Component/Notifier/Bridge/Novu/.gitattributes b/src/Symfony/Component/Notifier/Bridge/Novu/.gitattributes new file mode 100644 index 0000000000000..84c7add058fb5 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Novu/.gitattributes @@ -0,0 +1,4 @@ +/Tests export-ignore +/phpunit.xml.dist export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore diff --git a/src/Symfony/Component/Notifier/Bridge/Novu/.gitignore b/src/Symfony/Component/Notifier/Bridge/Novu/.gitignore new file mode 100644 index 0000000000000..c49a5d8df5c65 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Novu/.gitignore @@ -0,0 +1,3 @@ +vendor/ +composer.lock +phpunit.xml diff --git a/src/Symfony/Component/Notifier/Bridge/Novu/CHANGELOG.md b/src/Symfony/Component/Notifier/Bridge/Novu/CHANGELOG.md new file mode 100644 index 0000000000000..7e873f81cb0fe --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Novu/CHANGELOG.md @@ -0,0 +1,7 @@ +CHANGELOG +========= + +6.4 +--- + +* Add the bridge diff --git a/src/Symfony/Component/Notifier/Bridge/Novu/LICENSE b/src/Symfony/Component/Notifier/Bridge/Novu/LICENSE new file mode 100644 index 0000000000000..3ed9f412ce53d --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Novu/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2023-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/Novu/NovuOptions.php b/src/Symfony/Component/Notifier/Bridge/Novu/NovuOptions.php new file mode 100644 index 0000000000000..16bd950096062 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Novu/NovuOptions.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\Novu; + +use Symfony\Component\Notifier\Message\MessageOptionsInterface; + +/** + * @author Wouter van der Loop + */ +class NovuOptions implements MessageOptionsInterface +{ + public function __construct( + private readonly string|null $subscriberId = null, + private readonly string|null $firstName = null, + private readonly string|null $lastName = null, + private readonly string|null $email = null, + private readonly string|null $phone = null, + private readonly string|null $avatar = null, + private readonly string|null $locale = null, + private readonly array $options = [], + ) { + } + + public function toArray(): array + { + return array_merge($this->options, [ + 'firstName' => $this->firstName, + 'lastName' => $this->lastName, + 'email' => $this->email, + 'phone' => $this->phone, + 'avatar' => $this->avatar, + 'locale' => $this->locale, + ]); + } + + public function getRecipientId(): ?string + { + return $this->subscriberId ?? null; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Novu/NovuSubscriberRecipient.php b/src/Symfony/Component/Notifier/Bridge/Novu/NovuSubscriberRecipient.php new file mode 100644 index 0000000000000..027c39876e017 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Novu/NovuSubscriberRecipient.php @@ -0,0 +1,66 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\Novu; + +use Symfony\Component\Notifier\Recipient\RecipientInterface; + +/** + * @author Wouter van der Loop + */ +class NovuSubscriberRecipient implements RecipientInterface +{ + public function __construct( + private readonly string $subscriberId, + private readonly string|null $firstName = null, + private readonly string|null $lastName = null, + private readonly string|null $email = null, + private readonly string|null $phone = null, + private readonly string|null $avatar = null, + private readonly string|null $locale = null, + ) { + } + + public function getSubscriberId(): string + { + return $this->subscriberId; + } + + public function getFirstName(): ?string + { + return $this->firstName; + } + + public function getLastName(): ?string + { + return $this->lastName; + } + + public function getEmail(): ?string + { + return $this->email; + } + + public function getPhone(): ?string + { + return $this->phone; + } + + public function getAvatar(): ?string + { + return $this->avatar; + } + + public function getLocale(): ?string + { + return $this->locale; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Novu/NovuTransport.php b/src/Symfony/Component/Notifier/Bridge/Novu/NovuTransport.php new file mode 100644 index 0000000000000..dc6b7a46e9ad8 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Novu/NovuTransport.php @@ -0,0 +1,97 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\Novu; + +use Symfony\Component\Notifier\Exception\TransportException; +use Symfony\Component\Notifier\Exception\TransportExceptionInterface; +use Symfony\Component\Notifier\Exception\UnsupportedMessageTypeException; +use Symfony\Component\Notifier\Message\MessageInterface; +use Symfony\Component\Notifier\Message\PushMessage; +use Symfony\Component\Notifier\Message\SentMessage; +use Symfony\Component\Notifier\Transport\AbstractTransport; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; +use Symfony\Contracts\HttpClient\HttpClientInterface; + +/** + * @author Wouter van der Loop + */ +class NovuTransport extends AbstractTransport +{ + protected const HOST = 'web.novu.co'; + + public function __construct( + #[\SensitiveParameter] + protected string $apiKey, + HttpClientInterface $client = null, + EventDispatcherInterface $dispatcher = null + ) { + $this->apiKey = $apiKey; + parent::__construct($client, $dispatcher); + } + + public function __toString(): string + { + return sprintf('novu://%s', $this->getEndpoint()); + } + + public function supports(MessageInterface $message): bool + { + return $message instanceof PushMessage && (null === $message->getOptions() || $message->getOptions() instanceof NovuOptions); + } + + protected function doSend(MessageInterface $message): SentMessage + { + if (!$message instanceof PushMessage) { + throw new UnsupportedMessageTypeException(__CLASS__, PushMessage::class, $message); + } + + $options = $message->getOptions()?->toArray() ?? []; + + $body = [ + 'name' => $message->getSubject(), + 'to' => [ + 'subscriberId' => $message->getRecipientId(), + 'firstName' => $options['firstName'], + 'lastName' => $options['lastName'], + 'email' => $options['email'], + 'phone' => $options['phone'], + 'avatar' => $options['avatar'], + 'locale' => $options['locale'], + ], + 'payload' => json_decode($message->getContent()), + ]; + + $endpoint = sprintf('https://%s/v1/events/trigger', $this->getEndpoint()); + $response = $this->client->request('POST', $endpoint, [ + 'body' => $body, + 'headers' => [ + 'Authorization' => sprintf('ApiKey %s', $this->apiKey), + 'Content-Type' => 'application/json', + ], + ]); + + try { + $statusCode = $response->getStatusCode(); + } catch (TransportExceptionInterface $e) { + throw new TransportException('Could not reach the remote Novu server.', $response, 0, $e); + } + + if (201 !== $statusCode) { + $originalContent = $message->getSubject(); + $result = $response->toArray(false); + $error = $result['message']; + throw new TransportException(sprintf('Unable to post the Novu message: "%s" (%d: "%s").', $originalContent, $statusCode, $error), $response); + } + + return new SentMessage($message, (string) $this); + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Novu/NovuTransportFactory.php b/src/Symfony/Component/Notifier/Bridge/Novu/NovuTransportFactory.php new file mode 100644 index 0000000000000..09310cb4446cc --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Novu/NovuTransportFactory.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\Novu; + +use Symfony\Component\Notifier\Exception\UnsupportedSchemeException; +use Symfony\Component\Notifier\Transport\AbstractTransportFactory; +use Symfony\Component\Notifier\Transport\Dsn; + +/** + * @author Wouter van der Loop + */ +class NovuTransportFactory extends AbstractTransportFactory +{ + private const SCHEME = 'novu'; + + protected function getSupportedSchemes(): array + { + return [self::SCHEME]; + } + + public function create(Dsn $dsn): NovuTransport + { + $scheme = $dsn->getScheme(); + if (self::SCHEME !== $scheme) { + throw new UnsupportedSchemeException($dsn, self::SCHEME, $this->getSupportedSchemes()); + } + + $key = $this->getUser($dsn); + $host = 'default' === $dsn->getHost() ? null : $dsn->getHost(); + $port = $dsn->getPort(); + + return (new NovuTransport($key, $this->client, $this->dispatcher))->setHost($host)->setPort($port); + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Novu/README.md b/src/Symfony/Component/Notifier/Bridge/Novu/README.md new file mode 100644 index 0000000000000..9dd92568e1176 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Novu/README.md @@ -0,0 +1,73 @@ +Novu Notifier +============= + +Provides [Novu](https://novu.co/) integration for Symfony Notifier. + +DSN example +----------- + +``` +NOVU_DSN=novu://API_KEY@default +``` + +Notification example +-------------------- + +```php +class NovuNotification extends Notification implements PushNotificationInterface +{ + public function asPushMessage( + NovuSubscriberRecipient|RecipientInterface $recipient, + ?string $transport = null, + ): ?PushMessage { + return new PushMessage( + $this->getSubject(), + $this->getContent(), + new NovuOptions( + $recipient->getSubscriberId(), + $recipient->getFirstName(), + $recipient->getLastName(), + $recipient->getEmail(), + $recipient->getPhone(), + $recipient->getAvatar(), + $recipient->getLocale(), + [], + ), + ); + } +} +``` + +```php +$notification = new NovuNotification; +$notification->subject('test'); +$notification->channels(['push']); +$notification->content( + json_encode( + [ + 'param1' => 'Lorum Ipsum', + ] + ) +); + +$this->notifier->send( + $notification, + new NovuSubscriberRecipient( + "123", + 'Wouter', + 'van der Loop', + 'woutervdl@toppy.nl', + null, + null, + null, + ), +); +``` + +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/Notifier/Bridge/Novu/Tests/NovuOptionsTest.php b/src/Symfony/Component/Notifier/Bridge/Novu/Tests/NovuOptionsTest.php new file mode 100644 index 0000000000000..6d1125bf18bb0 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Novu/Tests/NovuOptionsTest.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\Novu\Tests; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Notifier\Bridge\Novu\NovuOptions; + +class NovuOptionsTest extends TestCase +{ + /** + * @group legacy + */ + public function testToArray() + { + $options = new NovuOptions( + 123, + 'Joe', + 'Smith', + 'test@example.com', + null, + null, + null, + [], + ); + + $this->assertSame( + [ + 'firstName' => 'Joe', + 'lastName' => 'Smith', + 'email' => 'test@example.com', + 'phone' => null, + 'avatar' => null, + 'locale' => null, + ], + $options->toArray() + ); + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Novu/Tests/NovuTransportFactoryTest.php b/src/Symfony/Component/Notifier/Bridge/Novu/Tests/NovuTransportFactoryTest.php new file mode 100644 index 0000000000000..99044057ce68a --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Novu/Tests/NovuTransportFactoryTest.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\Novu\Tests; + +use Symfony\Component\Notifier\Bridge\Novu\NovuTransportFactory; +use Symfony\Component\Notifier\Test\TransportFactoryTestCase; +use Symfony\Component\Notifier\Transport\TransportFactoryInterface; + +class NovuTransportFactoryTest extends TransportFactoryTestCase +{ + public function createFactory(): TransportFactoryInterface + { + return new NovuTransportFactory(); + } + + public static function supportsProvider(): iterable + { + yield [true, 'novu://host']; + yield [false, 'somethingElse://host']; + } + + public static function createProvider(): iterable + { + yield [ + 'novu://host.test', + 'novu://token@host.test', + ]; + } + + public static function incompleteDsnProvider(): iterable + { + yield 'missing key' => ['novu://host.test']; + } + + public static function unsupportedSchemeProvider(): iterable + { + yield ['somethingElse://token@host']; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Novu/Tests/NovuTransportTest.php b/src/Symfony/Component/Notifier/Bridge/Novu/Tests/NovuTransportTest.php new file mode 100644 index 0000000000000..b94183e4f464c --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Novu/Tests/NovuTransportTest.php @@ -0,0 +1,68 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\Novu\Tests; + +use Symfony\Component\HttpClient\MockHttpClient; +use Symfony\Component\Notifier\Bridge\Novu\NovuOptions; +use Symfony\Component\Notifier\Bridge\Novu\NovuTransport; +use Symfony\Component\Notifier\Exception\TransportException; +use Symfony\Component\Notifier\Message\PushMessage; +use Symfony\Component\Notifier\Message\SmsMessage; +use Symfony\Component\Notifier\Test\TransportTestCase; +use Symfony\Component\Notifier\Tests\Transport\DummyMessage; +use Symfony\Component\Notifier\Transport\TransportInterface; +use Symfony\Contracts\HttpClient\HttpClientInterface; +use Symfony\Contracts\HttpClient\ResponseInterface; + +class NovuTransportTest extends TransportTestCase +{ + public static function createTransport(HttpClientInterface $client = null): TransportInterface + { + return (new NovuTransport('9c9ced75881ddc65c033273f466b42d1', $client ?? new MockHttpClient()))->setHost('host.test'); + } + + public static function toStringProvider(): iterable + { + yield ['novu://host.test', self::createTransport()]; + } + + public static function supportedMessagesProvider(): iterable + { + yield [new PushMessage('test', '{}', new NovuOptions(123, null, null, 'test@example.com', null, null, null, []))]; + } + + public static function unsupportedMessagesProvider(): iterable + { + yield [new SmsMessage('0611223344', 'Hello!')]; + yield [new DummyMessage()]; + } + + public function testWithErrorResponseThrows() + { + $response = $this->createMock(ResponseInterface::class); + $response->expects($this->exactly(2)) + ->method('getStatusCode') + ->willReturn(400); + $response->expects($this->once()) + ->method('getContent') + ->willReturn(json_encode(['error' => 'Bad request', 'message' => 'subscriberId under property to is not configured'])); + + $client = new MockHttpClient(static fn (): ResponseInterface => $response); + + $transport = $this->createTransport($client); + + $this->expectException(TransportException::class); + $this->expectExceptionMessageMatches('/400: "subscriberId under property to is not configured"/'); + + $transport->send(new PushMessage('test', '{}', new NovuOptions(123, null, null, 'test@example.com', null, null, null, []))); + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Novu/composer.json b/src/Symfony/Component/Notifier/Bridge/Novu/composer.json new file mode 100644 index 0000000000000..539bd870cef9f --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Novu/composer.json @@ -0,0 +1,30 @@ +{ + "name": "symfony/novu-notifier", + "type": "symfony-notifier-bridge", + "description": "Provides Novu integration for Symfony Notifier.", + "keywords": ["novu", "notifier"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Wouter van der Loop", + "email": "woutervdl@toppy.nl" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=8.1", + "symfony/http-client": "^5.4|^6.0|^7.0", + "symfony/notifier": "^6.4" + }, + "autoload": { + "psr-4": { "Symfony\\Component\\Notifier\\Bridge\\Novu\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev" +} diff --git a/src/Symfony/Component/Notifier/Bridge/Novu/phpunit.xml.dist b/src/Symfony/Component/Notifier/Bridge/Novu/phpunit.xml.dist new file mode 100644 index 0000000000000..36bca0d250731 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Novu/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 b11264ce610cf..7a3413aee69f3 100644 --- a/src/Symfony/Component/Notifier/Exception/UnsupportedSchemeException.php +++ b/src/Symfony/Component/Notifier/Exception/UnsupportedSchemeException.php @@ -152,6 +152,10 @@ class UnsupportedSchemeException extends LogicException 'class' => Bridge\Mobyt\MobytTransportFactory::class, 'package' => 'symfony/mobyt-notifier', ], + 'novu' => [ + 'class' => Bridge\Novu\NovuTransportFactory::class, + 'package' => 'symfony/novu-notifier', + ], 'octopush' => [ 'class' => Bridge\Octopush\OctopushTransportFactory::class, 'package' => 'symfony/octopush-notifier', diff --git a/src/Symfony/Component/Notifier/Tests/Exception/UnsupportedSchemeExceptionTest.php b/src/Symfony/Component/Notifier/Tests/Exception/UnsupportedSchemeExceptionTest.php index d211d15422e8f..2edb9ae59f1c1 100644 --- a/src/Symfony/Component/Notifier/Tests/Exception/UnsupportedSchemeExceptionTest.php +++ b/src/Symfony/Component/Notifier/Tests/Exception/UnsupportedSchemeExceptionTest.php @@ -60,6 +60,7 @@ public static function setUpBeforeClass(): void Bridge\MessageMedia\MessageMediaTransportFactory::class => false, Bridge\MicrosoftTeams\MicrosoftTeamsTransportFactory::class => false, Bridge\Mobyt\MobytTransportFactory::class => false, + Bridge\Novu\NovuTransportFactory::class => false, Bridge\Octopush\OctopushTransportFactory::class => false, Bridge\OneSignal\OneSignalTransportFactory::class => false, Bridge\OrangeSms\OrangeSmsTransportFactory::class => false, @@ -138,6 +139,7 @@ public static function messageWhereSchemeIsPartOfSchemeToPackageMapProvider(): \ yield ['messagemedia', 'symfony/message-media-notifier']; yield ['microsoftteams', 'symfony/microsoft-teams-notifier']; yield ['mobyt', 'symfony/mobyt-notifier']; + yield ['novu', 'symfony/novu-notifier']; yield ['octopush', 'symfony/octopush-notifier']; yield ['onesignal', 'symfony/one-signal-notifier']; yield ['ovhcloud', 'symfony/ovh-cloud-notifier']; diff --git a/src/Symfony/Component/Notifier/Transport.php b/src/Symfony/Component/Notifier/Transport.php index db21d16134598..0428e2bba3538 100644 --- a/src/Symfony/Component/Notifier/Transport.php +++ b/src/Symfony/Component/Notifier/Transport.php @@ -62,6 +62,7 @@ final class Transport Bridge\MessageMedia\MessageMediaTransportFactory::class, Bridge\MicrosoftTeams\MicrosoftTeamsTransportFactory::class, Bridge\Mobyt\MobytTransportFactory::class, + Bridge\Novu\NovuTransportFactory::class, Bridge\Octopush\OctopushTransportFactory::class, Bridge\OneSignal\OneSignalTransportFactory::class, Bridge\OrangeSms\OrangeSmsTransportFactory::class,