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

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($type, string $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 $abstractType, ObjectType $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 $abstractType, ImplementingType $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. }