<?php declare(strict_types=1);
namespace App\Controller\CRUD;
use App\Model\ImpersonatingUser;
use App\RabbitMq\TasksPool;
use App\Response\ActionResult;
use App\Service\FormSavedMessage;
use Doctrine\DBAL\Connection;
use Doctrine\ORM\EntityManagerInterface;
use InvalidArgumentException;
use LogicException;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Form\FormError;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\ParameterBag;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\SwitchUserToken;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Core\Security;
use Throwable;
/**
* @template TEntityClass of object
* @template-extends EntityController<TEntityClass>
*/
abstract class FormController extends EntityController
{
protected Security $security;
protected TasksPool $tasksPool;
protected FormSavedMessage $formSavedMessage;
protected EventDispatcherInterface $eventDispatcher;
protected bool $useFormDataOptions = false;
/**
* @phpstan-param TEntityClass $entity
*/
abstract protected function getFormType($entity): string;
/**
* @param ParameterBag<mixed> $parameters Request parameters
* @return mixed
* @phpstan-return TEntityClass
*/
abstract public function provideEntity(ParameterBag $parameters);
/**
* @return string path to template
*/
abstract protected function getTemplate(FormInterface $form): string;
/**
* @phpstan-param TEntityClass $entity
*/
abstract protected function getRedirectUrl(Request $request, FormInterface $form, $entity): string;
/**
* @return array<string, array<mixed>|int|object|string|null>
* @phpstan-param TEntityClass $entity
*/
protected function processSubmit(Request $request, FormInterface $form, $entity): array
{
/** @var Connection $connection */
$connection = $this->doctrine->getConnection();
try {
$connection->beginTransaction();
$this->doctrine->getManager()->persist($entity);
// need to persist here ,so if in beforeUpdate we will have flush we will have managed entity
$this->doctrine->getManager()->persist($entity);
$this->beforeUpdate($request, $form, $entity);
$this->doctrine->getManager()->flush();
$this->afterUpdate($request, $form, $entity);
$this->doctrine->getManager()->flush();
$connection->commit();
} catch (Throwable $e) {
$connection->rollBack();
throw $e;
}
// this issue should be after all commits to the database are done, because otherwise consumer may receive the
// entity which does not exist yet in the database.
$this->dispatchPoolSend();
$entityClass = get_class($entity);
/** @var EntityManagerInterface|null $manager */
$manager = $this->doctrine->getManagerForClass($entityClass);
if (null !== $manager) {
$cache = $manager->getCache();
if (null !== $cache) {
if (!method_exists($entity, 'getId')) {
throw new InvalidArgumentException('Entity class must have getId method.');
}
$cache->evictEntity($entityClass, $entity->getId());
}
}
$this->showSavedMessage($entity);
return [];
}
/**
* @param array<string, array<mixed>|bool|int|float|object|string|null> $filters
*/
public function execute(array $filters = []): ActionResult
{
$attributes = new ParameterBag($filters);
$entity = $this->provideEntity($attributes);
$form = $this->createFormType(
$this->getFormType($entity),
$entity,
array_merge($this->getFormOptions($entity), $this->useFormDataOptions ? ['request_attributes' => $filters] : [])
);
return new ActionResult(
$this->getTemplate($form),
array_merge($this->templateParams, ['form' => $form->createView(), 'entity' => $entity]),
!($form->isSubmitted() && !$form->isValid()) // not is valid and was submitted.. else response considered valid
);
}
/**
* @param mixed[] $filters
* @param mixed[] $data
* @return mixed[]|FormInterface
*/
public function submit(array $filters, array $data)
{
$attributes = new ParameterBag($filters);
$entity = $this->provideEntity($attributes);
$options = array_merge($this->getFormOptions($entity), $this->useFormDataOptions ? ['request_attributes' => $filters] : []);
$options = array_merge(['csrf_protection' => false], $options);
$form = $this->createFormType(
$this->getFormType($entity),
$entity,
$options
);
$request = new Request();
if ($this->isValid($request, $form->submit($data, false), $entity)) {
return $this->processSubmit($request, $form, $entity);
}
return $form;
}
/**
* @return Response|ActionResult
*/
public function handleRequest(Request $request)
{
try {
$entity = $this->provideEntity($request->attributes);
} catch (AccessDeniedException $ex) {
if ($this->isHandlingJson($request) && $this->isAjaxRequest($request)) {
return new JsonResponse(['status' => false], 403);
}
throw $ex;
}
$form = $this->createFormType(
$this->getFormType($entity),
$entity,
array_merge($this->getFormOptions($entity), $this->useFormDataOptions ? ['request_attributes' => $request->attributes->all()] : [])
);
if ($this->isValid($request, $form->handleRequest($request), $entity)) {
$processedData = $this->processSubmit($request, $form, $entity);
if ($this->isHandlingJson($request) && $this->isAjaxRequest($request)) {
return $this->handleAjax($request, $form, $entity, $processedData);
}
$response = new ActionResult(null, $processedData);
$response->setRedirect($this->getRedirectUrl($request, $form, $entity));
return $response;
}
if ($this->isHandlingJson($request) && $this->isAjaxRequest($request)) {
return $this->handleAjax($request, $form, $entity, null);
}
return new ActionResult(
$this->getTemplate($form),
array_merge($this->templateParams, ['form' => $form->createView(), 'entity' => $entity]),
!($form->isSubmitted() && !$form->isValid()) // not is valid and was submitted.. else response considered valid
);
}
/**
* @phpstan-param TEntityClass $entity
* @return mixed[]
*/
protected function getFormOptions($entity): array
{
return [];
}
/**
* @phpstan-param TEntityClass $entity
*/
protected function isValid(Request $request, FormInterface $form, &$entity): bool
{
if (!($form->isSubmitted() && $form->isValid())) {
return false;
}
$token = $this->security->getToken();
if ($token instanceof SwitchUserToken) {
$impersonatorUser = $token->getOriginalToken()->getUser();
if (!($impersonatorUser instanceof ImpersonatingUser)) {
throw $this->createAccessDeniedException();
}
if (!$impersonatorUser->isDestructiveActions()) {
$form->addError(new FormError('Impersonation does not allow you to save anything.'));
return false;
}
}
return true;
}
/**
* @phpstan-param TEntityClass $entity
*/
protected function beforeUpdate(Request $request, FormInterface $form, $entity): void
{
// do nothing
}
/**
* @phpstan-param TEntityClass $entity
*/
protected function afterUpdate(Request $request, FormInterface $form, $entity): void
{
// do nothing
}
/**
* @phpstan-param TEntityClass $entity
* @param mixed[]|null $processedData
*/
protected function handleAjax(Request $request, FormInterface $form, $entity, ?array $processedData): Response
{
if ($form->isSubmitted() && $form->isValid()) {
return new JsonResponse(['status' => true, 'redirect' => $this->getRedirectUrl($request, $form, $entity)]);
}
$response = new ActionResult(
$this->getTemplate($form),
array_merge($this->templateParams, ['form' => $form->createView(), 'entity' => $entity]),
!($form->isSubmitted() && !$form->isValid())
);
if (null === $template = $response->getTemplate()) {
throw new LogicException('Can not render view without template name');
}
$httpResponse = $this->render($template, $response->getParams(), $response->getResponse());
$httpResponse->setStatusCode(Response::HTTP_BAD_REQUEST);
return $httpResponse;
}
/**
* Creates and returns a Form instance from the type of the form.
*
* @param string $type The fully qualified class name of the form type
* @param TEntityClass|null $entity The initial data for the form
* @param mixed[] $options Options for the form
*/
protected function createFormType(string $type, $entity = null, array $options = []): FormInterface
{
return $this->createForm($type, $entity, $options);
}
/**
* Sends pool of tasks if any available
*/
protected function dispatchPoolSend(): void
{
$this->tasksPool->send();
}
/**
* @phpstan-param TEntityClass $entity
*/
protected function showSavedMessage($entity): void
{
$this->formSavedMessage->setEnabled(true);
$this->formSavedMessage->showMessage($entity);
}
/**
* @required
*/
final public function setSecurity(Security $security): void
{
$this->security = $security;
}
/**
* @required
*/
final public function setTasksPool(TasksPool $tasksPool): void
{
$this->tasksPool = $tasksPool;
}
/**
* @required
*/
final public function setFormSavedMessage(FormSavedMessage $formSavedMessage): void
{
$this->formSavedMessage = $formSavedMessage;
}
/**
* @required
*/
final public function setEventDispatcher(EventDispatcherInterface $eventDispatcher): void
{
$this->eventDispatcher = $eventDispatcher;
}
}