├── tests
├── queries
│ ├── echo.gql
│ └── garage.gql
├── themes
│ └── graphql_twig_test_theme
│ │ ├── templates
│ │ ├── reset
│ │ │ ├── html.html.twig
│ │ │ └── page.html.twig
│ │ ├── node.html.twig
│ │ ├── pages
│ │ │ ├── static.html.twig
│ │ │ ├── no-title.html.twig
│ │ │ ├── error.html.twig
│ │ │ ├── no-arguments.html.twig
│ │ │ ├── one-argument.html.twig
│ │ │ └── multiple-arguments.html.twig
│ │ ├── blocks
│ │ │ ├── block-static.html.twig
│ │ │ ├── block-no-arguments.html.twig
│ │ │ ├── block-one-argument.html.twig
│ │ │ └── block-multiple-arguments.html.twig
│ │ ├── node.html.twig.gql
│ │ ├── graphql-echo.html.twig
│ │ └── graphql-garage.html.twig
│ │ ├── components
│ │ └── graphql-vehicle.twig
│ │ ├── graphql_twig_test_theme.theme
│ │ └── graphql_twig_test_theme.info.yml
└── src
│ ├── Traits
│ └── ThemeTestTrait.php
│ ├── Kernel
│ ├── ThemeTest.php
│ ├── EntityRenderTest.php
│ ├── BlockTest.php
│ └── RouteTest.php
│ └── Unit
│ └── GraphQLTwigExtensionTest.php
├── .scrutinizer.yml
├── assets
├── graphql.png
├── debug.js
└── debug.css
├── config
├── install
│ └── graphql_twig.settings.yml
└── schema
│ └── graphql_twig.schema.yml
├── graphql_twig.info.yml
├── graphql_twig.links.menu.yml
├── graphql_twig.libraries.yml
├── graphql_twig.routing.yml
├── composer.json
├── src
├── GraphQLTwigExtension.php
├── GraphQLFragmentNode.php
├── Plugin
│ ├── GraphQL
│ │ └── Fields
│ │ │ └── CurrentUrl.php
│ ├── Deriver
│ │ └── GraphQLTwigBlockDeriver.php
│ └── Block
│ │ └── GraphQLTwigBlock.php
├── GraphqlTwigServiceProvider.php
├── Controller
│ ├── ArgumentResolver.php
│ └── RouteController.php
├── GraphQLTokenParser.php
├── Routing
│ └── GraphQLTwigRouter.php
├── GraphQLNodeVisitor.php
├── Form
│ └── GraphQLTwigSettingsForm.php
├── Template
│ └── Loader
│ │ └── Loader.php
├── GraphQLNode.php
├── GraphQLTwigEnvironment.php
└── GraphQLTemplateTrait.php
├── phpcs.xml.dist
├── graphql_twig.services.yml
├── graphql_twig.module
├── README.md
└── .travis.yml
/tests/queries/echo.gql:
--------------------------------------------------------------------------------
1 | query($input: String!) {
2 | echo(input: $input)
3 | }
--------------------------------------------------------------------------------
/tests/themes/graphql_twig_test_theme/templates/reset/html.html.twig:
--------------------------------------------------------------------------------
1 | {{ page }}
2 |
--------------------------------------------------------------------------------
/tests/themes/graphql_twig_test_theme/templates/reset/page.html.twig:
--------------------------------------------------------------------------------
1 | {{ page.content }}
2 |
--------------------------------------------------------------------------------
/.scrutinizer.yml:
--------------------------------------------------------------------------------
1 | filter:
2 | excluded_paths:
3 | - 'tests/*'
4 |
5 | checks:
6 | php: true
7 |
--------------------------------------------------------------------------------
/assets/graphql.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/drupal-graphql/graphql-twig/HEAD/assets/graphql.png
--------------------------------------------------------------------------------
/tests/themes/graphql_twig_test_theme/templates/node.html.twig:
--------------------------------------------------------------------------------
1 |
{{ graphql.node.title }}
2 |
--------------------------------------------------------------------------------
/tests/themes/graphql_twig_test_theme/templates/pages/static.html.twig:
--------------------------------------------------------------------------------
1 | This page is static.
2 |
--------------------------------------------------------------------------------
/tests/themes/graphql_twig_test_theme/templates/pages/no-title.html.twig:
--------------------------------------------------------------------------------
1 | This page has no title.
2 |
--------------------------------------------------------------------------------
/tests/themes/graphql_twig_test_theme/templates/blocks/block-static.html.twig:
--------------------------------------------------------------------------------
1 | This is a static block.
2 |
--------------------------------------------------------------------------------
/config/install/graphql_twig.settings.yml:
--------------------------------------------------------------------------------
1 | # Debug output mode inside twig files: wrapped vs inside
2 | debug_placement: 'wrapped'
3 |
--------------------------------------------------------------------------------
/tests/themes/graphql_twig_test_theme/templates/pages/error.html.twig:
--------------------------------------------------------------------------------
1 | {#graphql
2 | query {
3 | shout
4 | }
5 | #}
6 | This page will lead to an error.
7 |
--------------------------------------------------------------------------------
/tests/themes/graphql_twig_test_theme/templates/node.html.twig.gql:
--------------------------------------------------------------------------------
1 | query($node: String!) {
2 | node:nodeById(id: $node) {
3 | title:entityLabel
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/tests/queries/garage.gql:
--------------------------------------------------------------------------------
1 | query {
2 | garage {
3 | type
4 | ... graphql_vehicle
5 | }
6 | }
7 | fragment graphql_vehicle on Vehicle {
8 | type
9 | wheels
10 | }
--------------------------------------------------------------------------------
/tests/themes/graphql_twig_test_theme/templates/graphql-echo.html.twig:
--------------------------------------------------------------------------------
1 | {#graphql
2 | query($input: String!) {
3 | echo(input: $input)
4 | }
5 | #}
6 | {{ graphql.echo }}
7 |
--------------------------------------------------------------------------------
/tests/themes/graphql_twig_test_theme/components/graphql-vehicle.twig:
--------------------------------------------------------------------------------
1 | {#graphql
2 | fragment graphql_vehicle on Vehicle {
3 | type
4 | wheels
5 | }
6 | #}
7 | A {{ type }} with {{ wheels }} wheels.
--------------------------------------------------------------------------------
/tests/themes/graphql_twig_test_theme/templates/blocks/block-no-arguments.html.twig:
--------------------------------------------------------------------------------
1 | {#graphql
2 | query {
3 | shout(word: "drupal")
4 | }
5 | #}
6 | This block shouts: {{ graphql.shout }}
7 |
--------------------------------------------------------------------------------
/tests/themes/graphql_twig_test_theme/templates/pages/no-arguments.html.twig:
--------------------------------------------------------------------------------
1 | {#graphql
2 | query {
3 | shout(word: "Drupal")
4 | }
5 | #}
6 | This page is supposed to shout: {{ graphql.shout }}
7 |
--------------------------------------------------------------------------------
/tests/themes/graphql_twig_test_theme/templates/blocks/block-one-argument.html.twig:
--------------------------------------------------------------------------------
1 | {#graphql
2 | query ($first: String!) {
3 | shout(word: $first)
4 | }
5 | #}
6 | This block shouts: {{ graphql.shout }}
7 |
--------------------------------------------------------------------------------
/graphql_twig.info.yml:
--------------------------------------------------------------------------------
1 | name: GraphQL Twig
2 | type: module
3 | description: 'Render twig templates with graphql data.'
4 | package: GraphQL
5 | core: 8.x
6 | dependencies:
7 | - graphql
8 | - drupal:system (>= 8.6)
9 |
--------------------------------------------------------------------------------
/tests/themes/graphql_twig_test_theme/templates/pages/one-argument.html.twig:
--------------------------------------------------------------------------------
1 | {#graphql
2 | query ($first: String!) {
3 | shout(word: $first)
4 | }
5 | #}
6 | This page is supposed to shout: {{ graphql.shout }}
7 |
--------------------------------------------------------------------------------
/graphql_twig.links.menu.yml:
--------------------------------------------------------------------------------
1 | graphql_twig.settings:
2 | title: 'GraphQL Twig'
3 | parent: system.admin_config_services
4 | description: 'Configure graphql twig.'
5 | route_name: graphql_twig.settings
6 | weight: 99
7 |
--------------------------------------------------------------------------------
/graphql_twig.libraries.yml:
--------------------------------------------------------------------------------
1 | debug:
2 | version: 1.x
3 | css:
4 | theme:
5 | assets/debug.css: {}
6 | js:
7 | assets/debug.js: {}
8 | dependencies:
9 | - core/jquery
10 | - core/jquery.once
11 | - core/drupal
12 |
--------------------------------------------------------------------------------
/tests/themes/graphql_twig_test_theme/templates/blocks/block-multiple-arguments.html.twig:
--------------------------------------------------------------------------------
1 | {#graphql
2 | query ($first: String!, $second: String!) {
3 | first:shout(word: $first)
4 | second:shout(word: $second)
5 | }
6 | #}
7 | This block shouts: {{ graphql.first }} and {{ graphql.second }}
8 |
--------------------------------------------------------------------------------
/tests/themes/graphql_twig_test_theme/templates/graphql-garage.html.twig:
--------------------------------------------------------------------------------
1 | {#graphql
2 | query {
3 | garage {
4 | type
5 | ... graphql_vehicle
6 | }
7 | }
8 | #}
9 | Garage:
10 | {% for vehicle in graphql.garage %}
11 | {% include '#graphql-vehicle' with vehicle %}
12 | {% endfor %}
13 |
--------------------------------------------------------------------------------
/tests/themes/graphql_twig_test_theme/templates/pages/multiple-arguments.html.twig:
--------------------------------------------------------------------------------
1 | {#graphql
2 | query ($first: String!, $second: String!) {
3 | first:shout(word: $first)
4 | second:shout(word: $second)
5 | }
6 | #}
7 | This page is supposed to shout: {{ graphql.first }} and {{ graphql.second }}
8 |
--------------------------------------------------------------------------------
/graphql_twig.routing.yml:
--------------------------------------------------------------------------------
1 | route_callbacks:
2 | - 'graphql_twig.router:routes'
3 |
4 | graphql_twig.settings:
5 | path: '/admin/config/services/graphql_twig'
6 | defaults:
7 | _form: 'Drupal\graphql_twig\Form\GraphQLTwigSettingsForm'
8 | _title: 'GraphQL Twig'
9 | requirements:
10 | _permission: 'administer site configuration'
11 |
--------------------------------------------------------------------------------
/tests/themes/graphql_twig_test_theme/graphql_twig_test_theme.theme:
--------------------------------------------------------------------------------
1 | [
14 | 'variables' => ['input' => ''],
15 | ],
16 | 'graphql_garage' => [
17 | 'variables' => [],
18 | ],
19 | ];
20 | }
21 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "drupal/graphql_twig",
3 | "type": "drupal-module",
4 | "description": "Feed Twig templates with GraphQL.",
5 | "homepage": "http://drupal.org/project/graphql_twig",
6 | "authors": [
7 | {
8 | "name": "Philipp Melab",
9 | "homepage": "https://www.drupal.org/u/pmelab"
10 | }
11 | ],
12 | "support": {
13 | "issues": "https://github.com/drupal-graphql/graphql-twig"
14 | },
15 | "license": "GPL-2.0+",
16 | "minimum-stability": "dev"
17 | }
18 |
--------------------------------------------------------------------------------
/src/GraphQLTwigExtension.php:
--------------------------------------------------------------------------------
1 |
2 |
3 | Default PHP CodeSniffer configuration for GraphQL.
4 | .
5 |
6 |
7 |
8 |
9 |
10 | src/Annotation
11 | src/Core/Annotation
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/GraphQLFragmentNode.php:
--------------------------------------------------------------------------------
1 | fragment = $fragment;
28 | parent::__construct();
29 | }
30 |
31 | }
32 |
--------------------------------------------------------------------------------
/src/Plugin/GraphQL/Fields/CurrentUrl.php:
--------------------------------------------------------------------------------
1 | ');
31 | }
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/graphql_twig.services.yml:
--------------------------------------------------------------------------------
1 | services:
2 | graphql_twig.twig.embed:
3 | class: Drupal\graphql_twig\GraphQLTwigExtension
4 | tags:
5 | - { name: twig.extension }
6 | graphql_twig.router:
7 | class: Drupal\graphql_twig\Routing\GraphQLTwigRouter
8 | arguments:
9 | - '@theme_handler'
10 | argument_resolver.graphql_twig:
11 | class: Drupal\graphql_twig\Controller\ArgumentResolver
12 |
13 | cache.graphql_twig.components:
14 | class: Drupal\Core\Cache\CacheBackendInterface
15 | tags:
16 | - { name: cache.bin }
17 | factory: cache_factory:get
18 | arguments: [graphql_twig_components]
19 |
20 | twig.loader.graphql:
21 | class: Drupal\graphql_twig\Template\Loader\Loader
22 | arguments:
23 | - '@theme.manager'
24 | - '%twig.config%'
25 | - '@cache.graphql_twig.components'
26 | - '@app.root'
27 | tags:
28 | - { name: twig.loader, priority: 1000 }
29 |
--------------------------------------------------------------------------------
/assets/debug.js:
--------------------------------------------------------------------------------
1 | (function ($, Drupal) {
2 | Drupal.behaviors.graphqlTwigDebug = {
3 | attach: function (context, settings) {
4 | $('.graphql-twig-debug-wrapper', context).once('graphql-debug').each(function () {
5 | var query = $(this).attr('data-graphql-query'),
6 | variables = $(this).attr('data-graphql-variables'),
7 | $form = $('').attr('action', Drupal.url('graphql/explorer')).appendTo(this),
8 | txt = document.createElement('textarea');
9 |
10 | txt.innerHTML = variables;
11 | variables = txt.value;
12 | $('').val(query).appendTo($form);
13 | $('').val(variables).appendTo($form);
14 | $('').appendTo($form);
15 | });
16 | }
17 | };
18 | }(jQuery, Drupal));
19 |
--------------------------------------------------------------------------------
/assets/debug.css:
--------------------------------------------------------------------------------
1 | .graphql-twig-errors {
2 | list-style: none;
3 | border: 1px solid #d80000;
4 | background: #ffdbe3;
5 | color: #a10000;
6 | padding: 1em;
7 | }
8 |
9 | .graphql-twig-errors li:not(:first-child) {
10 | margin-top: 1em;
11 | }
12 |
13 | .graphql-twig-debug-wrapper {
14 | position: relative;
15 | }
16 |
17 | .graphql-twig-debug-wrapper form {
18 | margin: 0!important;
19 | padding: 0!important;
20 | }
21 |
22 | input[type="submit"].graphql-twig-debug-button {
23 | z-index: 99;
24 | position: absolute;
25 | text-indent: -999em;
26 | border-radius: 50%;
27 | background: url(graphql.png) white center center no-repeat;
28 | background-size: 20px 20px;
29 | border: 1px solid #CCC;
30 | width: 26px;
31 | height: 26px;
32 | }
33 |
34 | .graphql-twig-debug-wrapper .graphql-twig-debug-button {
35 | position: absolute;
36 | top: 4px;
37 | left: 4px;
38 | display: none;
39 | }
40 |
41 | .graphql-twig-debug-parent:hover > .graphql-twig-debug-child .graphql-twig-debug-button,
42 | .graphql-twig-debug-wrapper:hover > form > .graphql-twig-debug-button {
43 | display: block;
44 | }
45 |
--------------------------------------------------------------------------------
/src/GraphqlTwigServiceProvider.php:
--------------------------------------------------------------------------------
1 | getDefinition('twig')
20 | ->setClass(GraphQLTwigEnvironment::class)
21 | ->addArgument(new Reference('graphql.query_processor'))
22 | ->addArgument(new Reference('renderer'));
23 |
24 | // Inject our own argument resolver.
25 | $def = $container->getDefinition('http_kernel.controller.argument_resolver');
26 | $argumentResolvers = $def->getArgument(1);
27 | $argumentResolvers[] = new Reference('argument_resolver.graphql_twig');
28 | $def->setArgument(1, $argumentResolvers);
29 | }
30 |
31 | }
32 |
--------------------------------------------------------------------------------
/src/Controller/ArgumentResolver.php:
--------------------------------------------------------------------------------
1 | getName(), [
23 | '_graphql_arguments',
24 | '_graphql_theme_hook',
25 | '_graphql_title',
26 | '_graphql_title_query',
27 | ]);
28 | }
29 |
30 | /**
31 | * @inheritdoc
32 | */
33 | public function resolve(Request $request, ArgumentMetadata $argument) {
34 | switch($argument->getName()) {
35 | case '_graphql_arguments':
36 | yield $request->attributes->get('_raw_variables')->all();
37 | break;
38 | case '_graphql_theme_hook':
39 | yield $request->attributes->get('_theme_hook');
40 | break;
41 | case '_graphql_title':
42 | yield $request->attributes->get('_title');
43 | break;
44 | case '_graphql_title_query':
45 | yield $request->attributes->get('_title_query');
46 | break;
47 | }
48 | }
49 |
50 |
51 | }
52 |
--------------------------------------------------------------------------------
/src/Plugin/Deriver/GraphQLTwigBlockDeriver.php:
--------------------------------------------------------------------------------
1 | get('theme_handler'));
21 | }
22 |
23 | public function __construct(ThemeHandlerInterface $themeHandler) {
24 | $this->themeHandler = $themeHandler;
25 | }
26 |
27 | public function getDerivativeDefinitions($base_plugin_definition) {
28 | foreach ($this->themeHandler->listInfo() as $themeName => $info) {
29 | if (isset($info->info['blocks'])) {
30 | foreach ($info->info['blocks'] as $name => $block) {
31 | $this->derivatives[$name] = [
32 | 'admin_label' => $this->t($block['label']),
33 | 'graphql_theme_hook' => $name,
34 | 'graphql_parameters' => isset($block['parameters']) ? $block['parameters'] : [],
35 | ] + $base_plugin_definition;
36 | }
37 | }
38 | }
39 | return $this->derivatives;
40 | }
41 |
42 |
43 | }
44 |
--------------------------------------------------------------------------------
/graphql_twig.module:
--------------------------------------------------------------------------------
1 | listInfo() as $themeName => $info) {
13 |
14 | // Register twig routes as theme hooks.
15 | if (isset($info->info['routes'])) {
16 | foreach ($info->info['routes'] as $name => $route) {
17 | $theme[$name] = [
18 | 'variables' => [
19 | 'graphql_arguments' => [],
20 | 'graphql_ext' => $name,
21 | ],
22 | 'function' => '_graphql_twig_missing_template',
23 | ];
24 | }
25 | }
26 |
27 | // Register twig blocks as theme hooks.
28 | if (isset($info->info['blocks'])) {
29 | foreach ($info->info['blocks'] as $name => $block) {
30 | $theme[$name] = [
31 | 'variables' => [
32 | 'graphql_arguments' => [],
33 | 'graphql_ext' => $name,
34 | ],
35 | 'function' => '_graphql_twig_missing_template',
36 | ];
37 | }
38 | }
39 |
40 | }
41 | return $theme;
42 | }
43 |
44 | /**
45 | * Emits an error message if a dynamic route template is missing.
46 | */
47 | function _graphql_twig_missing_template($variables) {
48 | return '' . t('Missing template for %ext.', [
49 | '%ext' => $variables['graphql_ext'],
50 | ]) . '
';
51 | }
52 |
--------------------------------------------------------------------------------
/src/GraphQLTokenParser.php:
--------------------------------------------------------------------------------
1 | parser->getStream();
21 | if (!$this->parser->isMainScope()) {
22 | throw new Twig_Error_Syntax(
23 | 'GraphQL queries cannot be defined in blocks.',
24 | $token->getLine(),
25 | $stream->getSourceContext()
26 | );
27 | }
28 |
29 | $stream->expect(Twig_Token::BLOCK_END_TYPE);
30 | $values = $this->parser->subparse([$this, 'decideBlockEnd'], TRUE);
31 | $stream->expect(Twig_Token::BLOCK_END_TYPE);
32 | if ($values instanceof \Twig_Node_Text) {
33 | try {
34 | return new GraphQLFragmentNode($values->getAttribute('data'));
35 | }
36 | catch (SyntaxError $error) {
37 | throw new Twig_Error_Syntax(
38 | $error->getMessage(),
39 | $token->getLine(),
40 | $stream->getSourceContext()
41 | );
42 | }
43 | }
44 |
45 | return NULL;
46 | }
47 |
48 | /**
49 | * {@inheritdoc}
50 | */
51 | public function decideBlockEnd(Twig_Token $token) {
52 | return $token->test('endgraphql');
53 | }
54 |
55 | /**
56 | * {@inheritdoc}
57 | */
58 | public function getTag() {
59 | return 'graphql';
60 | }
61 |
62 | }
63 |
--------------------------------------------------------------------------------
/src/Routing/GraphQLTwigRouter.php:
--------------------------------------------------------------------------------
1 | themeHandler = $themeHandler;
26 | }
27 |
28 | /**
29 | * Generate a list of routes based on theme info files.
30 | *
31 | * @return \Symfony\Component\Routing\Route[]
32 | * A list of routes defined by themes.
33 | */
34 | public function routes() {
35 | $routes = [];
36 | foreach ($this->themeHandler->listInfo() as $info) {
37 | if (isset($info->info['routes'])) {
38 | foreach ($info->info['routes'] as $name => $route) {
39 | $routes['graphql_twig.dynamic.' . $name] = new Route($route['path'], [
40 | '_controller' => RouteController::class . ':page',
41 | '_title_callback' => RouteController::class . ':title',
42 | '_title' => isset($route['title']) ? $route['title'] : NULL,
43 | '_title_query' => isset($route['title_query']) ? $route['title_query'] : NULL,
44 | '_graphql_theme_hook' => $name,
45 | ], isset($route['requirements']) ? $route['requirements'] : [
46 | '_access' => 'TRUE',
47 | ]);
48 | }
49 |
50 | }
51 | }
52 | return $routes;
53 | }
54 |
55 | }
56 |
--------------------------------------------------------------------------------
/tests/src/Traits/ThemeTestTrait.php:
--------------------------------------------------------------------------------
1 | prophesize(AccountProxy::class);
25 | $currentUser->isAuthenticated()->willReturn(TRUE);
26 | $currentUser->hasPermission(Argument::any())->willReturn(TRUE);
27 | $currentUser->id()->willReturn(1);
28 | $currentUser->getRoles()->willReturn(['administrator']);
29 | $this->container->set('current_user', $currentUser->reveal());
30 |
31 | // Prepare a mock graphql processor.
32 | $this->processor = $this->prophesize(QueryProcessor::class);
33 | $this->container->set('graphql.query_processor', $this->processor->reveal());
34 |
35 | $themeName = 'graphql_twig_test_theme';
36 |
37 | /** @var \Drupal\Core\Extension\ThemeHandler $themeHandler */
38 | $themeHandler = $this->container->get('theme_handler');
39 | /** @var \Drupal\Core\Theme\ThemeInitialization $themeInitialization */
40 | $themeInitialization = $this->container->get('theme.initialization');
41 | /** @var \Drupal\Core\Theme\ThemeManager $themeManager */
42 | $themeManager = $this->container->get('theme.manager');
43 |
44 | $themeHandler->install([$themeName]);
45 | $theme = $themeInitialization->initTheme($themeName);
46 | $themeManager->setActiveTheme($theme);
47 | }
48 | }
49 | }
--------------------------------------------------------------------------------
/tests/themes/graphql_twig_test_theme/graphql_twig_test_theme.info.yml:
--------------------------------------------------------------------------------
1 | name: GraphQL Twig test
2 | type: theme
3 | description: 'Test theme for GraphQL Twig integration'
4 | package: GraphQL
5 | version: VERSION
6 | core: 8.x
7 |
8 | routes:
9 | no_arguments:
10 | path: '/no-args'
11 | title: 'Shouting: DRUPAL'
12 | title_query: 'query { shout(word: "drupal") }'
13 | requirements:
14 | _access: 'TRUE'
15 |
16 | one_argument:
17 | path: '/one-arg/{first}'
18 | title: 'Shouting: {{shout}}'
19 | title_query: 'query ($first: String!) { shout(word: $first) }'
20 | requirements:
21 | _access: 'TRUE'
22 |
23 | multiple_arguments:
24 | path: '/multi-args/{first}/{second}'
25 | title: 'Shouting: {{first}} and {{second}}'
26 | title_query: 'query ($first: String!, $second: String!) { first:shout(word: $first) second:shout(word: $second) }'
27 | requirements:
28 | _access: 'TRUE'
29 |
30 | static:
31 | path: '/static'
32 | title: 'This is a static page'
33 | requirements:
34 | _access: 'TRUE'
35 |
36 | no_title:
37 | path: '/no-title'
38 | requirements:
39 | _access: 'TRUE'
40 |
41 | no_access:
42 | path: '/no-access'
43 | requirements:
44 | _access: 'FALSE'
45 |
46 | missing:
47 | path: '/missing'
48 | title: 'Missing template'
49 | requirements:
50 | _access: 'TRUE'
51 |
52 | error:
53 | path: '/error'
54 | title: 'Query error'
55 | requirements:
56 | _access: 'TRUE'
57 |
58 |
59 |
60 | blocks:
61 |
62 | block_no_arguments:
63 | label: 'No Arguments'
64 |
65 | block_one_argument:
66 | label: 'One Argument'
67 | parameters:
68 | first:
69 | title: First
70 | type: textfield
71 |
72 | block_multiple_arguments:
73 | label: 'Multiple Arguments'
74 | parameters:
75 | first:
76 | title: First
77 | type: textfield
78 | second:
79 | title: First
80 | type: textfield
81 |
82 | block_static:
83 | label: 'Static block'
84 |
85 | block_missing:
86 | label: 'Missing template'
87 |
--------------------------------------------------------------------------------
/src/Plugin/Block/GraphQLTwigBlock.php:
--------------------------------------------------------------------------------
1 | configuration['graphql_block'])) {
30 | foreach ($this->configuration['graphql_block'] as $arg) {
31 | $arguments[$arg['key']] = $arg['value'];
32 | }
33 | }
34 |
35 | foreach ($this->pluginDefinition['graphql_parameters'] as $name => $el) {
36 | foreach ($el as $key => $value) {
37 | if (in_array($key, ['title', 'description'])) {
38 | $form['graphql_block'][$name]['#' . $key] = $this->t($value);
39 | }
40 | else {
41 | $form['graphql_block'][$name]['#' . $key] = $value;
42 | }
43 |
44 | $form['graphql_block'][$name]['#default_value'] = isset($arguments[$name]) ? $arguments[$name] : '';
45 | }
46 | }
47 |
48 | return $form;
49 | }
50 |
51 | /**
52 | * @inheritdoc
53 | */
54 | public function blockSubmit($form, FormStateInterface $form_state) {
55 | parent::blockSubmit($form, $form_state);
56 |
57 | foreach (array_keys($this->pluginDefinition['graphql_parameters']) as $arg) {
58 | $values = $form_state->getValues();
59 | $this->configuration['graphql_block'][] = [
60 | 'key' => $arg,
61 | 'value' => $values['graphql_block'][$arg],
62 | ];
63 | }
64 | }
65 |
66 | /**
67 | * @inheritdoc
68 | */
69 | public function build() {
70 | $arguments = [];
71 |
72 | foreach ($this->configuration['graphql_block'] as $arg) {
73 | $arguments[$arg['key']] = $arg['value'];
74 | }
75 |
76 | return [
77 | '#theme' => $this->pluginDefinition['graphql_theme_hook'],
78 | '#graphql_arguments' => $arguments,
79 | ];
80 | }
81 |
82 | }
83 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # GraphQL Twig for Drupal
2 |
3 | [](https://travis-ci.org/drupal-graphql/graphql-twig)
4 | [](https://codecov.io/gh/drupal-graphql/graphql-twig)
5 | [](https://scrutinizer-ci.com/g/drupal-graphql/graphql-twig/?branch=8.x-1.x)
6 |
7 | The GraphQL Twig module allows you to inject data into Twig templates by simply adding
8 | a GraphQL query. No site building or pre-processing necessary.
9 |
10 | ## Requirements
11 |
12 | * Drupal >= 8.6
13 | * PHP >= 7.0
14 |
15 | ## Simple example
16 |
17 | The *"Powered by Drupal"* block only gives credit to Drupal, but what's a website
18 | without administrators and users? Right. Lets fix that.
19 |
20 | Place the following template override for the *"Powered by Drupal"* block in your theme:
21 |
22 | ```twig
23 | {#graphql
24 | query {
25 | admin:userById(id: "1") {
26 | uid
27 | name
28 | }
29 | user:currentUserContext {
30 | uid
31 | }
32 | }
33 | #}
34 |
35 | {% extends '@bartik/block.html.twig' %}
36 | {% block content %}
37 | {% embed '@graphql_twig/query.html.twig' %}
38 | {% block content %}
39 | {% set admin = graphql.admin %}
40 | {% set user = graphql.user %}
41 |
42 | {{ content }} and
43 | {% if user.uid == admin.uid %}
44 | you, {{ admin.name }}.
45 | {% else %}
46 | you, appreciated anonymous visitor.
47 | {% endif %}
48 |
49 | {% endblock %}
50 | {% endembed %}
51 | {% endblock %}
52 | ```
53 |
54 | For the sake of an example, we assumed that you based your theme on Bartik (which you probably didn't), but
55 | what else happens here? In the `{#graphql ... #}` comment block we annotated a simple GraphQL query, that will
56 | be executed in an additional preprocessing step, to populate your template with an additional variable called
57 | `graphql` that will contain the result. We injected additional information into our template without the need
58 | to fall back to site building or manual preprocessing.
59 | The templates content is wrapped in an embed of `@graphql_twig/query.html.twig`. This is not
60 | necessary, but will emit debug information if Twig's debug setting is set to `true`.
61 |
62 | This is the basic concept of GraphQL in Twig. Additionally it will collect query fragments from
63 | included templates, automatically try to match template variables to query arguments and enable you to tap
64 | into all features of the [Drupal GraphQL] module.
65 |
66 | More documentation coming soon!
67 |
68 | [Drupal GraphQL]: https://github.com/drupal-graphql/graphql
69 |
--------------------------------------------------------------------------------
/src/GraphQLNodeVisitor.php:
--------------------------------------------------------------------------------
1 | hasNode('parent')) {
51 | $parent = $node->getNode('parent');
52 | if ($parent instanceof \Twig_Node_Expression_Constant) {
53 | $this->parent = $parent->getAttribute('value');
54 | }
55 | }
56 |
57 | // Recurse into embedded templates.
58 | foreach ($node->getAttribute('embedded_templates') as $embed) {
59 | $this->doEnterNode($embed, $env);
60 | }
61 | }
62 |
63 | // Store identifiers of any static includes.
64 | // There is no way to make this work for dynamic includes.
65 | if ($node instanceof \Twig_Node_Include && !($node instanceof \Twig_Node_Embed)) {
66 | $ref = $node->getNode('expr');
67 | if ($ref instanceof \Twig_Node_Expression_Constant) {
68 | $this->includes[$node->getTemplateName()][] = $ref->getAttribute('value');
69 | }
70 | }
71 |
72 | // When encountering a GraphQL fragment, add it to the current query.
73 | if ($node instanceof GraphQLFragmentNode) {
74 | $this->query .= $node->fragment;
75 | }
76 |
77 | return $node;
78 | }
79 |
80 | /**
81 | * {@inheritdoc}
82 | */
83 | protected function doLeaveNode(Twig_Node $node, Twig_Environment $env) {
84 | if ($node instanceof \Twig_Node_Module) {
85 | // Store current query information to be compiled into the templates
86 | // `class_end`.
87 | $includes = isset($this->includes[$node->getTemplateName()]) ? $this->includes[$node->getTemplateName()] : [];
88 | $node->setNode('class_end', new GraphQLNode($this->query, $this->parent, $includes));
89 |
90 | // Reset query information for the next module.
91 | $this->query = '';
92 | $this->parent = '';
93 | }
94 | return $node;
95 | }
96 |
97 | }
98 |
--------------------------------------------------------------------------------
/src/Form/GraphQLTwigSettingsForm.php:
--------------------------------------------------------------------------------
1 | get('config.factory')
43 | );
44 | }
45 |
46 | /**
47 | * {@inheritdoc}
48 | */
49 | public function getFormId() {
50 | return 'graphql_twig_settings';
51 | }
52 |
53 | /**
54 | * {@inheritdoc}
55 | */
56 | protected function getEditableConfigNames() {
57 | return ['graphql_twig.settings'];
58 | }
59 |
60 | /**
61 | * {@inheritdoc}
62 | */
63 | public function buildForm(array $form, FormStateInterface $form_state) {
64 | $site_config = $this->config('graphql_twig.settings');
65 | $debug_placement = $site_config->get('debug_placement');
66 |
67 | $form['debug'] = [
68 | '#type' => 'details',
69 | '#title' => t('Debug'),
70 | '#open' => TRUE,
71 | ];
72 | $form['debug']['debug_placement'] = [
73 | '#type' => 'select',
74 | '#title' => t('Debug placement'),
75 | '#options' => [
76 | 'wrapped' => $this->t('wrapped'),
77 | 'inside' => $this->t('inside'),
78 | ],
79 | '#default_value' => $debug_placement,
80 | '#required' => TRUE,
81 | ];
82 |
83 | return parent::buildForm($form, $form_state);
84 | }
85 |
86 | /**
87 | * {@inheritdoc}
88 | */
89 | public function submitForm(array &$form, FormStateInterface $form_state) {
90 | $this->config('graphql_twig.settings')
91 | ->set('debug_placement', $form_state->getValue('debug_placement'))
92 | ->save();
93 |
94 | parent::submitForm($form, $form_state);
95 | }
96 |
97 | }
98 |
--------------------------------------------------------------------------------
/src/Template/Loader/Loader.php:
--------------------------------------------------------------------------------
1 | cacheBackend = $cacheBackend;
44 | $this->twigConfig = $twigConfig;
45 | $this->themeManager = $themeManager;
46 | }
47 |
48 | /**
49 | * List all components found within a specific path.
50 | *
51 | * @param string $path
52 | * The directory to scan for
53 | *
54 | * @return string[]
55 | * Map of component filenames keyed by component handle.
56 | */
57 | protected function listComponents($path) {
58 | if ($this->twigConfig['cache'] && $cache = $this->cacheBackend->get($path)) {
59 | return $cache->data;
60 | }
61 |
62 | foreach (file_scan_directory($path, '/.*\.twig$/') as $file) {
63 | $this->components[$file->name] = $file->uri;
64 | }
65 |
66 | if ($this->twigConfig['cache']) {
67 | $this->cacheBackend->set($path, $this->components);
68 | }
69 |
70 | return $this->components;
71 |
72 | }
73 |
74 | /**
75 | * {@inheritdoc}
76 | */
77 | protected function findTemplate($name) {
78 | if (is_null($this->components)) {
79 | // Scan the directory for any twig files and register them.
80 | // TODO: inherit components from base theme
81 | $activeTheme = $this->themeManager->getActiveTheme();
82 | $info = system_get_info('theme', $activeTheme->getName());
83 | $componentsDirectory = array_key_exists('components', $info)
84 | ? $info['components']
85 | : $activeTheme->getPath() . '/components';
86 | $this->components = $this->listComponents($componentsDirectory);
87 | }
88 |
89 | if ($name[0] === '#') {
90 | $component = substr($name, 1);
91 | if (array_key_exists($component, $this->components)) {
92 | return $this->components[$component];
93 | }
94 | }
95 |
96 | return FALSE;
97 | }
98 |
99 | }
100 |
--------------------------------------------------------------------------------
/src/Controller/RouteController.php:
--------------------------------------------------------------------------------
1 | get('graphql.query_processor'),
32 | $container->get('twig')
33 | );
34 | }
35 |
36 | /**
37 | * RouteController constructor.
38 | *
39 | * @param \Drupal\graphql\GraphQL\Execution\QueryProcessor $processor
40 | * A GraphQL query processor.
41 | * @param \Drupal\Core\Template\TwigEnvironment $twig
42 | * A Twig environment.
43 | */
44 | public function __construct(QueryProcessor $processor, TwigEnvironment $twig) {
45 | $this->queryProcessor = $processor;
46 | $this->twig = $twig;
47 | }
48 |
49 | /**
50 | * Generic page callback.
51 | *
52 | * Accepts a theme hook and an array of theme variables.
53 | *
54 | * @param $_graphql_theme_hook
55 | * The theme hook.
56 | * @param $_graphql_arguments
57 | * Query arguments
58 | *
59 | * @return array
60 | * The render array build.
61 | */
62 | public function page($_graphql_theme_hook, $_graphql_arguments) {
63 | return [
64 | '#theme' => $_graphql_theme_hook,
65 | '#graphql_arguments' => $_graphql_arguments,
66 | ];
67 | }
68 |
69 | /**
70 | * Build a page title from a twig template and a GraphQL query.
71 | *
72 | * @param $_graphql_title
73 | * @param $_graphql_title_query
74 | * @param $_graphql_arguments
75 | *
76 | * @return \Drupal\Component\Render\MarkupInterface|\Drupal\Core\StringTranslation\TranslatableMarkup|string
77 | * @throws \Drupal\Component\Plugin\Exception\PluginException
78 | */
79 | public function title($_graphql_title, $_graphql_title_query, $_graphql_arguments) {
80 | if (!$_graphql_title) {
81 | return FALSE;
82 | }
83 | if ($_graphql_title_query) {
84 | $result = $this->queryProcessor->processQuery('default:default',
85 | OperationParams::create([
86 | 'query' => $_graphql_title_query,
87 | 'variables' => $_graphql_arguments,
88 | ])
89 | );
90 |
91 | if ($result->errors) {
92 | $_graphql_title = reset($result->errors)->getMessage();
93 | }
94 | else {
95 | $_graphql_title = $this->twig->renderInline($_graphql_title, $result->data);
96 | }
97 | }
98 | else {
99 | $_graphql_title = $this->t($_graphql_title);
100 | }
101 | return $_graphql_title;
102 | }
103 |
104 | }
105 |
--------------------------------------------------------------------------------
/tests/src/Kernel/ThemeTest.php:
--------------------------------------------------------------------------------
1 | setupThemeTest();
44 | }
45 |
46 | /**
47 | * Test query assembly.
48 | */
49 | public function testQueryAssembly() {
50 | /** @var \Prophecy\Prophecy\MethodProphecy $process */
51 | $this->processor
52 | ->processQuery(Argument::any(), Argument::that(function (OperationParams $params) {
53 | return $params->query === $this->getQuery('garage.gql');
54 | }))
55 | ->willReturn(new QueryResult())
56 | ->shouldBeCalled();
57 |
58 | $element = ['#theme' => 'graphql_garage'];
59 | $this->render($element);
60 | }
61 |
62 | /**
63 | * Test query caching.
64 | */
65 | public function testCacheableQuery() {
66 |
67 | $metadata = new CacheableMetadata();
68 | $metadata->setCacheMaxAge(-1);
69 |
70 | $process = $this->processor
71 | ->processQuery(Argument::any(), Argument::any())
72 | ->willReturn(new QueryResult([], [], [], $metadata));
73 |
74 | $element = [
75 | '#theme' => 'graphql_garage',
76 | '#cache' => [
77 | 'keys' => ['garage'],
78 | ],
79 | ];
80 |
81 | $renderer = $this->container->get('renderer');
82 | $element_1 = $element;
83 | $element_2 = $element;
84 |
85 | $renderer->renderRoot($element_1);
86 | $renderer->renderRoot($element_2);
87 |
88 | $process->shouldHaveBeenCalledTimes(1);
89 | }
90 |
91 | /**
92 | * Test query caching.
93 | */
94 | public function testUncacheableQuery() {
95 |
96 | $metadata = new CacheableMetadata();
97 | $metadata->setCacheMaxAge(0);
98 |
99 | $process = $this->processor
100 | ->processQuery(Argument::any(), Argument::any())
101 | ->willReturn(new QueryResult([], [], [], $metadata));
102 |
103 | $element = [
104 | '#theme' => 'graphql_garage',
105 | '#cache' => [
106 | 'keys' => ['garage'],
107 | ],
108 | ];
109 |
110 | $renderer = $this->container->get('renderer');
111 | $element_1 = $element;
112 | $element_2 = $element;
113 |
114 | $renderer->renderRoot($element_1);
115 | $renderer->renderRoot($element_2);
116 |
117 | $process->shouldHaveBeenCalledTimes(2);
118 | }
119 |
120 | }
121 |
--------------------------------------------------------------------------------
/tests/src/Unit/GraphQLTwigExtensionTest.php:
--------------------------------------------------------------------------------
1 | twig = new \Twig_Environment(new\Twig_Loader_Array([
22 | 'query' => '{% graphql %}query ($arg: String!) { foo(id: [1, 2, 3], search: "test") { bar } }{% endgraphql %}',
23 | 'simple' => '{% graphql %}query a { foo }{% endgraphql %}',
24 | 'extend' => '{% extends "simple" %}',
25 | 'dynamic_extend' => '{% extends simple %}',
26 | 'override_extend' => '{% graphql %}query b { foo }{% endgraphql %}{% extends "simple" %}',
27 | 'include' => '{% graphql %}query a { foo }{% endgraphql %}{% include "sub_fragment" with { foo: "bar" } %}',
28 | 'embed' => '{% embed "embeddable" %}{% block test %} Override {% endblock %}{% endembed %}',
29 | 'embeddable' => '{% graphql %}query a { foo }{% endgraphql %}{% block test %} Test {% endblock %}',
30 | 'nested_include' => '{% graphql %}query a { foo }{% endgraphql %}{% include "fragment" with { foo: "bar" } %}',
31 | 'dynamic_include' => '{% graphql %}query a { foo }{% endgraphql %}{% include sub_fragment with { foo: "bar" } %}',
32 | 'fragment' => '{% graphql %}query b { foo }{% endgraphql %}{% include "sub_fragment" %}',
33 | 'sub_fragment' => '{% graphql %}query c { foo }{% endgraphql %}',
34 | 'extend_include' => '{% graphql %}query a { foo }{% endgraphql %}{% extends "fragment" %}',
35 | 'embed_include' => '{% embed "embeddable" %}{% block test %}{% include "fragment" %}{% endblock %}{% endembed %}',
36 | 'recursive' => '{% graphql %}query a { ... b }{% endgraphql %}{% include "recursive_include" %}',
37 | 'recursive_include' => '{% graphql %}fragment b on foo { bar }{% endgraphql %}{% include "recursive_include" %}',
38 | ]));
39 | $this->twig->addExtension(new GraphQLTwigExtension());
40 | }
41 |
42 | protected function assertGraphQLQuery($template, $query) {
43 | $template = $this->twig->loadTemplate($template);
44 | $this->assertTrue(method_exists($template, 'getGraphQLQuery'));
45 | $this->assertEquals($query, $template->getGraphQLQuery());
46 | }
47 |
48 | function testQuery() {
49 | $this->assertGraphQLQuery('query', 'query ($arg: String!) { foo(id: [1, 2, 3], search: "test") { bar } }');
50 | }
51 |
52 | function testExtend() {
53 | $this->assertGraphQLQuery('extend', 'query a { foo }');
54 | }
55 |
56 | function testDynamicExtend() {
57 | $this->assertGraphQLQuery('dynamic_extend', '');
58 | }
59 |
60 | function testInclude() {
61 | $this->assertGraphQLQuery('include', "query a { foo }\nquery c { foo }");
62 | }
63 |
64 | function testEmbed() {
65 | $this->assertGraphQLQuery('embed', "query a { foo }");
66 | }
67 |
68 | function testNestedInclude() {
69 | $this->assertGraphQLQuery('nested_include', "query a { foo }\nquery b { foo }\nquery c { foo }");
70 | }
71 |
72 | function testDynamicInclude() {
73 | $this->assertGraphQLQuery('dynamic_include', "query a { foo }");
74 | }
75 |
76 | function testExtendInclude() {
77 | $this->assertGraphQLQuery('extend_include', "query a { foo }\nquery c { foo }");
78 | }
79 |
80 | function testEmbedInclude() {
81 | $this->assertGraphQLQuery('embed_include', "query a { foo }\nquery b { foo }\nquery c { foo }");
82 | }
83 |
84 | function testRecursiveInclude() {
85 | $this->assertGraphQLQuery('recursive', "query a { ... b }\nfragment b on foo { bar }");
86 | }
87 |
88 | }
89 |
--------------------------------------------------------------------------------
/src/GraphQLNode.php:
--------------------------------------------------------------------------------
1 | query = trim($query);
67 | $this->parent = $parent;
68 | $this->includes = $includes;
69 |
70 | if ($this->query) {
71 | $document = Parser::parse($this->query);
72 |
73 | /** @var \GraphQL\Language\AST\OperationDefinitionNode[] $operations */
74 | $operations = array_filter(iterator_to_array($document->definitions->getIterator()), function (DefinitionNode $node) {
75 | return $node instanceof OperationDefinitionNode;
76 | });
77 |
78 | $this->hasOperations = (bool) $operations;
79 |
80 | $this->arguments = array_map(function (VariableDefinitionNode $node) {
81 | return $node->variable->name->value;
82 | }, array_reduce($operations, function ($carry, OperationDefinitionNode $node) {
83 | return array_merge($carry, iterator_to_array($node->variableDefinitions->getIterator()));
84 | }, []));
85 | }
86 |
87 | parent::__construct();
88 | }
89 |
90 | /**
91 | * {@inheritdoc}
92 | */
93 | public function compile(Twig_Compiler $compiler) {
94 | $compiler
95 | // Make the template implement the GraphQLTemplateTrait.
96 | ->write("\nuse \Drupal\graphql_twig\GraphQLTemplateTrait;\n")
97 | // Write metadata properties.
98 | ->write("\npublic static function hasGraphQLOperations() { return ")->repr($this->hasOperations)->write("; }\n")
99 | ->write("\npublic static function rawGraphQLQuery() { return ")->string($this->query)->write("; }\n")
100 | ->write("\npublic static function rawGraphQLParent() { return ")->string($this->parent)->write("; }\n");
101 |
102 | $compiler->write("\npublic static function rawGraphQLIncludes() { return [");
103 |
104 | foreach ($this->includes as $include) {
105 | $compiler->string($include)->write(",");
106 | }
107 |
108 | $compiler->write("]; }\n");
109 |
110 | $compiler->write("\npublic static function rawGraphQLArguments() { return [");
111 | foreach ($this->arguments as $argument) {
112 | $compiler->string($argument)->write(",");
113 | }
114 | $compiler->write("]; }\n");
115 | }
116 |
117 | }
118 |
--------------------------------------------------------------------------------
/tests/src/Kernel/EntityRenderTest.php:
--------------------------------------------------------------------------------
1 | setupThemeTest();
46 | $this->installConfig(['system', 'user', 'node']);
47 | $this->installEntitySchema('user');
48 | $this->installEntitySchema('node');
49 | $this->installSchema('system', ['sequences']);
50 | NodeType::create([
51 | 'type' => 'article',
52 | 'name' => 'Article',
53 | ])->save();
54 | }
55 |
56 | public function testNodeRender() {
57 | $node = Node::create([
58 | 'title' => 'Test',
59 | 'type' => 'article',
60 | 'uid' => User::create([
61 | 'name' => 'test',
62 | ])->save(),
63 | ]);
64 | $node->save();
65 |
66 | $this->processor->processQuery(Argument::any(), Argument::any())
67 | ->willReturn(new QueryResult([
68 | 'node' => [
69 | 'title' => 'Test',
70 | ],
71 | ]));
72 |
73 | $viewBuilder = $this->container->get('entity_type.manager')->getViewBuilder('node');
74 | $build = $viewBuilder->view($node);
75 | $result = $this->render($build);
76 | $this->assertContains('Test
', $result);
77 | }
78 |
79 | public function testCacheableNodeRender() {
80 | $node = Node::create([
81 | 'title' => 'Test',
82 | 'type' => 'article',
83 | 'uid' => User::create([
84 | 'name' => 'test',
85 | ])->save(),
86 | ]);
87 | $node->save();
88 |
89 | $process = $this->processor->processQuery(Argument::any(), Argument::any())
90 | ->willReturn(new QueryResult([
91 | 'node' => [
92 | 'title' => 'Test',
93 | ],
94 | ], [], [], (new CacheableMetadata())->setCacheMaxAge(-1)));
95 |
96 | $viewBuilder = $this->container->get('entity_type.manager')->getViewBuilder('node');
97 |
98 | $build = $viewBuilder->view($node);
99 | $this->render($build);
100 |
101 | $build = $viewBuilder->view($node);
102 | $this->render($build);
103 |
104 | $process->shouldHaveBeenCalledTimes(1);
105 | }
106 |
107 |
108 | public function testUncacheableNodeRender() {
109 | $node = Node::create([
110 | 'title' => 'Test',
111 | 'type' => 'article',
112 | 'uid' => User::create([
113 | 'name' => 'test',
114 | ])->save(),
115 | ]);
116 | $node->save();
117 | $metadata = new CacheableMetadata();
118 | $metadata->setCacheMaxAge(0);
119 |
120 | $process = $this->processor->processQuery(Argument::any(), Argument::any())
121 | ->willReturn(new QueryResult([
122 | 'node' => [
123 | 'title' => 'Test',
124 | ],
125 | ], [], [], $metadata));
126 |
127 | $viewBuilder = $this->container->get('entity_type.manager')->getViewBuilder('node');
128 |
129 | $build = $viewBuilder->view($node);
130 | $this->render($build);
131 |
132 | $build = $viewBuilder->view($node);
133 | $this->render($build);
134 |
135 | $process->shouldHaveBeenCalledTimes(2);
136 | }
137 |
138 | }
139 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: php
2 | sudo: false
3 |
4 | php:
5 | - 7.1
6 | - 7
7 |
8 | env:
9 | global:
10 | - DRUPAL_GRAPHQL=8.x-3.x
11 | - DRUPAL_BUILD_DIR=$TRAVIS_BUILD_DIR/../drupal
12 | - SIMPLETEST_DB=mysql://root:@127.0.0.1/graphql
13 | - TRAVIS=true
14 | matrix:
15 | - DRUPAL_CORE=8.6.x
16 |
17 | matrix:
18 | # Don't wait for the allowed failures to build.
19 | fast_finish: true
20 | include:
21 | - php: 7.1
22 | env:
23 | - DRUPAL_CORE=8.6.x
24 | # Only run code coverage on the latest php and drupal versions.
25 | - WITH_PHPDBG_COVERAGE=true
26 | allow_failures:
27 | # Allow the code coverage report to fail.
28 | - php: 7.1
29 | env:
30 | - DRUPAL_CORE=8.6.x
31 | # Only run code coverage on the latest php and drupal versions.
32 | - WITH_PHPDBG_COVERAGE=true
33 |
34 | mysql:
35 | database: graphql
36 | username: root
37 | encoding: utf8
38 |
39 | # Cache composer downloads.
40 | cache:
41 | directories:
42 | - $HOME/.composer
43 |
44 | before_install:
45 | # Disable xdebug.
46 | - phpenv config-rm xdebug.ini
47 |
48 | # Determine the php settings file location.
49 | - if [[ $TRAVIS_PHP_VERSION = hhvm* ]];
50 | then export PHPINI=/etc/hhvm/php.ini;
51 | else export PHPINI=$HOME/.phpenv/versions/$(phpenv version-name)/etc/conf.d/travis.ini;
52 | fi
53 |
54 | # PHP Deprecated: Automatically populating $HTTP_RAW_POST_DATA is deprecated
55 | # and will be removed in a future version. To avoid this warning set
56 | # 'always_populate_raw_post_data' to '-1' in php.ini and use the php://input
57 | # stream instead.
58 | - if [[ "$TRAVIS_PHP_VERSION" == "5.6" ]];
59 | then echo always_populate_raw_post_data = -1 >> $PHPINI;
60 | fi;
61 |
62 | # Disable the default memory limit.
63 | - echo memory_limit = -1 >> $PHPINI
64 |
65 | # Update composer.
66 | - composer self-update
67 |
68 | install:
69 | # Create the database.
70 | - mysql -e 'create database graphql'
71 |
72 | # Download Drupal 8 core from the Github mirror because it is faster.
73 | - git clone --branch $DRUPAL_CORE --depth 1 https://github.com/drupal/drupal.git $DRUPAL_BUILD_DIR
74 | - git clone --branch $DRUPAL_GRAPHQL --depth 1 https://github.com/drupal-graphql/graphql.git $DRUPAL_BUILD_DIR/modules/graphql
75 |
76 | # Reference the module in the build site.
77 | - ln -s $TRAVIS_BUILD_DIR $DRUPAL_BUILD_DIR/modules/graphql_twig
78 |
79 | # Copy the customized phpunit configuration file to the core directory so
80 | # the relative paths are correct.
81 | - cp $DRUPAL_BUILD_DIR/modules/graphql/phpunit.xml.dist $DRUPAL_BUILD_DIR/core/phpunit.xml
82 |
83 | # When running with phpdbg we need to replace all code occurrences that check
84 | # for 'cli' with 'phpdbg'. Some files might be write protected, hence the
85 | # fallback.
86 | - if [[ "$WITH_PHPDBG_COVERAGE" == "true" ]];
87 | then grep -rl 'cli' $DRUPAL_BUILD_DIR/core $DRUPAL_BUILD_DIR/modules | xargs sed -i "s/'cli'/'phpdbg'/g" || true;
88 | fi
89 |
90 | # Bring in the module dependencies without requiring a merge plugin. The
91 | # require also triggers a full 'composer install'.
92 | - composer --working-dir=$DRUPAL_BUILD_DIR require webonyx/graphql-php:^0.12.5
93 | - composer --working-dir=$DRUPAL_BUILD_DIR run-script drupal-phpunit-upgrade
94 |
95 | script:
96 | # Run the unit tests using phpdbg if the environment variable is 'true'.
97 | - if [[ "$WITH_PHPDBG_COVERAGE" == "true" ]];
98 | then phpdbg -qrr $DRUPAL_BUILD_DIR/vendor/bin/phpunit --configuration $DRUPAL_BUILD_DIR/core/phpunit.xml --coverage-clover $TRAVIS_BUILD_DIR/coverage.xml $TRAVIS_BUILD_DIR;
99 | fi
100 |
101 | # Run the unit tests with standard php otherwise.
102 | - if [[ "$WITH_PHPDBG_COVERAGE" != "true" ]];
103 | then $DRUPAL_BUILD_DIR/vendor/bin/phpunit --configuration $DRUPAL_BUILD_DIR/core/phpunit.xml $TRAVIS_BUILD_DIR;
104 | fi
105 |
106 | after_success:
107 | - if [[ "$WITH_PHPDBG_COVERAGE" == "true" ]];
108 | then bash <(curl -s https://codecov.io/bash);
109 | fi
110 |
--------------------------------------------------------------------------------
/src/GraphQLTwigEnvironment.php:
--------------------------------------------------------------------------------
1 | queryProcessor;
38 | }
39 |
40 | /**
41 | * The renderer instance.
42 | *
43 | * @var \Drupal\Core\Render\RendererInterface
44 | */
45 | protected $renderer;
46 |
47 | /**
48 | * Retrieve the renderer instance.
49 | *
50 | * @return \Drupal\Core\Render\RendererInterface
51 | * The renderer instance.
52 | */
53 | public function getRenderer() {
54 | return $this->renderer;
55 | }
56 |
57 | /**
58 | * {@inheritdoc}
59 | */
60 | public function __construct(
61 | $root,
62 | CacheBackendInterface $cache,
63 | $twig_extension_hash,
64 | StateInterface $state,
65 | \Twig_LoaderInterface $loader = NULL,
66 | array $options = [],
67 | QueryProcessor $queryProcessor = NULL,
68 | RendererInterface $renderer = NULL
69 | ) {
70 | $this->queryProcessor = $queryProcessor;
71 | $this->renderer = $renderer;
72 | parent::__construct(
73 | $root,
74 | $cache,
75 | $twig_extension_hash,
76 | $state,
77 | $loader,
78 | $options
79 | );
80 | }
81 |
82 | /**
83 | * Regular expression to find a GraphQL annotation in a twig comment.
84 | *
85 | * @var string
86 | */
87 | public static $GRAPHQL_ANNOTATION_REGEX = '/{#graphql\s+(?.*?)\s+#\}/s';
88 |
89 | /**
90 | * {@inheritdoc}
91 | */
92 | public function compileSource($source, $name = NULL) {
93 | if ($source instanceof \Twig_Source) {
94 | // Check if there is a `*.gql` file with the same name as the template.
95 | $graphqlFile = $source->getPath() . '.gql';
96 | if (file_exists($graphqlFile)) {
97 | $source = new \Twig_Source(
98 | '{% graphql %}' . file_get_contents($graphqlFile) . '{% endgraphql %}' . $source->getCode(),
99 | $source->getName(),
100 | $source->getPath()
101 | );
102 | }
103 | else {
104 | // Else, try to find an annotation.
105 | $source = new \Twig_Source(
106 | $this->replaceAnnotation($source->getCode()),
107 | $source->getName(),
108 | $source->getPath()
109 | );
110 | }
111 |
112 | }
113 | else {
114 | // For inline templates, only comment based annotations are supported.
115 | $source = $this->replaceAnnotation($source);
116 | }
117 |
118 | // Compile the modified source.
119 | return parent::compileSource($source, $name);
120 | }
121 |
122 | /**
123 | * Replace `{#graphql ... #}` annotations with `{% graphql ... %}` tags.
124 | *
125 | * @param string $code
126 | * The template code.
127 | *
128 | * @return string
129 | * The template code with all annotations replaced with tags.
130 | */
131 | public function replaceAnnotation($code) {
132 | return preg_replace(static::$GRAPHQL_ANNOTATION_REGEX, '{% graphql %}$1{% endgraphql %}', $code);
133 | }
134 |
135 | }
136 |
--------------------------------------------------------------------------------
/tests/src/Kernel/BlockTest.php:
--------------------------------------------------------------------------------
1 | installConfig(['graphql_twig']);
31 |
32 | $themeName = 'graphql_twig_test_theme';
33 |
34 | /** @var \Drupal\Core\Extension\ThemeHandler $themeHandler */
35 | $themeHandler = $this->container->get('theme_handler');
36 | /** @var \Drupal\Core\Theme\ThemeInitialization $themeInitialization */
37 | $themeInitialization = $this->container->get('theme.initialization');
38 | /** @var \Drupal\Core\Theme\ThemeManager $themeManager */
39 | $themeManager = $this->container->get('theme.manager');
40 |
41 | $themeHandler->install([$themeName]);
42 | $theme = $themeInitialization->initTheme($themeName);
43 | $themeManager->setActiveTheme($theme);
44 |
45 | $this->mockField('shout', [
46 | 'name' => 'shout',
47 | 'type' => 'String',
48 | 'arguments' => [
49 | 'word' => 'String!',
50 | ],
51 | ], function ($value, $args) {
52 | yield strtoupper($args['word']);
53 | });
54 |
55 | // Rebuild routes to include theme routes.
56 | $this->container->get('router.builder')->rebuild();
57 | }
58 |
59 | protected function placeGraphQLBlock($id, $arguments = []) {
60 | $parameters = [];
61 | foreach ($arguments as $key => $value) {
62 | $parameters[] = [
63 | 'key' => $key,
64 | 'value' => $value,
65 | ];
66 | }
67 | $block = $this->placeBlock('graphql_twig:' . $id, [
68 | 'region' => 'content',
69 | 'theme' => 'graphql_twig_test_theme',
70 | 'graphql_block' => $parameters,
71 | ]);
72 | $block->save();
73 | }
74 |
75 | /**
76 | * Test block without arguments.
77 | */
78 | public function testNoArguments() {
79 | $this->placeGraphQLBlock('block_no_arguments');
80 | $result = $this->container->get('http_kernel')->handle(Request::create('/static'));
81 | $content = $result->getContent();
82 | $this->assertContains('This block shouts: DRUPAL
', $content);
83 | }
84 |
85 | /**
86 | * Test block with one argument.
87 | */
88 | public function testOneArgument() {
89 | $this->placeGraphQLBlock('block_one_argument', [
90 | 'first' => 'drupal',
91 | ]);
92 | $result = $this->container->get('http_kernel')->handle(Request::create('/static'));
93 | $content = $result->getContent();
94 | $this->assertContains('This block shouts: DRUPAL
', $content);
95 | }
96 |
97 | /**
98 | * Test block with multiple arguments.
99 | */
100 | public function testMultipleArguments() {
101 | $this->placeGraphQLBlock('block_multiple_arguments', [
102 | 'first' => 'drupal',
103 | 'second' => 'graphql',
104 | ]);
105 | $result = $this->container->get('http_kernel')->handle(Request::create('/static'));
106 | $content = $result->getContent();
107 | $this->assertContains('This block shouts: DRUPAL and GRAPHQL
', $content);
108 | }
109 |
110 |
111 | /**
112 | * Test static block.
113 | */
114 | public function testStatic() {
115 | $this->placeGraphQLBlock('block_static');
116 | $result = $this->container->get('http_kernel')->handle(Request::create('/static'));
117 | $content = $result->getContent();
118 | $this->assertContains('This is a static block.
', $content);
119 | }
120 |
121 | /**
122 | * Test missing block template.
123 | */
124 | public function testMissing() {
125 | $this->placeGraphQLBlock('block_missing');
126 | $result = $this->container->get('http_kernel')->handle(Request::create('/static'));
127 | $content = $result->getContent();
128 | $this->assertContains('Missing template for block_missing.
', $content);
129 | }
130 |
131 | }
132 |
--------------------------------------------------------------------------------
/tests/src/Kernel/RouteTest.php:
--------------------------------------------------------------------------------
1 | container->get('theme_handler');
32 | /** @var \Drupal\Core\Theme\ThemeInitialization $themeInitialization */
33 | $themeInitialization = $this->container->get('theme.initialization');
34 | /** @var \Drupal\Core\Theme\ThemeManager $themeManager */
35 | $themeManager = $this->container->get('theme.manager');
36 |
37 | $themeHandler->install([$themeName]);
38 | $theme = $themeInitialization->initTheme($themeName);
39 | $themeManager->setActiveTheme($theme);
40 |
41 | $this->mockField('shout', [
42 | 'name' => 'shout',
43 | 'type' => 'String',
44 | 'arguments' => [
45 | 'word' => 'String!',
46 | ],
47 | ], function ($value, $args) {
48 | yield strtoupper($args['word']);
49 | });
50 |
51 | // Rebuild routes to include theme routes.
52 | $this->container->get('router.builder')->rebuild();
53 | }
54 |
55 | /**
56 | * Test page without arguments.
57 | */
58 | public function testNoArguments() {
59 | $result = $this->container->get('http_kernel')->handle(Request::create('/no-args'));
60 | $content = $result->getContent();
61 | $this->assertContains('Shouting: DRUPAL
', $content);
62 | $this->assertContains('This page is supposed to shout: DRUPAL
', $content);
63 | }
64 |
65 | /**
66 | * Test page with one argument.
67 | */
68 | public function testOneArgument() {
69 | $result = $this->container->get('http_kernel')->handle(Request::create('/one-arg/drupal'));
70 | $content = $result->getContent();
71 | $this->assertContains('Shouting: DRUPAL
', $content);
72 | $this->assertContains('This page is supposed to shout: DRUPAL
', $content);
73 | }
74 |
75 | /**
76 | * Test page with multiple arguments.
77 | */
78 | public function testMultipleArguments() {
79 | $result = $this->container->get('http_kernel')->handle(Request::create('/multi-args/drupal/graphql'));
80 | $content = $result->getContent();
81 | $this->assertContains('Shouting: DRUPAL and GRAPHQL
', $content);
82 | $this->assertContains('This page is supposed to shout: DRUPAL and GRAPHQL
', $content);
83 | }
84 |
85 | /**
86 | * Test page without query.
87 | */
88 | public function testStatic() {
89 | $result = $this->container->get('http_kernel')->handle(Request::create('/static'));
90 | $content = $result->getContent();
91 | $this->assertContains('This is a static page
', $content);
92 | $this->assertContains('This page is static.
', $content);
93 | }
94 |
95 | /**
96 | * Test page without title.
97 | */
98 | public function testNoTitle() {
99 | $result = $this->container->get('http_kernel')->handle(Request::create('/no-title'));
100 | $content = $result->getContent();
101 | $this->assertNotContains('', $content);
102 | $this->assertContains('
This page has no title.
', $content);
103 | }
104 |
105 | /**
106 | * Test page with forbidden access.
107 | */
108 | public function testNoAccess() {
109 | $result = $this->container->get('http_kernel')->handle(Request::create('/no-access'));
110 | $this->assertEquals(403, $result->getStatusCode());
111 | }
112 |
113 | /**
114 | * Test page with forbidden access.
115 | */
116 | public function testMissing() {
117 | $result = $this->container->get('http_kernel')->handle(Request::create('/missing'));
118 | $content = $result->getContent();
119 | $this->assertContains('Missing template
', $content);
120 | $this->assertContains('Missing template for missing.
', $content);
121 | }
122 |
123 | /**
124 | * Test page with a broken query.
125 | */
126 | public function testError() {
127 | $result = $this->container->get('http_kernel')->handle(Request::create('/error'));
128 | $content = $result->getContent();
129 | $this->assertContains('Field "shout" argument "word" of type "String!" is required but not provided.', $content);
130 | }
131 |
132 | }
133 |
--------------------------------------------------------------------------------
/src/GraphQLTemplateTrait.php:
--------------------------------------------------------------------------------
1 | queryProcessor = $queryProcessor;
55 | }
56 |
57 | /**
58 | * {@inheritdoc}
59 | */
60 | public function display(array $context, array $blocks = array()) {
61 | if (!static::hasGraphQLOperations()) {
62 | parent::display($context, $blocks);
63 | return;
64 | }
65 |
66 | if (isset($context['graphql_arguments'])) {
67 | $context = $context['graphql_arguments'];
68 | }
69 |
70 | $query = trim($this->getGraphQLQuery());
71 |
72 | if (!$query) {
73 | parent::display($context, $blocks);
74 | return;
75 | }
76 |
77 | $arguments = [];
78 | foreach (static::rawGraphQLArguments() as $var) {
79 | if (isset($context[$var])) {
80 | $arguments[$var] = $context[$var] instanceof EntityInterface ? $context[$var]->id() : $context[$var];
81 | }
82 | }
83 |
84 |
85 | $queryResult = $this->env->getQueryProcessor()->processQuery('default:default', OperationParams::create([
86 | 'query' => $query,
87 | 'variables' => $arguments,
88 | ]));
89 |
90 | $build = [
91 | '#cache' => [
92 | 'contexts' => $queryResult->getCacheContexts(),
93 | 'tags' => $queryResult->getCacheTags(),
94 | 'max-age' => $queryResult->getCacheMaxAge(),
95 | ],
96 | ];
97 |
98 | $this->env->getRenderer()->render($build);
99 |
100 | $config = \Drupal::config('graphql_twig.settings');
101 | $debug_placement = $config->get('debug_placement');
102 |
103 | if ($this->env->isDebug() && \Drupal::currentUser()->hasPermission('use graphql explorer')) {
104 | // Auto-attach the debug assets if necessary.
105 | $template_attached = ['#attached' => ['library' => ['graphql_twig/debug']]];
106 | $this->env->getRenderer()->render($template_attached);
107 | }
108 |
109 | if ($this->env->isDebug() && $debug_placement == 'wrapped') {
110 | printf(
111 | '',
112 | 'graphql-twig-debug-wrapper',
113 | htmlspecialchars($query),
114 | htmlspecialchars(json_encode($arguments))
115 | );
116 | }
117 |
118 | if ($queryResult->errors) {
119 | print('
');
120 | foreach ($queryResult->errors as $error) {
121 | printf('- %s
', $error->message);
122 | }
123 | print('
');
124 | }
125 | else {
126 | $context['graphql'] = $queryResult->data;
127 | if ($this->env->isDebug() && $debug_placement == 'inside') {
128 | $context['graphql_debug'] = [
129 | '#markup' => sprintf(
130 | '
',
131 | 'graphql-twig-debug-wrapper',
132 | htmlspecialchars($query),
133 | htmlspecialchars(json_encode($arguments))
134 | ),
135 | ];
136 |
137 | // Add the debug parent class to the element.
138 | /** @var \Drupal\Core\Template\Attribute $attributes */
139 | $attributes = $context['attributes'];
140 | $attributes->addClass('graphql-twig-debug-parent');
141 | }
142 |
143 | parent::display($context, $blocks);
144 | }
145 |
146 | if ($this->env->isDebug() && $debug_placement == 'wrapped') {
147 | print('
');
148 | }
149 | }
150 |
151 | /**
152 | * Recursively build the GraphQL query.
153 | *
154 | * Builds the templates GraphQL query by iterating through all included or
155 | * embedded templates recursively.
156 | */
157 | public function getGraphQLQuery() {
158 |
159 | $query = '';
160 | $includes = [];
161 |
162 | if ($this instanceof \Twig_Template) {
163 | $query = $this->getGraphQLFragment();
164 |
165 | $includes = array_keys($this->getGraphQLIncludes());
166 |
167 | // Recursively collect all included fragments.
168 | $includes = array_map(function ($template) {
169 | return $this->env->loadTemplate($template)->getGraphQLFragment();
170 | }, $includes);
171 |
172 | // Always add includes from parent templates.
173 | if ($parent = $this->getGraphQLParent()) {
174 | $includes += array_map(function ($template) {
175 | return $this->env->loadTemplate($template)->getGraphQLQuery();
176 | }, array_keys($parent->getGraphQLIncludes()));
177 | }
178 | }
179 |
180 |
181 | return implode("\n", [-1 => $query] + $includes);
182 | }
183 |
184 | /**
185 | * Get the files parent template.
186 | *
187 | * @return \Twig_Template|null
188 | * The parent template or null.
189 | */
190 | protected function getGraphQLParent() {
191 | return static::rawGraphQLParent() ? $this->env->loadTemplate(static::rawGraphQLParent()) : NULL;
192 | }
193 |
194 | /**
195 | * Retrieve the files graphql fragment.
196 | *
197 | * @return string
198 | * The GraphQL fragment.
199 | */
200 | public function getGraphQLFragment() {
201 | // If there is no query for this template, try to get one from the
202 | // parent template.
203 | if (!($query = static::rawGraphQLQuery()) && ($parent = $this->getGraphQLParent())) {
204 | $query = $parent->getGraphQLFragment();
205 | }
206 | return $query;
207 | }
208 |
209 | /**
210 | * Retrieve a list of all direct or indirect included templates.
211 | *
212 | * @param string[] $recursed
213 | * The list of templates already recursed into. Used internally.
214 | *
215 | * @return string[]
216 | * The list of included templates.
217 | */
218 | public function getGraphQLIncludes(&$recursed = []) {
219 |
220 | $includes = array_flip(static::rawGraphQLIncludes());
221 | foreach ($includes as $include => $key) {
222 | if (in_array($include, $recursed)) {
223 | continue;
224 | }
225 |
226 | $recursed[] = $include;
227 |
228 | // TODO: operate on template class instead.
229 | $includes += $this->env->loadTemplate($include)->getGraphQLIncludes($recursed);
230 | }
231 |
232 | return $includes;
233 | }
234 | }
235 |
--------------------------------------------------------------------------------