From cad41520231143361cc399551e2660302558a549 Mon Sep 17 00:00:00 2001 From: Philippe Jausions Date: Sun, 26 Jan 2025 16:16:32 +0100 Subject: [PATCH] [ISSUE-36] Allow to configure how deep the schema inspector should go for type/ofType sub-queries --- .gitignore | 3 +- README.md | 7 ++ bin/generate_schema_objects | 29 +++++- src/SchemaGenerator/SchemaClassGenerator.php | 49 ++++++---- src/SchemaGenerator/SchemaInspector.php | 98 +++++++++++-------- .../SchemaInspector/TypeSubQueryGenerator.php | 75 ++++++++++++++ tests/SchemaClassGeneratorTest.php | 12 +-- tests/TypeSubQueryGeneratorTest.php | 77 +++++++++++++++ 8 files changed, 279 insertions(+), 71 deletions(-) create mode 100644 src/SchemaGenerator/SchemaInspector/TypeSubQueryGenerator.php create mode 100644 tests/TypeSubQueryGeneratorTest.php diff --git a/.gitignore b/.gitignore index ced3610..73b9821 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ schema_object/* !schema_object/.gitkeep .phpunit.result.cache /build/ -composer.lock \ No newline at end of file +composer.lock +composer.phar diff --git a/README.md b/README.md index d208d41..cb619f9 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,13 @@ php vendor/bin/generate_schema_objects \ --namespace "Vendor\Custom\Namespace" ``` +## Notes + +### Dealing with `PHP Fatal error: Uncaught RuntimeException: Reached the limit of nesting in type info in ...SchemaGenerator\SchemaClassGenerator.php` +You might encounter this error when the schema is deeply nested for the types. +You can increase the depth of "type / ofType" by providing the `-D` option +(or `--type-of-type-depth`) when running the command. The default value is 4. + # Usage In all the examples below I'm going to use the super cool public Pokemon GraphQL API as an illustration. diff --git a/bin/generate_schema_objects b/bin/generate_schema_objects index 5c51f53..5b837c9 100755 --- a/bin/generate_schema_objects +++ b/bin/generate_schema_objects @@ -7,14 +7,24 @@ use GraphQL\SchemaGenerator\SchemaClassGenerator; $autoLoadFiles = [__DIR__ . '/../vendor/autoload.php', __DIR__ . '/../../../autoload.php']; -function readConfig() +/** + * @return array{ + * 0: string, + * 1: string, + * 2: array, + * 3: string, + * 4: int, + * } + */ +function readConfig(): array { $shortOptions = implode("", [ 'u:', 'h:', 'v:', 'd:', - 'n:' + 'n:', + 'D:', ]); $longOptions = [ @@ -23,6 +33,7 @@ function readConfig() 'authorization-header-value:', 'directory:', 'namespace:', + 'type-of-type-depth:', ]; $options = getopt($shortOptions, $longOptions); @@ -32,6 +43,7 @@ function readConfig() $authHeaderName = $options['authorization-header-name'] ?? $options['h'] ?? readline('Authorization header name: '); $authHeaderValue = $options['authorization-header-value'] ?? $options['v'] ?? readline('Authorization header value: '); $namespace = $options['n'] ?? $options['namespace'] ?? trim(readline('Custom namespace (optional): ')); + $typeOfTypeDepth = $options['type-of-type-depth'] ?? $options['D'] ?? 4; $authHeaders = []; @@ -39,7 +51,13 @@ function readConfig() $authHeaders = [$authHeaderName => $authHeaderValue]; } - return [$url, empty($customWriteDir) ? "" : $customWriteDir, $authHeaders, empty($namespace) ? ObjectBuilderInterface::DEFAULT_NAMESPACE : $namespace]; + return [ + $url, + empty($customWriteDir) ? "" : $customWriteDir, + $authHeaders, + empty($namespace) ? ObjectBuilderInterface::DEFAULT_NAMESPACE: $namespace, + (int) $typeOfTypeDepth, + ]; } // Require autoload.php depending on environment @@ -55,16 +73,17 @@ if (!$autoLoadFound) { throw new RuntimeException('Could not find vendor/autoload.php'); } -[$endpointUrl, $customWriteDir, $authHeaders, $namespace] = readConfig(); +[$endpointUrl, $customWriteDir, $authHeaders, $namespace, $typeOfTypeDepth] = readConfig(); $client = new Client($endpointUrl, $authHeaders); $scanner = new SchemaClassGenerator($client, $customWriteDir, $namespace); print "-------------------------------------------\n"; print "Generating schema objects from schema types\n"; +print "Using \"type / ofType\" depth: $typeOfTypeDepth\n"; print "-------------------------------------------\n"; -$scanner->generateRootQueryObject(); +$scanner->generateRootQueryObject($typeOfTypeDepth); print "-------------------------------------------\n"; print "Schema objects generation complete\n"; diff --git a/src/SchemaGenerator/SchemaClassGenerator.php b/src/SchemaGenerator/SchemaClassGenerator.php index e4920ad..af17027 100644 --- a/src/SchemaGenerator/SchemaClassGenerator.php +++ b/src/SchemaGenerator/SchemaClassGenerator.php @@ -10,6 +10,7 @@ use GraphQL\SchemaGenerator\CodeGenerator\ObjectBuilderInterface; use GraphQL\SchemaGenerator\CodeGenerator\QueryObjectClassBuilder; use GraphQL\SchemaGenerator\CodeGenerator\UnionObjectBuilder; +use GraphQL\SchemaGenerator\SchemaInspector\TypeSubQueryGenerator; use GraphQL\SchemaObject\QueryObject; use GraphQL\Util\StringLiteralFormatter; use RuntimeException; @@ -55,7 +56,7 @@ class SchemaClassGenerator */ public function __construct(Client $client, string $writeDir = '', string $namespace = ObjectBuilderInterface::DEFAULT_NAMESPACE) { - $this->schemaInspector = new SchemaInspector($client); + $this->schemaInspector = new SchemaInspector($client, new TypeSubQueryGenerator()); $this->generatedObjects = []; $this->writeDir = $writeDir; $this->generationNamespace = $namespace; @@ -63,11 +64,13 @@ public function __construct(Client $client, string $writeDir = '', string $names } /** + * @param int $typeOfTypeDepth + * * @return bool */ - public function generateRootQueryObject(): bool + public function generateRootQueryObject(int $typeOfTypeDepth = 4): bool { - $objectArray = $this->schemaInspector->getQueryTypeSchema(); + $objectArray = $this->schemaInspector->getQueryTypeSchema($typeOfTypeDepth); $rootObjectName = QueryObject::ROOT_QUERY_OBJECT_NAME; $queryTypeName = $objectArray['name']; //$rootObjectDescr = $objectArray['description']; @@ -79,7 +82,7 @@ public function generateRootQueryObject(): bool $this->generatedObjects[$queryTypeName] = true; $queryObjectBuilder = new QueryObjectClassBuilder($this->writeDir, $rootObjectName, $this->generationNamespace); - $this->appendQueryObjectFields($queryObjectBuilder, $rootObjectName, $objectArray['fields']); + $this->appendQueryObjectFields($queryObjectBuilder, $rootObjectName, $objectArray['fields'], $typeOfTypeDepth); $queryObjectBuilder->build(); return true; @@ -91,8 +94,9 @@ public function generateRootQueryObject(): bool * @param QueryObjectClassBuilder $queryObjectBuilder * @param string $currentTypeName * @param array $fieldsArray + * @param int $typeOfTypeDepth */ - private function appendQueryObjectFields(QueryObjectClassBuilder $queryObjectBuilder, string $currentTypeName, array $fieldsArray) + private function appendQueryObjectFields(QueryObjectClassBuilder $queryObjectBuilder, string $currentTypeName, array $fieldsArray, int $typeOfTypeDepth = 4): void { foreach ($fieldsArray as $fieldArray) { $name = $fieldArray['name']; @@ -110,7 +114,7 @@ private function appendQueryObjectFields(QueryObjectClassBuilder $queryObjectBui } else { // Generate nested type object if it wasn't generated - $objectGenerated = $this->generateObject($typeName, $typeKind); + $objectGenerated = $this->generateObject($typeName, $typeKind, $typeOfTypeDepth); if ($objectGenerated) { // Generate nested type arguments object if it wasn't generated @@ -129,20 +133,21 @@ private function appendQueryObjectFields(QueryObjectClassBuilder $queryObjectBui /** * @param string $objectName * @param string $objectKind + * @param int $typeOfTypeDepth * * @return bool */ - protected function generateObject(string $objectName, string $objectKind): bool + protected function generateObject(string $objectName, string $objectKind, int $typeOfTypeDepth = 4): bool { switch ($objectKind) { case FieldTypeKindEnum::OBJECT: - return $this->generateQueryObject($objectName); + return $this->generateQueryObject($objectName, $typeOfTypeDepth); case FieldTypeKindEnum::INPUT_OBJECT: - return $this->generateInputObject($objectName); + return $this->generateInputObject($objectName, $typeOfTypeDepth); case FieldTypeKindEnum::ENUM_OBJECT: return $this->generateEnumObject($objectName); case FieldTypeKindEnum::UNION_OBJECT: - return $this->generateUnionObject($objectName); + return $this->generateUnionObject($objectName, $typeOfTypeDepth); default: print "Couldn't generate type $objectName: generating $objectKind kind is not supported yet" . PHP_EOL; return false; @@ -151,21 +156,22 @@ protected function generateObject(string $objectName, string $objectKind): bool /** * @param string $objectName + * @param int $typeOfTypeDepth * * @return bool */ - protected function generateQueryObject(string $objectName): bool + protected function generateQueryObject(string $objectName, int $typeOfTypeDepth = 4): bool { if (array_key_exists($objectName, $this->generatedObjects)) { return true; } $this->generatedObjects[$objectName] = true; - $objectArray = $this->schemaInspector->getObjectSchema($objectName); + $objectArray = $this->schemaInspector->getObjectSchema($objectName, $typeOfTypeDepth); $objectName = $objectArray['name']; $objectBuilder = new QueryObjectClassBuilder($this->writeDir, $objectName, $this->generationNamespace); - $this->appendQueryObjectFields($objectBuilder, $objectName, $objectArray['fields']); + $this->appendQueryObjectFields($objectBuilder, $objectName, $objectArray['fields'], $typeOfTypeDepth); $objectBuilder->build(); return true; @@ -173,17 +179,18 @@ protected function generateQueryObject(string $objectName): bool /** * @param string $objectName + * @param int $typeOfTypeDepth * * @return bool */ - protected function generateInputObject(string $objectName): bool + protected function generateInputObject(string $objectName, int $typeOfTypeDepth = 4): bool { if (array_key_exists($objectName, $this->generatedObjects)) { return true; } $this->generatedObjects[$objectName] = true; - $objectArray = $this->schemaInspector->getInputObjectSchema($objectName); + $objectArray = $this->schemaInspector->getInputObjectSchema($objectName, $typeOfTypeDepth); $objectName = $objectArray['name']; $objectBuilder = new InputObjectClassBuilder($this->writeDir, $objectName, $this->generationNamespace); @@ -195,7 +202,7 @@ protected function generateInputObject(string $objectName): bool $objectGenerated = true; if ($typeKind !== FieldTypeKindEnum::SCALAR) { - $objectGenerated = $this->generateObject($typeName, $typeKind); + $objectGenerated = $this->generateObject($typeName, $typeKind, $typeOfTypeDepth); } if ($objectGenerated) { @@ -245,10 +252,11 @@ protected function generateEnumObject(string $objectName): bool /** * @param string $objectName + * @param int $typeOfTypeDepth * * @return bool */ - protected function generateUnionObject(string $objectName): bool + protected function generateUnionObject(string $objectName, int $typeOfTypeDepth = 4): bool { if (array_key_exists($objectName, $this->generatedObjects)) { return true; @@ -261,7 +269,7 @@ protected function generateUnionObject(string $objectName): bool $objectBuilder = new UnionObjectBuilder($this->writeDir, $objectName, $this->generationNamespace); foreach ($objectArray['possibleTypes'] as $possibleType) { - $this->generateObject($possibleType['name'], $possibleType['kind']); + $this->generateObject($possibleType['name'], $possibleType['kind'], $typeOfTypeDepth); $objectBuilder->addPossibleType($possibleType['name']); } $objectBuilder->build(); @@ -272,10 +280,11 @@ protected function generateUnionObject(string $objectName): bool /** * @param string $argsObjectName * @param array $arguments + * @param int $typeOfTypeDepth * * @return bool */ - protected function generateArgumentsObject(string $argsObjectName, array $arguments): bool + protected function generateArgumentsObject(string $argsObjectName, array $arguments, int $typeOfTypeDepth = 4): bool { if (array_key_exists($argsObjectName, $this->generatedObjects)) { return true; @@ -293,7 +302,7 @@ protected function generateArgumentsObject(string $argsObjectName, array $argume $objectGenerated = true; if ($typeKind !== FieldTypeKindEnum::SCALAR) { - $objectGenerated = $this->generateObject($typeName, $typeKind); + $objectGenerated = $this->generateObject($typeName, $typeKind, $typeOfTypeDepth); } if ($objectGenerated) { diff --git a/src/SchemaGenerator/SchemaInspector.php b/src/SchemaGenerator/SchemaInspector.php index 424ccc7..834a0ad 100644 --- a/src/SchemaGenerator/SchemaInspector.php +++ b/src/SchemaGenerator/SchemaInspector.php @@ -3,6 +3,7 @@ namespace GraphQL\SchemaGenerator; use GraphQL\Client; +use GraphQL\SchemaGenerator\SchemaInspector\TypeSubQueryGenerator; /** * Class SchemaInspector @@ -13,50 +14,51 @@ */ class SchemaInspector { - private const TYPE_SUB_QUERY = <<client = $client; + $this->typeSubQueryGenerate = ($typeSubQueryGenerate ?? new TypeSubQueryGenerator()); + + // End __construct(). } + /** + * @param integer $typeOfTypeDepth How deep it should go + * + * @return string + */ + private function getTypeSubQuery(int $typeOfTypeDepth=4): string + { + return $this->typeSubQueryGenerate->getSubTypeQuery($typeOfTypeDepth); + + // End getTypeSubQuery(). + } + + + /** + * @param integer $typeOfTypeDepth How deep it should go + * * @return array */ - public function getQueryTypeSchema(): array + public function getQueryTypeSchema(int $typeOfTypeDepth=4): array { $schemaQuery = "{ __schema{ @@ -69,12 +71,12 @@ public function getQueryTypeSchema(): array description isDeprecated deprecationReason - " . static::TYPE_SUB_QUERY . " + ".$this->getTypeSubQuery($typeOfTypeDepth)." args{ name description defaultValue - " . static::TYPE_SUB_QUERY . " + ".$this->getTypeSubQuery($typeOfTypeDepth)." } } } @@ -83,14 +85,18 @@ public function getQueryTypeSchema(): array $response = $this->client->runRawQuery($schemaQuery, true); return $response->getData()['__schema']['queryType']; + + // End getQueryTypeSchema(). } + /** - * @param string $objectName + * @param string $objectName The name of the object + * @param integer $typeOfTypeDepth How deep it should go * * @return array */ - public function getObjectSchema(string $objectName): array + public function getObjectSchema(string $objectName, int $typeOfTypeDepth=4): array { $schemaQuery = "{ __type(name: \"$objectName\") { @@ -101,12 +107,12 @@ public function getObjectSchema(string $objectName): array description isDeprecated deprecationReason - " . static::TYPE_SUB_QUERY . " + ".$this->getTypeSubQuery($typeOfTypeDepth)." args{ name description defaultValue - " . static::TYPE_SUB_QUERY . " + ".$this->getTypeSubQuery($typeOfTypeDepth)." } } } @@ -114,14 +120,18 @@ public function getObjectSchema(string $objectName): array $response = $this->client->runRawQuery($schemaQuery, true); return $response->getData()['__type']; + + // End getObjectSchema(). } + /** - * @param string $objectName + * @param string $objectName The name of the object + * @param integer $typeOfTypeDepth How deep it should go * * @return array */ - public function getInputObjectSchema(string $objectName): array + public function getInputObjectSchema(string $objectName, int $typeOfTypeDepth=4): array { $schemaQuery = "{ __type(name: \"$objectName\") { @@ -131,17 +141,20 @@ public function getInputObjectSchema(string $objectName): array name description defaultValue - " . static::TYPE_SUB_QUERY . " + ".$this->getTypeSubQuery($typeOfTypeDepth)." } } }"; $response = $this->client->runRawQuery($schemaQuery, true); return $response->getData()['__type']; + + // End getInputObjectSchema(). } + /** - * @param string $objectName + * @param string $objectName The name of the object * * @return array */ @@ -160,10 +173,13 @@ enumValues { $response = $this->client->runRawQuery($schemaQuery, true); return $response->getData()['__type']; + + // End getEnumObjectSchema(). } + /** - * @param string $objectName + * @param string $objectName The name of the object * * @return array */ @@ -182,5 +198,9 @@ public function getUnionObjectSchema(string $objectName): array $response = $this->client->runRawQuery($schemaQuery, true); return $response->getData()['__type']; + + // End getUnionObjectSchema(). } + + } diff --git a/src/SchemaGenerator/SchemaInspector/TypeSubQueryGenerator.php b/src/SchemaGenerator/SchemaInspector/TypeSubQueryGenerator.php new file mode 100644 index 0000000..2d164ac --- /dev/null +++ b/src/SchemaGenerator/SchemaInspector/TypeSubQueryGenerator.php @@ -0,0 +1,75 @@ + + */ + private $ofTypes = []; + + + /** + * @param integer $typeOfTypeDepth How deep it should go + * + * @return string + */ + public function getSubTypeQuery(int $typeOfTypeDepth=4): string + { + $ofType = $this->getOfTypeSubQuery($typeOfTypeDepth); + + return "type{ + name + kind + description{$ofType} +}"; + + // End getSubTypeQuery(). + } + + + /** + * @param integer $depth How deep it should go + * + * @return string + */ + private function getOfTypeSubQuery($depth): string + { + if (isset($this->ofTypes[$depth]) === false) { + $this->ofTypes[$depth] = $this->generateOfTypeSubQuery($depth, ' '); + } + + return $this->ofTypes[$depth]; + + // End getOfTypeSubQuery(). + } + + + /** + * @param integer $depth How deep it should go + * @param string $indent Current indentation + * + * @return string + */ + private function generateOfTypeSubQuery(int $depth, string $indent): string + { + if ($depth <= 0) { + return ''; + } + + $subQuery = $this->generateOfTypeSubQuery(($depth - 1), $indent.' '); + + return " +{$indent}ofType{ +{$indent} name +{$indent} kind{$subQuery} +{$indent}}"; + + // End generateOfTypeSubQuery(). + } + +} diff --git a/tests/SchemaClassGeneratorTest.php b/tests/SchemaClassGeneratorTest.php index bdd95bf..15764f9 100644 --- a/tests/SchemaClassGeneratorTest.php +++ b/tests/SchemaClassGeneratorTest.php @@ -909,12 +909,12 @@ public function __construct( parent::__construct($client, $writeDir, 'GraphQL\\Tests\\SchemaObject'); } - public function generateRootQueryObject(): bool + public function generateRootQueryObject(int $typeOfTypeDepth = 4): bool { return parent::generateRootQueryObject(); } - public function generateQueryObject(string $objectName): bool + public function generateQueryObject(string $objectName, int $typeOfTypeDepth = 4): bool { return parent::generateQueryObject($objectName); } @@ -924,17 +924,17 @@ public function generateEnumObject(string $objectName): bool return parent::generateEnumObject($objectName); } - public function generateInputObject(string $objectName): bool + public function generateInputObject(string $objectName, int $typeOfTypeDepth = 4): bool { return parent::generateInputObject($objectName); } - public function generateObject(string $objectName, string $objectKind): bool + public function generateObject(string $objectName, string $objectKind, int $typeOfTypeDepth = 4): bool { return parent::generateObject($objectName, $objectKind); } - public function generateArgumentsObject(string $argsObjectName, array $arguments): bool + public function generateArgumentsObject(string $argsObjectName, array $arguments, int $typeOfTypeDepth = 4): bool { return parent::generateArgumentsObject($argsObjectName, $arguments); } @@ -944,7 +944,7 @@ public function getTypeInfo(array $dataArray): array return parent::getTypeInfo($dataArray); } - public function generateUnionObject(string $objectName): bool + public function generateUnionObject(string $objectName, int $typeOfTypeDepth = 4): bool { return parent::generateUnionObject($objectName); } diff --git a/tests/TypeSubQueryGeneratorTest.php b/tests/TypeSubQueryGeneratorTest.php new file mode 100644 index 0000000..de4cbd7 --- /dev/null +++ b/tests/TypeSubQueryGeneratorTest.php @@ -0,0 +1,77 @@ +getSubTypeQuery(); + + $expected = <<assertEquals($expected, $actual); + + // End testItShouldGenerateSubQueryWith4OfTypeLevelsByDefault(). + } + + + /** + * @covers \GraphQL\SchemaGenerator\SchemaInspector\TypeSubQueryGenerator::getSubTypeQuery + * + * @return void + */ + public function testItShouldGenerateSubQueryWithoutOfTypeForZeroDepth(): void + { + $sut = new TypeSubQueryGenerator(); + + $actual = $sut->getSubTypeQuery(0); + + $expected = <<assertEquals($expected, $actual); + + // End testItShouldGenerateSubQueryWithoutOfTypeForZeroDepth(). + } + + +}