mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
DOCS: Document new schema config, change to resolver discovery pattern (#9781)
This commit is contained in:
parent
91c441103b
commit
fe972d62d5
@ -49,6 +49,8 @@ Let's populate a schema that is pre-configured for us out of the box, `default`.
|
||||
SilverStripe\GraphQL\Schema\Schema:
|
||||
schemas:
|
||||
default:
|
||||
config:
|
||||
# general schema config here
|
||||
types:
|
||||
# your generic types here
|
||||
models:
|
||||
@ -79,7 +81,10 @@ SilverStripe\GraphQL\Schema\Schema:
|
||||
src: app/_graphql
|
||||
```
|
||||
|
||||
It can also be an array of directories.
|
||||
[info]
|
||||
It is recommended that you define your sources as an array so that further source files are merged.
|
||||
Otherwise, another config file could completely override part of your schema definition.
|
||||
[/info]
|
||||
|
||||
**app/_config/graphql.yml**
|
||||
```yml
|
||||
@ -87,8 +92,8 @@ SilverStripe\GraphQL\Schema\Schema:
|
||||
schemas:
|
||||
default:
|
||||
src:
|
||||
myDir: app/_graphql
|
||||
myOtherDir: module/_graphql
|
||||
- app/_graphql
|
||||
- module/_graphql
|
||||
```
|
||||
|
||||
[info]
|
||||
@ -107,6 +112,8 @@ This doesn't mean there is never a need to flush your schema config. If you were
|
||||
**app/_graphql/schema.yml**
|
||||
```yaml
|
||||
# no schema key needed. it's implied!
|
||||
config:
|
||||
# your schema config here
|
||||
types:
|
||||
# your generic types here
|
||||
models:
|
||||
@ -121,7 +128,7 @@ mutations:
|
||||
|
||||
Your schema YAML file will get quite bloated if it's just used as a monolithic source of truth
|
||||
like this. We can tidy this up quite a bit by simply placing the files in directories that map
|
||||
to the keys they populate -- e.g. `types/`, `models/`, `queries/`, `mutations/`, etc.
|
||||
to the keys they populate -- e.g. `config/`, `types/`, `models/`, `queries/`, `mutations/`, etc.
|
||||
|
||||
There are two approaches to namespacing:
|
||||
* By filename
|
||||
@ -132,6 +139,11 @@ There are two approaches to namespacing:
|
||||
If you use a parent directory name (at any depth) of one of the four keywords above, it will
|
||||
be implicitly placed in the corresponding section of the schema.
|
||||
|
||||
**app/_graphql/types/config.yml**
|
||||
```yaml
|
||||
# my schema config here
|
||||
```
|
||||
|
||||
**app/_graphql/types/types.yml**
|
||||
```yaml
|
||||
# my type definitions here
|
||||
@ -169,23 +181,22 @@ The following are perfectly valid:
|
||||
* `app/_graphql/news-and-blog/models/blog.yml`
|
||||
* `app/_graphql/mySchema.yml`
|
||||
|
||||
### Changing schema defaults
|
||||
### Schema config
|
||||
|
||||
In addition to all the keys mentioned above, each schema can declare a couple of generic
|
||||
configuration files, `defaults` and `modelConfig`. These are
|
||||
mostly used for assigning or removing default plugins to models and operations.
|
||||
In addition to all the keys mentioned above, each schema can declare a generic
|
||||
configuration section, `config`. This are mostly used for assigning or removing plugins
|
||||
and resolvers.
|
||||
|
||||
[info]
|
||||
As of now, the only one of these being used
|
||||
is `modelConfig`, but `defaults` could some day apply non-model configuration to the schema.
|
||||
[/info]
|
||||
An important subsection of `config` is `modelConfig`, where you can configure settings for specific
|
||||
models, e.g. `DataObject`.
|
||||
|
||||
Like the other sections, it can have its own `modelConfig.yml`, or just be added as a `modelConfig:`
|
||||
Like the other sections, it can have its own `config.yml`, or just be added as a `config:`
|
||||
mapping to a generic schema yaml document.
|
||||
|
||||
**app/_graphql/modelConfig.yml**
|
||||
**app/_graphql/config.yml**
|
||||
```yaml
|
||||
DataObject:
|
||||
modelConfig:
|
||||
DataObject:
|
||||
plugins:
|
||||
inheritance: true
|
||||
operations:
|
||||
|
@ -84,8 +84,7 @@ tangential changes such as:
|
||||
|
||||
### Viewing the generated code
|
||||
|
||||
TODO, once we figure out where it will go
|
||||
|
||||
By default, the generated code is placed in the `.graphql/` directory in the root of your project.
|
||||
|
||||
### Further reading
|
||||
|
||||
|
@ -33,15 +33,16 @@ on the fly as closures. Resolvers must be static methods on a class, and are eva
|
||||
the schema build.
|
||||
[/notice]
|
||||
|
||||
### Adding a schema builder
|
||||
### Adding executable code
|
||||
|
||||
We can use the `builders` section of the config to add an implementation of `SchemaUpdater`.
|
||||
We can use the `execute` section of the config to add an implementation of `SchemaUpdater`.
|
||||
|
||||
```yaml
|
||||
SilverStripe\GraphQL\Schema\Schema:
|
||||
schemas:
|
||||
default:
|
||||
builders:
|
||||
config:
|
||||
execute:
|
||||
- 'MyProject\MySchema'
|
||||
```
|
||||
|
||||
|
@ -224,8 +224,8 @@ Page:
|
||||
|
||||
There are several settings you can apply to your model class (typically `DataObjectModel`),
|
||||
but because they can have distinct values _per schema_, the standard `_config` layer is not
|
||||
an option. Model configuration has to be done within the schema definition in the `modelConfig`
|
||||
section.
|
||||
an option. Model configuration has to be done within the schema config in the `modelConfig`
|
||||
subsection.
|
||||
|
||||
### Customising the type name
|
||||
|
||||
@ -245,9 +245,10 @@ the `$className` as a parameter.
|
||||
|
||||
Let's turn `MyProject\Models\Product` into the more specific `MyProjectProduct`
|
||||
|
||||
*app/_graphql/modelConfig.yml*
|
||||
*app/_graphql/config.yml*
|
||||
```yaml
|
||||
DataObject:
|
||||
modelConfig:
|
||||
DataObject:
|
||||
type_formatter: ['MyProject\Formatters', 'formatType' ]
|
||||
```
|
||||
|
||||
@ -277,9 +278,10 @@ public static function formatType(string $className): string
|
||||
You can also add prefixes to all your DataObject types. This can be a scalar value or a callable,
|
||||
using the same signature as `type_formatter`.
|
||||
|
||||
*app/_graphql/modelConfig.yml*
|
||||
*app/_graphql/config.yml*
|
||||
```yaml
|
||||
DataObject
|
||||
modelConfig:
|
||||
DataObject
|
||||
type_prefix: 'MyProject'
|
||||
```
|
||||
|
||||
|
@ -283,9 +283,10 @@ MyProject\Models\ProductCategory:
|
||||
|
||||
To disable sort globally, use `modelConfig`:
|
||||
|
||||
*app/_graphql/modelConfig.yml*
|
||||
*app/_graphql/config.yml*
|
||||
```yaml
|
||||
DataObject:
|
||||
modelConfig:
|
||||
DataObject:
|
||||
operations:
|
||||
read:
|
||||
plugins:
|
||||
|
@ -22,22 +22,43 @@ an explicit resolver and allow the system to discover one for you based on namin
|
||||
|
||||
Let's start by registering a resolver class(es) where we can define a bunch of these functions.
|
||||
|
||||
**app/_graphql/config.yml**
|
||||
```yaml
|
||||
SilverStripe\Core\Injector\Injector:
|
||||
SilverStripe\GraphQL\Schema\Registry\ResolverRegistry:
|
||||
constructor:
|
||||
myResolver: '%$MyProject\Resolvers\MyResolvers'
|
||||
resolvers:
|
||||
- MyProject\Resolvers\MyResolvers
|
||||
```
|
||||
|
||||
What we're registering here is called a `ResolverProvider`, and it must implement that interface.
|
||||
The only thing this class is obliged to do is return a method name for a resolver given a type name and
|
||||
`Field` object. If the class does not contain a resolver for that combination, it may return null and
|
||||
defer to other resolver providers, or ultimately fallback on the global default resolver.
|
||||
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.
|
||||
|
||||
```php
|
||||
public static function getResolverMethod(?string $typeName = null, ?Field $field = null): ?string;
|
||||
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:
|
||||
|
||||
```graphql
|
||||
@ -48,31 +69,33 @@ query {
|
||||
}
|
||||
```
|
||||
|
||||
An example implementation of this method for our example might be:
|
||||
Imagine we have two classes registered under `resolvers` -- `ClassA` and `ClassB`
|
||||
|
||||
* Does `resolveCountryName` exist?
|
||||
* Yes? Invoke
|
||||
* No? Continue
|
||||
* Does `resolveCountry` exist?
|
||||
* Yes? Invoke
|
||||
* No? Continue
|
||||
* Does `resolveName` exist?
|
||||
* Yes? Invoke
|
||||
* No? Continue
|
||||
* Return null. Maybe someone else knows how to deal with this.
|
||||
The logic will go like this:
|
||||
|
||||
You can implement whatever logic you like to help the resolver provider discover which of its methods
|
||||
it appropriate for the given type/field combination, but since the above pattern seems like a pretty common
|
||||
implementation, the module ships an abstract `DefaultResolverProvider` that applies this logic. You can just
|
||||
write the resolver methods!
|
||||
* `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**
|
||||
```yaml
|
||||
resolverStrategy: [ 'MyApp\Resolvers\Strategy', 'getResolverMethod' ]
|
||||
```
|
||||
|
||||
Let's add a resolver method to our resolver provider:
|
||||
|
||||
**app/src/Resolvers/MyResolvers.php**
|
||||
```php
|
||||
use SilverStripe\GraphQL\Schema\Resolver\DefaultResolverProvider;
|
||||
|
||||
class MyResolvers extends DefaultResolverProvider
|
||||
class MyResolvers
|
||||
{
|
||||
public static function resolveReadCountries()
|
||||
{
|
||||
@ -90,14 +113,6 @@ class MyResolvers extends DefaultResolverProvider
|
||||
}
|
||||
```
|
||||
|
||||
To recap, the `DefaultResolverProvider` will follow this workflow to locate a resolver
|
||||
for this query:
|
||||
|
||||
* `resolveQueryReadCountries()` (<typeName><fieldName>)
|
||||
* `resolveQuery()` (<typeName>)
|
||||
* `resolveReadCountries()` (<fieldName>)
|
||||
* `resolve` (catch-all)
|
||||
|
||||
|
||||
Now that we're using logic to discover our resolver, we can clean up the config a bit.
|
||||
|
||||
|
@ -105,11 +105,10 @@ class ModelCreator implements SchemaModelCreatorInterface
|
||||
|
||||
Just add it to the registry:
|
||||
|
||||
**app/_graphql/config.yml
|
||||
```yaml
|
||||
SilverStripe\Core\Injector\Injector:
|
||||
SilverStripe\GraphQL\Schema\Registry\SchemaModelCreatorRegistry:
|
||||
constructor:
|
||||
dataobject: '%$SilverStripe\GraphQL\Schema\DataObject\ModelCreator'
|
||||
modelCreators:
|
||||
- 'SilverStripe\GraphQL\Schema\DataObject\ModelCreator'
|
||||
```
|
||||
|
||||
### Further reading
|
||||
|
@ -73,10 +73,13 @@ public static function resolve(array $resolverContext = []): Closure
|
||||
Now, just add the operation to the `DataObjectModel` configuration
|
||||
to make it available to all DataObject types.
|
||||
|
||||
**app/_graphql/config.yml**
|
||||
```yaml
|
||||
SilverStripe\GraphQL\Schema\DataObject\DataObjectModel:
|
||||
modelConfig:
|
||||
DataObject:
|
||||
operations:
|
||||
duplicate: 'MyProject\Operations\DuplicateCreator'
|
||||
duplicate:
|
||||
class: 'MyProject\Operations\DuplicateCreator'
|
||||
```
|
||||
|
||||
And use it:
|
||||
|
@ -13,6 +13,43 @@ Docs for the current stable version (3.x) can be found
|
||||
[here](https://github.com/silverstripe/silverstripe-graphql/tree/3)
|
||||
[/alert]
|
||||
|
||||
## Getting the type name for a model class
|
||||
|
||||
Often times, you'll need to know the name of the type given a class name. There's a bit of context to this.
|
||||
|
||||
### Getting the type name at build time
|
||||
|
||||
If you need to know the name of the type _during the build_, e.g. creating the name of an operation, field, query, etc,
|
||||
you should use the `Build::requireActiveBuild()` accessor. This will get you the schema that is currently being built,
|
||||
and throw if no build is active. A more tolerant method is `getActiveBuild()` which will return null if no schema
|
||||
is being built.
|
||||
|
||||
```php
|
||||
Build::requireActiveBuild()->findOrMakeModel($className)->getName();
|
||||
```
|
||||
|
||||
### Getting the type name from within your app
|
||||
|
||||
If you need the type name during normal execution of your app, e.g. to display in your UI, you can rely
|
||||
on the cached typenames, which are persisted alongside your generated schema code.
|
||||
|
||||
```php
|
||||
Schema::create('default')->getTypeNameForClass($className);
|
||||
```
|
||||
|
||||
### Why is there a difference?
|
||||
|
||||
It is expensive to load all of the schema config. The `getTypeNameForClass` function avoids the need to
|
||||
load the config, and reads directly from the cache. To be clear, the following is functionally equivalent,
|
||||
but slow:
|
||||
|
||||
```php
|
||||
Schema::create('default')
|
||||
->loadFromConfig()
|
||||
->findOrMakeModel($className)
|
||||
->getName();
|
||||
```
|
||||
|
||||
## Persisting queries
|
||||
|
||||
A common pattern in GraphQL APIs is to store queries on the server by an identifier. This helps save
|
||||
|
Loading…
Reference in New Issue
Block a user