Completed
Pull Request — 8.x-3.x (#503)
by Sebastian
02:25
created

QueryRouteEnhancer::enhance()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 21
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 10
nc 4
nop 2
dl 0
loc 21
rs 9.0534
c 0
b 0
f 0
1
<?php
2
3
namespace Drupal\graphql\Routing;
4
5
use Drupal\Component\Utility\NestedArray;
6
use Drupal\Core\Routing\Enhancer\RouteEnhancerInterface;
7
use Drupal\graphql\QueryProvider\QueryProviderInterface;
8
use Drupal\graphql\Utility\JsonHelper;
9
use Symfony\Component\HttpFoundation\Request;
10
use Symfony\Component\Routing\Route;
11
12
class QueryRouteEnhancer implements RouteEnhancerInterface {
13
14
  const SINGLE = 'single';
15
  const BATCH = 'batch';
16
17
  /**
18
   * The query provider service.
19
   *
20
   * @var \Drupal\graphql\QueryProvider\QueryProviderInterface
21
   */
22
  protected $queryProvider;
23
24
  /**
25
   * QueryRouteEnhancer constructor.
26
   *
27
   * @param \Drupal\graphql\QueryProvider\QueryProviderInterface $queryProvider
28
   *   The query provider service.
29
   */
30
  public function __construct(QueryProviderInterface $queryProvider) {
31
    $this->queryProvider = $queryProvider;
32
  }
33
34
35
  /**
36
   * {@inheritdoc}
37
   */
38
  public function applies(Route $route) {
39
    return $route->hasDefault('_graphql');
40
  }
41
42
  /**
43
   * {@inheritdoc}
44
   */
45
  public function enhance(array $defaults, Request $request) {
46
    if (!empty($defaults['_controller'])) {
47
      return $defaults;
48
    }
49
50
    $params = $this->extractParams($request);
51
    if ($enhanced = $this->enhanceSingle($defaults, $params, $request)) {
52
      return $enhanced;
53
    }
54
55
    if ($enhanced = $this->enhanceBatch($defaults, $params, $request)) {
56
      return $enhanced;
57
    }
58
59
    // By default we assume a 'single' request. This is going to fail in the
60
    // graphql processor due to a missing query string but at least provides
61
    // the right format for the client to act upon.
62
    return $defaults + [
63
      '_controller' => $defaults['_graphql']['single'],
64
    ];
65
  }
66
67
  /**
68
   * Attempts to enhance the request as a batch query.
69
   *
70
   * @param array $defaults
71
   *   The controller defaults.
72
   * @param array $params
73
   *   The query parameters.
74
   * @param \Symfony\Component\HttpFoundation\Request $request
75
   *   The request object.
76
   *
77
   * @return array|bool
78
   *   The enhanced controller defaults.
79
   */
80
  protected function enhanceBatch(array $defaults, array $params, Request $request) {
81
    // PHP 5.5.x does not yet support the ARRAY_FILTER_USE_KEYS constant.
82
    $keys = array_filter(array_keys($params), function($index) {
83
      return is_numeric($index);
84
    });
85
86
    $queries = array_intersect_key($params, array_flip($keys));
87
    if (!isset($queries[0])) {
88
      return FALSE;
89
    }
90
91
    if (array_keys($queries) !== range(0, count($queries) - 1)) {
92
      // If this is not a continuously numeric array, don't do anything.
93
      return FALSE;
94
    }
95
96
    return $defaults + [
97
      '_controller' => $defaults['_graphql']['multiple'],
98
      'queries' => $queries,
99
      'type' => static::BATCH,
100
    ];
101
  }
102
103
  /**
104
   * Attempts to enhance the request as a single query.
105
   *
106
   * @param array $defaults
107
   *   The controller defaults.
108
   * @param array $params
109
   *   The query parameters.
110
   * @param \Symfony\Component\HttpFoundation\Request $request
111
   *   The request object.
112
   *
113
   * @return array|boolean
114
   *   The enhanced controller defaults.
115
   */
116
  protected function enhanceSingle(array $defaults, array $params, Request $request) {
117
    $values = $params + [
118
      'query' => empty($params['query']) ? $this->queryProvider->getQuery($params) : $params['query'],
119
      'variables' => [],
120
    ];
121
122
    if (empty($values['query'])) {
123
      return FALSE;
124
    }
125
126
    return $defaults + [
127
      '_controller' => $defaults['_graphql']['single'],
128
      'query' => !empty($values['query']) && is_string($values['query']) ? $values['query'] : '',
129
      'variables' => !empty($values['variables']) && is_array($values['variables']) ? $values['variables'] : [],
130
      'persisted' => empty($params['query']),
131
      'type' => static::SINGLE,
132
    ];
133
  }
134
135
  /**
136
   * Extract an associative array of query parameters from the request.
137
   *
138
   * If the given request does not have any POST body content it uses the GET
139
   * query parameters instead.
140
   *
141
   * @param \Symfony\Component\HttpFoundation\Request $request
142
   *   The request object.
143
   *
144
   * @return array
145
   *   An associative array of query parameters.
146
   */
147
  protected function extractParams(Request $request) {
148
    $values = JsonHelper::decodeParams($request->query->all());
149
150
    // The request body parameters might contain file upload mutations. We treat
151
    // them according to the graphql multipart request specification.
152
    //
153
    // @see https://github.com/jaydenseric/graphql-multipart-request-spec#server
154
    if ($body = JsonHelper::decodeParams($request->request->all())) {
155
      // Flatten the operations array if it exists.
156
      $operations = isset($body['operations']) && is_array($body['operations']) ? $body['operations'] : [];
157
      $values = array_merge($values, $body, $operations);
158
    }
159
160
    // The request body content has precedence of query parameters.
161
    if ($content = $request->getContent()) {
162
      $values = array_merge($values, JsonHelper::decodeParams(json_decode($content, TRUE)));
163
    }
164
165
    // According to the graphql multipart request specification, uploaded files
166
    // are referenced to variable placeholders in a map. Here, we resolve this
167
    // map by assigning the uploaded files to the corresponding variables.
168
    if (!empty($values['map']) && is_array($values['map']) && $files = $request->files->all()) {
169
      foreach ($files as $key => $file) {
170
        if (!isset($values['map'][$key])) {
171
          continue;
172
        }
173
174
        $paths = (array) $values['map'][$key];
175
        foreach ($paths as $path) {
176
          $path = explode('.', $path);
177
178
          if (NestedArray::keyExists($values, $path)) {
179
            NestedArray::setValue($values, $path, $file);
180
          }
181
        }
182
      }
183
    }
184
185
    return $values;
186
  }
187
188
189
}
190