Version 4.7.6

This commit is contained in:
Neo committed 2025-12-19 16:05:19 -08:00
1 parent 8ba7d43898
commit 7b7404e741
1085 files changed
+30590 -7837

No files matched your search

+2
View File
@@ -10,6 +10,7 @@ use GraphQL\Language\Source;
use GraphQL\Language\SourceLocation;
use GraphQL\Utils\Utils;
use JsonSerializable;
use ReturnTypeWillChange;
use Throwable;
use Traversable;
use function array_filter;
@@ -367,6 +368,7 @@ class Error extends Exception implements JsonSerializable, ClientAware
* @return mixed data which can be serialized by <b>json_encode</b>,
* which is a value of any type other than a resource.
*/
#[ReturnTypeWillChange]
public function jsonSerialize()
{
return $this->toSerializableArray();
+5 -5
View File
@@ -39,7 +39,7 @@ final class Warning
*
* @api
*/
public static function setWarningHandler(?callable $warningHandler = null)
public static function setWarningHandler(?callable $warningHandler = null) : void
{
self::$warningHandler = $warningHandler;
}
@@ -56,7 +56,7 @@ final class Warning
*
* @api
*/
public static function suppress($suppress = true)
public static function suppress($suppress = true) : void
{
if ($suppress === true) {
self::$enableWarnings = 0;
@@ -81,7 +81,7 @@ final class Warning
*
* @api
*/
public static function enable($enable = true)
public static function enable($enable = true) : void
{
if ($enable === true) {
self::$enableWarnings = self::ALL;
@@ -94,7 +94,7 @@ final class Warning
}
}
public static function warnOnce(string $errorMessage, int $warningId, ?int $messageLevel = null)
public static function warnOnce(string $errorMessage, int $warningId, ?int $messageLevel = null) : void
{
$messageLevel = $messageLevel ?? E_USER_WARNING;
@@ -107,7 +107,7 @@ final class Warning
}
}
public static function warn(string $errorMessage, int $warningId, ?int $messageLevel = null)
public static function warn(string $errorMessage, int $warningId, ?int $messageLevel = null) : void
{
$messageLevel = $messageLevel ?? E_USER_WARNING;
+1 -1
View File
@@ -115,7 +115,7 @@ class ExecutionResult implements JsonSerializable
/**
* @return mixed[]
*/
public function jsonSerialize()
public function jsonSerialize() : array
{
return $this->toArray();
}
+48 -49
View File
@@ -55,18 +55,18 @@ use function sprintf;
class ReferenceExecutor implements ExecutorImplementation
{
/** @var object */
private static $UNDEFINED;
protected static $UNDEFINED;
/** @var ExecutionContext */
private $exeContext;
protected $exeContext;
/** @var SplObjectStorage */
private $subFieldCache;
protected $subFieldCache;
private function __construct(ExecutionContext $context)
protected function __construct(ExecutionContext $context)
{
if (! self::$UNDEFINED) {
self::$UNDEFINED = Utils::undefined();
if (! static::$UNDEFINED) {
static::$UNDEFINED = Utils::undefined();
}
$this->exeContext = $context;
$this->subFieldCache = new SplObjectStorage();
@@ -87,7 +87,7 @@ class ReferenceExecutor implements ExecutorImplementation
?string $operationName,
callable $fieldResolver
) : ExecutorImplementation {
$exeContext = self::buildExecutionContext(
$exeContext = static::buildExecutionContext(
$schema,
$documentNode,
$rootValue,
@@ -116,7 +116,7 @@ class ReferenceExecutor implements ExecutorImplementation
};
}
return new self($exeContext);
return new static($exeContext);
}
/**
@@ -129,7 +129,7 @@ class ReferenceExecutor implements ExecutorImplementation
*
* @return ExecutionContext|array<Error>
*/
private static function buildExecutionContext(
protected static function buildExecutionContext(
Schema $schema,
DocumentNode $documentNode,
$rootValue,
@@ -227,7 +227,7 @@ class ReferenceExecutor implements ExecutorImplementation
*
* @return ExecutionResult|Promise
*/
private function buildResponse($data)
protected function buildResponse($data)
{
if ($this->isPromise($data)) {
return $data->then(function ($resolved) {
@@ -248,7 +248,7 @@ class ReferenceExecutor implements ExecutorImplementation
*
* @return array<mixed>|Promise|stdClass|null
*/
private function executeOperation(OperationDefinitionNode $operation, $rootValue)
protected function executeOperation(OperationDefinitionNode $operation, $rootValue)
{
$type = $this->getOperationRootType($this->exeContext->schema, $operation);
$fields = $this->collectFields($type, $operation->selectionSet, new ArrayObject(), new ArrayObject());
@@ -290,7 +290,7 @@ class ReferenceExecutor implements ExecutorImplementation
*
* @throws Error
*/
private function getOperationRootType(Schema $schema, OperationDefinitionNode $operation) : ObjectType
protected function getOperationRootType(Schema $schema, OperationDefinitionNode $operation) : ObjectType
{
switch ($operation->operation) {
case 'query':
@@ -339,7 +339,7 @@ class ReferenceExecutor implements ExecutorImplementation
* returns an Interface or Union type, the "runtime type" will be the actual
* Object type returned by that field.
*/
private function collectFields(
protected function collectFields(
ObjectType $runtimeType,
SelectionSetNode $selectionSet,
ArrayObject $fields,
@@ -352,7 +352,7 @@ class ReferenceExecutor implements ExecutorImplementation
if (! $this->shouldIncludeNode($selection)) {
break;
}
$name = self::getFieldEntryKey($selection);
$name = static::getFieldEntryKey($selection);
if (! isset($fields[$name])) {
$fields[$name] = new ArrayObject();
}
@@ -402,7 +402,7 @@ class ReferenceExecutor implements ExecutorImplementation
*
* @param FragmentSpreadNode|FieldNode|InlineFragmentNode $node
*/
private function shouldIncludeNode(SelectionNode $node) : bool
protected function shouldIncludeNode(SelectionNode $node) : bool
{
$variableValues = $this->exeContext->variableValues;
$skipDirective = Directive::skipDirective();
@@ -427,7 +427,7 @@ class ReferenceExecutor implements ExecutorImplementation
/**
* Implements the logic to compute the key of a given fields entry
*/
private static function getFieldEntryKey(FieldNode $node) : string
protected static function getFieldEntryKey(FieldNode $node) : string
{
return $node->alias === null ? $node->name->value : $node->alias->value;
}
@@ -437,7 +437,7 @@ class ReferenceExecutor implements ExecutorImplementation
*
* @param FragmentDefinitionNode|InlineFragmentNode $fragment
*/
private function doesFragmentConditionMatch(Node $fragment, ObjectType $type) : bool
protected function doesFragmentConditionMatch(Node $fragment, ObjectType $type) : bool
{
$typeConditionNode = $fragment->typeCondition;
if ($typeConditionNode === null) {
@@ -463,7 +463,7 @@ class ReferenceExecutor implements ExecutorImplementation
*
* @return array<mixed>|Promise|stdClass
*/
private function executeFieldsSerially(ObjectType $parentType, $rootValue, array $path, ArrayObject $fields)
protected function executeFieldsSerially(ObjectType $parentType, $rootValue, array $path, ArrayObject $fields)
{
$result = $this->promiseReduce(
array_keys($fields->getArrayCopy()),
@@ -472,7 +472,7 @@ class ReferenceExecutor implements ExecutorImplementation
$fieldPath = $path;
$fieldPath[] = $responseName;
$result = $this->resolveField($parentType, $rootValue, $fieldNodes, $fieldPath);
if ($result === self::$UNDEFINED) {
if ($result === static::$UNDEFINED) {
return $results;
}
$promise = $this->getPromise($result);
@@ -492,11 +492,11 @@ class ReferenceExecutor implements ExecutorImplementation
if ($this->isPromise($result)) {
return $result->then(static function ($resolvedResults) {
return self::fixResultsIfEmptyArray($resolvedResults);
return static::fixResultsIfEmptyArray($resolvedResults);
});
}
return self::fixResultsIfEmptyArray($result);
return static::fixResultsIfEmptyArray($result);
}
/**
@@ -511,14 +511,14 @@ class ReferenceExecutor implements ExecutorImplementation
*
* @return array<mixed>|Throwable|mixed|null
*/
private function resolveField(ObjectType $parentType, $rootValue, ArrayObject $fieldNodes, array $path)
protected function resolveField(ObjectType $parentType, $rootValue, ArrayObject $fieldNodes, array $path)
{
$exeContext = $this->exeContext;
$fieldNode = $fieldNodes[0];
$fieldName = $fieldNode->name->value;
$fieldDef = $this->getFieldDef($exeContext->schema, $parentType, $fieldName);
if ($fieldDef === null) {
return self::$UNDEFINED;
return static::$UNDEFINED;
}
$returnType = $fieldDef->getType();
// The resolve function's optional 3rd argument is a context value that
@@ -574,7 +574,7 @@ class ReferenceExecutor implements ExecutorImplementation
* added to the query type, but that would require mutating type
* definitions, which would cause issues.
*/
private function getFieldDef(Schema $schema, ObjectType $parentType, string $fieldName) : ?FieldDefinition
protected function getFieldDef(Schema $schema, ObjectType $parentType, string $fieldName) : ?FieldDefinition
{
static $schemaMetaFieldDef, $typeMetaFieldDef, $typeNameMetaFieldDef;
$schemaMetaFieldDef = $schemaMetaFieldDef ?? Introspection::schemaMetaFieldDef();
@@ -591,9 +591,8 @@ class ReferenceExecutor implements ExecutorImplementation
if ($fieldName === $typeNameMetaFieldDef->name) {
return $typeNameMetaFieldDef;
}
$tmp = $parentType->getFields();
return $tmp[$fieldName] ?? null;
return $parentType->findField($fieldName);
}
/**
@@ -604,7 +603,7 @@ class ReferenceExecutor implements ExecutorImplementation
*
* @return Throwable|Promise|mixed
*/
private function resolveFieldValueOrError(
protected function resolveFieldValueOrError(
FieldDefinition $fieldDef,
FieldNode $fieldNode,
callable $resolveFn,
@@ -636,7 +635,7 @@ class ReferenceExecutor implements ExecutorImplementation
*
* @return array<mixed>|Promise|stdClass|null
*/
private function completeValueCatchingError(
protected function completeValueCatchingError(
Type $returnType,
ArrayObject $fieldNodes,
ResolveInfo $info,
@@ -676,7 +675,7 @@ class ReferenceExecutor implements ExecutorImplementation
*
* @throws Error
*/
private function handleFieldError($rawError, ArrayObject $fieldNodes, array $path, Type $returnType) : void
protected function handleFieldError($rawError, ArrayObject $fieldNodes, array $path, Type $returnType) : void
{
$error = Error::createLocatedError(
$rawError,
@@ -723,7 +722,7 @@ class ReferenceExecutor implements ExecutorImplementation
* @throws Error
* @throws Throwable
*/
private function completeValue(
protected function completeValue(
Type $returnType,
ArrayObject $fieldNodes,
ResolveInfo $info,
@@ -799,7 +798,7 @@ class ReferenceExecutor implements ExecutorImplementation
/**
* @param mixed $value
*/
private function isPromise($value) : bool
protected function isPromise($value) : bool
{
return $value instanceof Promise || $this->exeContext->promiseAdapter->isThenable($value);
}
@@ -810,7 +809,7 @@ class ReferenceExecutor implements ExecutorImplementation
*
* @param mixed $value
*/
private function getPromise($value) : ?Promise
protected function getPromise($value) : ?Promise
{
if ($value === null || $value instanceof Promise) {
return $value;
@@ -843,7 +842,7 @@ class ReferenceExecutor implements ExecutorImplementation
*
* @return Promise|mixed|null
*/
private function promiseReduce(array $values, callable $callback, $initialValue)
protected function promiseReduce(array $values, callable $callback, $initialValue)
{
return array_reduce(
$values,
@@ -871,7 +870,7 @@ class ReferenceExecutor implements ExecutorImplementation
*
* @throws Exception
*/
private function completeListValue(ListOfType $returnType, ArrayObject $fieldNodes, ResolveInfo $info, array $path, &$results)
protected function completeListValue(ListOfType $returnType, ArrayObject $fieldNodes, ResolveInfo $info, array $path, &$results)
{
$itemType = $returnType->getWrappedType();
Utils::invariant(
@@ -906,7 +905,7 @@ class ReferenceExecutor implements ExecutorImplementation
*
* @throws Exception
*/
private function completeLeafValue(LeafType $returnType, &$result)
protected function completeLeafValue(LeafType $returnType, &$result)
{
try {
return $returnType->serialize($result);
@@ -930,7 +929,7 @@ class ReferenceExecutor implements ExecutorImplementation
*
* @throws Error
*/
private function completeAbstractValue(
protected function completeAbstractValue(
AbstractType $returnType,
ArrayObject $fieldNodes,
ResolveInfo $info,
@@ -941,7 +940,7 @@ class ReferenceExecutor implements ExecutorImplementation
$typeCandidate = $returnType->resolveType($result, $exeContext->contextValue, $info);
if ($typeCandidate === null) {
$runtimeType = self::defaultTypeResolver($result, $exeContext->contextValue, $info, $returnType);
$runtimeType = static::defaultTypeResolver($result, $exeContext->contextValue, $info, $returnType);
} elseif (is_callable($typeCandidate)) {
$runtimeType = Schema::resolveType($typeCandidate);
} else {
@@ -1001,7 +1000,7 @@ class ReferenceExecutor implements ExecutorImplementation
*
* @return Promise|Type|string|null
*/
private function defaultTypeResolver($value, $contextValue, ResolveInfo $info, AbstractType $abstractType)
protected function defaultTypeResolver($value, $contextValue, ResolveInfo $info, AbstractType $abstractType)
{
// First, look for `__typename`.
if ($value !== null &&
@@ -1066,7 +1065,7 @@ class ReferenceExecutor implements ExecutorImplementation
*
* @throws Error
*/
private function completeObjectValue(
protected function completeObjectValue(
ObjectType $returnType,
ArrayObject $fieldNodes,
ResolveInfo $info,
@@ -1116,7 +1115,7 @@ class ReferenceExecutor implements ExecutorImplementation
*
* @return Error
*/
private function invalidReturnTypeError(
protected function invalidReturnTypeError(
ObjectType $returnType,
$result,
ArrayObject $fieldNodes
@@ -1135,7 +1134,7 @@ class ReferenceExecutor implements ExecutorImplementation
*
* @throws Error
*/
private function collectAndExecuteSubfields(
protected function collectAndExecuteSubfields(
ObjectType $returnType,
ArrayObject $fieldNodes,
array $path,
@@ -1151,7 +1150,7 @@ class ReferenceExecutor implements ExecutorImplementation
* type. Memoizing ensures the subfields are not repeatedly calculated, which
* saves overhead when resolving lists of values.
*/
private function collectSubFields(ObjectType $returnType, ArrayObject $fieldNodes) : ArrayObject
protected function collectSubFields(ObjectType $returnType, ArrayObject $fieldNodes) : ArrayObject
{
if (! isset($this->subFieldCache[$returnType])) {
$this->subFieldCache[$returnType] = new SplObjectStorage();
@@ -1186,7 +1185,7 @@ class ReferenceExecutor implements ExecutorImplementation
*
* @return Promise|stdClass|array<mixed>
*/
private function executeFields(ObjectType $parentType, $rootValue, array $path, ArrayObject $fields)
protected function executeFields(ObjectType $parentType, $rootValue, array $path, ArrayObject $fields)
{
$containsPromise = false;
$results = [];
@@ -1194,7 +1193,7 @@ class ReferenceExecutor implements ExecutorImplementation
$fieldPath = $path;
$fieldPath[] = $responseName;
$result = $this->resolveField($parentType, $rootValue, $fieldNodes, $fieldPath);
if ($result === self::$UNDEFINED) {
if ($result === static::$UNDEFINED) {
continue;
}
if (! $containsPromise && $this->isPromise($result)) {
@@ -1204,7 +1203,7 @@ class ReferenceExecutor implements ExecutorImplementation
}
// If there are no promises, we can just return the object
if (! $containsPromise) {
return self::fixResultsIfEmptyArray($results);
return static::fixResultsIfEmptyArray($results);
}
// Otherwise, results is a map from field name to the result of resolving that
@@ -1222,7 +1221,7 @@ class ReferenceExecutor implements ExecutorImplementation
*
* @return array<mixed>|stdClass|mixed
*/
private static function fixResultsIfEmptyArray($results)
protected static function fixResultsIfEmptyArray($results)
{
if ($results === []) {
return new stdClass();
@@ -1237,7 +1236,7 @@ class ReferenceExecutor implements ExecutorImplementation
*
* @param array<string, Promise|mixed> $assoc
*/
private function promiseForAssocArray(array $assoc) : Promise
protected function promiseForAssocArray(array $assoc) : Promise
{
$keys = array_keys($assoc);
$valuesAndPromises = array_values($assoc);
@@ -1249,7 +1248,7 @@ class ReferenceExecutor implements ExecutorImplementation
$resolvedResults[$keys[$i]] = $value;
}
return self::fixResultsIfEmptyArray($resolvedResults);
return static::fixResultsIfEmptyArray($resolvedResults);
});
}
@@ -1258,7 +1257,7 @@ class ReferenceExecutor implements ExecutorImplementation
* @param InterfaceType|UnionType $returnType
* @param mixed $result
*/
private function ensureValidRuntimeType(
protected function ensureValidRuntimeType(
$runtimeTypeOrName,
AbstractType $returnType,
ResolveInfo $info,
@@ -9,6 +9,7 @@ use Countable;
use GraphQL\Utils\AST;
use InvalidArgumentException;
use IteratorAggregate;
use ReturnTypeWillChange;
use Traversable;
use function array_merge;
use function array_splice;
@@ -69,6 +70,7 @@ class NodeList implements ArrayAccess, IteratorAggregate, Countable
*
* @phpstan-return T
*/
#[ReturnTypeWillChange]
public function offsetGet($offset)// : Node
{
$item = $this->nodes[$offset];
@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace GraphQL\Language;
use JsonSerializable;
use ReturnTypeWillChange;
class SourceLocation implements JsonSerializable
{
@@ -46,6 +47,7 @@ class SourceLocation implements JsonSerializable
/**
* @return int[]
*/
#[ReturnTypeWillChange]
public function jsonSerialize()
{
return $this->toSerializableArray();
+21 -3
View File
@@ -559,10 +559,12 @@ class Helper
);
}
} else {
parse_str((string) $request->getBody(), $bodyParams);
if ($request instanceof ServerRequestInterface) {
$bodyParams = $request->getParsedBody();
}
if (! is_array($bodyParams)) {
throw new RequestError('Unexpected content type: ' . Utils::printSafeJson($contentType[0]));
if (! isset($bodyParams)) {
$bodyParams = $this->decodeContent((string) $request->getBody(), $contentType[0]);
}
}
}
@@ -576,6 +578,22 @@ class Helper
);
}
/**
* @return array<string, mixed>
*
* @throws RequestError
*/
protected function decodeContent(string $rawBody, string $contentType) : array
{
parse_str($rawBody, $bodyParams);
if (! is_array($bodyParams)) {
throw new RequestError('Unexpected content type: ' . Utils::printSafeJson($contentType));
}
return $bodyParams;
}
/**
* Converts query execution result to PSR-7 response
*
@@ -12,6 +12,7 @@ use GraphQL\Type\Schema;
use GraphQL\Utils\Utils;
use function is_array;
use function is_callable;
use function is_iterable;
use function is_string;
use function sprintf;
@@ -95,9 +96,9 @@ class FieldDefinition
if (is_callable($fields)) {
$fields = $fields();
}
if (! is_array($fields)) {
if (! is_iterable($fields)) {
throw new InvariantViolation(
sprintf('%s fields must be an array or a callable which returns such an array.', $type->name)
sprintf('%s fields must be an iterable or a callable which returns such an iterable.', $type->name)
);
}
$map = [];
@@ -123,6 +124,17 @@ class FieldDefinition
$fieldDef = self::create($field);
} elseif ($field instanceof self) {
$fieldDef = $field;
} elseif (is_callable($field)) {
if (! is_string($name)) {
throw new InvariantViolation(
sprintf(
'%s lazy fields must be an associative array with field names as keys.',
$type->name
)
);
}
$fieldDef = new UnresolvedFieldDefinition($type, $name, $field);
} else {
if (! is_string($name) || ! $field) {
throw new InvariantViolation(
@@ -137,7 +149,8 @@ class FieldDefinition
$fieldDef = self::create(['name' => $name, 'type' => $field]);
}
$map[$fieldDef->name] = $fieldDef;
$map[$fieldDef->getName()] = $fieldDef;
}
return $map;
@@ -180,6 +193,11 @@ class FieldDefinition
return null;
}
public function getName() : string
{
return $this->name;
}
public function getType() : Type
{
if (! isset($this->type)) {
@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace GraphQL\Type\Definition;
use GraphQL\Error\InvariantViolation;
interface HasFieldsType
{
/**
* @throws InvariantViolation
*/
public function getField(string $name) : FieldDefinition;
public function hasField(string $name) : bool;
public function findField(string $name) : ?FieldDefinition;
/**
* @return array<string, FieldDefinition>
*
* @throws InvariantViolation
*/
public function getFields() : array;
/**
* @return array<int, string>
*
* @throws InvariantViolation
*/
public function getFieldNames() : array;
}
@@ -11,6 +11,7 @@ use GraphQL\Utils\Utils;
use function count;
use function is_array;
use function is_callable;
use function is_iterable;
use function is_string;
use function sprintf;
@@ -80,14 +81,14 @@ class InputObjectType extends Type implements InputType, NullableType, NamedType
$fields = $fields();
}
if (! is_array($fields)) {
if (! is_iterable($fields)) {
throw new InvariantViolation(
sprintf('%s fields must be an array or a callable which returns such an array.', $this->name)
sprintf('%s fields must be an iterable or a callable which returns such an iterable.', $this->name)
);
}
foreach ($fields as $name => $field) {
if ($field instanceof Type) {
if ($field instanceof Type || is_callable($field)) {
$field = ['type' => $field];
}
$field = new InputObjectField($field + ['name' => $name]);
+2 -2
View File
@@ -24,8 +24,8 @@ class IntType extends ScalarType
//
// n.b. JavaScript's integers are safe between -(2^53 - 1) and 2^53 - 1 because
// they are internally represented as IEEE 754 doubles.
private const MAX_INT = 2147483647;
private const MIN_INT = -2147483648;
public const MAX_INT = 2147483647;
public const MIN_INT = -2147483648;
/** @var string */
public $name = Type::INT;
@@ -15,7 +15,7 @@ use function is_callable;
use function is_string;
use function sprintf;
class InterfaceType extends Type implements AbstractType, OutputType, CompositeType, NullableType, NamedType, ImplementingType
class InterfaceType extends TypeWithFields implements AbstractType, OutputType, CompositeType, NullableType, NamedType, ImplementingType
{
/** @var InterfaceTypeDefinitionNode|null */
public $astNode;
@@ -23,13 +23,6 @@ class InterfaceType extends Type implements AbstractType, OutputType, CompositeT
/** @var array<int, InterfaceTypeExtensionNode> */
public $extensionASTNodes;
/**
* Lazily initialized.
*
* @var array<string, FieldDefinition>
*/
private $fields;
/**
* Lazily initialized.
*
@@ -79,43 +72,6 @@ class InterfaceType extends Type implements AbstractType, OutputType, CompositeT
return $type;
}
public function getField(string $name) : FieldDefinition
{
if (! isset($this->fields)) {
$this->initializeFields();
}
Utils::invariant(isset($this->fields[$name]), 'Field "%s" is not defined for type "%s"', $name, $this->name);
return $this->fields[$name];
}
public function hasField(string $name) : bool
{
if (! isset($this->fields)) {
$this->initializeFields();
}
return isset($this->fields[$name]);
}
/**
* @return FieldDefinition[]
*/
public function getFields() : array
{
if (! isset($this->fields)) {
$this->initializeFields();
}
return $this->fields;
}
protected function initializeFields() : void
{
$fields = $this->config['fields'] ?? [];
$this->fields = FieldDefinition::defineFieldMap($this, $fields);
}
public function implementsInterface(InterfaceType $interfaceType) : bool
{
if (! isset($this->interfaceMap)) {
+1 -50
View File
@@ -55,7 +55,7 @@ use function sprintf;
* }
* ]);
*/
class ObjectType extends Type implements OutputType, CompositeType, NullableType, NamedType, ImplementingType
class ObjectType extends TypeWithFields implements OutputType, CompositeType, NullableType, NamedType, ImplementingType
{
/** @var ObjectTypeDefinitionNode|null */
public $astNode;
@@ -66,13 +66,6 @@ class ObjectType extends Type implements OutputType, CompositeType, NullableType
/** @var ?callable */
public $resolveFieldFn;
/**
* Lazily initialized.
*
* @var FieldDefinition[]
*/
private $fields;
/**
* Lazily initialized.
*
@@ -123,48 +116,6 @@ class ObjectType extends Type implements OutputType, CompositeType, NullableType
return $type;
}
/**
* @throws InvariantViolation
*/
public function getField(string $name) : FieldDefinition
{
if (! isset($this->fields)) {
$this->initializeFields();
}
Utils::invariant(isset($this->fields[$name]), 'Field "%s" is not defined for type "%s"', $name, $this->name);
return $this->fields[$name];
}
public function hasField(string $name) : bool
{
if (! isset($this->fields)) {
$this->initializeFields();
}
return isset($this->fields[$name]);
}
/**
* @return FieldDefinition[]
*
* @throws InvariantViolation
*/
public function getFields() : array
{
if (! isset($this->fields)) {
$this->initializeFields();
}
return $this->fields;
}
protected function initializeFields() : void
{
$fields = $this->config['fields'] ?? [];
$this->fields = FieldDefinition::defineFieldMap($this, $fields);
}
public function implementsInterface(InterfaceType $interfaceType) : bool
{
if (! isset($this->interfaceMap)) {
+7 -1
View File
@@ -11,6 +11,7 @@ use GraphQL\Language\AST\FragmentDefinitionNode;
use GraphQL\Language\AST\FragmentSpreadNode;
use GraphQL\Language\AST\InlineFragmentNode;
use GraphQL\Language\AST\SelectionSetNode;
use GraphQL\Type\Introspection;
use GraphQL\Type\Schema;
use function array_diff_key;
use function array_filter;
@@ -167,7 +168,12 @@ class QueryPlan
$implementors = [];
foreach ($selectionSet->selections as $selectionNode) {
if ($selectionNode instanceof FieldNode) {
$fieldName = $selectionNode->name->value;
$fieldName = $selectionNode->name->value;
if ($fieldName === Introspection::TYPE_NAME_FIELD_NAME) {
continue;
}
$type = $parentType->getField($fieldName);
$selectionType = $type->getType();
+2
View File
@@ -11,6 +11,7 @@ use GraphQL\Type\Introspection;
use GraphQL\Utils\Utils;
use JsonSerializable;
use ReflectionClass;
use ReturnTypeWillChange;
use function array_keys;
use function array_merge;
use function assert;
@@ -308,6 +309,7 @@ abstract class Type implements JsonSerializable
/**
* @return string
*/
#[ReturnTypeWillChange]
public function jsonSerialize()
{
return $this->toString();
@@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
namespace GraphQL\Type\Definition;
use GraphQL\Utils\Utils;
use function array_keys;
abstract class TypeWithFields extends Type implements HasFieldsType
{
/**
* Lazily initialized.
*
* @var array<string, FieldDefinition>
*/
private $fields;
private function initializeFields() : void
{
if (isset($this->fields)) {
return;
}
$fields = $this->config['fields'] ?? [];
$this->fields = FieldDefinition::defineFieldMap($this, $fields);
}
public function getField(string $name) : FieldDefinition
{
Utils::invariant($this->hasField($name), 'Field "%s" is not defined for type "%s"', $name, $this->name);
return $this->findField($name);
}
public function findField(string $name) : ?FieldDefinition
{
$this->initializeFields();
if (! isset($this->fields[$name])) {
return null;
}
if ($this->fields[$name] instanceof UnresolvedFieldDefinition) {
$this->fields[$name] = $this->fields[$name]->resolve();
}
return $this->fields[$name];
}
public function hasField(string $name) : bool
{
$this->initializeFields();
return isset($this->fields[$name]);
}
/** @inheritDoc */
public function getFields() : array
{
$this->initializeFields();
foreach ($this->fields as $name => $field) {
if (! ($field instanceof UnresolvedFieldDefinition)) {
continue;
}
$this->fields[$name] = $field->resolve();
}
return $this->fields;
}
/** @inheritDoc */
public function getFieldNames() : array
{
$this->initializeFields();
return array_keys($this->fields);
}
}
@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
namespace GraphQL\Type\Definition;
use GraphQL\Error\InvariantViolation;
use function is_array;
use function sprintf;
class UnresolvedFieldDefinition
{
/** @var Type $type */
private $type;
/** @var string $name */
private $name;
/** @var callable(): (FieldDefinition|array<string, mixed>|Type) $resolver */
private $resolver;
/**
* @param callable(): (FieldDefinition|array<string, mixed>|Type) $resolver
*/
public function __construct(Type $type, string $name, callable $resolver)
{
$this->type = $type;
$this->name = $name;
$this->resolver = $resolver;
}
public function getName() : string
{
return $this->name;
}
public function resolve() : FieldDefinition
{
$field = ($this->resolver)();
if ($field instanceof FieldDefinition) {
if ($field->name !== $this->name) {
throw new InvariantViolation(
sprintf('%s.%s should not dynamically change its name when resolved lazily.', $this->type->name, $this->name)
);
}
return $field;
}
if (! is_array($field)) {
return FieldDefinition::create(['name' => $this->name, 'type' => $field]);
}
if (! isset($field['name'])) {
$field['name'] = $this->name;
} elseif ($field['name'] !== $this->name) {
throw new InvariantViolation(
sprintf('%s.%s should not dynamically change its name when resolved lazily.', $this->type->name, $this->name)
);
}
if (isset($field['args']) && ! is_array($field['args'])) {
throw new InvariantViolation(
sprintf('%s.%s args must be an array.', $this->type->name, $this->name)
);
}
return FieldDefinition::create($field);
}
}
+6 -6
View File
@@ -293,7 +293,7 @@ EOD;
'Object and Interface types provide the fields they describe. Abstract ' .
'types, Union and Interface, provide the Object types possible ' .
'at runtime. List and NonNull types compose other types.',
'fields' => static function () {
'fields' => static function () : array {
return [
'kind' => [
'type' => Type::nonNull(self::_typeKind()),
@@ -337,7 +337,7 @@ EOD;
'args' => [
'includeDeprecated' => ['type' => Type::boolean(), 'defaultValue' => false],
],
'resolve' => static function (Type $type, $args) {
'resolve' => static function (Type $type, $args) : ?array {
if ($type instanceof ObjectType || $type instanceof InterfaceType) {
$fields = $type->getFields();
@@ -381,7 +381,7 @@ EOD;
'args' => [
'includeDeprecated' => ['type' => Type::boolean(), 'defaultValue' => false],
],
'resolve' => static function ($type, $args) {
'resolve' => static function ($type, $args) : ?array {
if ($type instanceof EnumType) {
$values = array_values($type->getValues());
@@ -484,7 +484,7 @@ EOD;
'description' =>
'Object and Interface types are described by a list of Fields, each of ' .
'which has a name, potentially a list of arguments, and a return type.',
'fields' => static function () {
'fields' => static function () : array {
return [
'name' => [
'type' => Type::nonNull(Type::string()),
@@ -540,7 +540,7 @@ EOD;
'Arguments provided to Fields or Directives and the input fields of an ' .
'InputObject are represented as Input Values which describe their type ' .
'and optionally a default value.',
'fields' => static function () {
'fields' => static function () : array {
return [
'name' => [
'type' => Type::nonNull(Type::string()),
@@ -661,7 +661,7 @@ EOD;
],
'args' => [
'type' => Type::nonNull(Type::listOf(Type::nonNull(self::_inputValue()))),
'resolve' => static function (Directive $directive) {
'resolve' => static function (Directive $directive) : array {
return $directive->args ?? [];
},
],
+9 -25
View File
@@ -20,9 +20,10 @@ use GraphQL\Type\Definition\UnionType;
use GraphQL\Utils\InterfaceImplementations;
use GraphQL\Utils\TypeInfo;
use GraphQL\Utils\Utils;
use InvalidArgumentException;
use Traversable;
use function array_map;
use function array_values;
use function get_class;
use function implode;
use function is_array;
use function is_callable;
@@ -60,13 +61,6 @@ class Schema
*/
private $resolvedTypes = [];
/**
* Lazily initialized.
*
* @var array<string, array<string, ObjectType|UnionType>>
*/
private $subTypeMap;
/**
* Lazily initialised.
*
@@ -507,25 +501,15 @@ class Schema
*/
public function isSubType(AbstractType $abstractType, ImplementingType $maybeSubType) : bool
{
if (! isset($this->subTypeMap[$abstractType->name])) {
$this->subTypeMap[$abstractType->name] = [];
if ($abstractType instanceof UnionType) {
foreach ($abstractType->getTypes() as $type) {
$this->subTypeMap[$abstractType->name][$type->name] = true;
}
} else {
$implementations = $this->getImplementations($abstractType);
foreach ($implementations->objects() as $type) {
$this->subTypeMap[$abstractType->name][$type->name] = true;
}
foreach ($implementations->interfaces() as $type) {
$this->subTypeMap[$abstractType->name][$type->name] = true;
}
}
if ($abstractType instanceof InterfaceType) {
return $maybeSubType->implementsInterface($abstractType);
}
return isset($this->subTypeMap[$abstractType->name][$maybeSubType->name]);
if ($abstractType instanceof UnionType) {
return $abstractType->isPossibleType($maybeSubType);
}
throw new InvalidArgumentException(sprintf('$abstractType must be of type UnionType|InterfaceType got: %s.', get_class($abstractType)));
}
/**
@@ -372,8 +372,9 @@ class SchemaValidationContext
*/
private function validateDirectivesAtLocation($directives, string $location)
{
$directivesNamed = [];
$schema = $this->schema;
/** @var array<string, array<int, DirectiveNode>> $potentiallyDuplicateDirectives */
$potentiallyDuplicateDirectives = [];
$schema = $this->schema;
foreach ($directives as $directive) {
$directiveName = $directive->name->value;
@@ -386,6 +387,7 @@ class SchemaValidationContext
);
continue;
}
$includes = Utils::some(
$schemaDirective->locations,
static function ($schemaLocation) use ($location) : bool {
@@ -402,17 +404,22 @@ class SchemaValidationContext
);
}
$existingNodes = $directivesNamed[$directiveName] ?? [];
$existingNodes[] = $directive;
$directivesNamed[$directiveName] = $existingNodes;
if ($schemaDirective->isRepeatable) {
continue;
}
$existingNodes = $potentiallyDuplicateDirectives[$directiveName] ?? [];
$existingNodes[] = $directive;
$potentiallyDuplicateDirectives[$directiveName] = $existingNodes;
}
foreach ($directivesNamed as $directiveName => $directiveList) {
foreach ($potentiallyDuplicateDirectives as $directiveName => $directiveList) {
if (count($directiveList) <= 1) {
continue;
}
$this->reportError(
sprintf('Directive @%s used twice at the same location.', $directiveName),
sprintf('Non-repeatable directive @%s used more than once at the same location.', $directiveName),
$directiveList
);
}
@@ -426,7 +433,7 @@ class SchemaValidationContext
$fieldMap = $type->getFields();
// Objects and Interfaces both must define one or more fields.
if (! $fieldMap) {
if ($fieldMap === []) {
$this->reportError(
sprintf('Type %s must define one or more fields.', $type->name),
$this->getAllNodes($type)
+31
View File
@@ -584,6 +584,8 @@ class AST
}
/**
* @deprecated use getOperationAST instead.
*
* Returns operation type ("query", "mutation" or "subscription") given a document and operation name
*
* @param string $operationName
@@ -608,4 +610,33 @@ class AST
return false;
}
/**
* Returns the operation within a document by name.
*
* If a name is not provided, an operation is only returned if the document has exactly one.
*
* @api
*/
public static function getOperationAST(DocumentNode $document, ?string $operationName = null) : ?OperationDefinitionNode
{
$operation = null;
foreach ($document->definitions->getIterator() as $node) {
if (! $node instanceof OperationDefinitionNode) {
continue;
}
if ($operationName === null) {
// We found a second operation, so we bail instead of returning an ambiguous result.
if ($operation !== null) {
return null;
}
$operation = $node;
} elseif ($node->name instanceof NameNode && $node->name->value === $operationName) {
return $node;
}
}
return $operation;
}
}
+1 -1
View File
@@ -489,7 +489,7 @@ class BuildClientSchema
'name' => $directive['name'],
'description' => $directive['description'],
'args' => $this->buildInputValueDefMap($directive['args']),
'isRepeatable' => $directive['isRepeatable'],
'isRepeatable' => $directive['isRepeatable'] ?? false,
'locations' => $directive['locations'],
]);
}
+5 -7
View File
@@ -7,6 +7,7 @@ namespace GraphQL\Utils;
use ArrayAccess;
use GraphQL\Type\Definition\EnumValueDefinition;
use InvalidArgumentException;
use ReturnTypeWillChange;
use SplObjectStorage;
use function array_key_exists;
use function array_search;
@@ -90,7 +91,7 @@ class MixedStore implements ArrayAccess
* <p>
* The return value will be casted to boolean if non-boolean was returned.
*/
public function offsetExists($offset)
public function offsetExists($offset) : bool
{
if ($offset === false) {
return $this->falseValueIsSet;
@@ -135,6 +136,7 @@ class MixedStore implements ArrayAccess
*
* @return mixed Can return all value types.
*/
#[ReturnTypeWillChange]
public function offsetGet($offset)
{
if ($offset === true) {
@@ -181,10 +183,8 @@ class MixedStore implements ArrayAccess
* @param mixed $value <p>
* The value to set.
* </p>
*
* @return void
*/
public function offsetSet($offset, $value)
public function offsetSet($offset, $value) : void
{
if ($offset === false) {
$this->falseValue = $value;
@@ -217,10 +217,8 @@ class MixedStore implements ArrayAccess
* @param mixed $offset <p>
* The offset to unset.
* </p>
*
* @return void
*/
public function offsetUnset($offset)
public function offsetUnset($offset) : void
{
if ($offset === true) {
$this->trueValue = null;
+9 -3
View File
@@ -494,14 +494,19 @@ class SchemaExtender
'locations' => $directive->locations,
'args' => static::extendArgs($directive->args),
'astNode' => $directive->astNode,
'isRepeatable' => $directive->isRepeatable,
]);
}
/**
* @param array<string, bool> $options
*/
public static function extend(Schema $schema, DocumentNode $documentAST, array $options = []) : Schema
{
public static function extend(
Schema $schema,
DocumentNode $documentAST,
array $options = [],
?callable $typeConfigDecorator = null
) : Schema {
if (! (isset($options['assumeValid']) || isset($options['assumeValidSDL']))) {
DocumentValidator::assertValidSDLExtension($documentAST, $schema);
}
@@ -579,7 +584,8 @@ class SchemaExtender
}
throw new Error('Unknown type: "' . $typeName . '". Ensure that this type exists either in the original schema, or is added in a type definition.', [$typeName]);
}
},
$typeConfigDecorator
);
static::$extendTypeCache = [];
+10 -8
View File
@@ -25,6 +25,7 @@ use GraphQL\Type\Definition\Directive;
use GraphQL\Type\Definition\EnumType;
use GraphQL\Type\Definition\FieldArgument;
use GraphQL\Type\Definition\FieldDefinition;
use GraphQL\Type\Definition\HasFieldsType;
use GraphQL\Type\Definition\ImplementingType;
use GraphQL\Type\Definition\InputObjectType;
use GraphQL\Type\Definition\InputType;
@@ -196,9 +197,10 @@ class TypeInfo
if ($type instanceof ImplementingType) {
$nestedTypes = array_merge($nestedTypes, $type->getInterfaces());
}
if ($type instanceof ObjectType || $type instanceof InterfaceType) {
foreach ($type->getFields() as $fieldName => $field) {
if (count($field->args ?? []) > 0) {
if ($type instanceof HasFieldsType) {
foreach ($type->getFields() as $field) {
if (count($field->args) > 0) {
$fieldArgTypes = array_map(
static function (FieldArgument $arg) : Type {
return $arg->getType();
@@ -212,7 +214,7 @@ class TypeInfo
}
}
if ($type instanceof InputObjectType) {
foreach ($type->getFields() as $fieldName => $field) {
foreach ($type->getFields() as $field) {
$nestedTypes[] = $field->getType();
}
}
@@ -414,11 +416,11 @@ class TypeInfo
if ($name === $typeNameMeta->name && $parentType instanceof CompositeType) {
return $typeNameMeta;
}
if ($parentType instanceof ObjectType ||
$parentType instanceof InterfaceType) {
$fields = $parentType->getFields();
return $fields[$name] ?? null;
if ($parentType instanceof ObjectType ||
$parentType instanceof InterfaceType
) {
return $parentType->findField($name);
}
return null;
@@ -85,15 +85,13 @@ class FieldsOnCorrectType extends ValidationRule
$interfaceUsageCount = [];
foreach ($schema->getPossibleTypes($type) as $possibleType) {
$fields = $possibleType->getFields();
if (! isset($fields[$fieldName])) {
if (! $possibleType->hasField($fieldName)) {
continue;
}
// This object type defines this field.
$suggestedObjectTypes[] = $possibleType->name;
foreach ($possibleType->getInterfaces() as $possibleInterface) {
$fields = $possibleInterface->getFields();
if (! isset($fields[$fieldName])) {
if (! $possibleInterface->hasField($fieldName)) {
continue;
}
// This interface type defines this field.
@@ -128,7 +126,7 @@ class FieldsOnCorrectType extends ValidationRule
private function getSuggestedFieldNames(Schema $schema, $type, $fieldName)
{
if ($type instanceof ObjectType || $type instanceof InterfaceType) {
$possibleFieldNames = array_keys($type->getFields());
$possibleFieldNames = $type->getFieldNames();
return Utils::suggestionList($fieldName, $possibleFieldNames);
}
@@ -250,10 +250,10 @@ class OverlappingFieldsCanBeMerged extends ValidationRule
$fieldName = $selection->name->value;
$fieldDef = null;
if ($parentType instanceof ObjectType ||
$parentType instanceof InterfaceType) {
$tmp = $parentType->getFields();
if (isset($tmp[$fieldName])) {
$fieldDef = $tmp[$fieldName];
$parentType instanceof InterfaceType
) {
if ($parentType->hasField($fieldName)) {
$fieldDef = $parentType->getField($fieldName);
}
}
$responseName = $selection->alias ? $selection->alias->value : $fieldName;
@@ -10,13 +10,14 @@ use GraphQL\Language\AST\FragmentDefinitionNode;
use GraphQL\Language\AST\FragmentSpreadNode;
use GraphQL\Language\AST\InlineFragmentNode;
use GraphQL\Language\AST\SelectionSetNode;
use GraphQL\Type\Definition\HasFieldsType;
use GraphQL\Type\Definition\InputObjectType;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Introspection;
use GraphQL\Utils\TypeInfo;
use GraphQL\Validator\ValidationContext;
use InvalidArgumentException;
use function class_alias;
use function method_exists;
use function sprintf;
abstract class QuerySecurityRule extends ValidationRule
@@ -117,8 +118,7 @@ abstract class QuerySecurityRule extends ValidationRule
case $selection instanceof FieldNode:
$fieldName = $selection->name->value;
$fieldDef = null;
if ($parentType && method_exists($parentType, 'getFields')) {
$tmp = $parentType->getFields();
if ($parentType instanceof HasFieldsType || $parentType instanceof InputObjectType) {
$schemaMetaFieldDef = Introspection::schemaMetaFieldDef();
$typeMetaFieldDef = Introspection::typeMetaFieldDef();
$typeNameMetaFieldDef = Introspection::typeNameMetaFieldDef();
@@ -129,8 +129,8 @@ abstract class QuerySecurityRule extends ValidationRule
$fieldDef = $typeMetaFieldDef;
} elseif ($fieldName === $typeNameMetaFieldDef->name) {
$fieldDef = $typeNameMetaFieldDef;
} elseif (isset($tmp[$fieldName])) {
$fieldDef = $tmp[$fieldName];
} elseif ($parentType->hasField($fieldName)) {
$fieldDef = $parentType->getField($fieldName);
}
}
$responseName = $this->getFieldName($selection);
@@ -34,6 +34,7 @@ class UniqueDirectivesPerLocation extends ValidationRule
public function getASTVisitor(ASTValidationContext $context)
{
/** @var array<string, true> $uniqueDirectiveMap */
$uniqueDirectiveMap = [];
$schema = $context->getSchema();
@@ -41,16 +42,22 @@ class UniqueDirectivesPerLocation extends ValidationRule
? $schema->getDirectives()
: Directive::getInternalDirectives();
foreach ($definedDirectives as $directive) {
$uniqueDirectiveMap[$directive->name] = ! $directive->isRepeatable;
if ($directive->isRepeatable) {
continue;
}
$uniqueDirectiveMap[$directive->name] = true;
}
$astDefinitions = $context->getDocument()->definitions;
foreach ($astDefinitions as $definition) {
if (! ($definition instanceof DirectiveDefinitionNode)) {
if (! ($definition instanceof DirectiveDefinitionNode)
|| $definition->repeatable
) {
continue;
}
$uniqueDirectiveMap[$definition->name->value] = $definition->repeatable;
$uniqueDirectiveMap[$definition->name->value] = true;
}
return [
@@ -81,7 +81,7 @@ class ValuesOfCorrectType extends ValidationRule
return null;
},
NodeKind::OBJECT => function (ObjectValueNode $node) use ($context, &$fieldName) {
NodeKind::OBJECT => function (ObjectValueNode $node) use ($context, &$fieldName) : ?VisitorOperation {
// Note: TypeInfo will traverse into a list's item type, so look to the
// parent input type to check if it is a list.
$type = Type::getNamedType($context->getInputType());
@@ -116,6 +116,8 @@ class ValuesOfCorrectType extends ValidationRule
)
);
}
return null;
},
NodeKind::OBJECT_FIELD => static function (ObjectFieldNode $node) use ($context) : void {
$parentType = Type::getNamedType($context->getParentInputType());
@@ -31,6 +31,7 @@ use GraphQL\Utils\TypeInfo;
use SplObjectStorage;
use function array_merge;
use function array_pop;
use function assert;
use function count;
/**
@@ -180,16 +181,15 @@ class ValidationContext extends ASTValidationContext
while (count($setsToVisit) > 0) {
$set = array_pop($setsToVisit);
for ($i = 0, $selectionCount = count($set->selections); $i < $selectionCount; $i++) {
$selection = $set->selections[$i];
foreach ($set->selections as $selection) {
if ($selection instanceof FragmentSpreadNode) {
$spreads[] = $selection;
} elseif ($selection instanceof FieldNode || $selection instanceof InlineFragmentNode) {
if ($selection->selectionSet) {
$setsToVisit[] = $selection->selectionSet;
}
} else {
throw InvariantViolation::shouldNotHappen();
assert($selection instanceof FieldNode || $selection instanceof InlineFragmentNode);
$selectionSet = $selection->selectionSet;
if ($selectionSet !== null) {
$setsToVisit[] = $selectionSet;
}
}
}
}