Helper   F
last analyzed

Complexity

Total Complexity 92

Size/Duplication

Total Lines 560
Duplicated Lines 0 %

Test Coverage

Coverage 82.97%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 92
eloc 224
dl 0
loc 560
ccs 190
cts 229
cp 0.8297
rs 2
c 1
b 0
f 0

18 Methods

Rating   Name   Duplication   Size   Complexity  
A parseRequestParams() 0 19 5
C parseHttpRequest() 0 44 13
A executeBatch() 0 17 4
C validateOperationParams() 0 40 17
A executeOperation() 0 10 3
A toPsrResponse() 0 9 2
A resolveRootValue() 0 9 2
A emitResponse() 0 8 2
A loadPersistedQuery() 0 20 4
A sendResponse() 0 8 2
A doConvertToPsrResponse() 0 11 1
B resolveHttpStatus() 0 33 7
A readRawBody() 0 3 1
A resolveContextValue() 0 13 2
A doSendResponse() 0 4 1
A resolveValidationRules() 0 21 3
C promiseToExecuteOperation() 0 86 15
B parsePsrRequest() 0 41 8

How to fix   Complexity   

Complex Class

Complex classes like Helper often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Helper, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace GraphQL\Server;
6
7
use GraphQL\Error\Error;
8
use GraphQL\Error\FormattedError;
9
use GraphQL\Error\InvariantViolation;
10
use GraphQL\Executor\ExecutionResult;
11
use GraphQL\Executor\Executor;
12
use GraphQL\Executor\Promise\Adapter\SyncPromiseAdapter;
13
use GraphQL\Executor\Promise\Promise;
14
use GraphQL\Executor\Promise\PromiseAdapter;
15
use GraphQL\GraphQL;
16
use GraphQL\Language\AST\DocumentNode;
17
use GraphQL\Language\Parser;
18
use GraphQL\Utils\AST;
19
use GraphQL\Utils\Utils;
20
use JsonSerializable;
21
use Psr\Http\Message\ResponseInterface;
22
use Psr\Http\Message\ServerRequestInterface;
23
use Psr\Http\Message\StreamInterface;
24
use function file_get_contents;
25
use function header;
26
use function is_array;
27
use function is_callable;
28
use function is_string;
29
use function json_decode;
30
use function json_encode;
31
use function json_last_error;
32
use function json_last_error_msg;
33
use function sprintf;
34
use function stripos;
35
36
/**
37
 * Contains functionality that could be re-used by various server implementations
38
 */
39
class Helper
40
{
41
    /**
42
     * Parses HTTP request using PHP globals and returns GraphQL OperationParams
43
     * contained in this request. For batched requests it returns an array of OperationParams.
44
     *
45
     * This function does not check validity of these params
46
     * (validation is performed separately in validateOperationParams() method).
47
     *
48
     * If $readRawBodyFn argument is not provided - will attempt to read raw request body
49
     * from `php://input` stream.
50
     *
51
     * Internally it normalizes input to $method, $bodyParams and $queryParams and
52
     * calls `parseRequestParams()` to produce actual return value.
53
     *
54
     * For PSR-7 request parsing use `parsePsrRequest()` instead.
55
     *
56
     * @return OperationParams|OperationParams[]
57
     *
58
     * @throws RequestError
59
     *
60
     * @api
61
     */
62 16
    public function parseHttpRequest(?callable $readRawBodyFn = null)
63
    {
64 16
        $method     = $_SERVER['REQUEST_METHOD'] ?? null;
65 16
        $bodyParams = [];
66 16
        $urlParams  = $_GET;
67
68 16
        if ($method === 'POST') {
69 14
            $contentType = $_SERVER['CONTENT_TYPE'] ?? null;
70
71 14
            if ($contentType === null) {
72 1
                throw new RequestError('Missing "Content-Type" header');
73
            }
74
75 13
            if (stripos($contentType, 'application/graphql') !== false) {
76 1
                $rawBody    = $readRawBodyFn
77 1
                    ? $readRawBodyFn()
78 1
                    : $this->readRawBody();
79 1
                $bodyParams = ['query' => $rawBody ?: ''];
80 12
            } elseif (stripos($contentType, 'application/json') !== false) {
81 8
                $rawBody    = $readRawBodyFn ?
82 8
                    $readRawBodyFn()
83 8
                    : $this->readRawBody();
84 8
                $bodyParams = json_decode($rawBody ?: '', true);
85
86 8
                if (json_last_error()) {
87 1
                    throw new RequestError('Could not parse JSON: ' . json_last_error_msg());
88
                }
89
90 7
                if (! is_array($bodyParams)) {
91 1
                    throw new RequestError(
92
                        'GraphQL Server expects JSON object or array, but got ' .
93 7
                        Utils::printSafeJson($bodyParams)
94
                    );
95
                }
96 4
            } elseif (stripos($contentType, 'application/x-www-form-urlencoded') !== false) {
97 1
                $bodyParams = $_POST;
98 3
            } elseif (stripos($contentType, 'multipart/form-data') !== false) {
99 1
                $bodyParams = $_POST;
100
            } else {
101 2
                throw new RequestError('Unexpected content type: ' . Utils::printSafeJson($contentType));
102
            }
103
        }
104
105 11
        return $this->parseRequestParams($method, $bodyParams, $urlParams);
106
    }
107
108
    /**
109
     * Parses normalized request params and returns instance of OperationParams
110
     * or array of OperationParams in case of batch operation.
111
     *
112
     * Returned value is a suitable input for `executeOperation` or `executeBatch` (if array)
113
     *
114
     * @param string  $method
115
     * @param mixed[] $bodyParams
116
     * @param mixed[] $queryParams
117
     *
118
     * @return OperationParams|OperationParams[]
119
     *
120
     * @throws RequestError
121
     *
122
     * @api
123
     */
124 14
    public function parseRequestParams($method, array $bodyParams, array $queryParams)
125
    {
126 14
        if ($method === 'GET') {
127 1
            $result = OperationParams::create($queryParams, true);
128 13
        } elseif ($method === 'POST') {
129 11
            if (isset($bodyParams[0])) {
130 1
                $result = [];
131 1
                foreach ($bodyParams as $index => $entry) {
132 1
                    $op       = OperationParams::create($entry);
133 1
                    $result[] = $op;
134
                }
135
            } else {
136 11
                $result = OperationParams::create($bodyParams);
137
            }
138
        } else {
139 2
            throw new RequestError('HTTP Method "' . $method . '" is not supported');
140
        }
141
142 12
        return $result;
143
    }
144
145
    /**
146
     * Checks validity of OperationParams extracted from HTTP request and returns an array of errors
147
     * if params are invalid (or empty array when params are valid)
148
     *
149
     * @return array<int, RequestError>
150
     *
151
     * @api
152
     */
153 36
    public function validateOperationParams(OperationParams $params)
154
    {
155 36
        $errors = [];
156 36
        if (! $params->query && ! $params->queryId) {
157 2
            $errors[] = new RequestError('GraphQL Request must include at least one of those two parameters: "query" or "queryId"');
158
        }
159
160 36
        if ($params->query && $params->queryId) {
161 1
            $errors[] = new RequestError('GraphQL Request parameters "query" and "queryId" are mutually exclusive');
162
        }
163
164 36
        if ($params->query !== null && (! is_string($params->query) || empty($params->query))) {
0 ignored issues
show
introduced by
The condition is_string($params->query) is always true.
Loading history...
165 1
            $errors[] = new RequestError(
166
                'GraphQL Request parameter "query" must be string, but got ' .
167 1
                Utils::printSafeJson($params->query)
168
            );
169
        }
170
171 36
        if ($params->queryId !== null && (! is_string($params->queryId) || empty($params->queryId))) {
0 ignored issues
show
introduced by
The condition is_string($params->queryId) is always true.
Loading history...
172 1
            $errors[] = new RequestError(
173
                'GraphQL Request parameter "queryId" must be string, but got ' .
174 1
                Utils::printSafeJson($params->queryId)
175
            );
176
        }
177
178 36
        if ($params->operation !== null && (! is_string($params->operation) || empty($params->operation))) {
0 ignored issues
show
introduced by
The condition is_string($params->operation) is always true.
Loading history...
179 1
            $errors[] = new RequestError(
180
                'GraphQL Request parameter "operation" must be string, but got ' .
181 1
                Utils::printSafeJson($params->operation)
182
            );
183
        }
184
185 36
        if ($params->variables !== null && (! is_array($params->variables) || isset($params->variables[0]))) {
0 ignored issues
show
introduced by
The condition is_array($params->variables) is always true.
Loading history...
186 1
            $errors[] = new RequestError(
187
                'GraphQL Request parameter "variables" must be object or JSON string parsed to object, but got ' .
188 1
                Utils::printSafeJson($params->getOriginalInput('variables'))
189
            );
190
        }
191
192 36
        return $errors;
193
    }
194
195
    /**
196
     * Executes GraphQL operation with given server configuration and returns execution result
197
     * (or promise when promise adapter is different from SyncPromiseAdapter)
198
     *
199
     * @return ExecutionResult|Promise
200
     *
201
     * @api
202
     */
203 25
    public function executeOperation(ServerConfig $config, OperationParams $op)
204
    {
205 25
        $promiseAdapter = $config->getPromiseAdapter() ?: Executor::getPromiseAdapter();
206 25
        $result         = $this->promiseToExecuteOperation($promiseAdapter, $config, $op);
207
208 23
        if ($promiseAdapter instanceof SyncPromiseAdapter) {
209 23
            $result = $promiseAdapter->wait($result);
210
        }
211
212 23
        return $result;
213
    }
214
215
    /**
216
     * Executes batched GraphQL operations with shared promise queue
217
     * (thus, effectively batching deferreds|promises of all queries at once)
218
     *
219
     * @param OperationParams[] $operations
220
     *
221
     * @return ExecutionResult|ExecutionResult[]|Promise
222
     *
223
     * @api
224
     */
225 3
    public function executeBatch(ServerConfig $config, array $operations)
226
    {
227 3
        $promiseAdapter = $config->getPromiseAdapter() ?: Executor::getPromiseAdapter();
228 3
        $result         = [];
229
230 3
        foreach ($operations as $operation) {
231 3
            $result[] = $this->promiseToExecuteOperation($promiseAdapter, $config, $operation, true);
232
        }
233
234 3
        $result = $promiseAdapter->all($result);
235
236
        // Wait for promised results when using sync promises
237 3
        if ($promiseAdapter instanceof SyncPromiseAdapter) {
238 3
            $result = $promiseAdapter->wait($result);
239
        }
240
241 3
        return $result;
242
    }
243
244
    /**
245
     * @param bool $isBatch
246
     *
247
     * @return Promise
248
     */
249 28
    private function promiseToExecuteOperation(
250
        PromiseAdapter $promiseAdapter,
251
        ServerConfig $config,
252
        OperationParams $op,
253
        $isBatch = false
254
    ) {
255
        try {
256 28
            if (! $config->getSchema()) {
257
                throw new InvariantViolation('Schema is required for the server');
258
            }
259
260 28
            if ($isBatch && ! $config->getQueryBatching()) {
261 1
                throw new RequestError('Batched queries are not supported by this server');
262
            }
263
264 27
            $errors = $this->validateOperationParams($op);
265
266 27
            if (! empty($errors)) {
267 1
                $errors = Utils::map(
268 1
                    $errors,
269
                    static function (RequestError $err) {
270 1
                        return Error::createLocatedError($err, null, null);
271 1
                    }
272
                );
273
274 1
                return $promiseAdapter->createFulfilled(
275 1
                    new ExecutionResult(null, $errors)
276
                );
277
            }
278
279 26
            $doc = $op->queryId
280 5
                ? $this->loadPersistedQuery($config, $op)
281 24
                : $op->query;
282
283 24
            if (! $doc instanceof DocumentNode) {
284 24
                $doc = Parser::parse($doc);
285
            }
286
287 23
            $operationType = AST::getOperation($doc, $op->operation);
288
289 23
            if ($operationType === false) {
290
                throw new RequestError('Failed to determine operation type');
291
            }
292
293 23
            if ($operationType !== 'query' && $op->isReadOnly()) {
294 1
                throw new RequestError('GET supports only query operation');
295
            }
296
297 22
            $result = GraphQL::promiseToExecute(
298 22
                $promiseAdapter,
299 22
                $config->getSchema(),
300 22
                $doc,
301 22
                $this->resolveRootValue($config, $op, $doc, $operationType),
302 22
                $this->resolveContextValue($config, $op, $doc, $operationType),
303 22
                $op->variables,
304 22
                $op->operation,
305 22
                $config->getFieldResolver(),
306 22
                $this->resolveValidationRules($config, $op, $doc, $operationType)
307
            );
308 6
        } catch (RequestError $e) {
309 3
            $result = $promiseAdapter->createFulfilled(
310 3
                new ExecutionResult(null, [Error::createLocatedError($e)])
311
            );
312 3
        } catch (Error $e) {
313 1
            $result = $promiseAdapter->createFulfilled(
314 1
                new ExecutionResult(null, [$e])
315
            );
316
        }
317
318
        $applyErrorHandling = static function (ExecutionResult $result) use ($config) {
319 25
            if ($config->getErrorsHandler()) {
320 1
                $result->setErrorsHandler($config->getErrorsHandler());
321
            }
322 25
            if ($config->getErrorFormatter() || $config->getDebug()) {
323 3
                $result->setErrorFormatter(
324 3
                    FormattedError::prepareFormatter(
325 3
                        $config->getErrorFormatter(),
326 3
                        $config->getDebug()
327
                    )
328
                );
329
            }
330
331 25
            return $result;
332 25
        };
333
334 25
        return $result->then($applyErrorHandling);
335
    }
336
337
    /**
338
     * @return mixed
339
     *
340
     * @throws RequestError
341
     */
342 5
    private function loadPersistedQuery(ServerConfig $config, OperationParams $operationParams)
343
    {
344
        // Load query if we got persisted query id:
345 5
        $loader = $config->getPersistentQueryLoader();
346
347 5
        if (! $loader) {
348 1
            throw new RequestError('Persisted queries are not supported by this server');
349
        }
350
351 4
        $source = $loader($operationParams->queryId, $operationParams);
352
353 4
        if (! is_string($source) && ! $source instanceof DocumentNode) {
354 1
            throw new InvariantViolation(sprintf(
355 1
                'Persistent query loader must return query string or instance of %s but got: %s',
356 1
                DocumentNode::class,
357 1
                Utils::printSafe($source)
358
            ));
359
        }
360
361 3
        return $source;
362
    }
363
364
    /**
365
     * @param string $operationType
366
     *
367
     * @return mixed[]|null
368
     */
369 22
    private function resolveValidationRules(
370
        ServerConfig $config,
371
        OperationParams $params,
372
        DocumentNode $doc,
373
        $operationType
374
    ) {
375
        // Allow customizing validation rules per operation:
376 22
        $validationRules = $config->getValidationRules();
377
378 22
        if (is_callable($validationRules)) {
379 4
            $validationRules = $validationRules($params, $doc, $operationType);
380
381 4
            if (! is_array($validationRules)) {
382 1
                throw new InvariantViolation(sprintf(
383 1
                    'Expecting validation rules to be array or callable returning array, but got: %s',
384 1
                    Utils::printSafe($validationRules)
385
                ));
386
            }
387
        }
388
389 21
        return $validationRules;
390
    }
391
392
    /**
393
     * @return mixed
394
     */
395 22
    private function resolveRootValue(ServerConfig $config, OperationParams $params, DocumentNode $doc, string $operationType)
396
    {
397 22
        $rootValue = $config->getRootValue();
398
399 22
        if (is_callable($rootValue)) {
400 1
            $rootValue = $rootValue($params, $doc, $operationType);
401
        }
402
403 22
        return $rootValue;
404
    }
405
406
    /**
407
     * @param string $operationType
408
     *
409
     * @return mixed
410
     */
411 22
    private function resolveContextValue(
412
        ServerConfig $config,
413
        OperationParams $params,
414
        DocumentNode $doc,
415
        $operationType
416
    ) {
417 22
        $context = $config->getContext();
418
419 22
        if (is_callable($context)) {
420 1
            $context = $context($params, $doc, $operationType);
421
        }
422
423 22
        return $context;
424
    }
425
426
    /**
427
     * Send response using standard PHP `header()` and `echo`.
428
     *
429
     * @param Promise|ExecutionResult|ExecutionResult[] $result
430
     * @param bool                                      $exitWhenDone
431
     *
432
     * @api
433
     */
434
    public function sendResponse($result, $exitWhenDone = false)
435
    {
436
        if ($result instanceof Promise) {
437
            $result->then(function ($actualResult) use ($exitWhenDone) {
438
                $this->doSendResponse($actualResult, $exitWhenDone);
439
            });
440
        } else {
441
            $this->doSendResponse($result, $exitWhenDone);
442
        }
443
    }
444
445
    private function doSendResponse($result, $exitWhenDone)
446
    {
447
        $httpStatus = $this->resolveHttpStatus($result);
448
        $this->emitResponse($result, $httpStatus, $exitWhenDone);
449
    }
450
451
    /**
452
     * @param mixed[]|JsonSerializable $jsonSerializable
453
     * @param int                      $httpStatus
454
     * @param bool                     $exitWhenDone
455
     */
456
    public function emitResponse($jsonSerializable, $httpStatus, $exitWhenDone)
457
    {
458
        $body = json_encode($jsonSerializable);
459
        header('Content-Type: application/json', true, $httpStatus);
460
        echo $body;
461
462
        if ($exitWhenDone) {
463
            exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
464
        }
465
    }
466
467
    /**
468
     * @return bool|string
469
     */
470
    private function readRawBody()
471
    {
472
        return file_get_contents('php://input');
473
    }
474
475
    /**
476
     * @param ExecutionResult|mixed[] $result
477
     *
478
     * @return int
479
     */
480 1
    private function resolveHttpStatus($result)
481
    {
482 1
        if (is_array($result) && isset($result[0])) {
483
            Utils::each(
484
                $result,
485
                static function ($executionResult, $index) {
486
                    if (! $executionResult instanceof ExecutionResult) {
487
                        throw new InvariantViolation(sprintf(
488
                            'Expecting every entry of batched query result to be instance of %s but entry at position %d is %s',
489
                            ExecutionResult::class,
490
                            $index,
491
                            Utils::printSafe($executionResult)
492
                        ));
493
                    }
494
                }
495
            );
496
            $httpStatus = 200;
497
        } else {
498 1
            if (! $result instanceof ExecutionResult) {
499
                throw new InvariantViolation(sprintf(
500
                    'Expecting query result to be instance of %s but got %s',
501
                    ExecutionResult::class,
502
                    Utils::printSafe($result)
503
                ));
504
            }
505 1
            if ($result->data === null && ! empty($result->errors)) {
506
                $httpStatus = 400;
507
            } else {
508 1
                $httpStatus = 200;
509
            }
510
        }
511
512 1
        return $httpStatus;
513
    }
514
515
    /**
516
     * Converts PSR-7 request to OperationParams[]
517
     *
518
     * @return OperationParams[]|OperationParams
519
     *
520
     * @throws RequestError
521
     *
522
     * @api
523
     */
524 16
    public function parsePsrRequest(ServerRequestInterface $request)
525
    {
526 16
        if ($request->getMethod() === 'GET') {
527 1
            $bodyParams = [];
528
        } else {
529 15
            $contentType = $request->getHeader('content-type');
530
531 15
            if (! isset($contentType[0])) {
532 1
                throw new RequestError('Missing "Content-Type" header');
533
            }
534
535 14
            if (stripos($contentType[0], 'application/graphql') !== false) {
536 1
                $bodyParams = ['query' => $request->getBody()->getContents()];
537 13
            } elseif (stripos($contentType[0], 'application/json') !== false) {
538 11
                $bodyParams = $request->getParsedBody();
539
540 11
                if ($bodyParams === null) {
541 2
                    throw new InvariantViolation(
542 2
                        'PSR-7 request is expected to provide parsed body for "application/json" requests but got null'
543
                    );
544
                }
545
546 9
                if (! is_array($bodyParams)) {
547 1
                    throw new RequestError(
548
                        'GraphQL Server expects JSON object or array, but got ' .
549 9
                        Utils::printSafeJson($bodyParams)
550
                    );
551
                }
552
            } else {
553 2
                $bodyParams = $request->getParsedBody();
554
555 2
                if (! is_array($bodyParams)) {
556
                    throw new RequestError('Unexpected content type: ' . Utils::printSafeJson($contentType[0]));
557
                }
558
            }
559
        }
560
561 12
        return $this->parseRequestParams(
562 12
            $request->getMethod(),
563 12
            $bodyParams,
564 12
            $request->getQueryParams()
565
        );
566
    }
567
568
    /**
569
     * Converts query execution result to PSR-7 response
570
     *
571
     * @param Promise|ExecutionResult|ExecutionResult[] $result
572
     *
573
     * @return Promise|ResponseInterface
574
     *
575
     * @api
576
     */
577 1
    public function toPsrResponse($result, ResponseInterface $response, StreamInterface $writableBodyStream)
578
    {
579 1
        if ($result instanceof Promise) {
580
            return $result->then(function ($actualResult) use ($response, $writableBodyStream) {
581
                return $this->doConvertToPsrResponse($actualResult, $response, $writableBodyStream);
582
            });
583
        }
584
585 1
        return $this->doConvertToPsrResponse($result, $response, $writableBodyStream);
586
    }
587
588 1
    private function doConvertToPsrResponse($result, ResponseInterface $response, StreamInterface $writableBodyStream)
589
    {
590 1
        $httpStatus = $this->resolveHttpStatus($result);
591
592 1
        $result = json_encode($result);
593 1
        $writableBodyStream->write($result);
594
595
        return $response
596 1
            ->withStatus($httpStatus)
597 1
            ->withHeader('Content-Type', 'application/json')
598 1
            ->withBody($writableBodyStream);
599
    }
600
}
601