#symfony #symfony4
#symfony #symfony4
Вопрос:
Я пытаюсь найти способ надлежащим образом реализовать механизм обновления одного или нескольких дочерних объектов. Родительский объект имеет отношение OneToMany к none, одному или нескольким дочерним объектам, и мне нужен способ обновить только те, которые предоставлены запросом ИСПРАВЛЕНИЯ.
До сих пор я не нашел ни одного достаточно конкретного примера, который указал бы мне правильное направление.
Проблема в том, что с текущим кодом это работает следующим образом:
- Если я использую конечные точки для добавления или обновления одного дочернего элемента, все работает нормально.
- Если я предоставляю все существующие элементы в запросе ИСПРАВЛЕНИЯ в точно выровненном порядке (например, порядок дочерних элементов, полученных с помощью запроса GET), все работает нормально.
- Если я предоставляю смешанный порядок элементов, он пытается обновить элементы в соответствии с исходным порядком (например, порядок дочерних элементов, полученных с помощью запроса 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 и вспомогательным сообщением в полезной нагрузке (на основе определенного типа проблемы).