<?php
declare(strict_types=1);
namespace GraphQL\Type\Definition;
use GraphQL\Error\Error;
use GraphQL\Error\InvariantViolation;
use GraphQL\Error\Warning;
use GraphQL\Language\AST\FieldDefinitionNode;
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;
/**
* @todo Move complexity-related code to it's own place
*/
#[\AllowDynamicProperties]
class FieldDefinition
{
public const DEFAULT_COMPLEXITY_FN = 'GraphQL\Type\Definition\FieldDefinition::defaultComplexity';
/** @var string */
public $name;
/** @var FieldArgument[] */
public $args;
/**
* Callback for resolving field value given parent value.
* Mutually exclusive with `map`
*
* @var callable|null
*/
public $resolveFn;
/**
* Callback for mapping list of parent values to list of field values.
* Mutually exclusive with `resolve`
*
* @var callable|null
*/
public $mapFn;
/** @var string|null */
public $description;
/** @var string|null */
public $deprecationReason;
/** @var FieldDefinitionNode|null */
public $astNode;
/**
* Original field definition config
*
* @var mixed[]
*/
public $config;
/** @var OutputType&Type */
private $type;
/** @var callable|string */
private $complexityFn;
/**
* @param mixed[] $config
*/
protected function __construct(array $config)
{
$this->name = $config['name'];
$this->resolveFn = $config['resolve'] ?? null;
$this->mapFn = $config['map'] ?? null;
$this->args = isset($config['args']) ? FieldArgument::createMap($config['args']) : [];
$this->description = $config['description'] ?? null;
$this->deprecationReason = $config['deprecationReason'] ?? null;
$this->astNode = $config['astNode'] ?? null;
$this->config = $config;
$this->complexityFn = $config['complexity'] ?? self::DEFAULT_COMPLEXITY_FN;
}
/**
* @param (callable():mixed[])|mixed[] $fields
*
* @return array<string, self>
*/
public static function defineFieldMap(Type $type, $fields) : array
{
if (is_callable($fields)) {
$fields = $fields();
}
if (! is_iterable($fields)) {
throw new InvariantViolation(
sprintf('%s fields must be an iterable or a callable which returns such an iterable.', $type->name)
);
}
$map = [];
foreach ($fields as $name => $field) {
if (is_array($field)) {
if (! isset($field['name'])) {
if (! is_string($name)) {
throw new InvariantViolation(
sprintf(
'%s fields must be an associative array with field names as keys or a function which returns such an array.',
$type->name
)
);
}
$field['name'] = $name;
}
if (isset($field['args']) && ! is_array($field['args'])) {
throw new InvariantViolation(
sprintf('%s.%s args must be an array.', $type->name, $name)
);
}
$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(
sprintf(
'%s.%s field config must be an array, but got: %s',
$type->name,
$name,
Utils::printSafe($field)
)
);
}
$fieldDef = self::create(['name' => $name, 'type' => $field]);
}
$map[$fieldDef->getName()] = $fieldDef;
}
return $map;
}
/**
* @param mixed[] $field
*
* @return FieldDefinition
*/
public static function create($field)
{
return new self($field);
}
/**
* @param int $childrenComplexity
*
* @return mixed
*/
public static function defaultComplexity($childrenComplexity)
{
return $childrenComplexity + 1;
}
/**
* @param string $name
*
* @return FieldArgument|null
*/
public function getArg($name)
{
foreach ($this->args ?? [] as $arg) {
/** @var FieldArgument $arg */
if ($arg->name === $name) {
return $arg;
}
}
return null;
}
public function getName() : string
{
return $this->name;
}
public function getType() : Type
{
if (! isset($this->type)) {
/**
* TODO: replace this phpstan cast with native assert
*
* @var Type&OutputType
*/
$type = Schema::resolveType($this->config['type']);
$this->type = $type;
}
return $this->type;
}
public function __isset(string $name) : bool
{
switch ($name) {
case 'type':
Warning::warnOnce(
"The public getter for 'type' on FieldDefinition has been deprecated and will be removed" .
" in the next major version. Please update your code to use the 'getType' method.",
Warning::WARNING_CONFIG_DEPRECATION
);
return isset($this->type);
}
return isset($this->$name);
}
public function __get(string $name)
{
switch ($name) {
case 'type':
Warning::warnOnce(
"The public getter for 'type' on FieldDefinition has been deprecated and will be removed" .
" in the next major version. Please update your code to use the 'getType' method.",
Warning::WARNING_CONFIG_DEPRECATION
);
return $this->getType();
default:
return $this->$name;
}
return null;
}
public function __set(string $name, $value)
{
switch ($name) {
case 'type':
Warning::warnOnce(
"The public setter for 'type' on FieldDefinition has been deprecated and will be removed" .
' in the next major version.',
Warning::WARNING_CONFIG_DEPRECATION
);
$this->type = $value;
break;
default:
$this->$name = $value;
break;
}
}
/**
* @return bool
*/
public function isDeprecated()
{
return (bool) $this->deprecationReason;
}
/**
* @return callable|callable
*/
public function getComplexityFn()
{
return $this->complexityFn;
}
/**
* @throws InvariantViolation
*/
public function assertValid(Type $parentType)
{
try {
Utils::assertValidName($this->name);
} catch (Error $e) {
throw new InvariantViolation(sprintf('%s.%s: %s', $parentType->name, $this->name, $e->getMessage()));
}
Utils::invariant(
! isset($this->config['isDeprecated']),
sprintf(
'%s.%s should provide "deprecationReason" instead of "isDeprecated".',
$parentType->name,
$this->name
)
);
$type = $this->getType();
if ($type instanceof WrappingType) {
$type = $type->getWrappedType(true);
}
Utils::invariant(
$type instanceof OutputType,
sprintf(
'%s.%s field type must be Output Type but got: %s',
$parentType->name,
$this->name,
Utils::printSafe($this->type)
)
);
Utils::invariant(
$this->resolveFn === null || is_callable($this->resolveFn),
sprintf(
'%s.%s field resolver must be a function if provided, but got: %s',
$parentType->name,
$this->name,
Utils::printSafe($this->resolveFn)
)
);
foreach ($this->args as $fieldArgument) {
$fieldArgument->assertValid($this, $type);
}
}
}