silverstripe-framework/docs/en/02_Developer_Guides/19_GraphQL/03_working_with_generic_types/03_resolver_discovery.md

4.3 KiB

title summary
The resolver discovery pattern How you can opt out of mapping fields to resolvers by adhering to naming conventions

Working with generic types

[CHILDREN asList]

[alert] 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 [/alert]

The resolver discovery pattern

When you define a query mutation, or any other field on a type, you can opt out of providing an explicit resolver and allow the system to discover one for you based on naming convention.

Let's start by registering a resolver class(es) where we can define a bunch of these functions.

app/_graphql/config.yml

resolvers:
  - MyProject\Resolvers\MyResolvers

What we're registering here is a generic class that should contain one or more static functions that resolve one or many fields. How those functions will be discovered relies on the resolver strategy.

Resolver strategy

Each schema config accepts a resolverStrategy property. This should map to a callable that will return a method name given a class name, type name, and Field instance.

public static function getResolverMethod(string $className, ?string $typeName = null, ?Field $field = null): ?string;

The default resolver strategy

By default, all schemas use SilverStripe\GraphQL\Schema\Resolver\DefaultResolverStrategy::getResolerMethod to discover resolver functions. The logic works like this:

  • Does resolve<TypeName><FieldName> exist?
    • Yes? Invoke
    • No? Continue
  • Does resolve<TypeName> exist?
    • Yes? Invoke
    • No? Continue
  • Does resolve<FieldName> exist?
    • Yes? Invoke
    • No? Continue
  • Does resolve exist?
    • Yes? Invoke
    • No? Return null. This resolver cannot be discovered

Let's look at our query again:

query {
  readCountries {
    name
  }
}

Imagine we have two classes registered under resolvers -- ClassA and ClassB

The logic will go like this:

  • ClassA::resolveCountryName
  • ClassA::resolveCountry
  • ClassA::resolveName
  • ClassA::resolve
  • ClassB::resolveCountryName
  • ClassB::resolveCountry
  • ClassB::resolveName
  • ClassB::resolve
  • Return null.

You can implement whatever strategy you like in your schema. Just register it to resolverStrategy in the config.

app/_graphql/config.yml

resolverStrategy: [ 'MyApp\Resolvers\Strategy', 'getResolverMethod' ]

Let's add a resolver method to our resolver provider:

app/src/Resolvers/MyResolvers.php


class MyResolvers
{
    public static function resolveReadCountries()
    {
        $results = [];
        $countries = Injector::inst()->get(Locales::class)->getCountries();
        foreach ($countries as $code => $name) {
            $results[] = [
                'code' => $code,
                'name' => $name
            ];
        }

        return $results;
    }
}

Now that we're using logic to discover our resolver, we can clean up the config a bit.

app/_graphql/schema.yml

  queries:
    readCountries: '[Country]'

Re-run the schema build, with a flush, and let's go!

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

Field resolvers

A less magical approach to resolver discovery is defining a fieldResolver property on your types. This is a generic handler for all fields on a given type and can be a nice middle ground between the rigor of hard coding everything and the opacity of discovery logic.

app/_graphql/schema.yml

  types:
    Country:
      fields:
        name: String
        code: String
      fieldResolver: [ 'MyProject\MyResolver', 'resolveCountryField' ]

You'll need to do explicit checks for the fieldName in your resolver to make this work.

public static function resolveCountryField($obj, $args, $context, ResolveInfo $info)
{
    $fieldName = $info->fieldName;
    if ($fieldName === 'image') {
        return $obj->getImage()->getURL();
    }
    // .. etc
}

Further reading

[CHILDREN]