issues/27: update crud from admin api

This commit is contained in:
Valery Petrov
2026-05-14 16:16:07 +03:00
committed by Valeriy Petrov
parent 839ccdffb5
commit bc5468e5a0
21 changed files with 755 additions and 1456 deletions
+184
View File
@@ -0,0 +1,184 @@
<?php
declare(strict_types=1);
namespace App\Service\Crud;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Serializer\Exception\ExceptionInterface as SerializerExceptionInterface;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;
/**
* Универсальный CRUD-ответчик для тонких контент-контроллеров.
*
* Инкапсулирует общую логику десериализации тела запроса по группе :write,
* валидации сущности, persist/flush и сериализации ответа по группе :read.
*
* Контроллер становится декларативным:
* return $this->crud->create($request, News::class, ['news:write'], ['news:read']);
*/
final class CrudResponder
{
public function __construct(
private EntityManagerInterface $em,
private SerializerInterface $serializer,
private ValidatorInterface $validator,
) {
}
/**
* @param list<string> $readGroups
*/
public function read(object $entity, array $readGroups): JsonResponse
{
return $this->json($entity, Response::HTTP_OK, $readGroups);
}
/**
* @template T of object
*
* @param class-string<T> $entityClass
* @param list<string> $writeGroups
* @param list<string> $readGroups
*/
public function create(
Request $request,
string $entityClass,
array $writeGroups,
array $readGroups,
bool $allowIdFromPayload = true,
): JsonResponse {
$payload = $this->decodePayload($request);
if ($payload === null) {
return $this->jsonError('Ожидается JSON-объект в теле запроса', Response::HTTP_BAD_REQUEST);
}
try {
/** @var T $entity */
$entity = $this->serializer->deserialize(
$request->getContent(),
$entityClass,
'json',
[
AbstractNormalizer::GROUPS => $writeGroups,
],
);
} catch (SerializerExceptionInterface $e) {
return $this->jsonError('Ошибка десериализации: ' . $e->getMessage(), Response::HTTP_BAD_REQUEST);
}
// Id в группе :write не присутствует (чтобы запретить инъекцию при update).
// На create поддерживаем явный id из payload, потому что у контент-сущностей
// нет GeneratedValue (id приходит из Bitrix-view).
if ($allowIdFromPayload && isset($payload['id']) && method_exists($entity, 'setId')) {
$id = (int) $payload['id'];
if ($id > 0) {
$entity->setId($id);
}
}
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<string> $writeGroups
* @param list<string> $readGroups
*/
public function update(
Request $request,
object $entity,
array $writeGroups,
array $readGroups,
): JsonResponse {
if ($this->decodePayload($request) === null) {
return $this->jsonError('Ожидается JSON-объект в теле запроса', Response::HTTP_BAD_REQUEST);
}
try {
$this->serializer->deserialize(
$request->getContent(),
$entity::class,
'json',
[
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
{
$this->em->remove($entity);
$this->em->flush();
return new JsonResponse(null, Response::HTTP_NO_CONTENT);
}
/**
* @return array<string, mixed>|null null если тело не является JSON-объектом
*/
private function decodePayload(Request $request): ?array
{
$data = json_decode($request->getContent(), true);
return is_array($data) ? $data : null;
}
private function validate(object $entity): ?JsonResponse
{
$errors = $this->validator->validate($entity);
if (count($errors) === 0) {
return null;
}
$formatted = [];
foreach ($errors as $error) {
$formatted[] = [
'property' => $error->getPropertyPath(),
'message' => $error->getMessage(),
];
}
return new JsonResponse(['errors' => $formatted], Response::HTTP_UNPROCESSABLE_ENTITY);
}
/**
* @param list<string> $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);
}
}