Skip to content
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;
}
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;
}
53 changes: 53 additions & 0 deletions src/Directives/DirectiveLocation.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

declare(strict_types=1);

namespace TheCodingMachine\GraphQLite\Directives;

/**
* GraphQL directive locations from the spec, both type-system and executable.
*
* Only FIELD_DEFINITION, INPUT_FIELD_DEFINITION, OBJECT and INPUT_OBJECT have apply hooks so far;
* the rest are listed but not yet wired. Backing values match the spec strings, so they line up
* with webonyx's location strings without conversion.
*/
enum DirectiveLocation: string
{
// Executable locations
case QUERY = 'QUERY';
case MUTATION = 'MUTATION';
case SUBSCRIPTION = 'SUBSCRIPTION';
case FIELD = 'FIELD';
case FRAGMENT_DEFINITION = 'FRAGMENT_DEFINITION';
case FRAGMENT_SPREAD = 'FRAGMENT_SPREAD';
case INLINE_FRAGMENT = 'INLINE_FRAGMENT';
case VARIABLE_DEFINITION = 'VARIABLE_DEFINITION';

// Type-system locations
case SCHEMA = 'SCHEMA';
case SCALAR = 'SCALAR';
case OBJECT = 'OBJECT';
case FIELD_DEFINITION = 'FIELD_DEFINITION';
case ARGUMENT_DEFINITION = 'ARGUMENT_DEFINITION';
case INTERFACE = 'INTERFACE';
case UNION = 'UNION';
case ENUM = 'ENUM';
case ENUM_VALUE = 'ENUM_VALUE';
case INPUT_OBJECT = 'INPUT_OBJECT';
case INPUT_FIELD_DEFINITION = 'INPUT_FIELD_DEFINITION';

public function isExecutable(): bool
{
return match ($this) {
self::QUERY, self::MUTATION, self::SUBSCRIPTION, self::FIELD,
self::FRAGMENT_DEFINITION, self::FRAGMENT_SPREAD, self::INLINE_FRAGMENT,
self::VARIABLE_DEFINITION => true,
default => false,
};
}

public function isTypeSystem(): bool
{
return ! $this->isExecutable();
}
}
Loading
Loading