Skip to content

Commit

Permalink
Subscription send only to others (#2298)
Browse files Browse the repository at this point in the history
Co-authored-by: Benedikt Franke <[email protected]>
  • Loading branch information
Stevemoretz and spawnia authored Feb 14, 2023
1 parent 1658994 commit 722ff4b
Show file tree
Hide file tree
Showing 4 changed files with 69 additions and 3 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ You can find and compare releases at the [GitHub release page](https://github.co
- Require implementations of `BatchedEntityResolver` to maintain the keys given in `array $representations` https://github.com/nuwave/lighthouse/pull/2286
- Use the strongest possible native types over PHPDocs
- Require filter directives such as `@whereKey` in `@delete`, `@forceDelete` and `@restore` https://github.com/nuwave/lighthouse/pull/2289
- Subscriptions can now be filtered via `$subscriber->socket_id` and `request()->header('X-Socket-ID')` https://github.com/nuwave/lighthouse/pull/2298

### Fixed

Expand Down
35 changes: 35 additions & 0 deletions docs/master/subscriptions/filtering-subscriptions.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,38 @@ class PostUpdatedSubscription extends GraphQLSubscription
}
}
```

## Only To Others

When building an application that utilizes event broadcasting, you may occasionally need to broadcast an event to all subscribers of a channel except for the current user.
You may accomplish this using the filter function, this following snippet is equivalent to [the `toOthers()` method from Laravel's broadcast helper](https://laravel.com/docs/9.x/broadcasting#only-to-others).

```php
namespace App\GraphQL\Subscriptions;

use Nuwave\Lighthouse\Subscriptions\Subscriber;
use Nuwave\Lighthouse\Schema\Types\GraphQLSubscription;

final class PostUpdatedSubscription extends GraphQLSubscription
{
/**
* Filter which subscribers should receive the subscription.
*/
public function filter(Subscriber $subscriber, mixed $root): bool
{
// Filter out the sender
return $subscriber->socket_id !== request()->header('X-Socket-ID');
}
}
```

When you initialize a Laravel Echo instance, a socket ID is assigned to the connection.
If you are using a global [Axios](https://github.com/mzabriskie/axios) instance to make HTTP requests from your JavaScript application, the socket ID will automatically be attached to every outgoing request in the `X-Socket-ID` header.
Then, you can access that in your filter function.

If you are not using a global Axios instance, you will need to manually configure your JavaScript application to send the `X-Socket-ID` header with all outgoing requests.
You may retrieve the socket ID using the `Echo.socketId()` method:

```js
const socketId = Echo.socketId();
```
14 changes: 14 additions & 0 deletions src/Subscriptions/Subscriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ class Subscriber
*/
public $channel;

/**
* X-Socket-ID header passed on the subscription query.
*/
public ?string $socket_id;

/**
* The topic subscribed to.
*
Expand Down Expand Up @@ -91,6 +96,13 @@ public function __construct(
$this->variables = $resolveInfo->variableValues;
$this->context = $context;

$xSocketID = request()->header('X-Socket-ID');
// @phpstan-ignore-next-line
if (is_array($xSocketID)) {
throw new \Exception('X-Socket-ID must be a string or null.');
}
$this->socket_id = $xSocketID;

$operation = $resolveInfo->operation;
assert($operation instanceof OperationDefinitionNode, 'Must be here, since webonyx/graphql-php validated the subscription.');

Expand All @@ -108,6 +120,7 @@ public function __construct(
public function __serialize(): array
{
return [
'socket_id' => $this->socket_id,
'channel' => $this->channel,
'topic' => $this->topic,
'query' => serialize(
Expand All @@ -133,6 +146,7 @@ public function __unserialize(array $data): void
);
assert($documentNode instanceof DocumentNode, 'We know the type since it is set during construction and serialized.');

$this->socket_id = $data['socket_id'];
$this->query = $documentNode;
$this->fieldName = $data['field_name'];
$this->args = $data['args'];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,8 @@ public function testDeleteSubscriber(): void

public function testSubscribersByTopic(): void
{
/** @var RedisStorageManager $storage */
$storage = $this->app->make(RedisStorageManager::class);
assert($storage instanceof RedisStorageManager);

$this->querySubscription();
$this->querySubscription();
Expand All @@ -97,10 +97,26 @@ public function testSubscribersByTopic(): void
$this->assertContainsOnlyInstancesOf(Subscriber::class, $createdSubscribers);
}

public function testSocketIDStoredOnSubscribe(): void
{
$storage = $this->app->make(RedisStorageManager::class);
assert($storage instanceof RedisStorageManager);

$this->querySubscription('taskCreated', [
'X-Socket-ID' => '1234.1234',
]);

$createdSubscriber = $storage->subscribersByTopic('TASK_CREATED')->first();

$this->assertSame('1234.1234', $createdSubscriber->socket_id);
}

/**
* @param array<string, mixed> $headers
*
* @return \Illuminate\Testing\TestResponse
*/
protected function querySubscription(string $topic = /** @lang GraphQL */ 'taskUpdated(id: 123)')
protected function querySubscription(string $topic = /** @lang GraphQL */ 'taskUpdated(id: 123)', array $headers = [])
{
return $this->graphQL(/** @lang GraphQL */ "
subscription {
Expand All @@ -109,6 +125,6 @@ protected function querySubscription(string $topic = /** @lang GraphQL */ 'taskU
name
}
}
");
", [], [], $headers);
}
}

0 comments on commit 722ff4b

Please sign in to comment.