Skip to content

Commit 192c78f

Browse files
committed
feat: explicit description attribute + SchemaFactory docblock toggle
Adds explicit `description` argument to every schema-defining attribute (Query, Mutation, Subscription, Type, ExtendType, Factory) and a SchemaFactory toggle to disable the docblock-as-description fallback. Addresses #453 and the type-level portion of #740. Behaviour highlights: - Explicit attribute description always wins; `''` deliberately blocks the docblock fallback; `null` falls through when the toggle is on. - `setDocblockDescriptionsEnabled(false)` suppresses every docblock fallback path so internal developer docblocks stop leaking to public schema consumers. - Duplicate descriptions across `#[Type]` + `#[ExtendType]` (or multiple `#[ExtendType]`s on the same class) now throw a DuplicateDescriptionOnTypeException naming every offending source. - `InputTypeGenerator::mapFactoryMethod()` closes the long-standing `// TODO: add comment argument.` — factory-produced input types now participate in description resolution. Consistency renames (breaking): - `QueryFieldDescriptor`/`InputFieldDescriptor` `$comment` -> `$description` and paired method renames (getDescription/withDescription/ withAddedDescriptionLines). - `AbstractRequest` -> `AbstractGraphQLElement` and `AnnotationReader::getRequestAnnotation()` -> `getGraphQLElementAnnotation()`. The new name reflects the class's role as the shared base for GraphQL schema-element attributes. Tests: +26 new (resolver precedence matrix, per-attribute integration, conflict exception, docblock-off regression). Full suite 528 passing across cs-check, phpstan, and phpunit.
1 parent b4629a6 commit 192c78f

50 files changed

Lines changed: 1241 additions & 190 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/AnnotationReader.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
use ReflectionMethod;
99
use ReflectionParameter;
1010
use ReflectionProperty;
11-
use TheCodingMachine\GraphQLite\Annotations\AbstractRequest;
11+
use TheCodingMachine\GraphQLite\Annotations\AbstractGraphQLElement;
1212
use TheCodingMachine\GraphQLite\Annotations\Decorate;
1313
use TheCodingMachine\GraphQLite\Annotations\EnumType;
1414
use TheCodingMachine\GraphQLite\Annotations\Exceptions\ClassNotFoundException;
@@ -201,11 +201,11 @@ public function getEnumTypeAnnotation(ReflectionClass $refClass): EnumType|null
201201
return $this->getClassAnnotation($refClass, EnumType::class);
202202
}
203203

204-
/** @param class-string<AbstractRequest> $annotationClass */
205-
public function getRequestAnnotation(ReflectionMethod $refMethod, string $annotationClass): AbstractRequest|null
204+
/** @param class-string<AbstractGraphQLElement> $annotationClass */
205+
public function getGraphQLElementAnnotation(ReflectionMethod $refMethod, string $annotationClass): AbstractGraphQLElement|null
206206
{
207207
$queryAnnotation = $this->getMethodAnnotation($refMethod, $annotationClass);
208-
assert($queryAnnotation instanceof AbstractRequest || $queryAnnotation === null);
208+
assert($queryAnnotation instanceof AbstractGraphQLElement || $queryAnnotation === null);
209209

210210
return $queryAnnotation;
211211
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TheCodingMachine\GraphQLite\Annotations;
6+
7+
/**
8+
* Shared base for every attribute that declares an invokable GraphQL schema element with a
9+
* return type — {@see Query}, {@see Mutation}, {@see Subscription}, and {@see Field}. Each of
10+
* those attributes inherits a GraphQL-level name, an explicit return type override, and a
11+
* schema description from this class.
12+
*/
13+
abstract class AbstractGraphQLElement
14+
{
15+
private string|null $outputType;
16+
17+
private string|null $name;
18+
19+
private string|null $description;
20+
21+
/** @param mixed[] $attributes */
22+
public function __construct(
23+
array $attributes = [],
24+
string|null $name = null,
25+
string|null $outputType = null,
26+
string|null $description = null,
27+
) {
28+
$this->outputType = $outputType ?? $attributes['outputType'] ?? null;
29+
$this->name = $name ?? $attributes['name'] ?? null;
30+
$this->description = $description ?? $attributes['description'] ?? null;
31+
}
32+
33+
/**
34+
* Returns the GraphQL return type for this schema element (as a string).
35+
* The string can represent the FQCN of the type or an entry in the container resolving to the GraphQL type.
36+
*/
37+
public function getOutputType(): string|null
38+
{
39+
return $this->outputType;
40+
}
41+
42+
/**
43+
* Returns the GraphQL name of the query/mutation/subscription/field.
44+
* If not specified, the name of the PHP method is used instead.
45+
*/
46+
public function getName(): string|null
47+
{
48+
return $this->name;
49+
}
50+
51+
/**
52+
* Returns the explicit description for this schema element, or null if none was provided.
53+
*
54+
* A null return means "no explicit description" and the schema builder may fall back to the
55+
* docblock summary (if docblock descriptions are enabled on the SchemaFactory). An explicit
56+
* empty string blocks the docblock fallback and produces an empty description.
57+
*/
58+
public function getDescription(): string|null
59+
{
60+
return $this->description;
61+
}
62+
}

src/Annotations/AbstractRequest.php

Lines changed: 0 additions & 37 deletions
This file was deleted.
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TheCodingMachine\GraphQLite\Annotations\Exceptions;
6+
7+
use BadMethodCallException;
8+
9+
use function implode;
10+
11+
/**
12+
* Thrown when both a #[Type] attribute and one or more #[ExtendType] attributes (or multiple
13+
* #[ExtendType] attributes alone) declare a `description` for the same GraphQL type.
14+
*
15+
* A GraphQL type has exactly one description, so GraphQLite must be able to pick a single
16+
* canonical source. Rather than silently resolving the conflict via declaration order, the
17+
* schema builder rejects the ambiguity with a clear error listing every offending source.
18+
*
19+
* Descriptions may therefore live on the base #[Type] OR on exactly one #[ExtendType], never
20+
* on both, and never on more than one #[ExtendType] for the same target class.
21+
*/
22+
class DuplicateDescriptionOnTypeException extends BadMethodCallException
23+
{
24+
/**
25+
* @param class-string<object> $targetClass
26+
* @param list<string> $sources Human-readable descriptions of the attribute sources
27+
* that contributed a description (e.g. class names).
28+
*/
29+
public static function forType(string $targetClass, array $sources): self
30+
{
31+
return new self(
32+
'A GraphQL type may only have a description declared on the #[Type] attribute OR on exactly one #[ExtendType] attribute, never more than one. '
33+
. 'Target type class "' . $targetClass . '" received descriptions from multiple sources: '
34+
. implode(', ', $sources) . '. '
35+
. 'Keep the description on the #[Type] attribute, or move it to at most one #[ExtendType] attribute.',
36+
);
37+
}
38+
}

src/Annotations/ExtendType.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,14 @@ class ExtendType
2121
/** @var class-string<object>|null */
2222
private string|null $class;
2323
private string|null $name;
24+
private string|null $description;
2425

2526
/** @param mixed[] $attributes */
2627
public function __construct(
2728
array $attributes = [],
2829
string|null $class = null,
2930
string|null $name = null,
31+
string|null $description = null,
3032
) {
3133
$className = isset($attributes['class']) ? ltrim($attributes['class'], '\\') : null;
3234
$className = $className ?? $class;
@@ -35,6 +37,7 @@ public function __construct(
3537
}
3638
$this->name = $name ?? $attributes['name'] ?? null;
3739
$this->class = $className;
40+
$this->description = $description ?? $attributes['description'] ?? null;
3841
if (! $this->class && ! $this->name) {
3942
throw new BadMethodCallException('In attribute #[ExtendType], missing one of the compulsory parameter "class" or "name".');
4043
}
@@ -55,4 +58,17 @@ public function getName(): string|null
5558
{
5659
return $this->name;
5760
}
61+
62+
/**
63+
* Returns the explicit description contributed by this type extension, or null if none was provided.
64+
*
65+
* A GraphQL type carries exactly one description. If both the base #[Type] and this #[ExtendType]
66+
* (or multiple #[ExtendType] attributes targeting the same class) provide a description, the
67+
* schema builder throws DuplicateDescriptionOnTypeException. Descriptions may therefore live on
68+
* #[Type] OR on at most one #[ExtendType], never on both.
69+
*/
70+
public function getDescription(): string|null
71+
{
72+
return $this->description;
73+
}
5874
}

src/Annotations/Factory.php

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,19 @@ class Factory
1515
{
1616
private string|null $name;
1717
private bool $default;
18+
private string|null $description;
1819

1920
/** @param mixed[] $attributes */
20-
public function __construct(array $attributes = [], string|null $name = null, bool|null $default = null)
21-
{
21+
public function __construct(
22+
array $attributes = [],
23+
string|null $name = null,
24+
bool|null $default = null,
25+
string|null $description = null,
26+
) {
2227
$this->name = $name ?? $attributes['name'] ?? null;
2328
// This IS the default if no name is set and no "default" attribute is passed.
2429
$this->default = $default ?? $attributes['default'] ?? ! isset($attributes['name']);
30+
$this->description = $description ?? $attributes['description'] ?? null;
2531

2632
if ($this->name === null && $this->default === false) {
2733
throw new GraphQLRuntimeException('A #[Factory] that has "default=false" attribute must be given a name (i.e. add a name="FooBarInput" attribute).');
@@ -44,4 +50,17 @@ public function isDefault(): bool
4450
{
4551
return $this->default;
4652
}
53+
54+
/**
55+
* Returns the explicit description for the GraphQL input type produced by this factory,
56+
* or null if none was provided.
57+
*
58+
* A null return means "no explicit description" and the schema builder may fall back to the
59+
* docblock summary (if docblock descriptions are enabled on the SchemaFactory). An explicit
60+
* empty string blocks the docblock fallback and produces an empty description.
61+
*/
62+
public function getDescription(): string|null
63+
{
64+
return $this->description;
65+
}
4766
}

src/Annotations/Field.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
use const E_USER_DEPRECATED;
1212

1313
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
14-
class Field extends AbstractRequest
14+
class Field extends AbstractGraphQLElement
1515
{
1616
private string|null $prefetchMethod;
1717

src/Annotations/Mutation.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,6 @@
77
use Attribute;
88

99
#[Attribute(Attribute::TARGET_METHOD)]
10-
class Mutation extends AbstractRequest
10+
class Mutation extends AbstractGraphQLElement
1111
{
1212
}

src/Annotations/Query.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,6 @@
77
use Attribute;
88

99
#[Attribute(Attribute::TARGET_METHOD)]
10-
class Query extends AbstractRequest
10+
class Query extends AbstractGraphQLElement
1111
{
1212
}

src/Annotations/Subscription.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,6 @@
77
use Attribute;
88

99
#[Attribute(Attribute::TARGET_METHOD)]
10-
class Subscription extends AbstractRequest
10+
class Subscription extends AbstractGraphQLElement
1111
{
1212
}

0 commit comments

Comments
 (0)