vendor/webonyx/graphql-php/src/Type/Schema.php line 547

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. namespace GraphQL\Type;
  4. use Generator;
  5. use GraphQL\Error\Error;
  6. use GraphQL\Error\InvariantViolation;
  7. use GraphQL\GraphQL;
  8. use GraphQL\Language\AST\SchemaDefinitionNode;
  9. use GraphQL\Language\AST\SchemaTypeExtensionNode;
  10. use GraphQL\Type\Definition\AbstractType;
  11. use GraphQL\Type\Definition\Directive;
  12. use GraphQL\Type\Definition\ImplementingType;
  13. use GraphQL\Type\Definition\InterfaceType;
  14. use GraphQL\Type\Definition\ObjectType;
  15. use GraphQL\Type\Definition\Type;
  16. use GraphQL\Type\Definition\UnionType;
  17. use GraphQL\Utils\InterfaceImplementations;
  18. use GraphQL\Utils\TypeInfo;
  19. use GraphQL\Utils\Utils;
  20. use InvalidArgumentException;
  21. use Traversable;
  22. use function array_map;
  23. use function get_class;
  24. use function implode;
  25. use function is_array;
  26. use function is_callable;
  27. use function sprintf;
  28. /**
  29.  * Schema Definition (see [related docs](type-system/schema.md))
  30.  *
  31.  * A Schema is created by supplying the root types of each type of operation:
  32.  * query, mutation (optional) and subscription (optional). A schema definition is
  33.  * then supplied to the validator and executor. Usage Example:
  34.  *
  35.  *     $schema = new GraphQL\Type\Schema([
  36.  *       'query' => $MyAppQueryRootType,
  37.  *       'mutation' => $MyAppMutationRootType,
  38.  *     ]);
  39.  *
  40.  * Or using Schema Config instance:
  41.  *
  42.  *     $config = GraphQL\Type\SchemaConfig::create()
  43.  *         ->setQuery($MyAppQueryRootType)
  44.  *         ->setMutation($MyAppMutationRootType);
  45.  *
  46.  *     $schema = new GraphQL\Type\Schema($config);
  47.  */
  48. class Schema
  49. {
  50.     /** @var SchemaConfig */
  51.     private $config;
  52.     /**
  53.      * Contains currently resolved schema types
  54.      *
  55.      * @var Type[]
  56.      */
  57.     private $resolvedTypes = [];
  58.     /**
  59.      * Lazily initialised.
  60.      *
  61.      * @var array<string, InterfaceImplementations>
  62.      */
  63.     private $implementationsMap;
  64.     /**
  65.      * True when $resolvedTypes contain all possible schema types
  66.      *
  67.      * @var bool
  68.      */
  69.     private $fullyLoaded false;
  70.     /** @var Error[] */
  71.     private $validationErrors;
  72.     /** @var SchemaTypeExtensionNode[] */
  73.     public $extensionASTNodes = [];
  74.     /**
  75.      * @param mixed[]|SchemaConfig $config
  76.      *
  77.      * @api
  78.      */
  79.     public function __construct($config)
  80.     {
  81.         if (is_array($config)) {
  82.             $config SchemaConfig::create($config);
  83.         }
  84.         // If this schema was built from a source known to be valid, then it may be
  85.         // marked with assumeValid to avoid an additional type system validation.
  86.         if ($config->getAssumeValid()) {
  87.             $this->validationErrors = [];
  88.         } else {
  89.             // Otherwise check for common mistakes during construction to produce
  90.             // clear and early error messages.
  91.             Utils::invariant(
  92.                 $config instanceof SchemaConfig,
  93.                 'Schema constructor expects instance of GraphQL\Type\SchemaConfig or an array with keys: %s; but got: %s',
  94.                 implode(
  95.                     ', ',
  96.                     [
  97.                         'query',
  98.                         'mutation',
  99.                         'subscription',
  100.                         'types',
  101.                         'directives',
  102.                         'typeLoader',
  103.                     ]
  104.                 ),
  105.                 Utils::getVariableType($config)
  106.             );
  107.             Utils::invariant(
  108.                 ! $config->types || is_array($config->types) || is_callable($config->types),
  109.                 '"types" must be array or callable if provided but got: ' Utils::getVariableType($config->types)
  110.             );
  111.             Utils::invariant(
  112.                 $config->directives === null || is_array($config->directives),
  113.                 '"directives" must be Array if provided but got: ' Utils::getVariableType($config->directives)
  114.             );
  115.         }
  116.         $this->config            $config;
  117.         $this->extensionASTNodes $config->extensionASTNodes;
  118.         if ($config->query !== null) {
  119.             $this->resolvedTypes[$config->query->name] = $config->query;
  120.         }
  121.         if ($config->mutation !== null) {
  122.             $this->resolvedTypes[$config->mutation->name] = $config->mutation;
  123.         }
  124.         if ($config->subscription !== null) {
  125.             $this->resolvedTypes[$config->subscription->name] = $config->subscription;
  126.         }
  127.         if (is_array($this->config->types)) {
  128.             foreach ($this->resolveAdditionalTypes() as $type) {
  129.                 if (isset($this->resolvedTypes[$type->name])) {
  130.                     Utils::invariant(
  131.                         $type === $this->resolvedTypes[$type->name],
  132.                         sprintf(
  133.                             'Schema must contain unique named types but contains multiple types named "%s" (see http://webonyx.github.io/graphql-php/type-system/#type-registry).',
  134.                             $type
  135.                         )
  136.                     );
  137.                 }
  138.                 $this->resolvedTypes[$type->name] = $type;
  139.             }
  140.         }
  141.         $this->resolvedTypes += Type::getStandardTypes() + Introspection::getTypes();
  142.         if ($this->config->typeLoader) {
  143.             return;
  144.         }
  145.         // Perform full scan of the schema
  146.         $this->getTypeMap();
  147.     }
  148.     /**
  149.      * @return Generator
  150.      */
  151.     private function resolveAdditionalTypes()
  152.     {
  153.         $types $this->config->types ?? [];
  154.         if (is_callable($types)) {
  155.             $types $types();
  156.         }
  157.         if (! is_array($types) && ! $types instanceof Traversable) {
  158.             throw new InvariantViolation(sprintf(
  159.                 'Schema types callable must return array or instance of Traversable but got: %s',
  160.                 Utils::getVariableType($types)
  161.             ));
  162.         }
  163.         foreach ($types as $index => $type) {
  164.             $type self::resolveType($type);
  165.             if (! $type instanceof Type) {
  166.                 throw new InvariantViolation(sprintf(
  167.                     'Each entry of schema types must be instance of GraphQL\Type\Definition\Type but entry at %s is %s',
  168.                     $index,
  169.                     Utils::printSafe($type)
  170.                 ));
  171.             }
  172.             yield $type;
  173.         }
  174.     }
  175.     /**
  176.      * Returns array of all types in this schema. Keys of this array represent type names, values are instances
  177.      * of corresponding type definitions
  178.      *
  179.      * This operation requires full schema scan. Do not use in production environment.
  180.      *
  181.      * @return array<string, Type>
  182.      *
  183.      * @api
  184.      */
  185.     public function getTypeMap() : array
  186.     {
  187.         if (! $this->fullyLoaded) {
  188.             $this->resolvedTypes $this->collectAllTypes();
  189.             $this->fullyLoaded   true;
  190.         }
  191.         return $this->resolvedTypes;
  192.     }
  193.     /**
  194.      * @return Type[]
  195.      */
  196.     private function collectAllTypes()
  197.     {
  198.         $typeMap = [];
  199.         foreach ($this->resolvedTypes as $type) {
  200.             $typeMap TypeInfo::extractTypes($type$typeMap);
  201.         }
  202.         foreach ($this->getDirectives() as $directive) {
  203.             if (! ($directive instanceof Directive)) {
  204.                 continue;
  205.             }
  206.             $typeMap TypeInfo::extractTypesFromDirectives($directive$typeMap);
  207.         }
  208.         // When types are set as array they are resolved in constructor
  209.         if (is_callable($this->config->types)) {
  210.             foreach ($this->resolveAdditionalTypes() as $type) {
  211.                 $typeMap TypeInfo::extractTypes($type$typeMap);
  212.             }
  213.         }
  214.         return $typeMap;
  215.     }
  216.     /**
  217.      * Returns a list of directives supported by this schema
  218.      *
  219.      * @return Directive[]
  220.      *
  221.      * @api
  222.      */
  223.     public function getDirectives()
  224.     {
  225.         return $this->config->directives ?? GraphQL::getStandardDirectives();
  226.     }
  227.     /**
  228.      * @param string $operation
  229.      *
  230.      * @return ObjectType|null
  231.      */
  232.     public function getOperationType($operation)
  233.     {
  234.         switch ($operation) {
  235.             case 'query':
  236.                 return $this->getQueryType();
  237.             case 'mutation':
  238.                 return $this->getMutationType();
  239.             case 'subscription':
  240.                 return $this->getSubscriptionType();
  241.             default:
  242.                 return null;
  243.         }
  244.     }
  245.     /**
  246.      * Returns schema query type
  247.      *
  248.      * @return ObjectType
  249.      *
  250.      * @api
  251.      */
  252.     public function getQueryType() : ?Type
  253.     {
  254.         return $this->config->query;
  255.     }
  256.     /**
  257.      * Returns schema mutation type
  258.      *
  259.      * @return ObjectType|null
  260.      *
  261.      * @api
  262.      */
  263.     public function getMutationType() : ?Type
  264.     {
  265.         return $this->config->mutation;
  266.     }
  267.     /**
  268.      * Returns schema subscription
  269.      *
  270.      * @return ObjectType|null
  271.      *
  272.      * @api
  273.      */
  274.     public function getSubscriptionType() : ?Type
  275.     {
  276.         return $this->config->subscription;
  277.     }
  278.     /**
  279.      * @return SchemaConfig
  280.      *
  281.      * @api
  282.      */
  283.     public function getConfig()
  284.     {
  285.         return $this->config;
  286.     }
  287.     /**
  288.      * Returns type by its name
  289.      *
  290.      * @api
  291.      */
  292.     public function getType(string $name) : ?Type
  293.     {
  294.         if (! isset($this->resolvedTypes[$name])) {
  295.             $type $this->loadType($name);
  296.             if (! $type) {
  297.                 return null;
  298.             }
  299.             $this->resolvedTypes[$name] = self::resolveType($type);
  300.         }
  301.         return $this->resolvedTypes[$name];
  302.     }
  303.     public function hasType(string $name) : bool
  304.     {
  305.         return $this->getType($name) !== null;
  306.     }
  307.     private function loadType(string $typeName) : ?Type
  308.     {
  309.         $typeLoader $this->config->typeLoader;
  310.         if (! isset($typeLoader)) {
  311.             return $this->defaultTypeLoader($typeName);
  312.         }
  313.         $type $typeLoader($typeName);
  314.         if (! $type instanceof Type) {
  315.             // Unless you know what you're doing, kindly resist the temptation to refactor or simplify this block. The
  316.             // twisty logic here is tuned for performance, and meant to prioritize the "happy path" (the result returned
  317.             // from the type loader is already a Type), and only checks for callable if that fails. If the result is
  318.             // neither a Type nor a callable, then we throw an exception.
  319.             if (is_callable($type)) {
  320.                 $type $type();
  321.                 if (! $type instanceof Type) {
  322.                     $this->throwNotAType($type$typeName);
  323.                 }
  324.             } else {
  325.                 $this->throwNotAType($type$typeName);
  326.             }
  327.         }
  328.         if ($type->name !== $typeName) {
  329.             throw new InvariantViolation(
  330.                 sprintf('Type loader is expected to return type "%s", but it returned "%s"'$typeName$type->name)
  331.             );
  332.         }
  333.         return $type;
  334.     }
  335.     protected function throwNotAType($typestring $typeName)
  336.     {
  337.         throw new InvariantViolation(
  338.             sprintf(
  339.                 'Type loader is expected to return a callable or valid type "%s", but it returned %s',
  340.                 $typeName,
  341.                 Utils::printSafe($type)
  342.             )
  343.         );
  344.     }
  345.     private function defaultTypeLoader(string $typeName) : ?Type
  346.     {
  347.         // Default type loader simply falls back to collecting all types
  348.         $typeMap $this->getTypeMap();
  349.         return $typeMap[$typeName] ?? null;
  350.     }
  351.     /**
  352.      * @param Type|callable():Type $type
  353.      */
  354.     public static function resolveType($type) : Type
  355.     {
  356.         if ($type instanceof Type) {
  357.             return $type;
  358.         }
  359.         return $type();
  360.     }
  361.     /**
  362.      * Returns all possible concrete types for given abstract type
  363.      * (implementations for interfaces and members of union type for unions)
  364.      *
  365.      * This operation requires full schema scan. Do not use in production environment.
  366.      *
  367.      * @param InterfaceType|UnionType $abstractType
  368.      *
  369.      * @return array<Type&ObjectType>
  370.      *
  371.      * @api
  372.      */
  373.     public function getPossibleTypes(Type $abstractType) : array
  374.     {
  375.         return $abstractType instanceof UnionType
  376.             $abstractType->getTypes()
  377.             : $this->getImplementations($abstractType)->objects();
  378.     }
  379.     /**
  380.      * Returns all types that implement a given interface type.
  381.      *
  382.      * This operations requires full schema scan. Do not use in production environment.
  383.      *
  384.      * @api
  385.      */
  386.     public function getImplementations(InterfaceType $abstractType) : InterfaceImplementations
  387.     {
  388.         return $this->collectImplementations()[$abstractType->name];
  389.     }
  390.     /**
  391.      * @return array<string, InterfaceImplementations>
  392.      */
  393.     private function collectImplementations() : array
  394.     {
  395.         if (! isset($this->implementationsMap)) {
  396.             /** @var array<string, array<string, Type>> $foundImplementations */
  397.             $foundImplementations = [];
  398.             foreach ($this->getTypeMap() as $type) {
  399.                 if ($type instanceof InterfaceType) {
  400.                     if (! isset($foundImplementations[$type->name])) {
  401.                         $foundImplementations[$type->name] = ['objects' => [], 'interfaces' => []];
  402.                     }
  403.                     foreach ($type->getInterfaces() as $iface) {
  404.                         if (! isset($foundImplementations[$iface->name])) {
  405.                             $foundImplementations[$iface->name] = ['objects' => [], 'interfaces' => []];
  406.                         }
  407.                         $foundImplementations[$iface->name]['interfaces'][] = $type;
  408.                     }
  409.                 } elseif ($type instanceof ObjectType) {
  410.                     foreach ($type->getInterfaces() as $iface) {
  411.                         if (! isset($foundImplementations[$iface->name])) {
  412.                             $foundImplementations[$iface->name] = ['objects' => [], 'interfaces' => []];
  413.                         }
  414.                         $foundImplementations[$iface->name]['objects'][] = $type;
  415.                     }
  416.                 }
  417.             }
  418.             $this->implementationsMap array_map(
  419.                 static function (array $implementations) : InterfaceImplementations {
  420.                     return new InterfaceImplementations($implementations['objects'], $implementations['interfaces']);
  421.                 },
  422.                 $foundImplementations
  423.             );
  424.         }
  425.         return $this->implementationsMap;
  426.     }
  427.     /**
  428.      * @deprecated as of 14.4.0 use isSubType instead, will be removed in 15.0.0.
  429.      *
  430.      * Returns true if object type is concrete type of given abstract type
  431.      * (implementation for interfaces and members of union type for unions)
  432.      *
  433.      * @api
  434.      * @codeCoverageIgnore
  435.      */
  436.     public function isPossibleType(AbstractType $abstractTypeObjectType $possibleType) : bool
  437.     {
  438.         return $this->isSubType($abstractType$possibleType);
  439.     }
  440.     /**
  441.      * Returns true if the given type is a sub type of the given abstract type.
  442.      *
  443.      * @param UnionType|InterfaceType  $abstractType
  444.      * @param ObjectType|InterfaceType $maybeSubType
  445.      *
  446.      * @api
  447.      */
  448.     public function isSubType(AbstractType $abstractTypeImplementingType $maybeSubType) : bool
  449.     {
  450.         if ($abstractType instanceof InterfaceType) {
  451.             return $maybeSubType->implementsInterface($abstractType);
  452.         }
  453.         if ($abstractType instanceof UnionType) {
  454.             return $abstractType->isPossibleType($maybeSubType);
  455.         }
  456.         throw new InvalidArgumentException(sprintf('$abstractType must be of type UnionType|InterfaceType got: %s.'get_class($abstractType)));
  457.     }
  458.     /**
  459.      * Returns instance of directive by name
  460.      *
  461.      * @api
  462.      */
  463.     public function getDirective(string $name) : ?Directive
  464.     {
  465.         foreach ($this->getDirectives() as $directive) {
  466.             if ($directive->name === $name) {
  467.                 return $directive;
  468.             }
  469.         }
  470.         return null;
  471.     }
  472.     public function getAstNode() : ?SchemaDefinitionNode
  473.     {
  474.         return $this->config->getAstNode();
  475.     }
  476.     /**
  477.      * Validates schema.
  478.      *
  479.      * This operation requires full schema scan. Do not use in production environment.
  480.      *
  481.      * @throws InvariantViolation
  482.      *
  483.      * @api
  484.      */
  485.     public function assertValid()
  486.     {
  487.         $errors $this->validate();
  488.         if ($errors) {
  489.             throw new InvariantViolation(implode("\n\n"$this->validationErrors));
  490.         }
  491.         $internalTypes Type::getStandardTypes() + Introspection::getTypes();
  492.         foreach ($this->getTypeMap() as $name => $type) {
  493.             if (isset($internalTypes[$name])) {
  494.                 continue;
  495.             }
  496.             $type->assertValid();
  497.             // Make sure type loader returns the same instance as registered in other places of schema
  498.             if (! $this->config->typeLoader) {
  499.                 continue;
  500.             }
  501.             Utils::invariant(
  502.                 $this->loadType($name) === $type,
  503.                 sprintf(
  504.                     'Type loader returns different instance for %s than field/argument definitions. Make sure you always return the same instance for the same type name.',
  505.                     $name
  506.                 )
  507.             );
  508.         }
  509.     }
  510.     /**
  511.      * Validates schema.
  512.      *
  513.      * This operation requires full schema scan. Do not use in production environment.
  514.      *
  515.      * @return InvariantViolation[]|Error[]
  516.      *
  517.      * @api
  518.      */
  519.     public function validate()
  520.     {
  521.         // If this Schema has already been validated, return the previous results.
  522.         if ($this->validationErrors !== null) {
  523.             return $this->validationErrors;
  524.         }
  525.         // Validate the schema, producing a list of errors.
  526.         $context = new SchemaValidationContext($this);
  527.         $context->validateRootTypes();
  528.         $context->validateDirectives();
  529.         $context->validateTypes();
  530.         // Persist the results of validation before returning to ensure validation
  531.         // does not run multiple times for this schema.
  532.         $this->validationErrors $context->getErrors();
  533.         return $this->validationErrors;
  534.     }
  535. }