Поймать «Нарушение ограничения целостности: ограничение 19 FOREIGN KEY не удалось» при удалении записей с ограниченным доступом

Вопрос относится к стеку технологий, который я использую:

  • Симфони 4.2.3
  • Доктрина ORM 2.6.3
  • Соната Админ 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 = "products"
     * )
     * @ORM\JoinColumn(nullable=false)
     * @Assert\NotBlank()
     */
    private $category;
    // ...
}

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

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

PDOException

SQLSTATE[23000]: Integrity constraint violation: 19 FOREIGN KEY constraint failed

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

В 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 — это то, что нужно?

Я не использовал Doctrine какое-то время, из-за большой головной боли, но я не понимаю, почему вы не можете сделать блок try{ }catch(PDOException $e){ }, а затем удалить оттуда сообщение об ошибке.

ArtisticPhoenix 03.03.2019 11:57

@ArtisticPhoenix Мне не помогает использовать блок try - catch. Я ищу универсальное решение. В противном случае мне придется использовать try - catch в каждом месте, где мне нужно удалить объект.

cezar 03.03.2019 12:03

Это at every place I need to delete an object не совсем верно, так как вы можете создать метод delete в своем model или entity и использовать его в качестве воронки для команды удаления, если вы последуете. Я забыл, как удалять объекты в доктрине, но я уверен, что вы можете создать метод и сделать там все, что вам нужно.

ArtisticPhoenix 03.03.2019 12:04
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Symfony Station Communiqué - 7 июля 2023 г
Symfony Station Communiqué - 7 июля 2023 г
Это коммюнике первоначально появилось на Symfony Station .
Оживление вашего приложения Laravel: Понимание режима обслуживания
Оживление вашего приложения Laravel: Понимание режима обслуживания
Здравствуйте, разработчики! В сегодняшней статье мы рассмотрим важный аспект управления приложениями, который часто упускается из виду в суете...
Установка и настройка Nginx и PHP на Ubuntu-сервере
Установка и настройка Nginx и PHP на Ubuntu-сервере
В этот раз я сделаю руководство по установке и настройке nginx и php на Ubuntu OS.
Коллекции в Laravel более простым способом
Коллекции в Laravel более простым способом
Привет, читатели, сегодня мы узнаем о коллекциях. В Laravel коллекции - это способ манипулировать массивами и играть с массивами данных. Благодаря...
Как установить PHP на Mac
Как установить PHP на Mac
PHP - это популярный язык программирования, который используется для разработки веб-приложений. Если вы используете Mac и хотите разрабатывать...
1
3
1 617
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

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

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

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

В этом Listener вы можете определить, какое это исключение, и использовать 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, поэтому также проверьте, какую версию вы используете, возможно, вам нужно обновление.

одна из причин, по которой я ненавижу Doctrine, почему нельзя просто обернуть это в блок try catch?

ArtisticPhoenix 03.03.2019 11:55

Ну, вы можете: D, вам просто нужно знать, что ловить, на самом деле Sonata конвертирует PDOException в ModelManagerException. Проверьте ту проблему github, о которой я упоминал. Соната — это адская вещь, она добавляет много нежелательных слоев, о которых вы в основном не подозреваете.

vytsci 03.03.2019 12:05

Я не использую ничего из этого, я использовал Doctrine для одного проекта, и этого было достаточно. Не то чтобы я не мог, я занимаюсь PHP уже 9 лет или около того. Но для меня это все равно, что носить наручники. Я, вероятно, создал более 50 веб-сайтов на различных платформах, и я управляю сайтом, который выполняет 150 миллионов поисковых запросов каждый день...

ArtisticPhoenix 03.03.2019 12:07

@ArtisticPhoenix вам нужно привыкнуть к ORM. Это очень полезный инструмент во многих случаях. И каждый раз, когда вы хотите строго контролировать что-то, вы сталкиваетесь с последствиями более высокой кривой обучения.

vytsci 03.03.2019 12:09

Нет, ORM - это в основном Junk IMO. Запросы не оптимизированы, они раздуты, плохо поддерживают большие наборы данных и т. д. Я имею дело с миллионами строк. Я могу делать с PDO все, что мне нужно, включая вставку табличных значений в класс. И небуферизованные запросы, которые являются единственным способом извлечь миллионы записей из БД. Не говоря уже о транзакциях... для ACID и т.д.

ArtisticPhoenix 03.03.2019 12:10

@ArtisticPhoenix кажется, что вы не понимаете цели ORM. И не видел никаких запросов, сгенерированных Doctrine ORM. ДА, при неправильном использовании ORM вы можете получить низкую производительность, но с правильной архитектурой, кешем и т. д. У вас будет правильно контролируемая среда для работы. Мы сталкиваемся с эпохой, когда вы просто не можете полагаться на одну технологию, вы должны использовать уровни поиска, такие как Elastic, ODM для баз данных на основе документов и ORM для баз данных SQL, потому что у каждого инструмента есть свои взлеты и падения, и вы не можете зацикливаться только на одном инструмент.

vytsci 03.03.2019 12:16

PS. Я не использую только одну технологию. Я использую sphinx shearchd аналогично резинке. MongoDB, MySQL, RabbitMQ, Cassandra и т. д. Для тех, кто не знает, как писать собственные запросы и управлять своими отношениями и стеком приложений, конечно, все в порядке. Я прочитал вики, stackoverflow.com/questions/448684/почему-вы-должны-использовать-форму и согласен с Chuck, который получил наибольшее количество голосов.

ArtisticPhoenix 03.03.2019 12:24

Спасибо @vytsci. Мне удалось решить проблему с помощью предоставленных вами ссылок.

cezar 07.03.2019 10:18
Ответ принят как подходящий

Мне удалось решить проблему, добавив собственный прослушиватель. Он ловит 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

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

Другие вопросы по теме