删除受限条目时捕获 "Integrity constraint violation: 19 FOREIGN KEY constraint failed"
Catch "Integrity constraint violation: 19 FOREIGN KEY constraint failed" when deleting restricted entries
问题与我使用的技术栈有关:
- Symfony 4.2.3
- 学说 ORM 2.6.3
- 奏鸣曲管理 3.45.2
- sqlite3 3.22(虽然RDBMS应该没有作用)
假设我们有两个实体: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="products"
* )
* @ORM\JoinColumn(nullable=false)
* @Assert\NotBlank()
*/
private $category;
// ...
}
产品 必须分配给 类别。 类别 可以有 0 个或更多 产品。如果 Category 包含任何 Products,则不得将其删除。 类别 只有在没有分配 产品 时才能删除。
当我尝试在 Sonata Admin 中删除具有 Products 的 Category 时,删除被阻止,正如预期的那样,并且抛出异常:
PDOException
SQLSTATE[23000]: Integrity constraint violation: 19 FOREIGN KEY constraint failed
现在,这是意料之中的事,但对最终用户来说不是很好。我想提供一条消息并通知用户 Category 无法删除,因为它仍然包含 Products.
在 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 事件监听器是正确的?
我相信这里描述了您要尝试做的事情:
我不会复制粘贴整个消息,但概念是创建 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
To use this feature in your PHP classes/controllers:
$flashManager = $this->get('sonata.core.flashmessage.manager');
$messages = $flashManager->get('success');
To use this feature in your templates, include the following template (with an optional domain parameter):
{% include '@SonataCore/FlashMessage/render.html.twig' %}
Note If necessary, you can also specify a translation domain to override configuration here:
{% 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
,所以还要检查您使用的是什么版本,也许您需要的是更新。
我设法通过添加自定义侦听器解决了问题。它在删除受限对象时捕获 ModelManagerException
。它适用于所有注册管理员。这是 class:
<?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
在我的特殊情况下,可能不允许删除管理员之外的任何对象。因此,该解决方案满足要求。我希望这个例子可以帮助其他人。您必须根据需要调整某些部分。
问题与我使用的技术栈有关:
- Symfony 4.2.3
- 学说 ORM 2.6.3
- 奏鸣曲管理 3.45.2
- sqlite3 3.22(虽然RDBMS应该没有作用)
假设我们有两个实体: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="products"
* )
* @ORM\JoinColumn(nullable=false)
* @Assert\NotBlank()
*/
private $category;
// ...
}
产品 必须分配给 类别。 类别 可以有 0 个或更多 产品。如果 Category 包含任何 Products,则不得将其删除。 类别 只有在没有分配 产品 时才能删除。
当我尝试在 Sonata Admin 中删除具有 Products 的 Category 时,删除被阻止,正如预期的那样,并且抛出异常:
PDOException
SQLSTATE[23000]: Integrity constraint violation: 19 FOREIGN KEY constraint failed
现在,这是意料之中的事,但对最终用户来说不是很好。我想提供一条消息并通知用户 Category 无法删除,因为它仍然包含 Products.
在 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 事件监听器是正确的?
我相信这里描述了您要尝试做的事情:
我不会复制粘贴整个消息,但概念是创建 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
To use this feature in your PHP classes/controllers:
$flashManager = $this->get('sonata.core.flashmessage.manager');
$messages = $flashManager->get('success');
To use this feature in your templates, include the following template (with an optional domain parameter):
{% include '@SonataCore/FlashMessage/render.html.twig' %}
Note If necessary, you can also specify a translation domain to override configuration here:
{% 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
,所以还要检查您使用的是什么版本,也许您需要的是更新。
我设法通过添加自定义侦听器解决了问题。它在删除受限对象时捕获 ModelManagerException
。它适用于所有注册管理员。这是 class:
<?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
在我的特殊情况下,可能不允许删除管理员之外的任何对象。因此,该解决方案满足要求。我希望这个例子可以帮助其他人。您必须根据需要调整某些部分。