diff --git a/.codacy.yaml b/.codacy.yaml
new file mode 100644
index 00000000..a99dc7ef
--- /dev/null
+++ b/.codacy.yaml
@@ -0,0 +1,4 @@
+---
+exclude_paths:
+ - "tests/**"
+ - "examples/**"
\ No newline at end of file
diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml
index 60afe970..bd760bb3 100644
--- a/.github/workflows/run-tests.yml
+++ b/.github/workflows/run-tests.yml
@@ -61,7 +61,7 @@ jobs:
composer self-update && composer --version
composer install --prefer-dist
- name: Run unit tests
- run: vendor/bin/phpunit --verbose
+ run: composer test
- name: Cancel workflow runs for commit on error
if: failure()
uses: ./.github/.release/actions/actions/utils/fast-jobs-failure
diff --git a/composer.json b/composer.json
index 70742b4d..a6c20455 100644
--- a/composer.json
+++ b/composer.json
@@ -13,7 +13,7 @@
}
],
"scripts": {
- "test": "./vendor/bin/phpunit tests/ --verbose --coverage-clover=coverage.clover",
+ "test": "XDEBUG_MODE=coverage ./vendor/bin/phpunit --coverage-text=coverage.txt --verbose ./tests",
"acceptance-test": [
"mkdir -p tests/Acceptance/reports",
"cp sdk-specifications/features/publish/publish-custom-mssg-type.feature tests/Acceptance/CustomMessageType/publish-custom-mssg-type.feature",
diff --git a/examples/AccessManager.php b/examples/AccessManager.php
new file mode 100644
index 00000000..857eb4c4
--- /dev/null
+++ b/examples/AccessManager.php
@@ -0,0 +1,525 @@
+setPublishKey($publishKey);
+$adminConfig->setSubscribeKey($subscribeKey);
+$adminConfig->setSecretKey($secretKey);
+$adminConfig->setUuid('admin-user');
+
+$admin = new PubNub($adminConfig);
+
+// Regular user instance (without secret key)
+$userConfig = new PNConfiguration();
+$userConfig->setPublishKey($publishKey);
+$userConfig->setSubscribeKey($subscribeKey);
+$userConfig->setUuid('regular-user');
+$user = new PubNub($userConfig);
+
+// Helper function to print test results
+function printResult($testName, $success, $message = '')
+{
+ $status = $success ? "β
PASS" : "β FAIL";
+ echo "$status - $testName";
+ if ($message) {
+ echo ": $message";
+ }
+ echo "\n";
+}
+
+echo "π§ Step 0: Prepare channels and users metadata\n";
+echo "----------------------------------------------\n";
+
+// snippet.prepare_metadata
+// Create channel metadata for demo channels
+$channels = ['public-channel', 'read-only-channel', 'private-channel', 'admin-only-channel'];
+foreach ($channels as $channel) {
+ try {
+ $admin->setChannelMetadata()
+ ->channel($channel)
+ ->setName(ucwords(str_replace('-', ' ', $channel)))
+ ->setDescription("Demo channel for access manager testing - " . $channel)
+ ->setCustom([
+ 'type' => $channel === 'admin-only-channel' ? 'admin' : 'user',
+ 'created' => date('Y-m-d H:i:s'),
+ 'demo' => true
+ ])
+ ->sync();
+ printResult("Create metadata for channel: $channel", true);
+ } catch (Exception $e) {
+ printResult("Create metadata for channel: $channel", false, $e->getMessage());
+ }
+}
+
+echo "\n";
+
+// Create user metadata for demo users
+$users = [
+ 'admin-user' => [
+ 'name' => 'Administrator User',
+ 'email' => 'admin@example.com',
+ 'role' => 'admin'
+ ],
+ 'regular-user' => [
+ 'name' => 'Regular User',
+ 'email' => 'user@example.com',
+ 'role' => 'user'
+ ],
+ 'other-user' => [
+ 'name' => 'Other User',
+ 'email' => 'other@example.com',
+ 'role' => 'user'
+ ]
+];
+
+foreach ($users as $uuid => $userData) {
+ try {
+ $response = $admin->setUUIDMetadata()
+ ->uuid($uuid)
+ ->name($userData['name'])
+ ->email($userData['email'])
+ ->custom([
+ 'role' => $userData['role'],
+ 'created' => date('Y-m-d H:i:s'),
+ 'demo' => true,
+ 'preferences' => [
+ 'notifications' => true,
+ 'theme' => 'light'
+ ]
+ ])
+ ->sync();
+ printResult("Create metadata for user: $uuid", true);
+ } catch (Exception $e) {
+ printResult("Create metadata for user: $uuid", false, $e->getMessage());
+ }
+}
+
+echo "\nMetadata preparation complete!\n\n";
+// snippet.end
+
+// Step 1: Admin grants a comprehensive token
+echo "π Step 1: Admin grants token with different permission levels\n";
+echo "-------------------------------------------------------------\n";
+
+// snippet.grant_token
+try {
+ $token = $admin->grantToken()
+ ->ttl(60) // 60 minutes
+ ->authorizedUuid('regular-user') // Restrict to specific user
+ ->addChannelResources([
+ 'public-channel' => ['read' => true, 'write' => true], // Full access
+ 'read-only-channel' => ['read' => true], // Read only - no write
+ 'private-channel' => ['read' => true, 'write' => true, 'manage' => true] // Full access including manage
+ ])
+ ->addChannelGroupResources([
+ 'user-group' => ['read' => true] // Read only for channel groups
+ ])
+ ->addUuidResources([
+ 'regular-user' => ['get' => true, 'update' => true], // Self metadata access
+ 'other-user' => ['get' => true] // Read-only access to other user's metadata
+ ])
+ ->meta([
+ 'purpose' => 'demo-token',
+ 'issued-by' => 'admin-user'
+ ])
+ ->sync();
+
+ printResult("Grant comprehensive token", true);
+ echo "Generated token: " . substr($token, 0, 50) . "...\n\n";
+} catch (Exception $e) {
+ printResult("Grant comprehensive token", false, $e->getMessage());
+ echo "β οΈ Cannot continue without token. Exiting.\n";
+ exit(1);
+}
+// snippet.end
+
+// Step 2: Parse the token to show its contents
+echo "π Step 2: Parse token to examine embedded permissions\n";
+echo "----------------------------------------------------\n";
+
+// snippet.parse_token
+try {
+ $parsedToken = $admin->parseToken($token);
+ printResult("Parse token", true);
+
+ echo "Token Details:\n";
+ echo "- Version: " . $parsedToken->getVersion() . "\n";
+ echo "- TTL: " . $parsedToken->getTtl() . " minutes\n";
+ echo "- Authorized UUID: " . ($parsedToken->getUuid() ?? 'None') . "\n";
+ echo "- Timestamp: " . date('Y-m-d H:i:s', $parsedToken->getTimestamp()) . "\n";
+
+ // Show channel permissions
+ echo "\nChannel Permissions:\n";
+ foreach (['public-channel', 'read-only-channel', 'private-channel'] as $channel) {
+ $permissions = $parsedToken->getChannelResource($channel);
+ if ($permissions) {
+ echo "- $channel: ";
+ $perms = [];
+ if ($permissions->hasRead()) {
+ $perms[] = 'read';
+ }
+ if ($permissions->hasWrite()) {
+ $perms[] = 'write';
+ }
+ if ($permissions->hasManage()) {
+ $perms[] = 'manage';
+ }
+ echo implode(', ', $perms) ?: 'none';
+ echo "\n";
+ }
+ }
+
+ // Show metadata
+ $metadata = $parsedToken->getMetadata();
+ if ($metadata) {
+ echo "\nToken Metadata:\n";
+ foreach ($metadata as $key => $value) {
+ echo "- $key: $value\n";
+ }
+ }
+} catch (Exception $e) {
+ printResult("Parse token", false, $e->getMessage());
+}
+// snippet.end
+
+echo "\n";
+
+// Step 3: Test user access WITHOUT token (should fail)
+echo "π« Step 3: Test user access WITHOUT token\n";
+echo "----------------------------------------\n";
+
+// snippet.access_denied_without_token
+try {
+ $user->publish()
+ ->channel('public-channel')
+ ->message(['text' => 'Hello without token!'])
+ ->sync();
+ printResult("User publish to public-channel WITHOUT token", false, "Should have failed but succeeded");
+} catch (Exception $e) {
+ printResult("User publish to public-channel WITHOUT token", true, "Correctly denied access");
+}
+
+try {
+ $user->history()
+ ->channel('public-channel')
+ ->count(1)
+ ->sync();
+ printResult("User read from public-channel WITHOUT token", false, "Should have failed but succeeded");
+} catch (Exception $e) {
+ printResult("User read from public-channel WITHOUT token", true, "Correctly denied access");
+}
+// snippet.end
+
+echo "\n";
+
+// Step 4: Set token and test access
+echo "π Step 4: Set token and test permitted operations\n";
+echo "-------------------------------------------------\n";
+
+// snippet.access_granted_with_token
+// Set the token for the user
+$user->setToken($token);
+echo "Token set for user.\n";
+
+// Test allowed operations
+try {
+ $result = $user->publish()
+ ->channel('public-channel')
+ ->message(['text' => 'Hello with token!', 'timestamp' => time()])
+ ->sync();
+ printResult("User publish to public-channel WITH token", true, "Message published successfully");
+} catch (Exception $e) {
+ printResult("User publish to public-channel WITH token", false, $e->getMessage());
+}
+
+try {
+ $result = $user->history()
+ ->channel('public-channel')
+ ->count(5)
+ ->sync();
+ printResult("User read from public-channel WITH token", true, "History retrieved successfully");
+} catch (Exception $e) {
+ printResult("User read from public-channel WITH token", false, $e->getMessage());
+}
+
+try {
+ $result = $user->history()
+ ->channel('private-channel')
+ ->count(5)
+ ->sync();
+ printResult("User read from private-channel WITH token", true, "History retrieved successfully");
+} catch (Exception $e) {
+ printResult("User read from private-channel WITH token", false, $e->getMessage());
+}
+// snippet.end
+
+echo "\n";
+
+// Step 5: Test restricted operations (should fail)
+echo "π« Step 5: Test operations beyond token permissions\n";
+echo "--------------------------------------------------\n";
+
+// snippet.permission_enforcement
+// Test read-only channel (can read but not write)
+try {
+ $user->history()
+ ->channel('read-only-channel')
+ ->count(1)
+ ->sync();
+ printResult("User read from read-only-channel WITH token", true, "Read access granted");
+} catch (Exception $e) {
+ printResult("User read from read-only-channel WITH token", false, $e->getMessage());
+}
+
+try {
+ $user->publish()
+ ->channel('read-only-channel')
+ ->message(['text' => 'Trying to write to read-only channel'])
+ ->sync();
+ printResult("User publish to read-only-channel WITH token", false, "Should have failed but succeeded");
+} catch (Exception $e) {
+ printResult("User publish to read-only-channel WITH token", true, "Correctly denied write access");
+}
+
+// Test channel not in token (should fail)
+try {
+ $user->publish()
+ ->channel('admin-only-channel')
+ ->message(['text' => 'Trying to access admin channel'])
+ ->sync();
+ printResult("User publish to admin-only-channel WITH token", false, "Should have failed but succeeded");
+} catch (Exception $e) {
+ printResult("User publish to admin-only-channel WITH token", true, "Correctly denied - no permissions");
+}
+
+try {
+ $user->history()
+ ->channel('admin-only-channel')
+ ->count(1)
+ ->sync();
+ printResult("User read from admin-only-channel WITH token", false, "Should have failed but succeeded");
+} catch (Exception $e) {
+ printResult("User read from admin-only-channel WITH token", true, "Correctly denied - no permissions");
+}
+// snippet.end
+
+echo "\n";
+
+// Step 6: Test UUID metadata operations
+echo "π€ Step 6: Test UUID metadata operations\n";
+echo "---------------------------------------\n";
+
+// snippet.uuid_metadata_operations
+try {
+ $result = $user->setUUIDMetadata()
+ ->uuid('regular-user')
+ ->name('Regular User')
+ ->email('user@example.com')
+ ->sync();
+ printResult("User update own metadata WITH token", true, "Metadata updated successfully");
+} catch (Exception $e) {
+ printResult("User update own metadata WITH token", false, $e->getMessage());
+}
+
+try {
+ $result = $user->getUUIDMetadata()
+ ->uuid('regular-user')
+ ->sync();
+ printResult("User get own metadata WITH token", true, "Metadata retrieved successfully");
+} catch (Exception $e) {
+ printResult("User get own metadata WITH token", false, $e->getMessage());
+}
+
+try {
+ $result = $user->getUUIDMetadata()
+ ->uuid('other-user')
+ ->sync();
+ printResult("User get other user metadata WITH token", true, "Read-only access allowed");
+} catch (Exception $e) {
+ printResult("User get other user metadata WITH token", false, $e->getMessage());
+}
+
+try {
+ $user->setUUIDMetadata()
+ ->uuid('other-user')
+ ->name('Cannot update this')
+ ->sync();
+ printResult("User update other user metadata WITH token", false, "Should have failed but succeeded");
+} catch (Exception $e) {
+ printResult("User update other user metadata WITH token", true, "Correctly denied - read only");
+}
+// snippet.end
+
+echo "\n";
+
+// Step 7: Admin operations (always work with secret key)
+echo "π Step 7: Admin operations (unrestricted access)\n";
+echo "------------------------------------------------\n";
+
+// snippet.admin_unrestricted_access
+try {
+ $result = $admin->publish()
+ ->channel('admin-only-channel')
+ ->message(['text' => 'Admin message', 'timestamp' => time()])
+ ->sync();
+ printResult("Admin publish to admin-only-channel", true, "Admin has unrestricted access");
+} catch (Exception $e) {
+ printResult("Admin publish to admin-only-channel", false, $e->getMessage());
+}
+
+try {
+ $result = $admin->history()
+ ->channel('admin-only-channel')
+ ->count(5)
+ ->sync();
+ printResult("Admin read from admin-only-channel", true, "Admin has unrestricted access");
+} catch (Exception $e) {
+ printResult("Admin read from admin-only-channel", false, $e->getMessage());
+}
+// snippet.end
+
+echo "\n";
+
+// Step 8: Revoke the token
+echo "ποΈ Step 8: Revoke token and test access\n";
+echo "--------------------------------------\n";
+
+// snippet.revoke_token
+try {
+ $admin->setToken($token);
+ print("Revoking token {{$token}}...\n");
+ $revokeResult = $admin->revokeToken()->token($token)->sync();
+ print_r($revokeResult);
+ $admin->setToken('');
+ printResult("Admin revoke token", true);
+ // token revoke propagation might take some time
+ $attempts = 0;
+ $waitForRevoke = getenv('WAIT_FOR_REVOKE') ?? true;
+ while ($waitForRevoke) {
+ $attempts++;
+ print("Attempt: $attempts\n");
+ if ($attempts > 10) {
+ printResult("User publish after token revocation", false, "Should have failed but succeeded");
+ break;
+ }
+ sleep(10);
+ // Test user access after revocation (should fail)
+ try {
+ $publishResult = $user->publish()
+ ->channel('public-channel')
+ ->message(['text' => 'Hello after revocation!'])
+ ->sync();
+ // print_r($publishResult);
+ // printResult("User publish after token revocation", false, "Should have failed but succeeded");
+ } catch (Exception $e) {
+ printResult("User publish after token revocation", true, "Correctly denied - token revoked");
+ break;
+ }
+ }
+} catch (Exception $e) {
+ printResult("Admin revoke token", false, $e->getMessage());
+}
+// snippet.end
+
+echo "\n";
+
+// Step 9: Pattern-based permissions example
+echo "π― Step 9: Pattern-based permissions example\n";
+echo "-------------------------------------------\n";
+
+// snippet.pattern_permissions
+try {
+ $patternToken = $admin->grantToken()
+ ->ttl(30)
+ ->authorizedUuid('regular-user')
+ ->addChannelPatterns([
+ 'news.*' => ['read' => true], // Read access to all channels starting with 'news'
+ 'chat.*' => ['read' => true, 'write' => true] // Full access to all 'chat' channels
+ ])
+ ->addUuidPatterns([
+ 'user.*' => ['get' => true] // Read access to all UUIDs starting with 'user'
+ ])
+ ->sync();
+
+ printResult("Grant pattern-based token", true);
+ echo "Pattern token: " . substr($patternToken, 0, 50) . "...\n";
+
+ // Parse pattern token
+ try {
+ $parsedPatternToken = $admin->parseToken($patternToken);
+ printResult("Parse pattern token", true);
+
+ echo "\nPattern Permissions:\n";
+ $patterns = $parsedPatternToken->getPatterns();
+ if (isset($patterns['chan'])) {
+ foreach ($patterns['chan'] as $pattern => $perms) {
+ $grantedPerms = [];
+ if (is_array($perms)) {
+ $grantedPerms = array_keys(array_filter($perms));
+ }
+ echo "- Channel pattern '$pattern': " . implode(', ', $grantedPerms) . "\n";
+ }
+ }
+ if (isset($patterns['uuid'])) {
+ foreach ($patterns['uuid'] as $pattern => $perms) {
+ $grantedPerms = [];
+ if (is_array($perms)) {
+ $grantedPerms = array_keys(array_filter($perms));
+ }
+ echo "- UUID pattern '$pattern': " . implode(', ', $grantedPerms) . "\n";
+ }
+ }
+ } catch (Exception $e) {
+ printResult("Parse pattern token", false, $e->getMessage());
+ }
+
+ // Test pattern-based access
+ $user->setToken($patternToken);
+
+ try {
+ $result = $user->history()
+ ->channel('news-sports')
+ ->count(1)
+ ->sync();
+ printResult("User read 'news-sports' with pattern token", true, "Pattern matched - access granted");
+ } catch (Exception $e) {
+ printResult("User read 'news-sports' with pattern token", false, $e->getMessage());
+ }
+
+ try {
+ $result = $user->publish()
+ ->channel('chat-general')
+ ->message(['text' => 'Hello chat!'])
+ ->sync();
+ printResult("User publish to 'chat-general' with pattern token", true, "Pattern matched - access granted");
+ } catch (Exception $e) {
+ printResult("User publish to 'chat-general' with pattern token", false, $e->getMessage());
+ }
+
+ try {
+ $user->publish()
+ ->channel('news-politics')
+ ->message(['text' => 'Cannot write here'])
+ ->sync();
+ printResult("User publish to 'news-politics' with pattern token", false, "Should have failed but succeeded");
+ } catch (Exception $e) {
+ printResult("User publish to 'news-politics' with pattern token", true, "Correctly denied - read only");
+ }
+} catch (Exception $e) {
+ printResult("Grant pattern-based token", false, $e->getMessage());
+}
+// snippet.end
+
+echo "\nπ Access Manager Demo Complete!\n";
diff --git a/examples/AppContext.php b/examples/AppContext.php
new file mode 100644
index 00000000..7679fc20
--- /dev/null
+++ b/examples/AppContext.php
@@ -0,0 +1,496 @@
+setSubscribeKey($subscribeKey);
+$config->setPublishKey($publishKey);
+$config->setUserId("php-app-context-sample-" . time());
+
+$pubnub = new PubNub($config);
+// snippet.end
+
+// snippet.sample_data
+$sampleUsers = [
+ [
+ 'id' => 'user_alice_' . time(),
+ 'name' => 'Alice Johnson',
+ 'email' => 'alice@example.com',
+ 'externalId' => 'EXT_ALICE_001',
+ 'profileUrl' => 'https://example.com/profiles/alice.jpg',
+ 'custom' => [
+ 'department' => 'Engineering',
+ 'role' => 'Senior Developer'
+ ]
+ ],
+ [
+ 'id' => 'user_bob_' . time(),
+ 'name' => 'Bob Smith',
+ 'email' => 'bob@example.com',
+ 'externalId' => 'EXT_BOB_001',
+ 'profileUrl' => 'https://example.com/profiles/bob.jpg',
+ 'custom' => [
+ 'department' => 'Marketing',
+ 'role' => 'Marketing Manager'
+ ]
+ ]
+];
+
+$sampleChannels = [
+ [
+ 'id' => 'channel_general_' . time(),
+ 'name' => 'General Discussion',
+ 'description' => 'Main channel for general discussions',
+ 'custom' => [
+ 'category' => 'company',
+ 'public' => true
+ ]
+ ],
+ [
+ 'id' => 'channel_dev_' . time(),
+ 'name' => 'Development Team',
+ 'description' => 'Development team coordination',
+ 'custom' => [
+ 'category' => 'team',
+ 'public' => false
+ ]
+ ]
+];
+// snippet.end
+
+// snippet.set_user_metadata
+foreach ($sampleUsers as $user) {
+ $setUserMetadataResult = $pubnub->setUuidMetadata()
+ ->uuid($user['id'])
+ ->name($user['name'])
+ ->email($user['email'])
+ ->externalId($user['externalId'])
+ ->profileUrl($user['profileUrl'])
+ ->custom($user['custom'])
+ ->sync();
+ assert($setUserMetadataResult->getId());
+ assert($setUserMetadataResult->getName() === $user['name']);
+ assert($setUserMetadataResult->getEmail() === $user['email']);
+ assert($setUserMetadataResult->getExternalId() === $user['externalId']);
+ assert($setUserMetadataResult->getProfileUrl() === $user['profileUrl']);
+ assert(json_encode($setUserMetadataResult->getCustom()) === json_encode($user['custom']));
+}
+// snippet.end
+
+// snippet.get_user_metadata
+$getUserMetadataResult = $pubnub->getUuidMetadata()
+ ->uuid($sampleUsers[0]['id'])
+ ->sync();
+assert($getUserMetadataResult->getId() === $sampleUsers[0]['id']);
+assert($getUserMetadataResult->getName() === $sampleUsers[0]['name']);
+assert($getUserMetadataResult->getEmail() === $sampleUsers[0]['email']);
+// snippet.end
+
+// snippet.get_all_user_metadata
+$getAllUserMetadataResult = $pubnub->getAllUuidMetadata()
+ ->includeFields(['customFields' => true, 'totalCount' => true])
+ ->limit(10)
+ ->sync();
+assert(count($getAllUserMetadataResult->getData()) >= 2);
+assert($getAllUserMetadataResult->getTotalCount() >= 2);
+assert(isInstanceOf($getAllUserMetadataResult->getData()[0], PNGetUUIDMetadataResult::class));
+// snippet.end
+
+// snippet.update_user_metadata
+$updatedCustomData = [
+ 'department' => 'Engineering - Updated',
+ 'role' => 'Lead Developer',
+ 'lastUpdated' => date('Y-m-d H:i:s')
+];
+
+$updateUserMetadataResult = $pubnub->setUuidMetadata()
+ ->uuid($sampleUsers[0]['id'])
+ ->name($sampleUsers[0]['name'] . ' - Updated')
+ ->email($sampleUsers[0]['email'])
+ ->externalId($sampleUsers[0]['externalId'])
+ ->profileUrl($sampleUsers[0]['profileUrl'])
+ ->custom($updatedCustomData)
+ ->sync();
+assert($updateUserMetadataResult->getName() === $sampleUsers[0]['name'] . ' - Updated');
+// snippet.end
+
+// snippet.remove_user_metadata
+$removeUserMetadataResult = $pubnub->removeUuidMetadata()
+ ->uuid($sampleUsers[1]['id'])
+ ->sync();
+assert($removeUserMetadataResult);
+// snippet.end
+
+// snippet.get_after_remove_error_handling
+// Verify user was removed by checking it's no longer in getUuidMetadata
+try {
+ $getAfterRemoveResult = $pubnub->getUuidMetadata()
+ ->uuid($sampleUsers[1]['id'])
+ ->sync();
+} catch (PubNubServerException $e) {
+ assert($e->getStatusCode() === 404);
+}
+// snippet.end
+
+// Step 3: Channel Management Operations
+
+// snippet.set_channel_metadata
+foreach ($sampleChannels as $channel) {
+ $setChannelMetadataResult = $pubnub->setChannelMetadata()
+ ->channel($channel['id'])
+ ->setName($channel['name'])
+ ->setDescription($channel['description'])
+ ->setCustom($channel['custom'])
+ ->sync();
+ assert($setChannelMetadataResult->getId() === $channel['id']);
+ assert($setChannelMetadataResult->getName() === $channel['name']);
+ assert($setChannelMetadataResult->getDescription() === $channel['description']);
+}
+// snippet.end
+
+// snippet.get_channel_metadata
+$getChannelMetadataResult = $pubnub->getChannelMetadata()
+ ->channel($sampleChannels[0]['id'])
+ ->sync();
+assert($getChannelMetadataResult->getId() === $sampleChannels[0]['id']);
+assert($getChannelMetadataResult->getName() === $sampleChannels[0]['name']);
+// snippet.end
+
+// snippet.get_all_channel_metadata
+$getAllChannelMetadataResult = $pubnub->getAllChannelMetadata()
+ ->includeFields(['customFields' => true, 'totalCount' => true])
+ ->limit(10)
+ ->sync();
+assert(count($getAllChannelMetadataResult->getData()) >= 2);
+// snippet.end
+
+// snippet.update_channel_metadata
+$updatedChannelCustom = [
+ 'category' => 'company-updated',
+ 'public' => true,
+ 'lastModified' => date('Y-m-d H:i:s')
+];
+
+$updateChannelMetadataResult = $pubnub->setChannelMetadata()
+ ->channel($sampleChannels[0]['id'])
+ ->setName($sampleChannels[0]['name'] . ' - Updated')
+ ->setDescription($sampleChannels[0]['description'] . ' - Updated')
+ ->setCustom($updatedChannelCustom)
+ ->sync();
+assert($updateChannelMetadataResult->getName() === $sampleChannels[0]['name'] . ' - Updated');
+// snippet.end
+
+// snippet.remove_channel_metadata
+$removeChannelMetadataResult = $pubnub->removeChannelMetadata()
+ ->channel($sampleChannels[1]['id'])
+ ->sync();
+assert($removeChannelMetadataResult);
+// snippet.end
+
+// Step 4: Channel Membership Operations (User-centric)
+
+// snippet.set_channel_memberships
+$memberships = [
+ new PNChannelMembership($sampleChannels[0]['id']),
+ new PNChannelMembership($sampleChannels[1]['id'])
+];
+
+$setMembershipsResult = $pubnub->setMemberships()
+ ->uuid($sampleUsers[0]['id'])
+ ->memberships($memberships)
+ ->sync();
+assert(count($setMembershipsResult->getData()) >= 1);
+// snippet.end
+
+// snippet.get_channel_memberships
+$membershipIncludes = new PNMembershipIncludes();
+$membershipIncludes->custom()->channel()->channelCustom();
+
+$getMembershipsResult = $pubnub->getMemberships()
+ ->uuid($sampleUsers[0]['id'])
+ ->include($membershipIncludes)
+ ->sync();
+assert(count($getMembershipsResult->getData()) >= 1);
+// snippet.end
+
+// snippet.manage_channel_memberships
+$setMembershipsList = [new PNChannelMembership($sampleChannels[0]['id'])];
+$removeMembershipsList = [new PNChannelMembership($sampleChannels[1]['id'])];
+
+$manageMembershipsResult = $pubnub->manageMemberships()
+ ->uuid($sampleUsers[0]['id'])
+ ->setMemberships($setMembershipsList)
+ ->removeMemberships($removeMembershipsList)
+ ->sync();
+assert(count($manageMembershipsResult->getData()) >= 0);
+// snippet.end
+
+// snippet.remove_channel_memberships
+$removeMembershipsList = [new PNChannelMembership($sampleChannels[0]['id'])];
+
+$removeMembershipsResult = $pubnub->removeMemberships()
+ ->uuid($sampleUsers[0]['id'])
+ ->memberships($removeMembershipsList)
+ ->sync();
+assert($removeMembershipsResult);
+// snippet.end
+
+// Step 5: Channel Members Operations (Channel-centric)
+
+// snippet.set_channel_members
+$channelMembers = [
+ new PNChannelMember($sampleUsers[0]['id']),
+ new PNChannelMember($sampleUsers[1]['id'])
+];
+
+$setMembersResult = $pubnub->setMembers()
+ ->channel($sampleChannels[0]['id'])
+ ->members($channelMembers)
+ ->sync();
+assert(count($setMembersResult->getData()) >= 1);
+// snippet.end
+
+// snippet.get_channel_members
+$memberIncludes = new PNMemberIncludes();
+$memberIncludes->custom()->user()->userCustom();
+
+$getMembersResult = $pubnub->getMembers()
+ ->channel($sampleChannels[0]['id'])
+ ->include($memberIncludes)
+ ->sync();
+assert(count($getMembersResult->getData()) >= 1);
+// snippet.end
+
+// snippet.manage_channel_members
+$setMembersList = [new PNChannelMember($sampleUsers[0]['id'])];
+$removeMembersList = [new PNChannelMember($sampleUsers[1]['id'])];
+
+$manageMembersResult = $pubnub->manageMembers()
+ ->channel($sampleChannels[0]['id'])
+ ->setMembers($setMembersList)
+ ->removeMembers($removeMembersList)
+ ->sync();
+assert(count($manageMembersResult->getData()) >= 0);
+// snippet.end
+
+// snippet.remove_channel_members
+$removeMembersList = [new PNChannelMember($sampleUsers[0]['id'])];
+
+$removeMembersResult = $pubnub->removeMembers()
+ ->channel($sampleChannels[0]['id'])
+ ->members($removeMembersList)
+ ->sync();
+assert($removeMembersResult);
+// snippet.end
+
+// Step 6: Advanced Features (pagination, filtering, sorting)
+
+// snippet.pagination_example
+// Get first page of users
+$firstPageResult = $pubnub->getAllUuidMetadata()
+ ->includeFields(['totalCount' => true])
+ ->limit(1) // Small limit to demonstrate pagination
+ ->sync();
+
+echo "First page: " . count($firstPageResult->getData()) . " users\n";
+echo "Total count: " . $firstPageResult->getTotalCount() . "\n";
+
+// Get next page if available
+if ($firstPageResult->getNext()) {
+ $secondPageResult = $pubnub->getAllUuidMetadata()
+ ->includeFields(['totalCount' => true])
+ ->limit(1)
+ ->page(['next' => $firstPageResult->getNext()])
+ ->sync();
+ echo "Second page: " . count($secondPageResult->getData()) . " users\n";
+}
+// snippet.end
+
+// snippet.filtering_example
+// Filter users by custom field (note: filtering by custom properties may have limitations)
+$filteredUsersResult = $pubnub->getAllUuidMetadata()
+ ->includeFields(['customFields' => true])
+ ->filter("name LIKE '*Alice*'")
+ ->sync();
+
+echo "Filtered users (name contains 'Alice'): " . count($filteredUsersResult->getData()) . "\n";
+
+// Filter channels by name
+$filteredChannelsResult = $pubnub->getAllChannelMetadata()
+ ->includeFields(['customFields' => true])
+ ->filter("name LIKE '*General*'")
+ ->sync();
+
+echo "Filtered channels (name contains 'General'): " . count($filteredChannelsResult->getData()) . "\n";
+// snippet.end
+
+// snippet.sorting_example
+// Sort users by name ascending
+$sortedUsersAscResult = $pubnub->getAllUuidMetadata()
+ ->sort(['name' => 'asc'])
+ ->limit(5)
+ ->sync();
+
+echo "Users sorted by name (ascending): " . count($sortedUsersAscResult->getData()) . "\n";
+foreach ($sortedUsersAscResult->getData() as $user) {
+ echo " - " . ($user->getName() ?: $user->getId()) . "\n";
+}
+
+// Sort users by updated time descending
+$sortedUsersDescResult = $pubnub->getAllUuidMetadata()
+ ->sort(['updated' => 'desc'])
+ ->limit(3)
+ ->sync();
+
+echo "Users sorted by updated time (descending): " . count($sortedUsersDescResult->getData()) . "\n";
+
+// Sort channels by name
+$sortedChannelsResult = $pubnub->getAllChannelMetadata()
+ ->sort(['name' => 'asc'])
+ ->limit(5)
+ ->sync();
+
+echo "Channels sorted by name: " . count($sortedChannelsResult->getData()) . "\n";
+// snippet.end
+
+// snippet.custom_fields_example
+// Create user with complex custom data
+$complexCustomData = [
+ 'profile' => [
+ 'avatar' => 'https://example.com/avatar.jpg',
+ 'bio' => 'Software engineer with 10+ years experience',
+ 'social' => [
+ 'twitter' => '@johndoe',
+ 'linkedin' => '/in/johndoe'
+ ]
+ ],
+ 'preferences' => [
+ 'theme' => 'dark',
+ 'language' => 'en',
+ 'notifications' => [
+ 'email' => true,
+ 'push' => false,
+ 'sms' => true
+ ]
+ ],
+ 'metadata' => [
+ 'created' => date('Y-m-d H:i:s'),
+ 'source' => 'api',
+ 'version' => '1.0'
+ ]
+];
+
+$complexUserResult = $pubnub->setUuidMetadata()
+ ->uuid('user_complex_' . time())
+ ->name('John Doe')
+ ->email('john.doe@example.com')
+ ->custom($complexCustomData)
+ ->sync();
+
+echo "Created user with complex custom data: " . $complexUserResult->getName() . "\n";
+echo "Custom data keys: " . implode(', ', array_keys((array)$complexUserResult->getCustom())) . "\n";
+// snippet.end
+
+// snippet.include_fields_example
+// Get users with selective field inclusion
+$selectiveFieldsResult = $pubnub->getAllUuidMetadata()
+ ->includeFields(['customFields' => true, 'totalCount' => false])
+ ->limit(2)
+ ->sync();
+
+echo "Users with custom fields (no total count): " . count($selectiveFieldsResult->getData()) . "\n";
+echo "Total count included: " . ($selectiveFieldsResult->getTotalCount() ? 'Yes' : 'No') . "\n";
+
+// Get memberships with channel details (using deprecated method for compatibility)
+if (!empty($sampleUsers)) {
+ try {
+ $membershipWithChannelResult = $pubnub->getMemberships()
+ ->uuid($sampleUsers[0]['id'])
+ ->includeFields(['channelFields' => true, 'customFields' => true])
+ ->limit(5)
+ ->sync();
+
+ echo "Memberships with channel details: " . count($membershipWithChannelResult->getData()) . "\n";
+ } catch (PubNubServerException $e) {
+ echo "Memberships query skipped (no memberships exist): " . $e->getStatusCode() . "\n";
+ }
+}
+// snippet.end
+
+// snippet.etag_example
+// Get user metadata with ETag
+$userWithEtagResult = $pubnub->getUuidMetadata()
+ ->uuid($sampleUsers[0]['id'])
+ ->sync();
+
+$etag = $userWithEtagResult->getETag();
+var_dump($etag);
+echo "User ETag: " . ($etag ?: 'Not available') . "\n";
+
+// Attempt conditional update using ETag
+if ($etag) {
+ try {
+ $conditionalUpdateResult = $pubnub->setUuidMetadata()
+ ->uuid($sampleUsers[0]['id'])
+ ->name($sampleUsers[0]['name'] . ' - Conditional Update')
+ ->email($sampleUsers[0]['email'])
+ ->ifMatchesEtag($etag)
+ ->sync();
+
+ echo "Conditional update successful: " . $conditionalUpdateResult->getName() . "\n";
+ } catch (PubNubServerException $e) {
+ if ($e->getStatusCode() === 412) {
+ echo "Conditional update failed: ETag mismatch (HTTP 412)\n";
+ } else {
+ echo "Update failed: " . $e->getMessage() . "\n";
+ }
+ }
+}
+// snippet.end
+
+// snippet.combined_advanced_features
+// Simplified complex query to avoid filter issues
+$advancedQueryResult = $pubnub->getAllUuidMetadata()
+ ->includeFields(['customFields' => true, 'totalCount' => true])
+ ->sort(['name' => 'asc'])
+ ->limit(5)
+ ->sync();
+
+echo "Advanced query results: " . count($advancedQueryResult->getData()) . " users\n";
+echo "Total available: " . ($advancedQueryResult->getTotalCount() ?: 'Unknown') . "\n";
+echo "Next page available: " . ($advancedQueryResult->getNext() ? 'Yes' : 'No') . "\n";
+
+// Get channel members with comprehensive includes (using deprecated method for compatibility)
+if (!empty($sampleChannels)) {
+ try {
+ $comprehensiveMembersResult = $pubnub->getMembers()
+ ->channel($sampleChannels[0]['id'])
+ ->includeFields(['customFields' => true, 'UUIDFields' => true, 'totalCount' => true])
+ ->sort(['name' => 'asc'])
+ ->limit(10)
+ ->sync();
+
+ echo "Channel members with full details: " . count($comprehensiveMembersResult->getData()) . "\n";
+ } catch (PubNubServerException $e) {
+ echo "Channel members query skipped (no members exist): " . $e->getStatusCode() . "\n";
+ }
+}
+// snippet.end
diff --git a/examples/FileSharing.php b/examples/FileSharing.php
index b855bbd4..67a9b5f0 100644
--- a/examples/FileSharing.php
+++ b/examples/FileSharing.php
@@ -5,8 +5,9 @@
use PubNub\PubNub;
use PubNub\PNConfiguration;
+// snippet.setup
$channelName = "file-channel";
-$fileName = "pn.gif";
+$testFileName = "pn.gif";
$config = new PNConfiguration();
$config->setSubscribeKey(getenv('SUBSCRIBE_KEY', 'demo'));
@@ -14,19 +15,51 @@
$config->setUserId('example');
$pubnub = new PubNub($config);
+// snippet.end
-// Sending file
-$fileHandle = fopen(__DIR__ . DIRECTORY_SEPARATOR . $fileName, "r");
+// snippet.send_file
+$fileHandle = fopen(__DIR__ . DIRECTORY_SEPARATOR . $testFileName, "r");
$sendFileResult = $pubnub->sendFile()
->channel($channelName)
- ->fileName($fileName)
+ ->fileName($testFileName)
->message("Hello from PHP SDK")
->fileHandle($fileHandle)
->sync();
-
fclose($fileHandle);
+$fileId = $sendFileResult->getFileId();
+$fileName = $sendFileResult->getFileName();
+
+print("File uploaded successfully: {$fileName} with ID: {$fileId}\n");
+// snippet.end
+
+// snippet.send_file_with_just_content
+$fileContent = file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . $testFileName);
+$sendFileResult = $pubnub->sendFile()
+ ->channel($channelName)
+ ->fileName($testFileName)
+ ->message("Hello from PHP SDK")
+ ->fileContent($fileContent)
+ ->sync();
+$fileId = $sendFileResult->getFileId();
+$fileName = $sendFileResult->getFileName();
+print("File uploaded successfully: {$fileName} with ID: {$fileId}\n");
+// snippet.end
+
+// snippet.publish_file_with_message
+$publishFileMessageResult = $pubnub->publishFileMessage()
+ ->channel($channelName)
+ ->fileId($fileId)
+ ->fileName($testFileName)
+ ->message("Hello from PHP SDK")
+ ->ttl(10)
+ ->meta(["key" => "value"])
+ ->customMessageType("custom")
+ ->sync();
+$timestamp = $publishFileMessageResult->getTimestamp();
+print("File message published successfully: {$timestamp}\n");
+// snippet.end
-// Listing files in the channel
+// snippet.list_files
$channelFiles = $pubnub->listFiles()->channel($channelName)->sync();
$fileCount = $channelFiles->getCount();
if ($fileCount > 0) {
@@ -38,19 +71,21 @@
} else {
print("There are no files in the channel {$channelName}\n");
}
+// snippet.end
+// snippet.get_download_url
$file = $channelFiles->getFiles()[0];
-print('Getting download URL for the file...');
+print("Getting download URL for the file...\n");
$downloadUrl = $pubnub->getFileDownloadUrl()
->channel($channelName)
->fileId($file->getId())
->fileName($file->getName())
->sync();
-printf("To download the file use the following URL: %s\n", $downloadUrl->getFileUrl());
+print("To download the file use the following URL: {$downloadUrl->getFileUrl()}\n");
-print("Downloading file...");
+print("Downloading file... ");
$downloadFile = $pubnub->downloadFile()
->channel($channelName)
->fileId($file->getId())
@@ -58,8 +93,9 @@
->sync();
file_put_contents(__DIR__ . DIRECTORY_SEPARATOR . $file->getName(), $downloadFile->getFileContent());
print("done. File saved as: {$file->getName()}\n");
+// snippet.end
-// deleting file
+// snippet.delete_file
$deleteFile = $pubnub->deleteFile()
->channel($channelName)
->fileId($file->getId())
@@ -71,3 +107,27 @@
} else {
print("Failed to delete file\n");
}
+// snippet.end
+
+// snippet.delete_all_files
+$fileList = $pubnub->listFiles()->channel($channelName)->sync();
+$fileCount = $fileList->getCount();
+if ($fileCount > 0) {
+ print("There are {$fileCount} files left in the channel {$channelName}\n");
+ foreach ($fileList->getFiles() as $idx => $file) {
+ $deleteFile = $pubnub->deleteFile()
+ ->channel($channelName)
+ ->fileId($file->getId())
+ ->fileName($file->getName())
+ ->sync();
+
+ if ($deleteFile->getStatus() === 200) {
+ print("File {$file->getId()} deleted successfully\n");
+ } else {
+ print("Failed to delete file {$file->getId()}\n");
+ }
+ }
+} else {
+ print("There are no files in the channel {$channelName}\n");
+}
+// snippet.end
diff --git a/examples/MessageActions.php b/examples/MessageActions.php
new file mode 100644
index 00000000..3f60d9ee
--- /dev/null
+++ b/examples/MessageActions.php
@@ -0,0 +1,493 @@
+setSubscribeKey($subscribeKey);
+$config->setPublishKey($publishKey);
+$config->setUserId("php-message-actions-demo-" . time());
+
+$pubnub = new PubNub($config);
+// snippet.end
+
+// Sample data for testing
+$testChannel = "message-actions-demo-" . time();
+$publishedMessages = [];
+
+echo "π Setting up test environment...\n";
+echo "Channel: $testChannel\n";
+echo "User ID: " . $config->getUserId() . "\n\n";
+
+// Publish some test messages to work with
+echo "π€ Publishing test messages...\n";
+for ($i = 1; $i <= 3; $i++) {
+ try {
+ $result = $pubnub->publish()
+ ->channel($testChannel)
+ ->message([
+ "id" => "msg_$i",
+ "text" => "Test message #$i for Message Actions demo",
+ "timestamp" => time(),
+ "sender" => $config->getUserId()
+ ])
+ ->sync();
+
+ $publishedMessages[] = [
+ 'id' => "msg_$i",
+ 'timetoken' => $result->getTimetoken()
+ ];
+ echo "β
Published message #$i - Timetoken: {$result->getTimetoken()}\n";
+ } catch (PubNubException $e) {
+ echo "β Failed to publish message #$i: " . $e->getMessage() . "\n";
+ }
+}
+
+echo "\n";
+
+// Phase 2: Core Message Actions Features
+
+// 2.1 Add Message Actions
+echo "=== PHASE 2.1: Adding Message Actions ===\n\n";
+
+$messageActions = [];
+
+// Add emoji reactions
+echo "π Adding emoji reactions...\n";
+$emojiReactions = [
+ ['type' => 'reaction', 'value' => 'π'],
+ ['type' => 'reaction', 'value' => 'β€οΈ'],
+ ['type' => 'reaction', 'value' => 'π'],
+ ['type' => 'reaction', 'value' => 'π']
+];
+
+foreach ($publishedMessages as $index => $msg) {
+ if ($index < 2) { // Add reactions to first 2 messages
+ $reaction = $emojiReactions[$index * 2];
+ try {
+ // snippet.add_emoji_reaction
+ $messageAction = new PNMessageAction([
+ 'type' => $reaction['type'],
+ 'value' => $reaction['value'],
+ 'messageTimetoken' => $msg['timetoken']
+ ]);
+
+ $result = $pubnub->addMessageAction()
+ ->channel($testChannel)
+ ->messageAction($messageAction)
+ ->sync();
+
+ $messageActions[] = [
+ 'messageTimetoken' => $msg['timetoken'],
+ 'actionTimetoken' => $result->actionTimetoken,
+ 'type' => $reaction['type'],
+ 'value' => $reaction['value']
+ ];
+
+ echo "β
Added {$reaction['value']} - Action Timetoken: {$result->actionTimetoken}\n";
+ // snippet.end
+ } catch (PubNubException $e) {
+ echo "β Failed to add reaction to message {$msg['id']}: " . $e->getMessage() . "\n";
+ }
+ }
+}
+
+// Add read receipts
+echo "\nπ Adding read receipts...\n";
+foreach ($publishedMessages as $index => $msg) {
+ try {
+ // snippet.add_read_receipt
+ $result = $pubnub->addMessageAction()
+ ->channel($testChannel)
+ ->messageAction(new PNMessageAction([
+ 'type' => 'receipt',
+ 'value' => 'read',
+ 'messageTimetoken' => $msg['timetoken']
+ ]))
+ ->sync();
+
+ $messageActions[] = [
+ 'messageTimetoken' => $msg['timetoken'],
+ 'actionTimetoken' => $result->actionTimetoken,
+ 'type' => 'receipt',
+ 'value' => 'read'
+ ];
+
+ echo "β
Added read receipt to message {$msg['id']} - Action Timetoken: {$result->actionTimetoken}\n";
+ // snippet.end
+ } catch (PubNubException $e) {
+ echo "β Failed to add read receipt to message {$msg['id']}: " . $e->getMessage() . "\n";
+ }
+}
+
+// Add custom metadata
+echo "\nπ·οΈ Adding custom metadata...\n";
+$customActions = [
+ ['type' => 'priority', 'value' => 'high'],
+ ['type' => 'category', 'value' => 'announcement'],
+ ['type' => 'flag', 'value' => 'important']
+];
+
+foreach ($publishedMessages as $index => $msg) {
+ if ($index < count($customActions)) {
+ $action = $customActions[$index];
+ try {
+ // snippet.add_custom_metadata
+ $result = $pubnub->addMessageAction()
+ ->channel($testChannel)
+ ->messageAction(new PNMessageAction([
+ 'type' => $action['type'],
+ 'value' => $action['value'],
+ 'messageTimetoken' => $msg['timetoken']
+ ]))
+ ->sync();
+
+ $messageActions[] = [
+ 'messageTimetoken' => $msg['timetoken'],
+ 'actionTimetoken' => $result->actionTimetoken,
+ 'type' => $action['type'],
+ 'value' => $action['value']
+ ];
+
+ echo "β
Added {$action['type']}:{$action['value']} - Action Timetoken: {$result->actionTimetoken}\n";
+ // snippet.end
+ } catch (PubNubException $e) {
+ echo "β Failed to add custom metadata to message {$msg['id']}: " . $e->getMessage() . "\n";
+ }
+ }
+}
+
+echo "\n";
+
+// 2.2 Retrieve Message Actions
+echo "=== PHASE 2.2: Retrieving Message Actions ===\n\n";
+
+// Get all message actions
+echo "π Retrieving all message actions...\n";
+try {
+ // snippet.get_all_message_actions
+ $result = $pubnub->getMessageActions()
+ ->channel($testChannel)
+ ->setLimit(100)
+ ->sync();
+
+ echo "β
Retrieved " . count($result->actions) . " message actions:\n";
+ foreach ($result->actions as $action) {
+ echo " - {$action->type}:{$action->value} (A: {$action->actionTimetoken}, M: {$action->messageTimetoken})\n";
+ }
+ // snippet.end
+} catch (PubNubException $e) {
+ echo "β Failed to retrieve message actions: " . $e->getMessage() . "\n";
+}
+
+// Get message actions with time range
+echo "\nπ
Retrieving message actions with time range...\n";
+if (count($messageActions) >= 2) {
+ try {
+ // snippet.get_message_actions_with_range
+ $result = $pubnub->getMessageActions()
+ ->channel($testChannel)
+ ->setStart($messageActions[count($messageActions) - 1]['actionTimetoken'])
+ ->setEnd($messageActions[0]['actionTimetoken'])
+ ->setLimit(50)
+ ->sync();
+
+ echo "β
Retrieved " . count($result->actions) . " message actions in range:\n";
+ foreach ($result->actions as $action) {
+ echo " - {$action->type}:{$action->value} (Action TT: {$action->actionTimetoken})\n";
+ }
+ // snippet.end
+ } catch (PubNubException $e) {
+ echo "β Failed to retrieve message actions with range: " . $e->getMessage() . "\n";
+ }
+}
+
+echo "\n";
+
+// 2.3 Fetch Messages with Actions
+echo "=== PHASE 2.3: Fetching Messages with Actions ===\n\n";
+
+echo "π¬ Fetching messages with their associated actions...\n";
+try {
+ // snippet.fetch_messages_with_actions
+ $result = $pubnub->fetchMessages()
+ ->channels([$testChannel])
+ ->includeMessageActions(true)
+ ->includeMeta(true)
+ ->includeUuid(true)
+ ->sync();
+
+ $messages = $result->getChannels()[$testChannel] ?? [];
+ echo "β
Retrieved " . count($messages) . " messages with actions:\n";
+
+ foreach ($messages as $messageData) {
+ echo " π Message: " . json_encode($messageData->getMessage()) . "\n";
+ echo " Timetoken: {$messageData->getTimetoken()}\n";
+ echo " UUID: {$messageData->getUuid()}\n";
+
+ $actions = $messageData->getActions();
+ if (!empty($actions)) {
+ echo " Actions:\n";
+ foreach ($actions as $actionType => $actionValues) {
+ foreach ($actionValues as $actionValue => $actionDetails) {
+ foreach ($actionDetails as $actionDetail) {
+ echo " - {$actionType}:{$actionValue} by {$actionDetail['uuid']} "
+ . "at {$actionDetail['actionTimetoken']}\n";
+ }
+ }
+ }
+ }
+ echo "\n";
+ }
+ // snippet.end
+} catch (PubNubException $e) {
+ echo "β Failed to fetch messages with actions: " . $e->getMessage() . "\n";
+}
+
+// 2.4 Remove Message Actions
+echo "=== PHASE 2.4: Removing Message Actions ===\n\n";
+
+echo "ποΈ Removing some message actions...\n";
+$actionsToRemove = array_slice($messageActions, 0, 2); // Remove first 2 actions
+
+foreach ($actionsToRemove as $actionToRemove) {
+ try {
+ // snippet.remove_message_action
+ $result = $pubnub->removeMessageAction()
+ ->channel($testChannel)
+ ->messageTimetoken($actionToRemove['messageTimetoken'])
+ ->actionTimetoken($actionToRemove['actionTimetoken'])
+ ->sync();
+
+ echo "β
Removed {$actionToRemove['type']}:{$actionToRemove['value']} action "
+ . "(A: {$actionToRemove['actionTimetoken']}, M: {$actionToRemove['messageTimetoken']})\n";
+ // snippet.end
+ } catch (PubNubException $e) {
+ echo "β Failed to remove message action: " . $e->getMessage() . "\n";
+ }
+}
+
+echo "\n";
+
+// Phase 3: Error Handling and Edge Cases
+echo "=== PHASE 3: Error Handling and Edge Cases ===\n\n";
+
+echo "π Testing error scenarios...\n";
+
+// Try to add action to non-existent message
+echo "π Testing action on non-existent message...\n";
+try {
+ // snippet.error_nonexistent_message
+ $result = $pubnub->addMessageAction()
+ ->channel($testChannel)
+ ->messageAction(new PNMessageAction([
+ 'type' => 'test',
+ 'value' => 'error',
+ 'messageTimetoken' => "99999999999999999" // Non-existent timetoken
+ ]))
+ ->sync();
+
+ echo "β οΈ Unexpectedly succeeded adding action to non-existent message\n";
+ // snippet.end
+} catch (PubNubException $e) {
+ echo "β
Expected error for non-existent message: " . $e->getMessage() . "\n";
+}
+
+// Try to remove non-existent action
+echo "\nπ Testing removal of non-existent action...\n";
+try {
+ // snippet.error_remove_nonexistent
+ $result = $pubnub->removeMessageAction()
+ ->channel($testChannel)
+ ->messageTimetoken($publishedMessages[0]['timetoken'])
+ ->actionTimetoken("99999999999999999") // Non-existent action timetoken
+ ->sync();
+
+ echo "β
Successfully handled removal of non-existent action\n";
+ // snippet.end
+} catch (PubNubException $e) {
+ echo "β
Expected error for non-existent action: " . $e->getMessage() . "\n";
+}
+
+echo "\n";
+
+// Phase 4: Practical Use Cases
+echo "=== PHASE 4: Practical Use Cases ===\n\n";
+
+// Use Case 1: Emoji Reactions System
+echo "π Use Case 1: Emoji Reactions System\n";
+function addEmojiReaction($pubnub, $channel, $messageTimetoken, $emoji, $userId)
+{
+ // snippet.emoji_reaction_system
+ try {
+ $result = $pubnub->addMessageAction()
+ ->channel($channel)
+ ->messageAction(new PNMessageAction([
+ 'type' => 'reaction',
+ 'value' => $emoji,
+ 'messageTimetoken' => $messageTimetoken
+ ]))
+ ->sync();
+
+ return [
+ 'success' => true,
+ 'actionTimetoken' => $result->actionTimetoken,
+ 'message' => "User $userId reacted with $emoji"
+ ];
+ } catch (PubNubException $e) {
+ return [
+ 'success' => false,
+ 'error' => $e->getMessage()
+ ];
+ }
+ // snippet.end
+}
+
+$emojiResult = addEmojiReaction($pubnub, $testChannel, $publishedMessages[0]['timetoken'], 'π', $config->getUserId());
+echo " " . ($emojiResult['success'] ? "β
" : "β") . " " . ($emojiResult['message'] ?? $emojiResult['error']) . "\n";
+
+// Use Case 2: Read Receipts System
+echo "\nπ Use Case 2: Read Receipts System\n";
+function markAsRead($pubnub, $channel, $messageTimetoken, $userId)
+{
+ // snippet.read_receipt_system
+ try {
+ $result = $pubnub->addMessageAction()
+ ->channel($channel)
+ ->messageAction(new PNMessageAction([
+ 'type' => 'receipt',
+ 'value' => 'read',
+ 'messageTimetoken' => $messageTimetoken
+ ]))
+ ->sync();
+
+ return [
+ 'success' => true,
+ 'actionTimetoken' => $result->actionTimetoken,
+ 'message' => "Message marked as read by $userId"
+ ];
+ } catch (PubNubException $e) {
+ return [
+ 'success' => false,
+ 'error' => $e->getMessage()
+ ];
+ }
+ // snippet.end
+}
+
+$readResult = markAsRead($pubnub, $testChannel, $publishedMessages[1]['timetoken'], $config->getUserId());
+echo " " . ($readResult['success'] ? "β
" : "β") . " " . ($readResult['message'] ?? $readResult['error']) . "\n";
+
+// Use Case 3: Message Delivery Confirmations
+echo "\nπ¨ Use Case 3: Message Delivery Confirmations\n";
+function confirmDelivery($pubnub, $channel, $messageTimetoken, $userId)
+{
+ // snippet.delivery_confirmation_system
+ try {
+ $result = $pubnub->addMessageAction()
+ ->channel($channel)
+ ->messageAction(new PNMessageAction([
+ 'type' => 'delivery',
+ 'value' => 'confirmed',
+ 'messageTimetoken' => $messageTimetoken
+ ]))
+ ->sync();
+
+ return [
+ 'success' => true,
+ 'actionTimetoken' => $result->actionTimetoken,
+ 'message' => "Delivery confirmed by $userId"
+ ];
+ } catch (PubNubException $e) {
+ return [
+ 'success' => false,
+ 'error' => $e->getMessage()
+ ];
+ }
+ // snippet.end
+}
+
+$deliveryResult = confirmDelivery($pubnub, $testChannel, $publishedMessages[2]['timetoken'], $config->getUserId());
+echo " " . ($deliveryResult['success'] ? "β
" : "β") . " "
+ . ($deliveryResult['message'] ?? $deliveryResult['error']) . "\n";
+
+// Use Case 4: Message Analytics Tagging
+echo "\nπ Use Case 4: Message Analytics Tagging\n";
+function tagForAnalytics($pubnub, $channel, $messageTimetoken, $tag, $value)
+{
+ // snippet.analytics_tagging_system
+ try {
+ $result = $pubnub->addMessageAction()
+ ->channel($channel)
+ ->messageAction(new PNMessageAction([
+ 'type' => $tag,
+ 'value' => $value,
+ 'messageTimetoken' => $messageTimetoken
+ ]))
+ ->sync();
+
+ return [
+ 'success' => true,
+ 'actionTimetoken' => $result->actionTimetoken,
+ 'message' => "Tagged message with $tag:$value"
+ ];
+ } catch (PubNubException $e) {
+ return [
+ 'success' => false,
+ 'error' => $e->getMessage()
+ ];
+ }
+ // snippet.end
+}
+
+$tagResult = tagForAnalytics($pubnub, $testChannel, $publishedMessages[0]['timetoken'], 'engagement', 'high');
+echo " " . ($tagResult['success'] ? "β
" : "β") . " " . ($tagResult['message'] ?? $tagResult['error']) . "\n";
+
+echo "\n";
+
+// Final verification
+echo "=== FINAL VERIFICATION ===\n\n";
+
+echo "π Final verification - getting all remaining message actions...\n";
+try {
+ $finalResult = $pubnub->getMessageActions()
+ ->channel($testChannel)
+ ->setLimit(100)
+ ->sync();
+
+ echo "β
Final count: " . count($finalResult->actions) . " message actions remaining\n";
+
+ // Group actions by type for summary
+ $actionsByType = [];
+ foreach ($finalResult->actions as $action) {
+ $type = $action->type;
+ if (!isset($actionsByType[$type])) {
+ $actionsByType[$type] = 0;
+ }
+ $actionsByType[$type]++;
+ }
+
+ echo "π Summary by type:\n";
+ foreach ($actionsByType as $type => $count) {
+ echo " - $type: $count actions\n";
+ }
+} catch (PubNubException $e) {
+ echo "β Failed to get final verification: " . $e->getMessage() . "\n";
+}
+
+echo "\n=== MESSAGE ACTIONS DEMO COMPLETE ===\n";
diff --git a/examples/MessagePersistance.php b/examples/MessagePersistance.php
new file mode 100644
index 00000000..8c00a0e9
--- /dev/null
+++ b/examples/MessagePersistance.php
@@ -0,0 +1,479 @@
+setPublishKey("demo");
+ $pnConfig->setSubscribeKey("demo");
+ $pnConfig->setSecretKey("demo"); // Required for delete operations
+ $pnConfig->setUserId("message-persistence-demo-user");
+
+ $this->pubnub = new PubNub($pnConfig);
+
+ echo "=== PubNub Message Persistence Demo ===\n\n";
+ }
+
+ /**
+ * Step 1: Publish sample messages to have data for persistence operations
+ */
+ public function publishSampleMessages()
+ {
+ echo "π€ Publishing sample messages...\n";
+
+ $messages = [
+ ["content" => "First demo message", "type" => "text", "timestamp" => time()],
+ ["content" => "Second demo message", "type" => "text", "timestamp" => time() + 1],
+ ["content" => "Third demo message with metadata", "type" => "rich", "timestamp" => time() + 2],
+ ["content" => "Fourth message for testing", "type" => "text", "timestamp" => time() + 3],
+ ["content" => "Fifth and final message", "type" => "text", "timestamp" => time() + 4]
+ ];
+
+ try {
+ // Publish to main demo channel
+ foreach ($messages as $index => $message) {
+ $result = $this->pubnub->publish()
+ ->channel($this->demoChannel)
+ ->message($message)
+ ->meta(["messageIndex" => $index + 1, "demo" => true])
+ ->sync();
+
+ $this->publishedTimetokens[] = $result->getTimetoken();
+ echo " β Published message " . ($index + 1) . " - Timetoken: " . $result->getTimetoken() . "\n";
+
+ // Small delay to ensure different timetokens
+ usleep(100000); // 0.1 seconds
+ }
+
+ // Publish to multiple channels for multi-channel demos
+ $this->pubnub->publish()
+ ->channel($this->multiChannel1)
+ ->message(["content" => "Message for channel 1", "channel" => 1])
+ ->sync();
+
+ $this->pubnub->publish()
+ ->channel($this->multiChannel2)
+ ->message(["content" => "Message for channel 2", "channel" => 2])
+ ->sync();
+
+ echo " β Published messages to multiple channels\n\n";
+
+ // Wait a bit for messages to be stored
+ sleep(2);
+ } catch (PubNubException $e) {
+ echo "β Error publishing messages: " . $e->getMessage() . "\n\n";
+ }
+ }
+
+ /**
+ * Demo 1: Fetch History (fetchMessages) - Basic Usage
+ */
+ public function demoFetchHistoryBasic()
+ {
+ echo "π Demo 1: Fetch History - Basic Usage\n";
+ echo "Retrieving last messages from channel...\n";
+
+ try {
+ $result = $this->pubnub->fetchMessages()
+ ->channels($this->demoChannel)
+ ->count(5)
+ ->sync();
+
+ echo " π Results:\n";
+ echo " - Start Timetoken: " . $result->getStartTimetoken() . "\n";
+ echo " - End Timetoken: " . $result->getEndTimetoken() . "\n";
+
+ $channels = $result->getChannels();
+ if (isset($channels[$this->demoChannel])) {
+ $messages = $channels[$this->demoChannel];
+ echo " - Message Count: " . count($messages) . "\n";
+
+ foreach ($messages as $index => $message) {
+ echo " Message " . ($index + 1) . ":\n";
+ echo " Content: " . json_encode($message->getMessage()) . "\n";
+ echo " Timetoken: " . $message->getTimetoken() . "\n";
+ if ($message->getMetadata()) {
+ echo " Meta: " . json_encode($message->getMetadata()) . "\n";
+ }
+ echo "\n";
+ }
+ }
+ } catch (PubNubException $e) {
+ echo "β Error: " . $e->getMessage() . "\n";
+ }
+
+ echo "\n";
+ }
+
+ /**
+ * Demo 2: Fetch History with Advanced Options
+ */
+ public function demoFetchHistoryAdvanced()
+ {
+ echo "π Demo 2: Fetch History - Advanced Options\n";
+ echo "Retrieving messages with metadata, message type, and UUID info...\n";
+
+ try {
+ $result = $this->pubnub->fetchMessages()
+ ->channels($this->demoChannel)
+ ->includeMeta(true)
+ ->includeMessageType(true)
+ ->includeUuid(true)
+ ->sync();
+
+ echo " π Advanced Results:\n";
+ $channels = $result->getChannels();
+ if (isset($channels[$this->demoChannel])) {
+ $messages = $channels[$this->demoChannel];
+
+ foreach ($messages as $index => $message) {
+ echo " Advanced Message " . ($index + 1) . ":\n";
+ echo " Content: " . json_encode($message->getMessage()) . "\n";
+ echo " Timetoken: " . $message->getTimetoken() . "\n";
+ echo " UUID: " . ($message->getUuid() ?? 'N/A') . "\n";
+ echo " Message Type: " . ($message->getMessageType() ?? 'N/A') . "\n";
+ echo " Meta: " . json_encode($message->getMetadata()) . "\n";
+ echo "\n";
+ }
+ }
+ } catch (PubNubException $e) {
+ echo "β Error: " . $e->getMessage() . "\n";
+ }
+
+ echo "\n";
+ }
+
+ /**
+ * Demo 3: Fetch History from Multiple Channels
+ */
+ public function demoFetchHistoryMultiChannel()
+ {
+ echo "π Demo 3: Fetch History - Multiple Channels\n";
+ echo "Retrieving messages from multiple channels...\n";
+
+ try {
+ $result = $this->pubnub->fetchMessages()
+ ->channels([$this->multiChannel1, $this->multiChannel2])
+ ->includeMeta(true)
+ ->sync();
+
+ echo " π Multi-Channel Results:\n";
+ $channels = $result->getChannels();
+
+ foreach ($channels as $channelName => $messages) {
+ echo " Channel: $channelName\n";
+ echo " Message Count: " . count($messages) . "\n";
+
+ foreach ($messages as $index => $message) {
+ echo " Message " . ($index + 1) . ": " . json_encode($message->getMessage()) . "\n";
+ }
+ echo "\n";
+ }
+ } catch (PubNubException $e) {
+ echo "β Error: " . $e->getMessage() . "\n";
+ }
+
+ echo "\n";
+ }
+
+ /**
+ * Demo 4: History Method - Basic Usage
+ */
+ public function demoHistoryBasic()
+ {
+ echo "π Demo 4: History Method - Basic Usage\n";
+ echo "Using history() method to retrieve messages...\n";
+
+ try {
+ $result = $this->pubnub->history()
+ ->channel($this->demoChannel)
+ ->count(5)
+ ->includeTimetoken(true)
+ ->sync();
+
+ echo " π History Results:\n";
+ echo " - Start Timetoken: " . $result->getStartTimetoken() . "\n";
+ echo " - End Timetoken: " . $result->getEndTimetoken() . "\n";
+
+ $messages = $result->getMessages();
+ echo " - Message Count: " . count($messages) . "\n";
+
+ foreach ($messages as $index => $message) {
+ echo " Message " . ($index + 1) . ":\n";
+ echo " Content: " . json_encode($message->getEntry()) . "\n";
+ echo " Timetoken: " . $message->getTimetoken() . "\n";
+ echo "\n";
+ }
+ } catch (PubNubException $e) {
+ echo "β Error: " . $e->getMessage() . "\n";
+ }
+
+ echo "\n";
+ }
+
+ /**
+ * Demo 5: History Method - Reverse Order (Oldest First)
+ */
+ public function demoHistoryReverse()
+ {
+ echo "π Demo 5: History Method - Reverse Order (Oldest First)\n";
+ echo "Retrieving oldest 3 messages first...\n";
+
+ try {
+ $result = $this->pubnub->history()
+ ->channel($this->demoChannel)
+ ->count(3)
+ ->reverse(true)
+ ->includeTimetoken(true)
+ ->sync();
+
+ echo " π Reverse History Results:\n";
+ $messages = $result->getMessages();
+
+ foreach ($messages as $index => $message) {
+ echo " Oldest Message " . ($index + 1) . ":\n";
+ echo " Content: " . json_encode($message->getEntry()) . "\n";
+ echo " Timetoken: " . $message->getTimetoken() . "\n";
+ echo "\n";
+ }
+ } catch (PubNubException $e) {
+ echo "β Error: " . $e->getMessage() . "\n";
+ }
+
+ echo "\n";
+ }
+
+ /**
+ * Demo 6: History Method - Time Range
+ */
+ public function demoHistoryTimeRange()
+ {
+ echo "π Demo 6: History Method - Time Range\n";
+
+ if (count($this->publishedTimetokens) >= 2) {
+ $startTime = $this->publishedTimetokens[0];
+ $endTime = $this->publishedTimetokens[2]; // Get messages between first and third
+
+ echo "Retrieving messages between timetoken $startTime and $endTime...\n";
+
+ try {
+ $result = $this->pubnub->history()
+ ->channel($this->demoChannel)
+ ->start($startTime)
+ ->end($endTime)
+ ->includeTimetoken(true)
+ ->sync();
+
+ echo " π Time Range Results:\n";
+ $messages = $result->getMessages();
+ echo " - Message Count: " . count($messages) . "\n";
+
+ foreach ($messages as $index => $message) {
+ echo " Message " . ($index + 1) . ":\n";
+ echo " Content: " . json_encode($message->getEntry()) . "\n";
+ echo " Timetoken: " . $message->getTimetoken() . "\n";
+ echo "\n";
+ }
+ } catch (PubNubException $e) {
+ echo "β Error: " . $e->getMessage() . "\n";
+ }
+ } else {
+ echo " β οΈ Not enough published messages for time range demo\n";
+ }
+
+ echo "\n";
+ }
+
+ /**
+ * Demo 7: Message Counts
+ */
+ public function demoMessageCounts()
+ {
+ echo "π’ Demo 7: Message Counts\n";
+ echo "Getting message counts for channels...\n";
+
+ try {
+ // Get the oldest timetoken for counting
+ $oldestTimetoken = !empty($this->publishedTimetokens) ?
+ min($this->publishedTimetokens) : "0";
+
+ $result = $this->pubnub->messageCounts()
+ ->channels([$this->demoChannel])
+ ->channelsTimetoken([$oldestTimetoken])
+ ->sync();
+
+ echo " π Message Count Results:\n";
+ $channels = $result->getChannels();
+
+ foreach ($channels as $channelName => $count) {
+ echo " Channel '$channelName': $count messages\n";
+ }
+ } catch (PubNubException $e) {
+ echo "β Error: " . $e->getMessage() . "\n";
+ }
+
+ echo "\n";
+ }
+
+ /**
+ * Demo 8: Message Counts - Multiple Channels
+ */
+ public function demoMessageCountsMultiple()
+ {
+ echo "π’ Demo 8: Message Counts - Multiple Channels\n";
+ echo "Getting message counts for multiple channels...\n";
+
+ try {
+ $result = $this->pubnub->messageCounts()
+ ->channels([$this->demoChannel, $this->multiChannel1, $this->multiChannel2])
+ ->channelsTimetoken(["0"]) // Single timetoken for all channels
+ ->sync();
+
+ echo " π Multiple Channel Count Results:\n";
+ $channels = $result->getChannels();
+
+ foreach ($channels as $channelName => $count) {
+ echo " Channel '$channelName': $count messages\n";
+ }
+ } catch (PubNubException $e) {
+ echo "β Error: " . $e->getMessage() . "\n";
+ }
+
+ echo "\n";
+ }
+
+ /**
+ * Demo 9: Delete Messages from History
+ * Note: This requires secret key and proper permissions
+ */
+ public function demoDeleteMessages()
+ {
+ echo "ποΈ Demo 9: Delete Messages from History\n";
+ echo "Attempting to delete specific message from history...\n";
+
+ if (!empty($this->publishedTimetokens)) {
+ // Delete the first message by using its timetoken
+ $targetTimetoken = $this->publishedTimetokens[0];
+ $startTimetoken = $targetTimetoken - 1;
+ $endTimetoken = $targetTimetoken;
+
+ echo " Deleting message with timetoken: $targetTimetoken\n";
+ echo " Using range: $startTimetoken to $endTimetoken\n";
+
+ try {
+ $this->pubnub->deleteMessages()
+ ->channel($this->demoChannel)
+ ->start($startTimetoken)
+ ->end($endTimetoken)
+ ->sync();
+
+ echo " β
Delete operation completed successfully\n";
+ echo " Note: Message deletion may take a moment to reflect in history\n";
+ } catch (PubNubException $e) {
+ echo " β οΈ Delete operation failed: " . $e->getMessage() . "\n";
+ echo " Note: Delete operations require proper key permissions and secret key\n";
+ }
+ } else {
+ echo " β οΈ No published messages available for deletion demo\n";
+ }
+
+ echo "\n";
+ }
+
+ /**
+ * Demo 10: Verify Deletion
+ */
+ public function demoVerifyDeletion()
+ {
+ echo "π Demo 10: Verify Deletion\n";
+ echo "Checking if message was deleted...\n";
+
+ try {
+ // Wait a moment for deletion to process
+ sleep(2);
+
+ $result = $this->pubnub->fetchMessages()
+ ->channels($this->demoChannel)
+ ->count(10)
+ ->sync();
+
+ echo " π Current Message Count After Deletion:\n";
+ $channels = $result->getChannels();
+ if (isset($channels[$this->demoChannel])) {
+ $messages = $channels[$this->demoChannel];
+ echo " - Messages remaining: " . count($messages) . "\n";
+
+ // Check if the first timetoken is still present
+ $remainingTimetokens = array_map(function ($msg) {
+ return $msg->getTimetoken();
+ }, $messages);
+
+ $deletedTimetoken = $this->publishedTimetokens[0] ?? null;
+ if ($deletedTimetoken && !in_array($deletedTimetoken, $remainingTimetokens)) {
+ echo " β
Message with timetoken $deletedTimetoken was successfully deleted\n";
+ } else {
+ echo " β οΈ Message may still be present (deletion can take time to propagate)\n";
+ }
+ }
+ } catch (PubNubException $e) {
+ echo "β Error: " . $e->getMessage() . "\n";
+ }
+
+ echo "\n";
+ }
+
+ /**
+ * Run all demos
+ */
+ public function runAllDemos()
+ {
+ // Step 1: Setup data
+ $this->publishSampleMessages();
+
+ // Step 2: Fetch History demos
+ $this->demoFetchHistoryBasic();
+ $this->demoFetchHistoryAdvanced();
+ $this->demoFetchHistoryMultiChannel();
+
+ // Step 3: History method demos
+ $this->demoHistoryBasic();
+ $this->demoHistoryReverse();
+ $this->demoHistoryTimeRange();
+
+ // Step 4: Message count demos
+ $this->demoMessageCounts();
+ $this->demoMessageCountsMultiple();
+
+ // Step 5: Delete operations (may require proper permissions)
+ $this->demoDeleteMessages();
+ $this->demoVerifyDeletion();
+
+ echo "π All Message Persistence demos completed!\n";
+ }
+}
+
+// Run the demo
+if (file_exists('vendor/autoload.php')) {
+ $demo = new MessagePersistenceDemo();
+ $demo->runAllDemos();
+} else {
+ echo "β Please run 'composer install' to install PubNub SDK dependencies first.\n";
+ echo "Make sure you have composer.json with PubNub dependency configured.\n";
+}
diff --git a/examples/MobilePush.php b/examples/MobilePush.php
new file mode 100644
index 00000000..6d0e49a1
--- /dev/null
+++ b/examples/MobilePush.php
@@ -0,0 +1,400 @@
+initializePubNub();
+ $this->initializeSampleData();
+ }
+
+ private function initializePubNub()
+ {
+ // snippet.setup
+ echo "π Initializing PubNub Mobile Push Demo...\n";
+ echo "================================================\n\n";
+
+ $pnConfig = new PNConfiguration();
+ $pnConfig->setSubscribeKey(getenv('SUBSCRIBE_KEY') ?: 'demo');
+ $pnConfig->setPublishKey(getenv('PUBLISH_KEY') ?: 'demo');
+ $pnConfig->setUserId('php-mobile-push-demo-' . time());
+
+ $this->pubnub = new PubNub($pnConfig);
+
+ echo "β
PubNub configured with demo keys\n";
+ echo "π± User ID: " . $pnConfig->getUserId() . "\n\n";
+ // snippet.end
+ }
+
+ private function initializeSampleData()
+ {
+ // snippet.sample_data
+ $this->sampleChannels = [
+ 'news-channel-' . time(),
+ 'alerts-channel-' . time(),
+ 'promotions-channel-' . time(),
+ 'updates-channel-' . time()
+ ];
+
+ $this->sampleDevices = [
+ 'fcm' => [
+ 'deviceId' => 'fcm-device-token-' . uniqid(),
+ 'pushType' => PNPushType::FCM,
+ 'name' => 'Android Device (FCM)'
+ ],
+ 'apns2_dev' => [
+ 'deviceId' => 'apns2-dev-token-' . uniqid(),
+ 'pushType' => PNPushType::APNS2,
+ 'environment' => 'development',
+ 'topic' => 'com.example.myapp',
+ 'name' => 'iOS Device (APNS2 - Development)'
+ ],
+ 'apns2_prod' => [
+ 'deviceId' => 'apns2-prod-token-' . uniqid(),
+ 'pushType' => PNPushType::APNS2,
+ 'environment' => 'production',
+ 'topic' => 'com.example.myapp',
+ 'name' => 'iOS Device (APNS2 - Production)'
+ ]
+ ];
+ // snippet.end
+
+ echo "π Sample data initialized:\n";
+ echo " Channels: " . implode(', ', $this->sampleChannels) . "\n";
+ echo " Devices: " . count($this->sampleDevices) . " sample devices\n\n";
+ }
+
+ public function runFullDemo()
+ {
+ echo "π― Starting comprehensive Mobile Push demo...\n";
+ echo "==============================================\n\n";
+
+ // Test all features for each device type
+ foreach ($this->sampleDevices as $deviceKey => $device) {
+ $this->runDeviceDemo($deviceKey, $device);
+ echo "\n" . str_repeat("-", 60) . "\n\n";
+ }
+
+ echo "β¨ Mobile Push demo completed successfully!\n";
+ }
+
+ private function runDeviceDemo($deviceKey, $device)
+ {
+ echo "π± Testing {$device['name']}\n";
+ echo " Device ID: {$device['deviceId']}\n";
+ echo " Push Type: {$device['pushType']}\n";
+
+ if (isset($device['environment'])) {
+ echo " Environment: {$device['environment']}\n";
+ }
+ if (isset($device['topic'])) {
+ echo " Topic: {$device['topic']}\n";
+ }
+ echo "\n";
+
+ // 1. Add device to channels
+ $this->demonstrateAddDeviceToChannels($device);
+
+ // 2. List channels for device
+ $this->demonstrateListChannelsForDevice($device);
+
+ // 3. Remove device from some channels
+ $this->demonstrateRemoveDeviceFromChannels($device);
+
+ // 4. List channels again to verify removal
+ $this->demonstrateListChannelsForDevice($device, "After Removal");
+
+ // 5. Remove all channels from device
+ $this->demonstrateRemoveAllChannelsFromDevice($device);
+
+ // 6. Final verification
+ $this->demonstrateListChannelsForDevice($device, "Final Verification");
+ }
+
+ private function demonstrateAddDeviceToChannels($device)
+ {
+ echo "1οΈβ£ Adding device to channels...\n";
+
+ try {
+ if ($device['pushType'] === PNPushType::FCM) {
+ // snippet.add_device_to_channel_fcm
+ $result = $this->pubnub->addChannelsToPush()
+ ->pushType(PNPushType::FCM)
+ ->channels($this->sampleChannels)
+ ->deviceId($device['deviceId'])
+ ->sync();
+ // snippet.end
+ } elseif ($device['pushType'] === PNPushType::APNS2) {
+ // snippet.add_device_to_channel_apns2
+ $result = $this->pubnub->addChannelsToPush()
+ ->pushType(PNPushType::APNS2)
+ ->channels($this->sampleChannels)
+ ->deviceId($device['deviceId'])
+ ->environment($device['environment'])
+ ->topic($device['topic'])
+ ->sync();
+ // snippet.end
+ }
+
+ echo " β
Successfully added device to " . count($this->sampleChannels) . " channels\n";
+ echo " π Channels: " . implode(', ', $this->sampleChannels) . "\n\n";
+ } catch (PubNubServerException $e) {
+ echo " β Server Error: " . $e->getMessage() . "\n";
+ echo " π Status Code: " . $e->getStatusCode() . "\n\n";
+ } catch (PubNubException $e) {
+ echo " β PubNub Error: " . $e->getMessage() . "\n\n";
+ } catch (Exception $e) {
+ echo " β General Error: " . $e->getMessage() . "\n\n";
+ }
+ }
+
+ private function demonstrateListChannelsForDevice($device, $context = "")
+ {
+ $title = $context ? "2οΈβ£ Listing channels for device ($context)..." : "2οΈβ£ Listing channels for device...";
+ echo "$title\n";
+
+ try {
+ if ($device['pushType'] === PNPushType::FCM) {
+ // snippet.list_channels_fcm
+ $result = $this->pubnub->listPushProvisions()
+ ->pushType(PNPushType::FCM)
+ ->deviceId($device['deviceId'])
+ ->sync();
+ // snippet.end
+ } elseif ($device['pushType'] === PNPushType::APNS2) {
+ // snippet.list_channels_apns2
+ $result = $this->pubnub->listPushProvisions()
+ ->pushType(PNPushType::APNS2)
+ ->deviceId($device['deviceId'])
+ ->environment($device['environment'])
+ ->topic($device['topic'])
+ ->sync();
+ // snippet.end
+ }
+
+ $channels = $result->getChannels();
+
+ if (!empty($channels)) {
+ echo " β
Device is registered for " . count($channels) . " channels:\n";
+ foreach ($channels as $channel) {
+ echo " β’ $channel\n";
+ }
+ } else {
+ echo " π No channels found for this device\n";
+ }
+ echo "\n";
+ } catch (PubNubServerException $e) {
+ echo " β Server Error: " . $e->getMessage() . "\n";
+ echo " π Status Code: " . $e->getStatusCode() . "\n\n";
+ } catch (PubNubException $e) {
+ echo " β PubNub Error: " . $e->getMessage() . "\n\n";
+ } catch (Exception $e) {
+ echo " β General Error: " . $e->getMessage() . "\n\n";
+ }
+ }
+
+ private function demonstrateRemoveDeviceFromChannels($device)
+ {
+ echo "3οΈβ£ Removing device from specific channels...\n";
+
+ // Remove from first 2 channels only
+ $channelsToRemove = array_slice($this->sampleChannels, 0, 2);
+
+ try {
+ if ($device['pushType'] === PNPushType::FCM) {
+ // snippet.remove_device_from_channels_fcm
+ $result = $this->pubnub->removeChannelsFromPush()
+ ->pushType(PNPushType::FCM)
+ ->channels($channelsToRemove)
+ ->deviceId($device['deviceId'])
+ ->sync();
+ // snippet.end
+ } elseif ($device['pushType'] === PNPushType::APNS2) {
+ // snippet.remove_device_from_channels_apns2
+ $result = $this->pubnub->removeChannelsFromPush()
+ ->pushType(PNPushType::APNS2)
+ ->channels($channelsToRemove)
+ ->deviceId($device['deviceId'])
+ ->environment($device['environment'])
+ ->topic($device['topic'])
+ ->sync();
+ // snippet.end
+ }
+
+ echo " β
Successfully removed device from " . count($channelsToRemove) . " channels\n";
+ echo " π Removed from: " . implode(', ', $channelsToRemove) . "\n\n";
+ } catch (PubNubServerException $e) {
+ echo " β Server Error: " . $e->getMessage() . "\n";
+ echo " π Status Code: " . $e->getStatusCode() . "\n\n";
+ } catch (PubNubException $e) {
+ echo " β PubNub Error: " . $e->getMessage() . "\n\n";
+ } catch (Exception $e) {
+ echo " β General Error: " . $e->getMessage() . "\n\n";
+ }
+ }
+
+ private function demonstrateRemoveAllChannelsFromDevice($device)
+ {
+ echo "4οΈβ£ Removing all push channels from device...\n";
+
+ try {
+ if ($device['pushType'] === PNPushType::FCM) {
+ // snippet.remove_all_channels_fcm
+ $result = $this->pubnub->removeAllPushChannelsForDevice()
+ ->pushType(PNPushType::FCM)
+ ->deviceId($device['deviceId'])
+ ->sync();
+ // snippet.end
+ } elseif ($device['pushType'] === PNPushType::APNS2) {
+ // snippet.remove_all_channels_apns2
+ $result = $this->pubnub->removeAllPushChannelsForDevice()
+ ->pushType(PNPushType::APNS2)
+ ->deviceId($device['deviceId'])
+ ->sync();
+ // snippet.end
+ }
+
+ echo " β
Successfully removed all push channels from device\n\n";
+ } catch (PubNubServerException $e) {
+ echo " β Server Error: " . $e->getMessage() . "\n";
+ echo " π Status Code: " . $e->getStatusCode() . "\n\n";
+ } catch (PubNubException $e) {
+ echo " β PubNub Error: " . $e->getMessage() . "\n\n";
+ } catch (Exception $e) {
+ echo " β General Error: " . $e->getMessage() . "\n\n";
+ }
+ }
+
+ public function demonstrateAdvancedFeatures()
+ {
+ echo "π§ Advanced Mobile Push Features Demo\n";
+ echo "====================================\n\n";
+
+ // Demonstrate error handling scenarios
+ $this->demonstrateErrorHandling();
+
+ // Demonstrate bulk operations
+ $this->demonstrateBulkOperations();
+ }
+
+ private function demonstrateErrorHandling()
+ {
+ echo "π οΈ Error Handling Scenarios:\n\n";
+
+ // Test with invalid push type
+ echo "π Testing invalid device ID...\n";
+ try {
+ $result = $this->pubnub->addChannelsToPush()
+ ->pushType(PNPushType::FCM)
+ ->channels(['test-channel'])
+ ->deviceId('') // Empty device ID
+ ->sync();
+ } catch (PubNubValidationException $e) {
+ echo " β
Correctly caught error: " . $e->getMessage() . "\n\n";
+ }
+
+ // Test with invalid channels
+ echo "π Testing with empty channels array...\n";
+ try {
+ $result = $this->pubnub->addChannelsToPush()
+ ->pushType(PNPushType::FCM)
+ ->channels([]) // Empty channels
+ ->deviceId('test-device-id')
+ ->sync();
+ } catch (PubNubValidationException $e) {
+ echo " β
Correctly caught error: " . $e->getMessage() . "\n\n";
+ }
+ }
+
+ private function demonstrateBulkOperations()
+ {
+ echo "π¦ Bulk Operations Demo:\n\n";
+
+ $bulkChannels = [
+ 'bulk-news-' . time(),
+ 'bulk-sports-' . time(),
+ 'bulk-weather-' . time(),
+ 'bulk-traffic-' . time(),
+ 'bulk-entertainment-' . time()
+ ];
+
+ $bulkDevice = [
+ 'deviceId' => 'bulk-demo-device-' . uniqid(),
+ 'pushType' => PNPushType::FCM,
+ 'name' => 'Bulk Demo Device'
+ ];
+
+ echo "π± Adding device to " . count($bulkChannels) . " channels at once...\n";
+
+ try {
+ $result = $this->pubnub->addChannelsToPush()
+ ->pushType($bulkDevice['pushType'])
+ ->channels($bulkChannels)
+ ->deviceId($bulkDevice['deviceId'])
+ ->sync();
+
+ echo " β
Successfully added to all channels\n";
+
+ // Verify the addition
+ $listResult = $this->pubnub->listPushProvisions()
+ ->pushType($bulkDevice['pushType'])
+ ->deviceId($bulkDevice['deviceId'])
+ ->sync();
+
+ $registeredChannels = $listResult->getChannels();
+ echo " π Verified: Device registered for " . count($registeredChannels) . " channels\n";
+
+ // Clean up - remove all
+ $this->pubnub->removeAllPushChannelsForDevice()
+ ->pushType($bulkDevice['pushType'])
+ ->deviceId($bulkDevice['deviceId'])
+ ->sync();
+
+ echo " π§Ή Cleaned up: Removed all channels\n\n";
+ } catch (Exception $e) {
+ echo " β Error in bulk operations: " . $e->getMessage() . "\n\n";
+ }
+ }
+}
+
+// Main execution
+if (php_sapi_name() === 'cli') {
+ try {
+ $demo = new MobilePushDemo();
+
+ // Run the comprehensive demo
+ $demo->runFullDemo();
+
+ // Run advanced features demo
+ $demo->demonstrateAdvancedFeatures();
+
+ echo "π All Mobile Push demos completed successfully!\n";
+ echo "π Refer to PubNub docs: https://www.pubnub.com/docs/sdks/php/api-reference/mobile-push\n";
+ } catch (Exception $e) {
+ echo "π₯ Fatal Error: " . $e->getMessage() . "\n";
+ echo "Stack trace:\n" . $e->getTraceAsString() . "\n";
+ exit(1);
+ }
+} else {
+ echo "β οΈ This demo is designed to run from the command line.\n";
+ echo "Usage: php " . basename(__FILE__) . "\n";
+}
diff --git a/examples/Publishing.php b/examples/Publishing.php
new file mode 100644
index 00000000..fd5518ee
--- /dev/null
+++ b/examples/Publishing.php
@@ -0,0 +1,121 @@
+setPublishKey("demo");
+$pnconfig->setSubscribeKey("demo");
+$pnconfig->setUserId("php-publish-demo-user");
+
+$pubnub = new PubNub($pnconfig);
+
+// snippet.basic_publish
+$result = $pubnub->publish()
+ ->channel("my_channel")
+ ->message(["text" => "Hello World!"])
+ ->sync();
+assert($result->getTimetoken() > 0);
+echo "Basic publish timetoken: {$result->getTimetoken()}\n";
+// snippet.end
+
+// snippet.basic_publish_too_long_message
+try {
+ $result = $pubnub->publish()
+ ->channel("my_channel")
+ ->message(["text" => "Hello World!", "description" => str_repeat("Get allows slightly shorter messages", 1000)])
+ ->sync();
+ echo "Basic publish timetoken: {$result->getTimetoken()}\n";
+} catch (PubNubException $e) {
+ $status = $e->getStatus();
+ assert($status->isError() == true);
+ assert($status->getStatusCode() == '414');
+ print("Publish failed: {$status->getException()->getBody()->message}\n");
+}
+// snippet.end
+
+// snippet.publish_with_meta
+$result = $pubnub->publish()
+ ->channel("my_channel")
+ ->message(["text" => "Message with meta"])
+ ->meta(["custom" => "metadata"])
+ ->sync();
+assert($result->getTimetoken() > 0);
+echo "Publish with meta timetoken: {$result->getTimetoken()}\n";
+// snippet.end
+
+// snippet.publish_with_ttl
+$result = $pubnub->publish()
+ ->channel("my_channel")
+ ->message(["text" => "Message with TTL"])
+ ->shouldStore(true)
+ ->ttl(24) // Store for 24 hours
+ ->sync();
+
+echo "Publish with TTL timetoken: {$result->getTimetoken()}\n";
+// snippet.end
+
+// snippet.publish_with_custom_type
+$result = $pubnub->publish()
+ ->channel("my_channel")
+ ->message(["text" => "Message with custom type"])
+ ->customMessageType("text")
+ ->sync();
+assert($result->getTimetoken() > 0);
+echo "Publish with custom type timetoken: {$result->getTimetoken()}\n";
+// snippet.end
+
+// snippet.publish_with_post
+$result = $pubnub->publish()
+ ->channel("my_channel")
+ ->message([
+ "text" => "Message using POST",
+ "description" => str_repeat("Post allows to publish longer messages", 750)
+ ])
+ ->usePost(true)
+ ->sync();
+assert($result->getTimetoken() > 0);
+echo "Publish with POST timetoken: {$result->getTimetoken()}\n";
+// snippet.end
+
+// snippet.publish_too_long_message_with_post
+try {
+ $result = $pubnub->publish()
+ ->channel("my_channel")
+ ->message([
+ "text" => "Message using POST",
+ "description" => str_repeat("Post allows to publish longer messages", 1410)
+ ])
+ ->usePost(true)
+ ->sync();
+ assert($result->getTimetoken() > 0);
+ echo "Publish with POST timetoken: {$result->getTimetoken()}\n";
+} catch (PubNubException $e) {
+ $status = $e->getStatus();
+ assert($status->isError() == true);
+ assert($status->getStatusCode() == 413);
+ print("Publish failed: {$status->getException()->getBody()->message}\n");
+}
+// snippet.end
+
+// snippet.fire
+$result = $pubnub->fire()
+ ->channel("my_channel")
+ ->message(["text" => "Fire message"])
+ ->sync();
+assert($result->getTimetoken() > 0);
+echo "Fire timetoken: {$result->getTimetoken()}\n";
+// snippet.end
+
+// snippet.signal
+$result = $pubnub->signal()
+ ->channel("my_channel")
+ ->message(["text" => "Signal message"])
+ ->sync();
+assert($result->getTimetoken() > 0);
+echo "Signal timetoken: {$result->getTimetoken()}\n";
+// snippet.end
diff --git a/examples/Subscribing.php b/examples/Subscribing.php
new file mode 100644
index 00000000..ac8958b5
--- /dev/null
+++ b/examples/Subscribing.php
@@ -0,0 +1,121 @@
+setPublishKey("demo"); // Replace with your publish key
+$pnconfig->setSubscribeKey("demo"); // Replace with your subscribe key
+$pnconfig->setUserId("php-subscriber-" . uniqid()); // Unique user ID for this demo
+// snippet.end
+
+// snippet.init
+// Create PubNub instance
+$pubnub = new PubNub($pnconfig);
+// snippet.end
+
+// snippet.callback
+// Create a custom callback class to handle messages and status updates
+class MySubscribeCallback extends SubscribeCallback
+{
+ public function status($pubnub, $status)
+ {
+ switch ($status->getCategory()) {
+ case PNStatusCategory::PNConnectedCategory:
+ echo "Connected to PubNub!\n";
+ break;
+ case PNStatusCategory::PNReconnectedCategory:
+ echo "Reconnected to PubNub!\n";
+ break;
+ case PNStatusCategory::PNDisconnectedCategory:
+ echo "Disconnected from PubNub!\n";
+ break;
+ case PNStatusCategory::PNUnexpectedDisconnectCategory:
+ echo "Unexpectedly disconnected from PubNub!\n";
+ break;
+ }
+ }
+
+ public function message($pubnub, $message)
+ {
+ echo "Received message: " . json_encode($message->getMessage()) . "\n";
+ echo "Channel: " . $message->getChannel() . "\n";
+ echo "Publisher: " . $message->getPublisher() . "\n";
+ echo "Timetoken: " . $message->getTimetoken() . "\n";
+ }
+
+ public function presence($pubnub, $presence)
+ {
+ echo "Presence event: " . $presence->getEvent() . "\n";
+ echo "Channel: " . $presence->getChannel() . "\n";
+ echo "UUID: " . $presence->getUuid() . "\n";
+ echo "Occupancy: " . $presence->getOccupancy() . "\n";
+ }
+}
+// snippet.end
+
+// snippet.subscribe
+// Add the callback to PubNub
+$pubnub->addListener(new MySubscribeCallback());
+
+// Subscribe to multiple channels concurrently
+$channels = ["demo_channel", "demo_channel2", "demo_channel3"];
+
+foreach ($channels as $channel) {
+ $pubnub->subscribe()
+ ->channels($channel)
+ ->withPresence()
+ ->execute();
+
+ echo "Subscribed to channel: $channel\n";
+}
+// snippet.end
+
+// snippet.history
+// Get message history
+function getHistory($pubnub, $channels)
+{
+ $channel = $channels[array_rand($channels)];
+ try {
+ $result = $pubnub->history()
+ ->channel($channel)
+ ->count(5)
+ ->sync();
+
+ echo "Message history for $channel:\n";
+ foreach ($result->getMessages() as $message) {
+ echo json_encode($message, JSON_PRETTY_PRINT) . "\n";
+ }
+ } catch (\Exception $e) {
+ echo "Error getting history: " . $e->getMessage() . "\n";
+ }
+}
+// snippet.end
+
+echo "Starting PubNub Subscriber...\n";
+echo "Press Ctrl+C to exit\n";
+
+// Main loop
+$lastHistoryTime = 0;
+
+while (true) {
+ $currentTime = time();
+
+ // Check history every 15 seconds
+ if ($currentTime - $lastHistoryTime >= 15) {
+ getHistory($pubnub, $channels);
+ $lastHistoryTime = $currentTime;
+ }
+
+ // Small sleep to prevent CPU overuse
+ usleep(100000); // 100ms
+}
diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon
index a9155b9f..2944a1aa 100644
--- a/phpstan-baseline.neon
+++ b/phpstan-baseline.neon
@@ -750,11 +750,6 @@ parameters:
count: 1
path: src/PubNub/Endpoints/FileSharing/DownloadFile.php
- -
- message: "#^Method PubNub\\\\Endpoints\\\\FileSharing\\\\FetchFileUploadS3Data\\:\\:channel\\(\\) has parameter \\$channel with no type specified\\.$#"
- count: 1
- path: src/PubNub/Endpoints/FileSharing/FetchFileUploadS3Data.php
-
-
message: "#^Method PubNub\\\\Endpoints\\\\FileSharing\\\\FetchFileUploadS3Data\\:\\:createResponse\\(\\) has parameter \\$result with no value type specified in iterable type array\\.$#"
count: 1
@@ -765,41 +760,6 @@ parameters:
count: 1
path: src/PubNub/Endpoints/FileSharing/FetchFileUploadS3Data.php
- -
- message: "#^Method PubNub\\\\Endpoints\\\\FileSharing\\\\FetchFileUploadS3Data\\:\\:fileName\\(\\) has no return type specified\\.$#"
- count: 1
- path: src/PubNub/Endpoints/FileSharing/FetchFileUploadS3Data.php
-
- -
- message: "#^Method PubNub\\\\Endpoints\\\\FileSharing\\\\FetchFileUploadS3Data\\:\\:fileName\\(\\) has parameter \\$fileName with no type specified\\.$#"
- count: 1
- path: src/PubNub/Endpoints/FileSharing/FetchFileUploadS3Data.php
-
- -
- message: "#^Method PubNub\\\\Endpoints\\\\FileSharing\\\\FetchFileUploadS3Data\\:\\:name\\(\\) has no return type specified\\.$#"
- count: 1
- path: src/PubNub/Endpoints/FileSharing/FetchFileUploadS3Data.php
-
- -
- message: "#^Method PubNub\\\\Endpoints\\\\FileSharing\\\\FileSharingEndpoint\\:\\:channel\\(\\) has parameter \\$channel with no type specified\\.$#"
- count: 1
- path: src/PubNub/Endpoints/FileSharing/FileSharingEndpoint.php
-
- -
- message: "#^Method PubNub\\\\Endpoints\\\\FileSharing\\\\FileSharingEndpoint\\:\\:fileId\\(\\) has parameter \\$fileId with no type specified\\.$#"
- count: 1
- path: src/PubNub/Endpoints/FileSharing/FileSharingEndpoint.php
-
- -
- message: "#^Method PubNub\\\\Endpoints\\\\FileSharing\\\\FileSharingEndpoint\\:\\:fileName\\(\\) has no return type specified\\.$#"
- count: 1
- path: src/PubNub/Endpoints/FileSharing/FileSharingEndpoint.php
-
- -
- message: "#^Method PubNub\\\\Endpoints\\\\FileSharing\\\\FileSharingEndpoint\\:\\:fileName\\(\\) has parameter \\$fileName with no type specified\\.$#"
- count: 1
- path: src/PubNub/Endpoints/FileSharing/FileSharingEndpoint.php
-
-
message: "#^Method PubNub\\\\Endpoints\\\\FileSharing\\\\GetFileDownloadUrl\\:\\:createResponse\\(\\) has parameter \\$result with no value type specified in iterable type array\\.$#"
count: 1
@@ -3020,11 +2980,6 @@ parameters:
count: 1
path: src/PubNub/Models/Consumer/FileSharing/PNGetFilesItem.php
- -
- message: "#^Binary operation \"\\.\" between 'Get file successβ¦' and array results in an error\\.$#"
- count: 1
- path: src/PubNub/Models/Consumer/FileSharing/PNGetFilesResult.php
-
-
message: "#^Method PubNub\\\\Models\\\\Consumer\\\\FileSharing\\\\PNGetFilesResult\\:\\:__construct\\(\\) has parameter \\$result with no type specified\\.$#"
count: 1
@@ -3630,11 +3585,6 @@ parameters:
count: 1
path: src/PubNub/Models/Consumer/Objects/UUID/PNGetAllUUIDMetadataResult.php
- -
- message: "#^Method PubNub\\\\Models\\\\Consumer\\\\Objects\\\\UUID\\\\PNGetUUIDMetadataResult\\:\\:__construct\\(\\) has parameter \\$custom with no value type specified in iterable type array\\.$#"
- count: 1
- path: src/PubNub/Models/Consumer/Objects/UUID/PNGetUUIDMetadataResult.php
-
-
message: "#^Method PubNub\\\\Models\\\\Consumer\\\\Objects\\\\UUID\\\\PNGetUUIDMetadataResult\\:\\:__construct\\(\\) has parameter \\$email with no value type specified in iterable type array\\.$#"
count: 1
@@ -3655,31 +3605,6 @@ parameters:
count: 1
path: src/PubNub/Models/Consumer/Objects/UUID/PNGetUUIDMetadataResult.php
- -
- message: "#^Method PubNub\\\\Models\\\\Consumer\\\\Objects\\\\UUID\\\\PNGetUUIDMetadataResult\\:\\:getCustom\\(\\) should return object but returns array\\.$#"
- count: 1
- path: src/PubNub/Models/Consumer/Objects/UUID/PNGetUUIDMetadataResult.php
-
- -
- message: "#^Parameter \\#6 \\$custom of class PubNub\\\\Models\\\\Consumer\\\\Objects\\\\UUID\\\\PNGetUUIDMetadataResult constructor expects array\\|null, stdClass given\\.$#"
- count: 1
- path: src/PubNub/Models/Consumer/Objects/UUID/PNGetUUIDMetadataResult.php
-
- -
- message: "#^Parameter \\#7 \\$updated of class PubNub\\\\Models\\\\Consumer\\\\Objects\\\\UUID\\\\PNGetUUIDMetadataResult constructor expects string\\|null, stdClass\\|null given\\.$#"
- count: 1
- path: src/PubNub/Models/Consumer/Objects/UUID/PNGetUUIDMetadataResult.php
-
- -
- message: "#^Parameter \\#8 \\$eTag of class PubNub\\\\Models\\\\Consumer\\\\Objects\\\\UUID\\\\PNGetUUIDMetadataResult constructor expects string\\|null, stdClass\\|null given\\.$#"
- count: 1
- path: src/PubNub/Models/Consumer/Objects/UUID/PNGetUUIDMetadataResult.php
-
- -
- message: "#^Property PubNub\\\\Models\\\\Consumer\\\\Objects\\\\UUID\\\\PNGetUUIDMetadataResult\\:\\:\\$custom type has no value type specified in iterable type array\\.$#"
- count: 1
- path: src/PubNub/Models/Consumer/Objects/UUID/PNGetUUIDMetadataResult.php
-
-
message: "#^Property PubNub\\\\Models\\\\Consumer\\\\Objects\\\\UUID\\\\PNGetUUIDMetadataResult\\:\\:\\$email \\(string\\) does not accept array\\.$#"
count: 1
@@ -5930,41 +5855,6 @@ parameters:
count: 1
path: tests/integrational/FetchMessagesTest.php
- -
- message: "#^Method PubNubTests\\\\integrational\\\\FilesTest\\:\\:testDeleteAllFiles\\(\\) has no return type specified\\.$#"
- count: 1
- path: tests/integrational/FilesTest.php
-
- -
- message: "#^Method PubNubTests\\\\integrational\\\\FilesTest\\:\\:testDownloadFiles\\(\\) has no return type specified\\.$#"
- count: 1
- path: tests/integrational/FilesTest.php
-
- -
- message: "#^Method PubNubTests\\\\integrational\\\\FilesTest\\:\\:testEmptyFileList\\(\\) has no return type specified\\.$#"
- count: 1
- path: tests/integrational/FilesTest.php
-
- -
- message: "#^Method PubNubTests\\\\integrational\\\\FilesTest\\:\\:testGetDownloadUrls\\(\\) has no return type specified\\.$#"
- count: 1
- path: tests/integrational/FilesTest.php
-
- -
- message: "#^Method PubNubTests\\\\integrational\\\\FilesTest\\:\\:testNonEmptyFileList\\(\\) has no return type specified\\.$#"
- count: 1
- path: tests/integrational/FilesTest.php
-
- -
- message: "#^Method PubNubTests\\\\integrational\\\\FilesTest\\:\\:testSendBinaryFile\\(\\) has no return type specified\\.$#"
- count: 1
- path: tests/integrational/FilesTest.php
-
- -
- message: "#^Method PubNubTests\\\\integrational\\\\FilesTest\\:\\:testSendTextFile\\(\\) has no return type specified\\.$#"
- count: 1
- path: tests/integrational/FilesTest.php
-
-
message: "#^Method Tests\\\\Integrational\\\\GetStateTest\\:\\:testCombination\\(\\) has no return type specified\\.$#"
count: 1
@@ -6695,11 +6585,6 @@ parameters:
count: 1
path: tests/integrational/SslTest.php
- -
- message: "#^Else branch is unreachable because previous condition is always true\\.$#"
- count: 1
- path: tests/integrational/SubscribePresenceTest.php
-
-
message: "#^Method Tests\\\\Integrational\\\\MySubscribeCallbackToTestPresence\\:\\:areBothConnectedAndDisconnectedInvoked\\(\\) has no return type specified\\.$#"
count: 1
@@ -6950,11 +6835,6 @@ parameters:
count: 1
path: tests/integrational/SubscribeTest.php
- -
- message: "#^Else branch is unreachable because previous condition is always true\\.$#"
- count: 1
- path: tests/integrational/SubscribeWildCardTest.php
-
-
message: "#^Method Tests\\\\Integrational\\\\MySubscribeCallbackToTestWildCard\\:\\:areBothConnectedAndDisconnectedInvoked\\(\\) has no return type specified\\.$#"
count: 1
diff --git a/phpunit.xml b/phpunit.xml
new file mode 100755
index 00000000..5e9c0d0d
--- /dev/null
+++ b/phpunit.xml
@@ -0,0 +1,21 @@
+
+
+
+
+ ./src/
+
+
+
+
+ ./tests/integrational/
+
+
+ ./tests/functional/
+
+
+
\ No newline at end of file
diff --git a/src/PubNub/Crypto/AesCbcCryptor.php b/src/PubNub/Crypto/AesCbcCryptor.php
index 34d6c35a..ebe0766f 100644
--- a/src/PubNub/Crypto/AesCbcCryptor.php
+++ b/src/PubNub/Crypto/AesCbcCryptor.php
@@ -23,7 +23,7 @@ public function __construct(string $cipherKey)
public function getIV(): string
{
- return random_bytes(self::IV_LENGTH);
+ return random_bytes(static::IV_LENGTH);
}
public function getCipherKey($cipherKey = null): string
@@ -41,8 +41,8 @@ public function encrypt(string $text, ?string $cipherKey = null): CryptoPayload
{
$secret = $this->getSecret($this->getCipherKey($cipherKey));
$iv = $this->getIV();
- $encrypted = openssl_encrypt($text, self::CIPHER_ALGO, $secret, OPENSSL_RAW_DATA, $iv);
- return new CryptoPayload($encrypted, $iv, self::CRYPTOR_ID);
+ $encrypted = openssl_encrypt($text, static::CIPHER_ALGO, $secret, OPENSSL_RAW_DATA, $iv);
+ return new CryptoPayload($encrypted, $iv, static::CRYPTOR_ID);
}
public function decrypt(CryptoPayload $payload, ?string $cipherKey = null)
@@ -50,8 +50,8 @@ public function decrypt(CryptoPayload $payload, ?string $cipherKey = null)
$text = $payload->getData();
$secret = $this->getSecret($this->getCipherKey($cipherKey));
$iv = $payload->getCryptorData();
- $decrypted = openssl_decrypt($text, self::CIPHER_ALGO, $secret, OPENSSL_RAW_DATA, $iv);
- $result = json_decode($decrypted);
+ $decrypted = openssl_decrypt($text, static::CIPHER_ALGO, $secret, OPENSSL_RAW_DATA, $iv);
+ $result = json_decode($decrypted, false);
if ($result === null) {
return $decrypted;
diff --git a/src/PubNub/CryptoModule.php b/src/PubNub/CryptoModule.php
index b3b1c2e3..68a35d08 100644
--- a/src/PubNub/CryptoModule.php
+++ b/src/PubNub/CryptoModule.php
@@ -10,6 +10,7 @@
use PubNub\Exceptions\PubNubCryptoException;
use PubNub\Exceptions\PubNubResponseParsingException;
+/** @phpstan-consistent-constructor */
class CryptoModule
{
public const CRYPTOR_VERSION = 1;
@@ -25,7 +26,7 @@ public function __construct($cryptorMap, string $defaultCryptorId)
$this->defaultCryptorId = $defaultCryptorId;
}
- public function registerCryptor(Cryptor $cryptor, ?string $cryptorId = null): self
+ public function registerCryptor(Cryptor $cryptor, ?string $cryptorId = null): static
{
if (is_null($cryptorId)) {
$cryptorId = $cryptor::CRYPTOR_ID;
@@ -70,7 +71,13 @@ public function encrypt($data, ?string $cryptorId = null): string
return base64_encode($header . $cryptoPayload->getData());
}
- public function decrypt(string | object $input): string | object
+ /**
+ * @param string | object $input
+ * @return string | object | array | null
+ * @throws PubNubCryptoException
+ * @throws PubNubResponseParsingException
+ */
+ public function decrypt(string | object $input): string | object | array | null
{
$input = $this->parseInput($input);
$data = base64_decode($input);
@@ -90,7 +97,7 @@ public function decrypt(string | object $input): string | object
public function encodeHeader(CryptoPayload $payload): string
{
- if ($payload->getCryptorId() == self::FALLBACK_CRYPTOR_ID) {
+ if ($payload->getCryptorId() == static::FALLBACK_CRYPTOR_ID) {
return '';
}
@@ -108,13 +115,13 @@ public function encodeHeader(CryptoPayload $payload): string
$cryptorDataLength = chr(255) . chr(hexdec($hexlen[0])) . chr(hexdec($hexlen[1]));
}
- return self::SENTINEL . $version . $payload->getCryptorId() . $cryptorDataLength . $payload->getCryptorData();
+ return static::SENTINEL . $version . $payload->getCryptorId() . $cryptorDataLength . $payload->getCryptorData();
}
public function decodeHeader(string $header): CryptoHeader
{
- if (strlen($header < 10) or substr($header, 0, 4) != self::SENTINEL) {
- return new CryptoHeader('', self::FALLBACK_CRYPTOR_ID, '', 0);
+ if (strlen($header < 10) or substr($header, 0, 4) != static::SENTINEL) {
+ return new CryptoHeader('', static::FALLBACK_CRYPTOR_ID, '', 0);
}
$sentinel = substr($header, 0, 4);
$version = ord($header[4]);
@@ -134,9 +141,9 @@ public function decodeHeader(string $header): CryptoHeader
return new CryptoHeader($sentinel, $cryptorId, $cryptorData, $headerLength);
}
- public static function legacyCryptor(string $cipherKey, bool $useRandomIV): self
+ public static function legacyCryptor(string $cipherKey, bool $useRandomIV): static
{
- return new self(
+ return new static(
[
LegacyCryptor::CRYPTOR_ID => new LegacyCryptor($cipherKey, $useRandomIV),
AesCbcCryptor::CRYPTOR_ID => new AesCbcCryptor($cipherKey),
@@ -145,14 +152,14 @@ public static function legacyCryptor(string $cipherKey, bool $useRandomIV): self
);
}
- public static function aesCbcCryptor(string $cipherKey, bool $useRandomIV): self
+ public static function aesCbcCryptor(string $cipherKey, bool $useRandomIV): static
{
- return new self(
+ return new static(
[
LegacyCryptor::CRYPTOR_ID => new LegacyCryptor($cipherKey, $useRandomIV),
AesCbcCryptor::CRYPTOR_ID => new AesCbcCryptor($cipherKey),
],
- aesCbcCryptor::CRYPTOR_ID
+ AesCbcCryptor::CRYPTOR_ID
);
}
diff --git a/src/PubNub/Endpoints/Endpoint.php b/src/PubNub/Endpoints/Endpoint.php
index 16f63772..62f7001d 100755
--- a/src/PubNub/Endpoints/Endpoint.php
+++ b/src/PubNub/Endpoints/Endpoint.php
@@ -368,6 +368,11 @@ protected function sendRequest(RequestInterface $request): PNEnvelope
$statusCode = $exception->getCode();
$response = substr($exception->getMessage(), strpos($exception->getMessage(), "\n") + 1);
$pnServerException = new PubNubServerException();
+ if (is_callable([$exception, 'getResponse'])) {
+ $response = $exception->getResponse()->getBody()->getContents();
+ } else {
+ $response = substr($exception->getMessage(), strpos($exception->getMessage(), "\n") + 1);
+ }
$pnServerException->setRawBody($response);
$pnServerException->setStatusCode($exception->getCode());
diff --git a/src/PubNub/Endpoints/FileSharing/FetchFileUploadS3Data.php b/src/PubNub/Endpoints/FileSharing/FetchFileUploadS3Data.php
index b21ba37e..77d0155d 100644
--- a/src/PubNub/Endpoints/FileSharing/FetchFileUploadS3Data.php
+++ b/src/PubNub/Endpoints/FileSharing/FetchFileUploadS3Data.php
@@ -16,13 +16,13 @@ class FetchFileUploadS3Data extends Endpoint
protected ?string $channel;
protected ?string $fileName;
- public function channel($channel): self
+ public function channel(string $channel): self
{
$this->channel = $channel;
return $this;
}
- public function fileName($fileName)
+ public function fileName(string $fileName): self
{
$this->fileName = $fileName;
return $this;
@@ -72,7 +72,7 @@ public function getOperationType()
return PNOperationType::PNFetchFileUploadS3DataAction;
}
- public function name()
+ public function name(): string
{
return "Fetch file upload S3 data";
}
diff --git a/src/PubNub/Endpoints/FileSharing/FileSharingEndpoint.php b/src/PubNub/Endpoints/FileSharing/FileSharingEndpoint.php
index 2b3cde0e..75ffc4d2 100644
--- a/src/PubNub/Endpoints/FileSharing/FileSharingEndpoint.php
+++ b/src/PubNub/Endpoints/FileSharing/FileSharingEndpoint.php
@@ -11,19 +11,19 @@ abstract class FileSharingEndpoint extends Endpoint
protected ?string $fileId;
protected ?string $fileName;
- public function channel($channel): self
+ public function channel(string $channel): static
{
$this->channel = $channel;
return $this;
}
- public function fileId($fileId): self
+ public function fileId(string $fileId): static
{
$this->fileId = $fileId;
return $this;
}
- public function fileName($fileName)
+ public function fileName(string $fileName): static
{
$this->fileName = $fileName;
return $this;
diff --git a/src/PubNub/Endpoints/FileSharing/PublishFileMessage.php b/src/PubNub/Endpoints/FileSharing/PublishFileMessage.php
index e073dbaa..4a258db6 100644
--- a/src/PubNub/Endpoints/FileSharing/PublishFileMessage.php
+++ b/src/PubNub/Endpoints/FileSharing/PublishFileMessage.php
@@ -2,7 +2,6 @@
namespace PubNub\Endpoints\FileSharing;
-use PubNub\Endpoints\Endpoint;
use PubNub\Enums\PNHttpMethod;
use PubNub\Enums\PNOperationType;
use PubNub\Models\Consumer\FileSharing\PNPublishFileMessageResult;
@@ -64,7 +63,7 @@ protected function buildPath()
$this->pubnub->getConfiguration()->getPublishKey(),
$this->pubnub->getConfiguration()->getSubscribeKey(),
urlencode($this->channel),
- urlencode($this->buildMessage())
+ $this->buildMessage()
);
}
@@ -73,9 +72,9 @@ public function encryptMessage($message)
$crypto = $this->pubnub->getCrypto();
$messageString = PubNubUtil::writeValueAsString($message);
if ($crypto) {
- return $crypto->encrypt($messageString);
+ return "\"" . urlencode($crypto->encrypt($messageString)) . "\"";
}
- return $messageString;
+ return urlencode($messageString);
}
protected function buildMessage()
diff --git a/src/PubNub/Endpoints/FileSharing/SendFile.php b/src/PubNub/Endpoints/FileSharing/SendFile.php
index 8afb3524..23ed555a 100644
--- a/src/PubNub/Endpoints/FileSharing/SendFile.php
+++ b/src/PubNub/Endpoints/FileSharing/SendFile.php
@@ -202,7 +202,7 @@ protected function buildPath()
protected function encryptPayload()
{
$crypto = $this->pubnub->getCrypto();
- if ($this->fileHandle) {
+ if (isset($this->fileHandle) && is_resource($this->fileHandle)) {
$fileContent = stream_get_contents($this->fileHandle);
} else {
$fileContent = $this->fileContent;
diff --git a/src/PubNub/Endpoints/Objects/Channel/GetAllChannelMetadata.php b/src/PubNub/Endpoints/Objects/Channel/GetAllChannelMetadata.php
index 71a581b2..5f9ff8c9 100644
--- a/src/PubNub/Endpoints/Objects/Channel/GetAllChannelMetadata.php
+++ b/src/PubNub/Endpoints/Objects/Channel/GetAllChannelMetadata.php
@@ -124,7 +124,7 @@ protected function customParams()
}
}
- $params['sort'] = $sortEntries;
+ $params['sort'] = join(",", $sortEntries);
}
return $params;
diff --git a/src/PubNub/Endpoints/Objects/MatchesETagTrait.php b/src/PubNub/Endpoints/Objects/MatchesETagTrait.php
index d571d4af..755d86dd 100644
--- a/src/PubNub/Endpoints/Objects/MatchesETagTrait.php
+++ b/src/PubNub/Endpoints/Objects/MatchesETagTrait.php
@@ -11,7 +11,7 @@ trait MatchesETagTrait
* @param string $eTag
* @return $this
*/
- public function ifMatchesETag(string $eTag): self
+ public function ifMatchesETag(string $eTag): static
{
$this->eTag = $eTag;
$this->customHeaders['If-Match'] = $eTag;
diff --git a/src/PubNub/Endpoints/Objects/Member/GetMembers.php b/src/PubNub/Endpoints/Objects/Member/GetMembers.php
index 9bc498c2..3961b778 100644
--- a/src/PubNub/Endpoints/Objects/Member/GetMembers.php
+++ b/src/PubNub/Endpoints/Objects/Member/GetMembers.php
@@ -174,7 +174,7 @@ protected function customParams()
}
}
- $params['sort'] = $sortEntries;
+ $params['sort'] = join(",", $sortEntries);
}
return $params;
diff --git a/src/PubNub/Endpoints/Objects/Member/ManageMembers.php b/src/PubNub/Endpoints/Objects/Member/ManageMembers.php
index cd1db2bb..5baaab38 100644
--- a/src/PubNub/Endpoints/Objects/Member/ManageMembers.php
+++ b/src/PubNub/Endpoints/Objects/Member/ManageMembers.php
@@ -289,7 +289,7 @@ protected function customParams()
}
}
- $params['sort'] = $sortEntries;
+ $params['sort'] = join(",", $sortEntries);
}
return $params;
diff --git a/src/PubNub/Endpoints/Objects/Member/RemoveMembers.php b/src/PubNub/Endpoints/Objects/Member/RemoveMembers.php
index 2e63ee40..a1308bdb 100644
--- a/src/PubNub/Endpoints/Objects/Member/RemoveMembers.php
+++ b/src/PubNub/Endpoints/Objects/Member/RemoveMembers.php
@@ -241,7 +241,7 @@ protected function customParams()
}
}
- $params['sort'] = $sortEntries;
+ $params['sort'] = join(",", $sortEntries);
}
return $params;
diff --git a/src/PubNub/Endpoints/Objects/Member/SetMembers.php b/src/PubNub/Endpoints/Objects/Member/SetMembers.php
index cbf43bd7..0596f70d 100644
--- a/src/PubNub/Endpoints/Objects/Member/SetMembers.php
+++ b/src/PubNub/Endpoints/Objects/Member/SetMembers.php
@@ -260,7 +260,7 @@ protected function customParams()
array_push($sortEntries, $key);
}
}
- $params['sort'] = $sortEntries;
+ $params['sort'] = join(",", $sortEntries);
}
return $params;
diff --git a/src/PubNub/Endpoints/Objects/Membership/GetMemberships.php b/src/PubNub/Endpoints/Objects/Membership/GetMemberships.php
index 5d62f66d..645fc45e 100644
--- a/src/PubNub/Endpoints/Objects/Membership/GetMemberships.php
+++ b/src/PubNub/Endpoints/Objects/Membership/GetMemberships.php
@@ -193,7 +193,7 @@ protected function customParams()
}
}
- $params['sort'] = $sortEntries;
+ $params['sort'] = join(",", $sortEntries);
}
return $params;
diff --git a/src/PubNub/Endpoints/Objects/Membership/ManageMemberships.php b/src/PubNub/Endpoints/Objects/Membership/ManageMemberships.php
index a0fb093a..794231b7 100644
--- a/src/PubNub/Endpoints/Objects/Membership/ManageMemberships.php
+++ b/src/PubNub/Endpoints/Objects/Membership/ManageMemberships.php
@@ -304,7 +304,7 @@ protected function customParams()
}
}
- $params['sort'] = $sortEntries;
+ $params['sort'] = join(",", $sortEntries);
}
return $params;
diff --git a/src/PubNub/Endpoints/Objects/Membership/RemoveMemberships.php b/src/PubNub/Endpoints/Objects/Membership/RemoveMemberships.php
index 785ae748..ec71f9e2 100644
--- a/src/PubNub/Endpoints/Objects/Membership/RemoveMemberships.php
+++ b/src/PubNub/Endpoints/Objects/Membership/RemoveMemberships.php
@@ -253,7 +253,7 @@ protected function customParams()
}
}
- $params['sort'] = $sortEntries;
+ $params['sort'] = join(",", $sortEntries);
}
return $params;
diff --git a/src/PubNub/Endpoints/Objects/Membership/SetMemberships.php b/src/PubNub/Endpoints/Objects/Membership/SetMemberships.php
index e358632b..1222ab6c 100644
--- a/src/PubNub/Endpoints/Objects/Membership/SetMemberships.php
+++ b/src/PubNub/Endpoints/Objects/Membership/SetMemberships.php
@@ -269,7 +269,7 @@ protected function customParams()
}
}
- $params['sort'] = $sortEntries;
+ $params['sort'] = join(",", $sortEntries);
}
return $params;
diff --git a/src/PubNub/Endpoints/Objects/UUID/GetAllUUIDMetadata.php b/src/PubNub/Endpoints/Objects/UUID/GetAllUUIDMetadata.php
index 3723b4aa..2117f0bf 100644
--- a/src/PubNub/Endpoints/Objects/UUID/GetAllUUIDMetadata.php
+++ b/src/PubNub/Endpoints/Objects/UUID/GetAllUUIDMetadata.php
@@ -124,7 +124,7 @@ protected function customParams()
}
}
- $params['sort'] = $sortEntries;
+ $params['sort'] = join(",", $sortEntries);
}
return $params;
diff --git a/src/PubNub/Endpoints/Objects/UUID/SetUUIDMetadata.php b/src/PubNub/Endpoints/Objects/UUID/SetUUIDMetadata.php
index c623d20d..139ca40e 100644
--- a/src/PubNub/Endpoints/Objects/UUID/SetUUIDMetadata.php
+++ b/src/PubNub/Endpoints/Objects/UUID/SetUUIDMetadata.php
@@ -26,10 +26,9 @@ class SetUUIDMetadata extends Endpoint
* @param string $uuid
* @return $this
*/
- public function uuid($uuid)
+ public function uuid($uuid): static
{
$this->uuid = $uuid;
-
return $this;
}
@@ -37,10 +36,60 @@ public function uuid($uuid)
* @param array $meta
* @return $this
*/
- public function meta($meta)
+ public function meta($meta): static
{
$this->meta = $meta;
+ return $this;
+ }
+
+ /**
+ * @param string $name
+ * @return $this
+ */
+ public function name(string $name): static
+ {
+ $this->meta['name'] = $name;
+
+ return $this;
+ }
+
+ /**
+ * @param string $externalId
+ * @return $this
+ */
+ public function externalId(string $externalId): static
+ {
+ $this->meta['externalId'] = $externalId;
+ return $this;
+ }
+ /**
+ * @param string $profileUrl
+ * @return $this
+ */
+ public function profileUrl(string $profileUrl): static
+ {
+ $this->meta['profileUrl'] = $profileUrl;
+ return $this;
+ }
+
+ /**
+ * @param string $email
+ * @return $this
+ */
+ public function email(string $email): static
+ {
+ $this->meta['email'] = $email;
+ return $this;
+ }
+
+ /**
+ * @param array $custom
+ * @return $this
+ */
+ public function custom(array $custom): static
+ {
+ $this->meta['custom'] = $custom;
return $this;
}
diff --git a/src/PubNub/Exceptions/PubNubServerException.php b/src/PubNub/Exceptions/PubNubServerException.php
index 02ac86ae..05fea378 100644
--- a/src/PubNub/Exceptions/PubNubServerException.php
+++ b/src/PubNub/Exceptions/PubNubServerException.php
@@ -18,7 +18,9 @@ class PubNubServerException extends PubNubException
protected function updateMessage()
{
$this->message = "Server responded with an error";
-
+ if ($this->body) {
+ $this->message .= " " . json_encode($this->body);
+ }
if ($this->statusCode > 0) {
$this->message .= " and the status code is " . $this->statusCode;
}
@@ -81,6 +83,8 @@ public function getServerErrorMessage()
{
if (isset($this->body->error->message)) {
return $this->body->error->message;
+ } elseif (isset($this->body->message)) {
+ return $this->body->message;
} else {
return null;
}
@@ -90,6 +94,8 @@ public function getServerErrorSource()
{
if (isset($this->body->error->source)) {
return $this->body->error->source;
+ } elseif (isset($this->body->source)) {
+ return $this->body->source;
} else {
return null;
}
@@ -99,6 +105,8 @@ public function getServerErrorDetails()
{
if (isset($this->body->error->details[0])) {
return $this->body->error->details[0];
+ } elseif (isset($this->body->details[0])) {
+ return $this->body->details[0];
} else {
return null;
}
diff --git a/src/PubNub/Exceptions/PubNubUnsubscribeException.php b/src/PubNub/Exceptions/PubNubUnsubscribeException.php
index b0f4fc11..c2c99072 100644
--- a/src/PubNub/Exceptions/PubNubUnsubscribeException.php
+++ b/src/PubNub/Exceptions/PubNubUnsubscribeException.php
@@ -2,7 +2,6 @@
namespace PubNub\Exceptions;
-
use PubNub\Builders\DTO\UnsubscribeOperation;
use PubNub\Managers\SubscriptionManager;
@@ -67,7 +66,8 @@ public function setChannelGroups(array $channelGroups)
* @param SubscriptionManager $subscriptionManager
* @return UnsubscribeOperation
*/
- public function getUnsubscribeOperation(SubscriptionManager $subscriptionManager) {
+ public function getUnsubscribeOperation(SubscriptionManager $subscriptionManager)
+ {
if ($this->all) {
return (new UnsubscribeOperation())
->setChannels($subscriptionManager->subscriptionState->prepareChannelList(false))
@@ -78,4 +78,4 @@ public function getUnsubscribeOperation(SubscriptionManager $subscriptionManager
->setChannelGroups($this->channelGroups);
}
}
-}
\ No newline at end of file
+}
diff --git a/src/PubNub/Models/Consumer/FileSharing/PNGetFilesResult.php b/src/PubNub/Models/Consumer/FileSharing/PNGetFilesResult.php
index 50d7396a..86e4f377 100644
--- a/src/PubNub/Models/Consumer/FileSharing/PNGetFilesResult.php
+++ b/src/PubNub/Models/Consumer/FileSharing/PNGetFilesResult.php
@@ -27,7 +27,7 @@ public function __construct($result)
public function __toString()
{
- return "Get file success with data: " . $this->data;
+ return "Get file success with data: " . var_export($this->data, true);
}
public function getData()
diff --git a/src/PubNub/Models/Consumer/Objects/PNIncludes.php b/src/PubNub/Models/Consumer/Objects/PNIncludes.php
index 6d6d4a9a..8325903a 100644
--- a/src/PubNub/Models/Consumer/Objects/PNIncludes.php
+++ b/src/PubNub/Models/Consumer/Objects/PNIncludes.php
@@ -28,25 +28,25 @@ public function __toString(): string
return implode(',', $result);
}
- public function custom(bool $custom = true): self
+ public function custom(bool $custom = true): static
{
$this->custom = $custom;
return $this;
}
- public function status(bool $status = true): self
+ public function status(bool $status = true): static
{
$this->status = $status;
return $this;
}
- public function totalCount(bool $totalCount = true): self
+ public function totalCount(bool $totalCount = true): static
{
$this->totalCount = $totalCount;
return $this;
}
- public function type(bool $type = true): self
+ public function type(bool $type = true): static
{
$this->type = $type;
return $this;
diff --git a/src/PubNub/Models/Consumer/Objects/UUID/PNGetUUIDMetadataResult.php b/src/PubNub/Models/Consumer/Objects/UUID/PNGetUUIDMetadataResult.php
index 3c185de6..c411b2c5 100755
--- a/src/PubNub/Models/Consumer/Objects/UUID/PNGetUUIDMetadataResult.php
+++ b/src/PubNub/Models/Consumer/Objects/UUID/PNGetUUIDMetadataResult.php
@@ -19,13 +19,13 @@ class PNGetUUIDMetadataResult
/** @var string */
protected $email;
- /** @var array */
+ /** @var mixed */
protected $custom;
- /** @var string */
+ /** @var ?string */
protected $updated;
- /** @var string */
+ /** @var ?string */
protected $eTag;
/**
@@ -35,9 +35,9 @@ class PNGetUUIDMetadataResult
* @param array $externalId
* @param array $profileUrl
* @param array $email
- * @param array $custom
- * @param string $updated
- * @param string $eTag
+ * @param mixed $custom
+ * @param ?string $updated
+ * @param ?string $eTag
*/
public function __construct(
$id,
@@ -62,7 +62,7 @@ public function __construct(
/**
* @return string
*/
- public function getId()
+ public function getId(): ?string
{
return $this->id;
}
@@ -70,7 +70,7 @@ public function getId()
/**
* @return string
*/
- public function getName()
+ public function getName(): ?string
{
return $this->name;
}
@@ -78,7 +78,7 @@ public function getName()
/**
* @return string
*/
- public function getExternalId()
+ public function getExternalId(): ?string
{
return $this->externalId;
}
@@ -86,7 +86,7 @@ public function getExternalId()
/**
* @return string
*/
- public function getProfileUrl()
+ public function getProfileUrl(): ?string
{
return $this->profileUrl;
}
@@ -94,15 +94,15 @@ public function getProfileUrl()
/**
* @return string
*/
- public function getEmail()
+ public function getEmail(): ?string
{
return $this->email;
}
/**
- * @return object
+ * @return mixed
*/
- public function getCustom()
+ public function getCustom(): mixed
{
return $this->custom;
}
@@ -110,7 +110,7 @@ public function getCustom()
/**
* @return string
*/
- public function getUpdated()
+ public function getUpdated(): ?string
{
return $this->updated;
}
@@ -118,7 +118,7 @@ public function getUpdated()
/**
* @return string
*/
- public function getETag()
+ public function getETag(): ?string
{
return $this->eTag;
}
@@ -190,11 +190,11 @@ public static function fromPayload(array $payload)
}
if (array_key_exists("updated", $meta)) {
- $updated = (object)$meta["updated"];
+ $updated = $meta["updated"];
}
if (array_key_exists("eTag", $meta)) {
- $eTag = (object)$meta["eTag"];
+ $eTag = $meta["eTag"];
}
return new PNGetUUIDMetadataResult(
diff --git a/src/PubNub/Models/ResponseHelpers/PNStatus.php b/src/PubNub/Models/ResponseHelpers/PNStatus.php
index 70ed8029..20198520 100755
--- a/src/PubNub/Models/ResponseHelpers/PNStatus.php
+++ b/src/PubNub/Models/ResponseHelpers/PNStatus.php
@@ -5,6 +5,7 @@
use PubNub\Enums\PNOperationType;
use PubNub\Enums\PNStatusCategory;
use PubNub\Exceptions\PubNubException;
+use PubNub\Exceptions\PubNubServerException;
class PNStatus
{
@@ -53,9 +54,9 @@ public function isError()
}
/**
- * @return PubNubException
+ * @return PubNubException | PubNubServerException | null
*/
- public function getException()
+ public function getException(): PubNubException | PubNubServerException | null
{
return $this->exception;
}
diff --git a/src/PubNub/PubNub.php b/src/PubNub/PubNub.php
index 41abc0fc..726d8f6e 100644
--- a/src/PubNub/PubNub.php
+++ b/src/PubNub/PubNub.php
@@ -56,7 +56,9 @@
use Psr\Log\LoggerInterface;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\NullLogger;
-use PubNub\Endpoints\FileSharing\{SendFile, DeleteFile, DownloadFile, GetFileDownloadUrl, ListFiles};
+use PubNub\Endpoints\FileSharing\{
+ SendFile, DeleteFile, DownloadFile, GetFileDownloadUrl, ListFiles, PublishFileMessage
+};
use PubNub\Models\Consumer\AccessManager\PNAccessManagerTokenResult;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Client\ClientInterface;
@@ -668,6 +670,11 @@ public function sendFile(): SendFile
return new SendFile($this);
}
+ public function publishFileMessage(): PublishFileMessage
+ {
+ return new PublishFileMessage($this);
+ }
+
public function deleteFile(): DeleteFile
{
return new DeleteFile($this);
diff --git a/tests/Examples/AccessManagerTest.php b/tests/Examples/AccessManagerTest.php
new file mode 100644
index 00000000..c6dc8ae3
--- /dev/null
+++ b/tests/Examples/AccessManagerTest.php
@@ -0,0 +1,37 @@
+assertFileExists($fileName);
+ $this->assertFileIsReadable($fileName);
+
+ // Let's make sure that the example has properly set up the environment
+ if (!getenv('SUBSCRIBE_KEY')) {
+ putenv('SUBSCRIBE_KEY=demo');
+ }
+ if (!getenv('PUBLISH_KEY')) {
+ putenv('PUBLISH_KEY=demo');
+ }
+ putenv('WAIT_FOR_REVOKE=0');
+
+ if (!getenv('SECRET_KEY')) {
+ putenv('SECRET_KEY=demo');
+ }
+
+ ob_start();
+ // Include the examples file
+ require_once $fileName;
+ $output = ob_get_clean();
+
+ $this->assertStringNotContainsString('FAIL', $output);
+ $this->assertStringNotContainsString('Exception', $output);
+ }
+}
diff --git a/tests/Examples/AppContextTest.php b/tests/Examples/AppContextTest.php
new file mode 100644
index 00000000..b6e4b974
--- /dev/null
+++ b/tests/Examples/AppContextTest.php
@@ -0,0 +1,32 @@
+assertFileExists($fileName);
+ $this->assertFileIsReadable($fileName);
+
+ // Let's make sure that the example has properly set up the environment
+ if (!getenv('SUBSCRIBE_KEY')) {
+ putenv('SUBSCRIBE_KEY=demo');
+ }
+ if (!getenv('PUBLISH_KEY')) {
+ putenv('PUBLISH_KEY=demo');
+ }
+
+ ob_start();
+ // Include the examples file
+ require_once $fileName;
+ $output = ob_get_clean();
+
+ $this->assertStringNotContainsString('Failed', $output);
+ $this->assertStringNotContainsString('Exception', $output);
+ }
+}
diff --git a/tests/Examples/FileSharingTest.php b/tests/Examples/FileSharingTest.php
new file mode 100644
index 00000000..b2db8f60
--- /dev/null
+++ b/tests/Examples/FileSharingTest.php
@@ -0,0 +1,32 @@
+assertFileExists($fileName);
+ $this->assertFileIsReadable($fileName);
+
+ // Let's make sure that the example has properly set up the environment
+ if (!getenv('SUBSCRIBE_KEY')) {
+ putenv('SUBSCRIBE_KEY=demo');
+ }
+ if (!getenv('PUBLISH_KEY')) {
+ putenv('PUBLISH_KEY=demo');
+ }
+
+ ob_start();
+ // Include the examples file
+ require_once $fileName;
+ $output = ob_get_clean();
+
+ $this->assertStringNotContainsString('Failed', $output);
+ $this->assertStringNotContainsString('Exception', $output);
+ }
+}
diff --git a/tests/Examples/MessageActionsTest.php b/tests/Examples/MessageActionsTest.php
new file mode 100644
index 00000000..b0fb5182
--- /dev/null
+++ b/tests/Examples/MessageActionsTest.php
@@ -0,0 +1,32 @@
+assertFileExists($fileName);
+ $this->assertFileIsReadable($fileName);
+
+ // Let's make sure that the example has properly set up the environment
+ if (!getenv('SUBSCRIBE_KEY')) {
+ putenv('SUBSCRIBE_KEY=demo');
+ }
+ if (!getenv('PUBLISH_KEY')) {
+ putenv('PUBLISH_KEY=demo');
+ }
+
+ ob_start();
+ // Include the examples file
+ require_once $fileName;
+ $output = ob_get_clean();
+
+ $this->assertStringContainsString('=== MESSAGE ACTIONS DEMO COMPLETE ===', $output);
+ $this->assertStringNotContainsString('Exception', $output);
+ }
+}
diff --git a/tests/Examples/MessagePersistanceTest.php b/tests/Examples/MessagePersistanceTest.php
new file mode 100644
index 00000000..9f56437e
--- /dev/null
+++ b/tests/Examples/MessagePersistanceTest.php
@@ -0,0 +1,32 @@
+assertFileExists($fileName);
+ $this->assertFileIsReadable($fileName);
+
+ // Let's make sure that the example has properly set up the environment
+ if (!getenv('SUBSCRIBE_KEY')) {
+ putenv('SUBSCRIBE_KEY=demo');
+ }
+ if (!getenv('PUBLISH_KEY')) {
+ putenv('PUBLISH_KEY=demo');
+ }
+
+ ob_start();
+ // Include the examples file
+ require_once $fileName;
+ $output = ob_get_clean();
+
+ $this->assertStringContainsString('π All Message Persistence demos completed!', $output);
+ $this->assertStringNotContainsString('Exception', $output);
+ }
+}
diff --git a/tests/Examples/MobilePushTest.php b/tests/Examples/MobilePushTest.php
new file mode 100644
index 00000000..312d1cb0
--- /dev/null
+++ b/tests/Examples/MobilePushTest.php
@@ -0,0 +1,32 @@
+assertFileExists($fileName);
+ $this->assertFileIsReadable($fileName);
+
+ // Let's make sure that the example has properly set up the environment
+ if (!getenv('SUBSCRIBE_KEY')) {
+ putenv('SUBSCRIBE_KEY=demo');
+ }
+ if (!getenv('PUBLISH_KEY')) {
+ putenv('PUBLISH_KEY=demo');
+ }
+
+ ob_start();
+ // Include the examples file
+ require_once $fileName;
+ $output = ob_get_clean();
+
+ $this->assertStringNotContainsString('Failed', $output);
+ $this->assertStringNotContainsString('Exception', $output);
+ }
+}
diff --git a/tests/Examples/PublishingTest.php b/tests/Examples/PublishingTest.php
new file mode 100644
index 00000000..b0894507
--- /dev/null
+++ b/tests/Examples/PublishingTest.php
@@ -0,0 +1,20 @@
+assertTrue(true); // If we reach this point, all examples passed
+ }
+}
diff --git a/tests/integrational/FilesTest.php b/tests/integrational/FilesTest.php
index 38321591..bc3e671f 100644
--- a/tests/integrational/FilesTest.php
+++ b/tests/integrational/FilesTest.php
@@ -8,8 +8,14 @@
use PubNubTests\helpers\PsrStubClient;
use PubNub\Exceptions\PubNubResponseParsingException;
use PubNub\Exceptions\PubNubServerException;
+use PubNub\Exceptions\PubNubConnectionException;
+use GuzzleHttp\Exception\ConnectException;
+use Psr\Http\Message\RequestInterface;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Client\ClientInterface;
-class FilesTest extends PubNubTestCase
+/** @phpstan-consistent-constructor */
+final class FilesTest extends PubNubTestCase
{
protected string $channel = "files-test";
protected string $textFilePath = __DIR__ . '/assets/spam.spam';
@@ -17,14 +23,49 @@ class FilesTest extends PubNubTestCase
protected ?string $textFileId;
protected ?string $binaryFileId;
- public function testEmptyFileList()
+ protected function cleanupFiles(): void
+ {
+ try {
+ $listResponse = $this->pubnub->listFiles()->channel($this->channel)->sync();
+
+ foreach ($listResponse->getData() as $file) {
+ $this->pubnub->deleteFile()
+ ->channel($this->channel)
+ ->fileId($file['id'])
+ ->fileName($file['name'])
+ ->sync();
+ }
+ } catch (\Exception $e) {
+ // Ignore cleanup errors, just print the error for debugging
+ print_r($e);
+ }
+ }
+
+ public static function setUpBeforeClass(): void
+ {
+ $instance = new static();
+ $instance->setUp();
+ $instance->cleanupFiles();
+ parent::setUpBeforeClass();
+ }
+
+ public static function tearDownAfterClass(): void
+ {
+
+ $instance = new static();
+ $instance->setUp();
+ $instance->cleanupFiles();
+ parent::tearDownAfterClass();
+ }
+
+ public function testEmptyFileList(): void
{
$response = $this->pubnub->listFiles()->channel($this->channel)->sync();
$this->assertNotEmpty($response);
$this->assertCount(0, $response->getData());
}
- public function testSendTextFile()
+ public function testSendTextFile(): void
{
$file = fopen($this->textFilePath, "r");
@@ -39,7 +80,7 @@ public function testSendTextFile()
$this->assertNotEmpty($response->getTimestamp());
}
- public function testSendBinaryFile()
+ public function testSendBinaryFile(): void
{
$file = fopen($this->binaryFilePath, "r");
@@ -54,7 +95,7 @@ public function testSendBinaryFile()
$this->assertNotEmpty($response->getTimestamp());
}
- public function testNonEmptyFileList()
+ public function testNonEmptyFileList(): void
{
$response = $this->pubnub->listFiles()->channel($this->channel)->sync();
$this->assertNotEmpty($response);
@@ -62,7 +103,7 @@ public function testNonEmptyFileList()
$this->textFileId = $response->getData()[0]['id'];
}
- public function testGetDownloadUrls()
+ public function testGetDownloadUrls(): void
{
$listFilesResponse = $this->pubnub->listFiles()->channel($this->channel)->sync();
foreach ($listFilesResponse->getData() as $file) {
@@ -76,7 +117,7 @@ public function testGetDownloadUrls()
}
}
- public function testDownloadFiles()
+ public function testDownloadFiles(): void
{
$listFilesResponse = $this->pubnub->listFiles()->channel($this->channel)->sync();
foreach ($listFilesResponse->getData() as $file) {
@@ -94,7 +135,7 @@ public function testDownloadFiles()
}
}
- public function testDeleteAllFiles()
+ public function testDeleteAllFiles(): void
{
$listFilesResponse = $this->pubnub->listFiles()->channel($this->channel)->sync();
foreach ($listFilesResponse->getData() as $file) {
@@ -130,7 +171,6 @@ public function testThrowErrorOnMalformedResponse(): void
->setResponseBody('{}')
->setResponseStatus(307)
->setResponseHeaders(['Location' => '']);
- $this->pubnub->setClient($client);
$pubnub->getFileDownloadUrl()->channel($this->channel)->fileId('none')->fileName('none')->sync();
}
@@ -139,4 +179,362 @@ public function testThrowErrorOnNoFileFound(): void
$this->expectException(PubNubServerException::class);
$this->pubnub->downloadFile()->channel($this->channel)->fileId('-')->fileName('-')->sync();
}
+
+ public function testFileUploadWithEncryption(): void
+ {
+ // Enable encryption in configuration
+ $pubnub = new PubNub($this->config_enc);
+ $file = fopen($this->textFilePath, "r");
+
+ $response = $pubnub->sendFile()
+ ->channel($this->channel)
+ ->fileHandle($file)
+ ->fileName(basename($this->textFilePath))
+ ->message("This is an encrypted file")
+ ->sync();
+
+ $this->assertNotEmpty($response);
+ $this->assertNotEmpty($response->getTimestamp());
+
+ // Verify file can be downloaded and decrypted
+ $downloadResponse = $pubnub->downloadFile()
+ ->channel($this->channel)
+ ->fileId($response->getFileId())
+ ->fileName(basename($this->textFilePath))
+ ->sync();
+
+ $this->assertEquals(
+ file_get_contents($this->textFilePath),
+ $downloadResponse->getFileContent()
+ );
+
+ $this->pubnub->deleteFile()
+ ->channel($this->channel)
+ ->fileId($response->getFileId())
+ ->fileName(basename($this->textFilePath))
+ ->sync();
+
+ fclose($file);
+ }
+
+ public function testFileUploadWithTTL(): void
+ {
+ $file = fopen($this->textFilePath, "r");
+
+ $response = $this->pubnub->sendFile()
+ ->channel($this->channel)
+ ->fileHandle($file)
+ ->fileName(basename($this->textFilePath))
+ ->message("This file will expire")
+ ->ttl(60) // 60 seconds TTL
+ ->sync();
+
+ $this->assertNotEmpty($response);
+ $this->assertNotEmpty($response->getTimestamp());
+
+ // Verify file exists immediately after upload
+ $listResponse = $this->pubnub->listFiles()
+ ->channel($this->channel)
+ ->sync();
+
+ $this->assertCount(1, $listResponse->getData());
+
+ fclose($file);
+ }
+
+ public function testFileUploadWithCustomMessageType(): void
+ {
+ $file = fopen($this->textFilePath, "r");
+
+ $response = $this->pubnub->sendFile()
+ ->channel($this->channel)
+ ->fileHandle($file)
+ ->fileName(basename($this->textFilePath))
+ ->message("This is a custom message type file")
+ ->customMessageType("file_upload")
+ ->sync();
+
+ $this->assertNotEmpty($response);
+ $this->assertNotEmpty($response->getTimestamp());
+
+ fclose($file);
+ }
+
+ public function testFileUploadWithMetadata(): void
+ {
+ $file = fopen($this->textFilePath, "r");
+
+ $metadata = [
+ "author" => "test_user",
+ "description" => "Test file with metadata",
+ "tags" => ["test", "metadata"]
+ ];
+
+ $response = $this->pubnub->sendFile()
+ ->channel($this->channel)
+ ->fileHandle($file)
+ ->fileName(basename($this->textFilePath))
+ ->message("This is a file with metadata")
+ ->meta($metadata)
+ ->sync();
+
+ $this->assertNotEmpty($response);
+ $this->assertNotEmpty($response->getTimestamp());
+
+ fclose($file);
+ }
+
+ public function testFileUploadWithEmptyFile(): void
+ {
+ // Create an empty file
+ $emptyFilePath = __DIR__ . '/assets/empty.txt';
+ file_put_contents($emptyFilePath, '');
+
+ try {
+ $file = fopen($emptyFilePath, "r");
+
+ $response = $this->pubnub->sendFile()
+ ->channel($this->channel)
+ ->fileHandle($file)
+ ->fileName('empty.txt')
+ ->message("This is an empty file")
+ ->sync();
+
+ $this->assertNotEmpty($response);
+ $this->assertNotEmpty($response->getFileId());
+ sleep(1);
+
+ // Verify file can be downloaded
+ $downloadResponse = $this->pubnub->downloadFile()
+ ->channel($this->channel)
+ ->fileId($response->getFileId())
+ ->fileName('empty.txt')
+ ->sync();
+
+ $this->assertEquals('', $downloadResponse->getFileContent());
+
+ fclose($file);
+ } finally {
+ // Clean up the temporary file
+ if (file_exists($emptyFilePath)) {
+ unlink($emptyFilePath);
+ }
+ }
+ }
+
+ public function testFileUploadWithLargeFile(): void
+ {
+ // Create a large file (5MB)
+ $largeFilePath = __DIR__ . '/assets/large.txt';
+ $largeContent = str_repeat('x', 5 * 1024 * 1024); // 5MB of data
+ file_put_contents($largeFilePath, $largeContent);
+
+ try {
+ $file = fopen($largeFilePath, "r");
+
+ $response = $this->pubnub->sendFile()
+ ->channel($this->channel)
+ ->fileHandle($file)
+ ->fileName('large.txt')
+ ->message("This is a large file")
+ ->sync();
+
+ $this->assertNotEmpty($response);
+ $this->assertNotEmpty($response->getFileId());
+
+ // Verify file can be downloaded
+ $downloadResponse = $this->pubnub->downloadFile()
+ ->channel($this->channel)
+ ->fileId($response->getFileId())
+ ->fileName('large.txt')
+ ->sync();
+
+ $this->assertEquals($largeContent, $downloadResponse->getFileContent());
+
+ fclose($file);
+ } finally {
+ // Clean up the temporary file
+ if (file_exists($largeFilePath)) {
+ unlink($largeFilePath);
+ }
+ }
+ }
+
+ public function testFileUploadWithInvalidParameters(): void
+ {
+ $file = fopen($this->textFilePath, "r");
+
+ // Test with invalid channel
+ $this->expectException(\PubNub\Exceptions\PubNubValidationException::class);
+ $this->pubnub->sendFile()
+ ->channel("") // Empty channel
+ ->fileHandle($file)
+ ->fileName(basename($this->textFilePath))
+ ->message("This should fail")
+ ->sync();
+
+ fclose($file);
+
+ // Test with invalid file handle
+ $this->expectException(\Exception::class);
+ $this->pubnub->sendFile()
+ ->channel($this->channel)
+ ->fileHandle(null) // Null file handle
+ ->fileName(basename($this->textFilePath))
+ ->message("This should fail")
+ ->sync();
+
+ // Test with invalid file name
+ $this->expectException(\PubNub\Exceptions\PubNubValidationException::class);
+ $this->pubnub->sendFile()
+ ->channel($this->channel)
+ ->fileHandle($file)
+ ->fileName("") // Empty file name
+ ->message("This should fail")
+ ->sync();
+ }
+
+ public function testFileDownloadWithEncryption(): void
+ {
+ // Create a test file with specific content
+ $filePath = __DIR__ . '/assets/encrypted.txt';
+ $fileContent = "This is encrypted content";
+ file_put_contents($filePath, $fileContent);
+
+ try {
+ // Upload file with encryption
+ $pubnub = new PubNub($this->config_enc);
+ $file = fopen($filePath, "r");
+
+ $response = $pubnub->sendFile()
+ ->channel($this->channel)
+ ->fileHandle($file)
+ ->fileName(basename($filePath))
+ ->message("This is an encrypted file")
+ ->sync();
+
+ $this->assertNotEmpty($response);
+ $this->assertNotEmpty($response->getFileId());
+
+ // Download and verify the encrypted file
+ $downloadResponse = $pubnub->downloadFile()
+ ->channel($this->channel)
+ ->fileId($response->getFileId())
+ ->fileName(basename($filePath))
+ ->sync();
+
+ $this->assertEquals($fileContent, $downloadResponse->getFileContent());
+
+ // Try downloading with non-encrypted client (should fail)
+
+ $downloadResponse = $this->pubnub->downloadFile()
+ ->channel($this->channel)
+ ->fileId($response->getFileId())
+ ->fileName(basename($filePath))
+ ->sync();
+
+ // We shoul be able to download the file with the non-encrypted client, but the content should be encrypted
+ $this->assertNotEquals($fileContent, $downloadResponse->getFileContent());
+
+ // manually decrypt the content
+ $decryptedContent = $this->config_enc->getCrypto()->decrypt($downloadResponse->getFileContent());
+ $this->assertEquals($fileContent, $decryptedContent);
+
+ fclose($file);
+ } finally {
+ if (file_exists($filePath)) {
+ unlink($filePath);
+ }
+ }
+ }
+
+ public function testFileDownloadWithInvalidFileId(): void
+ {
+ $this->expectException(PubNubServerException::class);
+
+ $this->pubnub->downloadFile()
+ ->channel($this->channel)
+ ->fileId('invalid-file-id')
+ ->fileName('test.txt')
+ ->sync();
+ }
+
+ public function testFileListingWithPagination(): void
+ {
+ // Upload multiple files
+ $files = [];
+ for ($i = 0; $i < 5; $i++) {
+ $filePath = __DIR__ . "/assets/test{$i}.txt";
+ file_put_contents($filePath, "content{$i}");
+ $files[] = $filePath;
+
+ $file = fopen($filePath, "r");
+ $this->pubnub->sendFile()
+ ->channel($this->channel)
+ ->fileHandle($file)
+ ->fileName(basename($filePath))
+ ->message("Test file {$i}")
+ ->sync();
+ fclose($file);
+ }
+
+ try {
+ // List files
+ $response = $this->pubnub->listFiles()
+ ->channel($this->channel)
+ ->sync();
+
+ $this->assertNotEmpty($response->getData());
+ $this->assertGreaterThanOrEqual(5, count($response->getData()));
+
+ // Verify we can get file details
+ foreach ($response->getData() as $file) {
+ $this->assertNotEmpty($file['id']);
+ $this->assertNotEmpty($file['name']);
+ $this->assertNotEmpty($file['created']);
+ }
+ } finally {
+ // Clean up test files
+ foreach ($files as $filePath) {
+ if (file_exists($filePath)) {
+ unlink($filePath);
+ }
+ }
+ }
+ }
+
+ public function testNetworkFailureScenario(): void
+ {
+ // Create a mock client that simulates network failure
+ $client = new class implements ClientInterface {
+ public function sendRequest(RequestInterface $request): ResponseInterface
+ {
+ throw new ConnectException(
+ "Network failure",
+ $request,
+ null,
+ ['errno' => CURLE_COULDNT_CONNECT]
+ );
+ }
+ };
+
+ $this->pubnub->setClient($client);
+
+ $file = fopen($this->textFilePath, "r");
+
+ try {
+ $this->expectException(PubNubConnectionException::class);
+
+ $this->pubnub->sendFile()
+ ->channel($this->channel)
+ ->fileHandle($file)
+ ->fileName(basename($this->textFilePath))
+ ->message("This should fail due to network")
+ ->sync();
+ } finally {
+ fclose($file);
+ // Restore original client
+ $this->pubnub->setClient(new \GuzzleHttp\Client());
+ }
+ }
}
diff --git a/tests/integrational/PublishTest.php b/tests/integrational/PublishTest.php
index 33abb1ca..61fba0df 100644
--- a/tests/integrational/PublishTest.php
+++ b/tests/integrational/PublishTest.php
@@ -147,7 +147,8 @@ public function testPublishDoNotStore()
public function testServerSideErrorSync()
{
$this->expectException(PubNubServerException::class);
- $this->expectExceptionMessage("Server responded with an error and the status code is 400");
+ $this->expectExceptionMessageMatches("/Server responded with an error/");
+ $this->expectExceptionMessageMatches("/and the status code is 400/");
$pnconf = PNConfiguration::demoKeys();
$pnconf->setPublishKey("fake");
@@ -174,7 +175,8 @@ public function testServerSideErrorEnvelope()
$exception = $envelope->getStatus()->getException();
$this->assertEquals(400, $exception->getStatusCode());
- $this->assertEquals("Server responded with an error and the status code is 400", $exception->getMessage());
+ $this->assertStringContainsString("Server responded with an error", $exception->getMessage());
+ $this->assertStringContainsString("and the status code is 400", $exception->getMessage());
$body = $exception->getBody();
$this->assertEquals(0, $body[0]);
diff --git a/tests/integrational/objects/uuid/GetUUIDMetadataEndpointTest.php b/tests/integrational/objects/uuid/GetUUIDMetadataEndpointTest.php
index 159c0a25..e90e96eb 100644
--- a/tests/integrational/objects/uuid/GetUUIDMetadataEndpointTest.php
+++ b/tests/integrational/objects/uuid/GetUUIDMetadataEndpointTest.php
@@ -4,7 +4,6 @@
use PubNubTestCase;
-
class GetUUIDMetadataEndpointTest extends PubNubTestCase
{
public function testGetMetadataFromUUID()
diff --git a/tests/unit/CryptoModule/CryptoModuleTest.php b/tests/unit/CryptoModule/CryptoModuleTest.php
index dc0a1943..102a8e64 100644
--- a/tests/unit/CryptoModule/CryptoModuleTest.php
+++ b/tests/unit/CryptoModule/CryptoModuleTest.php
@@ -5,11 +5,24 @@
use Generator;
use PHPUnit\Framework\TestCase;
use PubNub\CryptoModule;
+use PubNub\Crypto\AesCbcCryptor;
+use PubNub\Crypto\LegacyCryptor;
+use PubNub\Crypto\Cryptor;
+use PubNub\Crypto\Payload as CryptoPayload;
use PubNub\Exceptions\PubNubResponseParsingException;
+use PubNub\Exceptions\PubNubCryptoException;
+use TypeError;
class CryptoModuleTest extends TestCase
{
protected string $cipherKey = "myCipherKey";
+ protected string $testKey256 = "01234567890123456789012345678901"; // 32 bytes
+ protected string $testKeyShort = "shortkey";
+ protected string $testKeyLong = "verylongcipherkeyfortestingpurposesthatexceeds256bits";
+
+ // ============================================================================
+ // EXISTING TESTS (keeping current functionality)
+ // ============================================================================
/**
* @dataProvider decodeProvider
@@ -47,6 +60,663 @@ public function testEnode(CryptoModule $module, string $message, mixed $expected
$this->assertEquals($expected, $encrypted);
}
+ // ============================================================================
+ // CRYPTOR REGISTRATION AND MANAGEMENT TESTS
+ // ============================================================================
+
+ /**
+ * Test successful cryptor registration
+ */
+ public function testRegisterCryptorSuccess(): void
+ {
+ // Create a crypto module with empty cryptor map
+ $module = new CryptoModule([], "TEST");
+
+ // Create a new cryptor instance
+ $cryptor = new AesCbcCryptor($this->cipherKey);
+
+ // Register the cryptor - should succeed and return the module instance
+ $result = $module->registerCryptor($cryptor);
+
+ // Verify the method returns the module instance (fluent interface)
+ $this->assertSame($module, $result);
+
+ // Verify the cryptor was registered by trying to encrypt/decrypt
+ $testMessage = "test message";
+ $encrypted = $module->encrypt($testMessage, AesCbcCryptor::CRYPTOR_ID);
+ $this->assertNotEmpty($encrypted);
+
+ $decrypted = $module->decrypt($encrypted);
+ $this->assertEquals($testMessage, $decrypted);
+ }
+
+ /**
+ * Test cryptor registration with custom ID
+ */
+ public function testRegisterCryptorWithCustomId(): void
+ {
+ $customId = "CUST";
+ // Create a crypto module with empty cryptor map
+ $module = new CryptoModule([], $customId);
+
+ // Create a new cryptor instance
+ $cryptor = new class ($this->cipherKey) extends AesCbcCryptor {
+ public const CRYPTOR_ID = "CUST";
+ };
+
+ // Register the cryptor with a custom ID
+ $result = $module->registerCryptor($cryptor, $customId);
+
+ // Verify the method returns the module instance
+ $this->assertSame($module, $result);
+
+ // Verify the cryptor was registered with the custom ID
+ $testMessage = "test message with custom ID";
+ $encrypted = $module->encrypt($testMessage, $customId);
+ $this->assertNotEmpty($encrypted);
+ $decrypted = $module->decrypt($encrypted);
+ $this->assertEquals($testMessage, $decrypted);
+ }
+
+ /**
+ * Test cryptor registration with invalid ID length
+ */
+ public function testRegisterCryptorInvalidIdLength(): void
+ {
+ $module = new CryptoModule([], "TEST");
+ $cryptor = new AesCbcCryptor($this->cipherKey);
+
+ // Test with ID too short
+ $this->expectException(PubNubCryptoException::class);
+ $this->expectExceptionMessage('Malformed cryptor id');
+ $module->registerCryptor($cryptor, "ABC"); // 3 characters instead of 4
+ }
+
+ /**
+ * Test cryptor registration with invalid ID length - too long
+ */
+ public function testRegisterCryptorInvalidIdLengthTooLong(): void
+ {
+ $module = new CryptoModule([], "TEST");
+ $cryptor = new AesCbcCryptor($this->cipherKey);
+
+ // Test with ID too long
+ $this->expectException(PubNubCryptoException::class);
+ $this->expectExceptionMessage('Malformed cryptor id');
+ $module->registerCryptor($cryptor, "ABCDE"); // 5 characters instead of 4
+ }
+
+ /**
+ * Test cryptor registration with empty ID
+ */
+ public function testRegisterCryptorEmptyId(): void
+ {
+ $module = new CryptoModule([], "TEST");
+ $cryptor = new AesCbcCryptor($this->cipherKey);
+
+ // Test with empty ID
+ $this->expectException(PubNubCryptoException::class);
+ $this->expectExceptionMessage('Malformed cryptor id');
+ $module->registerCryptor($cryptor, ""); // Empty string
+ }
+
+ /**
+ * Test cryptor registration with duplicate ID
+ */
+ public function testRegisterCryptorDuplicateId(): void
+ {
+ // Create a crypto module with an existing cryptor
+ $existingCryptor = new LegacyCryptor($this->cipherKey, false);
+ $module = new CryptoModule([
+ 'TEST' => $existingCryptor
+ ], "TEST");
+
+ // Try to register another cryptor with the same ID
+ $newCryptor = new AesCbcCryptor($this->cipherKey);
+
+ $this->expectException(PubNubCryptoException::class);
+ $this->expectExceptionMessage('Cryptor id already in use');
+ $module->registerCryptor($newCryptor, "TEST");
+ }
+
+ /**
+ * Test cryptor registration with duplicate ID using default cryptor ID
+ */
+ public function testRegisterCryptorDuplicateDefaultId(): void
+ {
+ // Create a crypto module with existing AES cryptor
+ $existingCryptor = new AesCbcCryptor($this->cipherKey);
+ $module = new CryptoModule([
+ AesCbcCryptor::CRYPTOR_ID => $existingCryptor
+ ], AesCbcCryptor::CRYPTOR_ID);
+
+ // Try to register another AES cryptor (should use default CRYPTOR_ID)
+ $newCryptor = new AesCbcCryptor("different_key");
+
+ $this->expectException(PubNubCryptoException::class);
+ $this->expectExceptionMessage('Cryptor id already in use');
+ $module->registerCryptor($newCryptor); // Will use AesCbcCryptor::CRYPTOR_ID
+ }
+
+ /**
+ * Test cryptor registration with invalid cryptor instance
+ */
+ public function testRegisterCryptorInvalidInstance(): void
+ {
+ $module = new CryptoModule([], "TEST");
+
+ // Create an anonymous class that doesn't extend Cryptor
+ $invalidCryptor = new class
+ {
+ public function encrypt(mixed $data): string
+ {
+ return "fake";
+ }
+
+ public function decrypt(mixed $data): string
+ {
+ return "fake";
+ }
+ };
+
+ $this->expectException(TypeError::class);
+
+ // This will fail the instanceof check in registerCryptor
+ $reflection = new \ReflectionClass($module);
+ $method = $reflection->getMethod('registerCryptor');
+ $method->setAccessible(true);
+
+ // Call with invalid cryptor - this should trigger the instanceof check
+ $method->invoke($module, $invalidCryptor, "TEST");
+ }
+
+ /**
+ * Test cryptor map initialization
+ */
+ public function testCryptorMapInitialization(): void
+ {
+ // Test with empty cryptor map
+ $emptyModule = new CryptoModule([], "TEST");
+ $this->assertInstanceOf(CryptoModule::class, $emptyModule);
+
+ // Test with pre-populated cryptor map
+ $legacyCryptor = new LegacyCryptor($this->cipherKey, false);
+ $aesCryptor = new AesCbcCryptor($this->cipherKey);
+
+ $cryptorMap = [
+ LegacyCryptor::CRYPTOR_ID => $legacyCryptor,
+ AesCbcCryptor::CRYPTOR_ID => $aesCryptor
+ ];
+
+ $populatedModule = new CryptoModule($cryptorMap, LegacyCryptor::CRYPTOR_ID);
+ $this->assertInstanceOf(CryptoModule::class, $populatedModule);
+
+ // Verify both cryptors are accessible
+ $testMessage = "initialization test";
+
+ // Test legacy cryptor
+ $legacyEncrypted = $populatedModule->encrypt($testMessage, LegacyCryptor::CRYPTOR_ID);
+ $legacyDecrypted = $populatedModule->decrypt($legacyEncrypted);
+ $this->assertEquals($testMessage, $legacyDecrypted);
+
+ // Test AES cryptor
+ $aesEncrypted = $populatedModule->encrypt($testMessage, AesCbcCryptor::CRYPTOR_ID);
+ $aesDecrypted = $populatedModule->decrypt($aesEncrypted);
+ $this->assertEquals($testMessage, $aesDecrypted);
+ }
+
+ /**
+ * Test default cryptor ID validation
+ */
+ public function testDefaultCryptorIdValidation(): void
+ {
+ // Test with valid default cryptor ID
+ $legacyCryptor = new LegacyCryptor($this->cipherKey, false);
+ $cryptorMap = [LegacyCryptor::CRYPTOR_ID => $legacyCryptor];
+
+ $module = new CryptoModule($cryptorMap, LegacyCryptor::CRYPTOR_ID);
+ $this->assertInstanceOf(CryptoModule::class, $module);
+
+ // Test encryption with default cryptor (no explicit cryptor ID)
+ $testMessage = "default cryptor test";
+ $encrypted = $module->encrypt($testMessage); // Uses default cryptor
+ $this->assertNotEmpty($encrypted);
+
+ $decrypted = $module->decrypt($encrypted);
+ $this->assertEquals($testMessage, $decrypted);
+ }
+
+ /**
+ * Test factory methods create proper cryptor maps
+ */
+ public function testFactoryMethodsCryptorMapSetup(): void
+ {
+ // Test legacy cryptor factory
+ $legacyModule = CryptoModule::legacyCryptor($this->cipherKey, false);
+ $this->assertInstanceOf(CryptoModule::class, $legacyModule);
+
+ // Test that it can encrypt/decrypt with legacy cryptor as default
+ $testMessage = "factory legacy test";
+ $encrypted = $legacyModule->encrypt($testMessage);
+ $decrypted = $legacyModule->decrypt($encrypted);
+ $this->assertEquals($testMessage, $decrypted);
+
+ // Test AES CBC cryptor factory
+ $aesModule = CryptoModule::aesCbcCryptor($this->cipherKey, true);
+ $this->assertInstanceOf(CryptoModule::class, $aesModule);
+
+ // Test that it can encrypt/decrypt with AES cryptor as default
+ $testMessage2 = "factory aes test";
+ $encrypted2 = $aesModule->encrypt($testMessage2);
+ $decrypted2 = $aesModule->decrypt($encrypted2);
+ $this->assertEquals($testMessage2, $decrypted2);
+ }
+
+ // ============================================================================
+ // INPUT VALIDATION AND SANITIZATION TESTS
+ // ============================================================================
+
+ /**
+ * Test encryption with null input
+ */
+ public function testEncryptNullInput(): void
+ {
+ $module = CryptoModule::aesCbcCryptor($this->cipherKey, false);
+
+ // PHP will convert null to empty string, which should trigger the empty message exception
+ $this->expectException(PubNubResponseParsingException::class);
+ $this->expectExceptionMessage('Encryption error: message is empty');
+
+ $module->encrypt(null);
+ }
+
+ /**
+ * Test encryption with empty string
+ */
+ public function testEncryptEmptyString(): void
+ {
+ $module = CryptoModule::aesCbcCryptor($this->cipherKey, false);
+
+ $this->expectException(PubNubResponseParsingException::class);
+ $this->expectExceptionMessage('Encryption error: message is empty');
+
+ $module->encrypt('');
+ }
+
+ /**
+ * Test encryption with whitespace-only string
+ */
+ public function testEncryptWhitespaceOnlyString(): void
+ {
+ $module = CryptoModule::aesCbcCryptor($this->cipherKey, false);
+
+ // Whitespace-only strings should be valid for encryption
+ $whitespaceInputs = [
+ ' ', // single space
+ ' ', // multiple spaces
+ "\t", // tab
+ "\n", // newline
+ "\r\n", // carriage return + newline
+ " \t\n\r ", // mixed whitespace
+ ];
+
+ foreach ($whitespaceInputs as $input) {
+ $encrypted = $module->encrypt($input);
+ $this->assertNotEmpty($encrypted);
+
+ $decrypted = $module->decrypt($encrypted);
+ $this->assertEquals($input, $decrypted);
+ }
+ }
+
+ /**
+ * Test encryption with very large input
+ */
+ public function testEncryptLargeInput(): void
+ {
+ $module = CryptoModule::aesCbcCryptor($this->cipherKey, false);
+
+ // Create a large string (1MB)
+ $largeInput = str_repeat('A', 1024 * 1024);
+
+ $startTime = microtime(true);
+ $encrypted = $module->encrypt($largeInput);
+ $encryptTime = microtime(true) - $startTime;
+
+ $this->assertNotEmpty($encrypted);
+
+ $startTime = microtime(true);
+ $decrypted = $module->decrypt($encrypted);
+ $decryptTime = microtime(true) - $startTime;
+
+ $this->assertEquals($largeInput, $decrypted);
+
+ // Performance assertions (should complete within reasonable time)
+ $this->assertLessThan(5.0, $encryptTime, 'Encryption should complete within 5 seconds');
+ $this->assertLessThan(5.0, $decryptTime, 'Decryption should complete within 5 seconds');
+ }
+
+ /**
+ * Test encryption with special characters
+ */
+ public function testEncryptSpecialCharacters(): void
+ {
+ $module = CryptoModule::aesCbcCryptor($this->cipherKey, false);
+
+ $specialInputs = [
+ 'Hello δΈη', // Unicode Chinese characters
+ 'π Rocket emoji test π', // Emojis
+ 'ΓoΓ±o cafΓ© rΓ©sumΓ© naΓ―ve', // Accented characters
+ 'Special: !@#$%^&*()_+-={}[]|\\:";\'<>?,./', // Special ASCII characters
+ 'Math: βββββ β€β₯Β±ΓΓ·', // Mathematical symbols
+ 'Currency: $β¬Β£Β₯βΉβ½β©', // Currency symbols
+ 'Quotes: "Hello" \'World\' `Test`', // Various quotes
+ "Line\nBreaks\r\nAnd\tTabs", // Control characters
+ 'Null\x00Byte\x01Test', // Control bytes
+ ];
+
+ foreach ($specialInputs as $input) {
+ $encrypted = $module->encrypt($input);
+ $this->assertNotEmpty($encrypted, "Failed to encrypt: " . bin2hex($input));
+
+ $decrypted = $module->decrypt($encrypted);
+ $this->assertEquals($input, $decrypted, "Decrypt mismatch for: " . bin2hex($input));
+ }
+ }
+
+ /**
+ * Test encryption with binary data
+ */
+ public function testEncryptBinaryData(): void
+ {
+ $module = CryptoModule::aesCbcCryptor($this->cipherKey, false);
+
+ // Test various binary data patterns
+ $binaryInputs = [
+ "\x00\x01\x02\x03\x04\x05", // Sequential bytes
+ random_bytes(32), // Random binary data
+ "\xFF\xFE\xFD\xFC", // High-value bytes
+ str_repeat("\x00", 100), // Null bytes
+ "\x00\xFF\x00\xFF\x00\xFF", // Alternating pattern
+ ];
+
+ foreach ($binaryInputs as $input) {
+ $encrypted = $module->encrypt($input);
+ $this->assertNotEmpty($encrypted, "Failed to encrypt binary data: " . bin2hex($input));
+
+ $decrypted = $module->decrypt($encrypted);
+ $this->assertEquals($input, $decrypted, "Binary data mismatch for: " . bin2hex($input));
+ }
+ }
+
+ /**
+ * Test decryption with malformed input
+ */
+ public function testDecryptMalformedInput(): void
+ {
+ $module = CryptoModule::aesCbcCryptor($this->cipherKey, false);
+
+ $malformedInputs = [
+ 'not-base64-at-all!@#$',
+ 'VGhpcyBpcyBub3QgdmFsaWQgZW5jcnlwdGVkIGRhdGE=', // Valid base64 but invalid encrypted data
+ 'SGVsbG8gV29ybGQ', // Valid base64 but too short for encrypted data
+ '===invalid===', // Invalid base64 padding
+ 'YWJjZGVmZ2hpams', // Valid base64 but not encrypted format
+ ];
+
+ foreach ($malformedInputs as $input) {
+ try {
+ $result = $module->decrypt($input);
+ // If no exception is thrown, the result should still be reasonable
+ $this->assertTrue(is_string($result) or is_object($result) or is_array($result));
+ } catch (PubNubResponseParsingException $e) {
+ // Expected for malformed input
+ $this->assertStringContainsString('error', strtolower($e->getMessage()));
+ } catch (PubNubCryptoException $e) {
+ // Also acceptable for crypto-related errors
+ $this->assertStringContainsString('error', strtolower($e->getMessage()));
+ }
+ }
+ }
+
+ /**
+ * Test decryption with empty input
+ */
+ public function testDecryptEmptyInput(): void
+ {
+ $module = CryptoModule::aesCbcCryptor($this->cipherKey, false);
+
+ $this->expectException(PubNubResponseParsingException::class);
+ $this->expectExceptionMessage('Decryption error: message is empty');
+
+ $module->decrypt('');
+ }
+
+ /**
+ * Test parseInput method with various input types
+ * @dataProvider parseInputProvider
+ */
+ public function testParseInput(mixed $input, ?string $expected): void
+ {
+ $module = CryptoModule::aesCbcCryptor($this->cipherKey, false);
+
+ // Use reflection to access the protected parseInput method
+ $reflection = new \ReflectionClass($module);
+ $method = $reflection->getMethod('parseInput');
+ $method->setAccessible(true);
+
+ if ($expected === null) {
+ // Expecting an exception
+ $this->expectException(TypeError::class);
+ $method->invoke($module, $input);
+ } else {
+ $result = $method->invoke($module, $input);
+ $this->assertEquals($expected, $result);
+ }
+ }
+
+ /**
+ * Test parseInput with invalid data types
+ */
+ public function testParseInputInvalidTypes(): void
+ {
+ $module = CryptoModule::aesCbcCryptor($this->cipherKey, false);
+
+ // Use reflection to access the protected parseInput method
+ $reflection = new \ReflectionClass($module);
+ $method = $reflection->getMethod('parseInput');
+ $method->setAccessible(true);
+
+
+ $result = $method->invoke($module, 12345);
+ $this->assertEquals('12345', $result);
+ }
+
+ // ============================================================================
+ // DATA TYPE HANDLING TESTS
+ // ============================================================================
+
+ /**
+ * Test stringify method with different data types
+ * @dataProvider stringifyDataProvider
+ */
+ public function testStringify(mixed $input, ?string $expected): void
+ {
+ $module = CryptoModule::aesCbcCryptor($this->cipherKey, false);
+
+ // Use reflection to access the protected stringify method
+ $reflection = new \ReflectionClass($module);
+ $method = $reflection->getMethod('stringify');
+ $method->setAccessible(true);
+
+ $result = $method->invoke($module, $input);
+ $this->assertEquals($expected, $result);
+ }
+
+ /**
+ * Test encryption/decryption of JSON objects
+ */
+ public function testEncryptDecryptJsonObjects(): void
+ {
+ $module = CryptoModule::aesCbcCryptor($this->cipherKey, false);
+
+ $jsonObjects = [
+ // Simple JSON object
+ '{"name":"John","age":30}',
+
+ // JSON object with nested structure
+ '{"user":{"name":"Jane","profile":{"email":"jane@test.com","settings":{"theme":"dark"}}}}',
+
+ // JSON object with arrays
+ '{"items":["apple","banana","cherry"],"count":3}',
+
+ // JSON object with mixed data types
+ '{"string":"hello","number":42,"boolean":true,"null":null,"array":[1,2,3]}',
+
+ // Empty JSON object
+ '{}',
+ ];
+
+ foreach ($jsonObjects as $jsonString) {
+ $encrypted = $module->encrypt($jsonString);
+ $this->assertNotEmpty($encrypted, "Failed to encrypt JSON: " . $jsonString);
+
+ $decrypted = json_encode($module->decrypt($encrypted));
+ $this->assertEquals($jsonString, $decrypted, "JSON round-trip failed for: " . $jsonString);
+
+ // Verify the decrypted string is valid JSON
+ $decoded = json_decode($decrypted, true);
+ $this->assertNotNull($decoded, "Decrypted JSON is not valid: " . $decrypted);
+ }
+ }
+
+ /**
+ * Test encryption/decryption of arrays
+ */
+ public function testEncryptDecryptArrays(): void
+ {
+ $module = CryptoModule::aesCbcCryptor($this->cipherKey, false);
+
+ $testArrays = [
+ // Simple indexed array
+ ["apple", "banana", "cherry"],
+
+ ];
+
+ foreach ($testArrays as $array) {
+ // Convert array to JSON for encryption (this is typically how arrays are handled)
+ $jsonString = json_encode($array);
+
+ $encrypted = $module->encrypt($jsonString);
+ $this->assertNotEmpty($encrypted, "Failed to encrypt array: " . json_encode($array));
+
+ $decrypted = json_encode($module->decrypt($encrypted));
+ $this->assertEquals($jsonString, $decrypted, "Array round-trip failed for: " . json_encode($array));
+
+ // Verify the decrypted JSON can be converted back to the original array
+ $decodedArray = json_decode($decrypted, true);
+ $this->assertEquals($array, $decodedArray, "Array decode mismatch for: " . json_encode($array));
+ }
+ }
+
+ /**
+ * Test encryption/decryption of nested data structures
+ */
+ public function testEncryptDecryptNestedStructures(): void
+ {
+ $module = CryptoModule::aesCbcCryptor($this->cipherKey, false);
+
+ $nestedStructures = [
+ // Deeply nested object
+ [
+ "user" => [
+ "profile" => [
+ "personal" => [
+ "name" => "John Doe",
+ "contacts" => [
+ "email" => "john@example.com",
+ "phones" => ["123-456-7890", "098-765-4321"]
+ ]
+ ],
+ "preferences" => [
+ "notifications" => true,
+ "themes" => ["dark", "light"],
+ "settings" => [
+ "language" => "en",
+ "timezone" => "UTC"
+ ]
+ ]
+ ]
+ ]
+ ],
+
+ // Array of objects
+ [
+ "users" => [
+ ["id" => 1, "name" => "Alice", "roles" => ["admin", "user"]],
+ ["id" => 2, "name" => "Bob", "roles" => ["user"]],
+ ["id" => 3, "name" => "Charlie", "roles" => ["moderator", "user"]]
+ ]
+ ],
+
+ // Mixed nested structure with various data types
+ [
+ "config" => [
+ "database" => [
+ "connections" => [
+ "primary" => ["host" => "localhost", "port" => 5432, "ssl" => true],
+ "secondary" => ["host" => "backup.db", "port" => 5432, "ssl" => false]
+ ]
+ ],
+ "features" => [
+ "logging" => ["enabled" => true, "level" => "info", "handlers" => ["file", "console"]],
+ "caching" => ["ttl" => 3600, "driver" => "redis", "prefix" => "app_cache"]
+ ]
+ ]
+ ],
+
+ // Complex array structure
+ [
+ "matrix" => [
+ [1, 2, 3],
+ [4, 5, 6],
+ [7, 8, 9]
+ ],
+ "metadata" => [
+ "dimensions" => ["rows" => 3, "cols" => 3],
+ "statistics" => ["sum" => 45, "avg" => 5.0]
+ ]
+ ]
+ ];
+
+ foreach ($nestedStructures as $structure) {
+ // Convert structure to JSON for encryption
+ $jsonString = json_encode($structure);
+
+ $encrypted = $module->encrypt($jsonString);
+ $this->assertNotEmpty($encrypted, "Failed to encrypt nested structure");
+
+ $decrypted = json_encode($module->decrypt($encrypted));
+ $this->assertEquals($jsonString, $decrypted, "Nested structure round-trip failed");
+
+ // Verify the decrypted JSON can be converted back to the original structure
+ $decodedStructure = json_decode($decrypted, true);
+ $this->assertEquals($structure, $decodedStructure, "Nested structure decode mismatch");
+
+ // Verify specific nested values are accessible
+ if (isset($structure["user"]["profile"]["personal"]["name"])) {
+ $this->assertEquals("John Doe", $decodedStructure["user"]["profile"]["personal"]["name"]);
+ }
+ if (isset($structure["config"]["database"]["connections"]["primary"]["port"])) {
+ $this->assertEquals(5432, $decodedStructure["config"]["database"]["connections"]["primary"]["port"]);
+ }
+ }
+ }
+
+ // ============================================================================
+ // DATA PROVIDERS
+ // ============================================================================
+
protected function encodeProvider(): Generator
{
$legacyRandomModule = CryptoModule::legacyCryptor($this->cipherKey, true);
@@ -129,4 +799,86 @@ protected function decodeProvider(): Generator
'Hello world encrypted with aesCbcModule',
];
}
+
+ /**
+ * Data provider for parseInput tests
+ */
+ protected function parseInputProvider(): Generator
+ {
+ // Valid string inputs
+ yield ["simple string", "simple string"];
+ yield ["encrypted_base64_data", "encrypted_base64_data"];
+ yield [" trimmed ", " trimmed "]; // Should NOT be trimmed by parseInput
+
+ // Invalid inputs that should throw exceptions
+ yield [["other" => "data"], null]; // Missing pn_other key
+ yield [["pn_other" => ""], null]; // Empty pn_other value
+ yield [["pn_other" => " "], null]; // Whitespace-only pn_other value
+ yield [123, '123']; // Non-string, non-array input
+ yield [true, '1']; // Boolean input
+ yield [null, null]; // Null input (converted to empty string)
+ }
+
+ /**
+ * Data provider for stringify tests
+ */
+ protected function stringifyDataProvider(): Generator
+ {
+ // String values
+ yield ["simple string", "simple string"];
+ yield ["", ""];
+ yield [" whitespace ", " whitespace "];
+ yield ["Special chars: !@#$%^&*()", "Special chars: !@#$%^&*()"];
+ yield ["Unicode: π δΈη", "Unicode: π δΈη"];
+
+ // Numeric values
+ yield [123, "123"];
+ yield [0, "0"];
+ yield [-456, "-456"];
+ yield [3.14159, "3.14159"];
+ yield [1.23e10, "12300000000"];
+
+ // Boolean values
+ yield [true, "true"];
+ yield [false, "false"];
+
+ // Null value
+ yield [null, "null"];
+
+ // Array values (should be JSON encoded)
+ yield [["key" => "value"], '{"key":"value"}'];
+ yield [[1, 2, 3], '[1,2,3]'];
+ yield [[], '[]'];
+ yield [["nested" => ["data" => true]], '{"nested":{"data":true}}'];
+
+ // Mixed array
+ yield [
+ ["string" => "hello", "number" => 42, "bool" => true, "null" => null],
+ '{"string":"hello","number":42,"bool":true,"null":null}'
+ ];
+
+ // Complex nested structure
+ yield [
+ [
+ "user" => [
+ "name" => "John",
+ "settings" => ["theme" => "dark", "notifications" => true]
+ ],
+ "data" => [1, 2, 3]
+ ],
+ '{"user":{"name":"John","settings":{"theme":"dark","notifications":true}},"data":[1,2,3]}'
+ ];
+ }
+
+ /**
+ * Data provider for cipher key length tests
+ */
+ protected function cipherKeyLengthProvider(): Generator
+ {
+ // TODO: Implement data provider for different key lengths
+ yield [$this->testKeyShort];
+ yield [$this->testKey256];
+ yield [$this->testKeyLong];
+ yield [""];
+ }
}