From c91f880698a2d36178dde88c89bd0eff4d110d19 Mon Sep 17 00:00:00 2001 From: Robert van Steen Date: Thu, 19 Dec 2019 16:11:41 +0100 Subject: [PATCH] Relationship Resolvers (#132) * wip * Apply fixes from StyleCI [ci skip] [skip ci] * Ignore phpunit cache * Tests * Apply fixes from StyleCI [ci skip] [skip ci] --- .gitignore | 1 + src/Eloquent/ModelSchema.php | 4 ++ src/Field.php | 4 +- src/Fields/EloquentField.php | 62 +++++++++++++++++++ src/Fields/Field.php | 10 ++- .../Concerns/EagerLoadRelationships.php | 11 +++- src/Queries/SingleEntityQuery.php | 16 ----- src/Types/EloquentMutationInputType.php | 2 +- src/Types/EntityType.php | 22 +++---- tests/Feature/EntityQueryTest.php | 28 +++++++++ tests/Fixtures/Schemas/ArticleSchema.php | 3 + 11 files changed, 128 insertions(+), 35 deletions(-) diff --git a/.gitignore b/.gitignore index 66b58f50..4c311dd0 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ composer.lock .DS_Store .idea/ .vscode/ +.phpunit.result.cache diff --git a/src/Eloquent/ModelSchema.php b/src/Eloquent/ModelSchema.php index 30d3c339..36f0e7eb 100644 --- a/src/Eloquent/ModelSchema.php +++ b/src/Eloquent/ModelSchema.php @@ -350,6 +350,10 @@ public function getRelations(): Collection return $field; })->map(function (Field $field) { + if ($field instanceof EloquentField) { + return $field->getRelation($this->getModel()); + } + $accessor = $field->getAccessor(); Utils::invariant( diff --git a/src/Field.php b/src/Field.php index 02e34ec2..9c939e46 100644 --- a/src/Field.php +++ b/src/Field.php @@ -43,12 +43,12 @@ public static function float(): Fields\Field return self::getRegistry()->field(self::getRegistry()->float()); } - public static function model(string $class): Fields\Field + public static function model(string $class): Fields\EloquentField { return self::getRegistry()->eloquent($class); } - public static function collection(string $class): Fields\Field + public static function collection(string $class): Fields\EloquentField { return self::model($class)->list(); } diff --git a/src/Fields/EloquentField.php b/src/Fields/EloquentField.php index ab197bbd..1aaea627 100644 --- a/src/Fields/EloquentField.php +++ b/src/Fields/EloquentField.php @@ -2,9 +2,12 @@ namespace Bakery\Fields; +use Bakery\Utils\Utils; use Bakery\Eloquent\ModelSchema; use Bakery\Support\TypeRegistry; use Bakery\Types\Definitions\RootType; +use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\Relation; class EloquentField extends Field { @@ -18,6 +21,16 @@ class EloquentField extends Field */ protected $inverseRelationName; + /** + * @var callable|null + */ + protected $relationResolver; + + /** + * @var callable|null + */ + protected $collectionResolver; + /** * EloquentField constructor. * @@ -79,4 +92,53 @@ public function name(): string { return $this->getModelClass()->getTypename(); } + + /** + * Set a custom relation resolver. + */ + public function relation(callable $resolver): self + { + $this->relationResolver = $resolver; + + return $this; + } + + /** + * Return the Eloquent relation. + */ + public function getRelation(Model $model): Relation + { + if ($resolver = $this->relationResolver) { + return $resolver($model); + } + + $accessor = $this->getAccessor(); + + Utils::invariant( + method_exists($model, $accessor), + 'Relation "'.$accessor.'" is not defined on "'.get_class($model).'".' + ); + + return $model->{$accessor}(); + } + + /** + * Determine if the field has a relation resolver. + */ + public function hasRelationResolver(): bool + { + return isset($this->relationResolver); + } + + /** + * Get the result of the field. + */ + public function getResult(Model $model) + { + if ($resolver = $this->relationResolver) { + return $resolver($model)->get(); + } + + return $model->{$this->getAccessor()}; + } } diff --git a/src/Fields/Field.php b/src/Fields/Field.php index 60c219d9..a6828951 100644 --- a/src/Fields/Field.php +++ b/src/Fields/Field.php @@ -221,7 +221,7 @@ public function description(string $description): self } /** - * Return the name of the database column associated with this field. + * Return the name of the database column or method associated with this field. * * @return string|null */ @@ -524,6 +524,14 @@ public function resolve(callable $resolver): self return $this; } + /** + * Get the resolver. + */ + public function getResolver(): ?callable + { + return $this->resolver; + } + /** * Resolve the field. * diff --git a/src/Queries/Concerns/EagerLoadRelationships.php b/src/Queries/Concerns/EagerLoadRelationships.php index 7430f0bc..d0abf702 100644 --- a/src/Queries/Concerns/EagerLoadRelationships.php +++ b/src/Queries/Concerns/EagerLoadRelationships.php @@ -3,8 +3,8 @@ namespace Bakery\Queries\Concerns; use Bakery\Eloquent\ModelSchema; +use Bakery\Fields\EloquentField; use Bakery\Support\TypeRegistry; -use Bakery\Fields\PolymorphicField; use Illuminate\Database\Eloquent\Builder; trait EagerLoadRelationships @@ -32,15 +32,20 @@ protected function eagerLoadRelations(Builder $query, array $fields, ModelSchema continue; } + // If a custom relation resolver is provided we cannot eager load. + if ($field instanceof EloquentField && $field->hasRelationResolver()) { + continue; + } + $with = array_map(function ($with) use ($path) { return $path ? "{$path}.{$with}" : $with; }, $field->getWith() ?? []); $query->with($with); - if ($field->isRelationship() && ! $field instanceof PolymorphicField) { + if ($field instanceof EloquentField) { $accessor = $field->getAccessor(); - $related = $schema->getModel()->{$accessor}()->getRelated(); + $related = $field->getRelation($schema->getModel())->getRelated(); $relatedSchema = $this->registry->getSchemaForModel($related); $this->eagerLoadRelations($query, $subFields, $relatedSchema, $path ? "{$path}.{$accessor}" : $accessor); } diff --git a/src/Queries/SingleEntityQuery.php b/src/Queries/SingleEntityQuery.php index 201f941b..37f36f67 100644 --- a/src/Queries/SingleEntityQuery.php +++ b/src/Queries/SingleEntityQuery.php @@ -15,8 +15,6 @@ class SingleEntityQuery extends EloquentQuery { /** * Get the name of the query. - * - * @return string */ public function name(): string { @@ -29,8 +27,6 @@ public function name(): string /** * The return type of the query. - * - * @return \Bakery\Types\Definitions\RootType */ public function type(): RootType { @@ -39,8 +35,6 @@ public function type(): RootType /** * The arguments for the Query. - * - * @return array */ public function args(): array { @@ -49,12 +43,6 @@ public function args(): array /** * Resolve the EloquentQuery. - * - * @param Arguments $args - * @param mixed $root - * @param mixed $context - * @param \GraphQL\Type\Definition\ResolveInfo $info - * @return \Illuminate\Database\Eloquent\Model|null ?Model */ public function resolve(Arguments $args, $root, $context, ResolveInfo $info): ?Model { @@ -83,10 +71,6 @@ public function resolve(Arguments $args, $root, $context, ResolveInfo $info): ?M /** * Query by the arguments supplied to the query. - * - * @param Builder $query - * @param Arguments $args - * @return Builder */ protected function queryByArgs(Builder $query, Arguments $args): Builder { diff --git a/src/Types/EloquentMutationInputType.php b/src/Types/EloquentMutationInputType.php index f159ad3e..f6dee307 100644 --- a/src/Types/EloquentMutationInputType.php +++ b/src/Types/EloquentMutationInputType.php @@ -66,7 +66,7 @@ protected function getFieldsForRelation(string $relation, EloquentField $root): $fields = collect(); $root->setRegistry($this->registry); $inputType = 'Create'.$root->getName().'Input'; - $relationship = $this->model->{$root->getAccessor()}(); + $relationship = $root->getRelation($this->model); if ($root->isList()) { $name = Str::singular($relation).'Ids'; diff --git a/src/Types/EntityType.php b/src/Types/EntityType.php index 9213d2eb..eea965b1 100644 --- a/src/Types/EntityType.php +++ b/src/Types/EntityType.php @@ -95,7 +95,7 @@ protected function getRelationFields(): Collection protected function getFieldsForRelation(string $key, EloquentField $field): Collection { $fields = collect(); - $relationship = $this->model->{$field->getAccessor()}(); + $relationship = $field->getRelation($this->model); if ($field->isList()) { $fields = $fields->merge($this->getPluralRelationFields($key, $field)); @@ -126,12 +126,10 @@ protected function getPluralRelationFields(string $key, EloquentField $field): C $field = $field->args([ 'filter' => $this->registry->type($field->getName().'Filter')->nullable(), - ])->resolve(function (Model $model, string $accessor, Arguments $args) { - $relation = $model->{$accessor}(); + ])->resolve(function (Model $model, string $accessor, Arguments $args) use ($field) { + $relation = $field->getRelation($model); - $result = $args->isEmpty() ? $model->{$accessor} : $this->getRelationQuery($relation, $args)->get(); - - return $result; + return $args->isEmpty() ? $field->getResult($model) : $this->getRelationQuery($relation, $args)->get(); }); $fields->put($key, $field); @@ -142,10 +140,10 @@ protected function getPluralRelationFields(string $key, EloquentField $field): C ->args($field->getArgs()) ->nullable($field->isNullable()) ->viewPolicy($field->getViewPolicy()) - ->resolve(function (Model $model, string $accessor, Arguments $args) { - $relation = $model->{$accessor}(); + ->resolve(function (Model $model, string $accessor, Arguments $args) use ($field) { + $relation = $field->getRelation($model); - $result = $args->isEmpty() ? $model->{$accessor} : $this->getRelationQuery($relation, $args)->get(); + $result = $args->isEmpty() ? $field->getResult($model) : $this->getRelationQuery($relation, $args)->get(); return $result->pluck($relation->getRelated()->getKeyName()); }) @@ -155,10 +153,10 @@ protected function getPluralRelationFields(string $key, EloquentField $field): C ->accessor($field->getAccessor()) ->nullable($field->isNullable()) ->viewPolicy($field->getViewPolicy()) - ->resolve(function (Model $model, string $accessor, Arguments $args) { - $relation = $model->{$accessor}; + ->resolve(function (Model $model, string $accessor, Arguments $args) use ($field) { + $relation = $field->getRelation($model); - $result = $args->isEmpty() ? $model->{$accessor} : $this->getRelationQuery($relation, $args); + $result = $args->isEmpty() ? $field->getResult($model) : $this->getRelationQuery($relation, $args)->get(); return $result->count(); }) diff --git a/tests/Feature/EntityQueryTest.php b/tests/Feature/EntityQueryTest.php index b2594c72..5adf796e 100644 --- a/tests/Feature/EntityQueryTest.php +++ b/tests/Feature/EntityQueryTest.php @@ -130,6 +130,34 @@ public function it_can_filter_a_relation() $response->assertJsonFragment(['body' => 'Cool story'])->assertJsonMissing(['body' => 'Boo!']); } + /** @test */ + public function it_uses_custom_resolver_for_relationships() + { + $user = factory(User::class)->create(); + $article = factory(Article::class)->create(['user_id' => $user->id]); + + factory(Comment::class)->create(['commentable_id' => $article->id, 'author_id' => $user->id, 'body' => 'This is my comment.']); + factory(Comment::class)->create(['commentable_id' => $article->id, 'body' => 'This is a comment from someone else.']); + + $query = ' + query { + article { + id + myComments { + id + body + } + myCommentsCount + myCommentIds + } + } + '; + + $this->graphql($query) + ->assertJsonFragment(['body' => 'This is my comment.']) + ->assertJsonMissing(['body' => 'This is a comment from someone else.']); + } + /** @test */ public function it_shows_the_count_for_many_relationships() { diff --git a/tests/Fixtures/Schemas/ArticleSchema.php b/tests/Fixtures/Schemas/ArticleSchema.php index b58fae04..04a85224 100644 --- a/tests/Fixtures/Schemas/ArticleSchema.php +++ b/tests/Fixtures/Schemas/ArticleSchema.php @@ -32,6 +32,9 @@ public function relations(): array 'tags' => Field::collection(TagSchema::class), 'comments' => Field::collection(CommentSchema::class), 'remarks' => Field::collection(CommentSchema::class)->accessor('comments'), + 'myComments' => Field::collection(CommentSchema::class)->relation(function (Article $article) { + return $article->comments()->where('author_id', optional(auth()->user())->getAuthIdentifier()); + }), ]; } }