Простой REST с Symfony Serializer с Doctrine ORM, не поддерживается ли нормализация? - PullRequest
1 голос
/ 20 марта 2019

У меня есть вопрос / запрос на идеи относительно десериализации Json в стандартной среде Symfony 4.2.

Подумайте о следующих двух сущностях Пост и Автор . Каждый пост имеет ровно 1 автора (однонаправленная ассоциация ManyToOne).

Почтовая организация

/**
 * @ORM\Entity()
 */
class Post
{

    /**
     * @var int
     *
     * @ORM\Id
     * @ORM\Column(type="integer", options={"unsigned": true})
     * @ORM\GeneratedValue
     */
    private $id;

    /**
     * @var string
     *
     * @ORM\Column(type="text", length=50)
     */
    private $text;

    /**
     * @var Author
     *
     * @ORM\ManyToOne(targetEntity="Author", fetch="EAGER")
     */
    private $author;

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

    /**
     * @return string
     */
    public function getText(): string
    {
        return $this->text;
    }

    /**
     * @param string $text
     */
    public function setText(string $text): void
    {
        $this->text = $text;
    }

    /**
     * @return Author
     */
    public function getAuthor(): Author
    {
        return $this->author;
    }

    /**
     * @param Author $author
     */
    public function setAuthor(Author $author): void
    {
        $this->author = $author;
    }

}

Автор Entity

/**
 * @ORM\Entity()
 */
class Author
{

    /**
     * @var int
     *
     * @ORM\Id
     * @ORM\Column(type="integer", options={"unsigned": true})
     * @ORM\GeneratedValue
     */
    private $id;

    /**
     * @var string
     *
     * @ORM\Column(type="text", length=50)
     */
    private $name;

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

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

    /**
     * @param string $name
     */
    public function setName(string $name): void
    {
        $this->name = $name;
    }

}

Теперь давайте создадим простой Контроллер , который позволяет GET и POST сообщений:

class SimpleController extends AbstractController
{

    /**
     * @Route("/fapi/post/{id}", methods={"GET", "HEAD"})
     * @param int                 $id
     *
     * @return Response
     */
    public function getEntityAction($id)
    {

        $em = $this->getDoctrine()->getManager();

        $post = $em->getRepository(Post::class)->find($id);

        $encoders = array(new JsonEncoder());
        $normalizers = array(new ObjectNormalizer());
        $serializer = new Serializer($normalizers, $encoders);

        $serializedPost = $serializer->serialize($post, 'json');

        return new Response($serializedPost);
    }

    /**
     * @Route("/fapi/post", methods={"POST"})
     *
     * @param Request $request
     *
     * @return Response
     */
    public function postEntityAction(Request $request)
    {

        $data = $request->getContent();

        $encoders = array(new JsonEncoder());
        $normalizers = array(new ObjectNormalizer());
        $serializer = new Serializer($normalizers, $encoders);

        $post = $serializer->deserialize($data, Post::class, 'json');

        $em = $this->getDoctrine()->getManager();

        $em->persist($post);
        $em->flush();

        return new Response(''); // return newly created Post

    }

}

Предположим, что в базе данных уже существует Post (id: 1). Мы получаем его с помощью GET / FAPI / Post / 1 . Это результат:

{"id":1,"text":"Hello World!","author":{"id":1,"name":"Huber"}}

Это прекрасно, ObjectNormalizer Symfony заботится о нормализации объекта Post и также включает в себя объект Author.

Теперь давайте попробуем создать новый пост с уже существующим автором с помощью POST / fapi / post :

{"id":null,"text":"Hello Universe!","author":{"id":1,"name":"Huber"}}

Теперь мы получаем сообщение от Symfony:

NotNormalizableValueException: тип атрибута \ "id \" для класса \ "App \ Entity \ Post \" должен быть одним из \ "int \" (указана \ "NULL \")

Я ценю ошибку и понимаю, почему это происходит, но нет ли встроенного способа сериализации Symfony, чтобы гарантировать, что автоматически сгенерированные значения (с помощью ORM, с помощью конструктора, такого как UUID) в этом не учитываются "стандартный случай".

Решения, которые мне известны:

  1. вообще не отправляет id с запросом (но, согласно моему пониманию REST, следует полностью заменить представление объектов)
  2. с использованием GetSetMethodNormalizer вместо ObjectNormalizer (так как для id нет Setter, который работает, но имеет несколько других недостатков).
  3. исключая id из контекста денормализации (например, с помощью ignored_attributes)
  4. создание собственного нормализатора для решения этой ситуации

А сейчас давайте попробуем перейти от ObjectNormalizer к GetSetMethodNormalizer, чтобы преодолеть нашу ошибку и повторить попытку POST.

Теперь мы получаем еще одну ошибку:

FatalThrowableError: Аргумент 1, передаваемый в App \ Entity \ Post :: setAuthor (), должен быть экземпляром App \ Entity \ Author, массив указан

Согласно документации GetSetMethodNormalizer, дальнейшая нормализация не происходит при установке значений в денормализации.

Итак, я снова ценю ошибку и понимаю, что происходит.

API Platform решает эти проблемы с помощью пользовательской системы денормализации

Я понимаю и ценю, что Symfony и Doctrine - это два разных пакета, , но , так как они объединяются так часто (и, действительно, обычно прекрасно играют вместе):

Существует ли какой-либо встроенный или известный способ в сериализации Symfony, который в принципе позволяет мне в вышеупомянутых «стандартных случаях» добиться того, чтобы эта цепочка работала без ошибок и без специальной реализации нормализаторов или других?

База данных => Doctrine ORM => Symfony GET объект => json => POST same json => Symfony => Doctrine ORM => База данных

P.S .: Еще один маленький пример:

GET / fapi / post / 1 доставляет только

{"id":1,"text":"Hello World!","author":{"id":1,"name":"Huber"}}

поскольку сопоставление ManyToOne выбирается EAGER, если установлено значение LAZY по умолчанию, оно становится:

{"id":1,"text":"Hello World!","author":{"id":1,"name":"Huber","__initializer__":null,"__cloner__":null,"__isInitialized__":true}}

потому что ObjectNormalizer сериализует объект Doctrine Proxy.

P.P.S: Мне известно о JMSSerializerBundle , и я его много использовал, но мне очень нравится и поддерживает Symfony в том, что у него есть «собственный» компонент Serializer.

...