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

[GraphQL] How to specify GraphQL types in DTO? #3422

Closed
oleg-brizy opened this issue Feb 29, 2020 · 2 comments
Closed

[GraphQL] How to specify GraphQL types in DTO? #3422

oleg-brizy opened this issue Feb 29, 2020 · 2 comments

Comments

@oleg-brizy
Copy link

I am trying to solve #3420 with input class but I can't specify args types properly

/**
 * @ApiResource(
 *  graphql={
 *     "create"={
 *         "mutation"=CollectionTypeMutationResolver::class,
 *         "input"=CreateCollectionTypeInput::class
 *     }
 *   }
 * )
class CreateCollectionTypeInput
{
    /**
     * @var CollectionCategory
     */
    public $category;

    /**
     * @var array
     *
     * How to specify that this must be of type
     * [CollectionTypeFieldInput!]
     * ?
     */
    public $fields;
}

but in schema I get

category: String!
fields: Iterable!

I want Dto fields to match these args

"args"={
    "category"={"type"="ID", "description"="CollectionCategory"},
    "fields"={"type"="[CollectionTypeFieldInput!]"},
}

If I set in ApiResource both "input" and "args" then "input" is ignored (and I get error "fields must be IRIs")

@oleg-brizy
Copy link
Author

oleg-brizy commented Feb 29, 2020

I decorated type converter and added support for custom annotation for explicit GraphQL type.

src/Dto/CreateCollectionTypeInput.php

<?php

declare(strict_types=1);

namespace App\Dto;

use App\Annotation\GraphQLType;
use App\Entity\CollectionCategory;

// Note: @var is required by api-platform
//       without it the property is skipped and @GraphQLType has no effect

class CreateCollectionTypeInput
{
    /**
     * @var CollectionCategory
     * @GraphQLType("ID")
     */
    public $category;

    /**
     * @var array
     * @GraphQLType("[CollectionTypeFieldInput!]")
     */
    public $fields;
}

src/Annotation/GraphQLType.php

<?php

declare(strict_types=1);

namespace App\Annotation;

/**
 * @Annotation
 * @Target({"PROPERTY"})
 */
class GraphQLType
{
    public $value;
}

config/services.yaml

services:
    App\Type\TypeConverter:
        decorates: api_platform.graphql.type_converter
        arguments:
            $typesContainer: '@api_platform.graphql.types_container'

src/Type/TypeConverter.php

<?php

declare(strict_types=1);

namespace App\Type;

use ApiPlatform\Core\GraphQl\Type\TypeConverterInterface;
use ApiPlatform\Core\GraphQl\Type\TypesContainerInterface;
use Doctrine\Common\Annotations\Reader;
use GraphQL\Type\Definition\Type as GraphQLType;
use Symfony\Component\PropertyInfo\Type;
use App\Annotation\GraphQLType as GraphQLTypeAnnotation;

class TypeConverter implements TypeConverterInterface
{
    private TypeConverterInterface $decorated;
    private Reader $reader;
    private TypesContainerInterface $typesContainer;

    public function __construct(
        TypeConverterInterface $decorated,
        Reader $reader,
        TypesContainerInterface $typesContainer
    )
    {
        $this->decorated = $decorated;
        $this->reader = $reader;
        $this->typesContainer = $typesContainer;
    }

    public function convertType(
        Type $type,
        bool $input,
        ?string $queryName,
        ?string $mutationName,
        ?string $subscriptionName,
        string $resourceClass,
        string $rootResource,
        ?string $property,
        int $depth
    )
    {
        if ($property && class_exists($rootResource)) {
            $reflectionProperty = (new \ReflectionClass($rootResource))->getProperty($property);
            $annotation = $this->reader->getPropertyAnnotation($reflectionProperty, GraphQLTypeAnnotation::class);
            if ($annotation) {
                return $this->convertStringToTypeInstance($annotation->value);
            }
        }

        return $this->decorated->convertType(
            $type,
            $input,
            $queryName,
            $mutationName,
            $subscriptionName,
            $rootResource,
            $rootResource,
            $property,
            $depth
        );
    }

    public function resolveType(string $type): ?GraphQLType
    {
        return $this->decorated->resolveType($type);
    }

    private function convertStringToTypeInstance(string $typeName)
    {
        if (substr($typeName, -1) === "!") {
            $typeName = substr($typeName, 0, -1);
            return GraphQLType::nonNull($this->convertStringToTypeInstance($typeName));
        }

        if (substr($typeName, 0, 1) === "[" && substr($typeName, -1) === "]") {
            $typeName = substr($typeName, 1, -1);
            return GraphQLType::listOf($this->convertStringToTypeInstance($typeName));
        }

        if (($standardTypes = GraphQLType::getStandardTypes()) && isset($standardTypes[$typeName])) {
            return $standardTypes[$typeName];
        }

        return $this->typesContainer->get($typeName);
    }
}

src/DataTransformer/CreateCollectionTypeInputDataTransformer.php

final class CreateCollectionTypeInputDataTransformer implements DataTransformerInterface
{
    ...

    /**
     * @param CreateCollectionTypeInput $data
     */
    public function transform($data, string $to, array $context = []): CollectionType
    {
        $this->validator->validate($data);

        $collectionType = new CollectionType();
        if ($data->category) {
            $collectionType->setCategory($data->category);
        }

        $fields = new ArrayCollection();
        foreach ($data->fields as $fieldArgs) {
            $field = new CollectionTypeField();
            $field->setCollectionType($collectionType);
            $field->setTitle($fieldArgs['title']);
            $field->setType($fieldArgs['type']);

            $fields->add($field);
        }
        $collectionType->setFields($fields);

        return $collectionType; // is also validated
    }

    public function supportsTransformation($data, string $to, array $context = []): bool
    {
        if ($data instanceof CollectionType) {
            return false;
        }

        return (
            CollectionType::class === $to &&
            CreateCollectionTypeInput::class === ($context['input']['class'] ?? null)
        );
    }
}

This way I bypass the limitation on embeded resources only by IRI.


  1. For this to work I need this change https://github.com/api-platform/core/pull/3423/files The tests are failing but I don't have any more time to investigate because I spent a whole day trying to create a mutation and it is not ready yet. For my project I will use a fork of api-platform/core with that change.
  2. What do you think about this approach? Is it good enough to be included in core?

oleg-brizy added a commit to bagrinsergiu/api-platform-core that referenced this issue Mar 1, 2020
@oleg-brizy
Copy link
Author

Now that it settled down in my head: This solutions gives you complete freedom to customize the GraphQL types of your class, I just introduced the GraphQLType annotation to declare the type in the class itself instead of huge ifs in the decorated TypeConverter.

I will close this for now. Maybe later when I will get even more familiar with api-platform I will create a pull request (with tests) to add the annotation in core.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant