From 2776d2f8116107e959d7633b74d4f14a813896e5 Mon Sep 17 00:00:00 2001 From: Jeroeny Date: Sat, 12 Oct 2019 19:58:40 +0200 Subject: [PATCH] [Notifier] Add Firebase bridge --- .../FrameworkExtension.php | 2 + .../Resources/config/notifier_transports.xml | 4 + .../Notifier/Bridge/Firebase/.gitattributes | 2 + .../Notifier/Bridge/Firebase/CHANGELOG.md | 7 ++ .../Bridge/Firebase/FirebaseOptions.php | 67 +++++++++++++ .../Bridge/Firebase/FirebaseTransport.php | 88 +++++++++++++++++ .../Firebase/FirebaseTransportFactory.php | 44 +++++++++ .../Notifier/Bridge/Firebase/LICENSE | 19 ++++ .../Notification/AndroidNotification.php | 96 +++++++++++++++++++ .../Firebase/Notification/IOSNotification.php | 82 ++++++++++++++++ .../Firebase/Notification/WebNotification.php | 34 +++++++ .../Notifier/Bridge/Firebase/README.md | 12 +++ .../Notifier/Bridge/Firebase/composer.json | 35 +++++++ .../Notifier/Bridge/Firebase/phpunit.xml.dist | 31 ++++++ .../Exception/UnsupportedSchemeException.php | 4 + src/Symfony/Component/Notifier/Transport.php | 2 + 16 files changed, 529 insertions(+) create mode 100644 src/Symfony/Component/Notifier/Bridge/Firebase/.gitattributes create mode 100644 src/Symfony/Component/Notifier/Bridge/Firebase/CHANGELOG.md create mode 100644 src/Symfony/Component/Notifier/Bridge/Firebase/FirebaseOptions.php create mode 100644 src/Symfony/Component/Notifier/Bridge/Firebase/FirebaseTransport.php create mode 100644 src/Symfony/Component/Notifier/Bridge/Firebase/FirebaseTransportFactory.php create mode 100644 src/Symfony/Component/Notifier/Bridge/Firebase/LICENSE create mode 100644 src/Symfony/Component/Notifier/Bridge/Firebase/Notification/AndroidNotification.php create mode 100644 src/Symfony/Component/Notifier/Bridge/Firebase/Notification/IOSNotification.php create mode 100644 src/Symfony/Component/Notifier/Bridge/Firebase/Notification/WebNotification.php create mode 100644 src/Symfony/Component/Notifier/Bridge/Firebase/README.md create mode 100644 src/Symfony/Component/Notifier/Bridge/Firebase/composer.json create mode 100644 src/Symfony/Component/Notifier/Bridge/Firebase/phpunit.xml.dist diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 442acb9dab..6ad2eb8582 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -90,6 +90,7 @@ use Symfony\Component\Messenger\Transport\TransportFactoryInterface; use Symfony\Component\Messenger\Transport\TransportInterface; use Symfony\Component\Mime\MimeTypeGuesserInterface; use Symfony\Component\Mime\MimeTypes; +use Symfony\Component\Notifier\Bridge\Firebase\FirebaseTransportFactory; use Symfony\Component\Notifier\Bridge\Mattermost\MattermostTransportFactory; use Symfony\Component\Notifier\Bridge\Nexmo\NexmoTransportFactory; use Symfony\Component\Notifier\Bridge\Slack\SlackTransportFactory; @@ -2001,6 +2002,7 @@ class FrameworkExtension extends Extension MattermostTransportFactory::class => 'notifier.transport_factory.mattermost', NexmoTransportFactory::class => 'notifier.transport_factory.nexmo', TwilioTransportFactory::class => 'notifier.transport_factory.twilio', + FirebaseTransportFactory::class => 'notifier.transport_factory.firebase', ]; foreach ($classToServices as $class => $service) { diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.xml index 4625458280..10b3f1d850 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.xml @@ -30,6 +30,10 @@ + + + + diff --git a/src/Symfony/Component/Notifier/Bridge/Firebase/.gitattributes b/src/Symfony/Component/Notifier/Bridge/Firebase/.gitattributes new file mode 100644 index 0000000000..aa02dc6518 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Firebase/.gitattributes @@ -0,0 +1,2 @@ +/Tests export-ignore +/phpunit.xml.dist export-ignore diff --git a/src/Symfony/Component/Notifier/Bridge/Firebase/CHANGELOG.md b/src/Symfony/Component/Notifier/Bridge/Firebase/CHANGELOG.md new file mode 100644 index 0000000000..3cd6c94c4f --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Firebase/CHANGELOG.md @@ -0,0 +1,7 @@ +CHANGELOG +========= + +5.1.0 +----- + + * Created the bridge diff --git a/src/Symfony/Component/Notifier/Bridge/Firebase/FirebaseOptions.php b/src/Symfony/Component/Notifier/Bridge/Firebase/FirebaseOptions.php new file mode 100644 index 0000000000..0d098fe286 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Firebase/FirebaseOptions.php @@ -0,0 +1,67 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\Firebase; + +use Symfony\Component\Notifier\Message\MessageOptionsInterface; + +/** + * @author Jeroen Spee + * + * @see https://firebase.google.com/docs/cloud-messaging/xmpp-server-ref.html + * + * @experimental in 5.1 + */ +abstract class FirebaseOptions implements MessageOptionsInterface +{ + /** @var string the recipient */ + private $to; + + /** + * @var array + * + * @see https://firebase.google.com/docs/cloud-messaging/xmpp-server-ref.html#notification-payload-support + */ + protected $options; + + public function __construct(string $to, array $options) + { + $this->to = $to; + $this->options = $options; + } + + public function toArray(): array + { + return [ + 'to' => $this->to, + 'notification' => $this->options, + ]; + } + + public function getRecipientId(): ?string + { + return $this->to; + } + + public function title(string $title): self + { + $this->options['title'] = $title; + + return $this; + } + + public function body(string $body): self + { + $this->options['body'] = $body; + + return $this; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Firebase/FirebaseTransport.php b/src/Symfony/Component/Notifier/Bridge/Firebase/FirebaseTransport.php new file mode 100644 index 0000000000..eed61d8584 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Firebase/FirebaseTransport.php @@ -0,0 +1,88 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\Firebase; + +use Symfony\Component\Notifier\Exception\InvalidArgumentException; +use Symfony\Component\Notifier\Exception\LogicException; +use Symfony\Component\Notifier\Exception\TransportException; +use Symfony\Component\Notifier\Message\ChatMessage; +use Symfony\Component\Notifier\Message\MessageInterface; +use Symfony\Component\Notifier\Transport\AbstractTransport; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; +use Symfony\Contracts\HttpClient\HttpClientInterface; + +/** + * @author Jeroen Spee + * + * @experimental in 5.1 + */ +final class FirebaseTransport extends AbstractTransport +{ + protected const HOST = 'fcm.googleapis.com/fcm/send'; + + /** @var string */ + private $token; + + public function __construct(string $token, HttpClientInterface $client = null, EventDispatcherInterface $dispatcher = null) + { + $this->token = $token; + $this->client = $client; + + parent::__construct($client, $dispatcher); + } + + public function __toString(): string + { + return sprintf('firebase://%s', $this->getEndpoint()); + } + + public function supports(MessageInterface $message): bool + { + return $message instanceof ChatMessage; + } + + protected function doSend(MessageInterface $message): void + { + if (!$message instanceof ChatMessage) { + throw new LogicException(sprintf('The "%s" transport only supports instances of "%s" (instance of "%s" given).', __CLASS__, ChatMessage::class, \get_class($message))); + } + + $endpoint = sprintf('https://%s', $this->getEndpoint()); + $options = ($opts = $message->getOptions()) ? $opts->toArray() : []; + if (!isset($options['to'])) { + $options['to'] = $message->getRecipientId(); + } + if (null === $options['to']) { + throw new InvalidArgumentException(sprintf('The "%s" transport required the "to" option to be set', __CLASS__)); + } + $options['notification'] = $options['notification'] ?? []; + $options['notification']['body'] = $message->getSubject(); + $response = $this->client->request('POST', $endpoint, [ + 'headers' => [ + 'Authorization' => sprintf('key=%s', $this->token), + ], + 'json' => array_filter($options), + ]); + + $contentType = $response->getHeaders(false)['Content-Type'] ?? ''; + $jsonContents = 0 === strpos($contentType, 'application/json') ? $response->toArray(false) : null; + + if (200 !== $response->getStatusCode()) { + $errorMessage = $jsonContents ? $jsonContents['results']['error'] : $response->getContent(false); + + throw new TransportException(sprintf('Unable to post the Firebase message: %s.', $errorMessage), $response); + } + if ($jsonContents && isset($jsonContents['results']['error'])) { + throw new TransportException(sprintf('Unable to post the Firebase message: %s.', $jsonContents['error']), $response); + } + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Firebase/FirebaseTransportFactory.php b/src/Symfony/Component/Notifier/Bridge/Firebase/FirebaseTransportFactory.php new file mode 100644 index 0000000000..e4e55612d2 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Firebase/FirebaseTransportFactory.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\Firebase; + +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 Jeroen Spee + * + * @experimental in 5.1 + */ +final class FirebaseTransportFactory extends AbstractTransportFactory +{ + public function create(Dsn $dsn): TransportInterface + { + $scheme = $dsn->getScheme(); + $token = sprintf('%s:%s', $this->getUser($dsn), $this->getPassword($dsn)); + $host = 'default' === $dsn->getHost() ? null : $dsn->getHost(); + $port = $dsn->getPort(); + + if ('firebase' === $scheme) { + return (new FirebaseTransport($token, $this->client, $this->dispatcher))->setHost($host)->setPort($port); + } + + throw new UnsupportedSchemeException($dsn, 'firebase', $this->getSupportedSchemes()); + } + + protected function getSupportedSchemes(): array + { + return ['firebase']; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Firebase/LICENSE b/src/Symfony/Component/Notifier/Bridge/Firebase/LICENSE new file mode 100644 index 0000000000..1a1869751d --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Firebase/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2019 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/Firebase/Notification/AndroidNotification.php b/src/Symfony/Component/Notifier/Bridge/Firebase/Notification/AndroidNotification.php new file mode 100644 index 0000000000..b41f6a3bdc --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Firebase/Notification/AndroidNotification.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\Firebase\Notification; + +use Symfony\Component\Notifier\Bridge\Firebase\FirebaseOptions; + +/** + * @experimental in 5.1 + */ +final class AndroidNotification extends FirebaseOptions +{ + public function channelId(string $channelId): self + { + $this->options['android_channel_id'] = $channelId; + + return $this; + } + + public function icon(string $icon): self + { + $this->options['icon'] = $icon; + + return $this; + } + + public function sound(string $sound): self + { + $this->options['sound'] = $sound; + + return $this; + } + + public function tag(string $tag): self + { + $this->options['tag'] = $tag; + + return $this; + } + + public function color(string $color): self + { + $this->options['color'] = $color; + + return $this; + } + + public function clickAction(string $clickAction): self + { + $this->options['click_action'] = $clickAction; + + return $this; + } + + public function bodyLocKey(string $bodyLocKey): self + { + $this->options['body_loc_key'] = $bodyLocKey; + + return $this; + } + + /** + * @param string[] $bodyLocArgs + */ + public function bodyLocArgs(array $bodyLocArgs): self + { + $this->options['body_loc_args'] = $bodyLocArgs; + + return $this; + } + + public function titleLocKey(string $titleLocKey): self + { + $this->options['title_loc_key'] = $titleLocKey; + + return $this; + } + + /** + * @param string[] $titleLocArgs + */ + public function titleLocArgs(array $titleLocArgs): self + { + $this->options['title_loc_args'] = $titleLocArgs; + + return $this; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Firebase/Notification/IOSNotification.php b/src/Symfony/Component/Notifier/Bridge/Firebase/Notification/IOSNotification.php new file mode 100644 index 0000000000..b406bc6eef --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Firebase/Notification/IOSNotification.php @@ -0,0 +1,82 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\Firebase\Notification; + +use Symfony\Component\Notifier\Bridge\Firebase\FirebaseOptions; + +/** + * @experimental in 5.1 + */ +final class IOSNotification extends FirebaseOptions +{ + public function sound(string $sound): self + { + $this->options['sound'] = $sound; + + return $this; + } + + public function badge(string $badge): self + { + $this->options['badge'] = $badge; + + return $this; + } + + public function clickAction(string $clickAction): self + { + $this->options['click_action'] = $clickAction; + + return $this; + } + + public function subtitle(string $subtitle): self + { + $this->options['subtitle'] = $subtitle; + + return $this; + } + + public function bodyLocKey(string $bodyLocKey): self + { + $this->options['body_loc_key'] = $bodyLocKey; + + return $this; + } + + /** + * @param string[] $bodyLocArgs + */ + public function bodyLocArgs(array $bodyLocArgs): self + { + $this->options['body_loc_args'] = $bodyLocArgs; + + return $this; + } + + public function titleLocKey(string $titleLocKey): self + { + $this->options['title_loc_key'] = $titleLocKey; + + return $this; + } + + /** + * @param string[] $titleLocArgs + */ + public function titleLocArgs(array $titleLocArgs): self + { + $this->options['title_loc_args'] = $titleLocArgs; + + return $this; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Firebase/Notification/WebNotification.php b/src/Symfony/Component/Notifier/Bridge/Firebase/Notification/WebNotification.php new file mode 100644 index 0000000000..3860bf2a96 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Firebase/Notification/WebNotification.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\Firebase\Notification; + +use Symfony\Component\Notifier\Bridge\Firebase\FirebaseOptions; + +/** + * @experimental in 5.1 + */ +final class WebNotification extends FirebaseOptions +{ + public function icon(string $icon): self + { + $this->options['icon'] = $icon; + + return $this; + } + + public function clickAction(string $clickAction): self + { + $this->options['click_action'] = $clickAction; + + return $this; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Firebase/README.md b/src/Symfony/Component/Notifier/Bridge/Firebase/README.md new file mode 100644 index 0000000000..45da948c15 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Firebase/README.md @@ -0,0 +1,12 @@ +Firebase Notifier +================= + +Provides Firebase integration for Symfony Notifier. + +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/Firebase/composer.json b/src/Symfony/Component/Notifier/Bridge/Firebase/composer.json new file mode 100644 index 0000000000..bb85f9978c --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Firebase/composer.json @@ -0,0 +1,35 @@ +{ + "name": "symfony/firebase-notifier", + "type": "symfony-bridge", + "description": "Symfony Firebase Notifier Bridge", + "keywords": ["firebase", "notifier"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Jeroen Spee", + "homepage": "https://github.com/Jeroeny" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": "^7.2.5", + "symfony/http-client": "^4.3|^5.0", + "symfony/notifier": "^5.0" + }, + "autoload": { + "psr-4": { "Symfony\\Component\\Notifier\\Bridge\\Firebase\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Firebase/phpunit.xml.dist b/src/Symfony/Component/Notifier/Bridge/Firebase/phpunit.xml.dist new file mode 100644 index 0000000000..66b1cd5652 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Firebase/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 24bdebeb17..646edbaec9 100644 --- a/src/Symfony/Component/Notifier/Exception/UnsupportedSchemeException.php +++ b/src/Symfony/Component/Notifier/Exception/UnsupportedSchemeException.php @@ -42,6 +42,10 @@ class UnsupportedSchemeException extends LogicException 'class' => Bridge\Twilio\TwilioTransportFactory::class, 'package' => 'symfony/twilio-notifier', ], + 'firebase' => [ + 'class' => Bridge\Firebase\FirebaseTransportFactory::class, + 'package' => 'symfony/firebase-notifier', + ], ]; /** diff --git a/src/Symfony/Component/Notifier/Transport.php b/src/Symfony/Component/Notifier/Transport.php index 1671fca2c9..7e8d735404 100644 --- a/src/Symfony/Component/Notifier/Transport.php +++ b/src/Symfony/Component/Notifier/Transport.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Notifier; +use Symfony\Component\Notifier\Bridge\Firebase\FirebaseTransportFactory; use Symfony\Component\Notifier\Bridge\Mattermost\MattermostTransportFactory; use Symfony\Component\Notifier\Bridge\Nexmo\NexmoTransportFactory; use Symfony\Component\Notifier\Bridge\Slack\SlackTransportFactory; @@ -40,6 +41,7 @@ class Transport MattermostTransportFactory::class, NexmoTransportFactory::class, TwilioTransportFactory::class, + FirebaseTransportFactory::class, ]; private $factories;