Skip to content

Commit 3fe283f

Browse files
committed
feat: deprecation notice for enum cases missing #[EnumValue]
Announces the upcoming opt-in migration for PHP enums mapped to GraphQL enum types. Today every case is automatically exposed; a future major release will require #[EnumValue] on each case that should participate in the schema, matching the opt-in model #[Field] already uses for methods and properties on object types. The practical win is selective exposure: an internal enum case can opt out of the public schema simply by omitting #[EnumValue], instead of forcing schema authors to split an enum or rename cases. Runtime behaviour is unchanged. When an enum annotated with #[Type] exposes any case without an #[EnumValue] attribute, EnumTypeMapper emits an E_USER_DEPRECATED notice that names the specific cases so the migration path is mechanical — matching graphqlite's existing deprecation pattern (e.g. addControllerNamespace, setGlobTTL). Tests: +1 explicit deprecation assertion (using PHPUnit 11's expectUserDeprecationMessageMatches). Existing integration tests that exercise the Genre fixture surface the notice via PHPUnit's deprecation reporting, confirming the warning fires in realistic scenarios. Full suite 538/538 green across cs-check, phpstan, and phpunit; 4 intentional deprecation events reported. Docs: descriptions.md gains a "Future migration" subsection describing the planned opt-in model and the current advisory notice.
1 parent cc04b22 commit 3fe283f

3 files changed

Lines changed: 78 additions & 0 deletions

File tree

src/Mappers/Root/EnumTypeMapper.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,12 @@
3030
use function array_values;
3131
use function assert;
3232
use function enum_exists;
33+
use function implode;
3334
use function ltrim;
35+
use function sprintf;
36+
use function trigger_error;
37+
38+
use const E_USER_DEPRECATED;
3439

3540
/**
3641
* Maps an enum class to a GraphQL type (only available in PHP>=8.1)
@@ -143,11 +148,17 @@ private function mapByClassName(string $enumClass): EnumType|null
143148
$enumCaseDescriptions = [];
144149
/** @var array<string, string> $enumCaseDeprecationReasons */
145150
$enumCaseDeprecationReasons = [];
151+
/** @var list<string> $casesMissingEnumValueAttribute */
152+
$casesMissingEnumValueAttribute = [];
146153

147154
foreach ($reflectionEnum->getCases() as $reflectionEnumCase) {
148155
$docBlock = $this->docBlockFactory->create($reflectionEnumCase);
149156
$enumValueAttribute = $this->annotationReader->getEnumValueAnnotation($reflectionEnumCase);
150157

158+
if ($enumValueAttribute === null) {
159+
$casesMissingEnumValueAttribute[] = $reflectionEnumCase->getName();
160+
}
161+
151162
$enumCaseDescriptions[$reflectionEnumCase->getName()] = $this->descriptionResolver->resolve(
152163
$enumValueAttribute?->description,
153164
$docBlock->getSummary() ?: null,
@@ -172,11 +183,45 @@ private function mapByClassName(string $enumClass): EnumType|null
172183
}
173184
}
174185

186+
$this->warnAboutCasesMissingEnumValueAttribute($enumClass, $casesMissingEnumValueAttribute);
187+
175188
$type = new EnumType($enumClass, $typeName, $enumDescription, $enumCaseDescriptions, $enumCaseDeprecationReasons, $useValues);
176189

177190
return $this->cacheByName[$type->name] = $this->cacheByClass[$enumClass] = $type;
178191
}
179192

193+
/**
194+
* Emits a deprecation notice when a GraphQL-mapped enum exposes one or more cases without a
195+
* matching {@see EnumValue} attribute.
196+
*
197+
* Today every case is automatically exposed in the schema — this call site keeps that
198+
* behaviour intact. The notice announces the planned migration: a future major release will
199+
* require `#[EnumValue]` on each case that should participate in the schema, mirroring
200+
* `#[Field]`'s opt-in model on classes. Until then, adopters can start annotating cases
201+
* incrementally; the warning lists the specific cases that would be dropped after the
202+
* future default flip so the migration path is mechanical.
203+
*
204+
* @param class-string<UnitEnum> $enumClass
205+
* @param list<string> $casesMissingAttribute
206+
*/
207+
private function warnAboutCasesMissingEnumValueAttribute(string $enumClass, array $casesMissingAttribute): void
208+
{
209+
if ($casesMissingAttribute === []) {
210+
return;
211+
}
212+
213+
trigger_error(
214+
sprintf(
215+
'Enum "%s" is mapped to a GraphQL enum type but exposes one or more cases without a #[EnumValue] attribute. '
216+
. 'Today every case is automatically exposed; a future major release will require #[EnumValue] on each case that should participate in the schema, mirroring #[Field]\'s opt-in model. '
217+
. 'Add #[EnumValue] to each case you want to keep exposed. Cases currently exposed without an attribute: %s.',
218+
$enumClass,
219+
implode(', ', $casesMissingAttribute),
220+
),
221+
E_USER_DEPRECATED,
222+
);
223+
}
224+
180225
private function getTypeName(ReflectionClass $reflectionClass): string
181226
{
182227
$typeAnnotation = $this->annotationReader->getTypeAnnotation($reflectionClass);

tests/Integration/DescriptionTest.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,20 @@ public function testEnumValueAttributeProvidesCaseDescription(): void
105105
$this->assertSame('Fiction works including novels and short stories.', $fictionValue->description);
106106
}
107107

108+
public function testEnumWithCasesMissingEnumValueAttributeTriggersDeprecation(): void
109+
{
110+
// The Genre fixture deliberately leaves NonFiction without #[EnumValue] to exercise the
111+
// docblock-fallback path and to surface the deprecation announcing the future opt-in
112+
// migration. PHPUnit 11's expectUserDeprecationMessageMatches hooks into the library's
113+
// own error handler so the assertion works consistently with how deprecations surface
114+
// in CI output.
115+
$this->expectUserDeprecationMessageMatches('/#\[EnumValue\].*future major.*NonFiction/s');
116+
117+
$schema = $this->buildSchema(Book::class);
118+
// Force enum resolution — types are lazy-mapped until referenced.
119+
$schema->getType('Genre');
120+
}
121+
108122
public function testEnumCaseWithoutAttributeFallsBackToDocblock(): void
109123
{
110124
$schema = $this->buildSchema(Book::class);

website/docs/descriptions.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,25 @@ is an enum value.
129129
Omitting it falls back to the `@deprecated` tag on the case docblock. An explicit empty string
130130
`''` deliberately clears any inherited `@deprecated` tag.
131131

132+
### Future migration: `#[EnumValue]` will become required per case
133+
134+
Today every case of a `#[Type]`-mapped enum is automatically exposed in the GraphQL schema. A
135+
future major release will flip this to an opt-in model: **only cases carrying an explicit
136+
`#[EnumValue]` attribute will be exposed**, mirroring the way `#[Field]` opts individual
137+
methods and properties into an object type.
138+
139+
The benefit is selective exposure — today there is no way to map a subset of a PHP enum into
140+
GraphQL, which forces schema authors to split an enum into two or rename cases. Under the
141+
opt-in model, an internal enum case can simply omit `#[EnumValue]` to stay out of the public
142+
schema.
143+
144+
To surface the upcoming change, GraphQLite already emits a PHP `E_USER_DEPRECATED` notice at
145+
schema build time when an enum annotated with `#[Type]` exposes any case without an
146+
`#[EnumValue]` attribute. The notice names the specific cases that would be dropped after the
147+
flip so the migration path is mechanical: add `#[EnumValue]` to every case you want to keep.
148+
No runtime behaviour changes today — the notice only signals what the future default will
149+
require.
150+
132151
## Description uniqueness on `#[ExtendType]`
133152

134153
A GraphQL type has exactly one description, so GraphQLite enforces that the description for a

0 commit comments

Comments
 (0)