Failed Conditions
Push — master ( 48b44f...392b56 )
by Vladimir
04:42
created

Helper::promiseToExecuteOperation()   C

Complexity

Conditions 14
Paths 58

Size

Total Lines 79
Code Lines 45

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 44
CRAP Score 14.0021

Importance

Changes 0
Metric Value
eloc 45
dl 0
loc 79
ccs 44
cts 45
cp 0.9778
rs 6.2666
c 0
b 0
f 0
cc 14
nc 58
nop 4
crap 14.0021

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