Repository navigation
Conversation
|
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 penaltyWe'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 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. |
No, the problems with them are detailed in the description of this pull request. Sailor will currently not handle a field missing due to |
# 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
There was a problem hiding this comment.
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
@skipor@includedirectives - Updated
ObjectLikedeserialization to skip missing fields when they useNullConverter - 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.
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>
The first selection used to decide, so the generated type of a field depended on selection order. 🤖 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
🤖 Generated with Claude Code
🤖 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
…lusion 🤖 Generated with Claude Code
# Conflicts: # CHANGELOG.md # tests/Integration/SimpleTest.php
# Conflicts: # CHANGELOG.md
🤖 Generated with Claude Code
🤖 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
* 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

Part of #149
Servers omit fields under
@skipor@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
@propertyand an optionalmake()parameter.OmittableConvertermarks such fields, andObjectLike::fromGraphQL()tolerates 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.__typenamealways.Fields under a conditional selection may be missing only when another selection still provides their parent
singleObject @skip(if: $v) { value } singleObject { nested { value } }makesvalueomittable.singleObject @include(if: $v) { value }alone keepsvaluerequired.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 positionalmake()calls need updating.`ObjectLikeBuilder` signatures change
addProperty()requires a newbool $isOmittableargument.buildProperty()takes aPropertyDefinitioninstead of positional arguments.🤖 Generated with Claude Code