Skip to content

Support @skip and @include on the merged field model - #148

Merged
spawnia merged 12 commits into
masterfrom
skip-include-collect-fields
Oct 6, 2026
Merged

spawnia merged 12 commits into
masterfrom
skip-include-collect-fields

Conversation

@spawnia

@spawnia spawnia commented Oct 3, 2026 •

Copy link
Copy Markdown
Owner

Part of #149

Servers omit fields under @skip or @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
  • A field under @skip or @include, on itself, an enclosing inline fragment or a fragment spread, gets a nullable @property, an optional make() parameter and may be missing from the response.
  • OmittableConverter marks such fields, ObjectLike::fromGraphQL tolerates a missing field only behind it.
    The NonNullConverter stays inside, so an explicit null for 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.
  • A field is required as soon as one occurrence is selected unconditionally, __typename always.
  • Children of a conditional selection merged with another selection of the same field may be missing.
    Children of a lone conditional selection stay required.
  • ... on Article @skip(if: $skip) { content { text } } ... on Video { content { url } } keeps Article's text required.
Omittability follows from the conditions each occurrence is selected under, without counting selections

FieldCollector passes the set of @skip/@include conditions down collectFields, keyed by the printed directive.
Each CollectedField records that set for every occurrence.
Each Selection records 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
singleObject @skip(if: $value) { value }
singleObject @skip(if: $value) { nested { value } }

Both selections are present or absent together, so value stays required.
#79 counts two selections and makes it omittable.
The new example SkipObjectTwiceWithSameCondition pins 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.

ObjectLikeBuilder no longer deduplicates properties on this stack, so the PropertyDefinition class of #79 is not ported.
The property tuple gains an $isOmittable flag 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

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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 OmittableConverter and 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
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
…nt-definitions

# Conflicts:
#	src/Codegen/Generator.php
…skip-include-collect-fields

# Conflicts:
#	src/Codegen/FieldCollector.php
🤖 Generated with Claude Code
Base automatically changed from send-fragment-definitions to master October 6, 2026 19:26
…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
@spawnia
spawnia marked this pull request as ready for review October 6, 2026 19:27
@spawnia
spawnia merged commit 654fe64 into master Oct 6, 2026
36 of 38 checks passed
@spawnia
spawnia deleted the skip-include-collect-fields branch October 6, 2026 19:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants