GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.

Helper   F
last analyzed

Complexity

Total Complexity 92

Size/Duplication

Total Lines 560
Duplicated Lines 0 %

Test Coverage

Coverage 82.97%

Importance

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

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
A resolveValidationRules() 0 21 3
C promiseToExecuteOperation() 0 86 15
B resolveHttpStatus() 0 33 7
A readRawBody() 0 3 1
A resolveContextValue() 0 13 2
A doSendResponse() 0 4 1
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() === null) {
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 === null) {
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