Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions src/AnnotationReader.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,15 @@
use TheCodingMachine\GraphQLite\Annotations\SourceFieldInterface;
use TheCodingMachine\GraphQLite\Annotations\Type;
use TheCodingMachine\GraphQLite\Annotations\TypeInterface;
use TheCodingMachine\GraphQLite\Directives\InputObjectTypeDirective;
use TheCodingMachine\GraphQLite\Directives\ObjectTypeDirective;

use function array_diff_key;
use function array_filter;
use function array_key_exists;
use function array_map;
use function array_merge;
use function array_values;
use function assert;
use function count;
use function get_class;
Expand Down Expand Up @@ -326,6 +329,34 @@ static function (array $parameterAnnotations): ParameterAnnotations {
);
}

/**
* The {@see ObjectTypeDirective}s on a class.
*
* @param ReflectionClass<T> $refClass
*
* @return list<ObjectTypeDirective>
*
* @template T of object
*/
public function getObjectTypeDirectives(ReflectionClass $refClass): array
{
return array_values($this->getClassAnnotations($refClass, ObjectTypeDirective::class, false));
}

/**
* The {@see InputObjectTypeDirective}s on a class.
*
* @param ReflectionClass<T> $refClass
*
* @return list<InputObjectTypeDirective>
*
* @template T of object
*/
public function getInputObjectTypeDirectives(ReflectionClass $refClass): array
{
return array_values($this->getClassAnnotations($refClass, InputObjectTypeDirective::class, false));
}

public function getMiddlewareAnnotations(ReflectionMethod|ReflectionProperty $reflection): MiddlewareAnnotations
{
if ($reflection instanceof ReflectionMethod) {
Expand Down
18 changes: 18 additions & 0 deletions src/Directives/BehavioralFieldDirective.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace TheCodingMachine\GraphQLite\Directives;

use GraphQL\Type\Definition\FieldDefinition;
use TheCodingMachine\GraphQLite\Middlewares\FieldHandlerInterface;
use TheCodingMachine\GraphQLite\QueryFieldDescriptor;

/**
* A {@see FieldDirective} that also runs behavior. The {@see applyToField} hooks run in declaration
* order, chained ahead of the rest of the field pipe.
*/
interface BehavioralFieldDirective extends FieldDirective
{
public function applyToField(QueryFieldDescriptor $descriptor, FieldHandlerInterface $next): FieldDefinition|null;
}
17 changes: 17 additions & 0 deletions src/Directives/BehavioralInputFieldDirective.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

namespace TheCodingMachine\GraphQLite\Directives;

use TheCodingMachine\GraphQLite\InputField;
use TheCodingMachine\GraphQLite\InputFieldDescriptor;
use TheCodingMachine\GraphQLite\Middlewares\InputFieldHandlerInterface;

/**
* An {@see InputFieldDirective} that also runs behavior, dispatched through the input-field pipe.
*/
interface BehavioralInputFieldDirective extends InputFieldDirective
{
public function applyToInputField(InputFieldDescriptor $descriptor, InputFieldHandlerInterface $next): InputField|null;
}
18 changes: 18 additions & 0 deletions src/Directives/BehavioralInputObjectTypeDirective.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace TheCodingMachine\GraphQLite\Directives;

use TheCodingMachine\GraphQLite\InputObjectTypeDescriptor;
use TheCodingMachine\GraphQLite\Middlewares\InputObjectTypeHandlerInterface;
use TheCodingMachine\GraphQLite\Types\MutableInputObjectType;

/**
* An {@see InputObjectTypeDirective} that also runs behavior, e.g. `@oneOf` flipping a flag on the
* built input type.
*/
interface BehavioralInputObjectTypeDirective extends InputObjectTypeDirective
{
public function applyToInputObjectType(InputObjectTypeDescriptor $descriptor, InputObjectTypeHandlerInterface $next): MutableInputObjectType;
}
19 changes: 19 additions & 0 deletions src/Directives/BehavioralObjectTypeDirective.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

namespace TheCodingMachine\GraphQLite\Directives;

use TheCodingMachine\GraphQLite\Middlewares\ObjectTypeHandlerInterface;
use TheCodingMachine\GraphQLite\ObjectTypeDescriptor;
use TheCodingMachine\GraphQLite\Types\MutableObjectType;

/**
* An {@see ObjectTypeDirective} that also runs behavior, usually tweaking the built
* {@see MutableObjectType}. Dispatched through the object-type pipe in
* {@see \TheCodingMachine\GraphQLite\TypeGenerator}.
*/
interface BehavioralObjectTypeDirective extends ObjectTypeDirective
{
public function applyToObjectType(ObjectTypeDescriptor $descriptor, ObjectTypeHandlerInterface $next): MutableObjectType;
}
50 changes: 50 additions & 0 deletions src/Directives/BuiltIn/Deprecated.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

declare(strict_types=1);

namespace TheCodingMachine\GraphQLite\Directives\BuiltIn;

use Attribute;
use GraphQL\Type\Definition\Directive as WebonyxDirective;
use GraphQL\Type\Definition\FieldDefinition;
use TheCodingMachine\GraphQLite\Directives\BehavioralFieldDirective;
use TheCodingMachine\GraphQLite\Directives\DirectiveDefinition;
use TheCodingMachine\GraphQLite\Directives\DirectiveLocation;
use TheCodingMachine\GraphQLite\Middlewares\FieldHandlerInterface;
use TheCodingMachine\GraphQLite\QueryFieldDescriptor;

/**
* Binds `#[Deprecated]` to GraphQL's built-in `deprecated` directive. Putting it on a query,
* mutation, or field method/property sets the field's deprecation reason, which webonyx prints in
* the SDL as `deprecated(reason: ...)`.
*
* webonyx already declares the `deprecated` directive, so we don't register our own definition
* ({@see DirectiveDefinition::$builtIn} is `true`). The existing docblock deprecation support is
* untouched: a bare `#[Deprecated]` keeps the docblock reason when there is one, and passing
* `reason:` overrides it.
*/
#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_PROPERTY)]
final class Deprecated implements BehavioralFieldDirective
{
public function __construct(public readonly string|null $reason = null)
{
}

public static function definition(): DirectiveDefinition
{
return new DirectiveDefinition(
name: WebonyxDirective::DEPRECATED_NAME,
locations: [DirectiveLocation::FIELD_DEFINITION],
builtIn: true,
);
}

public function applyToField(QueryFieldDescriptor $descriptor, FieldHandlerInterface $next): FieldDefinition|null
{
// An explicit reason wins; a bare #[Deprecated] keeps an existing docblock deprecation
// reason, falling back to webonyx's default when there's neither.
$reason = $this->reason ?? $descriptor->getDeprecationReason() ?? WebonyxDirective::DEFAULT_DEPRECATION_REASON;

return $next->handle($descriptor->withDeprecationReason($reason));
}
}
42 changes: 42 additions & 0 deletions src/Directives/BuiltIn/OneOf.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

declare(strict_types=1);

namespace TheCodingMachine\GraphQLite\Directives\BuiltIn;

use Attribute;
use GraphQL\Type\Definition\Directive as WebonyxDirective;
use TheCodingMachine\GraphQLite\Directives\BehavioralInputObjectTypeDirective;
use TheCodingMachine\GraphQLite\Directives\DirectiveDefinition;
use TheCodingMachine\GraphQLite\Directives\DirectiveLocation;
use TheCodingMachine\GraphQLite\InputObjectTypeDescriptor;
use TheCodingMachine\GraphQLite\Middlewares\InputObjectTypeHandlerInterface;
use TheCodingMachine\GraphQLite\Types\MutableInputObjectType;

/**
* Binds `#[OneOf]` to webonyx's built-in `@oneOf` directive. Putting it on an `#[Input]` class sets
* the input object's `isOneOf` flag, which makes validation require one field and gets webonyx to
* print `@oneOf` in the SDL.
*
* webonyx already defines `@oneOf`, so we don't register our own definition for it
* ({@see DirectiveDefinition::$builtIn} is `true`).
*/
#[Attribute(Attribute::TARGET_CLASS)]
final class OneOf implements BehavioralInputObjectTypeDirective
{
public static function definition(): DirectiveDefinition
{
return new DirectiveDefinition(
name: WebonyxDirective::ONE_OF_NAME,
locations: [DirectiveLocation::INPUT_OBJECT],
builtIn: true,
);
}

public function applyToInputObjectType(InputObjectTypeDescriptor $descriptor, InputObjectTypeHandlerInterface $next): MutableInputObjectType
{
$type = $next->handle($descriptor);
$type->isOneOf = true;
return $type;
}
}
118 changes: 118 additions & 0 deletions src/Directives/DirectiveAstBuilder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
<?php

declare(strict_types=1);

namespace TheCodingMachine\GraphQLite\Directives;

use GraphQL\Language\AST\ArgumentNode;
use GraphQL\Language\AST\DirectiveNode;
use GraphQL\Language\AST\FieldDefinitionNode;
use GraphQL\Language\AST\InputObjectTypeDefinitionNode;
use GraphQL\Language\AST\InputValueDefinitionNode;
use GraphQL\Language\AST\NamedTypeNode;
use GraphQL\Language\AST\NameNode;
use GraphQL\Language\AST\NodeList;
use GraphQL\Language\AST\ObjectTypeDefinitionNode;
use GraphQL\Utils\AST;
use ReflectionClass;

/**
* Builds the AST nodes that let directive applications show up in printed SDL.
*
* webonyx reads applications from `astNode->directives` on the definition node (FieldDefinitionNode,
* InputValueDefinitionNode, etc.). GraphQLite doesn't otherwise set `astNode`, so this builds just
* enough of the node to carry the directives, with arguments encoded via {@see AST::astFromValue}.
*
* @internal
*/
final class DirectiveAstBuilder
{
public function __construct(private readonly DirectiveRegistry $registry)
{
}

/**
* @param list<DirectiveInterface> $directives Directive instances applied to this element.
*
* @return list<DirectiveNode>
*/
public function buildDirectiveNodes(array $directives): array
{
$nodes = [];
foreach ($directives as $directive) {
$definition = $this->registry->definitionFor($directive::class);
if ($definition === null) {
continue;
}
$nodes[] = $this->buildDirectiveNode($directive, $definition);
}
return $nodes;
}

private function buildDirectiveNode(DirectiveInterface $directive, DirectiveDefinition $definition): DirectiveNode
{
$arguments = $this->registry->argumentsFor($directive::class);
$reflection = new ReflectionClass($directive);

$argumentNodes = [];
foreach ($arguments as $argument) {
if (! $reflection->hasProperty($argument->name)) {
continue;
}
$property = $reflection->getProperty($argument->name);
$value = $property->getValue($directive);

$argumentNodes[] = new ArgumentNode([
'name' => new NameNode(['value' => $argument->name]),
'value' => AST::astFromValue($value, $argument->type),
]);
}

return new DirectiveNode([
'name' => new NameNode(['value' => $definition->name]),
'arguments' => new NodeList($argumentNodes),
]);
}

/** @param list<DirectiveNode> $directiveNodes */
public static function buildFieldDefinitionNode(string $name, array $directiveNodes): FieldDefinitionNode
{
return new FieldDefinitionNode([
'name' => new NameNode(['value' => $name]),
'type' => new NamedTypeNode(['name' => new NameNode(['value' => 'Unknown'])]),
'arguments' => new NodeList([]),
'directives' => new NodeList($directiveNodes),
]);
}

/** @param list<DirectiveNode> $directiveNodes */
public static function buildInputValueDefinitionNode(string $name, array $directiveNodes): InputValueDefinitionNode
{
return new InputValueDefinitionNode([
'name' => new NameNode(['value' => $name]),
'type' => new NamedTypeNode(['name' => new NameNode(['value' => 'Unknown'])]),
'directives' => new NodeList($directiveNodes),
]);
}

/** @param list<DirectiveNode> $directiveNodes */
public static function buildObjectTypeDefinitionNode(string $name, array $directiveNodes): ObjectTypeDefinitionNode
{
return new ObjectTypeDefinitionNode([
'name' => new NameNode(['value' => $name]),
'interfaces' => new NodeList([]),
'directives' => new NodeList($directiveNodes),
'fields' => new NodeList([]),
]);
}

/** @param list<DirectiveNode> $directiveNodes */
public static function buildInputObjectTypeDefinitionNode(string $name, array $directiveNodes): InputObjectTypeDefinitionNode
{
return new InputObjectTypeDefinitionNode([
'name' => new NameNode(['value' => $name]),
'directives' => new NodeList($directiveNodes),
'fields' => new NodeList([]),
]);
}
}
29 changes: 29 additions & 0 deletions src/Directives/DirectiveDefinition.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

declare(strict_types=1);

namespace TheCodingMachine\GraphQLite\Directives;

/**
* Metadata for a directive: name, valid locations, and an optional description. Returned by
* {@see DirectiveInterface::definition()}. Repeatability isn't here; it's read from the directive
* class's `#[Attribute]` flags (see {@see DirectiveValidator::isRepeatable()}).
*
* Argument types aren't listed here; they're read from the directive class's constructor when it's
* registered.
*
* Set {@see $builtIn} to true when the attribute binds behavior to a directive webonyx already
* defines (`@oneOf`, `@deprecated`, ...). Those still run their apply hook, but we don't register a
* second definition for them since webonyx already declares them on the schema.
*/
final class DirectiveDefinition
{
/** @param list<DirectiveLocation> $locations */
public function __construct(
public readonly string $name,
public readonly array $locations,
public readonly string|null $description = null,
public readonly bool $builtIn = false,
) {
}
}
14 changes: 14 additions & 0 deletions src/Directives/DirectiveInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

declare(strict_types=1);

namespace TheCodingMachine\GraphQLite\Directives;

/**
* Base marker for any GraphQLite custom directive. The one requirement is the static
* {@see definition} method returning the directive's metadata.
*/
interface DirectiveInterface
{
public static function definition(): DirectiveDefinition;
}
Loading
Loading