删除受限条目时,捕获“违反完整性约束:19 FOREIGN KEY约束失败”

时间:2019-03-03 10:30:13

标签: php symfony doctrine-orm doctrine sonata-admin

问题与我使用的技术栈有关:

  • Symfony 4.2.3
  • Docmrine ORM 2.6.3
  • Sonata Admin 3.45.2
  • sqlite3 3.22(尽管RDBMS不应发挥作用)

比方说,我们有两个实体:CategoryProduct,其中类别与产品的关系为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中具有 Products Category 时,按预期阻止了该删除,并引发了异常:

  

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);
}

但是感觉不对,因为我必须在管理员外部重新实现它。

处理此问题的最佳做法是什么?我可以在实体中实施某种验证吗?还是说教义事件监听器是正确的?

2 个答案:

答案 0 :(得分:1)

我相信您在尝试执行以下操作:

Symfony + Doctrine - Define an error message when integrity constraint error

我不会复制粘贴整个消息,但是概念是创建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');   要在模板中使用此功能,请包括以下模板(带有可选的domain参数):

     

{% 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,因此还要检查您使用的是哪个版本,也许您需要的是更新。

答案 1 :(得分:0)

我设法通过添加自定义侦听器解决了该问题。删除受限对象时,它将捕获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

在我的特殊情况下,不允许删除管理员以外的任何对象。因此,该解决方案满足要求。我希望这个例子可以帮助其他人。您必须根据需要调整某些部分。