vendor/webonyx/graphql-php/src/Server/Helper.php line 211

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