Исправлена ​​ошибка «Нарушение ограничения целостности: не выполнено ограничение 19 FOREIGN KEY» при удалении ограниченных записей - PullRequest
0 голосов
/ 03 марта 2019

Вопрос касается используемого мной стека технологий:

  • Symfony 4.2.3
  • Doctrine ORM 2.6.3
  • Sonata Admin 3.45.2
  • sqlite3 3.22 (хотя СУБД не должна играть роли)

Допустим, у нас есть две сущности: Category и Product, где категория отношения к продукту равна 1: nи продукт в категории n: 1.Это будет выглядеть так:

Category.php

class Category
{
    // ...
    /**
     * @ORM\OneToMany(
     *     targetEntity="App\Entity\Product",
     *     mappedBy="category",
     *     cascade={"persist"}
     * )
     * @Assert\Valid()
     */
    private $products;
    // ...
}

Product.php

class Product
{
    // ...
    /**
     * @ORM\ManyToOne(
     *     targetEntity="App\Entity\Category", 
     *     inversedBy="samplingDocumentations"
     * )
     * @ORM\JoinColumn(nullable=false)
     * @Assert\NotBlank()
     */
    private $samplingActivity;
    // ...
}

Продукт должен быть присвоен категории . Категория может иметь 0 или более Продукты .Если Категория содержит какие-либо Продукты , это НЕ должно быть удалено. Категория может быть удалена, только если ей не присвоены Продукты .

Когда я пытаюсь удалить Категория , которая имеет Продукты в Sonata Admin, удаление предотвращается, как и ожидалось, и генерируется исключение:

PDOException

SQLSTATE [23000]: нарушение ограничения целостности: 19 FOREIGN KEYограничение не удалось

Теперь это ожидается, но не очень приятно для конечного пользователя.Я хотел бы предоставить сообщение и сообщить пользователю, что Категория не может быть удалена, поскольку она по-прежнему содержит Продукты .

В Sonata Admin я использую обходной путьписать CategoryAdminController и реализовывать хук preDelete:

public function preDelete(Request $request, $object)
{
    if ($object->getProducts()->isEmpty()) {
        return null;
    }

    $count = $object->getProducts()->count();
    $objectName = $this->admin->toString($object);
    $this->addFlash(
        'sonata_flash_error',
        sprintf(
            'The category "%s" can not be deleted because it contains %s product(s).',
            $objectName,
            $count
        )
    );

    return $this->redirectTo($object);
}

Однако это не так, потому что я должен переопределить его за пределами администратора.

Что лучшепрактиковаться, чтобы справиться с этим?Могу ли я реализовать некоторую проверку в сущности?Или, может быть, слушатели событий Doctrine - это то, что нужно?

Ответы [ 2 ]

0 голосов
/ 07 марта 2019

Мне удалось решить проблему, добавив пользовательский слушатель.Ловит ModelManagerException при удалении ограниченного объекта.Это работает для всех зарегистрированных администраторов.Вот класс:

<?php

namespace App\EventListener;

use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Sonata\AdminBundle\Exception\ModelManagerException;

class ModelManagerExceptionResponseListener
{
    private $session;
    private $router;
    private $em;

    public function __construct(SessionInterface $session, UrlGeneratorInterface $router, EntityManagerInterface $em)
    {
        $this->session = $session;
        $this->router = $router;
        $this->em = $em;
    }

    public function onKernelException(GetResponseForExceptionEvent $event)
    {
        // get the exception
        $exception =  $event->getException();
        // we proceed only if it is ModelManagerException
        if (!$exception instanceof ModelManagerException) {
            return;
        }

        // get the route and id
        // if it wasn't a delete route we don't want to proceed
        $request = $event->getRequest();
        $route = $request->get('_route');
        $id = $request->get('id');
        if (substr($route, -6) !== 'delete') {
            return;
        }
        $route = str_replace('delete', 'edit', $route);

        // get the message
        // we proceed only if it is the desired message
        $message = $exception->getMessage();
        $failure = 'Failed to delete object: ';
        if (strpos($message, $failure) < 0) {
            return;
        }

        // get the object that can't be deleted
        $entity = str_replace($failure, '', $message);
        $repository = $this->em->getRepository($entity);
        $object = $repository->findOneById($id);

        $this->session->getFlashBag()
            ->add(
                'sonata_flash_error',
                sprintf('The item "%s" can not be deleted because other items depend on it.', $object)
            )
        ;

        // redirect to the edit form of the object
        $url = $this->router->generate($route, ['id' => $id]);
        $response = new RedirectResponse($url);
        $event->setResponse($response);
    }
}

И мы регистрируем сервис:

app.event_listener.pdoexception_listener:
    class: App\EventListener\ModelManagerExceptionResponseListener
    arguments:
        - '@session'
        - '@router'
        - '@doctrine.orm.entity_manager'
    tags:
        - { name: kernel.event_listener, event: kernel.exception }
    public: true # this maybe isn't needed

Вероятно, удаление любого объекта за пределами администратора не будет разрешено в моем конкретном случае.Поэтому это решение удовлетворяет требованиям.Я надеюсь, что этот пример может помочь другим.Вам придется адаптировать некоторые детали в соответствии с вашими потребностями.

0 голосов
/ 03 марта 2019

Я считаю, что то, что вы пытаетесь сделать, описано здесь:

Symfony + Doctrine - определите сообщение об ошибке при ошибке ограничения целостности

Я не буду копировать- вставьте сообщение целиком, но концепция заключается в том, чтобы создать onKernelResponse слушатель и прослушать PDOException.Как это сделать, есть много статей, я думаю, что вы можете легко найти в Интернете, я выбрал одну из первых, которую я нашел.

В этом Слушателе вы можете определить, какое это исключение, и использоватьflashbag либо по умолчанию symfony один:

https://symfony.com/doc/current/components/http_foundation/sessions.html

$session->getFlashBag()->add('notice', 'Profile updated');

Или вы можете использовать Sonata Core Flashbag:

https://sonata -project.org / bundles / core / master / doc / reference / flash_messages.html

Чтобы использовать эту функцию в ваших классах / контроллерах PHP:

$flashManager = $this->get('sonata.core.flashmessage.manager');

$messages = $flashManager->get('success'); Чтобы использовать эту функцию в своих шаблонах, включите следующий шаблон (с необязательным параметром домена):

{% include '@SonataCore/FlashMessage/render.html.twig' %}

Примечание При необходимости выздесь также можно указать домен перевода для переопределения конфигурации:

{% include '@SonataCore/FlashMessage/render.html.twig' with { domain: 'MyCustomBundle' } %}

Вы также можете взглянуть на эту статью https://tocacar.com/symfony2-how-to-modify-sonataadminbundles-error-message-on-entity-deletion-ca77cac343fa и переопределить CRUDController::deleteAction такВы можете обрабатывать такие ошибки.

И здесь вы можете найти код, который немного связан с вашей проблемой, оСтраница Sonata Admin GitHub https://github.com/sonata-project/SonataAdminBundle/issues/4485 она перехватывает PDOException, поэтому также проверьте, какую версию вы используете, может быть, то, что вам нужно, это обновление.

...