From 1f50c671039a611cfb48424160329288e47b29a2 Mon Sep 17 00:00:00 2001 From: Thiago Matos Date: Wed, 1 Jul 2026 13:44:35 -0400 Subject: [PATCH 1/2] feat: add schema selection helper --- src/GraphQL/Client.php | 7 +- src/GraphQL/SchemaSelection.php | 93 +++++++++++++++++++++++++++ tests/GraphQL/GenericClientTest.php | 32 +++++++++ tests/GraphQL/SchemaSelectionTest.php | 47 ++++++++++++++ 4 files changed, 178 insertions(+), 1 deletion(-) create mode 100644 src/GraphQL/SchemaSelection.php create mode 100644 tests/GraphQL/SchemaSelectionTest.php diff --git a/src/GraphQL/Client.php b/src/GraphQL/Client.php index b46bc5b..3d010b9 100644 --- a/src/GraphQL/Client.php +++ b/src/GraphQL/Client.php @@ -293,7 +293,12 @@ private function getDefaultSelection(?string $typeName): array private function unwrapSingleSelection($result, array $selection) { - if (!is_array($result) || count($selection) !== 1 || !is_string($selection[0])) { + if ( + !is_array($result) + || count($selection) !== 1 + || !array_key_exists(0, $selection) + || !is_string($selection[0]) + ) { return $result; } diff --git a/src/GraphQL/SchemaSelection.php b/src/GraphQL/SchemaSelection.php new file mode 100644 index 0000000..787a07c --- /dev/null +++ b/src/GraphQL/SchemaSelection.php @@ -0,0 +1,93 @@ +getFields() as $field) { + $fieldSelection = self::fieldSelection($field, $depth, $seen); + + if ($fieldSelection === null) { + continue; + } + + if (is_array($fieldSelection)) { + $selection[$field->getName()] = $fieldSelection; + continue; + } + + $selection[] = $fieldSelection; + } + + return $selection; + } + + /** + * @return string|array|null + */ + private static function fieldSelection(FieldDefinition $field, int $depth, array $seen) + { + $fieldName = $field->getName(); + $nestedSchemaClass = self::schemaClass($field->getType()->baseName()); + + if (!class_exists($nestedSchemaClass)) { + return $fieldName; + } + + if ($depth === 1) { + return null; + } + + $nestedSelection = self::selectionForSchema($nestedSchemaClass, $depth - 1, $seen); + + if ($nestedSelection === []) { + $nestedSelection = self::idSelection($nestedSchemaClass); + } + + return $nestedSelection === [] ? null : $nestedSelection; + } + + private static function idSelection(string $schemaClass): array + { + foreach ($schemaClass::definition()->getFields() as $field) { + if (substr($field->getName(), -2) === 'Id') { + return [$field->getName()]; + } + } + + return []; + } + + private static function schemaClass(?string $schema): string + { + if ($schema === null) { + return ''; + } + + if (strpos($schema, '\\') !== false) { + return $schema; + } + + return '\\ThothApi\\GraphQL\\Schemas\\' . ($schema === 'Abstract' ? 'GraphQLAbstract' : $schema); + } +} diff --git a/tests/GraphQL/GenericClientTest.php b/tests/GraphQL/GenericClientTest.php index dbb8cbd..1d0cab5 100644 --- a/tests/GraphQL/GenericClientTest.php +++ b/tests/GraphQL/GenericClientTest.php @@ -18,6 +18,7 @@ use ThothApi\GraphQL\Inputs\NewWork; use ThothApi\GraphQL\OperationRequest; use ThothApi\GraphQL\Schemas\Imprint; +use ThothApi\GraphQL\Schemas\Publication; use ThothApi\GraphQL\Schemas\Publisher; use ThothApi\GraphQL\Schemas\Work; @@ -200,6 +201,37 @@ public function testItHydratesNestedGeneratedSchemas(): void $this->assertSame('ACME Press', $work->getImprint()->getPublisher()->getPublisherName()); } + public function testItHydratesSingleAssociativeSelectionWithoutUnwrapping(): void + { + $mockHandler = new MockHandler([ + new Response(200, [], json_encode([ + 'data' => [ + 'work' => [ + 'publications' => [ + [ + 'publicationId' => 'publication-1', + 'publicationType' => 'PAPERBACK', + ], + ], + ], + ], + ])), + ]); + + $client = new Client(['handler' => HandlerStack::create($mockHandler)]); + + $work = $client->work('work-1', [ + 'publications' => [ + 'publicationId', + 'publicationType', + ], + ]); + + $this->assertInstanceOf(Work::class, $work); + $this->assertContainsOnlyInstancesOf(Publication::class, $work->getPublications()); + $this->assertSame('publication-1', $work->getPublications()[0]->getPublicationId()); + } + public function testGeneratedSchemaObjectsExposeSettersAndArrayData(): void { $work = new Work(); diff --git a/tests/GraphQL/SchemaSelectionTest.php b/tests/GraphQL/SchemaSelectionTest.php new file mode 100644 index 0000000..75fb867 --- /dev/null +++ b/tests/GraphQL/SchemaSelectionTest.php @@ -0,0 +1,47 @@ +assertContains('workId', $selection); + $this->assertContains('fullTitle', $selection); + $this->assertNotContains('imprint', $selection); + } + + public function testItBuildsNestedSelectionForSchema(): void + { + $selection = SchemaSelection::for('Work', 2); + + $this->assertArrayHasKey('imprint', $selection); + $this->assertContains('imprintId', $selection['imprint']); + $this->assertContains('imprintName', $selection['imprint']); + $this->assertNotContains('publisher', $selection['imprint']); + } + + public function testItBuildsSelectionAcceptedByGeneratedQueries(): void + { + $query = WorksQuery::operation(['limit' => 1], SchemaSelection::for('Work', 2))->toGraphQL(); + + $this->assertStringContainsString('works(limit: $limit) {', $query); + $this->assertStringContainsString('fullTitle', $query); + $this->assertStringContainsString('imprint {', $query); + $this->assertStringContainsString('imprintId', $query); + } + + public function testItRejectsInvalidDepth(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Depth must be greater than zero.'); + + SchemaSelection::for('Work', 0); + } +} From 13f1dcd8e1c5738290a416e2b8b035cd4bb99c3b Mon Sep 17 00:00:00 2001 From: Thiago Matos Date: Wed, 1 Jul 2026 14:30:29 -0400 Subject: [PATCH 2/2] fix: update vulnerable dependencies --- composer.lock | 86 +++++++++++++++++++++++++++++---------------------- 1 file changed, 49 insertions(+), 37 deletions(-) diff --git a/composer.lock b/composer.lock index 2bb7cc2..5261dcd 100644 --- a/composer.lock +++ b/composer.lock @@ -8,25 +8,26 @@ "packages": [ { "name": "guzzlehttp/guzzle", - "version": "7.9.2", + "version": "7.13.1", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "d281ed313b989f213357e3be1a179f02196ac99b" + "reference": "55901a76dfd2006a0cc012b9e3c5b487f796478d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/d281ed313b989f213357e3be1a179f02196ac99b", - "reference": "d281ed313b989f213357e3be1a179f02196ac99b", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/55901a76dfd2006a0cc012b9e3c5b487f796478d", + "reference": "55901a76dfd2006a0cc012b9e3c5b487f796478d", "shasum": "" }, "require": { "ext-json": "*", - "guzzlehttp/promises": "^1.5.3 || ^2.0.3", - "guzzlehttp/psr7": "^2.7.0", + "guzzlehttp/promises": "^2.5", + "guzzlehttp/psr7": "^2.12.3", "php": "^7.2.5 || ^8.0", "psr/http-client": "^1.0", - "symfony/deprecation-contracts": "^2.2 || ^3.0" + "symfony/deprecation-contracts": "^2.5 || ^3.0", + "symfony/polyfill-php80": "^1.25" }, "provide": { "psr/http-client-implementation": "1.0" @@ -35,8 +36,9 @@ "bamarni/composer-bin-plugin": "^1.8.2", "ext-curl": "*", "guzzle/client-integration-tests": "3.0.2", + "guzzlehttp/test-server": "^0.6", "php-http/message-factory": "^1.1", - "phpunit/phpunit": "^8.5.39 || ^9.6.20", + "phpunit/phpunit": "^8.5.52 || ^9.6.34", "psr/log": "^1.1 || ^2.0 || ^3.0" }, "suggest": { @@ -114,7 +116,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.9.2" + "source": "https://github.com/guzzle/guzzle/tree/7.13.1" }, "funding": [ { @@ -130,28 +132,29 @@ "type": "tidelift" } ], - "time": "2024-07-24T11:22:20+00:00" + "time": "2026-06-29T20:14:18+00:00" }, { "name": "guzzlehttp/promises", - "version": "2.0.4", + "version": "2.5.0", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "f9c436286ab2892c7db7be8c8da4ef61ccf7b455" + "reference": "4360e982f87f5f258bf872d094647791db2f4c8e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/f9c436286ab2892c7db7be8c8da4ef61ccf7b455", - "reference": "f9c436286ab2892c7db7be8c8da4ef61ccf7b455", + "url": "https://api.github.com/repos/guzzle/promises/zipball/4360e982f87f5f258bf872d094647791db2f4c8e", + "reference": "4360e982f87f5f258bf872d094647791db2f4c8e", "shasum": "" }, "require": { - "php": "^7.2.5 || ^8.0" + "php": "^7.2.5 || ^8.0", + "symfony/deprecation-contracts": "^2.5 || ^3.0" }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "phpunit/phpunit": "^8.5.39 || ^9.6.20" + "phpunit/phpunit": "^8.5.52 || ^9.6.34" }, "type": "library", "extra": { @@ -197,7 +200,7 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/2.0.4" + "source": "https://github.com/guzzle/promises/tree/2.5.0" }, "funding": [ { @@ -213,20 +216,20 @@ "type": "tidelift" } ], - "time": "2024-10-17T10:06:22+00:00" + "time": "2026-06-02T12:23:43+00:00" }, { "name": "guzzlehttp/psr7", - "version": "2.11.1", + "version": "2.12.3", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "640e2897bbee822dbc8af761d49e1a29b1f2a6b1" + "reference": "7ec62dc3f44aa218487dbed81a9bf9bc647be55d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/640e2897bbee822dbc8af761d49e1a29b1f2a6b1", - "reference": "640e2897bbee822dbc8af761d49e1a29b1f2a6b1", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/7ec62dc3f44aa218487dbed81a9bf9bc647be55d", + "reference": "7ec62dc3f44aa218487dbed81a9bf9bc647be55d", "shasum": "" }, "require": { @@ -235,7 +238,7 @@ "psr/http-message": "^1.1 || ^2.0", "ralouphie/getallheaders": "^3.0", "symfony/deprecation-contracts": "^2.5 || ^3.0", - "symfony/polyfill-php80": "^1.24" + "symfony/polyfill-php80": "^1.25" }, "provide": { "psr/http-factory-implementation": "1.0", @@ -316,7 +319,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.11.1" + "source": "https://github.com/guzzle/psr7/tree/2.12.3" }, "funding": [ { @@ -332,7 +335,7 @@ "type": "tidelift" } ], - "time": "2026-06-12T21:50:12+00:00" + "time": "2026-06-23T15:21:08+00:00" }, { "name": "psr/http-client", @@ -4298,16 +4301,16 @@ }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.31.0", + "version": "v1.38.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "3833d7255cc303546435cb650316bff708a1c75c" + "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", - "reference": "3833d7255cc303546435cb650316bff708a1c75c", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/2d446c214bdbe5b71bde5011b060a05fece3ae6b", + "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b", "shasum": "" }, "require": { @@ -4359,7 +4362,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.38.0" }, "funding": [ { @@ -4370,28 +4373,33 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2026-05-25T13:48:31+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.31.0", + "version": "v1.38.2", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" + "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", - "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", + "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", "shasum": "" }, "require": { + "ext-iconv": "*", "php": ">=7.2" }, "provide": { @@ -4439,7 +4447,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.38.2" }, "funding": [ { @@ -4450,12 +4458,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2026-05-27T06:59:30+00:00" }, { "name": "symfony/polyfill-php73",