Working with generic types
- Creating a generic type
- Creating a type that doesn't map to a DataObject
- Building a custom query
- Add a custom query for any type of data
- Adding arguments
- Add arguments to your fields, queries, and mutations
- The resolver discovery pattern
- How you can opt out of mapping fields to resolvers by adhering to naming conventions
- Adding pagination
- Add the pagination plugin to a generic query
- Adding descriptions
- Add descriptions to just about anything in your schema to improve your developer experience
- Enums, unions, and interfaces
- Add some non-object types to your schema
Building a custom query
We've defined the shape of our data, now we need a way to access it. For this,
we'll need a query. Let's add one to the queries
section of our config.
# app/_graphql/schema.yml
queries:
readCountries: '[Country]'
Resolving fields
Now we have a query that will return all the countries. In order to make this work, we'll need a resolver to tell the query where to get the data from. For this, we're going to have to break out of the configuration layer and write some PHP code.
// app/src/GraphQL/Resolver/MyResolver.php
namespace App\GraphQL\Resolver;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\i18n\Data\Locales;
class MyResolver
{
public static function resolveCountries(): array
{
$results = [];
$countries = Injector::inst()->get(Locales::class)->getCountries();
foreach ($countries as $code => $name) {
$results[] = [
'code' => $code,
'name' => $name,
];
}
return $results;
}
}
Resolvers are pretty loosely defined, and don't have to adhere to any specific contract other than that they must be static methods. You'll see why when we add it to the configuration:
# app/_graphql/schema.yml
queries:
readCountries:
type: '[Country]'
resolver: [ 'App\GraphQL\Resolver\MyResolver', 'resolveCountries' ]
Note the difference in syntax here between the type
and the resolver
- the type declaration
must have quotes around it, because we are saying "this is a list of Country
objects". The value
of this must be a YAML string. But the resolver must not be surrounded in quotes. It is explicitly
a YAML array, so that PHP recognises it as a callable
.
Now, we just have to build the schema:
vendor/bin/sake dev/graphql/build schema=default
Testing the query
Let's test this out in our GraphQL IDE. If you have the silverstripe/graphql-devtools
module installed, just go to /dev/graphql/ide
in your browser.
As you start typing, it should autocomplete for you.
Here's our query:
query {
readCountries {
name
code
}
}
And the expected response:
{
"data": {
"readCountries": [
{
"name": "Afghanistan",
"code": "af"
},
{
"name": "Åland Islands",
"code": "ax"
},
"... etc"
]
}
}
Keep in mind that plugins
don't apply in this context - at least without updating the resolver
to account for them. Most importantly this means you need to
implement your own canView()
checks. It also means you need
to add your own filter functionality, such as pagination.
Resolver method arguments
A resolver is executed in a particular query context, which is passed into the method as arguments.
mixed $value
: An optional value of the parent in your data graph. Defaults tonull
on the root level, but can be useful to retrieve the object when writing field-specific resolvers (see Resolver Discovery).array $args
: An array of optional arguments for this field (which is different from the Query Variables)array $context
: An arbitrary array which holds information shared between resolvers. Use implementors ofContextProvider
to get and set data, rather than relying on the array keys directly.?ResolveInfo
$info
: Data structure containing useful information for the resolving process (e.g. the field name). See Fetching Data in the underlying PHP library for details.
Using context providers
The $context
array can be useful to get access to the HTTP request,
retrieve the current member, or find out details about the schema.
You can use it through implementors of the ContextProvider
interface.
In the example below, we'll demonstrate how you could limit viewing the country code to
users with ADMIN permissions.
// app/src/GraphQL/Resolver/MyResolver.php
namespace App\GraphQL\Resolver;
use GraphQL\Type\Definition\ResolveInfo;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\GraphQL\QueryHandler\UserContextProvider;
use SilverStripe\Security\Permission;
use SilverStripe\i18n\Data\Locales;
class MyResolver
{
public static function resolveCountries(
mixed $value = null,
array $args = [],
array $context = [],
?ResolveInfo $info = null
): array {
$member = UserContextProvider::get($context);
$canViewCode = ($member && Permission::checkMember($member, 'ADMIN'));
$results = [];
$countries = Injector::inst()->get(Locales::class)->getCountries();
foreach ($countries as $code => $name) {
$results[] = [
'code' => $canViewCode ? $code : '',
'name' => $name,
];
}
return $results;
}
}
Resolver discovery
This is great, but as we write more and more queries for types with more and more fields, it's going to get awfully laborious mapping all these resolvers. Let's clean this up a bit by adding a bit of convention over configuration, and save ourselves a lot of time to boot. We can do that using the resolver discovery pattern.