Как правильно реализовать метод исправления RESTful API для обновления нескольких объектов с помощью отношения OneToMany на основе Symfony 4?

#symfony #symfony4

#symfony #symfony4

Вопрос:

Я пытаюсь найти способ надлежащим образом реализовать механизм обновления одного или нескольких дочерних объектов. Родительский объект имеет отношение OneToMany к none, одному или нескольким дочерним объектам, и мне нужен способ обновить только те, которые предоставлены запросом ИСПРАВЛЕНИЯ.

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

Проблема в том, что с текущим кодом это работает следующим образом:

  1. Если я использую конечные точки для добавления или обновления одного дочернего элемента, все работает нормально.
  2. Если я предоставляю все существующие элементы в запросе ИСПРАВЛЕНИЯ в точно выровненном порядке (например, порядок дочерних элементов, полученных с помощью запроса GET), все работает нормально.
  3. Если я предоставляю смешанный порядок элементов, он пытается обновить элементы в соответствии с исходным порядком (например, порядок дочерних элементов, полученных с помощью запроса GET) с помощью данных, предоставленных в запросе ИСПРАВЛЕНИЯ — это означает, что я не могу исправить, например, 3-й элемент, если я не предоставляю действительные данные для первых двух элементов в точном порядке.порядок. Если я предоставляю идентификаторы в запросе, он пытается обновить их вместо того, чтобы использовать их для обращения к определенному элементу.

Вот пример кода:

Реализация объектов:

 class ParentEntity
{
    /** @var int */
    private $id;

    /**
     * @var ArrayCollection|ChildEntity[]
     *
     * @ORMOneToMany(targetEntity="ChildEntity", mappedBy="parentEntity")
     */
    private $clindEntities;
}

class ChildEntity
{
    /** @var int */
    private $id;

    /**
     * @var ParentEntity
     *
     * @ORMManyToOne(targetEntity="ParentEntity", inversedBy="clindEntities")
     */
    private $parentEntity;

    /** @var string */
    private $someProperty;
}
 

Типы форм:

 class ParentEntityFormType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('clindEntities', TypeCollectionType::class, [
                'required' => true,
                'entry_type' => ChildEntityFormType::class,
                'by_reference' => true,
                'allow_add' => false,
                'allow_delete' => false,
            ]);
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'data_class' => ParentEntity::class,
            'csrf_protection' => false,
            'allow_extra_fields' => false
        ]);
    }
}

class ChildEntityFormType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('id', TypeTextType::class, [
                'required' => false,
                'trim' => true,
            ])
            ->add('someProperty', TypeTextType::class, [
                'required' => true,
                'trim' => true,
            ]);
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'data_class' => ChildEntity::class,
            'csrf_protection' => false,
            'allow_extra_fields' => false
        ]);
    }
}
 

Controller:

 class EntityController extends ApiBaseController
{
    /**
     * @Route("/api/v1/parent-entity/{id}", methods={"PATCH"}, name="api.v1.parent_entity.update")
     */
    public function updateParentEntity(Request $request, EntityManagerInterface $em, string $id)
    {
        $parentEntityRepository = $em->getRepository(ParentEntity::class);
        $parentEntity = $parentEntityRepository->find($id);
        if (!$parentEntity) {
            $apiProblemInterface = new AeApiProblemInterface(AeApiProblemType::ENTITY_NOT_FOUND);
            throw new AeApiProblemException($apiProblemInterface);
        }

        // Validate payload
        $form = $this->createForm(ParentEntityFormType::class, $parentEntity);
        $this->submitAndValidateForm($form, $request, false, false);

        // Save changes
        $em->flush();

        return $this->response($parentEntity, ['api_entity_metadata', 'api_parent_entity', 'api_clhild_entity'], Response::HTTP_OK);
    }
}
 

Original data structure and order (sample of ParentEntity):

 {
    "id": 22,
    "clindEntities": [
        {
            "id": 1,
            "someProperty": "Some test string 1"
        },
        {
            "id": 2,
            "someProperty": "Some test string 2"
        },
        {
            "id": 3,
            "someProperty": "Some test string 3"
        }
    ]
}
 

Итак, если я выполняю запрос на ИСПРАВЛЕНИЕ, чтобы /api/v1/parent-entity/22

 {
    "clindEntities": [
        {
            "id": 3,
            "someProperty": "Updated test string 3"
        },
        {
            "id": 1,
            "someProperty": "Updated test string 1"
        }
    ]
}
 

Это приведет к попытке изменить данные следующим образом (что, конечно, не удается из-за неуникального идентификатора):

 {
    "id": 22,
    "clindEntities": [
        {
            "id": 3,
            "someProperty": "Updated test string 3"
        },
        {
            "id": 1,
            "someProperty": "Updated test string 1"
        },
        {
            "id": 3,
            "someProperty": "Some test string 3"
        }
    ]
}
 

Какой подход я должен использовать, чтобы добиться обновления только дочерних элементов с точным идентификатором независимо от порядка, в котором они предоставляются?

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

PS: сходство это относится к конечной точке для добавления дочерних элементов с использованием метода POST. Хотя дочерние идентификаторы не предоставляются, система форм Symfony обновляет существующие элементы в исходном порядке вместо добавления новых.

Спасибо за любое предложение.

Ответ №1:

В итоге я провел итерацию по дочерним элементам, отправляя и проверяя их по отдельности через систему форм Symfony:

 class EntityController extends ApiBaseController
{
    /**
     * @Route("/api/v1/parent-entity/{id}", methods={"PATCH"}, name="api.v1.parent_entity.update")
     */
    public function updateParentEntity(Request $request, EntityManagerInterface $em, string $id)
    {
        $parentEntityRepository = $em->getRepository(ParentEntity::class);
        $parentEntity = $parentEntityRepository->find($id);
        if (!$parentEntity) {
            $apiProblemInterface = new AeApiProblemInterface(AeApiProblemType::ENTITY_NOT_FOUND);
            throw new AeApiProblemException($apiProblemInterface);
        }

        $childEntityRepository = $em->getRepository(ChildEntity::class);
        foreach ($data['childEntities'] as $childEntity) {
            if (!isset($childEntity['id'])) {
                $apiProblemInterface = new AeApiProblemInterface(AeApiProblemType::ENTITY_NOT_FOUND);
                throw new AeApiProblemException($apiProblemInterface);
            }

            $childEntity = $childEntityRepository->find($childEntity['id']);
            if (!$childEntity) {
                $apiProblemInterface = new AeApiProblemInterface(AeApiProblemType::ENTITY_NOT_FOUND);
                throw new AeApiProblemException($apiProblemInterface);
            }

            $form = $this->createForm(AccountCryptoType::class, $childEntity);
            $this->submitArrayAndValidateForm($form, $accountData, false, false);
        }

        // Validate payload
        $form = $this->createForm(ParentEntityFormType::class, $parentEntity);
        $this->submitAndValidateForm($form, $request, false, false);

        // Save changes
        $em->flush();

        return $this->response($parentEntity, ['api_entity_metadata', 'api_parent_entity', 'api_clhild_entity'], Response::HTTP_OK);
    }

    /**
     * Reusable helper method for form data submission and validation.
     */
    protected function submitArrayAndValidateForm(FormInterface $form, array $formData, bool $clearMissing = true)
    {
        $form->submit($formData, $clearMissing);

        // Validate
        if ($form->isSubmitted() amp;amp; $form->isValid()) {
            return;
        }
        $formErrors = $form->getErrors(true);

        // Object $formErrors can be string casted but we rather use custom stringification for more details
        if (count($formErrors)) {
            $errors = [];
            foreach ($formErrors as $formError) {
                $fieldName = $formError->getOrigin()->getName();
                $message = implode(', ', $formError->getMessageParameters());
                $message = str_replace('"', "*", $message);
                $messageTemplate = $formError->getMessageTemplate();
                $errors[] = sprintf('%s: %s %s', $fieldName, $messageTemplate, $message);
            }
        }

        $apiProblemInterface = new AeApiProblemInterface(AeApiProblemType::MISSING_OR_INVALID_PAYLOAD, join("; ", $errors));
        throw new AeApiProblemException($apiProblemInterface);
    }
}
 

Это работает удовлетворительно хорошо. Но да … если есть что-то лучшее для достижения эквивалентной функциональности, пожалуйста, дайте мне знать и, возможно, помогите другим, у кого могут возникнуть подобные трудности.

PS:

 $apiProblemInterface = new AeApiProblemInterface(AeApiProblemType::MISSING_OR_INVALID_PAYLOAD, "...");
throw new AeApiProblemException($apiProblemInterface);
 

это просто какой-то пользовательский способ реализации механизма Throwable и обработки исключений. Это можно понимать как:

 throw new Exception('Some message ...');
 

с той разницей, что это приводит к ответу API с HTTP-кодом ошибки, типом содержимого: application / problem json и вспомогательным сообщением в полезной нагрузке (на основе определенного типа проблемы).