Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extend directive command to allow choosing interfaces #1251

Merged
merged 16 commits into from Jun 11, 2020
Merged
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -12,6 +12,7 @@ You can find and compare releases at the [GitHub release page](https://github.co
### Added

- Add `@withCount` directive to eager load relationship counts on field access https://github.com/nuwave/lighthouse/pull/1390
- Extend `lighthouse:directive` artisan command to allow choosing interfaces https://github.com/nuwave/lighthouse/pull/1251

### Changed

Expand Down
5 changes: 5 additions & 0 deletions docs/master/api-reference/commands.md
Expand Up @@ -15,6 +15,11 @@ Create a class for a GraphQL directive.

php artisan lighthouse:directive

Use the `--type`, `--field` and `--argument` options to create type, field and
argument directives, respectively. The command will then ask you which
interfaces the directive should implement and add the required method stubs and
imports for you.

## ide-helper

Create IDE helper files to improve type checking and autocompletion.
Expand Down
2 changes: 1 addition & 1 deletion docs/master/eloquent/getting-started.md
Expand Up @@ -152,7 +152,7 @@ or [@first](../api-reference/directives.md#first), allow you to re-use those sco

```graphql
type Query {
users: [User]! @all(scopes: ["verified"])
users: [User]! @all(scopes: ["verified"])
}
```

Expand Down
156 changes: 154 additions & 2 deletions src/Console/DirectiveCommand.php
Expand Up @@ -2,6 +2,10 @@

namespace Nuwave\Lighthouse\Console;

use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Symfony\Component\Console\Input\InputOption;

class DirectiveCommand extends LighthouseGeneratorCommand
{
/**
Expand All @@ -16,7 +20,7 @@ class DirectiveCommand extends LighthouseGeneratorCommand
*
* @var string
*/
protected $description = 'Create a class for a directive.';
protected $description = 'Create a class for a custom schema directive.';

/**
* The type of class being generated.
Expand All @@ -25,6 +29,13 @@ class DirectiveCommand extends LighthouseGeneratorCommand
*/
protected $type = 'Directive';

/**
* The imports required by the various interfaces, if any.
*
* @var \Illuminate\Support\Collection<string>
*/
protected $imports;

protected function getNameInput(): string
{
return parent::getNameInput().'Directive';
Expand All @@ -36,10 +47,151 @@ protected function namespaceConfigKey(): string
}

/**
* Get the stub file for the generator.
* Build the class with the given name.
*
* @param string $name
*
* @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
*/
protected function buildClass($name): string
{
$this->imports = new Collection();

$stub = parent::buildClass($name);

if ($this->option('type')) {
$this->askForInterfaces($stub, [
'TypeManipulator',
'TypeMiddleware',
'TypeResolver',
'TypeExtensionManipulator',
]);
}

if ($this->option('field')) {
$this->askForInterfaces($stub, [
'FieldResolver',
'FieldMiddleware',
'FieldManipulator',
]);
}

if ($this->option('argument')) {
// Arg directives always either implement ArgDirective or ArgDirectiveForArray.
if ($this->confirm('Will your argument directive apply to a list of items?')) {
spawnia marked this conversation as resolved.
Show resolved Hide resolved
$this->implementInterface($stub, 'ArgDirectiveForArray', false);
} else {
$this->implementInterface($stub, 'ArgDirective', false);
}

$this->askForInterfaces($stub, [
'ArgTransformerDirective',
'ArgBuilderDirective',
'ArgResolver',
'ArgManipulator',
]);
}

if ($this->imports->isNotEmpty()) {
$stub = str_replace(
'{{ imports }}',
$this->imports
->filter()
->unique()
->implode("\n"),
$stub
);
}

$this->cleanupTemplatePlaceholders($stub);

return $stub;
}

/**
* Ask the user if the directive should implement any of the given interfaces.
*
* @param array<string> $interfaces
*/
protected function askForInterfaces(string &$stub, array $interfaces): void
{
foreach ($interfaces as $interface) {
if ($this->confirm('Should the directive implement the '.$interface.' middleware?')) {
$this->implementInterface($stub, $interface);
}
}
}

protected function implementInterface(string &$stub, string $interface, bool $withMethods = true): void
{
$stub = str_replace(
'{{ imports }}',
'use Nuwave\\Lighthouse\\Support\\Contracts\\'.$interface.";\n{{ imports }}",
$stub
);

$stub = str_replace(
'{{ implements }}',
$interface.', {{ implements }}',
$stub
);

if (! $withMethods) {
// No need to implement methods for this interface, so return early.
return;
}

$imports = $this->files->get($this->getStubForInterfaceImports($interface));
$imports = explode("\n", $imports);

$this->imports->push(...$imports);

$stub = str_replace(
'{{ methods }}',
$this->files->get($this->getStubForInterfaceMethods($interface))."\n\n{{ methods }}",
$stub
);
}

protected function cleanupTemplatePlaceholders(string &$stub): void
{
// If one or more interfaces are enabled, we are left with ", {{ implements }}".
$stub = str_replace(', {{ implements }}', '', $stub);

// If no interfaces were enabled, we are left with "implements {{ implements }}".
$stub = str_replace('implements {{ implements }}', '', $stub);

// When no imports were made, the {{ imports }} is still there.
$stub = str_replace("{{ imports }}\n", '', $stub);

// Whether or not methods were implemented, the {{ methods }} is still there.
$stub = str_replace("\n\n{{ methods }}", '', $stub);
}

protected function getStub(): string
{
return __DIR__.'/stubs/directive.stub';
}

protected function getStubForInterfaceMethods(string $interface): string
{
return __DIR__.'/stubs/directives/'.Str::snake($interface).'.stub';
}

protected function getStubForInterfaceImports(string $interface): string
{
return __DIR__.'/stubs/directives/'.Str::snake($interface).'_imports.stub';
}

/**
* @return array<array<mixed>>
*/
protected function getOptions(): array
{
return [
['type', null, InputOption::VALUE_NONE, 'Create a directive that can be applied to types.'],
['field', null, InputOption::VALUE_NONE, 'Create a directive that can be applied to fields.'],
['argument', null, InputOption::VALUE_NONE, 'Create a directive that can be applied to arguments.'],
];
}
}
5 changes: 4 additions & 1 deletion src/Console/stubs/directive.stub
Expand Up @@ -3,8 +3,11 @@
namespace DummyNamespace;

use Nuwave\Lighthouse\Schema\Directives\BaseDirective;
{{ imports }}

class DummyClass extends BaseDirective
class DummyClass extends BaseDirective implements {{ implements }}
{
// TODO implement the directive https://lighthouse-php.com/master/custom-directives/getting-started.html

{{ methods }}
}
11 changes: 11 additions & 0 deletions src/Console/stubs/directives/arg_builder_directive.stub
@@ -0,0 +1,11 @@
/**
* Add additional constraints to the builder based on the given argument value.
*
* @param \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder $builder
* @param mixed $value
* @return \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder
*/
public function handleBuilder($builder, $value)
{
// TODO implement the arg builder
}
spawnia marked this conversation as resolved.
Show resolved Hide resolved
Empty file.
17 changes: 17 additions & 0 deletions src/Console/stubs/directives/arg_manipulator.stub
@@ -0,0 +1,17 @@
/**
* Manipulate the AST.
*
* @param \Nuwave\Lighthouse\Schema\AST\DocumentAST $documentAST
* @param \GraphQL\Language\AST\InputValueDefinitionNode $argDefinition
* @param \GraphQL\Language\AST\FieldDefinitionNode $parentField
* @param \GraphQL\Language\AST\ObjectTypeDefinitionNode $parentType
* @return void
*/
public function manipulateArgDefinition(
DocumentAST &$documentAST,
InputValueDefinitionNode &$argDefinition,
FieldDefinitionNode &$parentField,
ObjectTypeDefinitionNode &$parentType
) {
// TODO implement the arg manipulator
}
4 changes: 4 additions & 0 deletions src/Console/stubs/directives/arg_manipulator_imports.stub
@@ -0,0 +1,4 @@
use GraphQL\Language\AST\FieldDefinitionNode;
use GraphQL\Language\AST\InputValueDefinitionNode;
use GraphQL\Language\AST\ObjectTypeDefinitionNode;
use Nuwave\Lighthouse\Schema\AST\DocumentAST;
9 changes: 9 additions & 0 deletions src/Console/stubs/directives/arg_resolver.stub
@@ -0,0 +1,9 @@
/**
* @param mixed $root The result of the parent resolver.
* @param mixed|\Nuwave\Lighthouse\Execution\Arguments\ArgumentSet|\Nuwave\Lighthouse\Execution\Arguments\ArgumentSet[] $value The slice of arguments that belongs to this nested resolver.
* @return mixed
*/
public function __invoke($root, $value)
{
// TODO implement the arg resolver
}
1 change: 1 addition & 0 deletions src/Console/stubs/directives/arg_resolver_imports.stub
@@ -0,0 +1 @@
use Nuwave\Lighthouse\Execution\Arguments\ArgumentSet;
10 changes: 10 additions & 0 deletions src/Console/stubs/directives/arg_transformer_directive.stub
@@ -0,0 +1,10 @@
/**
* Apply transformations on the value of an argument given to a field.
*
* @param mixed $argumentValue
* @return mixed
*/
public function transform($argumentValue)
{
// TODO implement the arg transformer
}
Empty file.
15 changes: 15 additions & 0 deletions src/Console/stubs/directives/field_manipulator.stub
@@ -0,0 +1,15 @@
/**
* Manipulate the AST based on a field definition.
*
* @param \Nuwave\Lighthouse\Schema\AST\DocumentAST $documentAST
* @param \GraphQL\Language\AST\FieldDefinitionNode $fieldDefinition
* @param \GraphQL\Language\AST\ObjectTypeDefinitionNode $parentType
* @return void
*/
public function manipulateFieldDefinition(
DocumentAST &$documentAST,
FieldDefinitionNode &$fieldDefinition,
ObjectTypeDefinitionNode &$parentType
) {
// TODO implement the field manipulator
}
3 changes: 3 additions & 0 deletions src/Console/stubs/directives/field_manipulator_imports.stub
@@ -0,0 +1,3 @@
use GraphQL\Language\AST\FieldDefinitionNode;
use GraphQL\Language\AST\ObjectTypeDefinitionNode;
use Nuwave\Lighthouse\Schema\AST\DocumentAST;
11 changes: 11 additions & 0 deletions src/Console/stubs/directives/field_middleware.stub
@@ -0,0 +1,11 @@
/**
* Wrap around the final field resolver.
*
* @param \Nuwave\Lighthouse\Schema\Values\FieldValue $fieldValue
* @param \Closure $next
* @return \Nuwave\Lighthouse\Schema\Values\FieldValue
*/
public function handleField(FieldValue $fieldValue, Closure $next)
{
// TODO implement the field middleware
}
2 changes: 2 additions & 0 deletions src/Console/stubs/directives/field_middleware_imports.stub
@@ -0,0 +1,2 @@
use Closure;
use Nuwave\Lighthouse\Schema\Values\FieldValue;
13 changes: 13 additions & 0 deletions src/Console/stubs/directives/field_resolver.stub
@@ -0,0 +1,13 @@
/**
* Set a field resolver on the FieldValue.
*
* This must call $fieldValue->setResolver() before returning
* the FieldValue.
*
* @param \Nuwave\Lighthouse\Schema\Values\FieldValue $fieldValue
* @return \Nuwave\Lighthouse\Schema\Values\FieldValue
*/
public function resolveField(FieldValue $fieldValue)
{
// TODO implement the field resolver
}
1 change: 1 addition & 0 deletions src/Console/stubs/directives/field_resolver_imports.stub
@@ -0,0 +1 @@
use Nuwave\Lighthouse\Schema\Values\FieldValue;
11 changes: 11 additions & 0 deletions src/Console/stubs/directives/type_extension_manipulator.stub
@@ -0,0 +1,11 @@
/**
* Apply manipulations from a type extension node.
*
* @param \Nuwave\Lighthouse\Schema\AST\DocumentAST $documentAST
* @param \GraphQL\Language\AST\TypeExtensionNode $typeExtension
* @return void
*/
public function manipulateTypeExtension(DocumentAST &$documentAST, TypeExtensionNode &$typeExtension)
{
// TODO implement the type extension manipulator
}
@@ -0,0 +1,2 @@
use GraphQL\Language\AST\TypeExtensionNode;
use Nuwave\Lighthouse\Schema\AST\DocumentAST;
11 changes: 11 additions & 0 deletions src/Console/stubs/directives/type_manipulator.stub
@@ -0,0 +1,11 @@
/**
* Apply manipulations from a type definition node.
*
* @param \Nuwave\Lighthouse\Schema\AST\DocumentAST $documentAST
* @param \GraphQL\Language\AST\TypeDefinitionNode $typeDefinition
* @return void
*/
public function manipulateTypeDefinition(DocumentAST &$documentAST, TypeDefinitionNode &$typeDefinition)
{
// TODO implement the type manipulator
}
2 changes: 2 additions & 0 deletions src/Console/stubs/directives/type_manipulator_imports.stub
@@ -0,0 +1,2 @@
use GraphQL\Language\AST\TypeDefinitionNode;
use Nuwave\Lighthouse\Schema\AST\DocumentAST;
11 changes: 11 additions & 0 deletions src/Console/stubs/directives/type_middleware.stub
@@ -0,0 +1,11 @@
/**
* Handle a type AST as it is converted to an executable type.
*
* @param \Nuwave\Lighthouse\Schema\Values\TypeValue $value
* @param \Closure $next
* @return \GraphQL\Type\Definition\Type
*/
public function handleNode(TypeValue $value, Closure $next)
{
// TODO implement the type middleware
}
2 changes: 2 additions & 0 deletions src/Console/stubs/directives/type_middleware_imports.stub
@@ -0,0 +1,2 @@
use Closure;
use Nuwave\Lighthouse\Schema\Values\TypeValue;