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
The resolver discovery pattern
How you can opt out of mapping fields to resolvers by adhering to naming conventions
Adding arguments
Add arguments to your fields, queries, and mutations
Adding pagination
Add the pagination plugin to a generic query
Enums, unions, and interfaces
Add some non-object types to your schema
Adding descriptions
Add descriptions to just about anything in your schema to improve your developer experience
You are viewing docs for a pre-release version of silverstripe/graphql (4.x). Help us improve it by joining #graphql on the Community Slack, and report any issues at github.com/silverstripe/silverstripe-graphql. Docs for the current stable version (3.x) can be found here

Building a custom query

We've now defined the shape of our data, now we need to build 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

types:
  Country:
    fields:
      code: String!
      name: String!
queries:
  readCountries: '[Country]'

Now we have a query that will return all the countries. In order to make this work, we'll need a resolver. For this, we're going to have to break out of the configuration layer and write some code.

app/src/Resolvers/MyResolver.php

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

  types:
    Country:
      fields:
        code: String!
        name: String!
  queries:
    readCountries:
      type: '[Country]'
      resolver: [ 'MyResolver', 'resolveCountries' ]

Now, we just have to build the schema:

$ vendor/bin/sake dev/graphql/build schema=default

Let's test this out in our GraphQL IDE. If you have the graphql-devtools module installed, just open it up and set it to the /graphql endpoint.

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. Most importantly, this means you need to implement your own canView() checks.

Resolver Method Arguments

A resolver is executed in a particular query context, which is passed into the method as arguments.

  • $value: An optional mixed value of the parent in your data graph. Defaults to null on the root level, but can be useful to retrieve the object when writing field-specific resolvers (see Resolver Discovery)
  • $args: An array of optional arguments for this field (which is different from the Query Variables)
  • $context: An arbitrary array which holds information shared between resolvers. Use implementors of SilverStripe\GraphQL\Schema\Interfaces\ContextProvider to get and set data, rather than relying on the array keys directly.
  • $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 SilverStripe\GraphQL\Schema\Interfaces\ContextProvider interface. In the example below, we'll demonstrate how you could limit viewing the country code to users with ADMIN permissions.

app/src/Resolvers/MyResolver.php

use GraphQL\Type\Definition\ResolveInfo;
use SilverStripe\GraphQL\QueryHandler\UserContextProvider;
use SilverStripe\Security\Permission;

class MyResolver
{
    public static function resolveCountries($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.

Further reading

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
The resolver discovery pattern
How you can opt out of mapping fields to resolvers by adhering to naming conventions
Adding arguments
Add arguments to your fields, queries, and mutations
Adding pagination
Add the pagination plugin to a generic query
Enums, unions, and interfaces
Add some non-object types to your schema
Adding descriptions
Add descriptions to just about anything in your schema to improve your developer experience