Skip to content

Commit 4dc9b62

Browse files
pmelabfubhy
authored andcommitted
Twig fixes. (#348)
1 parent 1a2f921 commit 4dc9b62

18 files changed

Lines changed: 591 additions & 149 deletions

.travis.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ env:
1010
global:
1111
- DRUPAL_BUILD_DIR=$TRAVIS_BUILD_DIR/../drupal
1212
- SIMPLETEST_DB=mysql://root:@127.0.0.1/graphql
13+
- TRAVIS=true
1314
matrix:
1415
- DRUPAL_CORE=8.2.x
1516
- DRUPAL_CORE=8.3.x

modules/graphql_twig/graphql_twig.module

Lines changed: 113 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,112 @@
55
* GraphQL Twig hook implementations.
66
*/
77

8-
use Drupal\graphql_twig\GraphQLNodeVisitor;
8+
use Youshido\GraphQL\Parser\Parser;
9+
use Youshido\GraphQL\Parser\Ast\ArgumentValue\Variable;
10+
use Drupal\Core\Entity\EntityInterface;
11+
use Drupal\graphql_twig\GraphQLTwigEnvironment;
12+
13+
/**
14+
* Implements hook_theme_registry_alter().
15+
*
16+
* Search for GraphQL enhanced templates in the theme registry and append
17+
* the GraphQL preprocessor.
18+
*/
19+
function graphql_twig_theme_registry_alter(&$theme_registry) {
20+
/** @var \Drupal\Core\Template\TwigEnvironment $twig */
21+
$twig = Drupal::service('twig');
22+
foreach ($theme_registry as $hook => $info) {
23+
$file = $info['path'] . '/' . $info['template'] . '.html.twig';
24+
if ($query = graphql_twig_get_query($file)) {
25+
$theme_registry[$hook]['graphql_enabled'] = TRUE;
26+
$theme_registry[$hook]['graphql_variables'] = graphql_twig_get_query_variables($query);
27+
$theme_registry[$hook]['preprocess functions'][] = 'graphql_twig_process_query';
28+
}
29+
}
30+
}
31+
32+
/**
33+
* Get the query string for a given template file.
34+
*
35+
* Checks for an annotation, a `*.gql` sibling file or returns NULL if nothing
36+
* is found.
37+
*
38+
* @param string $file
39+
* The template file path.
40+
*
41+
* @return string|null
42+
* The GraphQL query string or NULL.
43+
*/
44+
function graphql_twig_get_query($file) {
45+
if (file_exists($file)) {
46+
47+
$graphqlFile = str_replace('.html.twig', '.gql', $file);
48+
if (file_exists($graphqlFile)) {
49+
return file_get_contents($graphqlFile);
50+
}
51+
52+
preg_match(GraphQLTwigEnvironment::$GRAPHQL_ANNOTATION_REGEX, file_get_contents($file), $matches);
53+
if (array_key_exists('query', $matches)) {
54+
$source = (new Parser())->parse($matches['query']);
55+
if ($source['queries']) {
56+
return $matches['query'];
57+
}
58+
}
59+
}
60+
61+
return NULL;
62+
}
63+
64+
/**
65+
* Get the declared variables in a GraphQL query string.
66+
*
67+
* @param $query
68+
* The query string.
69+
*
70+
* @return array
71+
* A list of variable names.
72+
*/
73+
function graphql_twig_get_query_variables($query) {
74+
$source = (new Parser())->parse($query);
75+
return array_map(function (Variable $var) {
76+
return $var->getName();
77+
}, $source['variables']);
78+
}
79+
80+
/**
81+
* Implements hook_preprocess().
82+
*
83+
* Execute the GraphQL query if the theme hook is GraphQL enhanced and add
84+
* the query result as well as cache metadata.
85+
*/
86+
function graphql_twig_process_query(&$variables, $hook, $info) {
87+
if (isset($info['graphql_enabled']) && $info['graphql_enabled']) {
88+
/** @var \Drupal\graphql\QueryProcessor $processor */
89+
$processor = Drupal::service('graphql.query_processor');
90+
91+
/** @var \Drupal\Core\Template\TwigEnvironment $twig */
92+
$twig = Drupal::service('twig');
93+
94+
$file = $info['path'] . '/' . $info['template'] . '.html.twig';
95+
96+
$query = $twig->loadTemplate($file)->getGraphQLQuery();
97+
98+
$arguments = [];
99+
foreach ($info['graphql_variables'] as $var) {
100+
if (isset($variables[$var])) {
101+
$arguments[$var] = $variables[$var] instanceof EntityInterface ? $variables[$var]->id() : $variables[$var];
102+
}
103+
}
104+
105+
$queryResult = $processor->processQuery($query, $arguments);
106+
107+
$variables['graphql_result'] = $queryResult->getData();
108+
109+
$variables['#cache']['contexts'] = $queryResult->getCacheContexts();
110+
$variables['#cache']['tags'] = $queryResult->getCacheTags();
111+
$variables['#cache']['max-age'] = $queryResult->getCacheMaxAge();
112+
}
113+
}
9114

10115
/**
11116
* Implements hook_theme().
@@ -39,21 +144,15 @@ function graphql_twig_theme($existing, $type, $theme, $path) {
39144
continue;
40145
}
41146

42-
$content = file_get_contents($file->uri);
43-
44-
preg_match(GraphQLNodeVisitor::$GRAPHQL_TWIG_REGEX, $content, $matches);
45-
if (array_key_exists('query', $matches)) {
46-
$source = (new \Youshido\GraphQL\Parser\Parser())->parse($matches['query']);
147+
if ($query = graphql_twig_get_query($file->uri)) {
47148
$themeRegistry[$hook] = [
48149
'template' => $template,
49150
'path' => dirname($file->uri),
50151
'theme path' => $path,
51152
'variables' => [],
52153
];
53-
54-
foreach ($source['variables'] as $variable) {
55-
/** @var \Youshido\GraphQL\Parser\Ast\ArgumentValue\Variable $variable */
56-
$themeRegistry[$hook]['variables'][$variable->getName()] = null;
154+
foreach (graphql_twig_get_query_variables($query) as $var) {
155+
$themeRegistry[$hook]['variables'][$var] = NULL;
57156
}
58157
}
59158
}
@@ -64,8 +163,12 @@ function graphql_twig_theme($existing, $type, $theme, $path) {
64163

65164
/**
66165
* Implements hook_module_implements_alter().
166+
*
167+
* Make sure graphql_twig_theme runs last to not accidentially override existing
168+
* theme hooks.
67169
*/
68170
function graphql_twig_module_implements_alter(&$implementations, $hook) {
171+
69172
if ($hook == 'theme') {
70173
unset($implementations['graphql_twig']);
71174
$implementations['graphql_twig'] = FALSE;
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
services:
22
graphql_twig.twig.embed:
33
class: Drupal\graphql_twig\GraphQLTwigExtension
4-
arguments: ['@renderer']
54
tags:
65
- { name: twig.extension }
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
namespace Drupal\graphql_twig;
4+
5+
/**
6+
* A Twig node for collecting GraphQL query fragments in twig templates.
7+
*/
8+
class GraphQLFragmentNode extends \Twig_Node {
9+
10+
/**
11+
* The fragment string.
12+
*
13+
* @var string
14+
*/
15+
protected $fragment = "";
16+
17+
/**
18+
* GraphQLFragmentNode constructor.
19+
*
20+
* @param string $fragment
21+
* The query fragment.
22+
*/
23+
public function __construct($fragment) {
24+
$this->fragment = $fragment;
25+
parent::__construct();
26+
}
27+
28+
/**
29+
* Retrieve the stored query fragment.
30+
*
31+
* @return string
32+
* The query fragment.
33+
*/
34+
public function getFragment() {
35+
return $this->fragment;
36+
}
37+
38+
}

modules/graphql_twig/src/GraphQLNode.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,44 @@
44

55
use Twig_Compiler;
66

7+
/**
8+
* GraphQL meta information Twig node.
9+
*
10+
* A Twig node that will be attached to templates `class_end` to output the
11+
* collected graphql query and inheritance metadata. Not parsed directly but
12+
* injected by the `GraphQLNodeVisitor`.
13+
*/
714
class GraphQLNode extends \Twig_Node {
815

16+
/**
17+
* The modules query string.
18+
*
19+
* @var string
20+
*/
921
protected $query = "";
22+
23+
/**
24+
* The modules parent class.
25+
*
26+
* @var string
27+
*/
1028
protected $parent = "";
29+
30+
/**
31+
* The modules includes.
32+
* @var array
33+
*/
1134
protected $includes = [];
1235

1336
/**
1437
* GraphQLNode constructor.
1538
*
1639
* @param string $query
40+
* The query string.
1741
* @param string $parent
42+
* The parent template identifier.
1843
* @param array $includes
44+
* Identifiers for any included/referenced templates.
1945
*/
2046
public function __construct($query, $parent, $includes) {
2147
$this->query = $query;
@@ -24,9 +50,14 @@ public function __construct($query, $parent, $includes) {
2450
parent::__construct();
2551
}
2652

53+
/**
54+
* {@inheritdoc}
55+
*/
2756
public function compile(Twig_Compiler $compiler) {
2857
$compiler
58+
// Make the template implement the GraphQLTemplateTrait.
2959
->write("\nuse \Drupal\graphql_twig\GraphQLTemplateTrait;\n")
60+
// Write metadata properties.
3061
->write("\nprotected \$graphqlQuery = ")
3162
->string($this->query)
3263
->write(";\n")

modules/graphql_twig/src/GraphQLNodeVisitor.php

Lines changed: 46 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,62 +5,91 @@
55
use Twig_Environment;
66
use Twig_Node;
77

8+
/**
9+
* Scans a Twig template for query fragments and references to other templates.
10+
*/
811
class GraphQLNodeVisitor extends \Twig_BaseNodeVisitor {
912

10-
public static $GRAPHQL_TWIG_REGEX = '/.*\{#graphql\s+(?<query>.*)\s+#\}.*/s';
11-
13+
/**
14+
* The query string.
15+
*
16+
* @var string
17+
*/
1218
protected $query = '';
19+
20+
/**
21+
* The parent template identifier.
22+
*
23+
* @var string
24+
*/
1325
protected $parent = '';
26+
27+
/**
28+
* A list of referenced templates (include, embed).
29+
*
30+
* @var string[]
31+
*/
1432
protected $includes = [];
1533

34+
/**
35+
* {@inheritdoc}
36+
*/
1637
public function getPriority() {
1738
return 0;
1839
}
1940

41+
/**
42+
* {@inheritdoc}
43+
*/
2044
protected function doEnterNode(Twig_Node $node, Twig_Environment $env) {
2145

2246
if ($node instanceof \Twig_Node_Module) {
2347

24-
if (!$node->hasAttribute('source')) {
25-
return $node;
26-
}
27-
28-
$this->query = '';
29-
$this->parent = '';
30-
$this->includes = [];
31-
32-
$source = $node->getAttribute('source');
33-
preg_match(static::$GRAPHQL_TWIG_REGEX, $source, $matches);
34-
35-
if (array_key_exists('query', $matches)) {
36-
$this->query = $matches['query'];
37-
}
38-
48+
// If there is a parent node (created by `extends` or `embed`),
49+
// store it's identifier.
3950
if ($node->hasNode('parent')) {
4051
$parent = $node->getNode('parent');
4152
if ($parent instanceof \Twig_Node_Expression_Constant) {
4253
$this->parent = $parent->getAttribute('value');
4354
}
4455
}
4556

57+
// Recurse into embedded templates.
4658
foreach ($node->getAttribute('embedded_templates') as $embed) {
4759
$this->doEnterNode($embed, $env);
4860
}
4961
}
5062

63+
// Store identifiers of any static includes.
64+
// There is no way to make this work for dynamic includes.
5165
if ($node instanceof \Twig_Node_Include && !($node instanceof \Twig_Node_Embed)) {
5266
$ref = $node->getNode('expr');
5367
if ($ref instanceof \Twig_Node_Expression_Constant) {
5468
$this->includes[] = $ref->getAttribute('value');
5569
}
5670
}
5771

72+
// When encountering a GraphQL fragment, add it to the current query.
73+
if ($node instanceof GraphQLFragmentNode) {
74+
$this->query .= $node->getFragment();
75+
}
76+
5877
return $node;
5978
}
6079

80+
/**
81+
* {@inheritdoc}
82+
*/
6183
protected function doLeaveNode(Twig_Node $node, Twig_Environment $env) {
6284
if ($node instanceof \Twig_Node_Module) {
85+
// Store current query information to be compiled into the templates
86+
// `class_end`.
6387
$node->setNode('class_end', new GraphQLNode($this->query, $this->parent, $this->includes));
88+
89+
// Reset query information for the next module.
90+
$this->query = '';
91+
$this->parent = '';
92+
$this->includes = [];
6493
}
6594
return $node;
6695
}

0 commit comments

Comments
 (0)