190 lines
5.8 KiB
PHP
190 lines
5.8 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Service\Crud;
|
|
|
|
use Doctrine\ORM\EntityManagerInterface;
|
|
use JsonException;
|
|
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,
|
|
): 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->serializer->deserialize(
|
|
$this->encodePayload($payload),
|
|
$entityClass,
|
|
'json',
|
|
[
|
|
AbstractNormalizer::GROUPS => $writeGroups,
|
|
],
|
|
);
|
|
} catch (JsonException|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<string> $writeGroups
|
|
* @param list<string> $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->serializer->deserialize(
|
|
$this->encodePayload($payload),
|
|
$entity::class,
|
|
'json',
|
|
[
|
|
AbstractNormalizer::GROUPS => $writeGroups,
|
|
AbstractNormalizer::OBJECT_TO_POPULATE => $entity,
|
|
],
|
|
);
|
|
} catch (JsonException|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
|
|
{
|
|
try {
|
|
return $request->toArray();
|
|
} catch (JsonException) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $payload
|
|
*
|
|
* @throws JsonException
|
|
*/
|
|
private function encodePayload(array $payload): string
|
|
{
|
|
return json_encode($payload, JSON_THROW_ON_ERROR);
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|