From 16c108260fdaefd40cc9805319d2127512b41767 Mon Sep 17 00:00:00 2001 From: Artem Schander Date: Thu, 6 Nov 2025 11:00:40 +0100 Subject: [PATCH 1/2] Fix exponential time complexity in AliasArguments with circular type references MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Problem:** `getAliasesInFields()` traverses ALL possible fields in the type schema, not just fields present in the request data. With circular type references (e.g., PageInput ↔ SectionInput) and a depth limit calculated from request data, this causes exponential traversal. **Evidence:** - Mutations taking 11+ seconds instead of milliseconds - Logging showed 6 million recursive calls before timeout - Depth reaching 12-13 levels with circular paths **Solution:** Only traverse fields that exist in the actual request data. This prevents exponential explosion while preserving full alias functionality. **Performance:** - Before: 11+ seconds per mutation - After: ~150ms per mutation - Improvement: 98.7% reduction **Testing:** - Tested with deeply nested input types (11 levels) - Tested with circular type references - Verified alias functionality preserved (deprecated field name mapping) - Verified legitimate nested types work (e.g., Page->translations array) --- src/Support/AliasArguments/AliasArguments.php | 48 +++++++++++++++++-- 1 file changed, 44 insertions(+), 4 deletions(-) diff --git a/src/Support/AliasArguments/AliasArguments.php b/src/Support/AliasArguments/AliasArguments.php index a38b71c3..18803502 100644 --- a/src/Support/AliasArguments/AliasArguments.php +++ b/src/Support/AliasArguments/AliasArguments.php @@ -32,7 +32,7 @@ public function __construct(array $queryArguments, array $requestArguments) public function get(): array { - $pathsWithAlias = $this->getAliasesInFields($this->queryArguments); + $pathsWithAlias = $this->getAliasesInFields($this->queryArguments, $this->requestArguments); return (new ArrayKeyChange())->modify($this->requestArguments, $pathsWithAlias); } @@ -59,7 +59,19 @@ protected function getArrayDepth(array $array): int return $maxDepth; } - protected function getAliasesInFields(array $fields, $prefix = ''): array + /** + * Get aliases from fields, only traversing fields present in request data. + * + * This prevents exponential time complexity with circular type references by only + * exploring the actual data structure sent by the client, not all possible fields + * in the type schema. + * + * @param array $fields Type field definitions + * @param array|null $requestData Actual request data at this level (null for initial call) + * @param string $prefix Path prefix for nested fields + * @return array Map of field paths to their aliases + */ + protected function getAliasesInFields(array $fields, ?array $requestData = null, string $prefix = ''): array { // checks for traversal beyond the max depth // this scenario occurs in types with recursive relations @@ -69,6 +81,12 @@ protected function getAliasesInFields(array $fields, $prefix = ''): array $pathAndAlias = []; foreach ($fields as $name => $arg) { + // KEY FIX: Skip fields not present in actual request data + // This prevents exponential explosion with circular type references + if ($requestData !== null && !array_key_exists($name, $requestData)) { + continue; + } + $type = null; // $arg is either an array DSL notation or an InputObjectField @@ -91,7 +109,8 @@ protected function getAliasesInFields(array $fields, $prefix = ''): array $pathAndAlias[$newPrefix] = $alias; } - if ($this->isWrappedInList($type)) { + $isWrappedInList = $this->isWrappedInList($type); + if ($isWrappedInList) { $newPrefix .= '.*'; } @@ -101,7 +120,28 @@ protected function getAliasesInFields(array $fields, $prefix = ''): array continue; } - $pathAndAlias = $pathAndAlias + $this->getAliasesInFields($type->getFields(), $newPrefix); + // Get the actual data at this field (if requestData provided) + $fieldData = $requestData !== null ? ($requestData[$name] ?? null) : null; + + // If it's a list, process each item + if ($isWrappedInList && is_array($fieldData)) { + foreach ($fieldData as $item) { + if (is_array($item)) { + $pathAndAlias = $pathAndAlias + $this->getAliasesInFields( + $type->getFields(), + $item, + $newPrefix + ); + } + } + } elseif ($fieldData !== null && is_array($fieldData)) { + // Single object + $pathAndAlias = $pathAndAlias + $this->getAliasesInFields( + $type->getFields(), + $fieldData, + $newPrefix + ); + } } return $pathAndAlias; From a45b686b5028795b08d36e6dd0bd4faa471cdba4 Mon Sep 17 00:00:00 2001 From: Artem Schander Date: Thu, 6 Nov 2025 11:12:21 +0100 Subject: [PATCH 2/2] Fix PHPStan type annotations and code style - Add proper type annotation for $requestData parameter - Fix code style to match project standards --- src/Support/AliasArguments/AliasArguments.php | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Support/AliasArguments/AliasArguments.php b/src/Support/AliasArguments/AliasArguments.php index 18803502..8c978cd2 100644 --- a/src/Support/AliasArguments/AliasArguments.php +++ b/src/Support/AliasArguments/AliasArguments.php @@ -61,14 +61,15 @@ protected function getArrayDepth(array $array): int /** * Get aliases from fields, only traversing fields present in request data. - * + * * This prevents exponential time complexity with circular type references by only * exploring the actual data structure sent by the client, not all possible fields * in the type schema. * - * @param array $fields Type field definitions - * @param array|null $requestData Actual request data at this level (null for initial call) + * @param array $fields Type field definitions + * @param array|null $requestData Actual request data at this level (null for initial call) * @param string $prefix Path prefix for nested fields + * * @return array Map of field paths to their aliases */ protected function getAliasesInFields(array $fields, ?array $requestData = null, string $prefix = ''): array @@ -83,7 +84,7 @@ protected function getAliasesInFields(array $fields, ?array $requestData = null, foreach ($fields as $name => $arg) { // KEY FIX: Skip fields not present in actual request data // This prevents exponential explosion with circular type references - if ($requestData !== null && !array_key_exists($name, $requestData)) { + if ($requestData !== null && ! array_key_exists($name, $requestData)) { continue; } @@ -101,7 +102,7 @@ protected function getAliasesInFields(array $fields, ?array $requestData = null, continue; } - $newPrefix = $prefix ? $prefix . '.' . $name : $name; + $newPrefix = $prefix ? $prefix.'.'.$name : $name; $alias = $arg->config['alias'] ?? $arg->alias ?? null; @@ -116,7 +117,7 @@ protected function getAliasesInFields(array $fields, ?array $requestData = null, $type = $this->getWrappedType($type); - if (!($type instanceof InputObjectType)) { + if (! ($type instanceof InputObjectType)) { continue; }