GraphQLBundle icon indicating copy to clipboard operation
GraphQLBundle copied to clipboard

[Documentation] How to organize resolvers with resolver map

Open benjamindulau opened this issue 7 years ago • 6 comments

Q A
Bug report? no
Feature request? no
BC Break report? no
RFC? no
Version/Branch 0.11.10

I really struggle with the Resolver map and how I should organize things.

I'd like to abstract some of the work to reduce verbosity.

My goal is to obtain something as straightforward as the following:

type Query {
  user(id: ID!): User
}

type User {
  id: ID!
  email: String!
  firstName: String!
  lastName: String!
  avatar: Avatar!
}

type Avatar {
  url: String!
  placeholder: Boolean!
}
<?php
namespace ST\GraphQL\Resolver;


use Overblog\GraphQLBundle\Resolver\ResolverMap;
use GraphQL\Type\Definition\ResolveInfo;
use Overblog\GraphQLBundle\Definition\Argument;

class MainResolverMap extends ResolverMap
{
    private $queryResolver;
    private $userResolver;

    public function __construct(QueryResolver $queryResolver, UserResolver $userResolver)
    {
        $this->queryResolver = $queryResolver;
        $this->userResolver = $userResolver;
    }

    protected function map()
    {
        return [
            'Query' => $this->queryResolver,
            'User'  => $this->userResolver,
        ];
    }
}
<?php
namespace ST\GraphQL\Resolver;

use Overblog\GraphQLBundle\Definition\Argument;
use GraphQL\Type\Definition\ResolveInfo;
use ST\GraphQL\DataFetching\DataLoader;

class UserResolver
{
    private $dataLoader;

    public function __construct(DataLoader $dataLoader)
    {
        $this->dataLoader = $dataLoader;
    }

    public function resolveAvatar($value, Argument $args, \ArrayObject $context, ResolveInfo $info): array
    {
        // this would use some injected dependency
        return [
            'url' => 'http://....' . $value['id'] . '.jpg',
            'placeholder' => false,
        ];
    }
}

But because of the way the resolver map works, I can't figure out how to implement it this way.

Note that the following works:

class MainResolverMap extends ResolverMap
{
    private $userResolver;

    public function __construct(UserResolver $userResolver)
    {
        $this->userResolver = $userResolver;
    }

    protected function map()
    {
        return [
            'Query' => ...,
            'User' => [
                'avatar' => function ($value, Argument $args, \ArrayObject $context, ResolveInfo $info) {
                   return $this->userResolver->resolveAvatar($value, $args, $context, $info);
                }
            ]
        ];
    }
}

But it's clearly very verbose and would become a pain to maintain on a large project! I could move the User field map inside the UserResolver class, but still, it's too much verbosity.

Also tried some naive approaches like the following:

abstract class AbstractTypeResolver
{
    public function getResolveMap(): array
    {
        return [
            ResolverMap::RESOLVE_FIELD => function ($value, Argument $args, \ArrayObject $context, ResolveInfo $info) {
                $resolveMethod = 'resolve' . \ucfirst($info->fieldName);

                if (\method_exists($this, $resolveMethod)) {
                    return call_user_func([$this, $resolveMethod], $value, $args, $context, $info);
                }

                // Not good
                return $value[$info->fieldName];
            }
        ];
    }
}
class MainResolverMap extends ResolverMap
{
    private $queryResolver;
    private $userResolver;

    public function __construct(QueryResolver $queryResolver, UserResolver $userResolver)
    {
        $this->queryResolver = $queryResolver;
        $this->userResolver = $userResolver;
    }

    protected function map()
    {
        return [
            'Query' => $this->queryResolver->getResolveMap(),
            'User' => $this->userResolver->getResolveMap(),
        ];
    }
}

And then having UserResolver extending AbstractTypeResolver but it doesn't seem right.

In the end I just want to avoid having to repeat that a million time:

'avatar' => function ($value, Argument $args, \ArrayObject $context, ResolveInfo $info) {
    return $this->userResolver->resolveAvatar($value, $args, $context, $info);
}

And factor this logic somewhere hidden.

Any thoughts or recommendations?

benjamindulau avatar Dec 20 '18 22:12 benjamindulau

Can you try using callable resolver with __invoke method?

mcg-web avatar Dec 21 '18 08:12 mcg-web

@mcg-web You mean using tagged resolver services instead of the ResolverMap? Like the following?

class Greetings implements ResolverInterface
{
    public function __invoke(ResolveInfo $info, $name)
    {
        if($info->fieldName === 'hello'){
            return sprintf('hello %s!!!', $name);
        }
        else if($info->fieldName === 'goodbye'){
            return sprintf('goodbye %s!!!', $name);
        }
        else{
            throw new \DomainException('Unknown greetings');
        }
    }
}

benjamindulau avatar Dec 21 '18 09:12 benjamindulau

@mcg-web Same issue for me, could someone show better example how resolver und map?

riroxumigu avatar Jan 31 '19 17:01 riroxumigu

@riroxumigu this could maybe help you. I'll try to work on a resolver map using tagged "classic resolver" in the coming days.

mcg-web avatar Feb 01 '19 06:02 mcg-web

@mcg-web thank You! Thats helped but now I can't execute mutation method to update entity. I did eveyrthing like this https://github.com/overblog/GraphQLBundle/blob/master/docs/definitions/graphql-schema-language.md#define-mutations and class PostMutation which implements MutationInterface.

riroxumigu avatar Feb 03 '19 10:02 riroxumigu

@mcg-web any news?

armetiz avatar May 06 '19 14:05 armetiz