From fde7559ed1447c5e4733d66676ec2ecda97d76ea Mon Sep 17 00:00:00 2001 From: HECHT Axel Date: Tue, 24 Feb 2026 00:15:29 +0100 Subject: [PATCH] feat: add AST to generate GraphQL query --- src/DataCollector/GraphqlOrmDataCollector.php | 1 + src/Dialect/DataApiBuilderDialect.php | 27 +-- src/Dialect/DefaultDialect.php | 11 +- src/Dialect/GraphqlQueryDialect.php | 6 +- src/Exception/LogicException.php | 9 + src/GraphqlManager.php | 36 ++- src/Query/Ast/FieldNode.php | 18 ++ src/Query/Ast/QueryNode.php | 13 ++ src/Query/Ast/SelectionSetNode.php | 16 ++ src/Query/GraphqlQuery.php | 7 +- src/Query/GraphqlQueryBuilder.php | 4 +- src/Query/GraphqlQueryCompiler.php | 21 ++ src/Query/GraphqlQueryStringBuilder.php | 207 ++++++++---------- src/Query/GraphqlQueryTrace.php | 2 + src/Query/Printer/GraphqlPrinter.php | 31 +++ src/Query/Walker/AbstractGraphqlWalker.php | 99 +++++++++ src/Query/Walker/DABGraphqlWalker.php | 56 +++++ src/Query/Walker/DefaultGraphqlWalker.php | 30 +++ src/Query/Walker/GraphqlWalkerInterface.php | 12 + .../views/collector/graphql_orm.html.twig | 142 ++++++++++++ tests/Query/GraphqlQueryStringBuilderTest.php | 18 +- tests/Query/Printer/GraphqlPrinterTest.php | 53 +++++ tests/Query/Walker/DABGraphqlWalkerTest.php | 60 +++++ .../Query/Walker/DefaultGraphqlWalkerTest.php | 139 ++++++++++++ 24 files changed, 859 insertions(+), 159 deletions(-) create mode 100644 src/Exception/LogicException.php create mode 100644 src/Query/Ast/FieldNode.php create mode 100644 src/Query/Ast/QueryNode.php create mode 100644 src/Query/Ast/SelectionSetNode.php create mode 100644 src/Query/GraphqlQueryCompiler.php create mode 100644 src/Query/Printer/GraphqlPrinter.php create mode 100644 src/Query/Walker/AbstractGraphqlWalker.php create mode 100644 src/Query/Walker/DABGraphqlWalker.php create mode 100644 src/Query/Walker/DefaultGraphqlWalker.php create mode 100644 src/Query/Walker/GraphqlWalkerInterface.php create mode 100644 tests/Query/Printer/GraphqlPrinterTest.php create mode 100644 tests/Query/Walker/DABGraphqlWalkerTest.php create mode 100644 tests/Query/Walker/DefaultGraphqlWalkerTest.php diff --git a/src/DataCollector/GraphqlOrmDataCollector.php b/src/DataCollector/GraphqlOrmDataCollector.php index f805cae..81942a9 100644 --- a/src/DataCollector/GraphqlOrmDataCollector.php +++ b/src/DataCollector/GraphqlOrmDataCollector.php @@ -16,6 +16,7 @@ public function addQuery( ): void { $this->data['queries'][] = [ 'graphql' => $trace->graphql, + 'ast' => $trace->ast, 'variables' => $trace->variables, 'endpoint' => $trace->endpoint, 'caller' => $trace->caller, diff --git a/src/Dialect/DataApiBuilderDialect.php b/src/Dialect/DataApiBuilderDialect.php index 6442cb4..b0a96ac 100644 --- a/src/Dialect/DataApiBuilderDialect.php +++ b/src/Dialect/DataApiBuilderDialect.php @@ -4,22 +4,11 @@ namespace GraphqlOrm\Dialect; +use GraphqlOrm\Query\Walker\DABGraphqlWalker; +use GraphqlOrm\Query\Walker\GraphqlWalkerInterface; + final class DataApiBuilderDialect implements GraphqlQueryDialect { - public function wrapCollection(string $selection, int $indentLevel): string - { - $indent = str_repeat(' ', $indentLevel); - $innerIndent = str_repeat(' ', $indentLevel - 1); - - $selection = $this->indent($selection, $innerIndent); - - return << $items */ @@ -28,14 +17,8 @@ public function extractCollection(array $data): array return $items; } - private function indent(string $text, string $indent): string + public function createWalker(): GraphqlWalkerInterface { - return implode( - "\n", - array_map( - fn ($line) => $line !== '' ? $indent . $line : $line, - explode("\n", $text) - ) - ); + return new DABGraphqlWalker(); } } diff --git a/src/Dialect/DefaultDialect.php b/src/Dialect/DefaultDialect.php index 70e6a43..78187c3 100644 --- a/src/Dialect/DefaultDialect.php +++ b/src/Dialect/DefaultDialect.php @@ -4,15 +4,18 @@ namespace GraphqlOrm\Dialect; +use GraphqlOrm\Query\Walker\DefaultGraphqlWalker; +use GraphqlOrm\Query\Walker\GraphqlWalkerInterface; + final class DefaultDialect implements GraphqlQueryDialect { - public function wrapCollection(string $selection, int $indentLevel): string + public function extractCollection(array $data): array { - return $selection; + return $data; } - public function extractCollection(array $data): array + public function createWalker(): GraphqlWalkerInterface { - return $data; + return new DefaultGraphqlWalker(); } } diff --git a/src/Dialect/GraphqlQueryDialect.php b/src/Dialect/GraphqlQueryDialect.php index 1f2808f..00aff89 100644 --- a/src/Dialect/GraphqlQueryDialect.php +++ b/src/Dialect/GraphqlQueryDialect.php @@ -4,14 +4,16 @@ namespace GraphqlOrm\Dialect; +use GraphqlOrm\Query\Walker\GraphqlWalkerInterface; + interface GraphqlQueryDialect { - public function wrapCollection(string $selection, int $indentLevel): string; - /** * @param array $data * * @return array */ public function extractCollection(array $data): array; + + public function createWalker(): GraphqlWalkerInterface; } diff --git a/src/Exception/LogicException.php b/src/Exception/LogicException.php new file mode 100644 index 0000000..369a280 --- /dev/null +++ b/src/Exception/LogicException.php @@ -0,0 +1,9 @@ +trace->graphql = $graphql; + if ($graphql instanceof QueryNode) { + $compiled = $this->getQueryCompiler()->compile($graphql); + /** @var array $ast */ + $ast = json_decode(json_encode($graphql, JSON_THROW_ON_ERROR), true); + $context->trace->ast = $ast; + } else { + $compiled = $graphql; + } + + $context->trace->graphql = $compiled; try { $result = $this->client ->query( - $graphql, + $compiled, $context, $variables ); @@ -74,4 +83,17 @@ public function getDialect(): GraphqlQueryDialect { return $this->dialect; } + + public function getQueryCompiler(): GraphqlQueryCompiler + { + if ($this->compiler !== null) { + return $this->compiler; + } + + $walker = $this->dialect->createWalker(); + + $this->compiler = new GraphqlQueryCompiler($walker); + + return $this->compiler; + } } diff --git a/src/Query/Ast/FieldNode.php b/src/Query/Ast/FieldNode.php new file mode 100644 index 0000000..0495d23 --- /dev/null +++ b/src/Query/Ast/FieldNode.php @@ -0,0 +1,18 @@ + $arguments + */ + public function __construct( + public string $name, + public array $arguments = [], + public ?SelectionSetNode $selectionSet = null, + ) { + } +} diff --git a/src/Query/Ast/QueryNode.php b/src/Query/Ast/QueryNode.php new file mode 100644 index 0000000..75fabc0 --- /dev/null +++ b/src/Query/Ast/QueryNode.php @@ -0,0 +1,13 @@ +fields[] = $field; + } +} diff --git a/src/Query/GraphqlQuery.php b/src/Query/GraphqlQuery.php index c582930..609a61a 100644 --- a/src/Query/GraphqlQuery.php +++ b/src/Query/GraphqlQuery.php @@ -6,6 +6,7 @@ use GraphqlOrm\Exception\InvalidGraphqlResponseException; use GraphqlOrm\GraphqlManager; +use GraphqlOrm\Query\Ast\QueryNode; /** * @template T of object @@ -17,7 +18,7 @@ * @param GraphqlManager $manager */ public function __construct( - private string $graphql, + private QueryNode|string $graphql, private string $entityClass, private GraphqlManager $manager, ) { @@ -25,6 +26,10 @@ public function __construct( public function getGraphQL(): string { + if ($this->graphql instanceof QueryNode) { + return $this->manager->getQueryCompiler()->compile($this->graphql); + } + return $this->graphql; } diff --git a/src/Query/GraphqlQueryBuilder.php b/src/Query/GraphqlQueryBuilder.php index 847dda9..5b5244f 100644 --- a/src/Query/GraphqlQueryBuilder.php +++ b/src/Query/GraphqlQueryBuilder.php @@ -95,7 +95,7 @@ public function getQuery(): GraphqlQuery $manualSelect = $this->selectedFields !== null; - $graphql = (new GraphqlQueryStringBuilder($this->manager)) + $ast = (new GraphqlQueryStringBuilder($this->manager)) ->entity($this->entityClass) ->root($metadata->name) ->arguments($this->criteria) @@ -103,7 +103,7 @@ public function getQuery(): GraphqlQuery ->build(); return new GraphqlQuery( - $graphql, + $ast, $this->entityClass, $this->manager ); diff --git a/src/Query/GraphqlQueryCompiler.php b/src/Query/GraphqlQueryCompiler.php new file mode 100644 index 0000000..0a4be52 --- /dev/null +++ b/src/Query/GraphqlQueryCompiler.php @@ -0,0 +1,21 @@ +walker->walk($node); + } +} diff --git a/src/Query/GraphqlQueryStringBuilder.php b/src/Query/GraphqlQueryStringBuilder.php index f3cd7d8..7802d05 100644 --- a/src/Query/GraphqlQueryStringBuilder.php +++ b/src/Query/GraphqlQueryStringBuilder.php @@ -4,10 +4,13 @@ namespace GraphqlOrm\Query; -use GraphqlOrm\Exception\InvalidArgumentException; +use GraphqlOrm\Exception\LogicException; use GraphqlOrm\GraphqlManager; use GraphqlOrm\Metadata\GraphqlEntityMetadata; use GraphqlOrm\Metadata\GraphqlFieldMetadata; +use GraphqlOrm\Query\Ast\FieldNode; +use GraphqlOrm\Query\Ast\QueryNode; +use GraphqlOrm\Query\Ast\SelectionSetNode; /** * @template T of object @@ -80,83 +83,70 @@ public function entity(string $entityClass): self return $this; } - private function buildArguments(): string + public function build(): QueryNode { - if (!$this->arguments) { - return ''; - } - - $args = []; - - foreach ($this->arguments as $name => $value) { - $args[] = $name . ': ' . $this->formatValue($value); - } + $query = new QueryNode(); - return '(' . implode(', ', $args) . ')'; - } + $root = new FieldNode( + $this->root, + $this->arguments, + new SelectionSetNode() + ); - public function build(): string - { if ($this->manualSelect) { $tree = $this->buildSelectionTree($this->fields); - $selection = $this->buildFromTree($this->entityClass, $tree, 2); + $selection = $this->buildFromTreeAst($this->entityClass, $tree); } else { - $selection = $this->buildAllFields($this->entityClass, 2) ?? ''; + $selection = $this->buildAllFieldsAst($this->entityClass); } - $args = $this->buildArguments(); - $dialect = $this->manager->getDialect(); - $selection = $dialect->wrapCollection($selection, 2); + foreach ($selection->fields as $field) { + $root->selectionSet?->add($field); + } - return <<root}{$args} { -{$selection} - } -} -GRAPHQL; + $query->fields[] = $root; + + return $query; } /** * @param class-string $entityClass */ - private function buildAllFields(string $entityClass, int $level): ?string + private function buildAllFieldsAst(string $entityClass): SelectionSetNode { + $selection = new SelectionSetNode(); + if (isset($this->visited[$entityClass])) { - return null; + return $selection; } $this->visited[$entityClass] = true; try { - $metadata = $this->manager->metadataFactory->getMetadata($entityClass); - $dialect = $this->manager->getDialect(); - $lines = []; + $metadata = $this + ->manager + ->metadataFactory + ->getMetadata($entityClass); foreach ($metadata->fields as $field) { - $indent = str_repeat(' ', $level); - if ($field->relation !== null) { - $nested = $this->buildAllFields($field->relation, $level + 1); + $nested = $this->buildAllFieldsAst($field->relation); + + if ($nested->fields === []) { + $selection->add($this->relationFallbackAst($field)); - if ($nested === null || $nested === '') { - $lines[] = $indent . $this->relationFallbackSelection($field, $level); continue; } - if ($field->isCollection) { - $lines[] = $indent . $field->mappedFrom . " {\n" . $dialect->wrapCollection($nested, $level) . "\n" . $indent . '}'; - } else { - $lines[] = $indent . $field->mappedFrom . " {\n" . $nested . "\n" . $indent . '}'; - } + $selection->add(new FieldNode($field->mappedFrom, [], $nested)); continue; } - $lines[] = $indent . $field->mappedFrom; + $selection->add(new FieldNode($field->mappedFrom)); } - return implode("\n", $lines); + return $selection; } finally { unset($this->visited[$entityClass]); } @@ -206,137 +196,114 @@ private function buildSelectionTree(array $fields): array * @param class-string $entityClass * @param array $tree */ - private function buildFromTree(string $entityClass, array $tree, int $level): string + private function buildFromTreeAst(string $entityClass, array $tree, ): SelectionSetNode { - $metadata = $this->manager->metadataFactory->getMetadata($entityClass); + $selection = new SelectionSetNode(); - $lines = []; + $metadata = $this + ->manager + ->metadataFactory + ->getMetadata($entityClass); foreach ($tree as $fieldName => $children) { if (!\is_array($children)) { continue; } - $indent = str_repeat(' ', $level); $field = $this->findFieldMetadata($metadata, $fieldName); if ($field === null) { - $lines[] = $indent . $fieldName; + $selection->add(new FieldNode($fieldName)); + continue; } - if ($field->relation !== null) { - $explicit = isset($children['__explicit']); - unset($children['__explicit']); + if ($field->relation === null) { + $selection->add(new FieldNode($field->mappedFrom)); - $relationMetadata = $this->manager->metadataFactory->getMetadata($field->relation); + continue; + } - if ($relationMetadata->identifier !== null) { - $identifier = $relationMetadata->identifier->mappedFrom; + $explicit = isset($children['__explicit']); - $children[$identifier] ??= []; - } + unset($children['__explicit']); - if ($explicit) { - $nested = $this->buildAllFields($field->relation, $level + 1); + $relationMetadata = $this + ->manager + ->metadataFactory + ->getMetadata($field->relation); - if (!$nested) { - $lines[] = $indent . $this->relationFallbackSelection($field, $level); + if ($relationMetadata->identifier !== null) { + $identifier = $relationMetadata->identifier->mappedFrom; - continue; - } + $children[$identifier] ??= []; + } + + if ($explicit) { + $nested = $this->buildAllFieldsAst($field->relation); - $lines[] = $indent . $field->mappedFrom . " {\n" . $nested . "\n" . $indent . '}'; + if ($nested->fields === []) { + $selection->add($this->relationFallbackAst($field)); continue; } - if ($this->manualSelect && !$children) { - $lines[] = $indent . $this->relationFallbackSelection($field, $level); + $selection->add(new FieldNode($field->mappedFrom, [], $nested)); - continue; - } + continue; + } - $nested = $this->buildFromTree($field->relation, $children, $level + 1); + if ($this->manualSelect && $children === []) { + $selection->add($this->relationFallbackAst($field)); - if ($nested === '') { - $lines[] = $indent . $this->relationFallbackSelection($field, $level); + continue; + } - continue; - } + $nested = $this->buildFromTreeAst($field->relation, $children); - $lines[] = $indent . $field->mappedFrom . " {\n" . $nested . "\n" . $indent . '}'; + if ($nested->fields === []) { + $selection->add($this->relationFallbackAst($field)); continue; } - $lines[] = $indent . $field->mappedFrom; + $selection->add(new FieldNode($field->mappedFrom, [], $nested)); } - return implode("\n", $lines); + return $selection; } - private function relationFallbackSelection(GraphqlFieldMetadata $field, int $level): string + private function relationFallbackAst(GraphqlFieldMetadata $field): FieldNode { if ($field->relation === null) { - return $field->mappedFrom; + throw new LogicException('Relation metadata requested on non relation field.'); } - $dialect = $this->manager->getDialect(); - $relationMetadata = $this->manager->metadataFactory->getMetadata($field->relation); + $relationMetadata = $this + ->manager + ->metadataFactory + ->getMetadata($field->relation); $identifier = $relationMetadata->identifier?->mappedFrom ?? 'id'; - $indent = str_repeat(' ', $level); - - $inner = str_repeat(' ', $level + 1); + $selection = new SelectionSetNode(); + $selection->add(new FieldNode($identifier)); - if ($field->isCollection) { - return $field->mappedFrom . " {\n" . $inner . $dialect->wrapCollection($identifier, $level) . "\n" . $indent . '}'; - } - - return $field->mappedFrom . " {\n" . $inner . $identifier . "\n" . $indent . '}'; + return new FieldNode($field->mappedFrom, [], $selection); } private function findFieldMetadata(GraphqlEntityMetadata $metadata, string $name): ?GraphqlFieldMetadata { foreach ($metadata->fields as $field) { - if ($field->mappedFrom === $name || $field->property === $name) { + if ($field->mappedFrom === $name) { return $field; } - } - - return null; - } - private function formatValue(mixed $value): string - { - if (\is_string($value)) { - return '"' . addslashes($value) . '"'; - } - - if (\is_bool($value)) { - return $value ? 'true' : 'false'; - } - - if ($value === null) { - return 'null'; - } - - if (\is_array($value)) { - return '[' . implode( - ', ', - array_map( - fn ($v) => $this->formatValue($v), - $value - ) - ) . ']'; - } - - if (\is_scalar($value)) { - return (string) $value; + if ($field->property === $name) { + return $field; + } } - throw new InvalidArgumentException(\sprintf('Invalid GraphQL argument value of type "%s".', get_debug_type($value))); + return null; } } diff --git a/src/Query/GraphqlQueryTrace.php b/src/Query/GraphqlQueryTrace.php index 531ac28..305b19f 100644 --- a/src/Query/GraphqlQueryTrace.php +++ b/src/Query/GraphqlQueryTrace.php @@ -7,6 +7,8 @@ final class GraphqlQueryTrace { public string $graphql; + /** @var array */ + public ?array $ast = null; /** @var array */ public array $variables = []; /** @var array{ diff --git a/src/Query/Printer/GraphqlPrinter.php b/src/Query/Printer/GraphqlPrinter.php new file mode 100644 index 0000000..bd87f94 --- /dev/null +++ b/src/Query/Printer/GraphqlPrinter.php @@ -0,0 +1,31 @@ +buffer .= str_repeat(' ', $this->level) . $line . "\n"; + } + + public function indent(): void + { + ++$this->level; + } + + public function outdent(): void + { + --$this->level; + } + + public function get(): string + { + return rtrim($this->buffer, "\n"); + } +} diff --git a/src/Query/Walker/AbstractGraphqlWalker.php b/src/Query/Walker/AbstractGraphqlWalker.php new file mode 100644 index 0000000..f22fba9 --- /dev/null +++ b/src/Query/Walker/AbstractGraphqlWalker.php @@ -0,0 +1,99 @@ +formatArguments($field->arguments); + + if ($field->selectionSet === null) { + $this->printer->line($field->name . $args); + + return; + } + + $this->printer->line($field->name . $args . ' {'); + + $this->printer->indent(); + + $this->walkSelectionSet($field->selectionSet); + + $this->printer->outdent(); + + $this->printer->line('}'); + } + + protected function formatValue(mixed $value): string + { + if (\is_string($value)) { + return '"' . $value . '"'; + } + + if (\is_int($value) || \is_float($value)) { + return (string) $value; + } + + if (\is_bool($value)) { + return $value ? 'true' : 'false'; + } + + if ($value === null) { + return 'null'; + } + + if (\is_array($value)) { + $items = array_map( + fn ($v) => $this->formatValue($v), + $value + ); + + return '[' . implode(', ', $items) . ']'; + } + + if ($value instanceof \BackedEnum) { + return '"' . $value->value . '"'; + } + + if ($value instanceof \DateTimeInterface) { + return '"' . $value->format(DATE_ATOM) . '"'; + } + + throw new InvalidArgumentException(\sprintf('Unsupported GraphQL argument type "%s".', get_debug_type($value))); + } + + /** + * @param array $arguments + */ + protected function formatArguments(array $arguments): string + { + if ($arguments === []) { + return ''; + } + + $pairs = []; + + foreach ($arguments as $key => $value) { + $pairs[] = \sprintf('%s: %s', $key, $this->formatValue($value)); + } + + return '(' . implode(', ', $pairs) . ')'; + } + + protected function walkSelectionSet(SelectionSetNode $selectionSet): void + { + foreach ($selectionSet->fields as $child) { + $this->walkField($child); + } + } +} diff --git a/src/Query/Walker/DABGraphqlWalker.php b/src/Query/Walker/DABGraphqlWalker.php new file mode 100644 index 0000000..01b642b --- /dev/null +++ b/src/Query/Walker/DABGraphqlWalker.php @@ -0,0 +1,56 @@ +printer = new GraphqlPrinter(); + + $this->printer->line($query->operation . ' {'); + + $this->printer->indent(); + + foreach ($query->fields as $field) { + $this->walkRootField($field); + } + + $this->printer->outdent(); + + $this->printer->line('}'); + + return $this->printer->get(); + } + + private function walkRootField(FieldNode $field): void + { + $args = $this->formatArguments($field->arguments); + + $this->printer->line($field->name . $args . ' {'); + + $this->printer->indent(); + + $this->printer->line('items {'); + + $this->printer->indent(); + + if ($field->selectionSet !== null) { + $this->walkSelectionSet($field->selectionSet); + } + + $this->printer->outdent(); + + $this->printer->line('}'); + + $this->printer->outdent(); + + $this->printer->line('}'); + } +} diff --git a/src/Query/Walker/DefaultGraphqlWalker.php b/src/Query/Walker/DefaultGraphqlWalker.php new file mode 100644 index 0000000..b5eab2d --- /dev/null +++ b/src/Query/Walker/DefaultGraphqlWalker.php @@ -0,0 +1,30 @@ +printer = new GraphqlPrinter(); + + $this->printer->line($query->operation . ' {'); + + $this->printer->indent(); + + foreach ($query->fields as $field) { + $this->walkField($field); + } + + $this->printer->outdent(); + + $this->printer->line('}'); + + return $this->printer->get(); + } +} diff --git a/src/Query/Walker/GraphqlWalkerInterface.php b/src/Query/Walker/GraphqlWalkerInterface.php new file mode 100644 index 0000000..21a6383 --- /dev/null +++ b/src/Query/Walker/GraphqlWalkerInterface.php @@ -0,0 +1,12 @@ + + + {% if query.ast %} +

GraphQL AST

+
+ {{ _self.renderAst(query.ast, 0) }} +
+ {% endif %} +

Variables

{% if query.variables %}
{{ dump(query.variables) }}
@@ -205,6 +213,117 @@ border-radius:6px; {% endfor %} {% endif %} + {% macro renderAst(node, depth) %} + {% import _self as self %} + {% if node.operation is defined %} +
+
+ + {{ node.operation }} + + [depth: {{ depth }} | fields: {{ node.fields|length }}] + +
+
+ {% for field in node.fields %} + {{ self.renderAst(field, depth + 1) }} + {% endfor %} +
+
+ {% else %} + {% set hasChildren = node.selectionSet and node.selectionSet.fields|length > 0 %} +
+
+ {% if hasChildren %} + + {% else %} + + {% endif %} + + {{ node.name }} + {% if node.arguments is defined and node.arguments %} + ( + {% for k,v in node.arguments %} + {{ k }}={{ v }}{% if not loop.last %}, {% endif %} + {% endfor %} + ) + {% endif %} + + {% if hasChildren %} + + [depth: {{ depth }} | fields: {{ node.selectionSet.fields|length }}] + + {% endif %} +
+ {% if hasChildren %} +
+ {% for child in node.selectionSet.fields %} + {{ self.renderAst(child,depth + 1) }} + {% endfor %} +
+ {% endif %} +
+ {% endif %} + {% endmacro %} + + + {% endblock %} \ No newline at end of file diff --git a/tests/Query/GraphqlQueryStringBuilderTest.php b/tests/Query/GraphqlQueryStringBuilderTest.php index ced7c73..4aa5840 100644 --- a/tests/Query/GraphqlQueryStringBuilderTest.php +++ b/tests/Query/GraphqlQueryStringBuilderTest.php @@ -37,6 +37,8 @@ public function testBuildWithArgumentsFormatting(): void ]) ->build(); + $query = $manager->getQueryCompiler()->compile($query); + self::assertStringContainsString('task(id: 1, active: true, status: "OPEN", tags: ["a", "b"], nullable: null)', $query); } @@ -50,6 +52,8 @@ public function testManualSelectSimpleField(): void ->fields(['title'], true) ->build(); + $query = $manager->getQueryCompiler()->compile($query); + self::assertSame( <<build(); + $query = $manager->getQueryCompiler()->compile($query); + self::assertSame( <<fields(['user'], true) ->build(); + $query = $manager->getQueryCompiler()->compile($query); + self::assertSame( <<fields(['manager'], true) ->build(); + $query = $manager->getQueryCompiler()->compile($query); + self::assertStringContainsString( <<entity(User::class) ->build(); + $query = $manager->getQueryCompiler()->compile($query); + self::assertStringContainsString( <<fields(['customGraphqlField'], true) ->build(); + $query = $manager->getQueryCompiler()->compile($query); + self::assertStringContainsString('customGraphqlField', $query); } @@ -193,13 +207,15 @@ public function testFormatValueThrowsOnObject(): void $manager = $this->createManager(); - (new GraphqlQueryStringBuilder($manager)) + $query = (new GraphqlQueryStringBuilder($manager)) ->root('task') ->entity(Task::class) ->arguments([ 'invalid' => new \stdClass(), ]) ->build(); + + $manager->getQueryCompiler()->compile($query); } private function createManager(): GraphqlManager diff --git a/tests/Query/Printer/GraphqlPrinterTest.php b/tests/Query/Printer/GraphqlPrinterTest.php new file mode 100644 index 0000000..9d74026 --- /dev/null +++ b/tests/Query/Printer/GraphqlPrinterTest.php @@ -0,0 +1,53 @@ +line('query {'); + $printer->indent(); + $printer->line('tasks'); + $printer->outdent(); + $printer->line('}'); + + self::assertSame( + <<get() + ); + } + + public function testAddField(): void + { + $set = new SelectionSetNode(); + $set->add(new FieldNode('id')); + $set->add(new FieldNode('title')); + + self::assertCount(2, $set->fields); + self::assertSame('title', $set->fields[1]->name); + } + + public function testFieldNodeStoresValues(): void + { + $selection = new SelectionSetNode(); + + $field = new FieldNode('task', ['id' => 1], $selection); + + self::assertSame('task', $field->name); + self::assertSame(1, $field->arguments['id']); + self::assertSame($selection, $field->selectionSet); + } +} diff --git a/tests/Query/Walker/DABGraphqlWalkerTest.php b/tests/Query/Walker/DABGraphqlWalkerTest.php new file mode 100644 index 0000000..edc2036 --- /dev/null +++ b/tests/Query/Walker/DABGraphqlWalkerTest.php @@ -0,0 +1,60 @@ +add(new FieldNode('id')); + + $query->fields[] = new FieldNode('tasks', selectionSet: $selection); + + $walker = new DABGraphqlWalker(); + $graphql = $walker->walk($query); + + self::assertSame( + <<add(new FieldNode('id')); + + $query->fields[] = new FieldNode( + 'tasks', + selectionSet: $selection + ); + + $compiler = new GraphqlQueryCompiler(new DABGraphqlWalker()); + + $graphql = $compiler->compile($query); + + self::assertStringContainsString('tasks', $graphql); + } +} diff --git a/tests/Query/Walker/DefaultGraphqlWalkerTest.php b/tests/Query/Walker/DefaultGraphqlWalkerTest.php new file mode 100644 index 0000000..8b70ca9 --- /dev/null +++ b/tests/Query/Walker/DefaultGraphqlWalkerTest.php @@ -0,0 +1,139 @@ +add(new FieldNode('id')); + $selection->add(new FieldNode('title')); + + $query->fields[] = new FieldNode(name: 'tasks', selectionSet: $selection); + + $walker = new DefaultGraphqlWalker(); + $graphql = $walker->walk($query); + + self::assertSame( + <<add(new FieldNode('id')); + $userSelection->add(new FieldNode('name')); + + $taskSelection = new SelectionSetNode(); + $taskSelection->add(new FieldNode('id')); + $taskSelection->add(new FieldNode('user', selectionSet: $userSelection)); + + $query->fields[] = new FieldNode('tasks', selectionSet: $taskSelection); + + $walker = new DefaultGraphqlWalker(); + $graphql = $walker->walk($query); + + self::assertSame( + <<add(new FieldNode('id')); + + $query->fields[] = new FieldNode( + name: 'task', + arguments: [ + 'id' => 1, + 'active' => true, + ], + selectionSet: $selection + ); + + $walker = new DefaultGraphqlWalker(); + $graphql = $walker->walk($query); + + self::assertStringContainsString('task(id: 1, active: true)', $graphql); + } + + public function testFormatsValues(): void + { + $walker = new DefaultGraphqlWalker(); + + $ref = new \ReflectionClass($walker); + $method = $ref->getMethod('formatValue'); + $method->setAccessible(true); + + self::assertSame('"hello"', $method->invoke($walker, 'hello')); + self::assertSame('true', $method->invoke($walker, true)); + self::assertSame('null', $method->invoke($walker, null)); + self::assertSame('[1, 2]', $method->invoke($walker, [1, 2])); + } + + public function testThrowsOnUnsupportedType(): void + { + $this->expectException(InvalidArgumentException::class); + + $walker = new DefaultGraphqlWalker(); + + $ref = new \ReflectionClass($walker); + $method = $ref->getMethod('formatValue'); + $method->setAccessible(true); + + $method->invoke($walker, new \stdClass()); + } + + public function testCompilerUsesWalker(): void + { + $query = new QueryNode(); + + $selection = new SelectionSetNode(); + $selection->add(new FieldNode('id')); + + $query->fields[] = new FieldNode('tasks', selectionSet: $selection); + + $compiler = new GraphqlQueryCompiler(new DefaultGraphqlWalker()); + + $graphql = $compiler->compile($query); + + self::assertStringContainsString('tasks', $graphql); + } +}