11/19/2013

Knowing whether a Doctrine insert or update worked

Doctrine's flush() does not return a success flag. It either completes or it throws, so there is nothing to test afterwards and wrapping it is the right instinct:

use Doctrine\ORM\EntityManagerInterface;

public function create(EntityManagerInterface $entityManager): Response
{
    $article = new Article();
    $article->setTitle('Symfony entity title');

    try {
        $entityManager->persist($article);
        $entityManager->flush();
        $this->addFlash('success', 'Record inserted');
    } catch (\Throwable $e) {
        $this->addFlash('error', 'Record not inserted');
        // and log $e -- see below
    }

    return $this->redirectToRoute('article_index');
}

The mistake that makes the catch do nothing

A version of this snippet has been circulating since about 2013, and it is worth reading closely because one of its faults is still a live mistake in code written today:

catch (Exception $e) {

Inside a namespaced class — which every Symfony controller is — that does not refer to PHP's built-in Exception. It refers to App\Controller\Exception, a class that does not exist, so the catch block matches nothing and the exception escapes exactly as if the try/catch were not there. It needs the leading backslash:

catch (\Exception $e) {

\Throwable is the wider net and usually the one you want here, because a broken mapping surfaces as an \Error rather than an \Exception and would otherwise sail straight past.

Catch the specific failure when you can act on it

A blanket catch turns every cause into the same message. When you intend to tell the reader something useful, catch the case you can name:

use Doctrine\DBAL\Exception\UniqueConstraintViolationException;

try {
    $entityManager->persist($article);
    $entityManager->flush();
} catch (UniqueConstraintViolationException $e) {
    $this->addFlash('error', 'That title is already taken.');
} catch (\Throwable $e) {
    $logger->error('Could not save article', ['exception' => $e]);
    $this->addFlash('error', 'Something went wrong saving that.');
}

"That title is already taken" is actionable. "Record not inserted" is not, and swallowing the exception without logging it means nobody ever finds out why.

The entity manager closes after a failed flush

This is the part the old snippet does not mention and the part that actually bites. When flush() throws, Doctrine closes the entity manager. Every subsequent call on it in the same request throws EntityManagerClosed, so a controller that catches the error and then tries to save something else — an audit row, a fallback record — fails on the second write with a completely unrelated-looking error.

If you need to keep working after a failed write, that means starting from a fresh manager rather than reusing the closed one. Most of the time the right answer is simpler: catch it, flash it, log it, and return.

What replaced the old API

Old Now
$this->getDoctrine() Inject EntityManagerInterface (removed in Symfony 6.0)
->getEntityManager() ->getManager()
$this->get('session')->setFlash() $this->addFlash()
catch (Exception $e) catch (\Throwable $e)