This commit is contained in:
Kilian Hofmann
2021-06-01 19:55:55 +02:00
parent 052cbe3038
commit d8c489c714
328 changed files with 36645 additions and 0 deletions
@@ -0,0 +1,88 @@
<?php
namespace Nuwave\Lighthouse\Subscriptions;
use Exception;
use Illuminate\Http\Request;
use Nuwave\Lighthouse\Schema\Types\GraphQLSubscription;
use Nuwave\Lighthouse\Subscriptions\Contracts\StoresSubscriptions;
use Nuwave\Lighthouse\Subscriptions\Contracts\AuthorizesSubscriptions;
use Nuwave\Lighthouse\Subscriptions\Contracts\SubscriptionExceptionHandler;
class Authorizer implements AuthorizesSubscriptions
{
/**
* @var \Nuwave\Lighthouse\Subscriptions\Contracts\StoresSubscriptions
*/
protected $storage;
/**
* @var \Nuwave\Lighthouse\Subscriptions\SubscriptionRegistry
*/
protected $registry;
/**
* @var \Nuwave\Lighthouse\Subscriptions\Contracts\SubscriptionExceptionHandler
*/
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,
SubscriptionExceptionHandler $exceptionHandler
) {
$this->storage = $storage;
$this->registry = $registry;
$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()
);
if (! $subscriber) {
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);
}
);
if (! $authorized) {
$this->storage->deleteSubscriber($subscriber->channel);
}
return $authorized;
} catch (Exception $e) {
$this->exceptionHandler->handleAuthError($e);
return false;
}
}
}
@@ -0,0 +1,87 @@
<?php
namespace Nuwave\Lighthouse\Subscriptions;
use Pusher\Pusher;
use RuntimeException;
use Illuminate\Support\Arr;
use Nuwave\Lighthouse\Support\DriverManager;
use Nuwave\Lighthouse\Subscriptions\Contracts\Broadcaster;
use Nuwave\Lighthouse\Subscriptions\Broadcasters\LogBroadcaster;
use Nuwave\Lighthouse\Subscriptions\Broadcasters\PusherBroadcaster;
/**
* @method void broadcast(\Nuwave\Lighthouse\Subscriptions\Subscriber $subscriber, array $data)
* @method \Symfony\Component\HttpFoundation\Response hook(\Illuminate\Http\Request $request)
* @method \Symfony\Component\HttpFoundation\Response authorized(\Illuminate\Http\Request $request)
* @method \Symfony\Component\HttpFoundation\Response unauthorized(\Illuminate\Http\Request $request)
*/
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
*/
protected function createPusherDriver(array $config): PusherBroadcaster
{
$connection = $config['connection'] ?? 'pusher';
$driverConfig = config("broadcasting.connections.{$connection}");
if (empty($driverConfig) || $driverConfig['driver'] !== 'pusher') {
throw new RuntimeException("Could not initialize Pusher broadcast driver for connection: {$connection}.");
}
$appKey = Arr::get($driverConfig, 'key');
$appSecret = Arr::get($driverConfig, 'secret');
$appId = Arr::get($driverConfig, 'app_id');
$options = Arr::get($driverConfig, 'options', []);
$pusher = new Pusher($appKey, $appSecret, $appId, $options);
return new PusherBroadcaster($pusher);
}
/**
* Create instance of log driver.
*
* @param mixed[] $config
* @return \Nuwave\Lighthouse\Subscriptions\Broadcasters\LogBroadcaster
*/
protected function createLogDriver(array $config): LogBroadcaster
{
return new LogBroadcaster($config);
}
}
@@ -0,0 +1,101 @@
<?php
namespace Nuwave\Lighthouse\Subscriptions\Broadcasters;
use Illuminate\Support\Arr;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Nuwave\Lighthouse\Subscriptions\Subscriber;
use Nuwave\Lighthouse\Subscriptions\Contracts\Broadcaster;
class LogBroadcaster implements Broadcaster
{
/**
* The user-defined configuration options.
*
* @var mixed[]
*/
protected $config = [];
/**
* A map from channel names to data.
*
* @var mixed
*/
protected $broadcasts = [];
/**
* @param array $config
* @return void
*/
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);
}
/**
* 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);
}
/**
* Handle subscription web hook.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function hook(Request $request): JsonResponse
{
return response()->json(['message' => 'okay']);
}
/**
* Send data to subscriber.
*
* @param \Nuwave\Lighthouse\Subscriptions\Subscriber $subscriber
* @param array $data
* @return void
*/
public function broadcast(Subscriber $subscriber, array $data): void
{
$this->broadcasts[$subscriber->channel] = $data;
}
/**
* Get the data that is being broadcast.
*
* @param string|null $key
* @return array|null
*/
public function broadcasts(?string $key = null): ?array
{
return Arr::get($this->broadcasts, $key);
}
/**
* Get configuration options.
*
* @return mixed[]
*/
public function config(): array
{
return $this->config;
}
}
@@ -0,0 +1,107 @@
<?php
namespace Nuwave\Lighthouse\Subscriptions\Broadcasters;
use Illuminate\Support\Arr;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Collection;
use Nuwave\Lighthouse\Subscriptions\Subscriber;
use Nuwave\Lighthouse\Subscriptions\Contracts\Broadcaster;
use Nuwave\Lighthouse\Subscriptions\Contracts\StoresSubscriptions;
class PusherBroadcaster implements Broadcaster
{
const EVENT_NAME = 'lighthouse-subscription';
/**
* @var \Pusher\Pusher
*/
protected $pusher;
/**
* @var \Nuwave\Lighthouse\Subscriptions\Contracts\StoresSubscriptions
*/
protected $storage;
/**
* Create instance of pusher broadcaster.
*
* @param \Pusher\Pusher $pusher
* @return void
*/
public function __construct($pusher)
{
$this->pusher = $pusher;
$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(
$this->pusher->socket_auth($channel, $socketId),
true
);
return response()->json($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);
}
/**
* 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')
);
});
return response()->json(['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
{
$this->pusher->trigger(
$subscriber->channel,
self::EVENT_NAME,
[
'more' => true,
'result' => $data,
]
);
}
}
@@ -0,0 +1,16 @@
<?php
namespace Nuwave\Lighthouse\Subscriptions\Contracts;
use Illuminate\Http\Request;
interface AuthorizesSubscriptions
{
/**
* Authorize subscription request.
*
* @param \Illuminate\Http\Request $request
* @return bool
*/
public function authorize(Request $request);
}
@@ -0,0 +1,42 @@
<?php
namespace Nuwave\Lighthouse\Subscriptions\Contracts;
use Illuminate\Http\Request;
use Nuwave\Lighthouse\Subscriptions\Subscriber;
interface Broadcaster
{
/**
* Handle authorized subscription request.
*
* @param \Illuminate\Http\Request $request
* @return \Symfony\Component\HttpFoundation\Response
*/
public function authorized(Request $request);
/**
* Handle unauthorized subscription request.
*
* @param \Illuminate\Http\Request $request
* @return \Symfony\Component\HttpFoundation\Response
*/
public function unauthorized(Request $request);
/**
* Handle subscription web hook.
*
* @param \Illuminate\Http\Request $request
* @return \Symfony\Component\HttpFoundation\Response
*/
public function hook(Request $request);
/**
* Send data to subscriber.
*
* @param \Nuwave\Lighthouse\Subscriptions\Subscriber $subscriber
* @param mixed[] $data
* @return void
*/
public function broadcast(Subscriber $subscriber, array $data);
}
@@ -0,0 +1,36 @@
<?php
namespace Nuwave\Lighthouse\Subscriptions\Contracts;
use Illuminate\Http\Request;
use Nuwave\Lighthouse\Schema\Types\GraphQLSubscription;
interface BroadcastsSubscriptions
{
/**
* Push subscription data to subscribers.
*
* @param \Nuwave\Lighthouse\Schema\Types\GraphQLSubscription $subscription
* @param string $fieldName
* @param mixed $root
*/
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);
/**
* Authorize the subscription.
*
* @param \Illuminate\Http\Request $request
* @return \Symfony\Component\HttpFoundation\Response
*/
public function authorize(Request $request);
}
@@ -0,0 +1,24 @@
<?php
namespace Nuwave\Lighthouse\Subscriptions\Contracts;
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
interface ContextSerializer
{
/**
* Serialize the context.
*
* @param \Nuwave\Lighthouse\Support\Contracts\GraphQLContext $context
* @return string
*/
public function serialize(GraphQLContext $context);
/**
* Unserialize the context.
*
* @param string $context
* @return \Nuwave\Lighthouse\Support\Contracts\GraphQLContext
*/
public function unserialize(string $context);
}
@@ -0,0 +1,50 @@
<?php
namespace Nuwave\Lighthouse\Subscriptions\Contracts;
use Nuwave\Lighthouse\Subscriptions\Subscriber;
interface StoresSubscriptions
{
/**
* Get subscriber by request.
*
* @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.
*
* @param string $topic
* @return \Illuminate\Support\Collection<\Nuwave\Lighthouse\Subscriptions\Subscriber>
*/
public function subscribersByTopic(string $topic);
/**
* Store subscription.
*
* @param \Nuwave\Lighthouse\Subscriptions\Subscriber $subscriber
* @param string $topic
* @return void
*/
public function storeSubscriber(Subscriber $subscriber, string $topic);
/**
* Delete subscriber.
*
* @param string $channel
* @return \Nuwave\Lighthouse\Subscriptions\Subscriber|null
*/
public function deleteSubscriber(string $channel);
}
@@ -0,0 +1,24 @@
<?php
namespace Nuwave\Lighthouse\Subscriptions\Contracts;
use Throwable;
interface SubscriptionExceptionHandler
{
/**
* Handle authentication error.
*
* @param \Throwable $e
* @return void
*/
public function handleAuthError(Throwable $e);
/**
* Handle broadcast error.
*
* @param \Throwable $e
* @return void
*/
public function handleBroadcastError(Throwable $e);
}
@@ -0,0 +1,19 @@
<?php
namespace Nuwave\Lighthouse\Subscriptions\Contracts;
use Closure;
use Illuminate\Support\Collection;
interface 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);
}
@@ -0,0 +1,39 @@
<?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;
}
}
@@ -0,0 +1,38 @@
<?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,34 @@
<?php
namespace Nuwave\Lighthouse\Subscriptions;
use Throwable;
use Nuwave\Lighthouse\Subscriptions\Contracts\SubscriptionExceptionHandler;
class ExceptionHandler implements SubscriptionExceptionHandler
{
/**
* Handle authentication error.
*
* @param \Throwable $e
* @return void
*/
public function handleAuthError(Throwable $e): void
{
// Do nothing....
}
/**
* Handle broadcast error.
*
* @param \Throwable $e
* @return void
*/
public function handleBroadcastError(Throwable $e): void
{
info('graphql.broadcast.exception', [
'message' => $e->getMessage(),
'stack' => $e->getTrace(),
]);
}
}
@@ -0,0 +1,10 @@
<?php
namespace Nuwave\Lighthouse\Subscriptions\Exceptions;
use GraphQL\Error\Error;
class UnauthorizedSubscriber extends Error
{
//
}
@@ -0,0 +1,34 @@
<?php
namespace Nuwave\Lighthouse\Subscriptions\Iterators;
use Closure;
use Exception;
use Illuminate\Support\Collection;
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
{
$items->each(function ($item) use ($cb, $error): void {
try {
$cb($item);
} catch (Exception $e) {
if (! $error) {
throw $e;
}
$error($e);
}
});
}
}
@@ -0,0 +1,82 @@
<?php
namespace Nuwave\Lighthouse\Subscriptions;
use Illuminate\Support\Arr;
use Illuminate\Http\Request;
use Nuwave\Lighthouse\Support\Contracts\CreatesContext;
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
use Nuwave\Lighthouse\Subscriptions\Contracts\ContextSerializer;
class Serializer implements ContextSerializer
{
/**
* @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();
return serialize([
'request' => [
'query' => $request->query->all(),
'request' => $request->request->all(),
'attributes' => $request->attributes->all(),
'cookies' => [],
'files' => [],
'server' => Arr::except($request->server->all(), ['HTTP_AUTHORIZATION']),
'content' => $request->getContent(),
],
'user' => serialize($context->user()),
]);
}
/**
* Unserialize the context.
*
* @param string $context
* @return \Nuwave\Lighthouse\Support\Contracts\GraphQLContext
*/
public function unserialize(string $context): GraphQLContext
{
[
'request' => $rawRequest,
'user' => $rawUser
] = unserialize($context);
$request = new Request(
$rawRequest['query'],
$rawRequest['request'],
$rawRequest['attributes'],
$rawRequest['cookies'],
$rawRequest['files'],
$rawRequest['server'],
$rawRequest['content']
);
$request->setUserResolver(
function () use ($rawUser) {
return unserialize($rawUser);
}
);
return $this->createsContext->generate($request);
}
}
@@ -0,0 +1,136 @@
<?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;
}
}
@@ -0,0 +1,150 @@
<?php
namespace Nuwave\Lighthouse\Subscriptions;
use Serializable;
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;
class Subscriber implements Serializable
{
const MISSING_OPERATION_NAME = 'Must pass an operation name when using a subscription.';
/**
* @var string
*/
public $channel;
/**
* @var mixed
*/
public $root;
/**
* @var array
*/
public $args;
/**
* @var mixed
*/
public $context;
/**
* @var \GraphQL\Language\AST\DocumentNode
*/
public $query;
/**
* @var string
*/
public $operationName;
/**
* @param mixed[] $args
* @param \Nuwave\Lighthouse\Support\Contracts\GraphQLContext $context
* @param \GraphQL\Type\Definition\ResolveInfo $resolveInfo
* @return void
*
* @throws \Nuwave\Lighthouse\Exceptions\SubscriptionException
*/
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->channel = self::uniqueChannelName();
$this->args = $args;
$this->context = $context;
$documentNode = new DocumentNode([]);
$documentNode->definitions = $resolveInfo->fragments;
$documentNode->definitions[] = $resolveInfo->operation;
$this->query = $documentNode;
}
/**
* Unserialize subscription from a JSON string.
*
* @param string $subscription
* @return $this
*/
public function unserialize($subscription): self
{
$data = json_decode($subscription, true);
$this->operationName = $data['operation_name'];
$this->channel = $data['channel'];
$this->args = $data['args'];
$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()
{
return json_encode([
'operation_name' => $this->operationName,
'channel' => $this->channel,
'args' => $this->args,
'context' => $this->contextSerializer()->serialize($this->context),
'query' => serialize(
AST::toArray($this->query)
),
]);
}
/**
* Set root data.
*
* @param mixed $root
* @return $this
*/
public function setRoot($root): self
{
$this->root = $root;
return $this;
}
/**
* 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);
}
}
@@ -0,0 +1,136 @@
<?php
namespace Nuwave\Lighthouse\Subscriptions;
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;
class SubscriptionBroadcaster implements BroadcastsSubscriptions
{
/**
* @var \Nuwave\Lighthouse\GraphQL
*/
protected $graphQL;
/**
* @var \Nuwave\Lighthouse\Subscriptions\Contracts\AuthorizesSubscriptions
*/
protected $auth;
/**
* @var \Nuwave\Lighthouse\Subscriptions\StorageManager
*/
protected $storage;
/**
* @var \Nuwave\Lighthouse\Subscriptions\Contracts\SubscriptionIterator
*/
protected $iterator;
/**
* @var \Nuwave\Lighthouse\Subscriptions\BroadcastManager
*/
protected $broadcastManager;
/**
* @var \Illuminate\Contracts\Events\Dispatcher
*/
protected $eventsDispatcher;
/**
* @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,
BroadcastManager $broadcastManager,
EventsDispatcher $eventsDispatcher
) {
$this->graphQL = $graphQL;
$this->auth = $auth;
$this->storage = $storage;
$this->iterator = $iterator;
$this->broadcastManager = $broadcastManager;
$this->eventsDispatcher = $eventsDispatcher;
}
/**
* 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)
);
}
/**
* 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
->subscribersByTopic($topic)
->filter(function (Subscriber $subscriber) use ($subscription, $root): bool {
return $subscription->filter($subscriber, $root);
});
$this->iterator->process(
$subscribers,
function (Subscriber $subscriber) use ($root): void {
$data = $this->graphQL->executeQuery(
$subscriber->query,
$subscriber->context,
$subscriber->args,
$subscriber->setRoot($root),
$subscriber->operationName
);
$this->broadcastManager->broadcast(
$subscriber,
$data->jsonSerialize()
);
}
);
}
/**
* Authorize the subscription.
*
* @param \Illuminate\Http\Request $request
* @return \Symfony\Component\HttpFoundation\Response
*/
public function authorize(Request $request): Response
{
return $this->auth->authorize($request)
? $this->broadcastManager->authorized($request)
: $this->broadcastManager->unauthorized($request);
}
}
@@ -0,0 +1,186 @@
<?php
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 Nuwave\Lighthouse\Execution\ExtensionsResponse;
use Nuwave\Lighthouse\Schema\Types\GraphQLSubscription;
use Nuwave\Lighthouse\Schema\Types\NotFoundSubscription;
use Nuwave\Lighthouse\Subscriptions\Contracts\ContextSerializer;
class SubscriptionRegistry
{
/**
* @var \Nuwave\Lighthouse\Subscriptions\Contracts\ContextSerializer
*/
protected $serializer;
/**
* @var \Nuwave\Lighthouse\Subscriptions\StorageManager
*/
protected $storage;
/**
* @var \Nuwave\Lighthouse\GraphQL
*/
protected $graphQL;
/**
* A map from operation names to channel names.
*
* @var string[]
*/
protected $subscribers = [];
/**
* Active subscription fields of the schema.
*
* @var \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)
{
$this->serializer = $serializer;
$this->storage = $storage;
$this->graphQL = $graphQL;
}
/**
* Add subscription to registry.
*
* @param \Nuwave\Lighthouse\Schema\Types\GraphQLSubscription $subscription
* @param string $field
* @return $this
*/
public function register(GraphQLSubscription $subscription, string $field): self
{
$this->subscriptions[$field] = $subscription;
return $this;
}
/**
* Check if subscription is registered.
*
* @param string $key
* @return bool
*/
public function has(string $key): bool
{
return isset($this->subscriptions[$key]);
}
/**
* Get subscription keys.
*
* @return string[]
*/
public function keys(): array
{
return array_keys($this->subscriptions);
}
/**
* Get instance of subscription.
*
* @param string $key
* @return \Nuwave\Lighthouse\Schema\Types\GraphQLSubscription
*/
public function subscription(string $key): GraphQLSubscription
{
return $this->subscriptions[$key];
}
/**
* Add subscription to registry.
*
* @param \Nuwave\Lighthouse\Subscriptions\Subscriber $subscriber
* @param string $channel
* @return $this
*/
public function subscriber(Subscriber $subscriber, string $channel): self
{
if ($subscriber->channel) {
$this->storage->storeSubscriber($subscriber, $channel);
}
$this->subscribers[$subscriber->operationName] = $subscriber->channel;
return $this;
}
/**
* Get registered subscriptions.
*
* @param \Nuwave\Lighthouse\Subscriptions\Subscriber $subscriber
* @return \Illuminate\Support\Collection
*/
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(function (OperationDefinitionNode $node): bool {
return $node->operation === 'subscription';
})
->flatMap(function (OperationDefinitionNode $node): array {
return (new Collection($node->selectionSet->selections))
->map(function (FieldNode $field): string {
return $field->name->value;
})
->all();
})
->map(function ($subscriptionField): GraphQLSubscription {
return Arr::get(
$this->subscriptions,
$subscriptionField,
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
{
return new ExtensionsResponse(
'lighthouse_subscriptions',
[
'version' => 1,
'channels' => $this->subscribers,
]
);
}
}
@@ -0,0 +1,101 @@
<?php
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 Nuwave\Lighthouse\Exceptions\DefinitionException;
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\Subscriptions\Exceptions\UnauthorizedSubscriber;
class SubscriptionResolverProvider implements ProvidesSubscriptionResolver
{
/**
* @var \Nuwave\Lighthouse\Subscriptions\SubscriptionRegistry
*/
protected $subscriptionRegistry;
/**
* ResolverProvider constructor.
*
* @param \Nuwave\Lighthouse\Subscriptions\SubscriptionRegistry $subscriptionRegistry
* @return void
*/
public function __construct(SubscriptionRegistry $subscriptionRegistry)
{
$this->subscriptionRegistry = $subscriptionRegistry;
}
/**
* 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
*/
public function provideSubscriptionResolver(FieldValue $fieldValue): Closure
{
$fieldName = $fieldValue->getFieldName();
if ($directive = ASTHelper::directiveDefinition($fieldValue->getField(), SubscriptionDirective::NAME)) {
$className = ASTHelper::directiveArgValue($directive, 'class');
} else {
$className = Str::studly($fieldName);
}
$className = Utils::namespaceClassname(
$className,
$fieldValue->defaultNamespacesForParent(),
function (string $class): bool {
return is_subclass_of($class, GraphQLSubscription::class);
}
);
if (! $className) {
throw new DefinitionException(
"No class found for the subscription field {$fieldName}"
);
}
/** @var \Nuwave\Lighthouse\Schema\Types\GraphQLSubscription $subscription */
$subscription = app($className);
// Subscriptions can only be placed on a single field on the root
// query, so there is no need to consider the field path
$this->subscriptionRegistry->register(
$subscription,
$fieldName
);
return function ($root, array $args, GraphQLContext $context, ResolveInfo $resolveInfo) use ($subscription, $fieldName) {
if ($root instanceof Subscriber) {
return $subscription->resolve($root->root, $args, $context, $resolveInfo);
}
$subscriber = new Subscriber(
$args,
$context,
$resolveInfo
);
if (! $subscription->can($subscriber)) {
throw new UnauthorizedSubscriber(
'Unauthorized subscription request'
);
}
$this->subscriptionRegistry->subscriber(
$subscriber,
$subscription->encodeTopic($subscriber, $fieldName)
);
};
}
}
@@ -0,0 +1,27 @@
<?php
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
*/
public function pusher($router): void
{
$router->post('graphql/subscriptions/auth', [
'as' => 'lighthouse.subscriptions.auth',
'uses' => SubscriptionController::class.'@authorize',
]);
$router->post('graphql/subscriptions/webhook', [
'as' => 'lighthouse.subscriptions.auth',
'uses' => SubscriptionController::class.'@webhook',
]);
}
}
@@ -0,0 +1,78 @@
<?php
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\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 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\SubscriptionExceptionHandler;
class SubscriptionServiceProvider extends ServiceProvider
{
/**
* @param \Illuminate\Contracts\Events\Dispatcher $eventsDispatcher
* @param \Illuminate\Contracts\Config\Repository $configRepository
* @return void
*/
public function boot(EventsDispatcher $eventsDispatcher, ConfigRepository $configRepository): void
{
$eventsDispatcher->listen(
BroadcastSubscriptionEvent::class,
BroadcastSubscriptionListener::class
);
$eventsDispatcher->listen(
StartExecution::class,
SubscriptionRegistry::class.'@handleStartExecution'
);
$eventsDispatcher->listen(
BuildExtensionsResponse::class,
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');
call_user_func(
[$this->app->make($router), $method],
$this->app->make('router')
);
}
}
/**
* Register subscription services.
*
* @return void
*/
public function register(): void
{
$this->app->singleton(BroadcastManager::class);
$this->app->singleton(SubscriptionRegistry::class);
$this->app->singleton(StoresSubscriptions::class, StorageManager::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);
}
}