Skip to content
Snippets Groups Projects
Commit b574e28e authored by Alberto Silva's avatar Alberto Silva
Browse files

First version

parent 698fc258
Branches
Tags 8.x-1.0-rc1
No related merge requests found
# IntelliJ project files
.idea
*.iml
out
gen
# GraphQL fragment include
A module to include fragments inside a GraphQL query.
This module is useful when:
* You have a collection of paragraphs (or any other kind of entity) that is being used by multiple Content Types.
* GraphQL is used to export content data (ideally by using [Static Export](https://www.drupal.org/project/static_export) module) and you find the same repeated data structure on multiple queries.
To avoid having to repeat the same query for every content type, you should:
* use GraphQL fragments
* extract them to files
* use this module to include those fragment files into your GraphQL queries.
This module transforms the following:
```graphql
# include Image.gql
{
content:nodeById(id: "1") {
id: entityId
... on NodeArticle {
title
image: fieldImage {
entity {
...Image
}
}
}
}
}
```
into this:
```graphql
# sample content from Image.gql
fragment Image on MediaImage {
entityLabel
... on MediaImage {
mediaImage: fieldMediaImage {
alt
title
url
width
height
}
credit: fieldImageCredit
caption: fieldImageCaption
}
}
{
content:nodeById(id: "1") {
id: entityId
... on NodeArticle {
title
image: fieldImage {
entity {
...Image
}
}
}
}
}
```
## How it works ##
* Configure [fragments base directory](/admin/config/graphql/fragment-include/config). That is were your fragment files are located. It's usually a directory inside `/sites/default/`.
* Create a file inside the above directory, with the contents of your fragment, and with .gql extension. You can create subdirectories and include a fragment inside another fragment (infinite recursion protection is available).
* Add an include to a GraqhQL query, using the following format: `# include {PATH_TO_FILE_INSIDE_FRAGMENTS_BASE_DIR}.gql`
* Execute the query, and the contents from the fragment file will be appended to the query before execution.
## Debugging ##
If a fragment can not be found, a warning message is logged. Use [dblog](https://www.drupal.org/docs/8/core/modules/dblog) to view them.
## Caveats ##
* Due to the fact that the [GraphQL specification](https://graphql.github.io/graphql-spec/) does not support any kind of includes, we use comments and a "# include path.gql" syntax that is completely custom. You can change that syntax to your convenience, extending `graphql_fragment_include.graphql_fragment_loader` service.
* For the same above reason, [GraphiQL IDE](https://github.com/graphql/graphiql) will remove the above comments when clicking "Prettify" button.
## TODO ##
* Find a way to maintain fragment includes when clicking GraphiQL's "Prettify" button.
GraphQL Fragment Include
name: GraphQL Fragment Include
type: module
description: 'A module for including GraphQL fragments on a query'
package: GraphQL
core: 8.x
dependencies:
- graphql
- graphql_core
graphql_fragment_include.png

54.4 KiB

graphql_fragment_include.config:
path: '/admin/config/graphql/fragment-include/config'
defaults:
_form: '\Drupal\graphql_fragment_include\Form\GraphQLFragmentIncludeConfigForm'
_title: 'GraphQL Fragment Include Configuration'
requirements:
_permission: 'administer site configuration'
services:
graphql_fragment_include.graphql_fragment_loader:
class: Drupal\graphql_fragment_include\GraphQL\Fragment\GraphQLFragmentLoader
arguments:
[
"@logger.factory",
"@config.factory",
]
graphql.query_processor:
class: Drupal\graphql_fragment_include\GraphQL\Execution\QueryProcessor
arguments:
- '@cache_contexts_manager'
- '@plugin.manager.graphql.schema'
- '@cache.graphql.results'
- '@request_stack'
- '@graphql_fragment_include.graphql_fragment_loader'
<?php
namespace Drupal\graphql_fragment_include\Form;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
/**
* Graphql Fragment Include Configuration form class.
*/
class GraphQLFragmentIncludeConfigForm extends ConfigFormBase {
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'graphql_fragment_include_config_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form = parent::buildForm($form, $form_state);
$config = $this->config('graphql_fragment_include.settings');
$form['fragments_base_dir'] = [
'#type' => 'textfield',
'#title' => $this->t("Fragments base directory (relative to %drupal_root)", ['%drupal_root' => DRUPAL_ROOT]),
'#required' => TRUE,
'#description' => $this->t("It must start with a leading slash. Path to the base directory where your GraphQL Fragments are located, e.g.- /sites/default/graphql/fragments"),
'#default_value' => $config->get('fragments_base_dir'),
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$config = $this->config('graphql_fragment_include.settings');
$config->set('fragments_base_dir', $form_state->getValue('fragments_base_dir'));
$config->save();
return parent::submitForm($form, $form_state);
}
/**
* Return the configuration names.
*/
protected function getEditableConfigNames() {
return [
'graphql_fragment_include.settings',
];
}
}
<?php
namespace Drupal\graphql_fragment_include\GraphQL\Execution;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Cache\Context\CacheContextsManager;
use Drupal\graphql\GraphQL\Execution\QueryProcessor as BaseQueryProcessor;
use Drupal\graphql\Plugin\SchemaPluginManager;
use Drupal\graphql_fragment_include\GraphQL\Fragment\GraphQLFragmentLoaderInterface;
use GraphQL\Server\OperationParams;
use GraphQL\Server\ServerConfig;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* QueryProcessor service overriding BaseQueryProcessor.
*/
class QueryProcessor extends BaseQueryProcessor {
/**
* GraphQL fragment loader.
*
* @var \Drupal\graphql_fragment_include\GraphQL\Fragment\GraphQLFragmentLoaderInterface
*/
protected $graphQLFragmentLoader;
/**
* Processor constructor.
*
* @param \Drupal\Core\Cache\Context\CacheContextsManager $contextsManager
* The cache contexts manager service.
* @param \Drupal\graphql\Plugin\SchemaPluginManager $pluginManager
* The schema plugin manager.
* @param \Drupal\Core\Cache\CacheBackendInterface $cacheBackend
* The cache backend for caching query results.
* @param \Symfony\Component\HttpFoundation\RequestStack $requestStack
* The request stack.
* @param \Drupal\graphql_fragment_include\GraphQL\Fragment\GraphQLFragmentLoaderInterface $graphQLFragmentLoader
* The GraphQL fragment loader.
*/
public function __construct(
CacheContextsManager $contextsManager,
SchemaPluginManager $pluginManager,
CacheBackendInterface $cacheBackend,
RequestStack $requestStack,
GraphQLFragmentLoaderInterface $graphQLFragmentLoader
) {
parent::__construct($contextsManager, $pluginManager, $cacheBackend, $requestStack);
$this->graphQLFragmentLoader = $graphQLFragmentLoader;
}
/**
* Execute a single query with custom include loader.
*
* @param \GraphQL\Server\ServerConfig $config
* ServerConfig.
* @param \GraphQL\Server\OperationParams $params
* OperationParams.
*
* @return mixed
* The result.
*/
public function executeSingle(ServerConfig $config, OperationParams $params) {
// Load GraphQL fragments.
$params->query = $this->graphQLFragmentLoader->loadFragments($params->query);
return parent::executeSingle($config, $params);
}
}
<?php
namespace Drupal\graphql_fragment_include\GraphQL\Fragment;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
/**
* An implementation of GraphqlFragmentLoaderInterface.
*
* Allows inclusion of GraphQL Fragments on a query, defined by the
* following syntax:
* # include Fragment.gql
* Includes are not supported by GraphQL, so we need to use comments for them.
*/
class GraphQLFragmentLoader implements GraphQLFragmentLoaderInterface {
/**
* Logger instance for this module.
*
* @var \Psr\Log\LoggerInterface
*/
protected $logger;
/**
* Config object for this module.
*
* @var \Drupal\Core\Config\ImmutableConfig
*/
protected $config;
/**
* Loaded includes.
*
* @var array
*/
protected $loadedFragments;
/**
* Constructor for IncludeLoader.
*
* @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $loggerFactory
* Logger Factory.
* @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
* Drupal config service.
*/
public function __construct(
LoggerChannelFactoryInterface $loggerFactory,
ConfigFactoryInterface $configFactory
) {
$this->logger = $loggerFactory->get('graphql_fragment_include');
$this->config = $configFactory->get('graphql_fragment_include.settings');
}
/**
* Find any line defining a fragment include.
*
* Returns an array obtained from executing a regexp. This method should be
* overridden if include format needs to be changed.
*
* @param string $string
* The GraphQL string to process.
*
* @return array
* The matches array obtained from executing a regexp.
*/
public function findFragments(string $string) {
preg_match_all("/^#\s+include\s+(\S+)\s*$/m", $string, $matches);
return $matches;
}
/**
* {@inheritdoc}
*/
public function loadFragments(string $string) {
$matches = $this->findFragments($string);
if ($matches) {
$baseDir = DRUPAL_ROOT . $this->config->get("fragments_base_dir");
foreach ($matches[1] as $key => $match) {
$fragmentFile = $baseDir . '/' . $match;
if (!is_file($fragmentFile)) {
$this->logger->warning(t("No fragment '%fragment-file' found.", ['fragment-file' => $fragmentFile]));
continue;
}
// Avoid double loading a fragment.
if ($this->isFragmentAlreadyLoaded($fragmentFile)) {
$string = trim(str_replace($matches[0][$key], "", $string));
}
else {
$this->markFragmentAsLoaded($fragmentFile);
$fragmentData = trim(file_get_contents($fragmentFile));
// Recursively load other fragments.
$fragmentData = $this->loadFragments($fragmentData);
$string = trim(str_replace($matches[0][$key], "", $string));
$string .= "\n\n# START $match \n" . $fragmentData . "\n# END $match ";
}
}
}
return $string;
}
/**
* Detects if a string contains an include.
*
* @param string $string
* The string to check.
*
* @return bool
* True if a include is detected.
*
* protected function containsInclude(string $string) {
* if (preg_match("/^#\s+include\s+(\S+)\s*$/m", $string)) {
* return TRUE;
* }
* return FALSE;
* }
*/
/**
* Tells whether a fragment has been already loaded.
*
* GraphQL fragments can not be repeated.
*
* @param string $fragmentFile
* The file to check.
*
* @return bool
* True if it has been already loaded, false otherwise.
*/
protected function isFragmentAlreadyLoaded(string $fragmentFile) {
return isset($this->loadedFragments[$fragmentFile]);
}
/**
* Marks a fragment as already loaded.
*
* @param string $fragmentFile
* The file to be marked as already loaded.
*/
protected function markFragmentAsLoaded(string $fragmentFile) {
$this->loadedFragments[$fragmentFile] = TRUE;
}
}
<?php
namespace Drupal\graphql_fragment_include\GraphQL\Fragment;
/**
* Provides an interface for loading GraphQL Fragments.
*/
interface GraphQLFragmentLoaderInterface {
/**
* Find any line defining a fragment include.
*
* Returns an array obtained from executing a regexp. This method should be
* overridden if include format needs to be changed.
*
* @param string $string
* The GraphQL string to process.
*
* @return array
* The matches array obtained from executing a regexp.
*/
public function findFragments(string $string);
/**
* Find and load GraphQL fragments.
*
* They could be defined as:
* # include Fragment.gql
* Other implementations of this interface could define another format for
* fragment inclusion.
*
* @param string $string
* The GraphQL string to process.
*
* @return string
* The processed string.
*/
public function loadFragments(string $string);
}
<?php
namespace Drupal\graphql_fragment_include;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\DependencyInjection\ServiceProviderBase;
/**
* Modifies \Drupal\graphql\GraphQL\Execution\QueryProcessor service.
*/
class GraphqlFragmentIncludeServiceProvider extends ServiceProviderBase {
/**
* {@inheritdoc}
*/
public function alter(ContainerBuilder $container) {
$definition = $container->getDefinition('graphql.query_processor');
$definition->setClass('Drupal\graphql_fragment_include\GraphQL\Execution\QueryProcessor');
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment