diff --git a/.travis.yml b/.travis.yml index f248c95..a86533b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,10 +2,9 @@ language: php before_install: # If PHP >= 5.6, download & install PHPunit 5.7 to avoid builds failures - - if php -r "exit( (int)! version_compare( '$TRAVIS_PHP_VERSION', '5.6', '>=' ) );"; then wget -O phpunit https://phar.phpunit.de/phpunit-5.7.phar && chmod +x phpunit && mkdir ~/bin && mv -v phpunit ~/bin; fi + - if php -r "exit( (int)! version_compare( '$TRAVIS_PHP_VERSION', '5.6', '>=' ) );"; then wget -O phpunit https://phar.phpunit.de/phpunit-5.7.phar && chmod +x phpunit && mkdir -p ~/bin && mv -v phpunit ~/bin; fi php: - - 5.3 - 5.4 - 5.5 - 5.6 diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index 9d8f30f..af86873 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -68,6 +68,14 @@ protected function addAndroid() booleanNode("dry_run")->defaultFalse()->end()-> end()-> end()-> + arrayNode("fcm")-> + canBeUnset()-> + children()-> + scalarNode("api_key")->isRequired()->cannotBeEmpty()->end()-> + booleanNode("use_multi_curl")->defaultValue(true)->end()-> + booleanNode("dry_run")->defaultFalse()->end()-> + end()-> + end()-> end()-> end()-> end() diff --git a/DependencyInjection/RMSPushNotificationsExtension.php b/DependencyInjection/RMSPushNotificationsExtension.php index a0c4745..33b578e 100644 --- a/DependencyInjection/RMSPushNotificationsExtension.php +++ b/DependencyInjection/RMSPushNotificationsExtension.php @@ -22,8 +22,9 @@ class RMSPushNotificationsExtension extends Extension /** * Loads any resources/services we need * - * @param array $configs + * @param array $configs * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container + * * @return void */ public function load(array $configs, ContainerBuilder $container) @@ -35,7 +36,7 @@ public function load(array $configs, ContainerBuilder $container) $loader->load('services.xml'); $configuration = new Configuration(); - $config = $this->processConfiguration($configuration, $configs); + $config = $this->processConfiguration($configuration, $configs); $this->setInitialParams(); if (isset($config["android"])) { @@ -83,12 +84,12 @@ protected function setAndroidConfig(array $config) // C2DM $username = $config["android"]["username"]; $password = $config["android"]["password"]; - $source = $config["android"]["source"]; - $timeout = $config["android"]["timeout"]; + $source = $config["android"]["source"]; + $timeout = $config["android"]["timeout"]; if (isset($config["android"]["c2dm"])) { $username = $config["android"]["c2dm"]["username"]; $password = $config["android"]["c2dm"]["password"]; - $source = $config["android"]["c2dm"]["source"]; + $source = $config["android"]["c2dm"]["source"]; } $this->container->setParameter("rms_push_notifications.android.timeout", $timeout); $this->container->setParameter("rms_push_notifications.android.c2dm.username", $username); @@ -98,9 +99,35 @@ protected function setAndroidConfig(array $config) // GCM $this->container->setParameter("rms_push_notifications.android.gcm.enabled", isset($config["android"]["gcm"])); if (isset($config["android"]["gcm"])) { - $this->container->setParameter("rms_push_notifications.android.gcm.api_key", $config["android"]["gcm"]["api_key"]); - $this->container->setParameter("rms_push_notifications.android.gcm.use_multi_curl", $config["android"]["gcm"]["use_multi_curl"]); - $this->container->setParameter('rms_push_notifications.android.gcm.dry_run', $config["android"]["gcm"]["dry_run"]); + $this->container->setParameter( + "rms_push_notifications.android.gcm.api_key", + $config["android"]["gcm"]["api_key"] + ); + $this->container->setParameter( + "rms_push_notifications.android.gcm.use_multi_curl", + $config["android"]["gcm"]["use_multi_curl"] + ); + $this->container->setParameter( + 'rms_push_notifications.android.gcm.dry_run', + $config["android"]["gcm"]["dry_run"] + ); + } + + // FCM + $this->container->setParameter("rms_push_notifications.android.fcm.enabled", isset($config["android"]["fcm"])); + if (isset($config["android"]["fcm"])) { + $this->container->setParameter( + "rms_push_notifications.android.fcm.api_key", + $config["android"]["fcm"]["api_key"] + ); + $this->container->setParameter( + "rms_push_notifications.android.fcm.use_multi_curl", + $config["android"]["fcm"]["use_multi_curl"] + ); + $this->container->setParameter( + 'rms_push_notifications.android.fcm.dry_run', + $config["android"]["fcm"]["dry_run"] + ); } } @@ -127,8 +154,9 @@ protected function setMacConfig(array $config) /** * Sets Apple config into container * - * @param array $config + * @param array $config * @param $os + * * @throws \RuntimeException * @throws \LogicException */ @@ -146,7 +174,7 @@ protected function setAppleConfig(array $config, $os) if (realpath($config[$os]["pem"])) { // Absolute path $pemFile = $config[$os]["pem"]; - } elseif (realpath($this->kernelRootDir.DIRECTORY_SEPARATOR.$config[$os]["pem"]) ) { + } elseif (realpath($this->kernelRootDir.DIRECTORY_SEPARATOR.$config[$os]["pem"])) { // Relative path $pemFile = $this->kernelRootDir.DIRECTORY_SEPARATOR.$config[$os]["pem"]; } else { @@ -158,10 +186,13 @@ protected function setAppleConfig(array $config, $os) if ($config[$os]['json_unescaped_unicode']) { // Not support JSON_UNESCAPED_UNICODE option if (!version_compare(PHP_VERSION, '5.4.0', '>=')) { - throw new \LogicException(sprintf( - 'Can\'t use JSON_UNESCAPED_UNICODE option. This option can use only PHP Version >= 5.4.0. Your version: %s', - PHP_VERSION - )); + throw new \LogicException( + sprintf( + 'Can\'t use JSON_UNESCAPED_UNICODE option. ' . + 'This option can use only PHP Version >= 5.4.0. Your version: %s', + PHP_VERSION + ) + ); } } @@ -169,8 +200,14 @@ protected function setAppleConfig(array $config, $os) $this->container->setParameter(sprintf('rms_push_notifications.%s.timeout', $os), $config[$os]["timeout"]); $this->container->setParameter(sprintf('rms_push_notifications.%s.sandbox', $os), $config[$os]["sandbox"]); $this->container->setParameter(sprintf('rms_push_notifications.%s.pem', $os), $pemFile); - $this->container->setParameter(sprintf('rms_push_notifications.%s.passphrase', $os), $config[$os]["passphrase"]); - $this->container->setParameter(sprintf('rms_push_notifications.%s.json_unescaped_unicode', $os), (bool) $config[$os]['json_unescaped_unicode']); + $this->container->setParameter( + sprintf('rms_push_notifications.%s.passphrase', $os), + $config[$os]["passphrase"] + ); + $this->container->setParameter( + sprintf('rms_push_notifications.%s.json_unescaped_unicode', $os), + (bool)$config[$os]['json_unescaped_unicode'] + ); } /** @@ -182,7 +219,10 @@ protected function setBlackberryConfig(array $config) { $this->container->setParameter("rms_push_notifications.blackberry.enabled", true); $this->container->setParameter("rms_push_notifications.blackberry.timeout", $config["blackberry"]["timeout"]); - $this->container->setParameter("rms_push_notifications.blackberry.evaluation", $config["blackberry"]["evaluation"]); + $this->container->setParameter( + "rms_push_notifications.blackberry.evaluation", + $config["blackberry"]["evaluation"] + ); $this->container->setParameter("rms_push_notifications.blackberry.app_id", $config["blackberry"]["app_id"]); $this->container->setParameter("rms_push_notifications.blackberry.password", $config["blackberry"]["password"]); } @@ -190,6 +230,9 @@ protected function setBlackberryConfig(array $config) protected function setWindowsphoneConfig(array $config) { $this->container->setParameter("rms_push_notifications.windowsphone.enabled", true); - $this->container->setParameter("rms_push_notifications.windowsphone.timeout", $config["windowsphone"]["timeout"]); + $this->container->setParameter( + "rms_push_notifications.windowsphone.timeout", + $config["windowsphone"]["timeout"] + ); } } diff --git a/Device/Types.php b/Device/Types.php index c97e408..a686cbe 100644 --- a/Device/Types.php +++ b/Device/Types.php @@ -6,6 +6,7 @@ class Types { const OS_ANDROID_C2DM = "rms_push_notifications.os.android.c2dm"; const OS_ANDROID_GCM = "rms_push_notifications.os.android.gcm"; + const OS_ANDROID_FCM = "rms_push_notifications.os.android.fcm"; const OS_IOS = "rms_push_notifications.os.ios"; const OS_MAC = "rms_push_notifications.os.mac"; const OS_BLACKBERRY = "rms_push_notifications.os.blackberry"; diff --git a/Message/AndroidMessage.php b/Message/AndroidMessage.php index 1d9f04b..54e0580 100644 --- a/Message/AndroidMessage.php +++ b/Message/AndroidMessage.php @@ -43,6 +43,13 @@ class AndroidMessage implements MessageInterface */ protected $isGCM = false; + /** + * Whether this is a FCM message + * + * @var bool + */ + protected $isFCM = false; + /** * A collection of device identifiers that the message * is intended for. GCM use only @@ -52,11 +59,11 @@ class AndroidMessage implements MessageInterface protected $allIdentifiers = array(); /** - * Options for GCM messages + * Options for FCM messages * * @var array */ - protected $gcmOptions = array(); + protected $fcmOptions = array(); /** * Sets the string message @@ -136,7 +143,7 @@ public function setDeviceIdentifier($identifier) */ public function getTargetOS() { - return ($this->isGCM ? Types::OS_ANDROID_GCM : Types::OS_ANDROID_C2DM); + return ($this->isFCM ? Types::OS_ANDROID_FCM : ($this->isGCM ? Types::OS_ANDROID_GCM : Types::OS_ANDROID_C2DM)); } /** @@ -216,26 +223,89 @@ public function addGCMIdentifier($identifier) * Sets the GCM list * @param array $allIdentifiers */ - public function setAllIdentifiers($allIdentifiers) { + public function setAllIdentifiers($allIdentifiers) + { $this->allIdentifiers = array_combine($allIdentifiers, $allIdentifiers); } /** * Sets GCM options * @param array $options + * @deprecated GCM is being deprecated for FCM, both services share the same options */ public function setGCMOptions($options) { - $this->gcmOptions = $options; + $this->setFCMOptions($options); } /** * Returns GCM options + * @deprecated GCM is being deprecated for FCM, both services share the same options * * @return array */ public function getGCMOptions() { - return $this->gcmOptions; + return $this->getFCMOptions(); + } + + /** + * Set whether this is a FCM message + * (default false) + * + * @param $fcm + */ + public function setFCM($fcm) + { + $this->isFCM = !!$fcm; + } + + /** + * Returns whether this is a GCM message + * + * @return mixed + */ + public function isFCM() + { + return $this->isFCM; + } + + /** + * Returns an array of device identifiers + * Not used in C2DM + * + * @return mixed + */ + public function getFCMIdentifiers() + { + return array_values($this->allIdentifiers); + } + + /** + * Adds a device identifier to the GCM list + * @param string $identifier + */ + public function addFCMIdentifier($identifier) + { + $this->allIdentifiers[$identifier] = $identifier; + } + + /** + * Sets FCM options + * @param array $options + */ + public function setFCMOptions($options) + { + $this->fcmOptions = $options; + } + + /** + * Returns FCM options + * + * @return array + */ + public function getFCMOptions() + { + return $this->fcmOptions; } } diff --git a/README.md b/README.md index e3cc8af..8f1341e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # RMSPushNotificationsBundle ![](https://secure.travis-ci.org/richsage/RMSPushNotificationsBundle.png) -A bundle to allow sending of push notifications to mobile devices. Currently supports Android (C2DM, GCM), Blackberry and iOS devices. +A bundle to allow sending of push notifications to mobile devices. Currently supports Android (C2DM, GCM, FCM), Blackberry and iOS devices. ## Installation @@ -44,6 +44,10 @@ only be available if you provide configuration respectively for them. api_key: # This is titled "Server Key" when creating it use_multi_curl: # default is true dry_run: + fcm: + api_key: # This is titled "Server Key" when creating it + use_multi_curl: # default is true + dry_run: ios: timeout: 60 # Seconds to wait for connection timeout, default is 60 sandbox: @@ -99,6 +103,14 @@ Since both C2DM and GCM are still available, the `AndroidMessage` class has a sm to send as a GCM message rather than C2DM. +Since the deprecation of GCM for FCM, the follow is now recommended: + + use RMS\PushNotificationsBundle\Message\AndroidMessage; + + $message = new AndroidMessage(); + $message->setFCM(true); + + ## iOS Feedback service The Apple Push Notification service also exposes a Feedback service where you can get information about failed push notifications - see [here](https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/CommunicatingWIthAPS.html#//apple_ref/doc/uid/TP40008194-CH101-SW3) for further details. diff --git a/Resources/config/android.xml b/Resources/config/android.xml index 5802e00..703244a 100644 --- a/Resources/config/android.xml +++ b/Resources/config/android.xml @@ -6,6 +6,7 @@ RMS\PushNotificationsBundle\Service\OS\AndroidNotification RMS\PushNotificationsBundle\Service\OS\AndroidGCMNotification + RMS\PushNotificationsBundle\Service\OS\AndroidFCMNotification @@ -30,6 +31,17 @@ + + + %rms_push_notifications.android.fcm.api_key% + %rms_push_notifications.android.fcm.use_multi_curl% + %rms_push_notifications.android.timeout% + + null + %rms_push_notifications.android.fcm.dry_run% + + + diff --git a/Service/OS/AndroidFCMNotification.php b/Service/OS/AndroidFCMNotification.php new file mode 100644 index 0000000..49804ff --- /dev/null +++ b/Service/OS/AndroidFCMNotification.php @@ -0,0 +1,180 @@ +useDryRun = $dryRun; + $this->apiKey = $apiKey; + if (!$client) { + $client = ($useMultiCurl ? new MultiCurl() : new Curl()); + } + $client->setTimeout($timeout); + + $this->browser = new Browser($client); + $this->browser->getClient()->setVerifyPeer(false); + $this->logger = $logger; + } + + /** + * Sends the data to the given registration IDs via the FCM server + * + * @param \RMS\PushNotificationsBundle\Message\MessageInterface $message + * + * @throws \RMS\PushNotificationsBundle\Exception\InvalidMessageTypeException + * @return bool + */ + public function send(MessageInterface $message) + { + if (!$message instanceof AndroidMessage) { + throw new InvalidMessageTypeException( + sprintf( + "Message type '%s' not supported by FCM", + get_class( + $message + ) + ) + ); + } + if (!$message->isFCM()) { + throw new InvalidMessageTypeException("Non-FCM messages not supported by the Android FCM sender"); + } + + $headers = array( + "Authorization: key=".$this->apiKey, + "Content-Type: application/json", + ); + $data = array_merge( + $message->getFCMOptions(), + array("notification" => $message->getData()) + ); + + if ($this->useDryRun) { + $data['dry_run'] = true; + } + + // Perform the calls (in parallel) + $this->responses = array(); + $gcmIdentifiers = $message->getFCMIdentifiers(); + + if (count($message->getFCMIdentifiers()) == 1) { + $data['to'] = $gcmIdentifiers[0]; + $this->responses[] = $this->browser->post($this->apiURL, $headers, json_encode($data)); + } else { + // Chunk number of registration IDs according to the maximum allowed by GCM + $chunks = array_chunk($message->getFCMIdentifiers(), $this->registrationIdMaxCount); + + foreach ($chunks as $registrationIDs) { + $data['registration_ids'] = $registrationIDs; + $this->responses[] = $this->browser->post($this->apiURL, $headers, json_encode($data)); + } + } + + // If we're using multiple concurrent connections via MultiCurl + // then we should flush all requests + if ($this->browser->getClient() instanceof MultiCurl) { + $this->browser->getClient()->flush(); + } + + // Determine success + foreach ($this->responses as $response) { + $message = json_decode($response->getContent()); + if ($message === null || $message->success == 0 || $message->failure > 0) { + if ($message == null) { + $this->logger->error($response->getContent()); + } else { + foreach ($message->results as $result) { + if (isset($result->error)) { + $this->logger->error($result->error); + } + } + } + + return false; + } + } + + return true; + } + + /** + * Returns responses + * + * @return array + */ + public function getResponses() + { + return $this->responses; + } +} diff --git a/Tests/DependencyInjection/ConfigurationTest.php b/Tests/DependencyInjection/ConfigurationTest.php index b6e64aa..78dfb3b 100644 --- a/Tests/DependencyInjection/ConfigurationTest.php +++ b/Tests/DependencyInjection/ConfigurationTest.php @@ -125,6 +125,44 @@ public function testGCMIsOK() $this->assertTrue($config["android"]["gcm"]["dry_run"]); } + /** + * @expectedException \Symfony\Component\Config\Definition\Exception\InvalidConfigurationException + */ + public function testFCMRequiresAPIKey() + { + $arr = array( + array( + "android" => array( + "fcm" => array( + ) + ) + ), + ); + $config = $this->process($arr); + } + + public function testFCMIsOK() + { + $arr = array( + array( + "android" => array( + "fcm" => array( + "api_key" => "foo", + "use_multi_curl" => true, + "dry_run" => false, + ) + ) + ), + ); + $config = $this->process($arr); + $this->assertEquals("foo", $config["android"]["fcm"]["api_key"]); + $this->assertFalse($config["android"]["fcm"]["dry_run"]); + + $arr[0]["android"]["fcm"]["dry_run"] = true; + $config = $this->process($arr); + $this->assertTrue($config["android"]["fcm"]["dry_run"]); + } + /** * @expectedException \Symfony\Component\Config\Definition\Exception\InvalidConfigurationException */ diff --git a/Tests/Message/AndroidMessageTest.php b/Tests/Message/AndroidMessageTest.php index 62d8c76..f2782bc 100644 --- a/Tests/Message/AndroidMessageTest.php +++ b/Tests/Message/AndroidMessageTest.php @@ -81,6 +81,8 @@ public function testTypeChangesBasedOnGCM() $this->assertEquals(Types::OS_ANDROID_C2DM, $msg->getTargetOS()); $msg->setGCM(true); $this->assertEquals(Types::OS_ANDROID_GCM, $msg->getTargetOS()); + $msg->setFCM(true); + $this->assertEquals(Types::OS_ANDROID_FCM, $msg->getTargetOS()); } public function testSetIdentifierIsSingleEntryInGCMArray() diff --git a/composer.json b/composer.json index 2dfd3fa..a233f43 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name": "richsage/rms-push-notifications-bundle", "type": "symfony-bundle", "description": "Push notifications/messages for mobile devices", - "keywords": ["push", "notification", "bundle", "gcm", "c2dm", "ios", "apns", "blackberry", "mpns", "windowsphone"], + "keywords": ["push", "notification", "bundle", "fcm", "gcm", "c2dm", "ios", "apns", "blackberry", "mpns", "windowsphone"], "homepage": "https://github.com/richsage/RMSPushNotificationsBundle", "license": "MIT", "authors": [