Skip to content

Commit

Permalink
feat(graphql): New directive @type (#157)
Browse files Browse the repository at this point in the history
  • Loading branch information
LastDragon-ru authored May 1, 2024
2 parents 1460b62 + 9e5f219 commit e55b01c
Show file tree
Hide file tree
Showing 23 changed files with 617 additions and 157 deletions.
6 changes: 6 additions & 0 deletions packages/graphql/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,12 @@ Unlike the `@paginate` (and similar) directive, the `@stream` provides a uniform

[Read more](<docs/Directives/@stream.md>).

## `@type`

Converts scalar into GraphQL Type. Similar to Lighthouse's `@scalar` directive, but uses Laravel Container to resolve instance and also supports PHP enums.

[Read more](<docs/Directives/@type.md>).

[//]: # (end: 73f7f4a1d86b7731354837c827f1b9f9aa729879639aeab4fe63985913469f48)

# Scalars
Expand Down
35 changes: 35 additions & 0 deletions packages/graphql/docs/Directives/@type.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# `@type`

Converts scalar into GraphQL Type. Similar to Lighthouse's `@scalar` directive, but uses Laravel Container to resolve instance and also supports PHP enums.

[include:exec]: <../../../../dev/artisan dev:directive @type>
[//]: # (start: 90e03adede767b669ea39c2e4de42431b0ee3405074a4297b01666628f98ca45)
[//]: # (warning: Generated automatically. Do not edit.)

```graphql
"""
Converts scalar into GraphQL Type. Similar to Lighthouse's `@scalar`
directive, but uses Laravel Container to resolve instance and also
supports PHP enums.
"""
directive @type(
"""
Reference to a PHP Class/Enum (FQN).
If not PHP Enum, the Laravel Container with the following additional
arguments will be used to resolver the instance:
* `string $name` - the type name.
* `GraphQL\Language\AST\ScalarTypeDefinitionNode $node` - the AST node.
* `array&ScalarConfig $config` - the scalar configuration (if `GraphQL\Type\Definition\ScalarType`).
Resolved instance must be an `GraphQL\Type\Definition\Type&GraphQL\Type\Definition\NamedType` and have a name equal
to `$name` argument.
"""
class: String!
)
on
| SCALAR
```

[//]: # (end: 90e03adede767b669ea39c2e4de42431b0ee3405074a4297b01666628f98ca45)
6 changes: 5 additions & 1 deletion packages/graphql/src/Builder/Contracts/TypeDefinition.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use GraphQL\Type\Definition\NamedType;
use GraphQL\Type\Definition\Type;
use LastDragon_ru\LaraASP\GraphQL\Builder\Manipulator;
use LastDragon_ru\LaraASP\GraphQL\Utils\TypeReference;

interface TypeDefinition {
/**
Expand All @@ -17,7 +18,10 @@ public function getTypeName(TypeSource $source, Context $context): string;
/**
* Returns the type definition for given Source if possible. The name must be equal to `$name`.
*
* @return (TypeDefinitionNode&Node)|(Type&NamedType)|null
* @see TypeReference
*
* @return (TypeDefinitionNode&Node)|(Type&NamedType)|null Returning {@see Type} is deprecated, please use
* {@see TypeReference} instead.
*/
public function getTypeDefinition(
Manipulator $manipulator,
Expand Down
15 changes: 15 additions & 0 deletions packages/graphql/src/Builder/Manipulator.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
use LastDragon_ru\LaraASP\GraphQL\Builder\Context\HandlerContextOperators;
use LastDragon_ru\LaraASP\GraphQL\Builder\Contracts\Context;
use LastDragon_ru\LaraASP\GraphQL\Builder\Contracts\Operator;
use LastDragon_ru\LaraASP\GraphQL\Builder\Contracts\TypeDefinition;
use LastDragon_ru\LaraASP\GraphQL\Builder\Contracts\TypeProvider;
use LastDragon_ru\LaraASP\GraphQL\Builder\Contracts\TypeSource;
use LastDragon_ru\LaraASP\GraphQL\Builder\Exceptions\FakeTypeDefinitionIsNotFake;
Expand All @@ -34,7 +35,9 @@
use LastDragon_ru\LaraASP\GraphQL\Builder\Sources\InterfaceSource;
use LastDragon_ru\LaraASP\GraphQL\Builder\Sources\ObjectSource;
use LastDragon_ru\LaraASP\GraphQL\Builder\Sources\Source;
use LastDragon_ru\LaraASP\GraphQL\Package;
use LastDragon_ru\LaraASP\GraphQL\Utils\AstManipulator;
use LastDragon_ru\LaraASP\GraphQL\Utils\TypeReference;
use Nuwave\Lighthouse\Schema\DirectiveLocator;
use Nuwave\Lighthouse\Support\Contracts\Directive;
use Override;
Expand All @@ -43,6 +46,7 @@
use function array_unshift;
use function count;
use function implode;
use function trigger_deprecation;

class Manipulator extends AstManipulator implements TypeProvider {
// <editor-fold desc="TypeProvider">
Expand All @@ -63,6 +67,17 @@ public function getType(string $definition, TypeSource $source, Context $context
// Create new
$node = $instance->getTypeDefinition($this, $source, $context, $name);

if ($node instanceof Type && !($node instanceof TypeReference)) {
trigger_deprecation(
Package::Name,
'%{VERSION}',
'Returning `%s` from `%s` is deprecated, please use `%s` instead.',
Type::class,
TypeDefinition::class.'::getTypeDefinition()',
TypeReference::class,
);
}

if (!$node) {
throw new TypeDefinitionImpossibleToCreateType($definition, $source, $context);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
<?php declare(strict_types = 1);

namespace LastDragon_ru\LaraASP\GraphQL\Utils\Definitions;
namespace LastDragon_ru\LaraASP\GraphQL\Directives\Definitions;

use LastDragon_ru\LaraASP\GraphQL\Utils\Directives\AsEnum;
use LastDragon_ru\LaraASP\GraphQL\Directives\Type;

/**
* @internal
*/
class LaraAspAsEnumDirective extends AsEnum {
class TypeDirective extends Type {
// Lighthouse loads all classes from directive namespace this leads to
// 'Class "Orchestra\Testbench\TestCase" not found' error for our *Test
// classes. This class required to avoid this error.
Expand Down
135 changes: 135 additions & 0 deletions packages/graphql/src/Directives/Type.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
<?php declare(strict_types = 1);

namespace LastDragon_ru\LaraASP\GraphQL\Directives;

use GraphQL\Language\AST\ScalarTypeDefinitionNode;
use GraphQL\Type\Definition\EnumType;
use GraphQL\Type\Definition\NamedType;
use GraphQL\Type\Definition\PhpEnumType;
use GraphQL\Type\Definition\ScalarType;
use GraphQL\Type\Definition\Type as GraphQLType;
use LastDragon_ru\LaraASP\Core\Application\ContainerResolver;
use LastDragon_ru\LaraASP\Core\Utils\Cast;
use Nuwave\Lighthouse\Exceptions\DefinitionException;
use Nuwave\Lighthouse\Schema\DirectiveLocator;
use Nuwave\Lighthouse\Schema\Directives\BaseDirective;
use Nuwave\Lighthouse\Schema\Values\TypeValue;
use Nuwave\Lighthouse\Support\Contracts\TypeResolver;
use Override;
use UnitEnum;

use function is_a;

/**
* @phpstan-import-type ScalarConfig from ScalarType
*/
class Type extends BaseDirective implements TypeResolver {
final protected const ArgClass = 'class';

public function __construct(
protected readonly ContainerResolver $container,
) {
// empty
}

#[Override]
public static function definition(): string {
$name = DirectiveLocator::directiveName(static::class);
$argClass = self::ArgClass;
$nodeClass = ScalarTypeDefinitionNode::class;
$typeClass = GraphQLType::class.'&'.NamedType::class;
$scalarClass = ScalarType::class;

return <<<GRAPHQL
"""
Converts scalar into GraphQL Type. Similar to Lighthouse's `@scalar`
directive, but uses Laravel Container to resolve instance and also
supports PHP enums.
"""
directive @{$name}(
"""
Reference to a PHP Class/Enum (FQN).
If not PHP Enum, the Laravel Container with the following additional
arguments will be used to resolver the instance:
* `string \$name` - the type name.
* `{$nodeClass} \$node` - the AST node.
* `array&ScalarConfig \$config` - the scalar configuration (if `{$scalarClass}`).
Resolved instance must be an `{$typeClass}` and have a name equal
to `\$name` argument.
"""
{$argClass}: String!
) on SCALAR
GRAPHQL;
}

/**
* @return GraphQLType&NamedType
*/
#[Override]
public function resolveNode(TypeValue $value): GraphQLType {
// Type?
$class = Cast::toString($this->directiveArgValue(self::ArgClass));
$node = Cast::to(ScalarTypeDefinitionNode::class, $value->getTypeDefinition());
$name = $value->getTypeDefinitionName();
$type = match (true) {
is_a($class, GraphQLType::class, true) && is_a($class, NamedType::class, true)
=> $this->createType($name, $class, $node),
is_a($class, UnitEnum::class, true)
=> $this->createEnum($name, $class, $node),
default
=> null,
};

if (!($type instanceof GraphQLType) || !($type instanceof NamedType)) {
throw new DefinitionException(
"The `{$class}` is not a GraphQL type (`scalar {$name}`).",
);
} elseif ($type->name() !== $name) {
throw new DefinitionException(
"The type name must be `{$name}`, `{$type->name()}` given (`scalar {$name}`).",
);
} else {
// ok
}

// Return
return $type;
}

/**
* @param class-string<UnitEnum> $class
*/
private function createEnum(string $name, string $class, ScalarTypeDefinitionNode $node): EnumType {
return new PhpEnumType($class, $name);
}

/**
* @param class-string<GraphQLType&NamedType> $class
*/
private function createType(string $name, string $class, ScalarTypeDefinitionNode $node): object {
$args = [
'name' => $name,
'node' => $node,
];

if (is_a($class, ScalarType::class, true)) {
$args['config'] = $this->createTypeScalarConfig($name, $class, $node);
}

return $this->container->getInstance()->make($class, $args);
}

/**
* @return ScalarConfig
*/
private function createTypeScalarConfig(string $name, string $class, ScalarTypeDefinitionNode $node): array {
return [
'name' => $name,
'astNode' => $node,
'description' => $node->description?->value,
];
}
}
Loading

0 comments on commit e55b01c

Please sign in to comment.