This commit is contained in:
Your Name
2021-07-26 19:46:18 +02:00
parent e7a49138bb
commit aae17f10a6
818 changed files with 70695 additions and 16408 deletions
+31 -30
View File
@@ -4,9 +4,9 @@ namespace Nuwave\Lighthouse\Subscriptions;
use Exception;
use Illuminate\Http\Request;
use Nuwave\Lighthouse\Schema\Types\GraphQLSubscription;
use Nuwave\Lighthouse\Subscriptions\Contracts\StoresSubscriptions;
use Illuminate\Support\Str;
use Nuwave\Lighthouse\Subscriptions\Contracts\AuthorizesSubscriptions;
use Nuwave\Lighthouse\Subscriptions\Contracts\StoresSubscriptions;
use Nuwave\Lighthouse\Subscriptions\Contracts\SubscriptionExceptionHandler;
class Authorizer implements AuthorizesSubscriptions
@@ -26,12 +26,6 @@ class Authorizer implements AuthorizesSubscriptions
*/
protected $exceptionHandler;
/**
* @param \Nuwave\Lighthouse\Subscriptions\Contracts\StoresSubscriptions $storage
* @param \Nuwave\Lighthouse\Subscriptions\SubscriptionRegistry $registry
* @param \Nuwave\Lighthouse\Subscriptions\Contracts\SubscriptionExceptionHandler $exceptionHandler
* @return void
*/
public function __construct(
StoresSubscriptions $storage,
SubscriptionRegistry $registry,
@@ -42,47 +36,54 @@ class Authorizer implements AuthorizesSubscriptions
$this->exceptionHandler = $exceptionHandler;
}
/**
* Authorize subscription request.
*
* @param \Illuminate\Http\Request $request
* @return bool
*/
public function authorize(Request $request): bool
{
try {
$subscriber = $this->storage->subscriberByRequest(
$request->input(),
$request->headers->all()
);
$channel = $request->input('channel_name');
if (! is_string($channel)) {
return false;
}
if (! $subscriber) {
$channel = $this->sanitizeChannelName($channel);
$subscriber = $this->storage->subscriberByChannel($channel);
if ($subscriber === null) {
return false;
}
$subscriptions = $this->registry->subscriptions($subscriber);
if ($subscriptions->isEmpty()) {
return false;
}
$authorized = $subscriptions->reduce(
function ($authorized, GraphQLSubscription $subscription) use ($subscriber, $request): bool {
return $authorized === false
? false
: $subscription->authorize($subscriber, $request);
}
);
/** @var \Nuwave\Lighthouse\Schema\Types\GraphQLSubscription $subscription */
foreach ($subscriptions as $subscription) {
if (! $subscription->authorize($subscriber, $request)) {
$this->storage->deleteSubscriber($subscriber->channel);
if (! $authorized) {
$this->storage->deleteSubscriber($subscriber->channel);
return false;
}
}
return $authorized;
return true;
} catch (Exception $e) {
$this->exceptionHandler->handleAuthError($e);
return false;
}
}
/**
* Removes the prefix "presence-" from the channel name.
*
* Laravel Echo prefixes channel names with "presence-", but we don't.
*/
protected function sanitizeChannelName(string $channelName): string
{
if (Str::startsWith($channelName, 'presence-')) {
return Str::substr($channelName, 9);
}
return $channelName;
}
}
@@ -2,13 +2,16 @@
namespace Nuwave\Lighthouse\Subscriptions;
use Pusher\Pusher;
use RuntimeException;
use Illuminate\Contracts\Debug\ExceptionHandler as LaravelExceptionHandler;
use Illuminate\Support\Arr;
use Nuwave\Lighthouse\Support\DriverManager;
use Nuwave\Lighthouse\Subscriptions\Contracts\Broadcaster;
use Nuwave\Lighthouse\Subscriptions\Broadcasters\EchoBroadcaster;
use Nuwave\Lighthouse\Subscriptions\Broadcasters\LogBroadcaster;
use Nuwave\Lighthouse\Subscriptions\Broadcasters\PusherBroadcaster;
use Nuwave\Lighthouse\Subscriptions\Contracts\Broadcaster;
use Nuwave\Lighthouse\Support\DriverManager;
use Psr\Log\LoggerInterface;
use Pusher\Pusher;
use RuntimeException;
/**
* @method void broadcast(\Nuwave\Lighthouse\Subscriptions\Subscriber $subscriber, array $data)
@@ -18,42 +21,24 @@ use Nuwave\Lighthouse\Subscriptions\Broadcasters\PusherBroadcaster;
*/
class BroadcastManager extends DriverManager
{
/**
* Get configuration key.
*
* @return string
*/
protected function configKey(): string
{
return 'lighthouse.subscriptions.broadcasters';
}
/**
* Get configuration driver key.
*
* @return string
*/
protected function driverKey(): string
{
return 'lighthouse.subscriptions.broadcaster';
}
/**
* The interface the driver should implement.
*
* @return string
*/
protected function interface(): string
{
return Broadcaster::class;
}
/**
* Create instance of pusher driver.
*
* @param mixed[] $config
* @return \Nuwave\Lighthouse\Subscriptions\Broadcasters\PusherBroadcaster
* @throws \Pusher\PusherException
* @param array<string, mixed> $config
* @throws \RuntimeException
*/
protected function createPusherDriver(array $config): PusherBroadcaster
{
@@ -71,17 +56,23 @@ class BroadcastManager extends DriverManager
$pusher = new Pusher($appKey, $appSecret, $appId, $options);
return new PusherBroadcaster($pusher);
if ($driverConfig['log'] ?? false) {
$pusher->setLogger($this->app->make(LoggerInterface::class));
}
return new PusherBroadcaster($pusher, $this->app->make(LaravelExceptionHandler::class));
}
/**
* Create instance of log driver.
*
* @param mixed[] $config
* @return \Nuwave\Lighthouse\Subscriptions\Broadcasters\LogBroadcaster
* @param array<string, mixed> $config
*/
protected function createLogDriver(array $config): LogBroadcaster
{
return new LogBroadcaster($config);
}
protected function createEchoDriver(): EchoBroadcaster
{
return $this->app->make(EchoBroadcaster::class);
}
}
@@ -0,0 +1,47 @@
<?php
namespace Nuwave\Lighthouse\Subscriptions;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\SerializesModels;
use Nuwave\Lighthouse\Schema\Types\GraphQLSubscription;
use Nuwave\Lighthouse\Subscriptions\Contracts\BroadcastsSubscriptions;
class BroadcastSubscriptionJob implements ShouldQueue
{
use Queueable, SerializesModels;
/**
* The subscription field that was requested.
*
* @var \Nuwave\Lighthouse\Schema\Types\GraphQLSubscription
*/
public $subscription;
/**
* The name of the field.
*
* @var string
*/
public $fieldName;
/**
* The root element to be passed when resolving the subscription.
*
* @var mixed User defined.
*/
public $root;
public function __construct(GraphQLSubscription $subscription, string $fieldName, $root)
{
$this->subscription = $subscription;
$this->fieldName = $fieldName;
$this->root = $root;
}
public function handle(BroadcastsSubscriptions $broadcaster): void
{
$broadcaster->broadcast($this->subscription, $this->fieldName, $this->root);
}
}
@@ -0,0 +1,62 @@
<?php
namespace Nuwave\Lighthouse\Subscriptions\Broadcasters;
use Illuminate\Broadcasting\BroadcastManager;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Nuwave\Lighthouse\Subscriptions\Contracts\Broadcaster;
use Nuwave\Lighthouse\Subscriptions\Events\EchoSubscriptionEvent;
use Nuwave\Lighthouse\Subscriptions\Subscriber;
class EchoBroadcaster implements Broadcaster
{
/**
* @var \Illuminate\Broadcasting\BroadcastManager
*/
protected $broadcaster;
public function __construct(BroadcastManager $broadcaster)
{
$this->broadcaster = $broadcaster;
}
public function broadcast(Subscriber $subscriber, $data): void
{
$this->broadcaster->event(
new EchoSubscriptionEvent($subscriber->channel, $data)
);
}
public function authorized(Request $request): JsonResponse
{
$userId = md5(
$request->input('channel_name')
.$request->input('socket_id')
);
return new JsonResponse([
'channel_data' => [
'user_id' => $userId,
'user_info' => [],
],
], 200);
}
public function unauthorized(Request $request): JsonResponse
{
return new JsonResponse([
'message' => 'Unauthorized',
], 403);
}
public function hook(Request $request): JsonResponse
{
// Does nothing.
// The redis broadcaster has the lighthouse:subscribe command to take care of cleaning vacant channels.
return new JsonResponse([
'message' => 'okay',
], 200);
}
}
@@ -2,89 +2,66 @@
namespace Nuwave\Lighthouse\Subscriptions\Broadcasters;
use Illuminate\Support\Arr;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Nuwave\Lighthouse\Subscriptions\Subscriber;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Nuwave\Lighthouse\Subscriptions\Contracts\Broadcaster;
use Nuwave\Lighthouse\Subscriptions\Subscriber;
class LogBroadcaster implements Broadcaster
{
/**
* The user-defined configuration options.
*
* @var mixed[]
* @var array<string, mixed>
*/
protected $config = [];
/**
* A map from channel names to data.
*
* @var mixed
* @var array<string, mixed>
*/
protected $broadcasts = [];
/**
* @param array $config
* @return void
* @param array<string, mixed> $config
*/
public function __construct(array $config = [])
{
$this->config = $config;
}
/**
* Authorize subscription request.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function authorized(Request $request): JsonResponse
{
return response()->json(['message' => 'ok'], 200);
return new JsonResponse([
'message' => 'ok',
], 200);
}
/**
* Handle unauthorized subscription request.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function unauthorized(Request $request): JsonResponse
{
return response()->json(['error' => 'unauthorized'], 403);
return new JsonResponse([
'error' => 'unauthorized',
], 403);
}
/**
* Handle subscription web hook.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function hook(Request $request): JsonResponse
{
return response()->json(['message' => 'okay']);
return new JsonResponse([
'message' => 'okay',
], 200);
}
/**
* Send data to subscriber.
*
* @param \Nuwave\Lighthouse\Subscriptions\Subscriber $subscriber
* @param array $data
* @return void
*/
public function broadcast(Subscriber $subscriber, array $data): void
public function broadcast(Subscriber $subscriber, $data): void
{
$this->broadcasts[$subscriber->channel] = $data;
}
/**
* Get the data that is being broadcast.
*
* @param string|null $key
* @return array|null
* @return mixed The data that is being broadcast
*/
public function broadcasts(?string $key = null): ?array
public function broadcasts(?string $key = null)
{
return Arr::get($this->broadcasts, $key);
}
@@ -92,7 +69,7 @@ class LogBroadcaster implements Broadcaster
/**
* Get configuration options.
*
* @return mixed[]
* @return array<string, mixed>
*/
public function config(): array
{
@@ -2,106 +2,82 @@
namespace Nuwave\Lighthouse\Subscriptions\Broadcasters;
use Illuminate\Support\Arr;
use Illuminate\Http\Request;
use Illuminate\Contracts\Debug\ExceptionHandler;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Collection;
use Nuwave\Lighthouse\Subscriptions\Subscriber;
use Illuminate\Http\Request;
use Nuwave\Lighthouse\Subscriptions\Contracts\Broadcaster;
use Nuwave\Lighthouse\Subscriptions\Contracts\StoresSubscriptions;
use Nuwave\Lighthouse\Subscriptions\Subscriber;
use Pusher\ApiErrorException;
use Pusher\Pusher;
class PusherBroadcaster implements Broadcaster
{
const EVENT_NAME = 'lighthouse-subscription';
/**
* @var \Pusher\Pusher
*/
protected $pusher;
/**
* @var \Illuminate\Contracts\Debug\ExceptionHandler
*/
protected $exceptionHandler;
/**
* @var \Nuwave\Lighthouse\Subscriptions\Contracts\StoresSubscriptions
*/
protected $storage;
/**
* Create instance of pusher broadcaster.
*
* @param \Pusher\Pusher $pusher
* @return void
*/
public function __construct($pusher)
public function __construct(Pusher $pusher, ExceptionHandler $exceptionHandler)
{
$this->pusher = $pusher;
$this->exceptionHandler = $exceptionHandler;
$this->storage = app(StoresSubscriptions::class);
}
/**
* Authorize subscription request.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function authorized(Request $request): JsonResponse
{
$channel = $request->input('channel_name');
$socketId = $request->input('socket_id');
$data = json_decode(
$data = \Safe\json_decode(
$this->pusher->socket_auth($channel, $socketId),
true
);
return response()->json($data, 200);
return new JsonResponse($data, 200);
}
/**
* Handle unauthorized subscription request.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function unauthorized(Request $request): JsonResponse
{
return response()->json(['error' => 'unauthorized'], 403);
return new JsonResponse([
'error' => 'unauthorized',
], 403);
}
/**
* Handle subscription web hook.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function hook(Request $request): JsonResponse
{
(new Collection($request->input('events', [])))
->filter(function ($event): bool {
return Arr::get($event, 'name') === 'channel_vacated';
})
->each(function (array $event): void {
$this->storage->deleteSubscriber(
Arr::get($event, 'channel')
);
});
foreach ($request->input('events', []) as $event) {
if ($event['name'] === 'channel_vacated') {
$this->storage->deleteSubscriber($event['channel']);
}
}
return response()->json(['message' => 'okay']);
return new JsonResponse(['message' => 'okay']);
}
/**
* Send data to subscriber.
*
* @param \Nuwave\Lighthouse\Subscriptions\Subscriber $subscriber
* @param mixed[] $data
* @return void
*/
public function broadcast(Subscriber $subscriber, array $data): void
public function broadcast(Subscriber $subscriber, $data): void
{
$this->pusher->trigger(
$subscriber->channel,
self::EVENT_NAME,
[
'more' => true,
'result' => $data,
]
);
try {
$this->pusher->trigger(
$subscriber->channel,
self::EVENT_NAME,
[
'more' => true,
'result' => $data,
]
);
} catch (ApiErrorException $e) {
$this->exceptionHandler->report($e);
}
}
}
@@ -7,9 +7,8 @@ use Illuminate\Http\Request;
interface AuthorizesSubscriptions
{
/**
* Authorize subscription request.
* Is the subscription request authorized?
*
* @param \Illuminate\Http\Request $request
* @return bool
*/
public function authorize(Request $request);
@@ -7,10 +7,11 @@ use Nuwave\Lighthouse\Subscriptions\Subscriber;
interface Broadcaster
{
public const EVENT_NAME = 'lighthouse-subscription';
/**
* Handle authorized subscription request.
*
* @param \Illuminate\Http\Request $request
* @return \Symfony\Component\HttpFoundation\Response
*/
public function authorized(Request $request);
@@ -18,7 +19,6 @@ interface Broadcaster
/**
* Handle unauthorized subscription request.
*
* @param \Illuminate\Http\Request $request
* @return \Symfony\Component\HttpFoundation\Response
*/
public function unauthorized(Request $request);
@@ -26,7 +26,6 @@ interface Broadcaster
/**
* Handle subscription web hook.
*
* @param \Illuminate\Http\Request $request
* @return \Symfony\Component\HttpFoundation\Response
*/
public function hook(Request $request);
@@ -34,9 +33,8 @@ interface Broadcaster
/**
* Send data to subscriber.
*
* @param \Nuwave\Lighthouse\Subscriptions\Subscriber $subscriber
* @param mixed[] $data
* @param mixed $data The data to broadcast
* @return void
*/
public function broadcast(Subscriber $subscriber, array $data);
public function broadcast(Subscriber $subscriber, $data);
}
@@ -10,18 +10,13 @@ interface BroadcastsSubscriptions
/**
* Push subscription data to subscribers.
*
* @param \Nuwave\Lighthouse\Schema\Types\GraphQLSubscription $subscription
* @param string $fieldName
* @param mixed $root
* @return void
*/
public function broadcast(GraphQLSubscription $subscription, string $fieldName, $root);
/**
* Queue pushing subscription data to subscribers.
*
* @param \Nuwave\Lighthouse\Schema\Types\GraphQLSubscription $subscription
* @param string $fieldName
* @param mixed $root
* @return void
*/
public function queueBroadcast(GraphQLSubscription $subscription, string $fieldName, $root);
@@ -29,7 +24,6 @@ interface BroadcastsSubscriptions
/**
* Authorize the subscription.
*
* @param \Illuminate\Http\Request $request
* @return \Symfony\Component\HttpFoundation\Response
*/
public function authorize(Request $request);
@@ -9,7 +9,6 @@ interface ContextSerializer
/**
* Serialize the context.
*
* @param \Nuwave\Lighthouse\Support\Contracts\GraphQLContext $context
* @return string
*/
public function serialize(GraphQLContext $context);
@@ -17,7 +16,6 @@ interface ContextSerializer
/**
* Unserialize the context.
*
* @param string $context
* @return \Nuwave\Lighthouse\Support\Contracts\GraphQLContext
*/
public function unserialize(string $context);
@@ -7,43 +7,29 @@ use Nuwave\Lighthouse\Subscriptions\Subscriber;
interface StoresSubscriptions
{
/**
* Get subscriber by request.
* Find a subscriber by its channel key.
*
* @param array $input
* @param array $headers
* @return \Nuwave\Lighthouse\Subscriptions\Subscriber|null
*/
public function subscriberByRequest(array $input, array $headers);
/**
* Find subscriber by channel.
*
* @param string $channel
* @return \Nuwave\Lighthouse\Subscriptions\Subscriber|null
*/
public function subscriberByChannel(string $channel);
/**
* Get collection of subscribers by topic.
* Get all subscribers for a topic.
*
* @param string $topic
* @return \Illuminate\Support\Collection<\Nuwave\Lighthouse\Subscriptions\Subscriber>
*/
public function subscribersByTopic(string $topic);
/**
* Store subscription.
* Store subscriber for a topic.
*
* @param \Nuwave\Lighthouse\Subscriptions\Subscriber $subscriber
* @param string $topic
* @return void
*/
public function storeSubscriber(Subscriber $subscriber, string $topic);
/**
* Delete subscriber.
* Delete subscriber by its channel key.
*
* @param string $channel
* @return \Nuwave\Lighthouse\Subscriptions\Subscriber|null
*/
public function deleteSubscriber(string $channel);
@@ -9,7 +9,6 @@ interface SubscriptionExceptionHandler
/**
* Handle authentication error.
*
* @param \Throwable $e
* @return void
*/
public function handleAuthError(Throwable $e);
@@ -17,7 +16,6 @@ interface SubscriptionExceptionHandler
/**
* Handle broadcast error.
*
* @param \Throwable $e
* @return void
*/
public function handleBroadcastError(Throwable $e);
@@ -8,12 +8,18 @@ use Illuminate\Support\Collection;
interface SubscriptionIterator
{
/**
* Process collection of items.
* Process subscribers through the given callbacks.
*
* @param \Illuminate\Support\Collection $items
* @param \Closure $cb
* @param \Closure|null $error
* @param \Illuminate\Support\Collection<\Nuwave\Lighthouse\Subscriptions\Subscriber> $subscribers
* The subscribers that receive the current subscription.
*
* @param \Closure $handleSubscriber
* Receives each subscriber in the passed in collection.
* function(\Nuwave\Lighthouse\Subscriptions\Subscriber $subscriber)
*
* @param \Closure|null $handleError
* Is called when $handleSubscriber throws.
* @return void
*/
public function process(Collection $items, Closure $cb, Closure $error = null);
public function process(Collection $subscribers, Closure $handleSubscriber, Closure $handleError = null);
}
@@ -0,0 +1,50 @@
<?php
namespace Nuwave\Lighthouse\Subscriptions\Directives;
use Closure;
use Nuwave\Lighthouse\Execution\Utils\Subscription;
use Nuwave\Lighthouse\Schema\Directives\BaseDirective;
use Nuwave\Lighthouse\Schema\Values\FieldValue;
use Nuwave\Lighthouse\Support\Contracts\FieldMiddleware;
class BroadcastDirective extends BaseDirective implements FieldMiddleware
{
public static function definition(): string
{
return /** @lang GraphQL */ <<<'GRAPHQL'
"""
Broadcast the results of a mutation to subscribed clients.
"""
directive @broadcast(
"""
Name of the subscription that should be retriggered as a result of this operation.
"""
subscription: String!
"""
Specify whether or not the job should be queued.
This defaults to the global config option `lighthouse.subscriptions.queue_broadcasts`.
"""
shouldQueue: Boolean
) repeatable on FIELD_DEFINITION
GRAPHQL;
}
public function handleField(FieldValue $fieldValue, Closure $next): FieldValue
{
// Ensure this is run after the other field middleware directives
$fieldValue = $next($fieldValue);
$subscriptionField = $this->directiveArgValue('subscription');
$shouldQueue = $this->directiveArgValue('shouldQueue');
$fieldValue->resultHandler(function ($root) use ($subscriptionField, $shouldQueue) {
Subscription::broadcast($subscriptionField, $root, $shouldQueue);
return $root;
});
return $fieldValue;
}
}
@@ -0,0 +1,33 @@
<?php
namespace Nuwave\Lighthouse\Subscriptions\Directives;
use Nuwave\Lighthouse\Schema\Directives\BaseDirective;
use Nuwave\Lighthouse\Support\Contracts\Directive;
/**
* This directive exists as a placeholder and can be used
* to point to a custom subscription class.
*
* @see \Nuwave\Lighthouse\Schema\Types\GraphQLSubscription
*/
class SubscriptionDirective extends BaseDirective implements Directive
{
public const NAME = 'subscription';
public static function definition(): string
{
return /** @lang GraphQL */ <<<'GRAPHQL'
"""
Reference a class to handle the broadcasting of a subscription to clients.
The given class must extend `\Nuwave\Lighthouse\Schema\Types\GraphQLSubscription`.
"""
directive @subscription(
"""
A reference to a subclass of `\Nuwave\Lighthouse\Schema\Types\GraphQLSubscription`.
"""
class: String!
) on FIELD_DEFINITION
GRAPHQL;
}
}
@@ -1,39 +0,0 @@
<?php
namespace Nuwave\Lighthouse\Subscriptions\Events;
use Illuminate\Queue\SerializesModels;
use Nuwave\Lighthouse\Schema\Types\GraphQLSubscription as Subscription;
class BroadcastSubscriptionEvent
{
use SerializesModels;
/**
* @var \Nuwave\Lighthouse\Schema\Types\GraphQLSubscription
*/
public $subscription;
/**
* @var string
*/
public $fieldName;
/**
* @var mixed
*/
public $root;
/**
* @param \Nuwave\Lighthouse\Schema\Types\GraphQLSubscription $subscription
* @param string $fieldName
* @param mixed $root
* @return void
*/
public function __construct(Subscription $subscription, string $fieldName, $root)
{
$this->subscription = $subscription;
$this->fieldName = $fieldName;
$this->root = $root;
}
}
@@ -1,38 +0,0 @@
<?php
namespace Nuwave\Lighthouse\Subscriptions\Events;
use Illuminate\Contracts\Queue\ShouldQueue;
use Nuwave\Lighthouse\Subscriptions\Contracts\BroadcastsSubscriptions;
class BroadcastSubscriptionListener implements ShouldQueue
{
/**
* @var \Nuwave\Lighthouse\Subscriptions\Contracts\BroadcastsSubscriptions
*/
protected $broadcaster;
/**
* @param \Nuwave\Lighthouse\Subscriptions\Contracts\BroadcastsSubscriptions $broadcaster
* @return void
*/
public function __construct(BroadcastsSubscriptions $broadcaster)
{
$this->broadcaster = $broadcaster;
}
/**
* Handle the event.
*
* @param \Nuwave\Lighthouse\Subscriptions\Events\BroadcastSubscriptionEvent $event
* @return void
*/
public function handle(BroadcastSubscriptionEvent $event): void
{
$this->broadcaster->broadcast(
$event->subscription,
$event->fieldName,
$event->root
);
}
}
@@ -0,0 +1,44 @@
<?php
namespace Nuwave\Lighthouse\Subscriptions\Events;
use Illuminate\Broadcasting\Channel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
use Nuwave\Lighthouse\Subscriptions\Contracts\Broadcaster;
class EchoSubscriptionEvent implements ShouldBroadcastNow
{
/**
* @var string
*/
public $channel;
/**
* @var mixed The data to broadcast.
*/
public $data;
/**
* @param mixed $data The data to broadcast.
*/
public function __construct(string $channel, $data)
{
$this->channel = $channel;
$this->data = $data;
}
public function broadcastOn(): Channel
{
return new Channel($this->channel);
}
/**
* Returns an event name.
*
* Allows the echo client to receive this event using .listen('.lighthouse.subscription', () => ...).
*/
public function broadcastAs(): string
{
return Broadcaster::EVENT_NAME;
}
}
@@ -2,16 +2,13 @@
namespace Nuwave\Lighthouse\Subscriptions;
use Throwable;
use Nuwave\Lighthouse\Subscriptions\Contracts\SubscriptionExceptionHandler;
use Throwable;
class ExceptionHandler implements SubscriptionExceptionHandler
{
/**
* Handle authentication error.
*
* @param \Throwable $e
* @return void
*/
public function handleAuthError(Throwable $e): void
{
@@ -20,9 +17,6 @@ class ExceptionHandler implements SubscriptionExceptionHandler
/**
* Handle broadcast error.
*
* @param \Throwable $e
* @return void
*/
public function handleBroadcastError(Throwable $e): void
{
@@ -0,0 +1,70 @@
<?php
namespace Nuwave\Lighthouse\Subscriptions\Iterators;
use Closure;
use Exception;
use Illuminate\Contracts\Auth\Factory as AuthFactory;
use Illuminate\Contracts\Config\Repository as ConfigRepository;
use Illuminate\Support\Collection;
use Nuwave\Lighthouse\Subscriptions\Contracts\SubscriptionIterator;
use Nuwave\Lighthouse\Subscriptions\Subscriber;
use Nuwave\Lighthouse\Subscriptions\SubscriptionGuard;
/**
* Logs in the subscriber as their subscription is resolved.
*/
class AuthenticatingSyncIterator implements SubscriptionIterator
{
/**
* @var \Illuminate\Contracts\Config\Repository
*/
protected $configRepository;
/**
* @var \Illuminate\Contracts\Auth\Factory
*/
protected $authFactory;
public function __construct(ConfigRepository $configRepository, AuthFactory $authFactory)
{
$this->configRepository = $configRepository;
$this->authFactory = $authFactory;
}
public function process(Collection $subscribers, Closure $handleSubscriber, Closure $handleError = null): void
{
// Store the previous default guard name so we can restore it after we're done
$previousGuardName = $this->configRepository->get('auth.defaults.guard');
// Set our subscription guard as the default guard for the application
$this->authFactory->shouldUse(SubscriptionGuard::GUARD_NAME);
/** @var \Nuwave\Lighthouse\Subscriptions\SubscriptionGuard $guard */
$guard = $this->authFactory->guard(SubscriptionGuard::GUARD_NAME);
$subscribers->each(static function (Subscriber $item) use ($handleSubscriber, $handleError, $guard): void {
// If there is an authenticated user set in the context, set that user as the authenticated user
$user = $item->context->user();
if ($user !== null) {
$guard->setUser($user);
}
try {
$handleSubscriber($item);
} catch (Exception $e) {
if ($handleError === null) {
throw $e;
}
$handleError($e);
} finally {
// Unset the authenticated user after each iteration to restore the guard to a unauthenticated state
$guard->reset();
}
});
// Restore the previous default guard name
$this->authFactory->shouldUse($previousGuardName);
}
}
@@ -9,25 +9,17 @@ use Nuwave\Lighthouse\Subscriptions\Contracts\SubscriptionIterator;
class SyncIterator implements SubscriptionIterator
{
/**
* Process collection of items.
*
* @param \Illuminate\Support\Collection $items
* @param \Closure $cb
* @param \Closure|null $error
* @return void
*/
public function process(Collection $items, Closure $cb, Closure $error = null): void
public function process(Collection $subscribers, Closure $handleSubscriber, Closure $handleError = null): void
{
$items->each(function ($item) use ($cb, $error): void {
$subscribers->each(static function ($item) use ($handleSubscriber, $handleError): void {
try {
$cb($item);
$handleSubscriber($item);
} catch (Exception $e) {
if (! $error) {
if ($handleError === null) {
throw $e;
}
$error($e);
$handleError($e);
}
});
}
+7 -20
View File
@@ -2,34 +2,27 @@
namespace Nuwave\Lighthouse\Subscriptions;
use Illuminate\Support\Arr;
use Illuminate\Http\Request;
use Illuminate\Queue\SerializesAndRestoresModelIdentifiers;
use Illuminate\Support\Arr;
use Nuwave\Lighthouse\Subscriptions\Contracts\ContextSerializer;
use Nuwave\Lighthouse\Support\Contracts\CreatesContext;
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
use Nuwave\Lighthouse\Subscriptions\Contracts\ContextSerializer;
class Serializer implements ContextSerializer
{
use SerializesAndRestoresModelIdentifiers;
/**
* @var \Nuwave\Lighthouse\Support\Contracts\CreatesContext
*/
protected $createsContext;
/**
* @param \Nuwave\Lighthouse\Support\Contracts\CreatesContext $createsContext
* @return void
*/
public function __construct(CreatesContext $createsContext)
{
$this->createsContext = $createsContext;
}
/**
* Serialize the context.
*
* @param \Nuwave\Lighthouse\Support\Contracts\GraphQLContext $context
* @return string
*/
public function serialize(GraphQLContext $context): string
{
$request = $context->request();
@@ -44,16 +37,10 @@ class Serializer implements ContextSerializer
'server' => Arr::except($request->server->all(), ['HTTP_AUTHORIZATION']),
'content' => $request->getContent(),
],
'user' => serialize($context->user()),
'user' => $this->getSerializedPropertyValue($context->user()),
]);
}
/**
* Unserialize the context.
*
* @param string $context
* @return \Nuwave\Lighthouse\Support\Contracts\GraphQLContext
*/
public function unserialize(string $context): GraphQLContext
{
[
@@ -73,7 +60,7 @@ class Serializer implements ContextSerializer
$request->setUserResolver(
function () use ($rawUser) {
return unserialize($rawUser);
return $this->getRestoredPropertyValue($rawUser);
}
);
@@ -0,0 +1,170 @@
<?php
namespace Nuwave\Lighthouse\Subscriptions\Storage;
use Exception;
use Illuminate\Contracts\Cache\Factory as CacheFactory;
use Illuminate\Contracts\Config\Repository as ConfigRepository;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Nuwave\Lighthouse\Subscriptions\Contracts\StoresSubscriptions;
use Nuwave\Lighthouse\Subscriptions\Subscriber;
class CacheStorageManager implements StoresSubscriptions
{
/**
* The cache key for topics.
*
* @var string
*/
public const TOPIC_KEY = 'graphql.topic';
/**
* The cache key for subscribers.
*
* @var string
*/
public const SUBSCRIBER_KEY = 'graphql.subscriber';
/**
* The cache to store channels and topics.
*
* @var \Illuminate\Contracts\Cache\Repository
*/
protected $cache;
/**
* The time to live for items in the cache.
*
* @var int|null
*/
protected $ttl;
public function __construct(CacheFactory $cacheFactory, ConfigRepository $config)
{
$storage = $config->get('lighthouse.subscriptions.storage') ?? 'file';
if (! is_string($storage)) {
throw new Exception('Config setting lighthouse.subscriptions.storage must be a string or `null`, got: '.\Safe\json_encode($storage));
}
$this->cache = $cacheFactory->store($storage);
$ttl = $config->get('lighthouse.subscriptions.storage_ttl');
if (! is_null($ttl) && ! is_int($ttl)) {
throw new Exception('Config setting lighthouse.subscriptions.storage_ttl must be a int or `null`, got: '.\Safe\json_encode($ttl));
}
$this->ttl = $ttl;
}
public function subscriberByChannel(string $channel): ?Subscriber
{
return $this->cache->get(self::channelKey($channel));
}
public function subscribersByTopic(string $topic): Collection
{
/** @var \Illuminate\Support\Collection<\Nuwave\Lighthouse\Subscriptions\Subscriber> $subscribers */
$subscribers = $this
->retrieveTopic(self::topicKey($topic))
->map(function (string $channel): ?Subscriber {
return $this->subscriberByChannel($channel);
})
->filter();
return $subscribers;
}
public function storeSubscriber(Subscriber $subscriber, string $topic): void
{
$subscriber->topic = $topic;
$this->addSubscriberToTopic($subscriber);
$channelKey = self::channelKey($subscriber->channel);
if ($this->ttl === null) {
$this->cache->forever($channelKey, $subscriber);
} else {
// TODO: Change to just pass the ttl directly when support for Laravel <=5.7 is dropped
$this->cache->put($channelKey, $subscriber, Carbon::now()->addSeconds($this->ttl));
}
}
public function deleteSubscriber(string $channel): ?Subscriber
{
$subscriber = $this->cache->pull(self::channelKey($channel));
if ($subscriber !== null) {
$this->removeSubscriberFromTopic($subscriber);
}
return $subscriber;
}
/**
* Store a topic (list of channels) in the cache.
*
* @param \Illuminate\Support\Collection<string> $topic
*/
protected function storeTopic(string $key, Collection $topic): void
{
if ($this->ttl === null) {
$this->cache->forever($key, $topic);
} else {
// TODO: Change to just pass the ttl directly when support for Laravel <=5.7 is dropped
$this->cache->put($key, $topic, Carbon::now()->addSeconds($this->ttl));
}
}
/**
* Retrieve a topic (list of channels) from the cache.
*
* @return \Illuminate\Support\Collection<string>
*/
protected function retrieveTopic(string $key): Collection
{
return $this->cache->get($key, new Collection());
}
/**
* Add the subscriber to the topic they subscribe to.
*/
protected function addSubscriberToTopic(Subscriber $subscriber): void
{
$topicKey = self::topicKey($subscriber->topic);
$topic = $this->retrieveTopic($topicKey);
$topic->push($subscriber->channel);
$this->storeTopic($topicKey, $topic);
}
/**
* Remove the subscriber from the topic they are subscribed to.
*/
protected function removeSubscriberFromTopic(Subscriber $subscriber): void
{
$topicKey = self::topicKey($subscriber->topic);
$channelKeyToRemove = self::channelKey($subscriber->channel);
$topicWithoutSubscriber = $this
->retrieveTopic($topicKey)
->reject(function (string $channel) use ($channelKeyToRemove): bool {
return self::channelKey($channel) === $channelKeyToRemove;
});
if ($topicWithoutSubscriber->isEmpty()) {
$this->cache->forget($topicKey);
return;
}
$this->storeTopic($topicKey, $topicWithoutSubscriber);
}
protected static function channelKey(string $channel): string
{
return self::SUBSCRIBER_KEY.".{$channel}";
}
protected static function topicKey(string $topic): string
{
return self::TOPIC_KEY.".{$topic}";
}
}
@@ -0,0 +1,184 @@
<?php
namespace Nuwave\Lighthouse\Subscriptions\Storage;
use Illuminate\Contracts\Config\Repository as ConfigRepository;
use Illuminate\Contracts\Redis\Factory as RedisFactory;
use Illuminate\Support\Collection;
use Nuwave\Lighthouse\Subscriptions\Contracts\StoresSubscriptions;
use Nuwave\Lighthouse\Subscriptions\Subscriber;
/**
* Stores subscribers and topics in redis.
* - Topics are subscriptions like "userCreated" or "userDeleted".
* - Subscribers are clients that are listening to channels like "private-lighthouse-a7ef3d".
*
* This manager stores a SET of subscriber channels and the subscribers itself like this:
* - graphql.topic.userCreated = [ "presence-lighthouse-1", "presence-lighthouse-2", ... ]
* - graphql.topic.userDeleted = [ "presence-lighthouse-5", "presence-lighthouse-6", ... ]
* - graphql.subscriber.presence-lighthouse-1 = { query: "{ id, name }" }
* - graphql.subscriber.presence-lighthouse-2 = { query: "{ name, created_at }" }
*/
class RedisStorageManager implements StoresSubscriptions
{
public const TOPIC_KEY = 'graphql.topic';
public const SUBSCRIBER_KEY = 'graphql.subscriber';
/**
* @var \Illuminate\Redis\Connections\Connection
*/
protected $connection;
/**
* The time to live in seconds for items in the cache.
*
* @var int|null
*/
protected $ttl;
public function __construct(ConfigRepository $config, RedisFactory $redis)
{
$this->connection = $redis->connection(
$config->get('lighthouse.subscriptions.broadcasters.echo.connection') ?? 'default'
);
$this->ttl = $config->get('lighthouse.subscriptions.storage_ttl');
}
public function subscriberByChannel(string $channel): ?Subscriber
{
return $this->getSubscriber(
$this->channelKey($channel)
);
}
/**
* @return \Illuminate\Support\Collection<\Nuwave\Lighthouse\Subscriptions\Subscriber>
*/
public function subscribersByTopic(string $topic): Collection
{
// As explained in storeSubscriber, we use redis sets to store the names of subscribers of a topic.
// We can retrieve all members of a set using the command smembers.
$subscriberIds = $this->connection->command('smembers', [$this->topicKey($topic)]);
if (count($subscriberIds) === 0) {
return new Collection();
}
// Since we store the individual subscribers with a prefix,
// but not in the set, we have to add the prefix here.
$subscriberIds = array_map([$this, 'channelKey'], $subscriberIds);
// Using the mget command, we can retrieve multiple values from redis.
// This is like using multiple get calls (getSubscriber uses the get command).
$subscribers = $this->connection->command('mget', [$subscriberIds]);
return (new Collection($subscribers))
->map(function (string $subscriber) {
return $this->unserialize($subscriber);
})
->filter();
}
public function storeSubscriber(Subscriber $subscriber, string $topic): void
{
$subscriber->topic = $topic;
// In contrast to the CacheStorageManager, we use redis sets.
// Instead of reading the entire list, adding the subscriber and storing the list;
// we simply add the name of the subscriber to the set of subscribers of this topic using the sadd command...
$topicKey = $this->topicKey($topic);
$this->connection->command('sadd', [
$topicKey,
$subscriber->channel,
]);
// ...and refresh the ttl of this set as well.
if ($this->ttl !== null) {
$this->connection->command('expire', [$topicKey, $this->ttl]);
}
// Lastly, we store the subscriber as a serialized string...
$setCommand = 'set';
$setArguments = [
$this->channelKey($subscriber->channel),
$this->serialize($subscriber),
];
if ($this->ttl !== null) {
$setCommand = 'setex';
array_splice($setArguments, 1, 0, [$this->ttl]);
}
$this->connection->command($setCommand, $setArguments);
}
public function deleteSubscriber(string $channel): ?Subscriber
{
$key = $this->channelKey($channel);
$subscriber = $this->getSubscriber($key);
if ($subscriber !== null) {
// Like in storeSubscriber (but in reverse), we delete the subscriber...
$this->connection->command('del', [$key]);
// ...and remove it from the set of subscribers of this topic.
$this->connection->command('srem', [
$this->topicKey($subscriber->topic),
$channel,
]);
}
return $subscriber;
}
protected function getSubscriber(string $channelKey): ?Subscriber
{
$subscriber = $this->unserialize(
$this->connection->command('get', [$channelKey])
);
// unserialize could return false, so we make sure to only return a Subscriber or null
if ($subscriber instanceof Subscriber) {
return $subscriber;
}
return null;
}
protected function channelKey(string $channel): string
{
return self::SUBSCRIBER_KEY.'.'.$channel;
}
protected function topicKey(string $topic): string
{
return self::TOPIC_KEY.'.'.$topic;
}
/**
* @param mixed $value Value to serialize.
* @return mixed Storable value.
* @see \Illuminate\Cache\RedisStore::serialize
*/
protected function serialize($value)
{
$isProperNumber = is_numeric($value)
&& ($value !== INF && $value !== -INF)
&& ! is_nan(floatval($value));
return $isProperNumber
? $value
: serialize($value);
}
/**
* @param mixed $value Value to unserialize.
* @return mixed Unserialized value.
*/
protected function unserialize($value)
{
if (false === $value) {
return null;
}
return is_numeric($value)
? $value
: unserialize($value);
}
}
@@ -1,136 +0,0 @@
<?php
namespace Nuwave\Lighthouse\Subscriptions;
use Illuminate\Support\Arr;
use Illuminate\Cache\CacheManager;
use Illuminate\Support\Collection;
use Nuwave\Lighthouse\Subscriptions\Contracts\StoresSubscriptions;
class StorageManager implements StoresSubscriptions
{
/**
* The cache key for topics.
*
* @var string
*/
const TOPIC_KEY = 'graphql.topic';
/**
* The cache key for subscribers.
*
* @var string
*/
const SUBSCRIBER_KEY = 'graphql.subscriber';
/**
* @var \Illuminate\Contracts\Cache\Repository
*/
protected $cache;
/**
* @param \Illuminate\Cache\CacheManager $cacheManager
* @return void
*/
public function __construct(CacheManager $cacheManager)
{
$this->cache = $cacheManager->store(
config('lighthouse.subscriptions.storage', 'redis')
);
}
/**
* Get subscriber by request.
*
* @param array $input
* @param array $headers
* @return \Nuwave\Lighthouse\Subscriptions\Subscriber|null
*/
public function subscriberByRequest(array $input, array $headers): ?Subscriber
{
$channel = Arr::get($input, 'channel_name');
return $channel
? $this->subscriberByChannel($channel)
: null;
}
/**
* Find subscriber by channel.
*
* @param string $channel
* @return \Nuwave\Lighthouse\Subscriptions\Subscriber|null
*/
public function subscriberByChannel(string $channel): ?Subscriber
{
$key = self::SUBSCRIBER_KEY.".{$channel}";
return $this->cache->get($key);
}
/**
* Get collection of subscribers by channel.
*
* @param string $topic
* @return \Illuminate\Support\Collection<\Nuwave\Lighthouse\Subscriptions\Subscriber>
*/
public function subscribersByTopic(string $topic): Collection
{
$key = self::TOPIC_KEY.".{$topic}";
if (! $this->cache->has($key)) {
return new Collection;
}
$channels = json_decode($this->cache->get($key), true);
return (new Collection($channels))
->map(function (string $channel): ?Subscriber {
return $this->subscriberByChannel($channel);
})
->filter()
->values();
}
/**
* Store subscription.
*
* @param \Nuwave\Lighthouse\Subscriptions\Subscriber $subscriber
* @param string $topic
* @return void
*/
public function storeSubscriber(Subscriber $subscriber, string $topic): void
{
$topicKey = self::TOPIC_KEY.".{$topic}";
$subscriberKey = self::SUBSCRIBER_KEY.".{$subscriber->channel}";
$topic = $this->cache->has($topicKey)
? json_decode($this->cache->get($topicKey), true)
: [];
$topic[] = $subscriber->channel;
$this->cache->forever($topicKey, json_encode($topic));
$this->cache->forever($subscriberKey, $subscriber);
}
/**
* Delete subscriber.
*
* @param string $channel
* @return \Nuwave\Lighthouse\Subscriptions\Subscriber|null
*/
public function deleteSubscriber(string $channel): ?Subscriber
{
$key = self::SUBSCRIBER_KEY.".{$channel}";
$hasSubscriber = $this->cache->has($key);
$subscriber = $this->cache->get($key);
if ($hasSubscriber) {
$this->cache->forget($key);
}
return $subscriber;
}
}
+88 -59
View File
@@ -2,126 +2,160 @@
namespace Nuwave\Lighthouse\Subscriptions;
use Serializable;
use GraphQL\Language\AST\DocumentNode;
use GraphQL\Language\AST\NodeList;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQL\Utils\AST;
use Illuminate\Support\Str;
use GraphQL\Language\AST\DocumentNode;
use GraphQL\Type\Definition\ResolveInfo;
use Nuwave\Lighthouse\Exceptions\SubscriptionException;
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
use Nuwave\Lighthouse\Subscriptions\Contracts\ContextSerializer;
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
use Serializable;
class Subscriber implements Serializable
{
const MISSING_OPERATION_NAME = 'Must pass an operation name when using a subscription.';
/**
* A unique key for the subscriber's channel.
*
* This has to be unique for each subscriber, because each of them can send a different
* query and must receive a response that is specifically tailored towards that.
*
* @var string
*/
public $channel;
/**
* @var mixed
* The topic subscribed to.
*
* @var string
*/
public $root;
/**
* @var array
*/
public $args;
/**
* @var mixed
*/
public $context;
public $topic;
/**
* The contents of the query.
*
* @var \GraphQL\Language\AST\DocumentNode
*/
public $query;
/**
* The name of the queried field.
*
* Guaranteed be be unique because of
* @see \GraphQL\Validator\Rules\SingleFieldSubscription
*
* @var string
*/
public $operationName;
public $fieldName;
/**
* @param mixed[] $args
* @param \Nuwave\Lighthouse\Support\Contracts\GraphQLContext $context
* @param \GraphQL\Type\Definition\ResolveInfo $resolveInfo
* @return void
* The root element of the query.
*
* @throws \Nuwave\Lighthouse\Exceptions\SubscriptionException
* @var mixed Can be anything.
*/
public $root;
/**
* The args passed to the subscription query.
*
* @var array<string, mixed>
*/
public $args;
/**
* The variables passed to the subscription query.
*
* @var array<string, mixed>
*/
public $variables;
/**
* The context passed to the query.
*
* @var \Nuwave\Lighthouse\Support\Contracts\GraphQLContext
*/
public $context;
/**
* @param array<string, mixed> $args
*/
public function __construct(
array $args,
GraphQLContext $context,
ResolveInfo $resolveInfo
) {
$operationName = $resolveInfo->operation->name;
if (! $operationName) {
throw new SubscriptionException(
self::MISSING_OPERATION_NAME
);
}
$this->operationName = $operationName->value;
$this->fieldName = $resolveInfo->fieldName;
$this->channel = self::uniqueChannelName();
$this->args = $args;
$this->variables = $resolveInfo->variableValues;
$this->context = $context;
$documentNode = new DocumentNode([]);
$documentNode->definitions = $resolveInfo->fragments;
$documentNode->definitions[] = $resolveInfo->operation;
$this->query = $documentNode;
/**
* Must be here, since webonyx/graphql-php validated the subscription.
*
* @var \GraphQL\Language\AST\OperationDefinitionNode $operation
*/
$operation = $resolveInfo->operation;
$this->query = new DocumentNode([
'definitions' => new NodeList(array_merge(
$resolveInfo->fragments,
[$operation]
)),
]);
}
/**
* Unserialize subscription from a JSON string.
*
* @param string $subscription
* @return $this
*/
public function unserialize($subscription): self
public function unserialize($subscription): void
{
$data = json_decode($subscription, true);
$data = \Safe\json_decode($subscription, true);
$this->operationName = $data['operation_name'];
$this->channel = $data['channel'];
$this->topic = $data['topic'];
/**
* We know the type since it is set during construction and serialized.
*
* @var \GraphQL\Language\AST\DocumentNode $documentNode
*/
$documentNode = AST::fromArray(
unserialize($data['query'])
);
$this->query = $documentNode;
$this->fieldName = $data['field_name'];
$this->args = $data['args'];
$this->variables = $data['variables'];
$this->context = $this->contextSerializer()->unserialize(
$data['context']
);
$this->query = AST::fromArray(
unserialize($data['query'])
);
return $this;
}
/**
* Convert this into a JSON string.
*
* @return false|string
*/
public function serialize()
public function serialize(): string
{
return json_encode([
'operation_name' => $this->operationName,
return \Safe\json_encode([
'channel' => $this->channel,
'args' => $this->args,
'context' => $this->contextSerializer()->serialize($this->context),
'topic' => $this->topic,
'query' => serialize(
AST::toArray($this->query)
),
'field_name' => $this->fieldName,
'args' => $this->args,
'variables' => $this->variables,
'context' => $this->contextSerializer()->serialize($this->context),
]);
}
/**
* Set root data.
*
* @param mixed $root
* @return $this
* @deprecated set the attribute directly
*/
public function setRoot($root): self
{
@@ -132,17 +166,12 @@ class Subscriber implements Serializable
/**
* Generate a unique private channel name.
*
* @return string
*/
public static function uniqueChannelName(): string
{
return 'private-lighthouse-'.Str::random(32).'-'.time();
}
/**
* @return \Nuwave\Lighthouse\Subscriptions\Contracts\ContextSerializer
*/
protected function contextSerializer(): ContextSerializer
{
return app(ContextSerializer::class);
@@ -2,15 +2,15 @@
namespace Nuwave\Lighthouse\Subscriptions;
use Illuminate\Contracts\Bus\Dispatcher as BusDispatcher;
use Illuminate\Http\Request;
use Nuwave\Lighthouse\GraphQL;
use Symfony\Component\HttpFoundation\Response;
use Nuwave\Lighthouse\Schema\Types\GraphQLSubscription;
use Illuminate\Contracts\Events\Dispatcher as EventsDispatcher;
use Nuwave\Lighthouse\Subscriptions\Contracts\SubscriptionIterator;
use Nuwave\Lighthouse\Subscriptions\Contracts\AuthorizesSubscriptions;
use Nuwave\Lighthouse\Subscriptions\Contracts\BroadcastsSubscriptions;
use Nuwave\Lighthouse\Subscriptions\Events\BroadcastSubscriptionEvent;
use Nuwave\Lighthouse\Subscriptions\Contracts\StoresSubscriptions;
use Nuwave\Lighthouse\Subscriptions\Contracts\SubscriptionIterator;
use Symfony\Component\HttpFoundation\Response;
class SubscriptionBroadcaster implements BroadcastsSubscriptions
{
@@ -22,17 +22,17 @@ class SubscriptionBroadcaster implements BroadcastsSubscriptions
/**
* @var \Nuwave\Lighthouse\Subscriptions\Contracts\AuthorizesSubscriptions
*/
protected $auth;
protected $subscriptionAuthorizer;
/**
* @var \Nuwave\Lighthouse\Subscriptions\StorageManager
* @var \Nuwave\Lighthouse\Subscriptions\Contracts\StoresSubscriptions
*/
protected $storage;
protected $subscriptionStorage;
/**
* @var \Nuwave\Lighthouse\Subscriptions\Contracts\SubscriptionIterator
*/
protected $iterator;
protected $subscriptionIterator;
/**
* @var \Nuwave\Lighthouse\Subscriptions\BroadcastManager
@@ -40,96 +40,67 @@ class SubscriptionBroadcaster implements BroadcastsSubscriptions
protected $broadcastManager;
/**
* @var \Illuminate\Contracts\Events\Dispatcher
* @var \Illuminate\Contracts\Bus\Dispatcher
*/
protected $eventsDispatcher;
protected $busDispatcher;
/**
* @param \Nuwave\Lighthouse\GraphQL $graphQL
* @param \Nuwave\Lighthouse\Subscriptions\Contracts\AuthorizesSubscriptions $auth
* @param \Nuwave\Lighthouse\Subscriptions\StorageManager $storage
* @param \Nuwave\Lighthouse\Subscriptions\Contracts\SubscriptionIterator $iterator
* @param \Nuwave\Lighthouse\Subscriptions\BroadcastManager $broadcastManager
* @param \Illuminate\Contracts\Events\Dispatcher $eventsDispatcher
* @return void
*/
public function __construct(
GraphQL $graphQL,
AuthorizesSubscriptions $auth,
StorageManager $storage,
SubscriptionIterator $iterator,
AuthorizesSubscriptions $subscriptionAuthorizer,
StoresSubscriptions $subscriptionStorage,
SubscriptionIterator $subscriptionIterator,
BroadcastManager $broadcastManager,
EventsDispatcher $eventsDispatcher
BusDispatcher $busDispatcher
) {
$this->graphQL = $graphQL;
$this->auth = $auth;
$this->storage = $storage;
$this->iterator = $iterator;
$this->subscriptionAuthorizer = $subscriptionAuthorizer;
$this->subscriptionStorage = $subscriptionStorage;
$this->subscriptionIterator = $subscriptionIterator;
$this->broadcastManager = $broadcastManager;
$this->eventsDispatcher = $eventsDispatcher;
$this->busDispatcher = $busDispatcher;
}
/**
* Queue pushing subscription data to subscribers.
*
* @param \Nuwave\Lighthouse\Schema\Types\GraphQLSubscription $subscription
* @param string $fieldName
* @param mixed $root
* @return void
*/
public function queueBroadcast(GraphQLSubscription $subscription, string $fieldName, $root): void
{
$this->eventsDispatcher->dispatch(
new BroadcastSubscriptionEvent($subscription, $fieldName, $root)
);
$broadcastSubscriptionJob = new BroadcastSubscriptionJob($subscription, $fieldName, $root);
$broadcastSubscriptionJob->onQueue(config('lighthouse.subscriptions.broadcasts_queue_name'));
$this->busDispatcher->dispatch($broadcastSubscriptionJob);
}
/**
* Push subscription data to subscribers.
*
* @param \Nuwave\Lighthouse\Schema\Types\GraphQLSubscription $subscription
* @param string $fieldName
* @param mixed $root
* @return void
*/
public function broadcast(GraphQLSubscription $subscription, string $fieldName, $root): void
{
$topic = $subscription->decodeTopic($fieldName, $root);
$subscribers = $this->storage
$subscribers = $this->subscriptionStorage
->subscribersByTopic($topic)
->filter(function (Subscriber $subscriber) use ($subscription, $root): bool {
return $subscription->filter($subscriber, $root);
});
$this->iterator->process(
$this->subscriptionIterator->process(
$subscribers,
function (Subscriber $subscriber) use ($root): void {
$data = $this->graphQL->executeQuery(
$subscriber->root = $root;
$executionResult = $this->graphQL->executeQuery(
$subscriber->query,
$subscriber->context,
$subscriber->args,
$subscriber->setRoot($root),
$subscriber->operationName
$subscriber->variables,
$subscriber
);
$this->broadcastManager->broadcast(
$subscriber,
$data->jsonSerialize()
$this->graphQL->serializable($executionResult)
);
}
);
}
/**
* Authorize the subscription.
*
* @param \Illuminate\Http\Request $request
* @return \Symfony\Component\HttpFoundation\Response
*/
public function authorize(Request $request): Response
{
return $this->auth->authorize($request)
return $this->subscriptionAuthorizer->authorize($request)
? $this->broadcastManager->authorized($request)
: $this->broadcastManager->unauthorized($request);
}
@@ -0,0 +1,20 @@
<?php
namespace Nuwave\Lighthouse\Subscriptions;
use Illuminate\Http\Request;
use Nuwave\Lighthouse\Subscriptions\Contracts\BroadcastsSubscriptions;
use Symfony\Component\HttpFoundation\Response;
class SubscriptionController
{
public function authorize(Request $request, BroadcastsSubscriptions $broadcaster): Response
{
return $broadcaster->authorize($request);
}
public function webhook(Request $request, BroadcastManager $broadcastManager): Response
{
return $broadcastManager->hook($request);
}
}
@@ -0,0 +1,40 @@
<?php
namespace Nuwave\Lighthouse\Subscriptions;
use Illuminate\Auth\GuardHelpers;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Auth\Guard;
use RuntimeException;
class SubscriptionGuard implements Guard
{
use GuardHelpers;
public const GUARD_NAME = 'lighthouse_subscriptions';
/**
* The currently authenticated user.
*
* @var \Illuminate\Contracts\Auth\Authenticatable|null
*/
protected $user;
public function user(): ?Authenticatable
{
return $this->user;
}
public function reset(): void
{
$this->user = null;
}
/**
* @param array<mixed> $credentials
*/
public function validate(array $credentials = []): bool
{
throw new RuntimeException('The Lighthouse subscription guard cannot be used for credential based authentication.');
}
}
@@ -2,17 +2,21 @@
namespace Nuwave\Lighthouse\Subscriptions;
use Illuminate\Support\Arr;
use GraphQL\Language\AST\Node;
use Nuwave\Lighthouse\GraphQL;
use Illuminate\Support\Collection;
use GraphQL\Language\AST\FieldNode;
use Nuwave\Lighthouse\Events\StartExecution;
use GraphQL\Language\AST\OperationDefinitionNode;
use GraphQL\Type\Definition\ObjectType;
use Illuminate\Contracts\Config\Repository as ConfigRepository;
use Illuminate\Support\Collection;
use Nuwave\Lighthouse\Events\BuildExtensionsResponse;
use Nuwave\Lighthouse\Events\StartExecution;
use Nuwave\Lighthouse\Exceptions\DefinitionException;
use Nuwave\Lighthouse\Execution\ExtensionsResponse;
use Nuwave\Lighthouse\Schema\SchemaBuilder;
use Nuwave\Lighthouse\Schema\Types\GraphQLSubscription;
use Nuwave\Lighthouse\Schema\Types\NotFoundSubscription;
use Nuwave\Lighthouse\Subscriptions\Contracts\ContextSerializer;
use Nuwave\Lighthouse\Subscriptions\Contracts\StoresSubscriptions;
use Nuwave\Lighthouse\Support\Utils;
class SubscriptionRegistry
{
@@ -22,47 +26,45 @@ class SubscriptionRegistry
protected $serializer;
/**
* @var \Nuwave\Lighthouse\Subscriptions\StorageManager
* @var \Nuwave\Lighthouse\Subscriptions\Contracts\StoresSubscriptions
*/
protected $storage;
/**
* @var \Nuwave\Lighthouse\GraphQL
* @var \Nuwave\Lighthouse\Schema\SchemaBuilder
*/
protected $graphQL;
protected $schemaBuilder;
/**
* @var \Illuminate\Contracts\Config\Repository
*/
protected $configRepository;
/**
* A map from operation names to channel names.
*
* @var string[]
* @var array<string, string>
*/
protected $subscribers = [];
/**
* Active subscription fields of the schema.
*
* @var \Nuwave\Lighthouse\Schema\Types\GraphQLSubscription[]
* @var array<string, \Nuwave\Lighthouse\Schema\Types\GraphQLSubscription>
*/
protected $subscriptions = [];
/**
* @param \Nuwave\Lighthouse\Subscriptions\Contracts\ContextSerializer $serializer
* @param \Nuwave\Lighthouse\Subscriptions\StorageManager $storage
* @param \Nuwave\Lighthouse\GraphQL $graphQL
* @return void
*/
public function __construct(ContextSerializer $serializer, StorageManager $storage, GraphQL $graphQL)
public function __construct(ContextSerializer $serializer, StoresSubscriptions $storage, SchemaBuilder $schemaBuilder, ConfigRepository $configRepository)
{
$this->serializer = $serializer;
$this->storage = $storage;
$this->graphQL = $graphQL;
$this->schemaBuilder = $schemaBuilder;
$this->configRepository = $configRepository;
}
/**
* Add subscription to registry.
*
* @param \Nuwave\Lighthouse\Schema\Types\GraphQLSubscription $subscription
* @param string $field
* @return $this
*/
public function register(GraphQLSubscription $subscription, string $field): self
@@ -74,33 +76,44 @@ class SubscriptionRegistry
/**
* Check if subscription is registered.
*
* @param string $key
* @return bool
*/
public function has(string $key): bool
{
return isset($this->subscriptions[$key]);
if (isset($this->subscriptions[$key])) {
return true;
}
return $this->subscriptionType()->hasField($key);
}
/**
* Get subscription keys.
*
* @return string[]
* @return array<string>
*
* @deprecated Use the `GraphQL\Type\Schema::subscriptionType()->getFieldNames()` method directly.
*/
public function keys(): array
{
return array_keys($this->subscriptions);
return $this->subscriptionType()->getFieldNames();
}
/**
* Get instance of subscription.
*
* @param string $key
* @return \Nuwave\Lighthouse\Schema\Types\GraphQLSubscription
*/
public function subscription(string $key): GraphQLSubscription
{
if (! isset($this->subscriptions[$key])) {
/**
* Loading the field has the side effect of triggering a call to.
* @see \Nuwave\Lighthouse\Support\Contracts\ProvidesSubscriptionResolver::provideSubscriptionResolver()
* which is then expected to call @see register().
*
* TODO make this more explicit and safe
*/
$this->subscriptionType()->getField($key);
}
return $this->subscriptions[$key];
}
@@ -108,16 +121,12 @@ class SubscriptionRegistry
* Add subscription to registry.
*
* @param \Nuwave\Lighthouse\Subscriptions\Subscriber $subscriber
* @param string $channel
* @return $this
*/
public function subscriber(Subscriber $subscriber, string $channel): self
public function subscriber(Subscriber $subscriber, string $topic): self
{
if ($subscriber->channel) {
$this->storage->storeSubscriber($subscriber, $channel);
}
$this->subscribers[$subscriber->operationName] = $subscriber->channel;
$this->storage->storeSubscriber($subscriber, $topic);
$this->subscribers[$subscriber->fieldName] = $subscriber->channel;
return $this;
}
@@ -126,18 +135,14 @@ class SubscriptionRegistry
* Get registered subscriptions.
*
* @param \Nuwave\Lighthouse\Subscriptions\Subscriber $subscriber
* @return \Illuminate\Support\Collection
* @return \Illuminate\Support\Collection<\Nuwave\Lighthouse\Schema\Types\GraphQLSubscription>
*/
public function subscriptions(Subscriber $subscriber): Collection
{
// A subscription can be fired w/out a request so we must make
// sure the schema has been generated.
$this->graphQL->prepSchema();
return (new Collection($subscriber->query->definitions))
->filter(function (Node $node): bool {
return $node instanceof OperationDefinitionNode;
})
->filter(
Utils::instanceofMatcher(OperationDefinitionNode::class)
)
->filter(function (OperationDefinitionNode $node): bool {
return $node->operation === 'subscription';
})
@@ -148,39 +153,65 @@ class SubscriptionRegistry
})
->all();
})
->map(function ($subscriptionField): GraphQLSubscription {
return Arr::get(
$this->subscriptions,
$subscriptionField,
new NotFoundSubscription
);
->map(function (string $subscriptionField): GraphQLSubscription {
if ($this->has($subscriptionField)) {
return $this->subscription($subscriptionField);
}
return new NotFoundSubscription;
});
}
/**
* Reset the collection of subscribers when a new execution starts.
*
* @param \Nuwave\Lighthouse\Events\StartExecution $startExecution
* @return void
*/
public function handleStartExecution(StartExecution $startExecution): void
{
$this->subscribers = [];
}
/**
* Get all current subscribers.
*
* @return \Nuwave\Lighthouse\Execution\ExtensionsResponse
*/
public function handleBuildExtensionsResponse(): ExtensionsResponse
public function handleBuildExtensionsResponse(BuildExtensionsResponse $buildExtensionsResponse): ?ExtensionsResponse
{
return new ExtensionsResponse(
'lighthouse_subscriptions',
[
'version' => 1,
'channels' => $this->subscribers,
]
);
$subscriptionsConfig = $this->configRepository->get('lighthouse.subscriptions');
$channel = count($this->subscribers) > 0
? reset($this->subscribers)
: null;
if ($channel === null && ($subscriptionsConfig['exclude_empty'] ?? false)) {
return null;
}
$version = $subscriptionsConfig['version'] ?? 1;
switch ((int) $version) {
case 1:
$content = [
'version' => 1,
'channel' => $channel,
'channels' => $this->subscribers,
];
break;
case 2:
$content = [
'version' => 2,
'channel' => $channel,
];
break;
default:
throw new DefinitionException("Expected lighthouse.subscriptions.version to be 1 or 2, got: {$version}");
}
return new ExtensionsResponse('lighthouse_subscriptions', $content);
}
protected function subscriptionType(): ObjectType
{
$subscriptionType = $this->schemaBuilder->schema()->getSubscriptionType();
if ($subscriptionType === null) {
throw new DefinitionException('Schema is missing subscription root type.');
}
return $subscriptionType;
}
}
@@ -3,17 +3,17 @@
namespace Nuwave\Lighthouse\Subscriptions;
use Closure;
use Illuminate\Support\Str;
use Nuwave\Lighthouse\Support\Utils;
use GraphQL\Type\Definition\ResolveInfo;
use Nuwave\Lighthouse\Schema\AST\ASTHelper;
use Nuwave\Lighthouse\Schema\Values\FieldValue;
use Illuminate\Support\Str;
use Nuwave\Lighthouse\Exceptions\DefinitionException;
use Nuwave\Lighthouse\Schema\AST\ASTHelper;
use Nuwave\Lighthouse\Schema\Types\GraphQLSubscription;
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
use Nuwave\Lighthouse\Schema\Directives\SubscriptionDirective;
use Nuwave\Lighthouse\Support\Contracts\ProvidesSubscriptionResolver;
use Nuwave\Lighthouse\Schema\Values\FieldValue;
use Nuwave\Lighthouse\Subscriptions\Directives\SubscriptionDirective;
use Nuwave\Lighthouse\Subscriptions\Exceptions\UnauthorizedSubscriber;
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
use Nuwave\Lighthouse\Support\Contracts\ProvidesSubscriptionResolver;
use Nuwave\Lighthouse\Support\Utils;
class SubscriptionResolverProvider implements ProvidesSubscriptionResolver
{
@@ -22,12 +22,6 @@ class SubscriptionResolverProvider implements ProvidesSubscriptionResolver
*/
protected $subscriptionRegistry;
/**
* ResolverProvider constructor.
*
* @param \Nuwave\Lighthouse\Subscriptions\SubscriptionRegistry $subscriptionRegistry
* @return void
*/
public function __construct(SubscriptionRegistry $subscriptionRegistry)
{
$this->subscriptionRegistry = $subscriptionRegistry;
@@ -36,16 +30,16 @@ class SubscriptionResolverProvider implements ProvidesSubscriptionResolver
/**
* Provide a resolver for a subscription field in case no resolver directive is defined.
*
* @param \Nuwave\Lighthouse\Schema\Values\FieldValue $fieldValue
* @return \Closure
*
* @throws \Nuwave\Lighthouse\Exceptions\DefinitionException
*
* @return \Closure(mixed, array<string, mixed>, \Nuwave\Lighthouse\Support\Contracts\GraphQLContext, \GraphQL\Type\Definition\ResolveInfo): mixed
*/
public function provideSubscriptionResolver(FieldValue $fieldValue): Closure
{
$fieldName = $fieldValue->getFieldName();
if ($directive = ASTHelper::directiveDefinition($fieldValue->getField(), SubscriptionDirective::NAME)) {
$directive = ASTHelper::directiveDefinition($fieldValue->getField(), SubscriptionDirective::NAME);
if ($directive !== null) {
$className = ASTHelper::directiveArgValue($directive, 'class');
} else {
$className = Str::studly($fieldName);
@@ -96,6 +90,8 @@ class SubscriptionResolverProvider implements ProvidesSubscriptionResolver
$subscriber,
$subscription->encodeTopic($subscriber, $fieldName)
);
return null;
};
}
}
@@ -2,15 +2,12 @@
namespace Nuwave\Lighthouse\Subscriptions;
use Nuwave\Lighthouse\Support\Http\Controllers\SubscriptionController;
class SubscriptionRouter
{
/**
* Register the routes for pusher based subscriptions.
*
* @param \Illuminate\Routing\Router $router
* @return void
* @param \Illuminate\Contracts\Routing\Registrar|\Laravel\Lumen\Routing\Router $router
*/
public function pusher($router): void
{
@@ -20,8 +17,19 @@ class SubscriptionRouter
]);
$router->post('graphql/subscriptions/webhook', [
'as' => 'lighthouse.subscriptions.auth',
'as' => 'lighthouse.subscriptions.webhook',
'uses' => SubscriptionController::class.'@webhook',
]);
}
/**
* @param \Illuminate\Contracts\Routing\Registrar|\Laravel\Lumen\Routing\Router $router
*/
public function echoRoutes($router): void
{
$router->post('graphql/subscriptions/auth', [
'as' => 'lighthouse.subscriptions.auth',
'uses' => SubscriptionController::class.'@authorize',
]);
}
}
@@ -2,37 +2,54 @@
namespace Nuwave\Lighthouse\Subscriptions;
use Illuminate\Support\Str;
use Illuminate\Support\ServiceProvider;
use Nuwave\Lighthouse\Events\StartExecution;
use Nuwave\Lighthouse\Events\BuildExtensionsResponse;
use Nuwave\Lighthouse\Subscriptions\Iterators\SyncIterator;
use Illuminate\Auth\AuthManager;
use Illuminate\Container\Container;
use Illuminate\Contracts\Config\Repository as ConfigRepository;
use Illuminate\Contracts\Events\Dispatcher as EventsDispatcher;
use Nuwave\Lighthouse\Subscriptions\Contracts\ContextSerializer;
use Nuwave\Lighthouse\Subscriptions\Contracts\StoresSubscriptions;
use Nuwave\Lighthouse\Subscriptions\Contracts\SubscriptionIterator;
use Nuwave\Lighthouse\Support\Contracts\ProvidesSubscriptionResolver;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Str;
use Nuwave\Lighthouse\Events\BuildExtensionsResponse;
use Nuwave\Lighthouse\Events\RegisterDirectiveNamespaces;
use Nuwave\Lighthouse\Events\StartExecution;
use Nuwave\Lighthouse\Subscriptions\Contracts\AuthorizesSubscriptions;
use Nuwave\Lighthouse\Subscriptions\Contracts\BroadcastsSubscriptions;
use Nuwave\Lighthouse\Subscriptions\Events\BroadcastSubscriptionEvent;
use Nuwave\Lighthouse\Subscriptions\Events\BroadcastSubscriptionListener;
use Nuwave\Lighthouse\Subscriptions\Contracts\ContextSerializer;
use Nuwave\Lighthouse\Subscriptions\Contracts\StoresSubscriptions;
use Nuwave\Lighthouse\Subscriptions\Contracts\SubscriptionExceptionHandler;
use Nuwave\Lighthouse\Subscriptions\Contracts\SubscriptionIterator;
use Nuwave\Lighthouse\Subscriptions\Iterators\AuthenticatingSyncIterator;
use Nuwave\Lighthouse\Subscriptions\Iterators\SyncIterator;
use Nuwave\Lighthouse\Subscriptions\Storage\CacheStorageManager;
use Nuwave\Lighthouse\Subscriptions\Storage\RedisStorageManager;
use Nuwave\Lighthouse\Support\Contracts\ProvidesSubscriptionResolver;
class SubscriptionServiceProvider extends ServiceProvider
{
/**
* @param \Illuminate\Contracts\Events\Dispatcher $eventsDispatcher
* @param \Illuminate\Contracts\Config\Repository $configRepository
* @return void
*/
public function register(): void
{
$this->app->singleton(BroadcastManager::class);
$this->app->singleton(SubscriptionRegistry::class);
$this->app->singleton(StoresSubscriptions::class, static function (Container $app): StoresSubscriptions {
/** @var \Illuminate\Contracts\Config\Repository $configRepository */
$configRepository = $app->make(ConfigRepository::class);
switch ($configRepository->get('lighthouse.subscriptions.storage')) {
case 'redis':
return $app->make(RedisStorageManager::class);
default:
return $app->make(CacheStorageManager::class);
}
});
$this->app->bind(ContextSerializer::class, Serializer::class);
$this->app->bind(AuthorizesSubscriptions::class, Authorizer::class);
$this->app->bind(SubscriptionIterator::class, SyncIterator::class);
$this->app->bind(SubscriptionExceptionHandler::class, ExceptionHandler::class);
$this->app->bind(BroadcastsSubscriptions::class, SubscriptionBroadcaster::class);
$this->app->bind(ProvidesSubscriptionResolver::class, SubscriptionResolverProvider::class);
}
public function boot(EventsDispatcher $eventsDispatcher, ConfigRepository $configRepository): void
{
$eventsDispatcher->listen(
BroadcastSubscriptionEvent::class,
BroadcastSubscriptionListener::class
);
$eventsDispatcher->listen(
StartExecution::class,
SubscriptionRegistry::class.'@handleStartExecution'
@@ -43,36 +60,43 @@ class SubscriptionServiceProvider extends ServiceProvider
SubscriptionRegistry::class.'@handleBuildExtensionsResponse'
);
// Register the routes for the configured broadcaster. The specific
// method that is used can be changed, so we retrieve its name
// dynamically and then call it with an instance of 'router'.
$broadcaster = $configRepository->get('lighthouse.subscriptions.broadcaster');
if ($routesMethod = $configRepository->get("lighthouse.subscriptions.broadcasters.{$broadcaster}.routes")) {
[$router, $method] = Str::parseCallback($routesMethod, 'pusher');
$eventsDispatcher->listen(
RegisterDirectiveNamespaces::class,
static function (): string {
return __NAMESPACE__.'\\Directives';
}
);
call_user_func(
[$this->app->make($router), $method],
$this->app->make('router')
);
$this->registerBroadcasterRoutes($configRepository);
// If authentication is used, we can log in subscribers when broadcasting an update
if ($this->app->bound(AuthManager::class)) {
config([
'auth.guards.'.SubscriptionGuard::GUARD_NAME => [
'driver' => SubscriptionGuard::GUARD_NAME,
],
]);
$this->app->bind(SubscriptionIterator::class, AuthenticatingSyncIterator::class);
$this->app->make(AuthManager::class)->extend(SubscriptionGuard::GUARD_NAME, static function () {
return new SubscriptionGuard;
});
}
}
/**
* Register subscription services.
*
* @return void
*/
public function register(): void
protected function registerBroadcasterRoutes(ConfigRepository $configRepository): void
{
$this->app->singleton(BroadcastManager::class);
$this->app->singleton(SubscriptionRegistry::class);
$this->app->singleton(StoresSubscriptions::class, StorageManager::class);
$broadcaster = $configRepository->get('lighthouse.subscriptions.broadcaster');
$this->app->bind(ContextSerializer::class, Serializer::class);
$this->app->bind(AuthorizesSubscriptions::class, Authorizer::class);
$this->app->bind(SubscriptionIterator::class, SyncIterator::class);
$this->app->bind(SubscriptionExceptionHandler::class, ExceptionHandler::class);
$this->app->bind(BroadcastsSubscriptions::class, SubscriptionBroadcaster::class);
$this->app->bind(ProvidesSubscriptionResolver::class, SubscriptionResolverProvider::class);
if ($routesMethod = $configRepository->get("lighthouse.subscriptions.broadcasters.{$broadcaster}.routes")) {
[$routesProviderClass, $method] = Str::parseCallback($routesMethod, 'pusher');
/** @var class-string $routesProviderClass */
/** @var string $method */
$routesProvider = $this->app->make($routesProviderClass);
$router = $this->app->make('router');
$routesProvider->{$method}($router);
}
}
}