Repository navigation
Support @skip and @include on the merged field model - #148
Merged
Merged
Conversation
FieldCollector merges the fields of each operation per response path and concrete object type, resolving fragments itself. OperationGenerator walks that model without traversal state, adding __typename during emission. OperationStack and the duplicate check in ObjectLikeBuilder are gone. FoldFragments and AddTypename now only shape the document sent to the server. 🤖 Generated with Claude Code
The field collector resolves fragments itself, so FoldFragments only served the document sent to the server. Each operation now carries the fragments it uses, as written by the user, which also lifts the ban on fragment directives. Changes the document() strings of operations that use fragments. 🤖 Generated with Claude Code
🤖 Generated with Claude Code
Fields under @Skip or @include are generated nullable and may be absent from the response. FieldCollector records the conditions each occurrence of a field and of its parent field is selected under; a field is omittable when some occurrence of its parent can be present without it. Ports the example operations, tests and README section of #79 🤖 Generated with Claude Code
🤖 Generated with Claude Code
This was referenced Oct 3, 2026
Contributor
There was a problem hiding this comment.
Copilot review overview
🟢 Approval recommended
The implementation matches the documented semantics and is comprehensively covered by runtime and golden-file tests.
Review effort: Balanced
Findings: None
What changed in this PR
Adds merged-model support for GraphQL @skip and @include, allowing generated result classes to safely represent omitted fields.
Changes:
- Tracks conditional selection sets and derives field omittability.
- Adds
OmittableConverterand nullable generated properties. - Adds documentation, golden fixtures, and extensive integration coverage.
| File | Description |
|---|---|
src/Codegen/FieldCollector.php |
Collects directive conditions. |
src/Codegen/CollectedField.php |
Tracks field occurrence conditions. |
src/Codegen/Selection.php |
Determines field omittability. |
src/Codegen/OperationGenerator.php |
Passes omittability to generation. |
src/Codegen/ObjectLikeBuilder.php |
Generates nullable, optional fields. |
src/Convert/OmittableConverter.php |
Marks omittable converters. |
src/ObjectLike.php |
Accepts marked missing fields. |
src/Type/InputObjectTypeConfig.php |
Adapts builder invocation. |
tests/Unit/ObjectLikeTest.php |
Verifies ordinary nullable fields remain required. |
tests/Integration/SimpleTest.php |
Covers directive behavior and merging. |
tests/Integration/InlineFragmentsTest.php |
Covers polymorphic conditional selections. |
README.md |
Documents client directives. |
CHANGELOG.md |
Records the feature and fix. |
composer.json |
Autoloads inline-fragment fixtures. |
examples/simple/schema.graphql |
Adds a non-null test field. |
examples/simple/src/clientDirectives.graphql |
Adds conditional-selection fixtures. |
examples/simple/expected/Operations/* |
Updates simple-example golden outputs. |
examples/inline-fragments/src/SearchQuery.graphql |
Adds polymorphic fixtures. |
examples/inline-fragments/expected/Operations/* |
Adds inline-fragment golden outputs. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
spawnia
added this pull request to stack #147
October 3, 2026 17:34
FoldFragments and AddTypename modify nodes in place, so codegen read the folded document. 🤖 Generated with Claude Code
🤖 Generated with Claude Code
…nt-definitions # Conflicts: # src/Codegen/Generator.php
…skip-include-collect-fields # Conflicts: # src/Codegen/FieldCollector.php
🤖 Generated with Claude Code
…skip-include-collect-fields
…t-fields # Conflicts: # CHANGELOG.md # src/Codegen/CollectedField.php # src/Codegen/FieldCollector.php # src/Codegen/ObjectLikeBuilder.php # src/Codegen/OperationGenerator.php # src/Codegen/Selection.php
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.
Part of #149
Servers omit fields under
@skipor@include, but the generated classes require them.This generates those fields nullable and derives omittability from the merged field model of #144.
Alternatives: #79 (full support on current codegen), #143 (reject the directives), #146 (fields only)
Behavior matches https://github.com//pull/79 and is pinned by its examples and tests
@skipor@include, on itself, an enclosing inline fragment or a fragment spread, gets a nullable@property, an optionalmake()parameter and may be missing from the response.OmittableConvertermarks such fields,ObjectLike::fromGraphQLtolerates a missing field only behind it.The
NonNullConverterstays inside, so an explicitnullfor a non-null field still fails.@skip(if: false)and@include(if: true)count as unconditional,@skip(if: true)and@include(if: false)as conditional.__typenamealways.Children of a lone conditional selection stay required.
... on Article @skip(if: $skip) { content { text } } ... on Video { content { url } }keeps Article'stextrequired.Omittability follows from the conditions each occurrence is selected under, without counting selections
FieldCollectorpasses the set of@skip/@includeconditions downcollectFields, keyed by the printed directive.Each
CollectedFieldrecords that set for every occurrence.Each
Selectionrecords it for every occurrence of its parent field, per concrete object type.A field is required when every occurrence of its parent can only be present together with one of the field's occurrences.
In code: for every parent condition set, some field condition set is a subset of it.
That replaces the per-path and per-type counting in
OperationGenerator(countSelectionsByResponsePath,isOmittable,possibleParentTypeNames) of #79.The flag suggested in #144 cannot tell a lone conditional selection from one merged with another, so this tracks the condition set instead.
One intentional difference: selections sharing the same condition keep their children required
Both selections are present or absent together, so
valuestays required.#79 counts two selections and makes it omittable.
The new example
SkipObjectTwiceWithSameConditionpins this.Generated code equals the golden files of https://github.com//pull/79, except document strings
All ported operations generate identical classes.
Only
document()differs for the operations with fragment spreads, since #145 sends fragments as written.The omission logic takes 131 added lines in src, against 278 in https://github.com//pull/79
git diff --stat origin/send-fragment-definitions...HEAD -- src: 8 files, 131 insertions, 24 deletions.git diff --stat origin/master...origin/skip-non-nullable -- src: 6 files, 278 insertions, 34 deletions.ObjectLikeBuilderno longer deduplicates properties on this stack, so thePropertyDefinitionclass of #79 is not ported.The property tuple gains an
$isOmittableflag instead.Conditions are compared by their printed form, which stays sound but misses some exclusions
@skip(if: $a)and@include(if: $a)count as unrelated conditions, so fields can be marked omittable more often than needed, never less.Child classes stay keyed by response path only, as in #144.
When two concrete parent types select the same child object type with different subfields, a condition on one branch makes that branch's subfields omittable in the merged class, even where its parent is present.
🤖 Generated with Claude Code