Completed
Push — master ( 93ccd7...3b27ab )
by Vladimir
27s queued 13s
created

Helper::promiseToExecuteOperation()   C

Complexity

Conditions 14
Paths 58

Size

Total Lines 81
Code Lines 47

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 46
CRAP Score 14.0018

Importance

Changes 0
Metric Value
eloc 47
dl 0
loc 81
ccs 46
cts 47
cp 0.9787
rs 6.2666
c 0
b 0
f 0
cc 14
nc 58
nop 4
crap 14.0018

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 Error[]
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;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $errors returns an array which contains values of type GraphQL\Server\RequestError which are incompatible with the documented value type GraphQL\Error\Error.
Loading history...
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 23
            if ($operationType !== 'query' && $op->isReadOnly()) {
289 1
                throw new RequestError('GET supports only query operation');
290
            }
291
292 22
            $result = GraphQL::promiseToExecute(
293 22
                $promiseAdapter,
294 22
                $config->getSchema(),
295 22
                $doc,
296 22
                $this->resolveRootValue($config, $op, $doc, $operationType),
297 22
                $this->resolveContextValue($config, $op, $doc, $operationType),
298 22
                $op->variables,
299 22
                $op->operation,
300 22
                $config->getFieldResolver(),
301 22
                $this->resolveValidationRules($config, $op, $doc, $operationType)
302
            );
303 6
        } catch (RequestError $e) {
304 3
            $result = $promiseAdapter->createFulfilled(
305 3
                new ExecutionResult(null, [Error::createLocatedError($e)])
306
            );
307 3
        } catch (Error $e) {
308 1
            $result = $promiseAdapter->createFulfilled(
309 1
                new ExecutionResult(null, [$e])
310
            );
311
        }
312
313
        $applyErrorHandling = static function (ExecutionResult $result) use ($config) {
314 25
            if ($config->getErrorsHandler()) {
315 1
                $result->setErrorsHandler($config->getErrorsHandler());
316
            }
317 25
            if ($config->getErrorFormatter() || $config->getDebug()) {
318 3
                $result->setErrorFormatter(
319 3
                    FormattedError::prepareFormatter(
320 3
                        $config->getErrorFormatter(),
321 3
                        $config->getDebug()
322
                    )
323
                );
324
            }
325
326 25
            return $result;
327 25
        };
328
329 25
        return $result->then($applyErrorHandling);
330
    }
331
332
    /**
333
     * @return mixed
334
     *
335
     * @throws RequestError
336
     */
337 5
    private function loadPersistedQuery(ServerConfig $config, OperationParams $operationParams)
338
    {
339
        // Load query if we got persisted query id:
340 5
        $loader = $config->getPersistentQueryLoader();
341
342 5
        if (! $loader) {
343 1
            throw new RequestError('Persisted queries are not supported by this server');
344
        }
345
346 4
        $source = $loader($operationParams->queryId, $operationParams);
347
348 4
        if (! is_string($source) && ! $source instanceof DocumentNode) {
349 1
            throw new InvariantViolation(sprintf(
350 1
                'Persistent query loader must return query string or instance of %s but got: %s',
351 1
                DocumentNode::class,
352 1
                Utils::printSafe($source)
353
            ));
354
        }
355
356 3
        return $source;
357
    }
358
359
    /**
360
     * @param string $operationType
361
     *
362
     * @return mixed[]|null
363
     */
364 22
    private function resolveValidationRules(
365
        ServerConfig $config,
366
        OperationParams $params,
367
        DocumentNode $doc,
368
        $operationType
369
    ) {
370
        // Allow customizing validation rules per operation:
371 22
        $validationRules = $config->getValidationRules();
372
373 22
        if (is_callable($validationRules)) {
374 4
            $validationRules = $validationRules($params, $doc, $operationType);
375
376 4
            if (! is_array($validationRules)) {
377 1
                throw new InvariantViolation(sprintf(
378 1
                    'Expecting validation rules to be array or callable returning array, but got: %s',
379 1
                    Utils::printSafe($validationRules)
380
                ));
381
            }
382
        }
383
384 21
        return $validationRules;
385
    }
386
387
    /**
388
     * @param string $operationType
389
     *
390
     * @return mixed
391
     */
392 22
    private function resolveRootValue(ServerConfig $config, OperationParams $params, DocumentNode $doc, $operationType)
393
    {
394 22
        $root = $config->getRootValue();
395
396 22
        if (is_callable($root)) {
397 1
            $root = $root($params, $doc, $operationType);
398
        }
399
400 22
        return $root;
401
    }
402
403
    /**
404
     * @param string $operationType
405
     *
406
     * @return mixed
407
     */
408 22
    private function resolveContextValue(
409
        ServerConfig $config,
410
        OperationParams $params,
411
        DocumentNode $doc,
412
        $operationType
413
    ) {
414 22
        $context = $config->getContext();
415
416 22
        if (is_callable($context)) {
417 1
            $context = $context($params, $doc, $operationType);
418
        }
419
420 22
        return $context;
421
    }
422
423
    /**
424
     * Send response using standard PHP `header()` and `echo`.
425
     *
426
     * @param Promise|ExecutionResult|ExecutionResult[] $result
427
     * @param bool                                      $exitWhenDone
428
     *
429
     * @api
430
     */
431
    public function sendResponse($result, $exitWhenDone = false)
432
    {
433
        if ($result instanceof Promise) {
434
            $result->then(function ($actualResult) use ($exitWhenDone) {
435
                $this->doSendResponse($actualResult, $exitWhenDone);
436
            });
437
        } else {
438
            $this->doSendResponse($result, $exitWhenDone);
439
        }
440
    }
441
442
    private function doSendResponse($result, $exitWhenDone)
443
    {
444
        $httpStatus = $this->resolveHttpStatus($result);
445
        $this->emitResponse($result, $httpStatus, $exitWhenDone);
446
    }
447
448
    /**
449
     * @param mixed[]|JsonSerializable $jsonSerializable
450
     * @param int                      $httpStatus
451
     * @param bool                     $exitWhenDone
452
     */
453
    public function emitResponse($jsonSerializable, $httpStatus, $exitWhenDone)
454
    {
455
        $body = json_encode($jsonSerializable);
456
        header('Content-Type: application/json', true, $httpStatus);
457
        echo $body;
458
459
        if ($exitWhenDone) {
460
            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...
461
        }
462
    }
463
464
    /**
465
     * @return bool|string
466
     */
467
    private function readRawBody()
468
    {
469
        return file_get_contents('php://input');
470
    }
471
472
    /**
473
     * @param ExecutionResult|mixed[] $result
474
     *
475
     * @return int
476
     */
477 1
    private function resolveHttpStatus($result)
478
    {
479 1
        if (is_array($result) && isset($result[0])) {
480
            Utils::each(
481
                $result,
482
                static function ($executionResult, $index) {
483
                    if (! $executionResult instanceof ExecutionResult) {
484
                        throw new InvariantViolation(sprintf(
485
                            'Expecting every entry of batched query result to be instance of %s but entry at position %d is %s',
486
                            ExecutionResult::class,
487
                            $index,
488
                            Utils::printSafe($executionResult)
489
                        ));
490
                    }
491
                }
492
            );
493
            $httpStatus = 200;
494
        } else {
495 1
            if (! $result instanceof ExecutionResult) {
496
                throw new InvariantViolation(sprintf(
497
                    'Expecting query result to be instance of %s but got %s',
498
                    ExecutionResult::class,
499
                    Utils::printSafe($result)
500
                ));
501
            }
502 1
            if ($result->data === null && ! empty($result->errors)) {
503
                $httpStatus = 400;
504
            } else {
505 1
                $httpStatus = 200;
506
            }
507
        }
508
509 1
        return $httpStatus;
510
    }
511
512
    /**
513
     * Converts PSR-7 request to OperationParams[]
514
     *
515
     * @return OperationParams[]|OperationParams
516
     *
517
     * @throws RequestError
518
     *
519
     * @api
520
     */
521 16
    public function parsePsrRequest(ServerRequestInterface $request)
522
    {
523 16
        if ($request->getMethod() === 'GET') {
524 1
            $bodyParams = [];
525
        } else {
526 15
            $contentType = $request->getHeader('content-type');
527
528 15
            if (! isset($contentType[0])) {
529 1
                throw new RequestError('Missing "Content-Type" header');
530
            }
531
532 14
            if (stripos($contentType[0], 'application/graphql') !== false) {
533 1
                $bodyParams = ['query' => $request->getBody()->getContents()];
534 13
            } elseif (stripos($contentType[0], 'application/json') !== false) {
535 11
                $bodyParams = $request->getParsedBody();
536
537 11
                if ($bodyParams === null) {
538 2
                    throw new InvariantViolation(
539 2
                        'PSR-7 request is expected to provide parsed body for "application/json" requests but got null'
540
                    );
541
                }
542
543 9
                if (! is_array($bodyParams)) {
544 1
                    throw new RequestError(
545
                        'GraphQL Server expects JSON object or array, but got ' .
546 9
                        Utils::printSafeJson($bodyParams)
547
                    );
548
                }
549
            } else {
550 2
                $bodyParams = $request->getParsedBody();
551
552 2
                if (! is_array($bodyParams)) {
553
                    throw new RequestError('Unexpected content type: ' . Utils::printSafeJson($contentType[0]));
554
                }
555
            }
556
        }
557
558 12
        return $this->parseRequestParams(
559 12
            $request->getMethod(),
560 12
            $bodyParams,
561 12
            $request->getQueryParams()
562
        );
563
    }
564
565
    /**
566
     * Converts query execution result to PSR-7 response
567
     *
568
     * @param Promise|ExecutionResult|ExecutionResult[] $result
569
     *
570
     * @return Promise|ResponseInterface
571
     *
572
     * @api
573
     */
574 1
    public function toPsrResponse($result, ResponseInterface $response, StreamInterface $writableBodyStream)
575
    {
576 1
        if ($result instanceof Promise) {
577
            return $result->then(function ($actualResult) use ($response, $writableBodyStream) {
578
                return $this->doConvertToPsrResponse($actualResult, $response, $writableBodyStream);
579
            });
580
        }
581
582 1
        return $this->doConvertToPsrResponse($result, $response, $writableBodyStream);
583
    }
584
585 1
    private function doConvertToPsrResponse($result, ResponseInterface $response, StreamInterface $writableBodyStream)
586
    {
587 1
        $httpStatus = $this->resolveHttpStatus($result);
588
589 1
        $result = json_encode($result);
590 1
        $writableBodyStream->write($result);
591
592
        return $response
593 1
            ->withStatus($httpStatus)
594 1
            ->withHeader('Content-Type', 'application/json')
595 1
            ->withBody($writableBodyStream);
596
    }
597
}
598