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