Doctrine Embedded 在 GraphQL 中不显示(Api 平台)

Doctrine Embedded not show with GraphQL (Api Platform)

我在 Doctrine 和 Api 平台之间有问题

我需要一些帮助来序列化学说embedded/embeddable。

从现在开始我们正在使用 Api 带有 REST 方法的平台,但是我们想用 GraphQL 创建一个项目,但是有些字段没有显示。

我在这里做了一个测试仓库https://github.com/tattali/apip-graphql

当我查询时

{
  __type(name: "User") {
    name
    fields {
      name
    }
  }
}

我只得到未嵌入的字段

{
  "data": {
    "__type": {
      "name": "User",
      "fields": [
        {
          "name": "id"
        },
        {
          "name": "_id"
        },
        {
          "name": "firstname"
        },
        {
          "name": "lastname"
        }
      ]
    }
  }
}

这是我的实体

<?php

declare(strict_types=1);

namespace App\Entity;

use ApiPlatform\Core\Annotation as Api;
use App\Repository\UserRepository;
use Doctrine\ORM\Mapping as ORM;

/**
 * @Api\ApiResource
 * @ORM\Entity(repositoryClass=UserRepository::class)
 */
class User
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\Column(type="string", length=255)
     */
    private $firstname;

    /**
     * @ORM\Column(type="string", length=255)
     */
    private $lastname;

    /**
     * @ORM\Embedded(class="App\Entity\Remuneration")
     */
    private $actualRemuneration;

    /**
     * @ORM\Embedded(class="App\Entity\Remuneration")
     */
    private $expectedRemuneration;

    public function __construct()
    {
        $this->actualRemuneration = new Remuneration();
        $this->expectedRemuneration = new Remuneration();
    }

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getFirstname(): ?string
    {
        return $this->firstname;
    }

    public function setFirstname(string $firstname): self
    {
        $this->firstname = $firstname;

        return $this;
    }

    public function getLastname(): ?string
    {
        return $this->lastname;
    }

    public function setLastname(string $lastname): self
    {
        $this->lastname = $lastname;

        return $this;
    }

    public function getActualRemuneration(): ?Remuneration
    {
        return $this->actualRemuneration;
    }

    public function setActualRemuneration(Remuneration $actualRemuneration): self
    {
        $this->actualRemuneration = $actualRemuneration;

        return $this;
    }

    public function getExpectedRemuneration(): ?Remuneration
    {
        return $this->expectedRemuneration;
    }

    public function setExpectedRemuneration(Remuneration $expectedRemuneration): self
    {
        $this->expectedRemuneration = $expectedRemuneration;

        return $this;
    }
}
<?php

declare(strict_types=1);

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Embeddable
 */
class Remuneration
{
    /**
     * @ORM\Column(type="integer", nullable=true)
     */
    private $fixedSalary;

    /**
     * @ORM\Column(type="integer", nullable=true)
     */
    private $fullPackage;

    public function getFixedSalary(): ?int
    {
        return $this->fixedSalary;
    }

    public function setFixedSalary(?int $fixedSalary): self
    {
        $this->fixedSalary = $fixedSalary;

        return $this;
    }

    public function getFullPackage(): ?int
    {
        return $this->fullPackage;
    }

    public function setFullPackage(?int $fullPackage): self
    {
        $this->fullPackage = $fullPackage;

        return $this;
    }
}

我通过创建自定义 GraphQL 类型和规范器找到了破解解决方案

这并不完美,但可以满足基本需求

# config/services.yaml
services:
  # ...

  App\GraphQL\Type\EmbeddedObjectType:
    tags:
      - { name: api_platform.graphql.type }

  App\GraphQL\Type\TypeConverter:
    decorates: api_platform.graphql.type_converter
// src/GraphQL/Type/EmbeddedObjectType.php
<?php

declare(strict_types=1);

namespace App\GraphQL\Type;

use ApiPlatform\Core\GraphQl\Type\Definition\TypeInterface;
use GraphQL\Error\Error;
use GraphQL\Language\AST\ObjectValueNode;
use GraphQL\Type\Definition\ScalarType;
use GraphQL\Utils\AST;
use GraphQL\Utils\Utils;

final class EmbeddedObjectType extends ScalarType implements TypeInterface
{
    const NAME = 'EmbeddedObject';

    public function __construct()
    {
        $this->name = 'EmbeddedObject';
        $this->description = 'The `EmbeddedObject` scalar type represents file metadata data.';

        parent::__construct();
    }

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

    /**
     * {@inheritdoc}
     */
    public function serialize($value)
    {
        if (\is_array($value)) {
            return $value;
        }

        if (!\is_object($value)) {
            throw new Error(sprintf('Value must be an instance of %s to be represented by `EmbeddedObject`: %s', 'object', Utils::printSafe($value)));
        }
    }

    /**
     * {@inheritdoc}
     */
    public function parseValue($value): object
    {
        if (!\is_object($value)) {
            throw new Error(sprintf('`EmbeddedObject` could not parse: %s', Utils::printSafe($value)));
        }

        return $value;
    }

    /**
     * {@inheritdoc}
     */
    public function parseLiteral($valueNode, array $variables = null)
    {
        if ($valueNode instanceof ObjectValueNode) {
            return AST::valueFromASTUntyped($valueNode, $variables);
        }

        // Intentionally without message, as all information already in wrapped Exception
        throw new \Exception();
    }
}
// src/GraphQL/Type/TypeConverter.php
<?php

declare(strict_types=1);

namespace App\GraphQL\Type;

use ApiPlatform\Core\GraphQl\Type\TypeConverterInterface;
use Doctrine\ORM\EntityManagerInterface;
use GraphQL\Type\Definition\Type as GraphQLType;
use Symfony\Component\PropertyInfo\Type;

final class TypeConverter implements TypeConverterInterface
{
    private $typeConverter;
    private $entityManager;

    public function __construct(
        TypeConverterInterface $typeConverter,
        EntityManagerInterface $entityManager
    ) {
        $this->typeConverter = $typeConverter;
        $this->entityManager = $entityManager;
    }

    /**
     * {@inheritdoc}
     */
    public function convertType(Type $type, bool $input, ?string $queryName, ?string $mutationName, string $resourceClass, string $rootResource, ?string $property, int $depth)
    {
        if (false !== strpos((string) $resourceClass, 'App\Entity\')) {
            $metadata = $this->entityManager->getClassMetadata($resourceClass);
            if ($metadata->isEmbeddedClass) {
                return EmbeddedObjectType::NAME;
            }
        }

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

    public function resolveType(string $type): ?GraphQLType
    {
        return $this->typeConverter->resolveType($type);
    }
}
// src/Serializer/Normalizer/EmbeddedNormalizer.php
<?php

declare(strict_types=1);

namespace App\Serializer\Normalizer;

use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;

class EmbeddedNormalizer implements ContextAwareNormalizerInterface, NormalizerAwareInterface
{
    use NormalizerAwareTrait;

    const FORMAT = 'graphql';

    private $entityManager;
    private $propertyAccessor;

    private $metadata;

    public function __construct(
        EntityManagerInterface $entityManager
    ) {
        $this->propertyAccessor = PropertyAccess::createPropertyAccessor();
        $this->entityManager = $entityManager;
    }

    public function normalize($data, $format = null, array $context = [])
    {
        $array = [];
        foreach ($this->metadata->fieldNames as $name) {
            $this->propertyAccessor->setValue(
                $array,
                "[{$name}]",
                $this->normalizer->normalize($this->propertyAccessor->getValue($data, $name), $format, $context)
            );
        }

        return $array;
    }

    public function supportsNormalization($data, $format = null, array $context = [])
    {
        if (
            self::FORMAT === $format && \is_object($data) && false !== strpos(\get_class($data), 'App\Entity\')
        ) {
            $this->metadata = $this->entityManager->getClassMetadata(\get_class($data));
            if ($this->metadata->isEmbeddedClass) {
                return true;
            }
        }

        return false;
    }
}

像我查询的时候那样

{
  user (id: "/api/users/1") {
    lastname
    actualRemuneration
  }
}

渲染

{
  "data": {
    "user": {
      "lastname": "lastname 0",
      "actualRemuneration": {
        "fixedSalary": 190,
        "fullPackage": 198
      }
    }
  }
}

架构

你在哪里定义类型?

  App\GraphQL\Type\EmbeddedObjectType:
    tags:
      - { name: api_platform.graphql.type }

  App\GraphQL\Type\TypeConverter:
    decorates: api_platform.graphql.type_converter

对于那些有困难的人,我需要进行这些更改和设置。

// src/GraphQL/Type/EmbeddedObjectType.php

...
#50 if (\is_array($value)) {
#51     $value = (object) $value;
#52 }

#55 throw new Error(sprintf('`EmbeddedObject` could not parse: %s', Utils::printSafeJson($value)));
...

并添加服务:

// config/services.yaml

...
App\Serializer\Normalizer\EmbeddedNormalizer:
    decorates: "api_platform.serializer.normalizer.item"
    arguments: ["@doctrine.orm.entity_manager"]
...

如果您使用另一个 path/namespace(不是 App\Entity),例如:App\Embeddable:

// src/GraphQL/Type/TypeConverter.php

...
#30 if (false !== strpos((string) $resourceClass, 'App\Entity\') || false !== strpos((string) $resourceClass, 'App\Embeddable\')) {
...

// src/Serializer/Normalizer/EmbeddedNormalizer.php

...
#48 self::FORMAT === $format && \is_object($data) && (false !== strpos(\get_class($data), 'App\Entity\') || false !== strpos(\get_class($data), 'App\Embeddable\'))
...