Repository: pusher/pusher-http-php (php)

Issue:
Provide a way to asynchronously trigger events
This is an issue to keep track of the demand for triggering event asynchronously.

There are a couple of pull requests to add this functionality but neither provide any tests. See #4 and #11.

We're looking at sorting out a number of libraries so I'm cleaning out some older issues.



Discussion:
This would be super useful, as triggering events inline presents a serious scaling issue.

Has anyone come up with a successful implementation of a trigger_async method for the php client library, or is there a reason this was not possible?

Would be preferable to start using Guzzle here, as proposed in #95 

Note: the Guzzle client to be used should be passable via the constructor as optional argument, so everyone can keep using their Guzzle middleware.
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. If you'd like this issue to stay open please leave a comment indicating how this issue is affecting you. Thank you.

Pinning this to keep stalebot from closing it. This can now be implemented easily with Guzzle.

Patch:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -63,7 +63,8 @@ jobs:
       - name: Prepare README
         run: |
           export MAJOR=$(echo "${{ env.VERSION }}" | cut -d'.' -f1)
-          sed -i "s|\"pusher/pusher-php-server\": \"^[0-9]*\.0\"|\"pusher/pusher-php-server\": \"^${MAJOR}.0\"|" README.md
+          export MINOR=$(echo "${{ env.VERSION }}" | cut -d'.' -f2)
+          sed -i "s|\"pusher/pusher-php-server\": \"^[0-9]*\.[0-9]*\"|\"pusher/pusher-php-server\": \"^${MAJOR}.${MINOR}\"|" README.md
       - name: Prepare Pusher.php
         run: |
           sed -i "s|public static \$VERSION = '[^']*'|public static \$VERSION = '${{ env.VERSION }}'|" src/Pusher.php
diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
 # Changelog
 
+## 6.1.0
+
+* [ADDED] triggerAsync and triggerBatchAsync using the Guzzle async interface.
+
 ## 6.0.1
 
 * [CHANGED] Use type hints where possible (mixed type not available in PHP7).
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -18,7 +18,7 @@ Or add to `composer.json`:
 
 ```json
 "require": {
-	"pusher/pusher-php-server": "^6.0"
+	"pusher/pusher-php-server": "^6.1"
 }
 ```
 
@@ -132,6 +132,24 @@ $batch[] = array('channel' => 'my-channel', 'name' => 'my_event', 'data' => arra
 $pusher->triggerBatch($batch);
 ```
 
+### Asynchronous interface
+
+Both `trigger` and `triggerBatch` have asynchronous counterparts in
+`triggerAsync` and `triggerBatchAsync`. These functions return [Guzzle
+promises](https://github.com/guzzle/promises) which can be chained
+with `->then`:
+
+```php
+$promise = $pusher->triggerAsync( [ 'channel-1', 'channel-2' ], 'my_event', 'hello world' );
+
+$promise->then(function ($result) {
+  // do something with $result
+  return $result;
+});
+
+$final_result = $promise->wait();
+```
+
 ### Arrays
 
 Objects are automatically converted to JSON format:
diff --git a/src/Pusher.php b/src/Pusher.php
--- a/src/Pusher.php
+++ b/src/Pusher.php
@@ -6,6 +6,8 @@
 use Psr\Log\LoggerAwareTrait;
 use Psr\Log\LoggerInterface;
 use Psr\Log\LogLevel;
+use GuzzleHttp\Psr7\Request;
+use GuzzleHttp\Promise\PromiseInterface;
 
 class Pusher implements LoggerAwareInterface, PusherInterface
 {
@@ -14,7 +16,7 @@ class Pusher implements LoggerAwareInterface, PusherInterface
     /**
      * @var string Version
      */
-    public static $VERSION = '6.0.1';
+    public static $VERSION = '6.1.0';
 
     /**
      * @var null|PusherCrypto
@@ -326,8 +328,8 @@ public static function array_implode($glue, $separator, $array)
     }
 
     /**
-     * Trigger an event by providing event name and payload.
-     * Optionally provide a socket ID to exclude a client (most likely the sender).
+     * Helper function to prepare trigger request. Takes the same
+     * parameters as the public trigger functions.
      *
      * @param array|string $channels        A channel name or an array of channel names to publish the event on.
      * @param string       $event
@@ -340,7 +342,7 @@ public static function array_implode($glue, $separator, $array)
      * @throws GuzzleException
      *
      */
-    public function trigger($channels, $event, $data, $params = array(), $already_encoded = false) : object
+    public function make_request($channels, $event, $data, $params = array(), $already_encoded = false) : Request
     {
         if (is_string($channels) === true) {
             $channels = array($channels);
@@ -355,6 +357,7 @@ public function trigger($channels, $event, $data, $params = array(), $already_en
         foreach ($channels as $chan) {
             if (PusherCrypto::is_encrypted_channel($chan)) {
                 $has_encrypted_channel = true;
+                break;
             }
         }
 
@@ -400,10 +403,33 @@ public function trigger($channels, $event, $data, $params = array(), $already_en
             'X-Pusher-Library' => 'pusher-http-php '.self::$VERSION
         ];
 
-        $response = $this->client->post($path, [
-            'query' => array_merge($signature, $query_params),
-            'headers' => $headers,
-            'body' => $post_value,
+        $params = array_merge($signature, $query_params);
+        $query_string = self::array_implode('=', '&', $params);
+        $full_path = $path."?".$query_string;
+        $request = new Request('POST', $full_path, $headers, $post_value);
+
+        return $request;
+    }
+
+    /**
+     * Trigger an event by providing event name and payload.
+     * Optionally provide a socket ID to exclude a client (most likely the sender).
+     *
+     * @param array|string $channels        A channel name or an array of channel names to publish the event on.
+     * @param string       $event
+     * @param mixed        $data            Event data
+     * @param array        $params          [optional]
+     * @param bool         $already_encoded [optional]
+     *
+     * @throws PusherException   Throws PusherException if $channels is an array of size 101 or above or $socket_id is invalid
+     * @throws ApiErrorException Throws ApiErrorException if the Channels HTTP API responds with an error
+     * @throws GuzzleException
+     *
+     */
+    public function trigger($channels, $event, $data, $params = array(), $already_encoded = false) : object {
+        $request = $this->make_request($channels, $event, $data, $params, $already_encoded);
+
+        $response = $this->client->send($request, [
             'http_errors' => false,
             'base_uri' => $this->channels_url_prefix()
         ]);
@@ -425,16 +451,53 @@ public function trigger($channels, $event, $data, $params = array(), $already_en
     }
 
     /**
-     * Trigger multiple events at the same time.
+     * Asynchronously trigger an event by providing event name and payload.
+     * Optionally provide a socket ID to exclude a client (most likely the sender).
+     *
+     * @param array|string $channels        A channel name or an array of channel names to publish the event on.
+     * @param string       $event
+     * @param mixed        $data            Event data
+     * @param array        $params          [optional]
+     * @param bool         $already_encoded [optional]
+     *
+     */
+    public function triggerAsync($channels, $event, $data, $params = array(), $already_encoded = false) : PromiseInterface
+    {
+        $request = $this->make_request($channels, $event, $data, $params, $already_encoded);
+
+        $promise = $this->client->sendAsync($request, [
+            'http_errors' => false,
+            'base_uri' => $this->channels_url_prefix()
+        ])->then(function ($response) {
+            $status = $response->getStatusCode();
+
+            if ($status !== 200) {
+                $body = (string) $response->getBody();
+                throw new ApiErrorException($body, $status);
+            }
+
+            $result = json_decode($response->getBody());
+
+            if (property_exists($result, 'channels')) {
+                $result->channels = get_object_vars($result->channels);
+            }
+
+            return $result;
+        });
+
+        return $promise;
+    }
+
+    /**
+     * Helper function to prepare batch trigger request. Takes the same                                                                                                                                               * parameters as the public batch trigger functions.
      *
      * @param array $batch           [optional] An array of events to send
      * @param bool  $already_encoded [optional]
      *
      * @throws ApiErrorException Throws ApiErrorException if the Channels HTTP API responds with an error
-     * @throws GuzzleException
      *
-     */
-    public function triggerBatch($batch = array(), $already_encoded = false) : object
+     **/
+    public function make_batch_request($batch = array(), $already_encoded = false) : Request
     {
         foreach ($batch as $key => $event) {
             $this->validate_channel($event['channel']);
@@ -471,10 +534,29 @@ public function triggerBatch($batch = array(), $already_encoded = false) : objec
             'X-Pusher-Library' => 'pusher-http-php '.self::$VERSION
         ];
 
-        $response = $this->client->post($path, [
-            'query' => array_merge($query_params, $signature),
-            'body' => $post_value,
-            'headers' => $headers,
+        $params = array_merge($signature, $query_params);
+        $query_string = self::array_implode('=', '&', $params);
+        $full_path = $path."?".$query_string;
+        $request = new Request('POST', $full_path, $headers, $post_value);
+
+        return $request;
+    }
+
+    /**
+     * Trigger multiple events at the same time.
+     *
+     * @param array $batch           [optional] An array of events to send
+     * @param bool  $already_encoded [optional]
+     *
+     * @throws ApiErrorException Throws ApiErrorException if the Channels HTTP API responds with an error
+     * @throws GuzzleException
+     *
+     */
+    public function triggerBatch($batch = array(), $already_encoded = false) : object
+    {
+        $request = $this->make_batch_request($batch, $already_encoded);
+
+        $response = $this->client->send($request, [
             'http_errors' => false,
             'base_uri' => $this->channels_url_prefix()
         ]);
@@ -486,7 +568,50 @@ public function triggerBatch($batch = array(), $already_encoded = false) : objec
             throw new ApiErrorException($body, $status);
         }
 
-        return json_decode($response->getBody());
+        $result = json_decode($response->getBody());
+
+        if (property_exists($result, 'channels')) {
+            $result->channels = get_object_vars($result->channels);
+        }
+
+        return $result;
+    }
+
+    /**
+     * Asynchronously trigger multiple events at the same time.
+     *
+     * @param array $batch           [optional] An array of events to send
+     * @param bool  $already_encoded [optional]
+     *
+     * @throws ApiErrorException Throws ApiErrorException if the Channels HTTP API responds with an error
+     *
+     */
+    public function triggerBatchAsync($batch = array(), $already_encoded = false) : PromiseInterface
+    {
+        $request = $this->make_batch_request($batch, $already_encoded);
+
+        $promise = $this->client->sendAsync($request, [
+            'http_errors' => false,
+            'base_uri' => $this->channels_url_prefix()
+        ])->then(function ($response) {
+            $status = $response->getStatusCode();
+
+            if ($status !== 200) {
+                $body = (string) $response->getBody();
+                throw new ApiErrorException($body, $status);
+            }
+
+            $result = json_decode($response->getBody());
+
+            if (property_exists($result, 'channels')) {
+                $result->channels = get_object_vars($result->channels);
+            }
+
+            return $result;
+        });
+
+        return $promise;
+
     }
 
     /**
diff --git a/src/PusherInterface.php b/src/PusherInterface.php
--- a/src/PusherInterface.php
+++ b/src/PusherInterface.php
@@ -1,6 +1,7 @@
 <?php
 
 namespace Pusher;
+use GuzzleHttp\Promise\PromiseInterface;
 
 interface PusherInterface
 {
@@ -29,7 +30,20 @@ public function getSettings();
     public function trigger($channels, $event, $data, $socket_id = null, $already_encoded = false) : object;
 
     /**
-     * Trigger multiple events at the same time.
+     * Asynchronously trigger an event by providing event name and payload.
+     * Optionally provide a socket ID to exclude a client (most likely the sender).
+     *
+     * @param array|string $channels        A channel name or an array of channel names to publish the event on.
+     * @param string       $event
+     * @param mixed        $data            Event data
+     * @param array        $params          [optional]
+     * @param bool         $already_encoded [optional]
+     *
+     */
+    public function triggerAsync($channels, $event, $data, $params = array(), $already_encoded = false) : PromiseInterface;
+    
+    /**
+     * 
      *
      * @param array $batch           [optional] An array of events to send
      * @param bool  $already_encoded [optional]
@@ -42,7 +56,19 @@ public function trigger($channels, $event, $data, $socket_id = null, $already_en
     public function triggerBatch($batch = array(), $already_encoded = false) : object;
 
     /**
-     * Fetch channel information for a specific channel.
+     * Trigger multiple events at the same time.
+     *
+     * @param array $batch           [optional] An array of events to send
+     * @param bool  $already_encoded [optional]
+     *
+     * @throws PusherException   Throws exception if curl wasn't initialized correctly
+     * @throws ApiErrorException Throws ApiErrorException if the Channels HTTP API responds with an error
+     *
+     */
+    public function triggerBatchAsync($batch = array(), $already_encoded = false) : PromiseInterface;
+
+    /**
+     * Asynchronously trigger multiple events at the same time.
      *
      * @param string $channel The name of the channel
      * @param array  $params  Additional parameters for the query e.g. $params = array( 'info' => 'connection_count' )


Test patch:
diff --git a/tests/acceptance/TriggerAsyncTest.php b/tests/acceptance/TriggerAsyncTest.php
new file mode 100644
--- /dev/null
+++ b/tests/acceptance/TriggerAsyncTest.php
@@ -0,0 +1,121 @@
+<?php
+
+class TriggerAsyncTest extends PHPUnit\Framework\TestCase
+{
+    protected function setUp(): void
+    {
+        if (PUSHERAPP_AUTHKEY === '' || PUSHERAPP_SECRET === '' || PUSHERAPP_APPID === '') {
+            $this->markTestSkipped('Please set the
+            PUSHERAPP_AUTHKEY, PUSHERAPP_SECRET and
+            PUSHERAPP_APPID keys.');
+        } else {
+            $this->pusher = new Pusher\Pusher(PUSHERAPP_AUTHKEY, PUSHERAPP_SECRET, PUSHERAPP_APPID, ['host' => PUSHERAPP_HOST]);
+        }
+    }
+
+    public function testObjectConstruct()
+    {
+        $this->assertNotNull($this->pusher, 'Created new Pusher\Pusher object');
+    }
+
+    public function testStringPush()
+    {
+        $result = $this->pusher->triggerAsync('test_channel', 'my_event', 'Test string')->wait();
+        $this->assertEquals(new stdClass(), $result);
+    }
+
+    public function testArrayPush()
+    {
+        $result = $this->pusher->triggerAsync('test_channel', 'my_event', array('test' => 1))->wait();
+        $this->assertEquals(new stdClass(), $result);
+    }
+
+    public function testPushWithSocketId()
+    {
+        $result = $this->pusher->triggerAsync('test_channel', 'my_event', array('test' => 1), array('socket_id' => '123.456'))->wait();
+        $this->assertEquals(new stdClass(), $result);
+    }
+
+    public function testPushWithInfo()
+    {
+        $expectedMyChannel = new stdClass();
+        $expectedMyChannel->subscription_count = 1;
+        $expectedPresenceMyChannel = new stdClass();
+        $expectedPresenceMyChannel->user_count = 0;
+        $expectedPresenceMyChannel->subscription_count = 0;
+        $expectedResult = new stdClass();
+        $expectedResult->channels = array(
+            "my-channel" => $expectedMyChannel,
+            "presence-my-channel" => $expectedPresenceMyChannel,
+        );
+
+        $result = $this->pusher->triggerAsync(['my-channel', 'presence-my-channel'], 'my_event', array('test' => 1), array('info' => 'user_count,subscription_count'))->wait();
+        $this->assertEquals($expectedResult, $result);
+    }
+
+    public function testTLSPush()
+    {
+        $options = array(
+            'useTLS' => true,
+            'host'   => PUSHERAPP_HOST,
+        );
+        $pusher = new Pusher\Pusher(PUSHERAPP_AUTHKEY, PUSHERAPP_SECRET, PUSHERAPP_APPID, $options);
+
+        $result = $pusher->triggerAsync('test_channel', 'my_event', array('encrypted' => 1))->wait();
+        $this->assertEquals(new stdClass(), $result);
+    }
+
+    public function testSendingOver10kBMessageReturns413()
+    {
+        $this->expectException(\Pusher\ApiErrorException::class);
+        $this->expectExceptionCode('413');
+
+        $data = str_pad('', 11 * 1024, 'a');
+        $this->pusher->triggerAsync('test_channel', 'my_event', $data, array(), true)->wait();
+    }
+
+    public function testTriggeringEventOnOver100ChannelsThrowsException()
+    {
+        $this->expectException(\Pusher\PusherException::class);
+
+        $channels = array();
+        while (count($channels) <= 101) {
+            $channels[] = ('channel-'.count($channels));
+        }
+        $data = array('event_name' => 'event_data');
+        $this->pusher->triggerAsync($channels, 'my_event', $data)->wait();
+    }
+
+    public function testTriggeringEventOnMultipleChannels()
+    {
+        $data = array('event_name' => 'event_data');
+        $channels = array('test_channel_1', 'test_channel_2');
+        $result = $this->pusher->triggerAsync($channels, 'my_event', $data)->wait();
+        $this->assertEquals(new stdClass(), $result);
+    }
+
+    public function testTriggeringEventOnPrivateEncryptedChannelSuccess()
+    {
+        $options = ['encryption_master_key_base64' => 'Y0F6UkgzVzlGWk0zaVhxU05JR3RLenR3TnVDejl4TVY=',
+                    'host' => PUSHERAPP_HOST];
+        $this->pusher = new Pusher\Pusher(PUSHERAPP_AUTHKEY, PUSHERAPP_SECRET, PUSHERAPP_APPID, $options);
+
+        $data = array('event_name' => 'event_data');
+        $channels = array('private-encrypted-ceppaio');
+        $result = $this->pusher->triggerAsync($channels, 'my_event', $data)->wait();
+        $this->assertEquals(new stdClass(), $result);
+    }
+
+    public function testTriggeringEventOnMultipleChannelsWithEncryptedChannelPresentError()
+    {
+        $this->expectException(\Pusher\PusherException::class);
+
+        $options = ['encryption_master_key_base64' => 'Y0F6UkgzVzlGWk0zaVhxU05JR3RLenR3TnVDejl4TVY=',
+                    'host' => PUSHERAPP_HOST];
+        $this->pusher = new Pusher\Pusher(PUSHERAPP_AUTHKEY, PUSHERAPP_SECRET, PUSHERAPP_APPID, $options);
+
+        $data = array('event_name' => 'event_data');
+        $channels = array('my-chan-ceppaio', 'private-encrypted-ceppaio');
+        $this->pusher->triggerAsync($channels, 'my_event', $data)->wait();
+    }
+}
diff --git a/tests/acceptance/TriggerBatchAsyncTest.php b/tests/acceptance/TriggerBatchAsyncTest.php
new file mode 100644
--- /dev/null
+++ b/tests/acceptance/TriggerBatchAsyncTest.php
@@ -0,0 +1,234 @@
+<?php
+
+class TriggerBatchAsyncTest extends PHPUnit\Framework\TestCase
+{
+    protected function setUp(): void
+    {
+        if (PUSHERAPP_AUTHKEY === '' || PUSHERAPP_SECRET === '' || PUSHERAPP_APPID === '') {
+            $this->markTestSkipped('Please set the
+            PUSHERAPP_AUTHKEY, PUSHERAPP_SECRET and
+            PUSHERAPP_APPID keys.');
+        } else {
+            $this->pusher = new Pusher\Pusher(PUSHERAPP_AUTHKEY, PUSHERAPP_SECRET, PUSHERAPP_APPID, ['host' => PUSHERAPP_HOST]);
+        }
+    }
+
+    public function testObjectConstruct()
+    {
+        $this->assertNotNull($this->pusher, 'Created new Pusher\Pusher object');
+    }
+
+    public function testSimplePush()
+    {
+        $batch = array();
+        $batch[] = array('channel' => 'test_channel', 'name' => 'my_event', 'data' => array('my' => 'data'));
+        $result = $this->pusher->triggerBatchAsync($batch)->wait();
+        $this->assertEquals(new stdClass(), $result);
+    }
+
+    public function testTLSPush()
+    {
+        $options = array(
+            'useTLS' => true,
+            'host'   => PUSHERAPP_HOST,
+        );
+        $pc = new Pusher\Pusher(PUSHERAPP_AUTHKEY, PUSHERAPP_SECRET, PUSHERAPP_APPID, $options);
+
+        $batch = array();
+        $batch[] = array('channel' => 'test_channel', 'name' => 'my_event', 'data' => array('my' => 'data'));
+        $result = $pc->triggerBatchAsync($batch)->wait();
+        $this->assertEquals(new stdClass(), $result);
+    }
+
+    public function testTriggerBatchNonEncryptedEventsWithObjectPayloads()
+    {
+        $options = array(
+            'useTLS' => true,
+            'host'   => PUSHERAPP_HOST,
+        );
+        $pc = new Pusher\Pusher(PUSHERAPP_AUTHKEY, PUSHERAPP_SECRET, PUSHERAPP_APPID, $options);
+
+        $batch = array();
+        $batch[] = array('channel' => 'test_channel', 'name' => 'my_event', 'data' => array('my' => 'data'));
+        $batch[] = array('channel' => 'mio_canale', 'name' => 'my_event2', 'data' => array('my' => 'data2'));
+        $result = $pc->triggerBatchAsync($batch)->wait();
+        $this->assertEquals(new stdClass(), $result);
+    }
+
+    public function testTriggerBatchWithSingleEvent()
+    {
+        $options = array(
+            'useTLS' => true,
+            'host'   => PUSHERAPP_HOST,
+        );
+        $pc = new Pusher\Pusher(PUSHERAPP_AUTHKEY, PUSHERAPP_SECRET, PUSHERAPP_APPID, $options);
+
+        $batch = array();
+        $batch[] = array('channel' => 'test_channel', 'name' => 'my_event', 'data' => 'test-string');
+        $result = $pc->triggerBatchAsync($batch)->wait();
+        $this->assertEquals(new stdClass(), $result);
+    }
+
+    public function testTriggerBatchWithInfo()
+    {
+        $options = array(
+            'useTLS' => true,
+            'host'   => PUSHERAPP_HOST,
+        );
+        $pc = new Pusher\Pusher(PUSHERAPP_AUTHKEY, PUSHERAPP_SECRET, PUSHERAPP_APPID, $options);
+
+        $expectedMyChannel = new stdClass();
+        $expectedMyChannel->subscription_count = 1;
+        $expectedMyChannel2 = new stdClass();
+        $expectedPresenceMyChannel = new stdClass();
+        $expectedPresenceMyChannel->user_count = 0;
+        $expectedPresenceMyChannel->subscription_count = 0;
+        $expectedResult = new stdClass();
+        $expectedResult->batch = array(
+            $expectedMyChannel,
+            $expectedMyChannel2,
+            $expectedPresenceMyChannel
+        );
+
+        $batch = array();
+        $batch[] = array('channel' => 'my-channel', 'name' => 'my_event', 'data' => 'test-string', 'info' => 'subscription_count');
+        $batch[] = array('channel' => 'my-channel-2', 'name' => 'my_event', 'data' => 'test-string');
+        $batch[] = array('channel' => 'presence-my-channel', 'name' => 'my_event', 'data' => 'test-string', 'info' => 'user_count,subscription_count');
+        $result = $pc->triggerBatchAsync($batch)->wait();
+        $this->assertEquals($expectedResult, $result);
+    }
+
+    public function testTriggerBatchWithMultipleNonEncryptedEventsWithStringPayloads()
+    {
+        $options = array(
+            'useTLS' => true,
+            'host'   => PUSHERAPP_HOST,
+        );
+        $pc = new Pusher\Pusher(PUSHERAPP_AUTHKEY, PUSHERAPP_SECRET, PUSHERAPP_APPID, $options);
+
+        $batch = array();
+        $batch[] = array('channel' => 'test_channel', 'name' => 'my_event', 'data' => 'test-string');
+        $batch[] = array('channel' => 'test_channel2', 'name' => 'my_event2', 'data' => 'test-string2');
+        $result = $pc->triggerBatchAsync($batch)->wait();
+        $this->assertEquals(new stdClass(), $result);
+    }
+
+    public function testTriggerBatchWithMultipleCombinationsofStringAndObjectPayloads()
+    {
+        $options = array(
+            'useTLS' => true,
+            'host'   => PUSHERAPP_HOST,
+        );
+        $pc = new Pusher\Pusher(PUSHERAPP_AUTHKEY, PUSHERAPP_SECRET, PUSHERAPP_APPID, $options);
+
+        $batch = array();
+        $batch[] = array('channel' => 'test_channel', 'name' => 'my_event', 'data' => 'test-string');
+        $batch[] = array('channel' => 'test_channel2', 'name' => 'my_event2', 'data' => array('my' => 'data2'));
+        $result = $pc->triggerBatchAsync($batch)->wait();
+        $this->assertEquals(new stdClass(), $result);
+    }
+
+    public function testTriggerBatchWithWithEncryptedEventSuccess()
+    {
+        $options = array(
+            'useTLS'                       => true,
+            'host'                         => PUSHERAPP_HOST,
+            'encryption_master_key_base64' => 'Y0F6UkgzVzlGWk0zaVhxU05JR3RLenR3TnVDejl4TVY=',
+        );
+        $pc = new Pusher\Pusher(PUSHERAPP_AUTHKEY, PUSHERAPP_SECRET, PUSHERAPP_APPID, $options);
+
+        $batch = array();
+        $batch[] = array('channel' => 'private-encrypted-test_channel', 'name' => 'my_event', 'data' => 'test-string');
+        $result = $pc->triggerBatchAsync($batch)->wait();
+        $this->assertEquals(new stdClass(), $result);
+    }
+
+    public function testTriggerBatchWithMultipleEncryptedEventsSuccess()
+    {
+        $options = array(
+            'useTLS'                       => true,
+            'host'                         => PUSHERAPP_HOST,
+            'encryption_master_key_base64' => 'Y0F6UkgzVzlGWk0zaVhxU05JR3RLenR3TnVDejl4TVY=',
+        );
+        $pc = new Pusher\Pusher(PUSHERAPP_AUTHKEY, PUSHERAPP_SECRET, PUSHERAPP_APPID, $options);
+
+        $batch = array();
+        $batch[] = array('channel' => 'test_channel', 'name' => 'my_event', 'data' => 'test-string');
+        $batch[] = array('channel' => 'private-encrypted-test_channel2', 'name' => 'my_event2', 'data' => 'test-string2');
+        $result = $pc->triggerBatchAsync($batch)->wait();
+        $this->assertEquals(new stdClass(), $result);
+    }
+
+    public function testTriggerBatchWithMultipleCombinationsofStringsAndObjectsWithEncryptedEventSuccess()
+    {
+        $options = array(
+            'useTLS'                       => true,
+            'host'                         => PUSHERAPP_HOST,
+            'encryption_master_key_base64' => 'Y0F6UkgzVzlGWk0zaVhxU05JR3RLenR3TnVDejl4TVY=',
+        );
+        $pc = new Pusher\Pusher(PUSHERAPP_AUTHKEY, PUSHERAPP_SECRET, PUSHERAPP_APPID, $options);
+
+        $batch = array();
+        $batch[] = array('channel' => 'test_channel', 'name' => 'my_event', 'data' => 'secret-string');
+        $batch[] = array('channel' => 'private-encrypted-test_channel2', 'name' => 'my_event2', 'data' => array('my' => 'data2'));
+        $result = $pc->triggerBatchAsync($batch)->wait();
+        $this->assertEquals(new stdClass(), $result);
+    }
+
+    public function testTriggerBatchMultipleEventsWithEncryptedEventWithoutEncryptionMasterKeyError()
+    {
+        $this->expectException(Error::class);
+
+        $options = array(
+            'useTLS' => true,
+            'host'   => PUSHERAPP_HOST,
+        );
+        $pc = new Pusher\Pusher(PUSHERAPP_AUTHKEY, PUSHERAPP_SECRET, PUSHERAPP_APPID, $options);
+
+        $batch = array();
+        $batch[] = array('channel' => 'my_test_chan', 'name' => 'my_event', 'data' => array('my' => 'data'));
+        $batch[] = array('channel' => 'private-encrypted-ceppaio', 'name' => 'my_private_encrypted_event', 'data' => array('my' => 'to_be_encrypted_data_shhhht'));
+        $pc->triggerBatchAsync($batch)->wait();
+    }
+
+    public function testTriggerBatchWithMultipleEncryptedEventsWithEncryptionMasterKeySuccess()
+    {
+        $options = array(
+            'useTLS'                       => true,
+            'host'                         => PUSHERAPP_HOST,
+            'encryption_master_key_base64' => 'Y0F6UkgzVzlGWk0zaVhxU05JR3RLenR3TnVDejl4TVY=',
+        );
+        $pc = new Pusher\Pusher(PUSHERAPP_AUTHKEY, PUSHERAPP_SECRET, PUSHERAPP_APPID, $options);
+
+        $batch = array();
+        $batch[] = array('channel' => 'my_test_chan', 'name' => 'my_event', 'data' => array('my' => 'data'));
+        $batch[] = array('channel' => 'private-encrypted-ceppaio', 'name' => 'my_private_encrypted_event', 'data' => array('my' => 'to_be_encrypted_data_shhhht'));
+        $result = $pc->triggerBatchAsync($batch)->wait();
+        $this->assertEquals(new stdClass(), $result);
+    }
+
+    public function testSendingOver10kBMessageReturns413()
+    {
+        $this->expectException(\Pusher\ApiErrorException::class);
+        $this->expectExceptionMessage('content of this event');
+        $this->expectExceptionCode('413');
+
+        $data = str_pad('', 11 * 1024, 'a');
+        $batch = array();
+        $batch[] = array('channel' => 'test_channel', 'name' => 'my_event', 'data' => $data);
+        $this->pusher->triggerBatchAsync($batch, true)->wait();
+    }
+
+    public function testSendingOver10messagesReturns400()
+    {
+        $this->expectException(\Pusher\ApiErrorException::class);
+        $this->expectExceptionMessage('Batch too large');
+        $this->expectExceptionCode('400');
+
+        $batch = array();
+        foreach (range(1, 11) as $i) {
+            $batch[] = array('channel' => 'test_channel', 'name' => 'my_event', 'data' => array('index' => $i));
+        }
+        $this->pusher->triggerBatchAsync($batch, false)->wait();
+    }
+}