Skip to content

Commit

Permalink
feat(graphql)!: Builder type detection (= different types for each bu…
Browse files Browse the repository at this point in the history
…ilder).

Closes: #23
  • Loading branch information
LastDragon-ru committed Oct 30, 2022
2 parents 86d3547 + d677e65 commit 639c3bb
Show file tree
Hide file tree
Showing 15 changed files with 563 additions and 49 deletions.
19 changes: 19 additions & 0 deletions packages/graphql/src/Builder/BuilderInfo.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php declare(strict_types = 1);

namespace LastDragon_ru\LaraASP\GraphQL\Builder;

class BuilderInfo {
public function __construct(
protected string $name,
protected object $builder,
) {
}

public function getName(): string {
return $this->name;
}

public function getBuilder(): object {
return $this->builder;
}
}
81 changes: 75 additions & 6 deletions packages/graphql/src/Builder/Directives/HandlerDirective.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,16 @@

namespace LastDragon_ru\LaraASP\GraphQL\Builder\Directives;

use Closure;
use GraphQL\Language\AST\FieldDefinitionNode;
use GraphQL\Language\AST\InputValueDefinitionNode;
use GraphQL\Language\AST\ListTypeNode;
use Illuminate\Contracts\Container\Container;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Query\Builder as QueryBuilder;
use Illuminate\Support\Collection;
use Laravel\Scout\Builder as ScoutBuilder;
use LastDragon_ru\LaraASP\GraphQL\Builder\BuilderInfo;
use LastDragon_ru\LaraASP\GraphQL\Builder\Contracts\Handler;
use LastDragon_ru\LaraASP\GraphQL\Builder\Contracts\Operator;
use LastDragon_ru\LaraASP\GraphQL\Builder\Exceptions\Client\ConditionEmpty;
Expand All @@ -16,17 +22,26 @@
use LastDragon_ru\LaraASP\GraphQL\Builder\Property;
use LastDragon_ru\LaraASP\GraphQL\Utils\ArgumentFactory;
use Nuwave\Lighthouse\Execution\Arguments\ArgumentSet;
use Nuwave\Lighthouse\Pagination\PaginateDirective;
use Nuwave\Lighthouse\Schema\DirectiveLocator;
use Nuwave\Lighthouse\Schema\Directives\AllDirective;
use Nuwave\Lighthouse\Schema\Directives\BaseDirective;
use Nuwave\Lighthouse\Scout\SearchDirective;
use Nuwave\Lighthouse\Support\Utils;
use ReflectionClass;
use ReflectionFunction;
use ReflectionNamedType;

use function array_keys;
use function count;
use function is_a;
use function is_array;

abstract class HandlerDirective extends BaseDirective implements Handler {
public function __construct(
private Container $container,
private ArgumentFactory $factory,
private DirectiveLocator $directives,
) {
// empty
}
Expand All @@ -39,6 +54,10 @@ protected function getFactory(): ArgumentFactory {
return $this->factory;
}

protected function getDirectives(): DirectiveLocator {
return $this->directives;
}

/**
* @template T of object
*
Expand Down Expand Up @@ -97,18 +116,19 @@ public function handle(object $builder, Property $property, ArgumentSet $conditi
* @return T
*/
protected function call(object $builder, Property $property, ArgumentSet $operator): object {
// Operator & Value
/** @var Operator|null $op */
$op = null;
$value = null;
$filter = Utils::instanceofMatcher(Operator::class);

// Arguments?
if (count($operator->arguments) > 1) {
throw new ConditionTooManyOperators(
array_keys($operator->arguments),
);
}

// Operator & Value
/** @var Operator|null $op */
$op = null;
$value = null;
$filter = Utils::instanceofMatcher(Operator::class);

foreach ($operator->arguments as $name => $argument) {
/** @var Collection<int, Operator> $operators */
$operators = $argument->directives->filter($filter);
Expand Down Expand Up @@ -140,4 +160,53 @@ protected function call(object $builder, Property $property, ArgumentSet $operat
// Return
return $op->call($this, $builder, $property, $value);
}

protected function getBuilderInfo(FieldDefinitionNode $field): BuilderInfo {
// Scout?
$scout = false;
$directives = $this->getDirectives();

foreach ($field->arguments as $argument) {
if ($directives->associatedOfType($argument, SearchDirective::class)->isNotEmpty()) {
$scout = true;
break;
}
}

if ($scout) {
$builder = (new ReflectionClass(ScoutBuilder::class))->newInstanceWithoutConstructor();
$name = 'Scout';
$info = new BuilderInfo($name, $builder);

return $info;
}

// Query?
$argument = 'builder';
$directive = $directives->associatedOfType($field, AllDirective::class)->first()
?? $directives->associatedOfType($field, PaginateDirective::class)->first();
$resolver = $directive instanceof BaseDirective && $directive->directiveHasArgument($argument)
? $directive->getResolverFromArgument($argument)
: null;

if ($resolver instanceof Closure) {
$type = (new ReflectionFunction($resolver))->getReturnType();
$type = $type instanceof ReflectionNamedType ? $type->getName() : null;

if ($type && is_a($type, QueryBuilder::class, true)) {
$builder = (new ReflectionClass($type))->newInstanceWithoutConstructor();
$name = 'Query';
$info = new BuilderInfo($name, $builder);

return $info;
}
}

// Eloquent (default)
$builder = (new ReflectionClass(EloquentBuilder::class))->newInstanceWithoutConstructor();
$name = '';
$info = new BuilderInfo($name, $builder);

return $info;
}
}
210 changes: 210 additions & 0 deletions packages/graphql/src/Builder/Directives/HandlerDirectiveTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
<?php declare(strict_types = 1);

namespace LastDragon_ru\LaraASP\GraphQL\Builder\Directives;

use Closure;
use Exception;
use GraphQL\Language\AST\FieldDefinitionNode;
use GraphQL\Language\Parser;
use Illuminate\Contracts\Container\Container;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Query\Builder as QueryBuilder;
use Laravel\Scout\Builder as ScoutBuilder;
use LastDragon_ru\LaraASP\GraphQL\Builder\BuilderInfo;
use LastDragon_ru\LaraASP\GraphQL\Testing\Package\TestCase;
use LastDragon_ru\LaraASP\GraphQL\Utils\ArgumentFactory;
use Mockery;
use Nuwave\Lighthouse\Pagination\PaginateDirective;
use Nuwave\Lighthouse\Schema\DirectiveLocator;
use Nuwave\Lighthouse\Schema\Directives\AllDirective;
use Nuwave\Lighthouse\Scout\SearchDirective;

use function json_encode;

use const JSON_THROW_ON_ERROR;

/**
* @internal
* @coversDefaultClass \LastDragon_ru\LaraASP\GraphQL\Builder\Directives\HandlerDirective
*/
class HandlerDirectiveTest extends TestCase {
// <editor-fold desc="Tests">
// =========================================================================
/**
* @covers ::getBuilderInfo
*
* @dataProvider dataProviderGetBuilderInfo
*
* @param array{name: string, builder: string} $expected
* @param Closure(DirectiveLocator): FieldDefinitionNode $fieldFactory
*/
public function testGetBuilderInfo(array $expected, Closure $fieldFactory): void {
$directives = $this->app->make(DirectiveLocator::class);
$argFactory = Mockery::mock(ArgumentFactory::class);
$container = Mockery::mock(Container::class);
$field = $fieldFactory($directives);
$directive = new class($container, $argFactory, $directives) extends HandlerDirective {
public static function definition(): string {
throw new Exception('should not be called.');
}

public function getBuilderInfo(FieldDefinitionNode $field): BuilderInfo {
return parent::getBuilderInfo($field);
}
};

$actual = $directive->getBuilderInfo($field);

self::assertEquals(
$expected,
[
'name' => $actual->getName(),
'builder' => $actual->getBuilder()::class,
],
);
}
// </editor-fold>

// <editor-fold desc="DataProviders">
// =========================================================================
/**
* @return array<string, array{
* array{name: string, builder: string},
* Closure(DirectiveLocator): FieldDefinitionNode,
* }>
*/
public function dataProviderGetBuilderInfo(): array {
return [
'default' => [
[
'name' => '',
'builder' => EloquentBuilder::class,
],
static function (DirectiveLocator $directives): FieldDefinitionNode {
return Parser::fieldDefinition('field: String');
},
],
'@search' => [
[
'name' => 'Scout',
'builder' => ScoutBuilder::class,
],
static function (DirectiveLocator $directives): FieldDefinitionNode {
$directives->setResolved('search', SearchDirective::class);

return Parser::fieldDefinition('field(search: String @search): String');
},
],
'@all' => [
[
'name' => '',
'builder' => EloquentBuilder::class,
],
static function (DirectiveLocator $directives): FieldDefinitionNode {
$directives->setResolved('all', AllDirective::class);

return Parser::fieldDefinition('field: String @all');
},
],
'@all(query)' => [
[
'name' => 'Query',
'builder' => QueryBuilder::class,
],
static function (DirectiveLocator $directives): FieldDefinitionNode {
$directives->setResolved('all', AllDirective::class);

$class = json_encode(HandlerDirectiveTest__QueryBuilderResolver::class, JSON_THROW_ON_ERROR);
$field = Parser::fieldDefinition("field: String @all(builder: {$class})");

return $field;
},
],
'@all(custom)' => [
[
'name' => 'Query',
'builder' => HandlerDirectiveTest__CustomBuilder::class,
],
static function (DirectiveLocator $directives): FieldDefinitionNode {
$directives->setResolved('all', AllDirective::class);

$class = json_encode(HandlerDirectiveTest__CustomBuilderResolver::class, JSON_THROW_ON_ERROR);
$field = Parser::fieldDefinition("field: String @all(builder: {$class})");

return $field;
},
],
'@paginate' => [
[
'name' => '',
'builder' => EloquentBuilder::class,
],
static function (DirectiveLocator $directives): FieldDefinitionNode {
$directives->setResolved('paginate', PaginateDirective::class);

return Parser::fieldDefinition('field: String @paginate');
},
],
'@paginate(query)' => [
[
'name' => 'Query',
'builder' => QueryBuilder::class,
],
static function (DirectiveLocator $directives): FieldDefinitionNode {
$directives->setResolved('paginate', PaginateDirective::class);

$class = json_encode(HandlerDirectiveTest__QueryBuilderResolver::class, JSON_THROW_ON_ERROR);
$field = Parser::fieldDefinition("field: String @paginate(builder: {$class})");

return $field;
},
],
'@paginate(custom)' => [
[
'name' => 'Query',
'builder' => HandlerDirectiveTest__CustomBuilder::class,
],
static function (DirectiveLocator $directives): FieldDefinitionNode {
$directives->setResolved('paginate', PaginateDirective::class);

$class = json_encode(HandlerDirectiveTest__CustomBuilderResolver::class, JSON_THROW_ON_ERROR);
$field = Parser::fieldDefinition("field: String @paginate(builder: {$class})");

return $field;
},
],
];
}
// </editor-fold>
}

// @phpcs:disable PSR1.Classes.ClassDeclaration.MultipleClasses
// @phpcs:disable Squiz.Classes.ValidClassName.NotCamelCaps

/**
* @internal
* @noinspection PhpMultipleClassesDeclarationsInOneFile
*/
class HandlerDirectiveTest__QueryBuilderResolver {
public function __invoke(): QueryBuilder {
throw new Exception('should not be called.');
}
}

/**
* @internal
* @noinspection PhpMultipleClassesDeclarationsInOneFile
*/
class HandlerDirectiveTest__CustomBuilderResolver {
public function __invoke(): HandlerDirectiveTest__CustomBuilder {
throw new Exception('should not be called.');
}
}

/**
* @internal
* @noinspection PhpMultipleClassesDeclarationsInOneFile
*/
class HandlerDirectiveTest__CustomBuilder extends QueryBuilder {
// empty
}
7 changes: 6 additions & 1 deletion packages/graphql/src/Builder/Manipulator.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,11 @@

abstract class Manipulator extends AstManipulator implements TypeProvider {
public function __construct(
private Container $container,
DirectiveLocator $directives,
DocumentAST $document,
TypeRegistry $types,
private Container $container,
private BuilderInfo $builderInfo,
) {
parent::__construct($directives, $document, $types);
}
Expand All @@ -38,6 +39,10 @@ public function __construct(
protected function getContainer(): Container {
return $this->container;
}

protected function getBuilderInfo(): BuilderInfo {
return $this->builderInfo;
}
// </editor-fold>

// <editor-fold desc="TypeProvider">
Expand Down
Loading

0 comments on commit 639c3bb

Please sign in to comment.