graphql-php/src/Validator/ValidationContext.php

302 lines
7.9 KiB
PHP
Raw Normal View History

2015-07-15 20:05:46 +03:00
<?php
namespace GraphQL\Validator;
use GraphQL\Language\AST\FragmentSpreadNode;
use GraphQL\Language\AST\HasSelectionSet;
use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\AST\OperationDefinitionNode;
use GraphQL\Language\AST\VariableNode;
use GraphQL\Language\Visitor;
use \SplObjectStorage;
use GraphQL\Error\Error;
use GraphQL\Type\Schema;
use GraphQL\Language\AST\DocumentNode;
use GraphQL\Language\AST\FragmentDefinitionNode;
2015-07-15 20:05:46 +03:00
use GraphQL\Type\Definition\CompositeType;
use GraphQL\Type\Definition\FieldDefinition;
use GraphQL\Type\Definition\InputType;
use GraphQL\Type\Definition\Type;
use GraphQL\Utils\TypeInfo;
/**
* An instance of this class is passed as the "this" context to all validators,
* allowing access to commonly useful contextual information from within a
* validation rule.
*/
class ValidationContext
{
/**
* @var Schema
*/
private $schema;
2015-07-15 20:05:46 +03:00
/**
* @var DocumentNode
2015-07-15 20:05:46 +03:00
*/
private $ast;
2015-07-15 20:05:46 +03:00
/**
* @var TypeInfo
*/
private $typeInfo;
2015-07-15 20:05:46 +03:00
/**
* @var Error[]
*/
private $errors;
2015-07-15 20:05:46 +03:00
/**
* @var FragmentDefinitionNode[]
2015-07-15 20:05:46 +03:00
*/
private $fragments;
2015-07-15 20:05:46 +03:00
/**
* @var SplObjectStorage
*/
private $fragmentSpreads;
/**
* @var SplObjectStorage
*/
private $recursivelyReferencedFragments;
/**
* @var SplObjectStorage
*/
private $variableUsages;
/**
* @var SplObjectStorage
*/
private $recursiveVariableUsages;
/**
* ValidationContext constructor.
*
* @param Schema $schema
* @param DocumentNode $ast
* @param TypeInfo $typeInfo
*/
function __construct(Schema $schema, DocumentNode $ast, TypeInfo $typeInfo)
2015-07-15 20:05:46 +03:00
{
$this->schema = $schema;
$this->ast = $ast;
$this->typeInfo = $typeInfo;
$this->errors = [];
$this->fragmentSpreads = new SplObjectStorage();
$this->recursivelyReferencedFragments = new SplObjectStorage();
$this->variableUsages = new SplObjectStorage();
$this->recursiveVariableUsages = new SplObjectStorage();
}
/**
* @param Error $error
*/
function reportError(Error $error)
{
$this->errors[] = $error;
}
/**
* @return Error[]
*/
function getErrors()
{
return $this->errors;
2015-07-15 20:05:46 +03:00
}
/**
* @return Schema
*/
function getSchema()
{
return $this->schema;
2015-07-15 20:05:46 +03:00
}
/**
* @return DocumentNode
2015-07-15 20:05:46 +03:00
*/
function getDocument()
{
return $this->ast;
2015-07-15 20:05:46 +03:00
}
/**
Validation: improving overlapping fields quality This improves the overlapping fields validation performance and improves error reporting quality by separating the concepts of checking fields "within" a single collection of fields from checking fields "between" two different collections of fields. This ensures for deeply overlapping fields that nested fields are not checked against each other repeatedly. Extending this concept further, fragment spreads are no longer expanded inline before looking for conflicts, instead the fields within a fragment are compared to the fields with the selection set which contained the referencing fragment spread. e.g. ```graphql { same: a same: b ...X } fragment X on T { same: c same: d } ``` In the above example, the initial query body is checked "within" so `a` is compared to `b`. Also, the fragment `X` is checked "within" so `c` is compared to `d`. Because of the fragment spread, the query body and fragment `X` are checked "between" so that `a` and `b` are each compared to `c` and `d`. In this trivial example, no fewer checks are performed, but in the case where fragments are referenced multiple times, this reduces the overall number of checks (regardless of memoization). **BREAKING**: This can change the order of fields reported when a conflict arises when fragment spreads are involved. If you are checking the precise output of errors (e.g. for unit tests), you may find existing errors change from `"a" and "c" are different fields` to `"c" and "a" are different fields`. From a perf point of view, this is fairly minor as the memoization "PairSet" was already keeping these repeated checks from consuming time, however this will reduce the number of memoized hits because of the algorithm improvement. From an error reporting point of view, this reports nearest-common-ancestor issues when found in a fragment that comes later in the validation process. I've added a test which fails with the existing impl and now passes, as well as changed a comment. This also fixes an error where validation issues could be missed because of an over-eager memoization. I've also modified the `PairSet` to be aware of both forms of memoization, also represented by a previously failing test. ref: graphql/graphql-js#386
2018-02-11 19:45:35 +03:00
* @param string $name
* @return FragmentDefinitionNode|null
2015-07-15 20:05:46 +03:00
*/
function getFragment($name)
{
$fragments = $this->fragments;
2015-07-15 20:05:46 +03:00
if (!$fragments) {
$fragments = [];
foreach ($this->getDocument()->definitions as $statement) {
if ($statement->kind === NodeKind::FRAGMENT_DEFINITION) {
$fragments[$statement->name->value] = $statement;
}
}
$this->fragments = $fragments;
2015-07-15 20:05:46 +03:00
}
return isset($fragments[$name]) ? $fragments[$name] : null;
}
/**
* @param HasSelectionSet $node
* @return FragmentSpreadNode[]
*/
function getFragmentSpreads(HasSelectionSet $node)
{
$spreads = isset($this->fragmentSpreads[$node]) ? $this->fragmentSpreads[$node] : null;
if (!$spreads) {
$spreads = [];
$setsToVisit = [$node->selectionSet];
while (!empty($setsToVisit)) {
$set = array_pop($setsToVisit);
for ($i = 0; $i < count($set->selections); $i++) {
$selection = $set->selections[$i];
if ($selection->kind === NodeKind::FRAGMENT_SPREAD) {
$spreads[] = $selection;
} else if ($selection->selectionSet) {
$setsToVisit[] = $selection->selectionSet;
}
}
}
$this->fragmentSpreads[$node] = $spreads;
}
return $spreads;
}
/**
* @param OperationDefinitionNode $operation
* @return FragmentDefinitionNode[]
*/
function getRecursivelyReferencedFragments(OperationDefinitionNode $operation)
{
$fragments = isset($this->recursivelyReferencedFragments[$operation]) ? $this->recursivelyReferencedFragments[$operation] : null;
if (!$fragments) {
$fragments = [];
$collectedNames = [];
$nodesToVisit = [$operation];
while (!empty($nodesToVisit)) {
$node = array_pop($nodesToVisit);
$spreads = $this->getFragmentSpreads($node);
for ($i = 0; $i < count($spreads); $i++) {
$fragName = $spreads[$i]->name->value;
if (empty($collectedNames[$fragName])) {
$collectedNames[$fragName] = true;
$fragment = $this->getFragment($fragName);
if ($fragment) {
$fragments[] = $fragment;
$nodesToVisit[] = $fragment;
}
}
}
}
$this->recursivelyReferencedFragments[$operation] = $fragments;
}
return $fragments;
}
/**
* @param HasSelectionSet $node
* @return array List of ['node' => VariableNode, 'type' => ?InputObjectType]
*/
function getVariableUsages(HasSelectionSet $node)
{
$usages = isset($this->variableUsages[$node]) ? $this->variableUsages[$node] : null;
if (!$usages) {
$newUsages = [];
$typeInfo = new TypeInfo($this->schema);
Visitor::visit($node, Visitor::visitWithTypeInfo($typeInfo, [
NodeKind::VARIABLE_DEFINITION => function () {
return false;
},
NodeKind::VARIABLE => function (VariableNode $variable) use (&$newUsages, $typeInfo) {
$newUsages[] = ['node' => $variable, 'type' => $typeInfo->getInputType()];
}
]));
$usages = $newUsages;
$this->variableUsages[$node] = $usages;
}
return $usages;
}
/**
* @param OperationDefinitionNode $operation
* @return array List of ['node' => VariableNode, 'type' => ?InputObjectType]
*/
function getRecursiveVariableUsages(OperationDefinitionNode $operation)
{
$usages = isset($this->recursiveVariableUsages[$operation]) ? $this->recursiveVariableUsages[$operation] : null;
if (!$usages) {
$usages = $this->getVariableUsages($operation);
$fragments = $this->getRecursivelyReferencedFragments($operation);
$tmp = [$usages];
for ($i = 0; $i < count($fragments); $i++) {
$tmp[] = $this->getVariableUsages($fragments[$i]);
}
$usages = call_user_func_array('array_merge', $tmp);
$this->recursiveVariableUsages[$operation] = $usages;
}
return $usages;
}
2015-07-15 20:05:46 +03:00
/**
* Returns OutputType
*
* @return Type
*/
function getType()
{
return $this->typeInfo->getType();
2015-07-15 20:05:46 +03:00
}
/**
* @return CompositeType
*/
function getParentType()
{
return $this->typeInfo->getParentType();
2015-07-15 20:05:46 +03:00
}
/**
* @return InputType
*/
function getInputType()
{
return $this->typeInfo->getInputType();
2015-07-15 20:05:46 +03:00
}
/**
* @return InputType
*/
function getParentInputType()
{
return $this->typeInfo->getParentInputType();
}
2015-07-15 20:05:46 +03:00
/**
* @return FieldDefinition
*/
function getFieldDef()
{
return $this->typeInfo->getFieldDef();
2015-07-15 20:05:46 +03:00
}
function getDirective()
{
return $this->typeInfo->getDirective();
}
function getArgument()
{
return $this->typeInfo->getArgument();
}
2015-07-15 20:05:46 +03:00
}