silverstripe-framework/docs/en/02_Developer_Guides/19_GraphQL/05_plugins/03_writing_a_complex_plugin.md

296 lines
8.6 KiB
Markdown
Raw Normal View History

WIP: Add new graphql 4 docs (#9652) * DOCS: Add new graphql 4 docs * Reorganise docs * Docs done * Basic graphql index page * TOC for getting started * show folders on graphql index page * Add middleware note * Docs update * Update docs to reflect flushless schema * Docs updates * Docs for getByLink * Query caching docs * Docs on nested operations * update docs for new graphql dev admin * Docs for configurable operations * Replace readSiteTrees with readPages * Schema defaults docs * Docs for inherited plugins * Docs for customising * * Docs for field whitelisting * Change whitelist word * New docs on modelConfig * Document dev/build extension * Document default/global plugins * Document new input type fields config * Apply suggestions from code review Co-authored-by: Andre Kiste <bergice@users.noreply.github.com> * Note about when procedural schema gets built * Fix link * Apply suggestions from code review Co-authored-by: Andre Kiste <bergice@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Andre Kiste <bergice@users.noreply.github.com> * DOCS Note about plugins in custom queries * DOCS Note about filter and custom resolvers * DOCS Note about canview paging * DOCS Updated guidance on _extend See https://github.com/silverstripe/silverstripe-graphql/issues/296 * Apply suggestions from code review Co-authored-by: Andre Kiste <bergice@users.noreply.github.com> * DOCS Pre-release warning Co-authored-by: Ingo Schommer <ingo@silverstripe.com> Co-authored-by: Andre Kiste <bergice@users.noreply.github.com> Co-authored-by: Ingo Schommer <me@chillu.com>
2020-10-19 23:56:17 +02:00
---
title: Writing a complex plugin
summary: In this tutorial, we'll create a plugin that affects models, queries, and input types
---
# Plugins
[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](https://www.silverstripe.org/blog/community-slack-channel/),
and report any issues at [github.com/silverstripe/silverstripe-graphql](https://github.com/silverstripe/silverstripe-graphql).
Docs for the current stable version (3.x) can be found
[here](https://github.com/silverstripe/silverstripe-graphql/tree/3)
[/alert]
## Writing a complex plugin
For this example, we'll imagine that a lot of our DataObjects are geocoded, and this is ostensibly some kind of
`DataExtension` that adds lat/lon information to the DataObject, and maybe allows you to ask how close it is to
a given lat/lon pair.
We want any queries using these dataobjects to be able to search within a radius of a given lat/lon.
To do this, we'll need a few things:
* DataObjects that are geocodable should always expose their lat/lon fields
* `read` operations for DataObjects that are geocodable should include a `within` parameter
* An input type for this lat/lon parameter should be globally available to the schema
* A resolver should automatically filter the result set by proximity.
Let's get started.
### Step 1: Ensure DataObjects expose lat/lon fields
Since we're dealing with DataObjects, we'll need a `ModelTypePlugin`.
```php
class GeocodableDataObject implements ModelTypePlugin
{
public function getIdentifier(): string
{
return 'geocode';
}
public function apply(ModelType $type, Schema $schema, array $config = []): void
{
$class = $type->getModel()->getSourceClass();
// sanity check that this is a dataobject
Schema::invariant(
is_subclass_of($class, DataObject::class),
'The %s plugin can only be applied to types generated by %s models',
__CLASS__,
DataObject::class
);
// only apply the plugin to geocodable DataObjects
if (!Extensible::has_extension($class, Geocodable::class)) {
return;
}
$type->addField('Lat')
->addField('Lon');
}
}
```
Now all dataobjects that have the extension will be forced to expose their lat/lon fields.
### Step 2: Add a new parameter to the queries
We want any `readXXX` query to include a `within` parameter if it's for a geocodable DataObject.
For this, we're going to use `ModelQueryPlugin`, because this is for queries generated by a model.
```php
class GeocodableQuery implements ModelQueryPlugin
{
public function getIdentifier(): string
{
return 'geocodableQuery';
}
public function apply(ModelQuery $query, Schema $schema, array $config = []): void
{
$class = $query->getModel()->getSourceClass();
// Only apply to geocodable objects
if (!Extensible::has_extension($class, Geocodable::class)) {
return;
}
$query->addArg('within', 'SearchRadiusInput');
}
}
```
Now our read queries will have a new parameter:
```graphql
query readEvents(within: ...)
```
But we're not done yet! What is `SearchRadiusInput`? We haven't defined that yet. Ideally, we want our query
to look like this:
```graphql
query {
readEvents(within: {
lat: 123.123456,
lon: 123.123456,
proximity: 500,
unit: METER
}) {
nodes {
title
lat
lon
}
}
}
### Step 3: Adding an input type
We'll need this `SearchRadiusInput` to be shared across queries. It's not specific to any DataObject.
For this, we can use `SchemaUpdater`. For tidiness, let's just to this in the same `GeocodeableQuery`
class, since they share concerns.
```php
class GeocodableQuery implements ModelQueryPlugin, SchemUpdater
{
//...
public static function updateSchema(Schema $schema): void
{
$unitType = Enum::create('Unit', [
'METER' => 'METER',
'KILOMETER' => 'KILOMETER',
'FOOT' => 'FOOT',
'MILE' => 'MILE',
]);
$radiusType = InputType::create('SearchRadiusInput')
->setFields([
'lat' => 'Float!',
'lon' => 'Float!',
'proximity' => 'Int!',
'unit' => 'Unit!'
]);
$schema->addType($unitType);
$schema->addType($unitType);
}
}
```
So now we can run queries with these parameters, but we need to somehow apply it to the result set.
### Step 4: Add a resolver to apply the filter
All these DataObjects have their own resolvers already, so we can't really get into those to change
their functionality without a massive hack. This is where the idea of **resolver middleware** and
**resolver afterware** comes in really useful.
**Resolver middleware** runs _before_ the operation's assigned resolver
**Resolver afterware** runs _after_ the operation's assigned resolver
Middlewares and afterwares are pretty straightforward. They get the same `$args`, `$context`, and `$info`
parameters as the assigned resolver, but the first argument, `$result` is mutated with each resolver.
In this case, we're going to be filtering our DataList procedurally, transforming it into an array,
so we need to know that things like filters and sort have already been applied, as they expect a `DataList`
instance. So we'll need to do this fairly late in the chain. Afterware makes the most sense.
```php
public function apply(ModelQuery $query, Schema $schema, array $config = []): void
{
$class = $query->getModel()->getSourceClass();
// Only apply to geocodable objects
if (!Extensible::has_extension($class, Geocodable::class)) {
return;
}
$query->addArg('within', 'SearchRadiusInput');
$query->addResolverAfterware([static::class, 'applyRadius']);
}
public static function applyRadius($result, array $args): array
{
$results = [];
// imaginary class
$proximity = new Proximity($args['unit'], $args['lat'], $args['lon']);
foreach ($result as $record) {
if ($proximity->isWithin($args['proximity'], $record->Lat, $record->Lon)) {
$results[] = $record;
}
}
return $results;
}
```
Looking good!
But there's still one little gotcha. This is likely to be run after pagination has been executed, so our
`$result` parameter is probably an array of `edges`, `nodes`, etc.
**app/src/Resolvers/MyResolver.php**
```php
public static function applyRadius($result, array $args)
{
$results = [];
// imaginary class
$proximity = new Proximity($args['unit'], $args['lat'], $args['lon']);
foreach ($result['nodes'] as $record) {
if ($proximity->isWithin($args['proximity'], $record->Lat, $record->Lon)) {
$results[] = $record;
}
}
return [
'edges' => $results,
'nodes' => $results,
'pageInfo' => $result['pageInfo'],
];
}
```
If we added this plugin in middleware rather than afterware,
we could filter the result set by a list of `IDs` early on, which would allow us to keep
a `DataList` throughout the whole cycle, but this would force us to loop over an
unlimited result set, and that's never a good idea.
[notice]
If you've picked up on the inconsistency that the `pageInfo` property is now inaccurate, this is a long-standing
issue with doing post-query filters. Ideally, any middleware that filters a DataList should do it at the query level,
but that's not always possible.
[/notice]
### Step 5: Register the plugins
Back to Injector:
```yaml
SilverStripe\Core\Injector\Injector:
SilverStripe\GraphQL\Schema\Registry\PluginRegistry:
constructor:
2020-11-19 01:27:08 +01:00
- 'MyProject\Plugins\GeocodableQuery'
- 'MyProject\Plugins\GeocodableDataObject'
WIP: Add new graphql 4 docs (#9652) * DOCS: Add new graphql 4 docs * Reorganise docs * Docs done * Basic graphql index page * TOC for getting started * show folders on graphql index page * Add middleware note * Docs update * Update docs to reflect flushless schema * Docs updates * Docs for getByLink * Query caching docs * Docs on nested operations * update docs for new graphql dev admin * Docs for configurable operations * Replace readSiteTrees with readPages * Schema defaults docs * Docs for inherited plugins * Docs for customising * * Docs for field whitelisting * Change whitelist word * New docs on modelConfig * Document dev/build extension * Document default/global plugins * Document new input type fields config * Apply suggestions from code review Co-authored-by: Andre Kiste <bergice@users.noreply.github.com> * Note about when procedural schema gets built * Fix link * Apply suggestions from code review Co-authored-by: Andre Kiste <bergice@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Andre Kiste <bergice@users.noreply.github.com> * DOCS Note about plugins in custom queries * DOCS Note about filter and custom resolvers * DOCS Note about canview paging * DOCS Updated guidance on _extend See https://github.com/silverstripe/silverstripe-graphql/issues/296 * Apply suggestions from code review Co-authored-by: Andre Kiste <bergice@users.noreply.github.com> * DOCS Pre-release warning Co-authored-by: Ingo Schommer <ingo@silverstripe.com> Co-authored-by: Andre Kiste <bergice@users.noreply.github.com> Co-authored-by: Ingo Schommer <me@chillu.com>
2020-10-19 23:56:17 +02:00
```
### Step 6: Apply the plugins
We can apply the plugins to queries and dataobjects one of two ways:
* Add them on a case-by-case basis to our config
* Add them as default plugins so that we never have to worry about it.
Let's look at each approach:
#### Case-by-case
**app/_graphql/models.yml**
```yaml
MyProject\Models\Event:
plugins:
geocode: true
fields:
title: true
operations:
read:
plugins:
geocodeableQuery: true
```
This can get pretty verbose, so you might just want to register them as default plugins for all DataObjects
and their `read` operations.
#### Apply by default
```yaml
# apply the DataObject plugin
SilverStripe\GraphQL\Schema\DataObject\DataObjectModel:
default_plugins:
geocode: true
# apply the query plugin
SilverStripe\GraphQL\Schema\DataObject\ReadCreator:
default_plugins:
geocodableQuery: true
```
### Further reading
[CHILDREN]