Developing Schema
This framework adopts a code-first approach when constructing GraphQL schemas.
Usage
Exposing the GraphQL Endpoint
To expose the GraphQL API endpoint you need to use a custom Responders that forwards the request to the framewor's GraphQL handler.
php<?php namespace App\HttpResponder; use App\HttpRouteSymbol; use Distantmagic\Resonance\Attribute\RespondsToHttp; use Distantmagic\Resonance\Attribute\Singleton; use Distantmagic\Resonance\HttpResponder; use Distantmagic\Resonance\HttpResponder\GraphQL as ResonanceGraphQL; use Distantmagic\Resonance\HttpResponderInterface; use Distantmagic\Resonance\RequestMethod; use Distantmagic\Resonance\SingletonCollection; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; #[RespondsToHttp( method: RequestMethod::POST, pattern: '/graphql', routeSymbol: HttpRouteSymbol::GraphQL, )] #[Singleton(collection: SingletonCollection::HttpResponder)] final readonly class GraphQL extends HttpResponder { public function __construct(private ResonanceGraphQL $graphql) {} public function respond(ServerRequestInterface $request, ResponseInterface $response): HttpResponderInterface { return $this->graphql; } }
Authorization
You need to provide the Authorization gate for
SiteAction::
:
php<?php namespace App\SiteActionGate; use App\Role; use Distantmagic\Resonance\Attribute\DecidesSiteAction; use Distantmagic\Resonance\Attribute\Singleton; use Distantmagic\Resonance\SingletonCollection; use Distantmagic\Resonance\SiteAction; use Distantmagic\Resonance\SiteActionGate; use Distantmagic\Resonance\UserInterface; #[DecidesSiteAction(SiteAction::UseGraphQL)] #[Singleton(collection: SingletonCollection::SiteActionGate)] final readonly class UseGraphQLGate extends SiteActionGate { public function can(?UserInterface $user): bool { // Insert authorization rules here: return true; } }
Building GraphQL Root Query
To begin, prepare your root type class. It should be a singleton to ensure that the framework does not recreate it during each request.
You can build the GraphQL schema by using annotations and cascading types.
Only RootField
types are added directly into the GraphQL schema (either
queries or mutations). ObjectType
is used to extend the RootField
.
Since we are using singleton services, you do not need to implement lazy loading of types.
The lazy-loading of types won't provide any performance or memory benefits when combined with this framework, as it already constructs everything during the application bootstrap phase.
Let's start with an example type, PingType
:
php<?php use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\Type; #[Singleton] final class PingType extends ObjectType { public function __construct() { parent::__construct([ 'name' => 'Ping', 'fields' => [ 'message' => [ 'type' => Type::string(), 'resolve' => $this->resolveMessage(...), ], ], ]); } private function resolveMessage(string $rootValue): string { return $rootValue; } }
Next, create a provider that registers PingType
as the GraphQL Schema's root
query field:
You can use GraphQLRootFieldType::
to register queries and
GraphQLRootFieldType::
for mutations.
php<?php use Distantmagic\Resonance\GraphQLFieldableInterface; use Distantmagic\Resonance\GraphQLRootFieldType; #[GraphQLRootField( name: 'ping', type: GraphQLRootFieldType::Query, )] #[Singleton(collection: SingletonCollection::GraphQLRootField)] final readonly class Ping implements GraphQLFieldableInterface { public function __construct(private PingType $pingType) {} public function resolve(): string { return 'pong'; } public function toGraphQLField(): array { return [ 'type' => $this->pingType, 'resolve' => $this->resolve(...), ]; } }
Now, you should be able to query your API. The following GraphQL query:
graphqlquery Ping() { ping { message } }
Will yield this response:
json{ "data": { "ping": { "message": "pong" } } }
Object Types
Object types should extend GraphQL\
. First, they
should be registered in the Root Field, then they should be nested inside
each other:
php<?php declare(strict_types=1); namespace App\ObjectType; use Distantmagic\Resonance\Attribute\Singleton; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\Type; #[Singleton] final class PingType extends ObjectType { public function __construct() { parent::__construct([ 'name' => 'Ping', 'description' => 'Test field', 'fields' => [ 'message' => [ 'type' => Type::string(), 'description' => 'Always responds with pong', 'resolve' => $this->resolvePong(...), ], ], ]); } private function resolvePong(): string { return 'pong'; } }
Promises in Resolvers
GraphQL Resolvers
When working with GraphQL resolvers, it's a good practice to wrap asynchronous
operations like database queries or API requests in a SwooleFuture
:
php<?php use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\Type; use Distantmagic\Resonance\Attribute\Singleton; use Distantmagic\SwooleFuture\SwooleFuture; #[Singleton] final class PingType extends ObjectType { public function __construct() { parent::__construct([ 'name' => 'Ping', 'description' => 'Test field', 'fields' => [ 'message' => [ 'type' => Type::string(), 'description' => 'Always responds with pong', 'resolve' => $this->resolveMessage(...), ], ], ]); } private function resolveMessage(): SwooleFuture { return new SwooleFuture(function () { sleep(1); return 'pong'; }); } }
By using SwooleFuture
, you ensure the framework executes resolve callbacks in
parallel. That significantly improves performance, especially when multiple
resolvers are involved.
For example, the following GraphQL query will take around 1 second (instead of 3 seconds) because resolvers are executed in parallel.
graphqlquery MultiPing() { ping1: ping { message } ping2: ping { message } ping3: ping { message } }