diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index ea76107b01440..fe46f022de7e1 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -124,6 +124,7 @@ use Symfony\Component\Mime\MimeTypes; use Symfony\Component\Notifier\Bridge\AllMySms\AllMySmsTransportFactory; use Symfony\Component\Notifier\Bridge\AmazonSns\AmazonSnsTransportFactory; +use Symfony\Component\Notifier\Bridge\Chatwork\ChatworkTransportFactory; use Symfony\Component\Notifier\Bridge\Clickatell\ClickatellTransportFactory; use Symfony\Component\Notifier\Bridge\ContactEveryone\ContactEveryoneTransportFactory; use Symfony\Component\Notifier\Bridge\Discord\DiscordTransportFactory; @@ -2564,6 +2565,7 @@ private function registerNotifierConfiguration(array $config, ContainerBuilder $ $classToServices = [ AllMySmsTransportFactory::class => 'notifier.transport_factory.all-my-sms', AmazonSnsTransportFactory::class => 'notifier.transport_factory.amazon-sns', + ChatworkTransportFactory::class => 'notifier.transport_factory.chatwork', ClickatellTransportFactory::class => 'notifier.transport_factory.clickatell', ContactEveryoneTransportFactory::class => 'notifier.transport_factory.contact-everyone', DiscordTransportFactory::class => 'notifier.transport_factory.discord', diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php index 691588d730d92..237ae18a59eb7 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php @@ -13,6 +13,7 @@ use Symfony\Component\Notifier\Bridge\AllMySms\AllMySmsTransportFactory; use Symfony\Component\Notifier\Bridge\AmazonSns\AmazonSnsTransportFactory; +use Symfony\Component\Notifier\Bridge\Chatwork\ChatworkTransportFactory; use Symfony\Component\Notifier\Bridge\Clickatell\ClickatellTransportFactory; use Symfony\Component\Notifier\Bridge\ContactEveryone\ContactEveryoneTransportFactory; use Symfony\Component\Notifier\Bridge\Discord\DiscordTransportFactory; @@ -281,5 +282,10 @@ ->set('notifier.transport_factory.zendesk', ZendeskTransportFactory::class) ->parent('notifier.transport_factory.abstract') ->tag('chatter.transport_factory') + + ->set('notifier.transport_factory.chatwork', ChatworkTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('chatter.transport_factory') + ; }; diff --git a/src/Symfony/Component/Notifier/Bridge/Chatwork/.gitattributes b/src/Symfony/Component/Notifier/Bridge/Chatwork/.gitattributes new file mode 100644 index 0000000000000..84c7add058fb5 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Chatwork/.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/Chatwork/.gitignore b/src/Symfony/Component/Notifier/Bridge/Chatwork/.gitignore new file mode 100644 index 0000000000000..c49a5d8df5c65 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Chatwork/.gitignore @@ -0,0 +1,3 @@ +vendor/ +composer.lock +phpunit.xml diff --git a/src/Symfony/Component/Notifier/Bridge/Chatwork/CHANGELOG.md b/src/Symfony/Component/Notifier/Bridge/Chatwork/CHANGELOG.md new file mode 100644 index 0000000000000..fc914543b6158 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Chatwork/CHANGELOG.md @@ -0,0 +1,7 @@ +CHANGELOG +========= + +6.2 +--- + +* Add the bridge diff --git a/src/Symfony/Component/Notifier/Bridge/Chatwork/ChatworkMessageBodyBuilder.php b/src/Symfony/Component/Notifier/Bridge/Chatwork/ChatworkMessageBodyBuilder.php new file mode 100644 index 0000000000000..c0d45ef98bb90 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Chatwork/ChatworkMessageBodyBuilder.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\Chatwork; + +/** + * @author Ippei Sumida + */ +class ChatworkMessageBodyBuilder +{ + private array $to = []; + private string $body = ''; + private bool $selfUnread = false; + + public function to(array|string $userIds): self + { + if (\is_array($userIds)) { + $this->to = $userIds; + } else { + $this->to = [$userIds]; + } + + return $this; + } + + public function body(string $body): self + { + $this->body = $body; + + return $this; + } + + public function selfUnread(bool $selfUnread): self + { + $this->selfUnread = $selfUnread; + + return $this; + } + + public function getMessageBody(): array + { + $content = ''; + foreach ($this->to as $to) { + $content .= sprintf("[To:%s]\n", $to); + } + $content .= $this->body; + + return [ + 'body' => $content, + 'self_unread' => $this->selfUnread, + ]; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Chatwork/ChatworkOptions.php b/src/Symfony/Component/Notifier/Bridge/Chatwork/ChatworkOptions.php new file mode 100644 index 0000000000000..7c6c0e164f975 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Chatwork/ChatworkOptions.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\Chatwork; + +use Symfony\Component\Notifier\Message\MessageOptionsInterface; + +/** + * @author Ippei Sumida + */ +class ChatworkOptions implements MessageOptionsInterface +{ + private array $options; + + public function __construct(array $options = []) + { + $this->options = $options; + } + + public function toArray(): array + { + return $this->options; + } + + public function getRecipientId(): ?string + { + return ''; + } + + public function to(array|string $userIds): static + { + $this->options['to'] = $userIds; + + return $this; + } + + public function selfUnread(bool $selfUnread): static + { + $this->options['selfUnread'] = $selfUnread; + + return $this; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Chatwork/ChatworkTransport.php b/src/Symfony/Component/Notifier/Bridge/Chatwork/ChatworkTransport.php new file mode 100644 index 0000000000000..2d6f61c871aa8 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Chatwork/ChatworkTransport.php @@ -0,0 +1,96 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\Chatwork; + +use Symfony\Component\Notifier\Exception\TransportException; +use Symfony\Component\Notifier\Exception\TransportExceptionInterface; +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\HttpClientInterface; + +/** + * @author Ippei Sumida + */ +class ChatworkTransport extends AbstractTransport +{ + protected const HOST = 'api.chatwork.com'; + + private string $apiToken; + private string $roomId; + + public function __construct(string $apiToken, string $roomId, HttpClientInterface $client = null, EventDispatcherInterface $dispatcher = null) + { + $this->apiToken = $apiToken; + $this->roomId = $roomId; + parent::__construct($client, $dispatcher); + } + + public function __toString(): string + { + return sprintf('chatwork://%s?room_id=%s', $this->getEndpoint(), $this->roomId); + } + + public function supports(MessageInterface $message): bool + { + return $message instanceof ChatMessage && (null === $message->getOptions() || $message->getOptions() instanceof ChatworkOptions); + } + + protected function doSend(MessageInterface $message): SentMessage + { + if (!$message instanceof ChatMessage) { + throw new UnsupportedMessageTypeException(__CLASS__, ChatMessage::class, $message); + } + + $messageOptions = $message->getOptions(); + $options = $messageOptions ? $messageOptions->toArray() : []; + + $bodyBuilder = new ChatworkMessageBodyBuilder(); + if (\array_key_exists('to', $options)) { + $bodyBuilder->to($options['to']); + } + if (\array_key_exists('selfUnread', $options)) { + $bodyBuilder->selfUnread($options['selfUnread']); + } + + $messageBody = $bodyBuilder + ->body($message->getSubject()) + ->getMessageBody(); + + $endpoint = sprintf('https://%s/v2/rooms/%s/messages', $this->getEndpoint(), $this->roomId); + $response = $this->client->request('POST', $endpoint, [ + 'body' => $messageBody, + 'headers' => [ + 'X-ChatWorkToken' => $this->apiToken, + 'Content-Type' => 'application/x-www-form-urlencoded', + ], + ]); + + try { + $statusCode = $response->getStatusCode(); + } catch (TransportExceptionInterface $e) { + throw new TransportException('Could not reach the remote Chatwork server.', $response, 0, $e); + } + + if (200 !== $statusCode) { + $originalContent = $message->getSubject(); + $result = $response->toArray(false); + $errors = $result['errors']; + throw new TransportException(sprintf('Unable to post the Chatwork message: "%s" (%d: %s).', $originalContent, $statusCode, implode(', ', $errors)), $response); + } + + return new SentMessage($message, (string) $this); + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Chatwork/ChatworkTransportFactory.php b/src/Symfony/Component/Notifier/Bridge/Chatwork/ChatworkTransportFactory.php new file mode 100644 index 0000000000000..db7f17abb9ef2 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Chatwork/ChatworkTransportFactory.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\Chatwork; + +use Symfony\Component\Notifier\Exception\UnsupportedSchemeException; +use Symfony\Component\Notifier\Transport\AbstractTransportFactory; +use Symfony\Component\Notifier\Transport\Dsn; + +/** + * @author Ippei Sumida getScheme(); + if (self::SCHEME !== $scheme) { + throw new UnsupportedSchemeException($dsn, self::SCHEME, $this->getSupportedSchemes()); + } + + $token = $this->getUser($dsn); + $roomId = $dsn->getRequiredOption('room_id'); + $host = 'default' === $dsn->getHost() ? null : $dsn->getHost(); + $port = $dsn->getPort(); + + return (new ChatworkTransport($token, $roomId, $this->client, $this->dispatcher))->setHost($host)->setPort($port); + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Chatwork/LICENSE b/src/Symfony/Component/Notifier/Bridge/Chatwork/LICENSE new file mode 100644 index 0000000000000..0ece8964f767d --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Chatwork/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2022 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/Chatwork/README.md b/src/Symfony/Component/Notifier/Bridge/Chatwork/README.md new file mode 100644 index 0000000000000..6c61e1565a0d9 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Chatwork/README.md @@ -0,0 +1,19 @@ +Chatwork Notifier +================= + +Provides [Chatwork](https://go.chatwork.com/) integration for Symfony Notifier. + +DSN example +----------- + +``` +CHATWORK_DSN=chatwork://API_TOKEN@default?room_id=ID +``` + +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/Chatwork/Tests/ChatworkMessageBodyBuilderTest.php b/src/Symfony/Component/Notifier/Bridge/Chatwork/Tests/ChatworkMessageBodyBuilderTest.php new file mode 100644 index 0000000000000..07e09150406db --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Chatwork/Tests/ChatworkMessageBodyBuilderTest.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\Chatwork\Tests; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Notifier\Bridge\Chatwork\ChatworkMessageBodyBuilder; + +class ChatworkMessageBodyBuilderTest extends TestCase +{ + public function testSetTo() + { + $builder = new ChatworkMessageBodyBuilder(); + $builder->to(['abc', 'def']); + $property = new \ReflectionProperty($builder, 'to'); + $property->setAccessible(true); + $this->assertSame(['abc', 'def'], $property->getValue($builder)); + $builder->to('ghi'); + $this->assertSame(['ghi'], $property->getValue($builder)); + } + + public function testSetSelfUnread() + { + $builder = new ChatworkMessageBodyBuilder(); + $builder->selfUnread(true); + $property = new \ReflectionProperty($builder, 'selfUnread'); + $property->setAccessible(true); + $this->assertTrue($property->getValue($builder)); + } + + public function testSetBody() + { + $builder = new ChatworkMessageBodyBuilder(); + $builder->body('test body'); + $property = new \ReflectionProperty($builder, 'body'); + $property->setAccessible(true); + $this->assertEquals('test body', $property->getValue($builder)); + } + + public function testGetMessageBody() + { + $builder = new ChatworkMessageBodyBuilder(); + $builder + ->to(['abc', 'def']) + ->selfUnread(true) + ->body('test body') + ; + + $expectedBody = << $expectedBody, + 'self_unread' => true, + ]; + $this->assertEquals($expected, $builder->getMessageBody()); + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Chatwork/Tests/ChatworkOptionsTest.php b/src/Symfony/Component/Notifier/Bridge/Chatwork/Tests/ChatworkOptionsTest.php new file mode 100644 index 0000000000000..f194b37fb7f63 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Chatwork/Tests/ChatworkOptionsTest.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\Chatwork\Tests; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Notifier\Bridge\Chatwork\ChatworkOptions; + +class ChatworkOptionsTest extends TestCase +{ + public function testSetTo() + { + $options = new ChatworkOptions(); + $options->to(['abc', 'def']); + $this->assertSame(['to' => ['abc', 'def']], $options->toArray()); + + $options->to('ghi'); + $this->assertSame(['to' => 'ghi'], $options->toArray()); + } + + public function testSetSelfUnread() + { + $options = new ChatworkOptions(); + $options->selfUnread(true); + $this->assertSame(['selfUnread' => true], $options->toArray()); + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Chatwork/Tests/ChatworkTransportFactoryTest.php b/src/Symfony/Component/Notifier/Bridge/Chatwork/Tests/ChatworkTransportFactoryTest.php new file mode 100644 index 0000000000000..421ccb4042f3f --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Chatwork/Tests/ChatworkTransportFactoryTest.php @@ -0,0 +1,54 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\Chatwork\Tests; + +use Symfony\Component\Notifier\Bridge\Chatwork\ChatworkTransportFactory; +use Symfony\Component\Notifier\Test\TransportFactoryTestCase; +use Symfony\Component\Notifier\Transport\TransportFactoryInterface; + +class ChatworkTransportFactoryTest extends TransportFactoryTestCase +{ + public function createFactory(): TransportFactoryInterface + { + return new ChatworkTransportFactory(); + } + + public function supportsProvider(): iterable + { + yield [true, 'chatwork://host?room_id=testRoomId']; + yield [false, 'somethingElse://host?room_id=testRoomId']; + } + + public function createProvider(): iterable + { + yield [ + 'chatwork://host.test?room_id=testRoomId', + 'chatwork://token@host.test?room_id=testRoomId', + ]; + } + + public function incompleteDsnProvider(): iterable + { + yield 'missing token' => ['chatwork://host.test?room_id=testRoomId']; + } + + public function missingRequiredOptionProvider(): iterable + { + yield 'missing option: room_id' => ['chatwork://token@host']; + } + + public function unsupportedSchemeProvider(): iterable + { + yield ['somethingElse://token@host?room_id=testRoomId']; + yield ['somethingElse://token@host']; // missing "room_id" option + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Chatwork/Tests/ChatworkTransportTest.php b/src/Symfony/Component/Notifier/Bridge/Chatwork/Tests/ChatworkTransportTest.php new file mode 100644 index 0000000000000..08db6b056bef4 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Chatwork/Tests/ChatworkTransportTest.php @@ -0,0 +1,69 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\Chatwork\Tests; + +use Symfony\Component\HttpClient\MockHttpClient; +use Symfony\Component\Notifier\Bridge\Chatwork\ChatworkTransport; +use Symfony\Component\Notifier\Exception\TransportException; +use Symfony\Component\Notifier\Message\ChatMessage; +use Symfony\Component\Notifier\Message\MessageInterface; +use Symfony\Component\Notifier\Message\SmsMessage; +use Symfony\Component\Notifier\Test\TransportTestCase; +use Symfony\Component\Notifier\Transport\TransportInterface; +use Symfony\Contracts\HttpClient\HttpClientInterface; +use Symfony\Contracts\HttpClient\ResponseInterface; + +class ChatworkTransportTest extends TransportTestCase +{ + public function createTransport(HttpClientInterface $client = null): TransportInterface + { + return (new ChatworkTransport('testToken', 'testRoomId', $client ?? $this->createMock(HttpClientInterface::class)))->setHost('host.test'); + } + + public function toStringProvider(): iterable + { + yield ['chatwork://host.test?room_id=testRoomId', $this->createTransport()]; + } + + public function supportedMessagesProvider(): iterable + { + yield [new ChatMessage('Hello!')]; + } + + public function unsupportedMessagesProvider(): iterable + { + yield [new SmsMessage('0611223344', 'Hello!')]; + yield [$this->createMock(MessageInterface::class)]; + } + + 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(['errors' => ['first error', 'second error']])); + + $client = new MockHttpClient(static function () use ($response): ResponseInterface { + return $response; + }); + + $transport = $this->createTransport($client); + + $this->expectException(TransportException::class); + $this->expectExceptionMessageMatches('/first error, second error/'); + + $transport->send(new ChatMessage('testMessage')); + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Chatwork/composer.json b/src/Symfony/Component/Notifier/Bridge/Chatwork/composer.json new file mode 100644 index 0000000000000..fc152cf076937 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Chatwork/composer.json @@ -0,0 +1,30 @@ +{ + "name": "symfony/chatwork-notifier", + "type": "symfony-notifier-bridge", + "description": "Provides Chatwork integration for Symfony Notifier.", + "keywords": ["chatwork", "notifier", "chat", "sms"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Ippei Sumida", + "email": "ippey.s@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=8.1", + "symfony/http-client": "^5.4|^6.0", + "symfony/notifier": "^6.2" + }, + "autoload": { + "psr-4": { "Symfony\\Component\\Notifier\\Bridge\\Chatwork\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev" +} diff --git a/src/Symfony/Component/Notifier/Bridge/Chatwork/phpunit.xml.dist b/src/Symfony/Component/Notifier/Bridge/Chatwork/phpunit.xml.dist new file mode 100644 index 0000000000000..155be9021e7b4 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Chatwork/phpunit.xml.dist @@ -0,0 +1,31 @@ + + + + + + + + + + ./Tests/ + + + + + + ./ + + ./Resources + ./Tests + ./vendor + + + + diff --git a/src/Symfony/Component/Notifier/Transport.php b/src/Symfony/Component/Notifier/Transport.php index 8389474e1468c..b9829b8ebdf68 100644 --- a/src/Symfony/Component/Notifier/Transport.php +++ b/src/Symfony/Component/Notifier/Transport.php @@ -13,6 +13,7 @@ use Symfony\Component\Notifier\Bridge\AllMySms\AllMySmsTransportFactory; use Symfony\Component\Notifier\Bridge\AmazonSns\AmazonSnsTransportFactory; +use Symfony\Component\Notifier\Bridge\Chatwork\ChatworkTransportFactory; use Symfony\Component\Notifier\Bridge\Clickatell\ClickatellTransportFactory; use Symfony\Component\Notifier\Bridge\ContactEveryone\ContactEveryoneTransportFactory; use Symfony\Component\Notifier\Bridge\Discord\DiscordTransportFactory; @@ -73,6 +74,7 @@ final class Transport private const FACTORY_CLASSES = [ AllMySmsTransportFactory::class, AmazonSnsTransportFactory::class, + ChatworkTransportFactory::class, ClickatellTransportFactory::class, ContactEveryoneTransportFactory::class, DiscordTransportFactory::class,