/** * Creates a new visitor instance which maintains a provided TypeInfo instance * along with visiting visitor. */ static function visitWithTypeInfo(TypeInfo $typeInfo, $visitor) { return ['enter' => function ($node) use($typeInfo, $visitor) { $typeInfo->enter($node); $fn = self::getVisitFn($visitor, $node->kind, false); if ($fn) { $result = call_user_func_array($fn, func_get_args()); if ($result) { $typeInfo->leave($node); if ($result instanceof Node) { $typeInfo->enter($result); } } return $result; } return null; }, 'leave' => function ($node) use($typeInfo, $visitor) { $fn = self::getVisitFn($visitor, $node->kind, true); $result = $fn ? call_user_func_array($fn, func_get_args()) : null; $typeInfo->leave($node); return $result; }]; }
/** * This uses a specialized visitor which runs multiple visitors in parallel, * while maintaining the visitor skip and break API. * * @param Schema $schema * @param Document $documentAST * @param array $rules * @return array */ public static function visitUsingRules(Schema $schema, Document $documentAST, array $rules) { $typeInfo = new TypeInfo($schema); $context = new ValidationContext($schema, $documentAST, $typeInfo); $errors = []; // TODO: convert to class $visitInstances = function ($ast, $instances) use($typeInfo, $context, &$errors, &$visitInstances) { $skipUntil = new \SplFixedArray(count($instances)); $skipCount = 0; Visitor::visit($ast, ['enter' => function ($node, $key) use($typeInfo, $instances, $skipUntil, &$skipCount, &$errors, $context, $visitInstances) { $typeInfo->enter($node); for ($i = 0; $i < count($instances); $i++) { // Do not visit this instance if it returned false for a previous node if ($skipUntil[$i]) { continue; } $result = null; // Do not visit top level fragment definitions if this instance will // visit those fragments inline because it // provided `visitSpreadFragments`. if ($node->kind === Node::FRAGMENT_DEFINITION && $key !== null && !empty($instances[$i]['visitSpreadFragments'])) { $result = Visitor::skipNode(); } else { $enter = Visitor::getVisitFn($instances[$i], false, $node->kind); if ($enter instanceof \Closure) { // $enter = $enter->bindTo($instances[$i]); $result = call_user_func_array($enter, func_get_args()); } else { $result = null; } } if ($result instanceof VisitorOperation) { if ($result->doContinue) { $skipUntil[$i] = $node; $skipCount++; // If all instances are being skipped over, skip deeper traversal if ($skipCount === count($instances)) { for ($k = 0; $k < count($instances); $k++) { if ($skipUntil[$k] === $node) { $skipUntil[$k] = null; $skipCount--; } } return Visitor::skipNode(); } } else { if ($result->doBreak) { $instances[$i] = null; } } } else { if ($result && self::isError($result)) { self::append($errors, $result); for ($j = $i - 1; $j >= 0; $j--) { $leaveFn = Visitor::getVisitFn($instances[$j], true, $node->kind); if ($leaveFn) { // $leaveFn = $leaveFn->bindTo($instances[$j]) $result = call_user_func_array($leaveFn, func_get_args()); if ($result instanceof VisitorOperation) { if ($result->doBreak) { $instances[$j] = null; } } else { if (self::isError($result)) { self::append($errors, $result); } else { if ($result !== null) { throw new \Exception("Config cannot edit document."); } } } } } $typeInfo->leave($node); return Visitor::skipNode(); } else { if ($result !== null) { throw new \Exception("Config cannot edit document."); } } } } // If any validation instances provide the flag `visitSpreadFragments` // and this node is a fragment spread, validate the fragment from // this point. if ($node instanceof FragmentSpread) { $fragment = $context->getFragment($node->name->value); if ($fragment) { $fragVisitingInstances = []; foreach ($instances as $idx => $inst) { if (!empty($inst['visitSpreadFragments']) && !$skipUntil[$idx]) { $fragVisitingInstances[] = $inst; } } if (!empty($fragVisitingInstances)) { $visitInstances($fragment, $fragVisitingInstances); } } } }, 'leave' => function ($node) use($instances, $typeInfo, $skipUntil, &$skipCount, &$errors) { for ($i = count($instances) - 1; $i >= 0; $i--) { if ($skipUntil[$i]) { if ($skipUntil[$i] === $node) { $skipUntil[$i] = null; $skipCount--; } continue; } $leaveFn = Visitor::getVisitFn($instances[$i], true, $node->kind); if ($leaveFn) { // $leaveFn = $leaveFn.bindTo($instances[$i]); $result = call_user_func_array($leaveFn, func_get_args()); if ($result instanceof VisitorOperation) { if ($result->doBreak) { $instances[$i] = null; } } else { if (self::isError($result)) { self::append($errors, $result); } else { if ($result !== null) { throw new \Exception("Config cannot edit document."); } } } } } $typeInfo->leave($node); }]); }; // Visit the whole document with instances of all provided rules. $allRuleInstances = []; foreach ($rules as $rule) { $allRuleInstances[] = $rule($context); } $visitInstances($documentAST, $allRuleInstances); return $errors; }