GraphQL module for Drupal is poorly documented and the existing documentation is behind on new versions of the module. In these two articles I will share my experience with this module and what I found reading Stack overflow and issues on Github.
Drupal 8 and GraphQL. Part 2:
- Custom query
- Query examples
- Disable caching for a query
Custom query
Let's say the Views functionality is not enough for us, so we will write a custom query. Let's have a look at the following example: a website has web pages for different products. Every page is a Drupal node. If we visit such a web page, its address will be /node/:nid. But if we want something more fancy, like /shop/jam or /shop/bicycle, we need to search nodes by alias.
Let's lay a basis for the future selection. We create the Product content type and install Pathauto module to generate pattern-based paths.
composer require drupal/pathauto
Then we create aliases for URLs of the products:
We generate the URLs and choose to display the Product content type as GraphQL:
Also we add this content type to Exposed Content in GraphQL module.
Now we need to write a query in the our graphql_custom_fields module (see previous article). Add a new class:
<?php
namespace Drupal\graphql_custom_fields\Plugin\GraphQL\Fields;
use Drupal\graphql_core\GraphQL\FieldPluginBase;
use Drupal\node\Entity\Node;
use Youshido\GraphQL\Execution\ResolveInfo;
/**
* Retrieve Node by url alias
*
* @GraphQLField(
* id = "node_by_alias",
* name = "NodeByAlias",
* type = "Entity",
* arguments = {
* "alias" = "String"
* },
* )
*/
class NodeByAlias extends FieldPluginBase {
public function resolveValues($value, array $args, ResolveInfo $info) {
$path = \Drupal::service('path.alias_manager')->getPathByAlias('/'.trim($args['alias'], '/'));
if(preg_match('/node\/(\d+)/', $path, $matches)) {
$node = Node::load($matches[1]);
yield $node;
}
}
}
All this looks quite similar to the previous case. We don't specify types here because it is a query, not a field, but we have the alias parameter, which is the alias for a URL of a product.
Everything is ready to run the query. Let's test it! Also, don't forget that you can find all the available fields and queries in the Docs sidebar.
query Product($alias: String!) {
product: NodeByAlias(alias: $alias) {
...product
}
}
fragment product on NodeProduct {
name: entityLabel
body
fieldPicture
}
At the bottom left of the explorer you can enter the variables for the query. Let’s try to get information on jam:
{
"alias": "/shop/jam"
}
We run the query... and get the expected result!
Query examples
Let's look at a short example on type creation. Suppose we want to send a website some settings, for example, we created a module with a settings form. In this example we will try to send several fields from Basic Site Settings (/admin/config/system/site-information) in a query.
In the Types directory, create a file with a new type BasicSettings.php:
<?php
namespace Drupal\graphql_custom_fields\Plugin\GraphQL\Types;
use Drupal\graphql_core\GraphQL\TypePluginBase;
/**
* GraphQL type representing Gravitzapa settings.
*
* @GraphQLType(
* id = "basic_settings_type",
* name = "BasicSettings"
* )
*/
class BasicSettings extends TypePluginBase {
}
It's all simple, name is just the name of the type. Then we create a query as in the previous part:
<?php
namespace Drupal\graphql_custom_fields\Plugin\GraphQL\Fields;
use Drupal\graphql_core\GraphQL\FieldPluginBase;
use Youshido\GraphQL\Execution\ResolveInfo;
/**
* Retrieve basic site settings
*
* @GraphQLField(
* id = "basic_settings",
* name = "BasicSettings",
* type = "BasicSettings",
* )
*/
class BasicSettings extends FieldPluginBase {
public function resolveValues($value, array $args, ResolveInfo $info) {
$config = \Drupal::config('system.site');
yield ['name' => $config->get('name'), 'slogan' => $config->get('slogan')];
}
}
Note that we indicated the class we created as the type. Now we create two fields to show name and slogan. Name field:
<?php
namespace Drupal\graphql_custom_fields\Plugin\GraphQL\Fields;
use Drupal\graphql_core\GraphQL\FieldPluginBase;
use Youshido\GraphQL\Execution\ResolveInfo;
/**
* GraphQL field resolving a Basic site settings field "name".
*
* @GraphQLField(
* id = "basic_site_name",
* name = "name",
* type = "String",
* types = {"BasicSettings"}
* )
*/
class BasicSiteName extends FieldPluginBase {
public function resolveValues($value, array $args, ResolveInfo $info) {
yield $value['name'];
}
}
Code for the slogan field will be the same, so we'll skip it. Here is our query:
query Settings {
BasicSettings {
name
slogan
}
}
But what if we need to send a data array? For example, we want to get a list of random products. Let's create a new query that returns a certain number of random products.
<?php
namespace Drupal\graphql_custom_fields\Plugin\GraphQL\Fields;
use Drupal\Core\DependencyInjection\DependencySerializationTrait;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\graphql_core\GraphQL\FieldPluginBase;
use Youshido\GraphQL\Execution\ResolveInfo;
/**
* Retrieve random products
*
* @GraphQLField(
* id = "random_products",
* name = "RandomProducts",
* type = "Entity",
* arguments = {
* "count" = "Int"
* },
* multi = true
* )
*/
class RandomProducts extends FieldPluginBase implements ContainerFactoryPluginInterface {
use DependencySerializationTrait;
protected $entityTypeManager;
public static function create(ContainerInterface $container, array $configuration, $pluginId, $pluginDefinition) {
return new static($configuration, $pluginId, $pluginDefinition, $container->get('entity_type.manager'));
}
public function __construct(array $configuration, $pluginId, $pluginDefinition, EntityTypeManagerInterface $entityTypeManager) {
$this->entityTypeManager = $entityTypeManager;
parent::__construct($configuration, $pluginId, $pluginDefinition);
}
public function resolveValues($value, array $args, ResolveInfo $info) {
$query = \Drupal::entityQuery('node');
$query->condition('type', 'product');
$result = $query->execute();
if (count($result)) {
$keys = array_rand($result, $args['count']);
$keys = array_filter($result, function($key) use ($keys) {
return in_array($key, $keys);
}, ARRAY_FILTER_USE_KEY);
$nodes = $this->entityTypeManager->getStorage('node')->loadMultiple($keys);
foreach ($nodes as $node) {
yield $node;
}
} else {
yield null;
}
}
}
As you can see, we have a new parameter, multi, which indicates that the result of the query is an array. The rest of the new code will get $entityTypeManager for the class to load nodes. New query will look like this:
query RandomProduct($count: Int!) {
products: RandomProducts(count: $count) {
...product
}
}
fragment product on NodeProduct {
name: entityLabel
body
fieldPicture
}
If we run the query several times, we'll get the same results 😔 They will change only after we clear the Drupal cache. So, what do we do? Next, we'll learn how not to cache specific queries.
Disable caching for a query
So, we know that all GraphQL queries are cached through Drupal. Exploring the settings of GraphQL module, I came across this variable:
parameters:
graphql.config:
# GraphQL result cache:
#
# By default, the GraphQL results get cached. This can be disabled during development.
#
# @default true
result_cache: true
If we take our chances and set it to false, we'll see that the query from before works as expected: we get different results. But this is not an option for us. Why would we turn off caching for all queries? Let's rather find a way to disable only for specific queries.
Drupal 8 allows us to change the caching status of a query. To do so, we'll use RequestPolicyInterface and then observe how the query for random products will behave.
You can create a new module, but I will demonstrate how it works on the already existing module for schema fields. We add the Cache folder and files for the class and the settings:
RequestPolicy.php
<?php
namespace Drupal\graphql_custom_fields\Cache;
use Drupal\Core\PageCache\RequestPolicyInterface;
use Symfony\Component\HttpFoundation\Request;
class RequestPolicy implements RequestPolicyInterface {
public function check(Request $request) {
return static::ALLOW;
}
}
All queries are still cached.
graphql_custom_fields.services.yml
services:
graphql.request_custom_policy:
class: Drupal\graphql_custom_fields\Cache\RequestPolicy
tags:
- { name: graphql_request_policy }
Let's look at the structure of a GraphQL query, for example, in Chrome:
This query has the operationName parameter. We can use it to distinguish the query we want from the others:
<?php
namespace Drupal\graphql_custom_fields\Cache;
use Drupal\Core\PageCache\RequestPolicyInterface;
use Symfony\Component\HttpFoundation\Request;
class RequestPolicy implements RequestPolicyInterface {
public function check(Request $request) {
$json = json_decode($request->getContent());
if (!is_null($json) && isset($json->operationName) && strtolower($json->operationName) === 'randomproduct') {
return static::DENY;
}
return static::ALLOW;
}
}
If we run the query for random products several times, the results will be different every time!
By now we've got enough queries to start developing the JS-powered part of our website.
Before we start sending requests from front-end to back-end, we need to allow CORS in Drupal. To do that, we change these values in services.yml of our website:
cors.config:
enabled: true
# Specify allowed headers, like 'x-allowed-header'.
allowedHeaders: ['x-csrf-token','authorization','content-type','accept','origin','x-requested-with']
# Specify allowed request methods, specify ['*'] to allow all possible ones.
allowedMethods: ['*']
# Configure requests allowed from specific origins.
allowedOrigins: ['*']
# Sets the Access-Control-Expose-Headers header.
exposedHeaders: false
# Sets the Access-Control-Max-Age header.
maxAge: 1000
# Sets the Access-Control-Allow-Credentials header.
supportsCredentials: false