diff --git a/src/Language/AST/FragmentDefinitionNode.php b/src/Language/AST/FragmentDefinitionNode.php index 04193b1..4543b03 100644 --- a/src/Language/AST/FragmentDefinitionNode.php +++ b/src/Language/AST/FragmentDefinitionNode.php @@ -10,13 +10,21 @@ class FragmentDefinitionNode extends Node implements ExecutableDefinitionNode, H */ public $name; + /** + * Note: fragment variable definitions are experimental and may be changed + * or removed in the future. + * + * @var VariableDefinitionNode[]|NodeList + */ + public $variableDefinitions; + /** * @var NamedTypeNode */ public $typeCondition; /** - * @var DirectiveNode[] + * @var DirectiveNode[]|NodeList */ public $directives; diff --git a/src/Language/Parser.php b/src/Language/Parser.php index d40414a..08481b6 100644 --- a/src/Language/Parser.php +++ b/src/Language/Parser.php @@ -49,7 +49,6 @@ use GraphQL\Language\AST\UnionTypeExtensionNode; use GraphQL\Language\AST\VariableNode; use GraphQL\Language\AST\VariableDefinitionNode; use GraphQL\Error\SyntaxError; -use GraphQL\Type\TypeKind; /** * Parses string containing GraphQL query or [type definition](type-system/type-language.md) to Abstract Syntax Tree. @@ -67,10 +66,25 @@ class Parser * in the source that they correspond to. This configuration flag * disables that behavior for performance or testing.) * + * experimentalFragmentVariables: boolean, + * (If enabled, the parser will understand and parse variable definitions + * contained in a fragment definition. They'll be represented in the + * `variableDefinitions` field of the FragmentDefinitionNode. + * + * The syntax is identical to normal, query-defined variables. For example: + * + * fragment A($var: Boolean = false) on T { + * ... + * } + * + * Note: this feature is experimental and may change or be removed in the + * future.) + * * @api * @param Source|string $source * @param array $options * @return DocumentNode + * @throws SyntaxError */ public static function parse($source, array $options = []) { @@ -639,11 +653,19 @@ class Parser $this->expectKeyword('fragment'); $name = $this->parseFragmentName(); + + // Experimental support for defining variables within fragments changes + // the grammar of FragmentDefinition: + // - fragment FragmentName VariableDefinitions? on TypeCondition Directives? SelectionSet + $variableDefinitions = null; + if (isset($this->lexer->options['experimentalFragmentVariables'])) { + $variableDefinitions = $this->parseVariableDefinitions(); + } $this->expectKeyword('on'); $typeCondition = $this->parseNamedType(); - return new FragmentDefinitionNode([ 'name' => $name, + 'variableDefinitions' => $variableDefinitions, 'typeCondition' => $typeCondition, 'directives' => $this->parseDirectives(false), 'selectionSet' => $this->parseSelectionSet(), diff --git a/src/Language/Printer.php b/src/Language/Printer.php index 7c123a3..ed25c66 100644 --- a/src/Language/Printer.php +++ b/src/Language/Printer.php @@ -131,7 +131,11 @@ class Printer ], ' '); }, NodeKind::FRAGMENT_DEFINITION => function(FragmentDefinitionNode $node) { - return "fragment {$node->name} on {$node->typeCondition} " + // Note: fragment variable definitions are experimental and may be changed + // or removed in the future. + return "fragment {$node->name}" + . $this->wrap('(', $this->join($node->variableDefinitions, ', '), ')') + . " on {$node->typeCondition} " . $this->wrap('', $this->join($node->directives, ' '), ' ') . $node->selectionSet; }, diff --git a/src/Language/Visitor.php b/src/Language/Visitor.php index fc0a1e7..707a4b1 100644 --- a/src/Language/Visitor.php +++ b/src/Language/Visitor.php @@ -115,7 +115,15 @@ class Visitor NodeKind::ARGUMENT => ['name', 'value'], NodeKind::FRAGMENT_SPREAD => ['name', 'directives'], NodeKind::INLINE_FRAGMENT => ['typeCondition', 'directives', 'selectionSet'], - NodeKind::FRAGMENT_DEFINITION => ['name', 'typeCondition', 'directives', 'selectionSet'], + NodeKind::FRAGMENT_DEFINITION => [ + 'name', + // Note: fragment variable definitions are experimental and may be changed + // or removed in the future. + 'variableDefinitions', + 'typeCondition', + 'directives', + 'selectionSet' + ], NodeKind::INT => [], NodeKind::FLOAT => [], diff --git a/tests/Language/ParserTest.php b/tests/Language/ParserTest.php index f4032e6..b34cd9a 100644 --- a/tests/Language/ParserTest.php +++ b/tests/Language/ParserTest.php @@ -453,10 +453,23 @@ fragment $fragmentName on Type { $this->assertEquals(null, $result->loc); } + /** + * @it Experimental: allows parsing fragment defined variables + */ + public function testExperimentalAllowsParsingFragmentDefinedVariables() + { + $source = new Source('fragment a($v: Boolean = false) on t { f(v: $v) }'); + // not throw + Parser::parse($source, ['experimentalFragmentVariables' => true]); + + $this->setExpectedException(SyntaxError::class); + Parser::parse($source); + } + /** * @it contains location information that only stringifys start/end */ - public function testConvertToArray() + public function testContainsLocationInformationThatOnlyStringifysStartEnd() { $source = new Source('{ id }'); $result = Parser::parse($source); diff --git a/tests/Language/PrinterTest.php b/tests/Language/PrinterTest.php index 42bc0bc..9d4cce2 100644 --- a/tests/Language/PrinterTest.php +++ b/tests/Language/PrinterTest.php @@ -132,6 +132,28 @@ class PrinterTest extends \PHPUnit_Framework_TestCase $this->assertEquals($expected, Printer::doPrint($mutationAstWithArtifacts)); } + /** + * @it Experimental: correctly prints fragment defined variables + */ + public function testExperimentalCorrectlyPrintsFragmentDefinedVariables() + { + $fragmentWithVariable = Parser::parse(' + fragment Foo($a: ComplexType, $b: Boolean = false) on TestType { + id + } + ', + ['experimentalFragmentVariables' => true] + ); + + $this->assertEquals( + Printer::doPrint($fragmentWithVariable), + 'fragment Foo($a: ComplexType, $b: Boolean = false) on TestType { + id +} +' + ); + } + /** * @it correctly prints single-line with leading space and quotation */ diff --git a/tests/Language/VisitorTest.php b/tests/Language/VisitorTest.php index 5cfd0b1..6ccc2d9 100644 --- a/tests/Language/VisitorTest.php +++ b/tests/Language/VisitorTest.php @@ -326,6 +326,60 @@ class VisitorTest extends \PHPUnit_Framework_TestCase $this->assertEquals($expected, $visited); } + /** + * @it Experimental: visits variables defined in fragments + */ + public function testExperimentalVisitsVariablesDefinedInFragments() + { + $ast = Parser::parse( + 'fragment a($v: Boolean = false) on t { f }', + ['experimentalFragmentVariables' => true] + ); + $visited = []; + + Visitor::visit($ast, [ + 'enter' => function($node) use (&$visited) { + $visited[] = ['enter', $node->kind, isset($node->value) ? $node->value : null]; + }, + 'leave' => function($node) use (&$visited) { + $visited[] = ['leave', $node->kind, isset($node->value) ? $node->value : null]; + }, + ]); + + $expected = [ + ['enter', 'Document', null], + ['enter', 'FragmentDefinition', null], + ['enter', 'Name', 'a'], + ['leave', 'Name', 'a'], + ['enter', 'VariableDefinition', null], + ['enter', 'Variable', null], + ['enter', 'Name', 'v'], + ['leave', 'Name', 'v'], + ['leave', 'Variable', null], + ['enter', 'NamedType', null], + ['enter', 'Name', 'Boolean'], + ['leave', 'Name', 'Boolean'], + ['leave', 'NamedType', null], + ['enter', 'BooleanValue', false], + ['leave', 'BooleanValue', false], + ['leave', 'VariableDefinition', null], + ['enter', 'NamedType', null], + ['enter', 'Name', 't'], + ['leave', 'Name', 't'], + ['leave', 'NamedType', null], + ['enter', 'SelectionSet', null], + ['enter', 'Field', null], + ['enter', 'Name', 'f'], + ['leave', 'Name', 'f'], + ['leave', 'Field', null], + ['leave', 'SelectionSet', null], + ['leave', 'FragmentDefinition', null], + ['leave', 'Document', null], + ]; + + $this->assertEquals($expected, $visited); + } + /** * @it visits kitchen sink */