Building a Custom GraphQL API Module for Drupal 10 Article Management

Travis Christopher
9 min readOct 6, 2024

--

Building a Custom GraphQL API Module for Drupal 10 Article Management

In this tutorial, we’ll walk through the process of creating a custom GraphQL API module for Drupal 10, focusing on article management. This module will allow you to query, create, update, and delete articles using GraphQL.

## Prerequisites

- Drupal 10 installed

- Basic knowledge of Drupal module development

- Familiarity with GraphQL concepts

## Step 1: Create the Module Structure

First, let’s create the directory for our new module:

mkdir -p web/modules/custom/custom_graphql_api
cd web/modules/custom/custom_graphql_api

## Step 2: Create Module Files

Create the following files in the `custom_graphql_api` directory:

1. `custom_graphql_api.info.yml`

2. `custom_graphql_api.module`

3. `custom_graphql_api.services.yml`

## Step 3: Define Module Information

Edit `custom_graphql_api.info.yml`:

name: Custom GraphQL API
type: module
description: 'Provides a custom GraphQL API for article management.'
package: Custom
core_version_requirement: ^9 || ^10
dependencies:
- drupal:node
- graphql:graphql
- next:next
- entityqueue:entityqueue
- paragraphs:paragraphs

## Step 4: Create the Module File

Edit `custom_graphql_api.module`:

<?php
/**
* @file
* Contains custom_graphql_api.module.
*/
use Drupal\Core\Routing\RouteMatchInterface;
/**
* Implements hook_help().
*/
function custom_graphql_api_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
case 'help.page.custom_graphql_api':
$output = '';
$output .= '<h3>' . t('About') . '</h3>';
$output .= '<p>' . t('This module provides a custom GraphQL API for article management.') . '</p>';
return $output;
default:
}
}
/**
* Implements hook_theme().
*/
function custom_graphql_api_theme() {
return [
'article_graphql_data' => [
'variables' => [
'article' => NULL,
],
],
];
}

## Step 5: Define Services

Edit `custom_graphql_api.services.yml`:

services:
custom_graphql_api.schema_extension:
class: Drupal\custom_graphql_api\Plugin\GraphQL\SchemaExtension\CustomGraphQLApiSchemaExtension
tags:
- { name: graphql_schema_extension }
custom_graphql_api.article_manager:
class: Drupal\custom_graphql_api\ArticleManager
arguments: ['@entity_type.manager', '@database']

## Step 6: Create the Schema Extension

Create a new file `src/Plugin/GraphQL/SchemaExtension/CustomGraphQLApiSchemaExtension.php`:

<?php

namespace Drupal\custom_graphql_api\Plugin\GraphQL\SchemaExtension;

use Drupal\graphql\GraphQL\ResolverBuilder;
use Drupal\graphql\GraphQL\ResolverRegistryInterface;
use Drupal\graphql\Plugin\GraphQL\SchemaExtension\SdlSchemaExtensionPluginBase;
use Drupal\custom_graphql_api\GraphQL\Access\ArticleAccessCheck;

/**
* @SchemaExtension(
* id = "custom_graphql_api",
* name = "Custom GraphQL API Schema Extension",
* description = "Schema extension for the custom GraphQL API",
* schema = "next"
* )
*/
class CustomGraphQLApiSchemaExtension extends SdlSchemaExtensionPluginBase {

/**
* {@inheritdoc}
*/
public function registerResolvers(ResolverRegistryInterface $registry) {
$builder = new ResolverBuilder();

$this->addQueryFields($registry, $builder);
$this->addMutationFields($registry, $builder);
$this->addCustomTypes($registry, $builder);
}

/**
* Add custom query fields.
*/
protected function addQueryFields(ResolverRegistryInterface $registry, ResolverBuilder $builder) {
// Query to fetch an article by ID
$registry->addFieldResolver('Query', 'articleById',
$builder->produce('entity_load')
->map('type', $builder->fromValue('node'))
->map('bundles', $builder->fromValue(['article']))
->map('id', $builder->fromArgument('id'))
->addExtraField('access', $builder->fromValue([ArticleAccessCheck::class, 'checkQueryAccess']))
);

// Query to fetch multiple articles
$registry->addFieldResolver('Query', 'articles',
$builder->produce('query_articles')
->map('limit', $builder->fromArgument('limit'))
->map('offset', $builder->fromArgument('offset'))
->addExtraField('access', $builder->fromValue([ArticleAccessCheck::class, 'checkQueryAccess']))
);
}

/**
* Add custom mutation fields.
*/
protected function addMutationFields(ResolverRegistryInterface $registry, ResolverBuilder $builder) {
// Mutation to create a new article
$registry->addFieldResolver('Mutation', 'createArticle',
$builder->produce('create_article')
->map('input', $builder->fromArgument('input'))
->addExtraField('access', $builder->fromValue([ArticleAccessCheck::class, 'checkCreateAccess']))
);

// Mutation to update an existing article
$registry->addFieldResolver('Mutation', 'updateArticle',
$builder->produce('update_article')
->map('id', $builder->fromArgument('id'))
->map('input', $builder->fromArgument('input'))
->addExtraField('access', $builder->fromValue([ArticleAccessCheck::class, 'checkUpdateAccess']))
);

// Mutation to delete an article
$registry->addFieldResolver('Mutation', 'deleteArticle',
$builder->produce('delete_article')
->map('id', $builder->fromArgument('id'))
->addExtraField('access', $builder->fromValue([ArticleAccessCheck::class, 'checkDeleteAccess']))
);
}

/**
* Add custom types.
*/
protected function addCustomTypes(ResolverRegistryInterface $registry, ResolverBuilder $builder) {
// Resolvers for the Article type
$registry->addFieldResolver('Article', 'id',
$builder->produce('entity_id')
->map('entity', $builder->fromParent())
);

$registry->addFieldResolver('Article', 'title',
$builder->produce('entity_label')
->map('entity', $builder->fromParent())
);

$registry->addFieldResolver('Article', 'body',
$builder->produce('property_path')
->map('type', $builder->fromValue('entity:node'))
->map('value', $builder->fromParent())
->map('path', $builder->fromValue('body.value'))
);

$registry->addFieldResolver('Article', 'created',
$builder->produce('entity_created')
->map('entity', $builder->fromParent())
);

$registry->addFieldResolver('Article', 'author',
$builder->compose(
$builder->produce('entity_owner')
->map('entity', $builder->fromParent()),
$builder->produce('entity_label')
->map('entity', $builder->fromParent())
)
);
}

/**
* {@inheritdoc}
*/
public function getSchema() {
return <<<GQL
extend type Query {
articleById(id: ID!): Article
articles(limit: Int = 10, offset: Int = 0): [Article!]!
}

extend type Mutation {
createArticle(input: CreateArticleInput!): Article
updateArticle(id: ID!, input: UpdateArticleInput!): Article
deleteArticle(id: ID!): Boolean
}

type Article implements NodeInterface {
id: ID!
title: String!
body: String
created: DateTime!
author: String
}

input CreateArticleInput {
title: String!
body: String
}

input UpdateArticleInput {
title: String
body: String
}
GQL;
}
}

## Step 7: Create the ArticleManager Service

Create a new file `src/ArticleManager.php`:

<?php

namespace Drupal\custom_graphql_api;

use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Database\Connection;
use Drupal\node\NodeInterface;

/**
* Service for managing articles.
*/
class ArticleManager {

/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;

/**
* The database connection.
*
* @var \Drupal\Core\Database\Connection
*/
protected $database;

/**
* Constructs a new ArticleManager object.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Database\Connection $database
* The database connection.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, Connection $database) {
$this->entityTypeManager = $entity_type_manager;
$this->database = $database;
}

/**
* Retrieves a list of articles.
*
* @param int $limit
* The number of articles to retrieve.
* @param int $offset
* The number of articles to skip.
*
* @return \Drupal\node\NodeInterface[]
* An array of article entities.
*/
public function getArticles($limit = 10, $offset = 0) {
$query = $this->entityTypeManager->getStorage('node')->getQuery()
->condition('type', 'article')
->condition('status', NodeInterface::PUBLISHED)
->sort('created', 'DESC')
->range($offset, $limit)
->accessCheck(TRUE);

$nids = $query->execute();
return $this->entityTypeManager->getStorage('node')->loadMultiple($nids);
}

/**
* Creates a new article.
*
* @param array $values
* An array of values to set on the new article.
*
* @return \Drupal\node\NodeInterface
* The newly created article entity.
*
* @throws \Drupal\Core\Entity\EntityStorageException
*/
public function createArticle(array $values) {
$article = $this->entityTypeManager->getStorage('node')->create([
'type' => 'article',
'title' => $values['title'],
'body' => [
'value' => $values['body'],
'format' => 'basic_html',
],
'status' => NodeInterface::PUBLISHED,
]);

$article->save();
return $article;
}

/**
* Updates an existing article.
*
* @param int $id
* The ID of the article to update.
* @param array $values
* An array of values to update on the article.
*
* @return \Drupal\node\NodeInterface|null
* The updated article entity, or null if not found.
*
* @throws \Drupal\Core\Entity\EntityStorageException
*/
public function updateArticle($id, array $values) {
$article = $this->entityTypeManager->getStorage('node')->load($id);

if (!$article || $article->bundle() !== 'article') {
return null;
}

if (isset($values['title'])) {
$article->setTitle($values['title']);
}

if (isset($values['body'])) {
$article->set('body', [
'value' => $values['body'],
'format' => 'basic_html',
]);
}

$article->save();
return $article;
}

/**
* Deletes an article.
*
* @param int $id
* The ID of the article to delete.
*
* @return bool
* TRUE if the article was successfully deleted, FALSE otherwise.
*
* @throws \Drupal\Core\Entity\EntityStorageException
*/
public function deleteArticle($id) {
$article = $this->entityTypeManager->getStorage('node')->load($id);

if (!$article || $article->bundle() !== 'article') {
return false;
}

$article->delete();
return true;
}

}

The implementation of the `ArticleManager` class provides methods for managing articles:

1. `getArticles($limit = 10, $offset = 0)`:

- Uses EntityQuery to fetch published articles.

- Sorts articles by creation date in descending order.

- Implements pagination using `$limit` and `$offset`.

- Performs an access check to ensure the current user has permission to view the articles.

2. `createArticle(array $values)`:

- Creates a new article node with the provided title and body.

- Sets the article status to published.

- Saves the new article and returns the entity.

3. `updateArticle($id, array $values)`:

- Loads the article by ID and checks if it exists and is of type ‘article’.

- Updates the title and/or body if provided in the $values array.

- Saves the updated article and returns the entity.

4. `deleteArticle($id)`:

- Loads the article by ID and checks if it exists and is of type ‘article’.

- Deletes the article and returns a boolean indicating success or failure.

Each method includes proper error handling and type hinting for better code quality and debugging. The class also uses dependency injection for the EntityTypeManager and database Connection, allowing for easier testing and maintenance.

## Step 8: Implement Custom GraphQL Plugins

Create the following files in the `src/Plugin/GraphQL/DataProducer` directory:

1. `QueryArticles.php`

2. `CreateArticle.php`

3. `UpdateArticle.php`

4. `DeleteArticle.php`

Each file should contain a class that extends `DataProducerPluginBase` and implements the necessary logic for querying, creating, updating, and deleting articles.

<?php

namespace Drupal\custom_graphql_api\Plugin\GraphQL\DataProducer;

use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\graphql\Plugin\GraphQL\DataProducer\DataProducerPluginBase;
use Drupal\custom_graphql_api\ArticleManager;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
* Creates a new article.
*
* @DataProducer(
* id = "create_article",
* name = @Translation("Create Article"),
* description = @Translation("Creates a new article."),
* produces = @ContextDefinition("any",
* label = @Translation("Article")
* ),
* consumes = {
* "input" = @ContextDefinition("any",
* label = @Translation("Article input")
* )
* }
* )
*/
class CreateArticle extends DataProducerPluginBase implements ContainerFactoryPluginInterface {

/**
* The article manager service.
*
* @var \Drupal\custom_graphql_api\ArticleManager
*/
protected $articleManager;

/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('custom_graphql_api.article_manager')
);
}

/**
* CreateArticle constructor.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\custom_graphql_api\ArticleManager $article_manager
* The article manager service.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, ArticleManager $article_manager) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->articleManager = $article_manager;
}

/**
* Creates a new article.
*
* @param array $input
* The input data for creating the article.
*
* @return \Drupal\node\NodeInterface
* The newly created article entity.
*/
public function resolve(array $input) {
return $this->articleManager->createArticle($input);
}

}

2. `UpdateArticle.php`:

<?php

namespace Drupal\custom_graphql_api\Plugin\GraphQL\DataProducer;

use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\graphql\Plugin\GraphQL\DataProducer\DataProducerPluginBase;
use Drupal\custom_graphql_api\ArticleManager;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
* Updates an existing article.
*
* @DataProducer(
* id = "update_article",
* name = @Translation("Update Article"),
* description = @Translation("Updates an existing article."),
* produces = @ContextDefinition("any",
* label = @Translation("Article")
* ),
* consumes = {
* "id" = @ContextDefinition("string",
* label = @Translation("Article ID")
* ),
* "input" = @ContextDefinition("any",
* label = @Translation("Article input")
* )
* }
* )
*/
class UpdateArticle extends DataProducerPluginBase implements ContainerFactoryPluginInterface {

/**
* The article manager service.
*
* @var \Drupal\custom_graphql_api\ArticleManager
*/
protected $articleManager;

/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('custom_graphql_api.article_manager')
);
}

/**
* UpdateArticle constructor.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\custom_graphql_api\ArticleManager $article_manager
* The article manager service.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, ArticleManager $article_manager) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->articleManager = $article_manager;
}

/**
* Updates an existing article.
*
* @param string $id
* The ID of the article to update.
* @param array $input
* The input data for updating the article.
*
* @return \Drupal\node\NodeInterface|null
* The updated article entity, or null if not found.
*/
public function resolve($id, array $input) {
return $this->articleManager->updateArticle($id, $input);
}

}

3. `DeleteArticle.php`:

<?php

namespace Drupal\custom_graphql_api\Plugin\GraphQL\DataProducer;

use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\graphql\Plugin\GraphQL\DataProducer\DataProducerPluginBase;
use Drupal\custom_graphql_api\ArticleManager;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
* Deletes an existing article.
*
* @DataProducer(
* id = "delete_article",
* name = @Translation("Delete Article"),
* description = @Translation("Deletes an existing article."),
* produces = @ContextDefinition("boolean",
* label = @Translation("Deletion status")
* ),
* consumes = {
* "id" = @ContextDefinition("string",
* label = @Translation("Article ID")
* )
* }
* )
*/
class DeleteArticle extends DataProducerPluginBase implements ContainerFactoryPluginInterface {

/**
* The article manager service.
*
* @var \Drupal\custom_graphql_api\ArticleManager
*/
protected $articleManager;

/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('custom_graphql_api.article_manager')
);
}

/**
* DeleteArticle constructor.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\custom_graphql_api\ArticleManager $article_manager
* The article manager service.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, ArticleManager $article_manager) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->articleManager = $article_manager;
}

/**
* Deletes an existing article.
*
* @param string $id
* The ID of the article to delete.
*
* @return bool
* True if the article was successfully deleted, false otherwise.
*/
public function resolve($id) {
return $this->articleManager->deleteArticle($id);
}

}

These custom GraphQL plugins implement the core functionality for creating, updating, and deleting articles. They leverage the `ArticleManager` service to perform the actual operations on the article entities.

## Step 9: Implement Access Checks

Create a new file `src/GraphQL/Access/ArticleAccessCheck.php`:

<?php

namespace Drupal\custom_graphql_api\GraphQL\Access;

use Drupal\Core\Session\AccountInterface;
use Drupal\graphql\GraphQL\Execution\FieldContext;

/**
* Provides access checks for article-related GraphQL fields.
*/
class ArticleAccessCheck {

public static function checkQueryAccess(AccountInterface $account, FieldContext $field_context) {
return $account->hasPermission('access content');
}

// Other access check methods...
}

## Step 10: Update the Schema Extension with Access Checks

Modify the `CustomGraphQLApiSchemaExtension.php` file to include access checks for each resolver.

## Step 11: Enable the Module

Enable your custom module using Drush or the Drupal admin interface:

```bash
drush en custom_graphql_api
```

## Step 12: Test Your GraphQL API

You can now test your custom GraphQL API using GraphQL IDE or by making requests to the GraphQL endpoint. Here are some example queries and mutations:

### Query an article by ID:

```graphql
query {
articleById(id: "1") {
id
title
body
created
author {
name
email
}
}
}
```

### Query multiple articles:

```graphql
query {
articles(limit: 5, offset: 0) {
id
title
body
created
}
}
```

### Create a new article:

```graphql
mutation {
createArticle(input: {
title: "New Article Title"
body: "This is the content of the new article."
}) {
id
title
body
}
}
```

### Update an existing article:

```graphql
mutation {
updateArticle(id: "1", input: {
title: "Updated Article Title"
body: "This is the updated content of the article."
}) {
id
title
body
}
}
```

### Delete an article:

```graphql
mutation {
deleteArticle(id: "1")
}
```

## Conclusion

This tutorial has walked you through the process of creating a custom GraphQL API module for Drupal 10, focused on article management. The module provides a solid foundation for querying, creating, updating, and deleting articles through a GraphQL API.

You can further extend this module by adding more fields, implementing pagination, or integrating with other Drupal features like taxonomies and media entities. Remember to implement proper error handling, input validation, and security measures in a production environment. Also, consider implementing caching strategies to improve the performance of your GraphQL API.

Happy coding!

--

--

Travis Christopher
Travis Christopher

Written by Travis Christopher

0 Followers

Principal Lead Developer at Arttus

No responses yet