Repository navigation
fix: Undefined input handling (refuse explicit null, default to Undefined) and list-formatted description metadata - #838
Merged
oojacoboo merged 2 commits intoOct 10, 2026
Conversation
…ined) and list-formatted description metadata - An explicit null for an argument or input field whose receiving PHP type contains Undefined but not null is refused with NullArgumentException, naming the field, instead of failing with a TypeError - Undefined takes precedence over the implicit null default, so an omitted T|null|Undefined field without a PHP default resolves to Undefined::VALUE and prints no `= null`; omitted fields without a default are hydrated with Undefined unless the constructor assigned them - Undefined-typed arguments and input fields say in their description whether null is accepted, controlled by SchemaFactory::setUndefinedDescriptionsEnabled() - Description metadata (Cost, the Undefined sentence, middleware items) renders as a Markdown list via withAddedDescriptionItem(); withAddedDescriptionLines() is deprecated - Argument @PARAM descriptions respect setDocblockDescriptionsEnabled(false) (thecodingmachine#793)
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #838 +/- ##
============================================
- Coverage 95.72% 92.08% -3.65%
- Complexity 1773 2158 +385
============================================
Files 154 204 +50
Lines 4586 5746 +1160
============================================
+ Hits 4390 5291 +901
- Misses 196 455 +259 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Four related changes to how GraphQLite handles
Undefinedinput, plus one fix to the docblock description toggle from #793:nullfor an argument or input field typedT|Undefined(withoutnull) is refused with a client error instead of aTypeError.Undefinedtakes precedence over the implicitnulldefault, so an omittedT|null|Undefinedfield resolves toUndefined::VALUE.nullis accepted.#[Cost]line, the sentence above, and anything a middleware adds) is rendered as a Markdown list after the description.@paramdescriptions respectsetDocblockDescriptionsEnabled(false).1. Refusing an explicit null for
T|UndefinedGraphQL can't express "may be omitted, but not null": an input field is optional only if it is nullable (
Int), or if it has a default that hides the omission (Int! = 5). SoUndefinedTypeMapperexposesint|UndefinedasInt, which is correct, but when a client then sendsnull, GraphQLite assigns it to a property (or passes it to a setter, constructor parameter or method argument) whose PHP type can't hold it, and the request fails with aTypeError.Now, when the PHP type that receives the value contains
Undefinedbut notnull, an explicitnullis refused before anything is assigned or the resolver runs:Parameters\NullArgumentExceptionsits next toMissingArgumentExceptionand extendsGraphQLException, so it is client-safe. The check runs inInputTypeParameter::resolve(), which every hydration path goes through: public properties, setters,__call, promoted and plain constructor parameters,#[Factory]parameters, query/mutation arguments, lists, and variables bound tonull. Nullability is decided by the sink that actually receives the value (a constructor parameter or setter can differ from the property it fills).T|null|Undefinedstill acceptsnull. This follows the spec editor's guidance in graphql/graphql-spec#476 to enforce "optional non-null" with server-side validation.2.
Undefinedbefore the implicitnulldefaultTypeHandler::mapInputPropertyapplied the nullable rule (default = null) before theUndefinedrule, whilemapParameterchecked them in the opposite order. Soint|null|Undefined $x;without a PHP default printed= null, webonyx fillednullwhen the field was omitted, and it never resolved toUndefined. The docs exampleprotected int|null|Undefined $age;hit this.Now
Undefinedis checked first in both. An omitted field without a PHP default is hydrated withUndefined::VALUE(viaInputField::resolveOmitted()), including properties left uninitialized and required constructor parameters, unless the constructor already assigned the property. Phpdoc-only types (@var int|null|Undefined) are handled too. A typed property with an explicit default (= null,= 5) keeps it and still prints it.3. Saying whether null is accepted
Applied directives aren't exposed by introspection (graphql/graphql-spec#300) and webonyx's
SchemaPrinterdoesn't print custom ones, so the description is the only place every client sees the rule. Undefined-typed arguments and input fields get one of:May be omitted; null is not accepted.(T|Undefined)May be omitted; null is accepted.(T|null|Undefined)It isn't added when the exposed GraphQL type is non-null (for example forced with
inputType: 'Int!'), and it is never duplicated.SchemaFactory::setUndefinedDescriptionsEnabled(false)turns it off, matchingsetDocblockDescriptionsEnabled(). It is on by default: a description change doesn't break clients, only schemas that useUndefinedare affected, and those fields' printed defaults change in this release anyway.4. Description metadata as a list
QueryFieldDescriptorandInputFieldDescriptorkeep the description separate from appended metadata items, andDescriptionResolver::appendItems()renders them once:Middlewares add items with the new
withAddedDescriptionItem(). Items keep middleware order, survivewithDescription(), and are de-duplicated (including against text the description already contains).QueryFieldDescriptor::withAddedDescriptionLines()keeps its behaviour and is deprecated withtrigger_error, as other deprecations in the codebase are.5. #793 follow-up
TypeHandler::mapParametercomputed the@paramdescription behind the docblock toggle and then overwrote it unconditionally, so argument descriptions still appeared withsetDocblockDescriptionsEnabled(false). The overwrite is removed.Breaking changes
Documented in the 8.4.0 CHANGELOG with migration notes:
#[Cost]text in introspection and printed schemas becomes the list item- Cost: complexity = .... Anything parsing or snapshotting descriptions needs updating.T|null|Undefinedfield without a PHP default resolves toUndefined::VALUEinstead ofnulland no longer prints= null. Code treatingnullas "not sent" should compare againstUndefined::VALUE.Tests
__call), with and without PHP defaults: omitted givesUndefined, a value comes through,nullis accepted or refused as typed, refusals run underDebugFlag::RETHROW_UNSAFE_EXCEPTIONSso aTypeErrorfails the test, and variables bound tonullor left out.descriptionattribute (+#[EnumValue]) + SchemaFactory docblock toggle #793 toggle on arguments.--prefer-lowest, on the fork.