Skip to content

Handle fields omitted through @skip or @include - #79

Closed
spawnia wants to merge 35 commits into
masterfrom
skip-non-nullable
Closed

spawnia wants to merge 35 commits into
masterfrom
skip-non-nullable

Conversation

@spawnia

@spawnia spawnia commented Nov 10, 2022 •

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 on the current code generation and lets them be missing.

Alternatives: #148 (full support on the merged field model), #146 (fields only), #143 (reject the directives)

A field under `@skip` or `@include` becomes nullable and may be missing from the response
  • This applies to the directive on the field itself, on an enclosing inline fragment or on a fragment spread.
  • The field gets a nullable @property and an optional make() parameter.
  • OmittableConverter marks such fields, and 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.
  • A field is required as soon as one of its selections is unconditional, __typename always.
  • Sailor does not evaluate the directive arguments at runtime, so it also accepts a missing field the directive included.
Fields under a conditional selection may be missing only when another selection still provides their parent

singleObject @skip(if: $v) { value } singleObject { nested { value } } makes value omittable.
singleObject @include(if: $v) { value } alone keeps value required.

A second pass over the document counts selections per response path and possible parent object type.
OperationGenerator::isOmittable() checks those counts for the ancestors of each field.

The counting is an approximation: two selections under the same condition make their fields omittable, although both are present together.
#148 derives this exactly from a merged field model.

Generated classes change for operations that use the directives

Non-null fields under the directives become nullable.
Their make() parameters become optional and move behind the required ones, so positional make() calls need updating.

`ObjectLikeBuilder` signatures change

addProperty() requires a new bool $isOmittable argument.
buildProperty() takes a PropertyDefinition instead of positional arguments.

🤖 Generated with Claude Code

@CRC-Mismatch

CRC-Mismatch commented Apr 17, 2023 •

Copy link
Copy Markdown
Contributor

I was checking #77, #78 and this for an upcoming task I'm about to take on later this week or the next.

Is it currently already possible to properly use the directives, or are there still any limitations that this PR is meant to solve?

TL;DR - We need to suppress unneeded queries and results to reduce the performance penalty

We're transitioning back-ends from an close-to-unmaintainable REST-only system to a new back-end designed from scratch with GraphQL-only support in mind. To enable the front-end developers to "chop" the transitioning so we can deliver a production-ready migration without having to delay everything for months (or years), we've developed a BFF that "translates" the most coupled legacy REST contracts and endpoints to the new model using Sailor. I've developed a Symfony bundle wrapper (mismatch/sailor-bundle) to ease its integration in our current projects' architecture.

Problem is, we can't deliver the BFF (and by association the clients' migration) as "production-ready" until we can solve the huge performance penalty due to making one REST request become ~15 GraphQL queries and mutations (most of the queries are already compressed in 3 requests, but the mutations are mostly one-per-request) since when we originally started, we couldn't leverage the @include and @skip directives.

Without them, we'll have to create one variation of each query or mutation for each possible scenario where we don't need something, which could solve the performance problem with some clever coding to decide when to use what; but that would mean having around 4x the number of generated classes, and the same amount of new code-paths (incurring in huge NPath and Cyclomatic complexities, in a code that already suffers from these).

I'd like to know the current status, as when I take on that task, if something is still amiss, I'm probably going to first try and work on this PR, instead of turning our BFF's code into a Hydra right away.

@spawnia spawnia added the bug An error within Sailor label Apr 18, 2023
@spawnia

spawnia commented Apr 18, 2023

Copy link
Copy Markdown
Owner Author

Is it currently already possible to properly use the directives, or are there still any limitations that this PR is meant to solve?

No, the problems with them are detailed in the description of this pull request. Sailor will currently not handle a field missing due to @skip or @include gracefully. Feel free to fork and propose a solution to this pull request, it is currently only a failing test.

spawnia and others added 4 commits February 16, 2026 23:10
# Conflicts:
#	tests/Integration/SimpleTest.php
Expand test coverage for fields omitted through client directives:
- Fix existing test to use correct ObjectLike-level data (not Result envelope)
- Add @include on non-nullable field (IncludeNonNullable query + expected code)
- Test field-omitted and field-present cases for both @Skip and @include
- Test nullable fields with @skip/@include (ClientDirectiveQuery)
- Test fragment spread and inline fragment @Skip scenarios
- Test via Result::fromStdClass with full response envelope
- All omission tests assert accessing the missing field returns null

These are intentionally failing tests that define the expected behavior
for the @skip/@include implementation (see #77).

https://claude.ai/code/session_0124DAusRQCLHMsFSbeR1gfT
Repository owner deleted a comment from coderabbitai Bot Feb 17, 2026
Repository owner deleted a comment from coderabbitai Bot Feb 17, 2026
Repository owner deleted a comment from coderabbitai Bot Feb 17, 2026
spawnia and others added 5 commits February 17, 2026 13:05
…e fields

In codegen, unwrap NonNull when a field has @Skip or @include directives,
since the server may omit such fields. At runtime, allow missing fields
in fromGraphQL() when the converter is NullConverter instead of throwing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@spawnia
spawnia marked this pull request as ready for review February 17, 2026 12:40
@spawnia
spawnia requested a review from Copilot February 17, 2026 12:40

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.

Pull request overview

This PR adds support for GraphQL's @skip and @include client directives, allowing fields to be conditionally omitted from queries. When these directives are used, the implementation marks affected fields as nullable in generated classes and gracefully handles their absence in server responses, addressing issue #77.

Changes:

  • Modified code generation to unwrap non-null types for fields with @skip or @include directives
  • Updated ObjectLike deserialization to skip missing fields when they use NullConverter
  • Added comprehensive tests for client directives on fields, fragment spreads, and inline fragments

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/Codegen/OperationGenerator.php Added logic to detect @skip/@include directives on fields and unwrap NonNull types to allow nullability
src/ObjectLike.php Modified field deserialization to skip missing fields when using NullConverter instead of throwing exceptions
examples/simple/src/clientDirectives.graphql Added test queries for SkipNonNullable and IncludeNonNullable operations
examples/simple/schema.graphql Added nonNullable: String! field to Query type for testing
examples/simple/expected/Operations/SkipNonNullable/*.php Generated classes for SkipNonNullable operation showing NullConverter usage
examples/simple/expected/Operations/IncludeNonNullable/*.php Generated classes for IncludeNonNullable operation showing NullConverter usage
tests/Integration/SimpleTest.php Added 12 new test methods covering various scenarios of fields being omitted or present with client directives
README.md Added documentation section explaining client directive support with code examples
CHANGELOG.md Added entry for v1.3.0 documenting this fix

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/Codegen/OperationGenerator.php Outdated
spawnia and others added 3 commits February 17, 2026 14:44
Extend the @skip/@include directive handling to also check parent inline
fragments. Direct children of fragments with these directives are made
nullable (since the server may omit them), while nested descendants
preserve their original nullability.

Refactor depth tracking: use unified $selectionNestingDepth counter and
$inlineFragmentsByDepth map keyed by depth, eliminating redundant resets
and improving code clarity. Deduplicate directive checking with new
hasSkipOrIncludeDirective() helper method.

Exception: __typename field is never unwrapped, as it's always available.

Fixes: #79 (comment)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
PHPStan correctly identified that the search property can be null,
so we need to assert it's not null before accessing array offsets.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>

Copilot AI commented Feb 17, 2026

Copy link
Copy Markdown

@spawnia I've opened a new pull request, #136, to work on those changes. Once the pull request is ready, I'll request review from you.

spawnia added 14 commits October 3, 2026 17:45
The first selection used to decide, so the generated type of a field
depended on selection order.

🤖 Generated with Claude Code
@Skip(if: false) and @include(if: true) never omit the field.

🤖 Generated with Claude Code
🤖 Generated with Claude Code
🤖 Generated with Claude Code
When a conditional selection of a field merges with another selection
of the same response path, the server may return the field without the
subfields only the conditional selection requested. A lone conditional
selection keeps its subfields required.

🤖 Generated with Claude Code
Only absence is tolerated for fields under @Skip or @include, a present
null in a non-null position still fails validation.

🤖 Generated with Claude Code
🤖 Generated with Claude Code
Also compute omittability next to its use and order helpers after
their caller.

🤖 Generated with Claude Code
🤖 Generated with Claude Code
# Conflicts:
#	CHANGELOG.md
#	tests/Integration/SimpleTest.php

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

🟡 Changes recommended

Response-path counting conflates mutually exclusive concrete-type branches, producing incorrectly omittable fields and weakened response validation.

Review effort: Balanced
Findings: 1 Medium severity

Open (1)

Comment thread src/Codegen/OperationGenerator.php Outdated
🤖 Generated with Claude Code
Selections in mutually exclusive concrete-type branches share a
response path but never merge into the same object, so they no longer
make each other's subfields omittable.

🤖 Generated with Claude Code
@spawnia
spawnia marked this pull request as draft October 3, 2026 17:25
@spawnia spawnia removed the bug An error within Sailor label Oct 3, 2026
spawnia added a commit that referenced this pull request Oct 6, 2026
* Split operation codegen into field collection and emission

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

* Send fragment definitions instead of folding them into inline fragments

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

* Add changelog entry

🤖 Generated with Claude Code

* Support @Skip and @include on the merged field model

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

* Add changelog entries

🤖 Generated with Claude Code

* Keep the source document unmodified for codegen

FoldFragments and AddTypename modify nodes in place, so codegen read the folded document.

🤖 Generated with Claude Code

* Resolve introspection meta fields during field collection

🤖 Generated with Claude Code

* Cover directives on fragment definitions

🤖 Generated with Claude Code
@spawnia

spawnia commented Oct 6, 2026

Copy link
Copy Markdown
Owner Author

🤖 Superseded by #148, which builds on the merged field model of #144.

@spawnia spawnia closed this Oct 6, 2026
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.

5 participants