Tập tành viết Graphql Server trong Drupal Using graphql-php

6th May 2022
Table of contents

Have you ever wanted to interact with Drupal data through a GraphQL client? Lots of people do. Most of the time, the Drupal GraphQL module is the tool that you want. It is great for things like:

  • A React JS app that shows a catalog of products
  • A Gatsby blog
  • Building an API for many different clients to consume

However, there is one case that the Graphql module does not cover: building a Graphql schema for data that is not represented as a Drupal entity.

The Graphql module maintainers decided to only support entities. There were two big reasons for this:

  • Under normal circumstances, just about every piece of your Drupal content is an entity.
  • The graphql-php symfony package is a good enough abstraction layer for exposing other types of data.

In this article, I will be discussing how to implement a custom graphql-php endpoint and schema for interacting with a custom, non-entity data source.

Why?

If you’ve gotten this far, you may want to ask yourself “why is my data not an entity?” There are a few acceptable reasons:

Performance

Is part of your use case inserting tons of records at once? In this case, you may not want your data to be a Drupal entity. This will let you take advantage of MySQL bulk inserts.

Inheritance

When you came to the site, was the data already not an entity? Unfortunately, this often justifies keeping it that way rather than doing a time-consuming migration.

Implementing graphql-php

The graphql-php docs are pretty good. It is not much of a leap to implement this in Drupal.

Here is a summary of the steps:

  1. Install graphql-php with composer
  2. Set up a Drupal route to serve Graphql
  3. Establish a Graphql schema
  4. Establish the resolver and its arguments
  5. Execute and serve the Graphql Response

Step One: Set up a Drupal route to serve Graphql

We’ll start with a basic Drupal controller.

<?php

namespace Drupal\my_graphql_module\Controller;

use Drupal\Core\Controller\ControllerBase;
use Symfony\Component\HttpFoundation\Request;

class MyGraphqlController extends ControllerBase {

 /**
  * Route callback.
  */
 public function handleRequest(Request $request) {
   return [];
 }
}

You don’t need to approach this differently than a normal Drupal route.

Here is what the route definition might look like in my_graphql_module.routing.yml:

my_graphql_module.list_recipient_graphql:
 path: '/list-recipient-graphql'
 defaults:
   _title: 'List recipient graphql endpoint'
   _controller: '\Drupal\my_graphql_module\Controller\ListRecipientGraphql::handleRequest'
 methods: [POST]
 requirements:
   _list_recipient_graphql: "TRUE"

A few things to note:

  • It is wise to restrict the route to allow only the POST method since that is how Graphql clients send queries.
  • The _list_recipient_graphql requirement would be an Access Service. Any of the other Drupal route access methods would also work.

Step Two: Assumptions - Your Data, and How You Want to Access it

For this tutorial, I’ll assume your data is a simple mysql table similar to this:

+-----------------+------------------+------+-----+---------+----------------+
| Field           | Type             | Null | Key | Default | Extra          |
+-----------------+------------------+------+-----+---------+----------------+
| contact_id      | int(10) unsigned | YES  | MUL | NULL    |                |
| list_nid        | int(10) unsigned | NO   | MUL | NULL    |                |
| status          | varchar(256)     | NO   |     | NULL    |                |
| id              | int(10) unsigned | NO   | PRI | NULL    | auto_increment |
| email           | varchar(256)     | NO   |     | NULL    |                |
+-----------------+------------------+------+-----+---------+----------------+

In the real world, this roughly translates to a record that links contacts to mailing lists. You can see now why we would want to insert lots of these at once! Let’s also say that you have a React component where you would like to use the Apollo client to display, filter and page through this data.

Step Three: Establish a Graphql Schema

We can start with a relatively simple Graphql schema. See below (note, the resolver is blank for now):

<?php

namespace Drupal\my_graphql_module\Controller;

use Drupal\Core\Controller\ControllerBase;
use Symfony\Component\HttpFoundation\Request;
use GraphQL\Type\Schema;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;

class MyGraphqlController extends ControllerBase {

 /**
  * Route callback.
  */
 public function handleRequest(Request $request) {
   // The schema for a List Recipient.
   $list_recipient_type = [
     'name' => 'ListRecipient',
     'fields' => [
       'email' => [
         'type' => Type::string(),
         'description' => 'Recipient email',
       ],
       'contact_nid' => [
         'type' => Type::int(),
         'description' => 'The recipient contact node ID',
       ],
       'list_nid' => [
         'type' => Type::int(),
         'description' => 'The recipient list node ID',
       ],
       'name' => [
         'type' => Type::string(),
         'description' => 'Contact name',
       ],
       'id' => [
         'type' => Type::int(),
         'description' => 'The primary key.',
       ],
     ],
   ];

   $list_recipients_query = new ObjectType([
     'name' => 'Query',
     'fields' => [
       'ListRecipients' => [
         'type' => Type::listOf($list_recipient_type),
         'resolve' => function($root_value, $args) {
           // We'll fill this in later. This is where we actually get the data, and it
           // depends on paging and filter arguments.
         }
       ],
     ],
   ]);

   $schema = new Schema([
     'query' => $list_recipients_query,
   ]);
 }
}

You will notice that the ListRecipient Graphql type looks pretty similar to our database schema. That is pretty much its job - it establishes what fields are allowed in Graphql requests and it must match the fields that our resolver returns.

Step Four: Resolver and Arguments

In this step, we will add the resolver and the argument definition. Here it is:

<?php

namespace Drupal\my_graphql_module\Controller;

use Drupal\Core\Controller\ControllerBase;
use Symfony\Component\HttpFoundation\Request;
use GraphQL\Type\Schema;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\InputObjectType;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\my_graphql_module\ListRecipientManager;

class MyGraphqlController extends ControllerBase {

   /**
  * The recipient manager
  *
  * It's usually wise to inject some kind of service to be your
  * resolver - though you don't have to.
  *
  * @var \Drupal\my_graphql_module\ListRecipientManager
  */
 protected $recipientManager;

 /**
  * The ContactSearchModalFormController constructor.
  *
  * @param \Drupal\my_graphql_module\ListRecipientManager $recipient_manager
  *   The recipient manager.
  */
 public function __construct(ListRecipientManager $recipient_manager) {
   $this->recipientManager = $recipient_manager;
 }

 /**
  * {@inheritdoc}
  *
  * @param \Symfony\Component\DependencyInjection\ContainerInterface $container
  *   The Drupal service container.
  *
  * @return static
  */
 public static function create(ContainerInterface $container) {
   return new static(
     $container->get('my_graphql_module.list_recipient_manager'),
   );
 }

 /**
  * Route callback.
  */
 public function handleRequest(Request $request) {
   // The schema for a List Recipient.
   $list_recipient_type = [
     'name' => 'ListRecipient',
     'fields' => [
       'email' => [
         'type' => Type::string(),
         'description' => 'Recipient email',
       ],
       'contact_nid' => [
         'type' => Type::int(),
         'description' => 'The recipient contact node ID',
       ],
       'list_nid' => [
         'type' => Type::int(),
         'description' => 'The recipient list node ID',
       ],
       'name' => [
         'type' => Type::string(),
         'description' => 'Contact name',
       ],
       'id' => [
         'type' => Type::int(),
         'description' => 'The primary key.',
       ],
     ],
   ];

   // The filter input type.
   $filter_type = new InputObjectType([
     'name' => 'FilterType',
     'fields' => [
       'listId' => [
         'type' => Type::int(),
         'description' => 'The list node ID',
       ],
     ],
   ]);

   $list_recipients_query = new ObjectType([
     'name' => 'Query',
     'fields' => [
       'ListRecipients' => [
         'args' => [
           'offset' => [
             'type' => Type::int(),
             'description' => 'Offset for query.',
           ],
           'limit' => [
             'type' => Type::int(),
             'description' => 'Limit for query.',
           ],
           'filter' => [
             'type' => $filter_type,
             'description' => 'The list recipient filter object',
           ],
         ],
         'type' => Type::listOf($list_recipient_type),
         'resolve' => function($root_value, $args) {
           return $this->recipientManager->getRecipients($args['filter']['listId'], $args['offset'], $args['limit']);
         }
       ],
     ],
   ]);

   $schema = new Schema([
     'query' => $list_recipients_query,
   ]);
 }
}

I will first explain the “args” property of the ListRecipients query. “args” defines anything that you would like to allow Graphql clients to pass in that may affect how the resolver works. In the above example, we establish filter and paging support. If we wanted to support sorting, we would implement it via args too: think of args as the portal through which you supply your resolver with everything it needs to fetch the data. Here is the Graphql you could use to query this schema:

query ListRecipientQuery(
     $limit: Int,
     $offset: Int,
     $filter: FilterType
   )
 {
   ListRecipients(
     limit: $limit
     offset: $offset
     filter: $filter
   )
   {
     email
     name
   }
 }

Step Five: Execute and Serve the Graphql Response

The last thing you need to do is tell graphql-php to execute the incoming query. Here is the whole thing:

<?php
 
namespace Drupal\my_graphql_module\Controller;
 
use Drupal\Core\Controller\ControllerBase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\my_graphql_module\ListRecipientManager;
use Drupal\Component\Serialization\Json;
use Symfony\Component\HttpFoundation\JsonResponse;
 
use GraphQL\Type\Schema;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\InputObjectType;
use GraphQL\GraphQL;
 
class MyGraphqlController extends ControllerBase {
 
   /**
  * The recipient manager
  *
  * It's usually wise to inject some kind of service to be your
  * resolver - though you don't have to.
  *
  * @var \Drupal\my_graphql_module\ListRecipientManager
  */
 protected $recipientManager;
 
 /**
  * The ContactSearchModalFormController constructor.
  *
  * @param \Drupal\my_graphql_module\ListRecipientManager $recipient_manager
  *   The recipient manager.
  */
 public function __construct(ListRecipientManager $recipient_manager) {
   $this->recipientManager = $recipient_manager;
 }
 
 /**
  * {@inheritdoc}
  *
  * @param \Symfony\Component\DependencyInjection\ContainerInterface $container
  *   The Drupal service container.
  *
  * @return static
  */
 public static function create(ContainerInterface $container) {
   return new static(
     $container->get('my_graphql_module.list_recipient_manager'),
   );
 }
 
 /**
  * Route callback.
  */
 public function handleRequest(Request $request) {
   // The schema for a List Recipient.
   $list_recipient_type = [
     'name' => 'ListRecipient',
     'fields' => [
       'email' => [
         'type' => Type::string(),
         'description' => 'Recipient email',
       ],
       'contact_nid' => [
         'type' => Type::int(),
         'description' => 'The recipient contact node ID',
       ],
       'list_nid' => [
         'type' => Type::int(),
         'description' => 'The recipient list node ID',
       ],
       'name' => [
         'type' => Type::string(),
         'description' => 'Contact name',
       ],
       'id' => [
         'type' => Type::int(),
         'description' => 'The primary key.',
       ],
     ],
   ];
 
   // The filter input type.
   $filter_type = new InputObjectType([
     'name' => 'FilterType',
     'fields' => [
       'listId' => [
         'type' => Type::int(),
         'description' => 'The list node ID',
       ],
     ],
   ]);
 
   $list_recipients_query = new ObjectType([
     'name' => 'Query',
     'fields' => [
       'ListRecipients' => [
         'args' => [
           'offset' => [
             'type' => Type::int(),
             'description' => 'Offset for query.',
           ],
           'limit' => [
             'type' => Type::int(),
             'description' => 'Limit for query.',
           ],
           'filter' => [
             'type' => $filter_type,
             'description' => 'The list recipient filter object',
           ],
         ],
         'type' => Type::listOf($list_recipient_type),
         'resolve' => function($root_value, $args) {
           return $this->recipientManager->getRecipients($args['filter']['listId'], $args['offset'], $args['limit']);
         }
       ],
     ],
   ]);
 
   $schema = new Schema([
     'query' => $list_recipients_query,
   ]);
 
   $body = Json::decode($request->getContent());
   $graphql = $body['query'];
 
   if (!$graphql) {
     return new JsonResponse(['message' => 'No query was found'], 400);
   }
 
   $variables = !empty($body['variables']) ? $body['variables'] : [];
 
   $result = Graphql::executeQuery($schema, $graphql, NULL, NULL, $variables)->toArray();
   return new JsonResponse($result);
 }
}

Conclusion

I hope that you will find this helpful! Remember that the graphql-php docs are very good as well.

Check back soon for an article on supporting GraphQL mutations and error handling!

Bạn thấy bài viết này như thế nào?
2 reactions

Add new comment

Image CAPTCHA
Enter the characters shown in the image.
Câu nói tâm đắc: “Điều tuyệt với nhất trong cuộc sống là làm được những việc mà người khác tin là không thể!”

Related Articles

There’s no denying that GraphQL is the latest addition for API development. For front end frameworks many GraphQL clients are available and Apollo

Over the past decade, REST has become the standard (yet a fuzzy one) for designing web APIs

The same route handler function is called in REST whereas in GraphQL query can be called to form nested response with multiple resources.

This session will introduce GraphQL queries and demonstrate the advantages of changing the Drupal push model to a pull model by letting the template define its data requirements

Drupal is a renowned name when it comes to website development. The kind of features and control it allows you to have on your content is quite impressive.