diff --git a/composer.json b/composer.json index d03652cee8f6f..2cb40c768618c 100644 --- a/composer.json +++ b/composer.json @@ -142,6 +142,7 @@ "predis/predis": "~1.1", "psr/http-client": "^1.0", "psr/simple-cache": "^1.0|^2.0|^3.0", + "pusher/pusher-php-server": "^7.0", "symfony/mercure-bundle": "^0.3", "symfony/phpunit-bridge": "^5.4|^6.0", "symfony/runtime": "self.version", diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php index c5e0371d933ba..0124c36dae652 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php @@ -50,6 +50,7 @@ use Symfony\Component\Notifier\Bridge\OvhCloud\OvhCloudTransportFactory; use Symfony\Component\Notifier\Bridge\PagerDuty\PagerDutyTransportFactory; use Symfony\Component\Notifier\Bridge\Plivo\PlivoTransportFactory; +use Symfony\Component\Notifier\Bridge\Pusher\PusherTransportFactory; use Symfony\Component\Notifier\Bridge\RingCentral\RingCentralTransportFactory; use Symfony\Component\Notifier\Bridge\RocketChat\RocketChatTransportFactory; use Symfony\Component\Notifier\Bridge\Sendberry\SendberryTransportFactory; @@ -331,5 +332,9 @@ ->set('notifier.transport_factory.pager-duty', PagerDutyTransportFactory::class) ->parent('notifier.transport_factory.abstract') ->tag('chatter.transport_factory') + + ->set('notifier.transport_factory.slack', PusherTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('chatter.transport_factory') ; }; diff --git a/src/Symfony/Component/Notifier/Bridge/Pusher/.gitattributes b/src/Symfony/Component/Notifier/Bridge/Pusher/.gitattributes new file mode 100644 index 0000000000000..84c7add058fb5 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Pusher/.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/Pusher/.gitignore b/src/Symfony/Component/Notifier/Bridge/Pusher/.gitignore new file mode 100644 index 0000000000000..c49a5d8df5c65 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Pusher/.gitignore @@ -0,0 +1,3 @@ +vendor/ +composer.lock +phpunit.xml diff --git a/src/Symfony/Component/Notifier/Bridge/Pusher/CHANGELOG.md b/src/Symfony/Component/Notifier/Bridge/Pusher/CHANGELOG.md new file mode 100644 index 0000000000000..c7d6080bb1103 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Pusher/CHANGELOG.md @@ -0,0 +1,7 @@ +CHANGELOG +========= + +6.3 +---- + + * Add the bridge diff --git a/src/Symfony/Component/Notifier/Bridge/Pusher/LICENSE b/src/Symfony/Component/Notifier/Bridge/Pusher/LICENSE new file mode 100644 index 0000000000000..f961401699b27 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Pusher/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2023 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/Pusher/PusherNotification.php b/src/Symfony/Component/Notifier/Bridge/Pusher/PusherNotification.php new file mode 100644 index 0000000000000..2bd8f67067d2b --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Pusher/PusherNotification.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\Pusher; + +use Symfony\Component\Notifier\Message\PushMessage; +use Symfony\Component\Notifier\Notification\Notification; +use Symfony\Component\Notifier\Notification\PushNotificationInterface; +use Symfony\Component\Notifier\Recipient\RecipientInterface; + +/** + * @author Yasmany Cubela Medina + */ +class PusherNotification extends Notification implements PushNotificationInterface +{ + public function asPushMessage(RecipientInterface $recipient, string $transport = null): ?PushMessage + { + return new PushMessage($this->getSubject(), $this->getContent(), new PusherOptions($recipient instanceof PusherRecipientInterface ? $recipient->getChannels() : [])); + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Pusher/PusherOptions.php b/src/Symfony/Component/Notifier/Bridge/Pusher/PusherOptions.php new file mode 100755 index 0000000000000..e67e9003973b8 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Pusher/PusherOptions.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\Pusher; + +use Symfony\Component\Notifier\Message\MessageOptionsInterface; + +/** + * @author Yasmany Cubela Medina + */ +final class PusherOptions implements MessageOptionsInterface +{ + private array $channels; + + public function __construct(array $channels) + { + $this->channels = $channels; + } + + public function toArray(): array + { + return $this->channels; + } + + public function getRecipientId(): ?string + { + return null; + } + + public function getChannels(): array + { + return $this->channels; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Pusher/PusherRecipient.php b/src/Symfony/Component/Notifier/Bridge/Pusher/PusherRecipient.php new file mode 100644 index 0000000000000..5ce14e6a1799e --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Pusher/PusherRecipient.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\Pusher; + +/** + * @author Yasmany Cubela Medina + */ +class PusherRecipient implements PusherRecipientInterface +{ + private array $channels; + + public function __construct(array $channels) + { + $this->channels = $channels; + } + + public function getChannels(): array + { + return $this->channels; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Pusher/PusherRecipientInterface.php b/src/Symfony/Component/Notifier/Bridge/Pusher/PusherRecipientInterface.php new file mode 100644 index 0000000000000..59e9c914cea4f --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Pusher/PusherRecipientInterface.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\Pusher; + +use Symfony\Component\Notifier\Recipient\RecipientInterface; + +/** + * @author Yasmany Cubela Medina + */ +interface PusherRecipientInterface extends RecipientInterface +{ + public function getChannels(): array; +} diff --git a/src/Symfony/Component/Notifier/Bridge/Pusher/PusherTransport.php b/src/Symfony/Component/Notifier/Bridge/Pusher/PusherTransport.php new file mode 100755 index 0000000000000..b99af8154339b --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Pusher/PusherTransport.php @@ -0,0 +1,72 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\Pusher; + +use Pusher\Pusher; +use Symfony\Component\Notifier\Exception\LogicException; +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; +use Throwable; + +/** + * @author Yasmany Cubela Medina + */ +final class PusherTransport extends AbstractTransport +{ + private $pusherClient; + + public function __construct(Pusher $pusherClient, HttpClientInterface $client = null, EventDispatcherInterface $dispatcher = null) + { + $this->pusherClient = $pusherClient; + + parent::__construct($client, $dispatcher); + } + + public function __toString(): string + { + $settings = $this->pusherClient->getSettings(); + preg_match('/api-([\w]+)\.pusher\.com$/m', $settings['host'], $server); + + return sprintf('pusher://%s?server=%s', $settings['app_id'], $server[1]); + } + + public function supports(MessageInterface $message): bool + { + return $message instanceof PushMessage && (null === $message->getOptions() || $message->getOptions() instanceof PusherOptions); + } + + protected function doSend(MessageInterface $message): SentMessage + { + if (!$message instanceof PushMessage) { + throw new UnsupportedMessageTypeException(__CLASS__, PushMessage::class, $message); + } + + $options = $message->getOptions(); + + if (!$options instanceof PusherOptions) { + throw new LogicException(sprintf('The "%s" transport only supports instances of "%s" for options.', __CLASS__, PusherOptions::class)); + } + + try { + $this->pusherClient->trigger($options->getChannels(), $message->getSubject(), $message->getContent(), [], true); + } catch (Throwable) { + throw new \RuntimeException('An error occurred at Pusher Notifier Transport.'); + } + + return new SentMessage($message, $this->__toString()); + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Pusher/PusherTransportFactory.php b/src/Symfony/Component/Notifier/Bridge/Pusher/PusherTransportFactory.php new file mode 100755 index 0000000000000..dfbe4a7698ba6 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Pusher/PusherTransportFactory.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\Pusher; + +use Pusher\Pusher; +use Symfony\Component\DependencyInjection\Attribute\Autoconfigure; +use Symfony\Component\Notifier\Exception\MissingRequiredOptionException; +use Symfony\Component\Notifier\Exception\UnsupportedSchemeException; +use Symfony\Component\Notifier\Transport\AbstractTransportFactory; +use Symfony\Component\Notifier\Transport\Dsn; +use Symfony\Component\Notifier\Transport\TransportInterface; + +/** + * @author Yasmany Cubela Medina + */ +final class PusherTransportFactory extends AbstractTransportFactory +{ + public function create(Dsn $dsn): TransportInterface + { + if ('pusher' !== $dsn->getScheme()) { + throw new UnsupportedSchemeException($dsn, 'pusher', $this->getSupportedSchemes()); + } + + if (null === $dsn->getUser() || null === $dsn->getPassword() || null === $dsn->getOption('server')) { + throw new MissingRequiredOptionException('Pusher needs a APP_KEY, APP_SECRET AND SERVER specified.'); + } + + $options = [ + 'cluster' => $dsn->getOption('server', 'mt1'), + ]; + + $pusherClient = new Pusher($dsn->getUser(), $dsn->getPassword(), $dsn->getHost(), $options); + + return new PusherTransport($pusherClient, $this->client, $this->dispatcher); + } + + protected function getSupportedSchemes(): array + { + return ['pusher']; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Pusher/README.md b/src/Symfony/Component/Notifier/Bridge/Pusher/README.md new file mode 100644 index 0000000000000..79b4d760c194b --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Pusher/README.md @@ -0,0 +1,40 @@ +Pusher Notifier +============== + +Provides [Pusher](https://pusher.com) integration for Symfony Notifier. + +DSN example +----------- + +``` +PUSHER_DSN=pusher://APP_KEY:APP_SECRET@APP_ID?server=SERVER +``` + +where: + +- `APP_KEY` is your app unique key +- `APP_SECRET` is your app unique and secret password +- `APP_ID` is your app unique id +- `SERVER` is your app server + +valid DSN's are: + +``` +PUSHER_DSN=pusher://as8d09a0ds8:as8d09a8sd0a8sd0@123123123?server=mt1 +``` + +invalid DSN's are: + +``` +PUSHER_DSN=pusher://asdasdasd@asdasdasd?server=invalid-server +PUSHER_DSN=pusher://:asdasdasd@asdasdasd?server=invalid-server +PUSHER_DSN=pusher://asdadasdasd:asdasdasd@asdasdasd?server=invalid-server +``` + +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/Pusher/Tests/PusherOptionsTest.php b/src/Symfony/Component/Notifier/Bridge/Pusher/Tests/PusherOptionsTest.php new file mode 100644 index 0000000000000..f93b58fb29630 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Pusher/Tests/PusherOptionsTest.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\Pusher\Tests; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Notifier\Bridge\Pusher\PusherOptions; + +/** + * @author Yasmany Cubela Medina + * + * @coversNothing + */ +final class PusherOptionsTest extends TestCase +{ + /** + * @dataProvider toArrayProvider + * @dataProvider toArraySimpleOptionsProvider + */ + public function testToArray(array $options, array $expected = null) + { + static::assertSame($expected ?? $options, (new PusherOptions($options))->toArray()); + } + + public function toArrayProvider(): iterable + { + yield 'empty is allowed' => [ + [], + [], + ]; + } + + public function toArraySimpleOptionsProvider(): iterable + { + yield [[]]; + } + + public function setProvider(): iterable + { + yield ['async', 'async', true]; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Pusher/Tests/PusherTransportFactoryTest.php b/src/Symfony/Component/Notifier/Bridge/Pusher/Tests/PusherTransportFactoryTest.php new file mode 100644 index 0000000000000..1bd9a688fe8c9 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Pusher/Tests/PusherTransportFactoryTest.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\Pusher\Tests; + +use Symfony\Component\Notifier\Bridge\Pusher\PusherTransportFactory; +use Symfony\Component\Notifier\Test\TransportFactoryTestCase; +use Symfony\Component\Notifier\Transport\TransportFactoryInterface; + +/** + * @author Yasmany Cubela Medina + * + * @coversNothing + */ +final class PusherTransportFactoryTest extends TransportFactoryTestCase +{ + /** + * @return PusherTransportFactory + */ + public function createFactory(): TransportFactoryInterface + { + return new PusherTransportFactory(); + } + + public function createProvider(): iterable + { + yield [ + 'pusher://key:secret@id?server=mt1', + 'pusher://key:secret@id?server=mt1', + ]; + } + + public function supportsProvider(): iterable + { + yield [true, 'pusher://key:secret@id?server=mt1']; + yield [false, 'somethingElse://xoxb-TestToken@host?server=testChannel']; + } + + public function incompleteDsnProvider(): iterable + { + yield 'missing secret' => ['pusher://key@id?server=mt1']; + } + + public function unsupportedSchemeProvider(): iterable + { + yield ['somethingElse://something@else']; + } + + public function missingRequiredOptionProvider(): iterable + { + yield ['pusher://key:secret@id']; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Pusher/Tests/PusherTransportTest.php b/src/Symfony/Component/Notifier/Bridge/Pusher/Tests/PusherTransportTest.php new file mode 100644 index 0000000000000..897ba189ac74c --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Pusher/Tests/PusherTransportTest.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\Pusher\Tests; + +use Pusher\Pusher; +use Symfony\Component\Notifier\Bridge\Pusher\PusherTransport; +use Symfony\Component\Notifier\Message\PushMessage; +use Symfony\Component\Notifier\Message\SmsMessage; +use Symfony\Component\Notifier\Test\TransportTestCase; +use Symfony\Component\Notifier\Tests\Fixtures\DummyHttpClient; +use Symfony\Component\Notifier\Tests\Fixtures\DummyMessage; +use Symfony\Component\Notifier\Transport\TransportInterface; +use Symfony\Contracts\HttpClient\HttpClientInterface; + +/** + * @author Yasmany Cubela Medina + * + * @coversNothing + */ +final class PusherTransportTest extends TransportTestCase +{ + public function toStringProvider(): iterable + { + yield ['pusher://key:secret@app?server=mt1', $this->createTransport()]; + } + + /** + * @return PusherTransport + */ + public static function createTransport(HttpClientInterface $client = null): TransportInterface + { + return new PusherTransport(new Pusher('key', 'secret', 'app'), $client ?? new DummyHttpClient()); + } + + public static function supportedMessagesProvider(): iterable + { + yield [new PushMessage('event', 'data')]; + } + + public static function unsupportedMessagesProvider(): iterable + { + yield [new SmsMessage('0611223344', 'Hello!')]; + yield [new DummyMessage()]; + } + + public function testCanSetCustomHost() + { + static::markTestSkipped('Does not apply for this provider.'); + } + + public function testCanSetCustomPort() + { + static::markTestSkipped('Does not apply for this provider.'); + } + + public function testCanSetCustomHostAndPort() + { + static::markTestSkipped('Does not apply for this provider.'); + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Pusher/composer.json b/src/Symfony/Component/Notifier/Bridge/Pusher/composer.json new file mode 100644 index 0000000000000..1a27f2904cb74 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Pusher/composer.json @@ -0,0 +1,34 @@ +{ + "name": "symfony/pusher-notifier", + "type": "symfony-notifier-bridge", + "description": "Symfony Pusher Notifier Bridge", + "keywords": ["pusher", "notifier"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Yasmany Cubela Medina", + "email": "yasmanycm@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=8.1", + "pusher/pusher-php-server": "^7.0", + "symfony/http-client": "^5.4|^6.0", + "symfony/notifier": "^6.2" + }, + "require-dev": { + "symfony/event-dispatcher": "^5.4|^6.0" + }, + "autoload": { + "psr-4": { "Symfony\\Component\\Notifier\\Bridge\\Pusher\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev" +} diff --git a/src/Symfony/Component/Notifier/Bridge/Pusher/phpunit.xml.dist b/src/Symfony/Component/Notifier/Bridge/Pusher/phpunit.xml.dist new file mode 100644 index 0000000000000..27ba9f69b7dfd --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Pusher/phpunit.xml.dist @@ -0,0 +1,31 @@ + + + + + + + + + + ./Tests/ + + + + + + ./ + + + ./Resources + ./Tests + ./vendor + + +