Version 4.5.0

This commit is contained in:
Neo committed 2025-12-19 05:51:58 -08:00
1 parent 1a0c7fd3c2
commit f79dcf067a
3791 files changed
+248032 -76372

No files matched your search

+31
View File
@@ -0,0 +1,31 @@
<?php
namespace GraphQL\Error;
/**
* This interface is used for [default error formatting](error-handling.md).
*
* 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 a client.
*
* @api
* @return bool
*/
public function isClientSafe();
/**
* 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();
}
+12
View File
@@ -0,0 +1,12 @@
<?php
namespace GraphQL\Error;
/**
* Collection of flags for [error debugging](error-handling.md#debugging-tools).
*/
class Debug
{
const INCLUDE_DEBUG_MESSAGE = 1;
const INCLUDE_TRACE = 2;
const RETHROW_INTERNAL_EXCEPTIONS = 4;
}
+300
View File
@@ -0,0 +1,300 @@
<?php
namespace GraphQL\Error;
use GraphQL\Language\Source;
use GraphQL\Language\SourceLocation;
use GraphQL\Utils\Utils;
/**
* Describes an Error found during the parse, validate, or
* execute phases of performing a GraphQL operation. In addition to a message
* and stack trace, it also includes information about the locations in a
* GraphQL document and/or execution result that correspond to the Error.
*
* When the error was caused by an exception thrown in resolver, original exception
* is available via `getPrevious()`.
*
* Also read related docs on [error handling](error-handling.md)
*
* Class extends standard PHP `\Exception`, so all standard methods of base `\Exception` class
* are available in addition to those listed below.
*/
class Error extends \Exception implements \JsonSerializable, ClientAware
{
const CATEGORY_GRAPHQL = 'graphql';
const CATEGORY_INTERNAL = 'internal';
/**
* A message describing the Error for debugging purposes.
*
* @var string
*/
public $message;
/**
* @var SourceLocation[]
*/
private $locations;
/**
* An array describing the JSON-path into the execution response which
* corresponds to this error. Only included for errors during execution.
*
* @var array
*/
public $path;
/**
* An array of GraphQL AST Nodes corresponding to this error.
*
* @var array
*/
public $nodes;
/**
* The source GraphQL document corresponding to this error.
*
* @var Source|null
*/
private $source;
/**
* @var array
*/
private $positions;
/**
* @var bool
*/
private $isClientSafe;
/**
* @var string
*/
protected $category;
/**
* Given an arbitrary Error, presumably thrown while attempting to execute a
* GraphQL operation, produce a new GraphQLError aware of the location in the
* document responsible for the original Error.
*
* @param $error
* @param array|null $nodes
* @param array|null $path
* @return Error
*/
public static function createLocatedError($error, $nodes = null, $path = null)
{
if ($error instanceof self) {
if ($error->path && $error->nodes) {
return $error;
} else {
$nodes = $nodes ?: $error->nodes;
$path = $path ?: $error->path;
}
}
$source = $positions = $originalError = null;
if ($error instanceof self) {
$message = $error->getMessage();
$originalError = $error;
$nodes = $error->nodes ?: $nodes;
$source = $error->source;
$positions = $error->positions;
} else if ($error instanceof \Exception || $error instanceof \Throwable) {
$message = $error->getMessage();
$originalError = $error;
} else {
$message = (string) $error;
}
return new static(
$message ?: 'An unknown error occurred.',
$nodes,
$source,
$positions,
$path,
$originalError
);
}
/**
* @param Error $error
* @return array
*/
public static function formatError(Error $error)
{
return $error->toSerializableArray();
}
/**
* @param string $message
* @param array|null $nodes
* @param Source $source
* @param array|null $positions
* @param array|null $path
* @param \Throwable $previous
*/
public function __construct(
$message,
$nodes = null,
Source $source = null,
$positions = null,
$path = null,
$previous = null
)
{
parent::__construct($message, 0, $previous);
if ($nodes instanceof \Traversable) {
$nodes = iterator_to_array($nodes);
}
$this->nodes = $nodes;
$this->source = $source;
$this->positions = $positions;
$this->path = $path;
if ($previous instanceof ClientAware) {
$this->isClientSafe = $previous->isClientSafe();
$this->category = $previous->getCategory() ?: static::CATEGORY_INTERNAL;
} else if ($previous) {
$this->isClientSafe = false;
$this->category = static::CATEGORY_INTERNAL;
} else {
$this->isClientSafe = true;
$this->category = static::CATEGORY_GRAPHQL;
}
}
/**
* @inheritdoc
*/
public function isClientSafe()
{
return $this->isClientSafe;
}
/**
* @inheritdoc
*/
public function getCategory()
{
return $this->category;
}
/**
* @return Source|null
*/
public function getSource()
{
if (null === $this->source) {
if (!empty($this->nodes[0]) && !empty($this->nodes[0]->loc)) {
$this->source = $this->nodes[0]->loc->source;
}
}
return $this->source;
}
/**
* @return array
*/
public function getPositions()
{
if (null === $this->positions) {
if (!empty($this->nodes)) {
$positions = array_map(function($node) {
return isset($node->loc) ? $node->loc->start : null;
}, $this->nodes);
$this->positions = array_filter($positions, function($p) {
return $p !== null;
});
}
}
return $this->positions;
}
/**
* 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()
{
if (null === $this->locations) {
$positions = $this->getPositions();
$source = $this->getSource();
if ($positions && $source) {
$this->locations = array_map(function ($pos) use ($source) {
return $source->getLocation($pos);
}, $positions);
} else {
$this->locations = [];
}
}
return $this->locations;
}
/**
* 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()
{
return $this->path;
}
/**
* Returns array representation of error suitable for serialization
*
* @deprecated Use FormattedError::createFromException() instead
* @return array
*/
public function toSerializableArray()
{
$arr = [
'message' => $this->getMessage()
];
$locations = Utils::map($this->getLocations(), function(SourceLocation $loc) {
return $loc->toSerializableArray();
});
if (!empty($locations)) {
$arr['locations'] = $locations;
}
if (!empty($this->path)) {
$arr['path'] = $this->path;
}
return $arr;
}
/**
* Specify data which should be serialized to JSON
* @link http://php.net/manual/en/jsonserializable.jsonserialize.php
* @return mixed data which can be serialized by <b>json_encode</b>,
* which is a value of any type other than a resource.
* @since 5.4.0
*/
function jsonSerialize()
{
return $this->toSerializableArray();
}
}
+278
View File
@@ -0,0 +1,278 @@
<?php
namespace GraphQL\Error;
use GraphQL\Language\SourceLocation;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\WrappingType;
use GraphQL\Utils\Utils;
/**
* This class is used for [default error formatting](error-handling.md).
* 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 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
* @return array
* @throws \Throwable
*/
public static function createFromException($e, $debug = false, $internalErrorMessage = null)
{
Utils::invariant(
$e instanceof \Exception || $e instanceof \Throwable,
"Expected exception, got %s",
Utils::getVariableType($e)
);
$internalErrorMessage = $internalErrorMessage ?: self::$internalErrorMessage;
if ($e instanceof ClientAware) {
$formattedError = [
'message' => $e->isClientSafe() ? $e->getMessage() : $internalErrorMessage,
'category' => $e->getCategory()
];
} else {
$formattedError = [
'message' => $internalErrorMessage,
'category' => Error::CATEGORY_INTERNAL
];
}
if ($e instanceof Error) {
$locations = Utils::map($e->getLocations(), function(SourceLocation $loc) {
return $loc->toSerializableArray();
});
if (!empty($locations)) {
$formattedError['locations'] = $locations;
}
if (!empty($e->path)) {
$formattedError['path'] = $e->path;
}
}
if ($debug) {
$formattedError = self::addDebugEntries($formattedError, $e, $debug);
}
return $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
* @return array
* @throws \Throwable
*/
public static function addDebugEntries(array $formattedError, $e, $debug)
{
if (!$debug) {
return $formattedError;
}
Utils::invariant(
$e instanceof \Exception || $e instanceof \Throwable,
"Expected exception, got %s",
Utils::getVariableType($e)
);
$debug = (int) $debug;
if ($debug & Debug::RETHROW_INTERNAL_EXCEPTIONS) {
if (!$e instanceof Error) {
throw $e;
} else if ($e->getPrevious()) {
throw $e->getPrevious();
}
}
$isInternal = !$e instanceof ClientAware || !$e->isClientSafe();
if (($debug & Debug::INCLUDE_DEBUG_MESSAGE) && $isInternal) {
// Displaying debugMessage as a first entry:
$formattedError = ['debugMessage' => $e->getMessage()] + $formattedError;
}
if ($debug & Debug::INCLUDE_TRACE) {
if ($e instanceof \ErrorException || $e instanceof \Error) {
$formattedError += [
'file' => $e->getFile(),
'line' => $e->getLine(),
];
}
$isTrivial = $e instanceof Error && !$e->getPrevious();
if (!$isTrivial) {
$debugging = $e->getPrevious() ?: $e;
$formattedError['trace'] = static::toSafeTrace($debugging);
}
}
return $formattedError;
}
/**
* Prepares final error formatter taking in account $debug flags.
* If initial formatter is not set, FormattedError::createFromException is used
*
* @param callable|null $formatter
* @param $debug
* @return callable|\Closure
*/
public static function prepareFormatter(callable $formatter = null, $debug)
{
$formatter = $formatter ?: function($e) {
return FormattedError::createFromException($e);
};
if ($debug) {
$formatter = function($e) use ($formatter, $debug) {
return FormattedError::addDebugEntries($formatter($e), $e, $debug);
};
}
return $formatter;
}
/**
* Returns error trace as serializable array
*
* @api
* @param \Throwable $error
* @return array
*/
public static function toSafeTrace($error)
{
$trace = $error->getTrace();
// Remove invariant entries as they don't provide much value:
if (
isset($trace[0]['function']) && isset($trace[0]['class']) &&
('GraphQL\Utils\Utils::invariant' === $trace[0]['class'].'::'.$trace[0]['function'])) {
array_shift($trace);
}
// Remove root call as it's likely error handler trace:
else if (!isset($trace[0]['file'])) {
array_shift($trace);
}
return array_map(function($err) {
$safeErr = array_intersect_key($err, ['file' => true, 'line' => true]);
if (isset($err['function'])) {
$func = $err['function'];
$args = !empty($err['args']) ? array_map([__CLASS__, 'printVar'], $err['args']) : [];
$funcStr = $func . '(' . implode(", ", $args) . ')';
if (isset($err['class'])) {
$safeErr['call'] = $err['class'] . '::' . $funcStr;
} else {
$safeErr['function'] = $funcStr;
}
}
return $safeErr;
}, $trace);
}
/**
* @param $var
* @return string
*/
public static function printVar($var)
{
if ($var instanceof Type) {
// FIXME: Replace with schema printer call
if ($var instanceof WrappingType) {
$var = $var->getWrappedType(true);
}
return 'GraphQLType: ' . $var->name;
}
if (is_object($var)) {
return 'instance of ' . get_class($var) . ($var instanceof \Countable ? '(' . count($var) . ')' : '');
}
if (is_array($var)) {
return 'array(' . count($var) . ')';
}
if ('' === $var) {
return '(empty string)';
}
if (is_string($var)) {
return "'" . addcslashes($var, "'") . "'";
}
if (is_bool($var)) {
return $var ? 'true' : 'false';
}
if (is_scalar($var)) {
return $var;
}
if (null === $var) {
return 'null';
}
return gettype($var);
}
/**
* @deprecated as of v0.8.0
* @param $error
* @param SourceLocation[] $locations
* @return array
*/
public static function create($error, array $locations = [])
{
$formatted = [
'message' => $error
];
if (!empty($locations)) {
$formatted['locations'] = array_map(function($loc) { return $loc->toArray();}, $locations);
}
return $formatted;
}
/**
* @param \ErrorException $e
* @deprecated as of v0.10.0, use general purpose method createFromException() instead
* @return array
*/
public static function createFromPHPError(\ErrorException $e)
{
return [
'message' => $e->getMessage(),
'severity' => $e->getSeverity(),
'trace' => self::toSafeTrace($e)
];
}
}
@@ -0,0 +1,15 @@
<?php
namespace GraphQL\Error;
/**
* Class InvariantVoilation
*
* Note:
* This exception should not inherit base Error exception as it is raised when there is an error somewhere in
* user-land code
*
* @package GraphQL\Error
*/
class InvariantViolation extends \LogicException
{
}
+71
View File
@@ -0,0 +1,71 @@
<?php
namespace GraphQL\Error;
use GraphQL\Language\Source;
use GraphQL\Language\SourceLocation;
class SyntaxError extends Error
{
/**
* @param Source $source
* @param int $position
* @param string $description
*/
public function __construct(Source $source, $position, $description)
{
$location = $source->getLocation($position);
$line = $location->line + $source->locationOffset->line - 1;
$columnOffset = self::getColumnOffset($source, $location);
$column = $location->column + $columnOffset;
$syntaxError =
"Syntax Error {$source->name} ({$line}:{$column}) $description\n" .
"\n".
self::highlightSourceAtLocation($source, $location);
parent::__construct($syntaxError, null, $source, [$position]);
}
/**
* @param Source $source
* @param SourceLocation $location
* @return string
*/
public static function highlightSourceAtLocation(Source $source, SourceLocation $location)
{
$line = $location->line;
$lineOffset = $source->locationOffset->line - 1;
$columnOffset = self::getColumnOffset($source, $location);
$contextLine = $line + $lineOffset;
$prevLineNum = (string) ($contextLine - 1);
$lineNum = (string) $contextLine;
$nextLineNum = (string) ($contextLine + 1);
$padLen = mb_strlen($nextLineNum, 'UTF-8');
$unicodeChars = json_decode('"\u2028\u2029"'); // Quick hack to get js-compatible representation of these chars
$lines = preg_split('/\r\n|[\n\r' . $unicodeChars . ']/su', $source->body);
$whitespace = function ($len) {
return str_repeat(' ', $len);
};
$lpad = function ($len, $str) {
return str_pad($str, $len - mb_strlen($str, 'UTF-8') + 1, ' ', STR_PAD_LEFT);
};
$lines[0] = $whitespace($source->locationOffset->column - 1) . $lines[0];
return
($line >= 2 ? $lpad($padLen, $prevLineNum) . ': ' . $lines[$line - 2] . "\n" : '') .
($lpad($padLen, $lineNum) . ': ' . $lines[$line - 1] . "\n") .
($whitespace(2 + $padLen + $location->column - 1 + $columnOffset) . "^\n") .
($line < count($lines) ? $lpad($padLen, $nextLineNum) . ': ' . $lines[$line] . "\n" : '');
}
public static function getColumnOffset(Source $source, SourceLocation $location)
{
return $location->line === 1 ? $source->locationOffset->column - 1 : 0;
}
}
+28
View File
@@ -0,0 +1,28 @@
<?php
namespace GraphQL\Error;
/**
* Class UserError
*
* Error caused by actions of GraphQL clients. Can be safely displayed to a client...
*
* @package GraphQL\Error
*/
class UserError extends \RuntimeException implements ClientAware
{
/**
* @return bool
*/
public function isClientSafe()
{
return true;
}
/**
* @return string
*/
public function getCategory()
{
return 'user';
}
}
+104
View File
@@ -0,0 +1,104 @@
<?php
namespace GraphQL\Error;
/**
* Encapsulates warnings produced by the library.
*
* Warnings can be suppressed (individually or all) if required.
* Also it is possible to override warning handler (which is **trigger_error()** by default)
*/
final class Warning
{
const WARNING_NAME = 1;
const WARNING_ASSIGN = 2;
const WARNING_CONFIG = 4;
const WARNING_FULL_SCHEMA_SCAN = 8;
const WARNING_CONFIG_DEPRECATION = 16;
const WARNING_NOT_A_TYPE = 32;
const ALL = 63;
static $enableWarnings = self::ALL;
static $warned = [];
static private $warningHandler;
/**
* Sets warning handler which can intercept all system warnings.
* When not set, trigger_error() is used to notify about warnings.
*
* @api
* @param callable|null $warningHandler
*/
public static function setWarningHandler(callable $warningHandler = null)
{
self::$warningHandler = $warningHandler;
}
/**
* Suppress warning by id (has no effect when custom warning handler is set)
*
* Usage example:
* Warning::suppress(Warning::WARNING_NOT_A_TYPE)
*
* When passing true - suppresses all warnings.
*
* @api
* @param bool|int $suppress
*/
static function suppress($suppress = true)
{
if (true === $suppress) {
self::$enableWarnings = 0;
} else if (false === $suppress) {
self::$enableWarnings = self::ALL;
} else {
$suppress = (int) $suppress;
self::$enableWarnings &= ~$suppress;
}
}
/**
* Re-enable previously suppressed warning by id
*
* Usage example:
* Warning::suppress(Warning::WARNING_NOT_A_TYPE)
*
* When passing true - re-enables all warnings.
*
* @api
* @param bool|int $enable
*/
public static function enable($enable = true)
{
if (true === $enable) {
self::$enableWarnings = self::ALL;
} else if (false === $enable) {
self::$enableWarnings = 0;
} else {
$enable = (int) $enable;
self::$enableWarnings |= $enable;
}
}
static function warnOnce($errorMessage, $warningId, $messageLevel = null)
{
if (self::$warningHandler) {
$fn = self::$warningHandler;
$fn($errorMessage, $warningId);
} else if ((self::$enableWarnings & $warningId) > 0 && !isset(self::$warned[$warningId])) {
self::$warned[$warningId] = true;
trigger_error($errorMessage, $messageLevel ?: E_USER_WARNING);
}
}
static function warn($errorMessage, $warningId, $messageLevel = null)
{
if (self::$warningHandler) {
$fn = self::$warningHandler;
$fn($errorMessage, $warningId);
} else if ((self::$enableWarnings & $warningId) > 0) {
trigger_error($errorMessage, $messageLevel ?: E_USER_WARNING);
}
}
}