diff --git a/src/Error/ClientAware.php b/src/Error/ClientAware.php index df69d14..3b2f085 100644 --- a/src/Error/ClientAware.php +++ b/src/Error/ClientAware.php @@ -2,24 +2,29 @@ namespace GraphQL\Error; /** - * Interface ClientAware + * This interface is used for [default error formatting](error-handling/). * - * @package GraphQL\Error + * Only errors implementing this interface (and returning true from `isClientSafe()`) + * will be formatted with original error message. + * + * All other errors will be formatted with generic "Internal server error". */ interface ClientAware { /** - * Returns true when exception message is safe to be displayed to client + * Returns true when exception message is safe to be displayed to a client. * + * @api * @return bool */ public function isClientSafe(); /** - * Returns string describing error category. + * Returns string describing a category of the error. * * Value "graphql" is reserved for errors produced by query parsing or validation, do not use it. * + * @api * @return string */ public function getCategory(); diff --git a/src/Error/Debug.php b/src/Error/Debug.php index 221b58b..acd4b12 100644 --- a/src/Error/Debug.php +++ b/src/Error/Debug.php @@ -1,7 +1,9 @@ x, column => y] locations within the source GraphQL document - * which correspond to this error. - * - * Errors during validation often contain multiple locations, for example to - * point out two things with the same name. Errors during execution include a - * single location, the field which produced the error. - * * @var SourceLocation[] */ private $locations; @@ -219,6 +217,17 @@ class Error extends \Exception implements \JsonSerializable, ClientAware } /** + * An array of locations within the source GraphQL document which correspond to this error. + * + * Each entry has information about `line` and `column` within source GraphQL document: + * $location->line; + * $location->column; + * + * Errors during validation often contain multiple locations, for example to + * point out to field mentioned in multiple fragments. Errors during execution include a + * single location, the field which produced the error. + * + * @api * @return SourceLocation[] */ public function getLocations() @@ -240,9 +249,10 @@ class Error extends \Exception implements \JsonSerializable, ClientAware } /** - * Returns an array describing the JSON-path into the execution response which - * corresponds to this error. Only included for errors during execution. + * Returns an array describing the path from the root value to the field which produced this error. + * Only included for execution errors. * + * @api * @return array|null */ public function getPath() diff --git a/src/Error/FormattedError.php b/src/Error/FormattedError.php index 65e4490..8da8ff7 100644 --- a/src/Error/FormattedError.php +++ b/src/Error/FormattedError.php @@ -7,23 +7,36 @@ use GraphQL\Type\Definition\WrappingType; use GraphQL\Utils\Utils; /** - * Class FormattedError - * - * @package GraphQL\Error + * This class is used for [default error formatting](error-handling/). + * It converts PHP exceptions to [spec-compliant errors](https://facebook.github.io/graphql/#sec-Errors) + * and provides tools for error debugging. */ class FormattedError { private static $internalErrorMessage = 'Internal server error'; + /** + * Set default error message for internal errors formatted using createFormattedError(). + * This value can be overridden by passing 3rd argument to `createFormattedError()`. + * + * @api + * @param string $msg + */ public static function setInternalErrorMessage($msg) { self::$internalErrorMessage = $msg; } /** - * Standard GraphQL error formatter. Converts any exception to GraphQL error - * conforming to GraphQL spec + * Standard GraphQL error formatter. Converts any exception to array + * conforming to GraphQL spec. * + * This method only exposes exception message when exception implements ClientAware interface + * (or when debug flags are passed). + * + * For a list of available debug flags see GraphQL\Error\Debug constants. + * + * @api * @param \Throwable $e * @param bool|int $debug * @param string $internalErrorMessage @@ -73,6 +86,9 @@ class FormattedError } /** + * Decorates spec-compliant $formattedError with debug entries according to $debug flags + * (see GraphQL\Error\Debug for available flags) + * * @param array $formattedError * @param \Throwable $e * @param bool $debug @@ -148,8 +164,9 @@ class FormattedError } /** - * Converts error trace to serializable array + * Returns error trace as serializable array * + * @api * @param \Throwable $error * @return array */ diff --git a/src/Error/Warning.php b/src/Error/Warning.php index 9ab84df..fa6a666 100644 --- a/src/Error/Warning.php +++ b/src/Error/Warning.php @@ -1,15 +1,20 @@ getPrevious() would + * contain original exception. + * + * @api + * @var \GraphQL\Error\Error[] */ public $errors; /** + * User-defined serializable array of extensions included in serialized result. + * Conforms to + * + * @api * @var array */ public $extensions; @@ -56,6 +77,7 @@ class ExecutionResult implements \JsonSerializable * // ... other keys * ); * + * @api * @param callable $errorFormatter * @return $this */ @@ -75,6 +97,7 @@ class ExecutionResult implements \JsonSerializable * return array_map($formatter, $errors); * } * + * @api * @param callable $handler * @return $this */ @@ -85,11 +108,16 @@ class ExecutionResult implements \JsonSerializable } /** - * Converts GraphQL result to array using provided errors handler and formatter. + * Converts GraphQL query result to spec-compliant serializable array using provided + * errors handler and formatter. * - * Default error formatter is GraphQL\Error\FormattedError::createFromException - * Default error handler will simply return all errors formatted. No errors are filtered. + * If debug argument is passed, output of error formatter is enriched which debugging information + * ("debugMessage", "trace" keys depending on flags). * + * $debug argument must be either bool (only adds "debugMessage" to result) or sum of flags from + * GraphQL\Error\Debug + * + * @api * @param bool|int $debug * @return array */ diff --git a/src/Executor/Executor.php b/src/Executor/Executor.php index 53aee8b..90362cb 100644 --- a/src/Executor/Executor.php +++ b/src/Executor/Executor.php @@ -31,23 +31,7 @@ use GraphQL\Utils\TypeInfo; use GraphQL\Utils\Utils; /** - * Terminology - * - * "Definitions" are the generic name for top-level statements in the document. - * Examples of this include: - * 1) Operations (such as a query) - * 2) Fragments - * - * "Operations" are a generic name for requests in the document. - * Examples of this include: - * 1) query, - * 2) mutation - * - * "Selections" are the statements that can appear legally and at - * single level of the query. These include: - * 1) field references e.g "a" - * 2) fragment "spreads" e.g. "...c" - * 3) inline fragment "spreads" e.g. "...on Type { a }" + * Implements the "Evaluating requests" section of the GraphQL specification. */ class Executor { @@ -88,8 +72,12 @@ class Executor } /** - * Executes DocumentNode against given schema + * Executes DocumentNode against given $schema. * + * Always returns ExecutionResult and never throws. All errors which occur during operation + * execution are collected in `$result->errors`. + * + * @api * @param Schema $schema * @param DocumentNode $ast * @param $rootValue @@ -134,9 +122,12 @@ class Executor } /** - * Executes DocumentNode against given $schema using given $promiseAdapter for deferred resolvers. - * Returns promise which is always fullfilled with instance of ExecutionResult + * Same as executeQuery(), but requires promise adapter and returns a promise which is always + * fulfilled with an instance of ExecutionResult and never rejected. * + * Useful for async PHP platforms. + * + * @api * @param PromiseAdapter $promiseAdapter * @param Schema $schema * @param DocumentNode $ast @@ -1083,7 +1074,7 @@ class Executor 'for value: ' . Utils::printSafe($result) . '. Switching to slow resolution method using `isTypeOf` ' . 'of all possible implementations. It requires full schema scan and degrades query performance significantly. '. ' Make sure your `resolveType` always returns valid implementation or throws.', - Warning::FULL_SCHEMA_SCAN_WARNING + Warning::WARNING_FULL_SCHEMA_SCAN ); } $runtimeType = self::defaultTypeResolver($result, $exeContext->contextValue, $info, $returnType); diff --git a/src/Executor/Promise/PromiseAdapter.php b/src/Executor/Promise/PromiseAdapter.php index f9ea529..6f36a03 100644 --- a/src/Executor/Promise/PromiseAdapter.php +++ b/src/Executor/Promise/PromiseAdapter.php @@ -1,19 +1,24 @@ parseDocument(); } - /** * Given a string containing a GraphQL value (ex. `[42]`), parse the AST for * that value. - * Throws GraphQL\Error\SyntaxError if a syntax error is encountered. + * Throws `GraphQL\Error\SyntaxError` if a syntax error is encountered. * * This is useful within tools that operate upon GraphQL Values directly and * in isolation of complete GraphQL documents. * - * Consider providing the results to the utility function: GraphQL\Utils\AST::valueFromAST(). + * Consider providing the results to the utility function: `GraphQL\Utils\AST::valueFromAST()`. * + * @api * @param Source|string $source * @param array $options * @return BooleanValueNode|EnumValueNode|FloatValueNode|IntValueNode|ListValueNode|ObjectValueNode|StringValueNode|VariableNode @@ -94,13 +99,14 @@ class Parser /** * Given a string containing a GraphQL Type (ex. `[Int!]`), parse the AST for * that type. - * Throws GraphQL\Error\SyntaxError if a syntax error is encountered. + * Throws `GraphQL\Error\SyntaxError` if a syntax error is encountered. * * This is useful within tools that operate upon GraphQL Types directly and * in isolation of complete GraphQL documents. * - * Consider providing the results to the utility function: GraphQL\Utils\AST::typeFromAST(). + * Consider providing the results to the utility function: `GraphQL\Utils\AST::typeFromAST()`. * + * @api * @param Source|string $source * @param array $options * @return ListTypeNode|NameNode|NonNullTypeNode diff --git a/src/Language/Printer.php b/src/Language/Printer.php index e269da1..7e7336b 100644 --- a/src/Language/Printer.php +++ b/src/Language/Printer.php @@ -40,11 +40,24 @@ use GraphQL\Language\AST\UnionTypeDefinitionNode; use GraphQL\Language\AST\VariableDefinitionNode; use GraphQL\Utils\Utils; +/** + * Prints AST to string. Capable of printing GraphQL queries and Type definition language. + * Useful for pretty-printing queries or printing back AST for logging, documentation, etc. + * + * Usage example: + * + * ```php + * $query = 'query myQuery {someField}'; + * $ast = GraphQL\Language\Parser::parse($query); + * $printed = GraphQL\Language\Printer::doPrint($ast); + * ``` + */ class Printer { /** * Prints AST to string. Capable of printing GraphQL queries and Type definition language. * + * @api * @param Node $ast * @return string */ diff --git a/src/Language/Visitor.php b/src/Language/Visitor.php index b2b4401..d76b330 100644 --- a/src/Language/Visitor.php +++ b/src/Language/Visitor.php @@ -15,44 +15,95 @@ class VisitorOperation public $removeNode; } +/** + * Utility for efficient AST traversal and modification. + * + * `visit()` will walk through an AST using a depth first traversal, calling + * the visitor's enter function at each node in the traversal, and calling the + * leave function after visiting that node and all of it's child nodes. + * + * By returning different values from the enter and leave functions, the + * behavior of the visitor can be altered, including skipping over a sub-tree of + * the AST (by returning false), editing the AST by returning a value or null + * to remove the value, or to stop the whole traversal by returning BREAK. + * + * When using `visit()` to edit an AST, the original AST will not be modified, and + * a new version of the AST with the changes applied will be returned from the + * visit function. + * + * $editedAST = Visitor::visit($ast, [ + * 'enter' => function ($node, $key, $parent, $path, $ancestors) { + * // return + * // null: no action + * // Visitor::skipNode(): skip visiting this node + * // Visitor::stop(): stop visiting altogether + * // Visitor::removeNode(): delete this node + * // any value: replace this node with the returned value + * }, + * 'leave' => function ($node, $key, $parent, $path, $ancestors) { + * // return + * // null: no action + * // Visitor::stop(): stop visiting altogether + * // Visitor::removeNode(): delete this node + * // any value: replace this node with the returned value + * } + * ]); + * + * Alternatively to providing enter() and leave() functions, a visitor can + * instead provide functions named the same as the [kinds of AST nodes](reference/#graphqllanguageastnodekind), + * or enter/leave visitors at a named key, leading to four permutations of + * visitor API: + * + * 1) Named visitors triggered when entering a node a specific kind. + * + * Visitor::visit($ast, [ + * 'Kind' => function ($node) { + * // enter the "Kind" node + * } + * ]); + * + * 2) Named visitors that trigger upon entering and leaving a node of + * a specific kind. + * + * Visitor::visit($ast, [ + * 'Kind' => [ + * 'enter' => function ($node) { + * // enter the "Kind" node + * } + * 'leave' => function ($node) { + * // leave the "Kind" node + * } + * ] + * ]); + * + * 3) Generic visitors that trigger upon entering and leaving any node. + * + * Visitor::visit($ast, [ + * 'enter' => function ($node) { + * // enter any node + * }, + * 'leave' => function ($node) { + * // leave any node + * } + * ]); + * + * 4) Parallel visitors for entering and leaving nodes of a specific kind. + * + * Visitor::visit($ast, [ + * 'enter' => [ + * 'Kind' => function($node) { + * // enter the "Kind" node + * } + * }, + * 'leave' => [ + * 'Kind' => function ($node) { + * // leave the "Kind" node + * } + * ] + * ]); + */ class Visitor { - /** - * Returns marker for visitor break - * - * @return VisitorOperation - */ - public static function stop() - { - $r = new VisitorOperation(); - $r->doBreak = true; - return $r; - } - - /** - * Returns marker for skipping current node - * - * @return VisitorOperation - */ - public static function skipNode() - { - $r = new VisitorOperation(); - $r->doContinue = true; - return $r; - } - - /** - * Returns marker for removing a node - * - * @return VisitorOperation - */ - public static function removeNode() - { - $r = new VisitorOperation(); - $r->removeNode = true; - return $r; - } - public static $visitorKeys = [ NodeKind::NAME => [], NodeKind::DOCUMENT => ['definitions'], @@ -96,90 +147,9 @@ class Visitor ]; /** - * visit() will walk through an AST using a depth first traversal, calling - * the visitor's enter function at each node in the traversal, and calling the - * leave function after visiting that node and all of it's child nodes. - * - * By returning different values from the enter and leave functions, the - * behavior of the visitor can be altered, including skipping over a sub-tree of - * the AST (by returning false), editing the AST by returning a value or null - * to remove the value, or to stop the whole traversal by returning BREAK. - * - * When using visit() to edit an AST, the original AST will not be modified, and - * a new version of the AST with the changes applied will be returned from the - * visit function. - * - * $editedAST = Visitor::visit($ast, [ - * 'enter' => function ($node, $key, $parent, $path, $ancestors) { - * // return - * // null: no action - * // Visitor::skipNode(): skip visiting this node - * // Visitor::stop(): stop visiting altogether - * // Visitor::removeNode(): delete this node - * // any value: replace this node with the returned value - * }, - * 'leave' => function ($node, $key, $parent, $path, $ancestors) { - * // return - * // null: no action - * // Visitor::stop(): stop visiting altogether - * // Visitor::removeNode(): delete this node - * // any value: replace this node with the returned value - * } - * ]); - * - * Alternatively to providing enter() and leave() functions, a visitor can - * instead provide functions named the same as the kinds of AST nodes, or - * enter/leave visitors at a named key, leading to four permutations of - * visitor API: - * - * 1) Named visitors triggered when entering a node a specific kind. - * - * Visitor::visit($ast, [ - * 'Kind' => function ($node) { - * // enter the "Kind" node - * } - * ]); - * - * 2) Named visitors that trigger upon entering and leaving a node of - * a specific kind. - * - * Visitor::visit($ast, [ - * 'Kind' => [ - * 'enter' => function ($node) { - * // enter the "Kind" node - * } - * 'leave' => function ($node) { - * // leave the "Kind" node - * } - * ] - * ]); - * - * 3) Generic visitors that trigger upon entering and leaving any node. - * - * Visitor::visit($ast, [ - * 'enter' => function ($node) { - * // enter any node - * }, - * 'leave' => function ($node) { - * // leave any node - * } - * ]); - * - * 4) Parallel visitors for entering and leaving nodes of a specific kind. - * - * Visitor::visit($ast, [ - * 'enter' => [ - * 'Kind' => function($node) { - * // enter the "Kind" node - * } - * }, - * 'leave' => [ - * 'Kind' => function ($node) { - * // leave the "Kind" node - * } - * ] - * ]); + * Visit the AST (see class description for details) * + * @api * @param Node $root * @param array $visitor * @param array $keyMap @@ -335,6 +305,45 @@ class Visitor return $newRoot; } + /** + * Returns marker for visitor break + * + * @api + * @return VisitorOperation + */ + public static function stop() + { + $r = new VisitorOperation(); + $r->doBreak = true; + return $r; + } + + /** + * Returns marker for skipping current node + * + * @api + * @return VisitorOperation + */ + public static function skipNode() + { + $r = new VisitorOperation(); + $r->doContinue = true; + return $r; + } + + /** + * Returns marker for removing a node + * + * @api + * @return VisitorOperation + */ + public static function removeNode() + { + $r = new VisitorOperation(); + $r->removeNode = true; + return $r; + } + /** * @param $visitors * @return array diff --git a/src/Schema.php b/src/Schema.php index 8e7335b..42cc703 100644 --- a/src/Schema.php +++ b/src/Schema.php @@ -5,29 +5,6 @@ namespace GraphQL; /** * Schema Definition * - * A Schema is created by supplying the root types of each type of operation: - * query, mutation (optional) and subscription (optional). A schema definition is - * then supplied to the validator and executor. - * - * Example: - * - * $schema = new GraphQL\Schema([ - * 'query' => $MyAppQueryRootType, - * 'mutation' => $MyAppMutationRootType, - * ]); - * - * Note: If an array of `directives` are provided to GraphQL\Schema, that will be - * the exact list of directives represented and allowed. If `directives` is not - * provided then a default set of the specified directives (e.g. @include and - * @skip) will be used. If you wish to provide *additional* directives to these - * specified directives, you must explicitly declare them. Example: - * - * $mySchema = new GraphQL\Schema([ - * ... - * 'directives' => array_merge(GraphQL::getInternalDirectives(), [ $myCustomDirective ]), - * ]) - * - * @package GraphQL * @deprecated moved to GraphQL\Type\Schema */ class Schema extends \GraphQL\Type\Schema diff --git a/src/Server/Helper.php b/src/Server/Helper.php index af8de39..3754ae7 100644 --- a/src/Server/Helper.php +++ b/src/Server/Helper.php @@ -19,17 +19,152 @@ use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\StreamInterface; /** - * Class Helper * Contains functionality that could be re-used by various server implementations - * - * @package GraphQL\Server */ class Helper { + + /** + * Parses HTTP request using PHP globals and returns GraphQL OperationParams + * contained in this request. For batched requests it returns an array of OperationParams. + * + * This function does not check validity of these params + * (validation is performed separately in validateOperationParams() method). + * + * If $readRawBodyFn argument is not provided - will attempt to read raw request body + * from `php://input` stream. + * + * Internally it normalizes input to $method, $bodyParams and $queryParams and + * calls `parseRequestParams()` to produce actual return value. + * + * For PSR-7 request parsing use `parsePsrRequest()` instead. + * + * @api + * @param callable|null $readRawBodyFn + * @return OperationParams|OperationParams[] + * @throws RequestError + */ + public function parseHttpRequest(callable $readRawBodyFn = null) + { + $method = isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : null; + $bodyParams = []; + $urlParams = $_GET; + + if ($method === 'POST') { + $contentType = isset($_SERVER['CONTENT_TYPE']) ? $_SERVER['CONTENT_TYPE'] : null; + + if (stripos($contentType, 'application/graphql') !== false) { + $rawBody = $readRawBodyFn ? $readRawBodyFn() : $this->readRawBody(); + $bodyParams = ['query' => $rawBody ?: '']; + } else if (stripos($contentType, 'application/json') !== false) { + $rawBody = $readRawBodyFn ? $readRawBodyFn() : $this->readRawBody(); + $bodyParams = json_decode($rawBody ?: '', true); + + if (json_last_error()) { + throw new RequestError("Could not parse JSON: " . json_last_error_msg()); + } + if (!is_array($bodyParams)) { + throw new RequestError( + "GraphQL Server expects JSON object or array, but got " . + Utils::printSafeJson($bodyParams) + ); + } + } else if (stripos($contentType, 'application/x-www-form-urlencoded') !== false) { + $bodyParams = $_POST; + } else if (null === $contentType) { + throw new RequestError('Missing "Content-Type" header'); + } else { + throw new RequestError("Unexpected content type: " . Utils::printSafeJson($contentType)); + } + } + + return $this->parseRequestParams($method, $bodyParams, $urlParams); + } + + /** + * Parses normalized request params and returns instance of OperationParams + * or array of OperationParams in case of batch operation. + * + * Returned value is a suitable input for `executeOperation` or `executeBatch` (if array) + * + * @api + * @param string $method + * @param array $bodyParams + * @param array $queryParams + * @return OperationParams|OperationParams[] + * @throws RequestError + */ + public function parseRequestParams($method, array $bodyParams, array $queryParams) + { + if ($method === 'GET') { + $result = OperationParams::create($queryParams, true); + } else if ($method === 'POST') { + if (isset($bodyParams[0])) { + $result = []; + foreach ($bodyParams as $index => $entry) { + $op = OperationParams::create($entry); + $result[] = $op; + } + } else { + $result = OperationParams::create($bodyParams); + } + } else { + throw new RequestError('HTTP Method "' . $method . '" is not supported'); + } + return $result; + } + + /** + * Checks validity of OperationParams extracted from HTTP request and returns an array of errors + * if params are invalid (or empty array when params are valid) + * + * @api + * @param OperationParams $params + * @return Error[] + */ + public function validateOperationParams(OperationParams $params) + { + $errors = []; + if (!$params->query && !$params->queryId) { + $errors[] = new RequestError('GraphQL Request must include at least one of those two parameters: "query" or "queryId"'); + } + if ($params->query && $params->queryId) { + $errors[] = new RequestError('GraphQL Request parameters "query" and "queryId" are mutually exclusive'); + } + + if ($params->query !== null && (!is_string($params->query) || empty($params->query))) { + $errors[] = new RequestError( + 'GraphQL Request parameter "query" must be string, but got ' . + Utils::printSafeJson($params->query) + ); + } + if ($params->queryId !== null && (!is_string($params->queryId) || empty($params->queryId))) { + $errors[] = new RequestError( + 'GraphQL Request parameter "queryId" must be string, but got ' . + Utils::printSafeJson($params->queryId) + ); + } + + if ($params->operation !== null && (!is_string($params->operation) || empty($params->operation))) { + $errors[] = new RequestError( + 'GraphQL Request parameter "operation" must be string, but got ' . + Utils::printSafeJson($params->operation) + ); + } + if ($params->variables !== null && (!is_array($params->variables) || isset($params->variables[0]))) { + $errors[] = new RequestError( + 'GraphQL Request parameter "variables" must be object or JSON string parsed to object, but got ' . + Utils::printSafeJson($params->getOriginalInput('variables')) + ); + } + return $errors; + } + /** * Executes GraphQL operation with given server configuration and returns execution result - * (or promise when promise adapter is different than SyncPromiseAdapter) + * (or promise when promise adapter is different from SyncPromiseAdapter) * + * @api * @param ServerConfig $config * @param OperationParams $op * @@ -51,6 +186,7 @@ class Helper * Executes batched GraphQL operations with shared promise queue * (thus, effectively batching deferreds|promises of all queries at once) * + * @api * @param ServerConfig $config * @param OperationParams[] $operations * @return ExecutionResult[]|Promise @@ -241,138 +377,9 @@ class Helper } /** - * Parses HTTP request and returns GraphQL OperationParams contained in this request. - * For batched requests it returns an array of OperationParams. + * Send response using standard PHP `header()` and `echo`. * - * This function doesn't check validity of these params. - * - * If $readRawBodyFn argument is not provided - will attempt to read raw request body from php://input stream - * - * @param callable|null $readRawBodyFn - * @return OperationParams|OperationParams[] - * @throws RequestError - */ - public function parseHttpRequest(callable $readRawBodyFn = null) - { - $method = isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : null; - $bodyParams = []; - $urlParams = $_GET; - - if ($method === 'POST') { - $contentType = isset($_SERVER['CONTENT_TYPE']) ? $_SERVER['CONTENT_TYPE'] : null; - - if (stripos($contentType, 'application/graphql') !== false) { - $rawBody = $readRawBodyFn ? $readRawBodyFn() : $this->readRawBody(); - $bodyParams = ['query' => $rawBody ?: '']; - } else if (stripos($contentType, 'application/json') !== false) { - $rawBody = $readRawBodyFn ? $readRawBodyFn() : $this->readRawBody(); - $bodyParams = json_decode($rawBody ?: '', true); - - if (json_last_error()) { - throw new RequestError("Could not parse JSON: " . json_last_error_msg()); - } - if (!is_array($bodyParams)) { - throw new RequestError( - "GraphQL Server expects JSON object or array, but got " . - Utils::printSafeJson($bodyParams) - ); - } - } else if (stripos($contentType, 'application/x-www-form-urlencoded') !== false) { - $bodyParams = $_POST; - } else if (null === $contentType) { - throw new RequestError('Missing "Content-Type" header'); - } else { - throw new RequestError("Unexpected content type: " . Utils::printSafeJson($contentType)); - } - } - - return $this->parseRequestParams($method, $bodyParams, $urlParams); - } - - /** - * Converts PSR7 request to OperationParams[] - * - * @param ServerRequestInterface $request - * @return array|Helper - * @throws RequestError - */ - public function parsePsrRequest(ServerRequestInterface $request) - { - if ($request->getMethod() === 'GET') { - $bodyParams = []; - } else { - $contentType = $request->getHeader('content-type'); - - if (!isset($contentType[0])) { - throw new RequestError('Missing "Content-Type" header'); - } - - if (stripos('application/graphql', $contentType[0]) !== false) { - $bodyParams = ['query' => $request->getBody()->getContents()]; - } else if (stripos('application/json', $contentType[0]) !== false) { - $bodyParams = $request->getParsedBody(); - - if (null === $bodyParams) { - throw new InvariantViolation( - "PSR request is expected to provide parsed body for \"application/json\" requests but got null" - ); - } - - if (!is_array($bodyParams)) { - throw new RequestError( - "GraphQL Server expects JSON object or array, but got " . - Utils::printSafeJson($bodyParams) - ); - } - } else { - $bodyParams = $request->getParsedBody(); - - if (!is_array($bodyParams)) { - throw new RequestError("Unexpected content type: " . Utils::printSafeJson($contentType[0])); - } - } - } - - return $this->parseRequestParams( - $request->getMethod(), - $bodyParams, - $request->getQueryParams() - ); - } - - /** - * Converts query execution result to PSR response - * - * @param Promise|ExecutionResult|ExecutionResult[] $result - * @param ResponseInterface $response - * @param StreamInterface $writableBodyStream - * @return Promise|ResponseInterface - */ - public function toPsrResponse($result, ResponseInterface $response, StreamInterface $writableBodyStream) - { - if ($result instanceof Promise) { - return $result->then(function($actualResult) use ($response, $writableBodyStream) { - return $this->doConvertToPsrResponse($actualResult, $response, $writableBodyStream); - }); - } else { - return $this->doConvertToPsrResponse($result, $response, $writableBodyStream); - } - } - - private function doConvertToPsrResponse($result, ResponseInterface $response, StreamInterface $writableBodyStream) - { - $httpStatus = $this->resolveHttpStatus($result); - - $result = json_encode($result); - $writableBodyStream->write($result); - - return $response - ->withStatus($httpStatus) - ->withHeader('Content-Type', 'application/json') - ->withBody($writableBodyStream); - } - - /** + * @api * @param Promise|ExecutionResult|ExecutionResult[] $result * @param bool $exitWhenDone */ @@ -404,38 +411,6 @@ class Helper } } - /** - * Parses normalized request params and returns instance of OperationParams or array of OperationParams in - * case of batch operation. - * - * Returned value is a suitable input for `executeOperation` or `executeBatch` (if array) - * - * @param string $method - * @param array $bodyParams - * @param array $queryParams - * @return OperationParams|OperationParams[] - * @throws RequestError - */ - public function parseRequestParams($method, array $bodyParams, array $queryParams) - { - if ($method === 'GET') { - $result = OperationParams::create($queryParams, true); - } else if ($method === 'POST') { - if (isset($bodyParams[0])) { - $result = []; - foreach ($bodyParams as $index => $entry) { - $op = OperationParams::create($entry); - $result[] = $op; - } - } else { - $result = OperationParams::create($bodyParams); - } - } else { - throw new RequestError('HTTP Method "' . $method . '" is not supported'); - } - return $result; - } - /** * @return bool|string */ @@ -444,50 +419,6 @@ class Helper return file_get_contents('php://input'); } - /** - * Checks validity of operation params and returns array of errors (empty array when params are valid) - * - * @param OperationParams $params - * @return Error[] - */ - public function validateOperationParams(OperationParams $params) - { - $errors = []; - if (!$params->query && !$params->queryId) { - $errors[] = new RequestError('GraphQL Request must include at least one of those two parameters: "query" or "queryId"'); - } - if ($params->query && $params->queryId) { - $errors[] = new RequestError('GraphQL Request parameters "query" and "queryId" are mutually exclusive'); - } - - if ($params->query !== null && (!is_string($params->query) || empty($params->query))) { - $errors[] = new RequestError( - 'GraphQL Request parameter "query" must be string, but got ' . - Utils::printSafeJson($params->query) - ); - } - if ($params->queryId !== null && (!is_string($params->queryId) || empty($params->queryId))) { - $errors[] = new RequestError( - 'GraphQL Request parameter "queryId" must be string, but got ' . - Utils::printSafeJson($params->queryId) - ); - } - - if ($params->operation !== null && (!is_string($params->operation) || empty($params->operation))) { - $errors[] = new RequestError( - 'GraphQL Request parameter "operation" must be string, but got ' . - Utils::printSafeJson($params->operation) - ); - } - if ($params->variables !== null && (!is_array($params->variables) || isset($params->variables[0]))) { - $errors[] = new RequestError( - 'GraphQL Request parameter "variables" must be object or JSON string parsed to object, but got ' . - Utils::printSafeJson($params->getOriginalInput('variables')) - ); - } - return $errors; - } - /** * @param $result * @return int @@ -522,4 +453,89 @@ class Helper } return $httpStatus; } + + /** + * Converts PSR-7 request to OperationParams[] + * + * @api + * @param ServerRequestInterface $request + * @return array|Helper + * @throws RequestError + */ + public function parsePsrRequest(ServerRequestInterface $request) + { + if ($request->getMethod() === 'GET') { + $bodyParams = []; + } else { + $contentType = $request->getHeader('content-type'); + + if (!isset($contentType[0])) { + throw new RequestError('Missing "Content-Type" header'); + } + + if (stripos('application/graphql', $contentType[0]) !== false) { + $bodyParams = ['query' => $request->getBody()->getContents()]; + } else if (stripos('application/json', $contentType[0]) !== false) { + $bodyParams = $request->getParsedBody(); + + if (null === $bodyParams) { + throw new InvariantViolation( + "PSR-7 request is expected to provide parsed body for \"application/json\" requests but got null" + ); + } + + if (!is_array($bodyParams)) { + throw new RequestError( + "GraphQL Server expects JSON object or array, but got " . + Utils::printSafeJson($bodyParams) + ); + } + } else { + $bodyParams = $request->getParsedBody(); + + if (!is_array($bodyParams)) { + throw new RequestError("Unexpected content type: " . Utils::printSafeJson($contentType[0])); + } + } + } + + return $this->parseRequestParams( + $request->getMethod(), + $bodyParams, + $request->getQueryParams() + ); + } + + /** + * Converts query execution result to PSR-7 response + * + * @api + * @param Promise|ExecutionResult|ExecutionResult[] $result + * @param ResponseInterface $response + * @param StreamInterface $writableBodyStream + * @return Promise|ResponseInterface + */ + public function toPsrResponse($result, ResponseInterface $response, StreamInterface $writableBodyStream) + { + if ($result instanceof Promise) { + return $result->then(function($actualResult) use ($response, $writableBodyStream) { + return $this->doConvertToPsrResponse($actualResult, $response, $writableBodyStream); + }); + } else { + return $this->doConvertToPsrResponse($result, $response, $writableBodyStream); + } + } + + private function doConvertToPsrResponse($result, ResponseInterface $response, StreamInterface $writableBodyStream) + { + $httpStatus = $this->resolveHttpStatus($result); + + $result = json_encode($result); + $writableBodyStream->write($result); + + return $response + ->withStatus($httpStatus) + ->withHeader('Content-Type', 'application/json') + ->withBody($writableBodyStream); + } } diff --git a/src/Server/OperationParams.php b/src/Server/OperationParams.php index 91301db..905dc9a 100644 --- a/src/Server/OperationParams.php +++ b/src/Server/OperationParams.php @@ -2,29 +2,37 @@ namespace GraphQL\Server; /** - * Class QueryParams - * Represents all available parsed query parameters - * - * @package GraphQL\Server + * Structure representing parsed HTTP parameters for GraphQL operation */ class OperationParams { /** - * @var string - */ - public $query; - - /** + * Id of the query (when using persistent queries). + * + * Valid aliases (case-insensitive): + * - id + * - queryId + * - documentId + * + * @api * @var string */ public $queryId; /** + * @api + * @var string + */ + public $query; + + /** + * @api * @var string */ public $operation; /** + * @api * @var array */ public $variables; @@ -42,10 +50,10 @@ class OperationParams /** * Creates an instance from given array * + * @api * @param array $params * @param bool $readonly - * - * @return static + * @return OperationParams */ public static function create(array $params, $readonly = false) { @@ -80,6 +88,7 @@ class OperationParams } /** + * @api * @param string $key * @return mixed */ @@ -89,6 +98,10 @@ class OperationParams } /** + * Indicates that operation is executed in read-only context + * (e.g. via HTTP GET request) + * + * @api * @return bool */ public function isReadOnly() diff --git a/src/Server/ServerConfig.php b/src/Server/ServerConfig.php index 5eb515e..dbc9e28 100644 --- a/src/Server/ServerConfig.php +++ b/src/Server/ServerConfig.php @@ -6,10 +6,28 @@ use GraphQL\Executor\Promise\PromiseAdapter; use GraphQL\Type\Schema; use GraphQL\Utils\Utils; +/** + * Server configuration class. + * Could be passed directly to server constructor. List of options accepted by **create** method is + * [described in docs](executing-queries/#server-configuration-options). + * + * Usage example: + * + * $config = GraphQL\Server\ServerConfig::create() + * ->setSchema($mySchema) + * ->setContext($myContext); + * + * $server = new GraphQL\Server\StandardServer($config); + */ class ServerConfig { /** - * @return static + * Converts an array of options to instance of ServerConfig + * (or just returns empty config when array is not passed). + * + * @api + * @param array $config + * @return ServerConfig */ public static function create(array $config = []) { @@ -80,14 +98,18 @@ class ServerConfig private $persistentQueryLoader; /** - * @return mixed|callable + * @api + * @param Schema $schema + * @return $this */ - public function getContext() + public function setSchema(Schema $schema) { - return $this->context; + $this->schema = $schema; + return $this; } /** + * @api * @param mixed|\Closure $context * @return $this */ @@ -98,6 +120,7 @@ class ServerConfig } /** + * @api * @param mixed|\Closure $rootValue * @return $this */ @@ -107,6 +130,123 @@ class ServerConfig return $this; } + /** + * Expects function(Throwable $e) : array + * + * @api + * @param callable $errorFormatter + * @return $this + */ + public function setErrorFormatter(callable $errorFormatter) + { + $this->errorFormatter = $errorFormatter; + return $this; + } + + /** + * Expects function(array $errors, callable $formatter) : array + * + * @api + * @param callable $handler + * @return $this + */ + public function setErrorsHandler(callable $handler) + { + $this->errorsHandler = $handler; + return $this; + } + + /** + * Set validation rules for this server. + * + * @api + * @param array|callable + * @return $this + */ + public function setValidationRules($validationRules) + { + if (!is_callable($validationRules) && !is_array($validationRules) && $validationRules !== null) { + throw new InvariantViolation( + 'Server config expects array of validation rules or callable returning such array, but got ' . + Utils::printSafe($validationRules) + ); + } + + $this->validationRules = $validationRules; + return $this; + } + + /** + * @api + * @param callable $fieldResolver + * @return $this + */ + public function setFieldResolver(callable $fieldResolver) + { + $this->fieldResolver = $fieldResolver; + return $this; + } + + /** + * Expects function($queryId, OperationParams $params) : string|DocumentNode + * + * This function must return query string or valid DocumentNode. + * + * @api + * @param callable $persistentQueryLoader + * @return $this + */ + public function setPersistentQueryLoader(callable $persistentQueryLoader) + { + $this->persistentQueryLoader = $persistentQueryLoader; + return $this; + } + + /** + * Set response debug flags. See GraphQL\Error\Debug class for a list of all available flags + * + * @api + * @param bool|int $set + * @return $this + */ + public function setDebug($set = true) + { + $this->debug = $set; + return $this; + } + + /** + * Allow batching queries (disabled by default) + * + * @api + * @param bool $enableBatching + * @return $this + */ + public function setQueryBatching($enableBatching) + { + $this->queryBatching = (bool) $enableBatching; + return $this; + } + + /** + * @api + * @param PromiseAdapter $promiseAdapter + * @return $this + */ + public function setPromiseAdapter(PromiseAdapter $promiseAdapter) + { + $this->promiseAdapter = $promiseAdapter; + return $this; + } + + /** + * @return mixed|callable + */ + public function getContext() + { + return $this->context; + } + /** * @return mixed|callable */ @@ -115,18 +255,6 @@ class ServerConfig return $this->rootValue; } - /** - * Set schema instance - * - * @param Schema $schema - * @return $this - */ - public function setSchema(Schema $schema) - { - $this->schema = $schema; - return $this; - } - /** * @return Schema */ @@ -143,30 +271,6 @@ class ServerConfig return $this->errorFormatter; } - /** - * Expects function(Throwable $e) : array - * - * @param callable $errorFormatter - * @return $this - */ - public function setErrorFormatter(callable $errorFormatter) - { - $this->errorFormatter = $errorFormatter; - return $this; - } - - /** - * Expects function(array $errors, callable $formatter) : array - * - * @param callable $handler - * @return $this - */ - public function setErrorsHandler(callable $handler) - { - $this->errorsHandler = $handler; - return $this; - } - /** * @return callable|null */ @@ -183,16 +287,6 @@ class ServerConfig return $this->promiseAdapter; } - /** - * @param PromiseAdapter $promiseAdapter - * @return $this - */ - public function setPromiseAdapter(PromiseAdapter $promiseAdapter) - { - $this->promiseAdapter = $promiseAdapter; - return $this; - } - /** * @return array|callable */ @@ -201,25 +295,6 @@ class ServerConfig return $this->validationRules; } - /** - * Set validation rules for this server. - * - * @param array|callable - * @return $this - */ - public function setValidationRules($validationRules) - { - if (!is_callable($validationRules) && !is_array($validationRules) && $validationRules !== null) { - throw new InvariantViolation( - 'Server config expects array of validation rules or callable returning such array, but got ' . - Utils::printSafe($validationRules) - ); - } - - $this->validationRules = $validationRules; - return $this; - } - /** * @return callable */ @@ -228,16 +303,6 @@ class ServerConfig return $this->fieldResolver; } - /** - * @param callable $fieldResolver - * @return $this - */ - public function setFieldResolver(callable $fieldResolver) - { - $this->fieldResolver = $fieldResolver; - return $this; - } - /** * @return callable */ @@ -246,19 +311,6 @@ class ServerConfig return $this->persistentQueryLoader; } - /** - * A function that takes an input id and returns a valid Document. - * If provided, this will allow your GraphQL endpoint to execute a document specified via `queryId`. - * - * @param callable $persistentQueryLoader - * @return ServerConfig - */ - public function setPersistentQueryLoader(callable $persistentQueryLoader) - { - $this->persistentQueryLoader = $persistentQueryLoader; - return $this; - } - /** * @return bool */ @@ -267,18 +319,6 @@ class ServerConfig return $this->debug; } - /** - * Set response debug flags, see GraphQL\Error\Debug class for a list of available flags - * - * @param bool|int $set - * @return $this - */ - public function setDebug($set = true) - { - $this->debug = $set; - return $this; - } - /** * @return bool */ @@ -286,16 +326,4 @@ class ServerConfig { return $this->queryBatching; } - - /** - * Allow batching queries - * - * @param bool $enableBatching - * @return ServerConfig - */ - public function setQueryBatching($enableBatching) - { - $this->queryBatching = (bool) $enableBatching; - return $this; - } } diff --git a/src/Server/StandardServer.php b/src/Server/StandardServer.php index ed8e135..80b1be3 100644 --- a/src/Server/StandardServer.php +++ b/src/Server/StandardServer.php @@ -10,12 +10,26 @@ use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\StreamInterface; /** - * Class StandardServer + * GraphQL server compatible with both: [express-graphql](https://github.com/graphql/express-graphql) + * and [Apollo Server](https://github.com/apollographql/graphql-server). + * Usage Example: * - * GraphQL server compatible with both: - * https://github.com/graphql/express-graphql and https://github.com/apollographql/graphql-server + * $server = new StandardServer([ + * 'schema' => $mySchema + * ]); + * $server->handleRequest(); + * + * Or using [ServerConfig](reference/#graphqlserverserverconfig) instance: + * + * $config = GraphQL\Server\ServerConfig::create() + * ->setSchema($mySchema) + * ->setContext($myContext); + * + * $server = new GraphQL\Server\StandardServer($config); + * $server->handleRequest(); + * + * See [dedicated section in docs](executing-queries/#using-server) for details. * - * @package GraphQL\Server */ class StandardServer { @@ -30,8 +44,9 @@ class StandardServer private $helper; /** - * Standard GraphQL HTTP server implementation + * Creates new instance of a standard GraphQL HTTP server * + * @api * @param ServerConfig|array $config */ public function __construct($config) @@ -46,22 +61,36 @@ class StandardServer } /** - * Executes GraphQL operation with given server configuration and returns execution result - * (or promise when promise adapter is different from SyncPromiseAdapter) + * Parses HTTP request, executes and emits response (using standard PHP `header` function and `echo`) * - * @param ServerRequestInterface $request - * @return ExecutionResult|ExecutionResult[]|Promise + * By default (when $parsedBody is not set) it uses PHP globals to parse a request. + * It is possible to implement request parsing elsewhere (e.g. using framework Request instance) + * and then pass it to the server. + * + * See `executeRequest()` if you prefer to emit response yourself + * (e.g. using Response object of some framework) + * + * @api + * @param OperationParams|OperationParams[] $parsedBody + * @param bool $exitWhenDone */ - public function executePsrRequest(ServerRequestInterface $request) + public function handleRequest($parsedBody = null, $exitWhenDone = false) { - $parsedBody = $this->helper->parsePsrRequest($request); - return $this->executeRequest($parsedBody); + $result = $this->executeRequest($parsedBody); + $this->helper->sendResponse($result, $exitWhenDone); } /** - * Executes GraphQL operation with given server configuration and returns execution result - * (or promise when promise adapter is different from SyncPromiseAdapter) + * Executes GraphQL operation and returns execution result + * (or promise when promise adapter is different from SyncPromiseAdapter). * + * By default (when $parsedBody is not set) it uses PHP globals to parse a request. + * It is possible to implement request parsing elsewhere (e.g. using framework Request instance) + * and then pass it to the server. + * + * PSR-7 compatible method executePsrRequest() does exactly this. + * + * @api * @param OperationParams|OperationParams[] $parsedBody * @return ExecutionResult|ExecutionResult[]|Promise * @throws InvariantViolation @@ -79,7 +108,13 @@ class StandardServer } } - /** + /** + * Executes PSR-7 request and fulfills PSR-7 response. + * + * See `executePsrRequest()` if you prefer to create response yourself + * (e.g. using specific JsonResponse instance of some framework). + * + * @api * @param ServerRequestInterface $request * @param ResponseInterface $response * @param StreamInterface $writableBodyStream @@ -96,12 +131,28 @@ class StandardServer } /** - * @param OperationParams|OperationParams[] $parsedBody - * @param bool $exitWhenDone + * Executes GraphQL operation and returns execution result + * (or promise when promise adapter is different from SyncPromiseAdapter) + * + * @api + * @param ServerRequestInterface $request + * @return ExecutionResult|ExecutionResult[]|Promise */ - public function handleRequest($parsedBody = null, $exitWhenDone = false) + public function executePsrRequest(ServerRequestInterface $request) { - $result = $this->executeRequest($parsedBody); - $this->helper->sendResponse($result, $exitWhenDone); + $parsedBody = $this->helper->parsePsrRequest($request); + return $this->executeRequest($parsedBody); + } + + /** + * Returns an instance of Server helper, which contains most of the actual logic for + * parsing / validating / executing request (which could be re-used by other server implementations) + * + * @api + * @return Helper + */ + public function getHelper() + { + return $this->helper; } } diff --git a/src/Type/Definition/Config.php b/src/Type/Definition/Config.php index 997531c..ba4cb27 100644 --- a/src/Type/Definition/Config.php +++ b/src/Type/Definition/Config.php @@ -65,7 +65,7 @@ class Config Warning::warnOnce( 'GraphQL\Type\Defintion\Config is deprecated and will be removed in the next version. ' . 'See https://github.com/webonyx/graphql-php/issues/148 for alternatives', - Warning::CONFIG_DEPRECATION_WARNING, + Warning::WARNING_CONFIG_DEPRECATION, E_USER_DEPRECATED ); @@ -143,7 +143,7 @@ class Config if (!self::$allowCustomOptions) { Warning::warnOnce( sprintf('Error in "%s" type definition: Non-standard keys "%s" ' . $suffix, $typeName, implode(', ', $unexpectedKeys)), - Warning::CONFIG_WARNING + Warning::WARNING_CONFIG ); } $map = array_intersect_key($map, $definitions); diff --git a/src/Type/Definition/ListOfType.php b/src/Type/Definition/ListOfType.php index 49119b1..6c45466 100644 --- a/src/Type/Definition/ListOfType.php +++ b/src/Type/Definition/ListOfType.php @@ -11,7 +11,7 @@ use GraphQL\Utils\Utils; class ListOfType extends Type implements WrappingType, OutputType, InputType { /** - * @var callable|Type + * @var ObjectType|InterfaceType|UnionType|ScalarType|InputObjectType|EnumType */ public $ofType; @@ -40,7 +40,7 @@ class ListOfType extends Type implements WrappingType, OutputType, InputType /** * @param bool $recurse - * @return mixed + * @return ObjectType|InterfaceType|UnionType|ScalarType|InputObjectType|EnumType */ public function getWrappedType($recurse = false) { diff --git a/src/Type/Definition/NonNull.php b/src/Type/Definition/NonNull.php index 9be6f06..4499c74 100644 --- a/src/Type/Definition/NonNull.php +++ b/src/Type/Definition/NonNull.php @@ -11,7 +11,7 @@ use GraphQL\Utils\Utils; class NonNull extends Type implements WrappingType, OutputType, InputType { /** - * @var callable|Type + * @var ObjectType|InterfaceType|UnionType|ScalarType|InputObjectType|EnumType */ private $ofType; @@ -40,8 +40,8 @@ class NonNull extends Type implements WrappingType, OutputType, InputType /** * @param bool $recurse - * @return mixed - * @throws \Exception + * @return ObjectType|InterfaceType|UnionType|ScalarType|InputObjectType|EnumType + * @throws InvariantViolation */ public function getWrappedType($recurse = false) { diff --git a/src/Type/Definition/ResolveInfo.php b/src/Type/Definition/ResolveInfo.php index 7fe0174..38a259b 100644 --- a/src/Type/Definition/ResolveInfo.php +++ b/src/Type/Definition/ResolveInfo.php @@ -12,12 +12,15 @@ use GraphQL\Type\Schema; use GraphQL\Utils\Utils; /** - * Class ResolveInfo - * @package GraphQL\Type\Definition + * Structure containing information useful for field resolution process. + * Passed as 3rd argument to every field resolver. See [docs on field resolving (data fetching)](data-fetching/). */ class ResolveInfo { /** + * The name of the field being resolved + * + * @api * @var string */ public $fieldName; @@ -29,47 +32,74 @@ class ResolveInfo public $fieldASTs; /** + * AST of all nodes referencing this field in the query. + * + * @api * @var FieldNode[] */ public $fieldNodes; /** + * Expected return type of the field being resolved + * + * @api * @var ScalarType|ObjectType|InterfaceType|UnionType|EnumType|ListOfType|NonNull */ public $returnType; /** - * @var ObjectType|InterfaceType|UnionType + * Parent type of the field being resolved + * + * @api + * @var ObjectType */ public $parentType; /** + * Path to this field from the very root value + * + * @api * @var array */ public $path; /** + * Instance of a schema used for execution + * + * @api * @var Schema */ public $schema; /** + * AST of all fragments defined in query + * + * @api * @var FragmentDefinitionNode[] */ public $fragments; /** + * Root value passed to query execution + * + * @api * @var mixed */ public $rootValue; /** + * AST of operation definition node (query, mutation) + * + * @api * @var OperationDefinitionNode */ public $operation; /** - * @var array + * Array of variables passed to query execution + * + * @api + * @var array */ public $variableValues; @@ -107,6 +137,10 @@ class ResolveInfo * ] * ] * + * Warning: this method it is a naive implementation which does not take into account + * conditional typed fragments. So use it with care for fields of interface and union types. + * + * @api * @param int $depth How many levels to include in output * @return array */ diff --git a/src/Type/Definition/Type.php b/src/Type/Definition/Type.php index c42ba46..749780d 100644 --- a/src/Type/Definition/Type.php +++ b/src/Type/Definition/Type.php @@ -4,17 +4,12 @@ namespace GraphQL\Type\Definition; use GraphQL\Error\InvariantViolation; use GraphQL\Utils\Utils; -/* -export type GraphQLType = -GraphQLScalarType | -GraphQLObjectType | -GraphQLInterfaceType | -GraphQLUnionType | -GraphQLEnumType | -GraphQLInputObjectType | -GraphQLList | -GraphQLNonNull; -*/ +/** + * Registry of standard GraphQL types + * and a base class for all other types. + * + * @package GraphQL\Type\Definition + */ abstract class Type implements \JsonSerializable { const STRING = 'String'; @@ -29,6 +24,7 @@ abstract class Type implements \JsonSerializable private static $internalTypes; /** + * @api * @return IDType */ public static function id() @@ -37,6 +33,7 @@ abstract class Type implements \JsonSerializable } /** + * @api * @return StringType */ public static function string() @@ -45,6 +42,7 @@ abstract class Type implements \JsonSerializable } /** + * @api * @return BooleanType */ public static function boolean() @@ -53,6 +51,7 @@ abstract class Type implements \JsonSerializable } /** + * @api * @return IntType */ public static function int() @@ -61,6 +60,7 @@ abstract class Type implements \JsonSerializable } /** + * @api * @return FloatType */ public static function float() @@ -69,7 +69,8 @@ abstract class Type implements \JsonSerializable } /** - * @param $wrappedType + * @api + * @param ObjectType|InterfaceType|UnionType|ScalarType|InputObjectType|EnumType $wrappedType * @return ListOfType */ public static function listOf($wrappedType) @@ -78,7 +79,8 @@ abstract class Type implements \JsonSerializable } /** - * @param $wrappedType + * @api + * @param ObjectType|InterfaceType|UnionType|ScalarType|InputObjectType|EnumType $wrappedType * @return NonNull */ public static function nonNull($wrappedType) @@ -113,7 +115,8 @@ abstract class Type implements \JsonSerializable } /** - * @param $type + * @api + * @param Type $type * @return bool */ public static function isInputType($type) @@ -123,7 +126,8 @@ abstract class Type implements \JsonSerializable } /** - * @param $type + * @api + * @param Type $type * @return bool */ public static function isOutputType($type) @@ -133,6 +137,7 @@ abstract class Type implements \JsonSerializable } /** + * @api * @param $type * @return bool */ @@ -142,7 +147,8 @@ abstract class Type implements \JsonSerializable } /** - * @param $type + * @api + * @param Type $type * @return bool */ public static function isCompositeType($type) @@ -151,7 +157,8 @@ abstract class Type implements \JsonSerializable } /** - * @param $type + * @api + * @param Type $type * @return bool */ public static function isAbstractType($type) @@ -160,8 +167,9 @@ abstract class Type implements \JsonSerializable } /** - * @param $type - * @return Type + * @api + * @param Type $type + * @return ObjectType|InterfaceType|UnionType|ScalarType|InputObjectType|EnumType|ListOfType */ public static function getNullableType($type) { @@ -169,8 +177,9 @@ abstract class Type implements \JsonSerializable } /** - * @param $type - * @return UnmodifiedType + * @api + * @param Type $type + * @return ObjectType|InterfaceType|UnionType|ScalarType|InputObjectType|EnumType */ public static function getNamedType($type) { diff --git a/src/Type/Definition/WrappingType.php b/src/Type/Definition/WrappingType.php index f2469ce..a5cee76 100644 --- a/src/Type/Definition/WrappingType.php +++ b/src/Type/Definition/WrappingType.php @@ -1,15 +1,11 @@ $MyAppQueryRootType, * 'mutation' => $MyAppMutationRootType, * ]); * - * Note: If an array of `directives` are provided to GraphQL\Schema, that will be - * the exact list of directives represented and allowed. If `directives` is not - * provided then a default set of the specified directives (e.g. @include and - * @skip) will be used. If you wish to provide *additional* directives to these - * specified directives, you must explicitly declare them. Example: + * Or using Schema Config instance: * - * $mySchema = new GraphQL\Schema([ - * ... - * 'directives' => array_merge(GraphQL::getInternalDirectives(), [ $myCustomDirective ]), - * ]) + * $config = GraphQL\Type\SchemaConfig::create() + * ->setQuery($MyAppQueryRootType) + * ->setMutation($MyAppMutationRootType); + * + * $schema = new GraphQL\Type\Schema($config); * * @package GraphQL */ @@ -71,9 +66,10 @@ class Schema /** * Schema constructor. * + * @api * @param array|SchemaConfig $config */ - public function __construct($config = null) + public function __construct($config) { if (func_num_args() > 1 || $config instanceof Type) { trigger_error( @@ -131,6 +127,7 @@ class Schema /** * Returns schema query type * + * @api * @return ObjectType */ public function getQueryType() @@ -141,6 +138,7 @@ class Schema /** * Returns schema mutation type * + * @api * @return ObjectType|null */ public function getMutationType() @@ -151,6 +149,7 @@ class Schema /** * Returns schema subscription * + * @api * @return ObjectType|null */ public function getSubscriptionType() @@ -159,6 +158,7 @@ class Schema } /** + * @api * @return SchemaConfig */ public function getConfig() @@ -172,6 +172,7 @@ class Schema * * This operation requires full schema scan. Do not use in production environment. * + * @api * @return Type[] */ public function getTypeMap() @@ -186,6 +187,7 @@ class Schema /** * Returns type by it's name * + * @api * @param string $name * @return Type */ @@ -248,6 +250,7 @@ class Schema * * This operation requires full schema scan. Do not use in production environment. * + * @api * @param AbstractType $abstractType * @return ObjectType[] */ @@ -311,6 +314,7 @@ class Schema * Returns true if object type is concrete type of given abstract type * (implementation for interfaces and members of union type for unions) * + * @api * @param AbstractType $abstractType * @param ObjectType $possibleType * @return bool @@ -328,16 +332,18 @@ class Schema /** * Returns a list of directives supported by this schema * + * @api * @return Directive[] */ public function getDirectives() { - return $this->config->directives ?: GraphQL::getInternalDirectives(); + return $this->config->directives ?: GraphQL::getStandardDirectives(); } /** * Returns instance of directive by name * + * @api * @param $name * @return Directive */ @@ -367,6 +373,7 @@ class Schema * * This operation requires full schema scan. Do not use in production environment. * + * @api * @throws InvariantViolation */ public function assertValid() diff --git a/src/Type/SchemaConfig.php b/src/Type/SchemaConfig.php index bdee004..ee47404 100644 --- a/src/Type/SchemaConfig.php +++ b/src/Type/SchemaConfig.php @@ -7,10 +7,18 @@ use GraphQL\Type\Definition\Type; use GraphQL\Utils\Utils; /** - * Class Config - * Note: properties are marked as public for performance reasons. They should be considered read-only. + * Schema configuration class. + * Could be passed directly to schema constructor. List of options accepted by **create** method is + * [described in docs](type-system/schema/#configuration-options). + * + * Usage example: + * + * $config = SchemaConfig::create() + * ->setQuery($myQueryType) + * ->setTypeLoader($myTypeLoader); + * + * $schema = new Schema($config); * - * @package GraphQL */ class SchemaConfig { @@ -46,7 +54,9 @@ class SchemaConfig /** * Converts an array of options to instance of SchemaConfig + * (or just returns empty config when array is not passed). * + * @api * @param array $options * @return SchemaConfig */ @@ -128,14 +138,7 @@ class SchemaConfig } /** - * @return ObjectType - */ - public function getQuery() - { - return $this->query; - } - - /** + * @api * @param ObjectType $query * @return SchemaConfig */ @@ -146,14 +149,7 @@ class SchemaConfig } /** - * @return ObjectType - */ - public function getMutation() - { - return $this->mutation; - } - - /** + * @api * @param ObjectType $mutation * @return SchemaConfig */ @@ -164,14 +160,7 @@ class SchemaConfig } /** - * @return ObjectType - */ - public function getSubscription() - { - return $this->subscription; - } - - /** + * @api * @param ObjectType $subscription * @return SchemaConfig */ @@ -182,14 +171,7 @@ class SchemaConfig } /** - * @return Type[] - */ - public function getTypes() - { - return $this->types ?: []; - } - - /** + * @api * @param Type[]|callable $types * @return SchemaConfig */ @@ -200,14 +182,7 @@ class SchemaConfig } /** - * @return Directive[] - */ - public function getDirectives() - { - return $this->directives ?: []; - } - - /** + * @api * @param Directive[] $directives * @return SchemaConfig */ @@ -218,14 +193,7 @@ class SchemaConfig } /** - * @return callable - */ - public function getTypeLoader() - { - return $this->typeLoader; - } - - /** + * @api * @param callable $typeLoader * @return SchemaConfig */ @@ -234,4 +202,58 @@ class SchemaConfig $this->typeLoader = $typeLoader; return $this; } + + /** + * @api + * @return ObjectType + */ + public function getQuery() + { + return $this->query; + } + + /** + * @api + * @return ObjectType + */ + public function getMutation() + { + return $this->mutation; + } + + /** + * @api + * @return ObjectType + */ + public function getSubscription() + { + return $this->subscription; + } + + /** + * @api + * @return Type[] + */ + public function getTypes() + { + return $this->types ?: []; + } + + /** + * @api + * @return Directive[] + */ + public function getDirectives() + { + return $this->directives ?: []; + } + + /** + * @api + * @return callable + */ + public function getTypeLoader() + { + return $this->typeLoader; + } } diff --git a/src/Utils/TypeInfo.php b/src/Utils/TypeInfo.php index 0ccbffd..a03ecf7 100644 --- a/src/Utils/TypeInfo.php +++ b/src/Utils/TypeInfo.php @@ -98,7 +98,7 @@ class TypeInfo Warning::warnOnce( 'One of the schema types is not a valid type definition instance. '. 'Try running $schema->assertValid() to find out the cause of this warning.', - Warning::NOT_A_TYPE + Warning::WARNING_NOT_A_TYPE ); return $typeMap; } diff --git a/src/Utils/Utils.php b/src/Utils/Utils.php index be96621..1861eb0 100644 --- a/src/Utils/Utils.php +++ b/src/Utils/Utils.php @@ -35,7 +35,7 @@ class Utils $cls = get_class($obj); Warning::warn( "Trying to set non-existing property '$key' on class '$cls'", - Warning::ASSIGN_WARNING + Warning::WARNING_ASSIGN ); } $obj->{$key} = $value; @@ -437,7 +437,7 @@ class Utils 'Name "'.$name.'" must not begin with "__", which is reserved by ' . 'GraphQL introspection. In a future release of graphql this will ' . 'become an exception', - Warning::NAME_WARNING + Warning::WARNING_NAME ); } diff --git a/src/Validator/DocumentValidator.php b/src/Validator/DocumentValidator.php index f87e708..d154dcb 100644 --- a/src/Validator/DocumentValidator.php +++ b/src/Validator/DocumentValidator.php @@ -48,6 +48,24 @@ use GraphQL\Validator\Rules\UniqueVariableNames; use GraphQL\Validator\Rules\VariablesAreInputTypes; use GraphQL\Validator\Rules\VariablesInAllowedPosition; +/** + * Implements the "Validation" section of the spec. + * + * Validation runs synchronously, returning an array of encountered errors, or + * an empty array if no errors were encountered and the document is valid. + * + * A list of specific validation rules may be provided. If not provided, the + * default list of rules defined by the GraphQL specification will be used. + * + * Each validation rule is an instance of GraphQL\Validator\Rules\AbstractValidationRule + * which returns a visitor (see the [GraphQL\Language\Visitor API](reference/#graphqllanguagevisitor)). + * + * Visitor methods are expected to return an instance of [GraphQL\Error\Error](reference/#graphqlerrorerror), + * or array of such instances when invalid. + * + * Optionally a custom TypeInfo instance may be provided. If not provided, one + * will be created from the provided schema. + */ class DocumentValidator { private static $rules = []; @@ -59,9 +77,36 @@ class DocumentValidator private static $initRules = false; /** - * Returns all validation rules + * Primary method for query validation. See class description for details. * - * @return callable[] + * @api + * @param Schema $schema + * @param DocumentNode $ast + * @param AbstractValidationRule[]|null $rules + * @param TypeInfo|null $typeInfo + * @return Error[] + */ + public static function validate( + Schema $schema, + DocumentNode $ast, + array $rules = null, + TypeInfo $typeInfo = null + ) + { + if (null === $rules) { + $rules = static::allRules(); + } + $typeInfo = $typeInfo ?: new TypeInfo($schema); + $errors = static::visitUsingRules($schema, $typeInfo, $ast, $rules); + return $errors; + } + + + /** + * Returns all global validation rules. + * + * @api + * @return AbstractValidationRule[] */ public static function allRules() { @@ -128,8 +173,12 @@ class DocumentValidator } /** - * Returns validation rule + * Returns global validation rule by name. Standard rules are named by class name, so + * example usage for such rules: * + * $rule = DocumentValidator::getRule(GraphQL\Validator\Rules\QueryComplexity::class); + * + * @api * @param string $name * @return AbstractValidationRule */ @@ -146,8 +195,9 @@ class DocumentValidator } /** - * Add rule to list of default validation rules + * Add rule to list of global validation rules * + * @api * @param AbstractValidationRule $rule */ public static function addRule(AbstractValidationRule $rule) @@ -155,43 +205,6 @@ class DocumentValidator self::$rules[$rule->getName()] = $rule; } - /** - * Implements the "Validation" section of the spec. - * - * Validation runs synchronously, returning an array of encountered errors, or - * an empty array if no errors were encountered and the document is valid. - * - * A list of specific validation rules may be provided. If not provided, the - * default list of rules defined by the GraphQL specification will be used. - * - * Each validation rules is a function which returns a visitor - * (see the GraphQL\Language\Visitor API). Visitor methods are expected to return - * GraphQL\Error\Error, or arrays of GraphQL\Error\Error when invalid. - * - * Optionally a custom TypeInfo instance may be provided. If not provided, one - * will be created from the provided schema. - * - * @param Schema $schema - * @param DocumentNode $ast - * @param array|null $rules - * @param TypeInfo|null $typeInfo - * @return Error[] - */ - public static function validate( - Schema $schema, - DocumentNode $ast, - array $rules = null, - TypeInfo $typeInfo = null - ) - { - if (null === $rules) { - $rules = static::allRules(); - } - $typeInfo = $typeInfo ?: new TypeInfo($schema); - $errors = static::visitUsingRules($schema, $typeInfo, $ast, $rules); - return $errors; - } - public static function isError($value) { return is_array($value) diff --git a/tests/Executor/AbstractPromiseTest.php b/tests/Executor/AbstractPromiseTest.php index e4feeef..5b0f576 100644 --- a/tests/Executor/AbstractPromiseTest.php +++ b/tests/Executor/AbstractPromiseTest.php @@ -87,9 +87,9 @@ class AbstractPromiseTest extends \PHPUnit_Framework_TestCase } }'; - Warning::suppress(Warning::FULL_SCHEMA_SCAN_WARNING); + Warning::suppress(Warning::WARNING_FULL_SCHEMA_SCAN); $result = GraphQL::execute($schema, $query); - Warning::enable(Warning::FULL_SCHEMA_SCAN_WARNING); + Warning::enable(Warning::WARNING_FULL_SCHEMA_SCAN); $expected = [ 'data' => [ @@ -174,9 +174,9 @@ class AbstractPromiseTest extends \PHPUnit_Framework_TestCase } }'; - Warning::suppress(Warning::FULL_SCHEMA_SCAN_WARNING); + Warning::suppress(Warning::WARNING_FULL_SCHEMA_SCAN); $result = GraphQL::execute($schema, $query); - Warning::enable(Warning::FULL_SCHEMA_SCAN_WARNING); + Warning::enable(Warning::WARNING_FULL_SCHEMA_SCAN); $expected = [ 'data' => [ diff --git a/tests/Executor/ExecutorLazySchemaTest.php b/tests/Executor/ExecutorLazySchemaTest.php index 92c0394..b7d2181 100644 --- a/tests/Executor/ExecutorLazySchemaTest.php +++ b/tests/Executor/ExecutorLazySchemaTest.php @@ -122,11 +122,11 @@ class ExecutorLazySchemaTest extends \PHPUnit_Framework_TestCase ], ]); - Warning::suppress(Warning::FULL_SCHEMA_SCAN_WARNING); + Warning::suppress(Warning::WARNING_FULL_SCHEMA_SCAN); $result = Executor::execute($schema, Parser::parse($query)); $this->assertEquals($expected, $result); - Warning::enable(Warning::FULL_SCHEMA_SCAN_WARNING); + Warning::enable(Warning::WARNING_FULL_SCHEMA_SCAN); $result = Executor::execute($schema, Parser::parse($query)); $this->assertEquals(1, count($result->errors)); $this->assertInstanceOf('PHPUnit_Framework_Error_Warning', $result->errors[0]->getPrevious()); diff --git a/tests/Executor/UnionInterfaceTest.php b/tests/Executor/UnionInterfaceTest.php index 827ea9e..8fc3d8a 100644 --- a/tests/Executor/UnionInterfaceTest.php +++ b/tests/Executor/UnionInterfaceTest.php @@ -256,9 +256,9 @@ class UnionInterfaceTest extends \PHPUnit_Framework_TestCase ] ]; - Warning::suppress(Warning::FULL_SCHEMA_SCAN_WARNING); + Warning::suppress(Warning::WARNING_FULL_SCHEMA_SCAN); $this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->john)->toArray()); - Warning::enable(Warning::FULL_SCHEMA_SCAN_WARNING); + Warning::enable(Warning::WARNING_FULL_SCHEMA_SCAN); } /** @@ -294,9 +294,9 @@ class UnionInterfaceTest extends \PHPUnit_Framework_TestCase ] ]; - Warning::suppress(Warning::FULL_SCHEMA_SCAN_WARNING); + Warning::suppress(Warning::WARNING_FULL_SCHEMA_SCAN); $this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->john)->toArray()); - Warning::enable(Warning::FULL_SCHEMA_SCAN_WARNING); + Warning::enable(Warning::WARNING_FULL_SCHEMA_SCAN); } /** @@ -351,9 +351,9 @@ class UnionInterfaceTest extends \PHPUnit_Framework_TestCase ] ]; - Warning::suppress(Warning::FULL_SCHEMA_SCAN_WARNING); + Warning::suppress(Warning::WARNING_FULL_SCHEMA_SCAN); $this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->john)->toArray()); - Warning::enable(Warning::FULL_SCHEMA_SCAN_WARNING); + Warning::enable(Warning::WARNING_FULL_SCHEMA_SCAN); } /** diff --git a/tests/Server/RequestParsingTest.php b/tests/Server/RequestParsingTest.php index 9b8e74c..c26922c 100644 --- a/tests/Server/RequestParsingTest.php +++ b/tests/Server/RequestParsingTest.php @@ -175,7 +175,7 @@ class RequestParsingTest extends \PHPUnit_Framework_TestCase } catch (InvariantViolation $e) { // Expecting parsing exception to be thrown somewhere else: $this->assertEquals( - 'PSR request is expected to provide parsed body for "application/json" requests but got null', + 'PSR-7 request is expected to provide parsed body for "application/json" requests but got null', $e->getMessage() ); } diff --git a/tests/Type/ConfigTest.php b/tests/Type/ConfigTest.php index 0d7d645..1a15134 100644 --- a/tests/Type/ConfigTest.php +++ b/tests/Type/ConfigTest.php @@ -15,13 +15,13 @@ class ConfigTest extends \PHPUnit_Framework_TestCase { public function setUp() { - Warning::suppress(Warning::CONFIG_DEPRECATION_WARNING); + Warning::suppress(Warning::WARNING_CONFIG_DEPRECATION); } public static function tearDownAfterClass() { Config::disableValidation(); - Warning::enable(Warning::CONFIG_DEPRECATION_WARNING); + Warning::enable(Warning::WARNING_CONFIG_DEPRECATION); } public function testToggling() diff --git a/tests/Type/ValidationTest.php b/tests/Type/ValidationTest.php index f1dcb25..6c062f3 100644 --- a/tests/Type/ValidationTest.php +++ b/tests/Type/ValidationTest.php @@ -122,13 +122,13 @@ class ValidationTest extends \PHPUnit_Framework_TestCase $this->notInputTypes[] = $this->String; - Warning::suppress(Warning::NOT_A_TYPE); + Warning::suppress(Warning::WARNING_NOT_A_TYPE); } public function tearDown() { parent::tearDown(); - Warning::enable(Warning::NOT_A_TYPE); + Warning::enable(Warning::WARNING_NOT_A_TYPE); } public function testRejectsTypesWithoutNames()