$readGroups */ public function read(object $entity, array $readGroups): JsonResponse { return $this->json($entity, Response::HTTP_OK, $readGroups); } /** * @template T of object * * @param class-string $entityClass * @param list $writeGroups * @param list $readGroups */ public function create( Request $request, string $entityClass, array $writeGroups, array $readGroups, ): JsonResponse { $payload = $this->decodePayload($request); if ($payload === null) { return $this->jsonError('Ожидается JSON-объект в теле запроса', Response::HTTP_BAD_REQUEST); } unset($payload['id']); try { /** @var T $entity */ $entity = $this->denormalizer->denormalize( $payload, $entityClass, null, [ AbstractNormalizer::GROUPS => $writeGroups, ], ); } catch (SerializerExceptionInterface $e) { return $this->jsonError('Ошибка десериализации: ' . $e->getMessage(), Response::HTTP_BAD_REQUEST); } if (($validationResponse = $this->validate($entity)) !== null) { return $validationResponse; } $this->em->persist($entity); $this->em->flush(); return $this->json($entity, Response::HTTP_CREATED, $readGroups); } /** * @param list $writeGroups * @param list $readGroups */ public function update( Request $request, object $entity, array $writeGroups, array $readGroups, ): JsonResponse { $payload = $this->decodePayload($request); if ($payload === null) { return $this->jsonError('Ожидается JSON-объект в теле запроса', Response::HTTP_BAD_REQUEST); } unset($payload['id']); try { $this->denormalizer->denormalize( $payload, $entity::class, null, [ AbstractNormalizer::GROUPS => $writeGroups, AbstractNormalizer::OBJECT_TO_POPULATE => $entity, ], ); } catch (SerializerExceptionInterface $e) { return $this->jsonError('Ошибка десериализации: ' . $e->getMessage(), Response::HTTP_BAD_REQUEST); } if (($validationResponse = $this->validate($entity)) !== null) { return $validationResponse; } $this->em->flush(); return $this->json($entity, Response::HTTP_OK, $readGroups); } public function delete(object $entity): JsonResponse { try { $this->em->remove($entity); $this->em->flush(); } catch (DbalException $e) { // Сохраняем легаси-контракт: при FK / NOT NULL / unique ошибках БД // отдаём 500 + {error, message}. См. старый ArticleController::delete. return new JsonResponse( ['error' => 'Ошибка при удалении записи', 'message' => $e->getMessage()], Response::HTTP_INTERNAL_SERVER_ERROR, ); } return new JsonResponse(null, Response::HTTP_NO_CONTENT); } /** * @return array|null null если тело не является JSON-объектом * * Ловим как нативный \JsonException, так и Symfony\...\HttpFoundation\Exception\JsonException * (последний наследует UnexpectedValueException, а не \JsonException, и без * широкого перехвата Symfony ErrorListener перехватит ошибку до нашего try/catch). */ private function decodePayload(Request $request): ?array { try { return $request->toArray(); } catch (JsonException|\UnexpectedValueException) { return null; } } private function validate(object $entity): ?JsonResponse { $errors = $this->validator->validate($entity); if (count($errors) === 0) { return null; } // BC: легаси-контроллеры возвращали именно сериализованный ConstraintViolationList // с кодом 400. Этот же формат продолжаем отдавать здесь, чтобы фронтенду // не пришлось переписывать парсинг ошибок. $json = $this->serializer->serialize($errors, 'json'); return new JsonResponse($json, Response::HTTP_BAD_REQUEST, [], true); } /** * @param list $groups */ private function json(mixed $data, int $status, array $groups): JsonResponse { $json = $this->serializer->serialize($data, 'json', [ AbstractNormalizer::GROUPS => $groups, ]); return new JsonResponse($json, $status, [], true); } private function jsonError(string $message, int $status): JsonResponse { return new JsonResponse(['error' => $message], $status); } }