-
-
Notifications
You must be signed in to change notification settings - Fork 570
Description
Hey.
Currently, webonyx/graphql-php
validates the resolved subscription field value against field type, even though a subscription isn't required to return an "initial" root item:
use GraphQL\GraphQL;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Schema;
use GraphQL\Type\SchemaConfig;
class Subscription
{
}
class User
{
}
$subscriptionType = new ObjectType([
'name' => 'Subscription',
'fields' => [
'newUser' => [
'type' => Type::nonNull(new ObjectType([
'name' => 'User',
'fields' => [
'name' => [
'type' => Type::string(),
]
],
'isTypeOf' => fn(mixed $value) => $value instanceof User,
])),
'resolve' => function () {
// ... store the subscription somewhere (in-memory, redis etc)
return new Subscription();
},
],
],
]);
$schema = new Schema(
(new SchemaConfig())
->setSubscription($subscriptionType)
);
// Errors: Expected value of type "User" but got: instance of Subscription.
$result = GraphQL::executeQuery($schema, 'subscription { newUser { name } }');
$output = $result->toArray();
In this case, a subscription is expected to be "emitting" instances of User
type and class. If it were a query or a mutation, it would be expected that resolve
returns a single resolved item of type User
, and the error would be correct.
However, in case of subscriptions, they aren't expected to instantly resolve an item - it can be delayed indefinitely, and the first (initial) response might be something entirely different. It would be useful if the resolve
method would be able to return an arbitrary value (null
or any object) that could be used further down the line to generate an HTTP response:
// For demonstration purposes. Currently wouldn't work because $data gets converted to an array
$subscription = $result->data['newUser'];
assert($subscription instanceof Subscription);
header('Location: ' . $subscription->redirectUri());
or use it for testing purposes:
// For demonstration purposes. Currently wouldn't work because $data gets converted to an array
$subscription = $result->data['newUser'];
assert($subscription instanceof Subscription);
$subscriptionDataSender->send($subscription, new User('Alex'));
$subscription->assertDataSent([
'name' => 'Alex',
]);
One example of this is Lighthouse. In their docs, they suggest creating subscriptions like so:
type Subscription {
postUpdated(author: ID): Post
}
Field's type being nullable allows them to partly work around the issue by returning null
from the resolver:
However, a nullable field type suggests that a client may receive null
, when in fact the resolver for each item suggests that it cannot:
final class PostUpdated extends GraphQLSubscription
{
//
// |
// |
// ↓
public function resolve(mixed $root, array $args, GraphQLContext $context, ResolveInfo $resolveInfo): Post
{
// Optionally manipulate the `$root` item before it gets broadcasted to
// subscribed client(s).
$root->load(['author', 'author.achievements']);
return $root;
}
}
So users are forced to mark subscription field types as nullable even when they are not. Without changes to webonyx/graphql-php
, there's no way to fix this.
Another workaround that I've come up with is throwing an exception:
$subscriptionType = new ObjectType([
'name' => 'Subscription',
'fields' => [
'newUser' => [
'type' => Type::nonNull(new ObjectType([
'name' => 'User',
'fields' => [
'name' => [
'type' => Type::string(),
]
],
'isTypeOf' => fn(mixed $value) => $value instanceof User,
])),
'resolve' => function () {
// ... store the subscription somewhere (in-memory, redis etc)
throw new SubscribedException($subscription);
},
],
],
]);
$result = GraphQL::executeQuery($schema, 'subscription { newUser { name } }');
assert(count($result->errors) === 1);
assert($result->errors[0]->getPrevious() instanceof SubscribedException);
$subscription = $result->errors[0]->getPrevious()->subscription;
But this is also very hacky, and suggests that something went wrong, when in fact the query was executed successfully and a subscription was created.
I believe there should be a documented and flexible way of implementing this, without forcing nullable types or throwing exceptions, as both are suboptimal.
Maybe a special interface like interface Subscription {}
that webonyx/graphql-php
would internally check for, and treat it as a special kind of resolved value, like Deferred
is treated? What do you think?