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:

  1. Under normal circumstances, just about every piece of your Drupal content is an entity.
  2. 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.

    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!