diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index a3e2d05..f4a6ca9 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -4,6 +4,7 @@ namespace GraphqlOrm\DependencyInjection; +use GraphqlOrm\Dialect\DefaultDialect; use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; @@ -20,6 +21,10 @@ public function getConfigTreeBuilder(): TreeBuilder ->cannotBeEmpty() ->end() + ->scalarNode('dialect') + ->defaultValue(DefaultDialect::class) + ->end() + ->arrayNode('headers') ->scalarPrototype()->end() ->defaultValue([]) diff --git a/src/DependencyInjection/GraphqlOrmExtension.php b/src/DependencyInjection/GraphqlOrmExtension.php index 8a29b3a..56ec761 100644 --- a/src/DependencyInjection/GraphqlOrmExtension.php +++ b/src/DependencyInjection/GraphqlOrmExtension.php @@ -32,6 +32,11 @@ public function load(array $configs, ContainerBuilder $container): void $config['endpoint'] ); + $container->setParameter( + 'graphql_orm.dialect', + $config['dialect'] + ); + $container->setParameter( 'graphql_orm.headers', $config['headers'] diff --git a/src/Dialect/DataApiBuilderDialect.php b/src/Dialect/DataApiBuilderDialect.php new file mode 100644 index 0000000..6442cb4 --- /dev/null +++ b/src/Dialect/DataApiBuilderDialect.php @@ -0,0 +1,41 @@ +indent($selection, $innerIndent); + + return << $items */ + $items = $data['items'] ?? []; + + return $items; + } + + private function indent(string $text, string $indent): string + { + return implode( + "\n", + array_map( + fn ($line) => $line !== '' ? $indent . $line : $line, + explode("\n", $text) + ) + ); + } +} diff --git a/src/Dialect/DefaultDialect.php b/src/Dialect/DefaultDialect.php new file mode 100644 index 0000000..70e6a43 --- /dev/null +++ b/src/Dialect/DefaultDialect.php @@ -0,0 +1,18 @@ + $data + * + * @return array + */ + public function extractCollection(array $data): array; +} diff --git a/src/GraphqlManager.php b/src/GraphqlManager.php index 6825e3b..743e2f7 100644 --- a/src/GraphqlManager.php +++ b/src/GraphqlManager.php @@ -6,6 +6,8 @@ use GraphqlOrm\Client\GraphqlClientInterface; use GraphqlOrm\DataCollector\GraphqlOrmDataCollector; +use GraphqlOrm\Dialect\DefaultDialect; +use GraphqlOrm\Dialect\GraphqlQueryDialect; use GraphqlOrm\Execution\GraphqlExecutionContext; use GraphqlOrm\Hydrator\EntityHydrator; use GraphqlOrm\Metadata\GraphqlEntityMetadataFactory; @@ -27,6 +29,7 @@ public function __construct( public EntityHydrator $hydrator, public GraphqlOrmDataCollector $collector, public int $maxDepth, + public GraphqlQueryDialect $dialect = new DefaultDialect(), ) { } @@ -62,4 +65,9 @@ public function execute( return $entities; } + + public function getDialect(): GraphqlQueryDialect + { + return $this->dialect; + } } diff --git a/src/Query/GraphqlQuery.php b/src/Query/GraphqlQuery.php index bd20a68..c582930 100644 --- a/src/Query/GraphqlQuery.php +++ b/src/Query/GraphqlQuery.php @@ -41,7 +41,25 @@ public function getResult(): array return $this ->manager ->execute($this->graphql, hydration: function ($result, $context) use ($metadata) { - $rows = $result['data'][$metadata->name] ?? null; + $dialect = $this->manager->getDialect(); + $data = $result['data'] ?? []; + + if (!\array_key_exists($metadata->name, $data)) { + return []; + } + + $root = $data[$metadata->name]; + + if ($root === null) { + return []; + } + + if (!\is_array($root)) { + throw InvalidGraphqlResponseException::expectedArray($root); + } + + $collection = $dialect->extractCollection($root); + $rows = !empty($collection) ? $collection : null; if ($rows === null) { return []; @@ -51,7 +69,7 @@ public function getResult(): array throw InvalidGraphqlResponseException::expectedArray($rows); } - if ($rows && array_is_list($rows) === false) { + if (array_is_list($rows) === false) { $rows = [$rows]; } diff --git a/src/Query/GraphqlQueryStringBuilder.php b/src/Query/GraphqlQueryStringBuilder.php index 8777326..f3cd7d8 100644 --- a/src/Query/GraphqlQueryStringBuilder.php +++ b/src/Query/GraphqlQueryStringBuilder.php @@ -105,6 +105,8 @@ public function build(): string } $args = $this->buildArguments(); + $dialect = $this->manager->getDialect(); + $selection = $dialect->wrapCollection($selection, 2); return <<manager->metadataFactory->getMetadata($entityClass); + $dialect = $this->manager->getDialect(); $lines = []; foreach ($metadata->fields as $field) { @@ -141,7 +144,11 @@ private function buildAllFields(string $entityClass, int $level): ?string continue; } - $lines[] = $indent . $field->mappedFrom . " {\n" . $nested . "\n" . $indent . '}'; + if ($field->isCollection) { + $lines[] = $indent . $field->mappedFrom . " {\n" . $dialect->wrapCollection($nested, $level) . "\n" . $indent . '}'; + } else { + $lines[] = $indent . $field->mappedFrom . " {\n" . $nested . "\n" . $indent . '}'; + } continue; } @@ -274,6 +281,7 @@ private function relationFallbackSelection(GraphqlFieldMetadata $field, int $lev if ($field->relation === null) { return $field->mappedFrom; } + $dialect = $this->manager->getDialect(); $relationMetadata = $this->manager->metadataFactory->getMetadata($field->relation); @@ -283,6 +291,10 @@ private function relationFallbackSelection(GraphqlFieldMetadata $field, int $lev $inner = str_repeat(' ', $level + 1); + if ($field->isCollection) { + return $field->mappedFrom . " {\n" . $inner . $dialect->wrapCollection($identifier, $level) . "\n" . $indent . '}'; + } + return $field->mappedFrom . " {\n" . $inner . $identifier . "\n" . $indent . '}'; } diff --git a/src/Repository/GraphqlEntityRepository.php b/src/Repository/GraphqlEntityRepository.php index 6cb4dc2..ea0fe38 100644 --- a/src/Repository/GraphqlEntityRepository.php +++ b/src/Repository/GraphqlEntityRepository.php @@ -63,7 +63,25 @@ public function findBy(array $criteria): array return $this ->manager ->execute($graphql, hydration: function (array $result, GraphqlExecutionContext $context) use ($metadata) { - $rows = $result['data'][$metadata->name] ?? null; + $dialect = $this->manager->getDialect(); + $data = $result['data'] ?? []; + + if (!\array_key_exists($metadata->name, $data)) { + return []; + } + + $root = $data[$metadata->name]; + + if ($root === null) { + return []; + } + + if (!\is_array($root)) { + throw InvalidGraphqlResponseException::expectedArray($root); + } + + $collection = $dialect->extractCollection($root); + $rows = !empty($collection) ? $collection : null; if ($rows === null) { return []; @@ -73,7 +91,7 @@ public function findBy(array $criteria): array throw InvalidGraphqlResponseException::expectedArray($rows); } - if ($rows && array_is_list($rows) === false) { + if (array_is_list($rows) === false) { $rows = [$rows]; } diff --git a/src/Resources/config/services.php b/src/Resources/config/services.php index cfdf36b..65325e8 100644 --- a/src/Resources/config/services.php +++ b/src/Resources/config/services.php @@ -12,6 +12,8 @@ use GraphqlOrm\Metadata\GraphqlEntityMetadataFactory; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; +use function Symfony\Component\DependencyInjection\Loader\Configurator\service; + return static function (ContainerConfigurator $config) { $services = $config->services() ->defaults() @@ -20,8 +22,16 @@ $services->set(GraphqlEntityMetadataFactory::class); $services->set(EntityHydrator::class); + + $services + ->set('graphql_orm.dialect', '%graphql_orm.dialect%') + ->autowire() + ->autoconfigure(); + $services->set(GraphqlManager::class) - ->arg('$maxDepth', '%graphql_orm.max_depth%'); + ->arg('$maxDepth', '%graphql_orm.max_depth%') + ->arg('$dialect', service('graphql_orm.dialect')); + $services->set(GraphqlClientInterface::class); $services->set(GraphqlClient::class) diff --git a/tests/Dialect/DataApiBuilderDialectTest.php b/tests/Dialect/DataApiBuilderDialectTest.php new file mode 100644 index 0000000..c361d61 --- /dev/null +++ b/tests/Dialect/DataApiBuilderDialectTest.php @@ -0,0 +1,40 @@ + [ + ['id' => 1], + ['id' => 2], + ], + ]; + + self::assertSame($data['items'], $dialect->extractCollection($data)); + } + + public function testExtractCollectionReturnsEmptyWhenNoItems(): void + { + $dialect = new DataApiBuilderDialect(); + + self::assertSame([], $dialect->extractCollection([])); + } + + public function testExtractCollectionReturnsEmptyWhenItemsNull(): void + { + $dialect = new DataApiBuilderDialect(); + + self::assertSame([], $dialect->extractCollection([ + 'items' => null, + ])); + } +} diff --git a/tests/Dialect/DefaultDialectTest.php b/tests/Dialect/DefaultDialectTest.php new file mode 100644 index 0000000..7dd172c --- /dev/null +++ b/tests/Dialect/DefaultDialectTest.php @@ -0,0 +1,29 @@ + 1], + ['id' => 2], + ]; + + self::assertSame($data, $dialect->extractCollection($data)); + } + + public function testExtractCollectionReturnsEmptyArray(): void + { + $dialect = new DefaultDialect(); + + self::assertSame([], $dialect->extractCollection([])); + } +} diff --git a/tests/Query/GraphqlQueryTest.php b/tests/Query/GraphqlQueryTest.php index 4a33fa1..208e1a2 100644 --- a/tests/Query/GraphqlQueryTest.php +++ b/tests/Query/GraphqlQueryTest.php @@ -4,6 +4,7 @@ namespace GraphqlOrm\Tests\Query; +use GraphqlOrm\Dialect\DefaultDialect; use GraphqlOrm\Execution\GraphqlExecutionContext; use GraphqlOrm\GraphqlManager; use GraphqlOrm\Hydrator\EntityHydrator; @@ -145,6 +146,10 @@ private function createManager(array $response, ?EntityHydrator $hydrator = null ->method('execute') ->willReturnCallback(fn ($_, $hydration) => $hydration($response, new GraphqlExecutionContext(), [])); + $manager + ->method('getDialect') + ->willReturn(new DefaultDialect()); + return $manager; } } diff --git a/tests/Repository/GraphqlEntityRepositoryTest.php b/tests/Repository/GraphqlEntityRepositoryTest.php index a6435f7..bb75f46 100644 --- a/tests/Repository/GraphqlEntityRepositoryTest.php +++ b/tests/Repository/GraphqlEntityRepositoryTest.php @@ -4,6 +4,7 @@ namespace GraphqlOrm\Tests\Repository; +use GraphqlOrm\Dialect\DefaultDialect; use GraphqlOrm\Exception\InvalidGraphqlResponseException; use GraphqlOrm\Execution\GraphqlExecutionContext; use GraphqlOrm\GraphqlManager; @@ -149,6 +150,10 @@ private function createManager(array $response, ?EntityHydrator $hydrator = null ->method('execute') ->willReturnCallback(fn ($_, $hydration) => $hydration($response, $this->createStub(GraphqlExecutionContext::class))); + $manager + ->method('getDialect') + ->willReturn(new DefaultDialect()); + return $manager; } }