Completed
Push — master ( ddc3a0...dbafdf )
by Vladimir
13s queued 11s
created

Helper   F

Complexity

Total Complexity 93

Size/Duplication

Total Lines 527
Duplicated Lines 0 %

Test Coverage

Coverage 82.95%

Importance

Changes 0
Metric Value
wmc 93
dl 0
loc 527
ccs 180
cts 217
cp 0.8295
rs 2
c 0
b 0
f 0

18 Methods

Rating   Name   Duplication   Size   Complexity  
A toPsrResponse() 0 8 2
A parseRequestParams() 0 18 5
A resolveRootValue() 0 9 2
C parseHttpRequest() 0 37 15
A emitResponse() 0 8 2
A executeBatch() 0 16 4
A loadPersistedQuery() 0 20 4
B parsePsrRequest() 0 41 8
A sendResponse() 0 8 2
C validateOperationParams() 0 36 17
A doConvertToPsrResponse() 0 11 1
A resolveValidationRules() 0 17 3
C promiseToExecuteOperation() 0 68 14
B resolveHttpStatus() 0 29 7
A executeOperation() 0 10 3
A readRawBody() 0 3 1
A resolveContextValue() 0 9 2
A doSendResponse() 0 4 1

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