PostgreSQL 18 can report an ON DELETE RESTRICT or ON UPDATE RESTRICT failure as SQLSTATE 23001. A Doctrine DBAL version that maps only 23503 may therefore throw a generic DriverException instead of ForeignKeyConstraintViolationException.
Last updated: November 10, 2026.
<?php
use Doctrine\DBAL\Exception\DriverException;
use Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException;
try {
$connection->executeStatement('DELETE FROM parent WHERE id = ?', [$id]);
} catch (ForeignKeyConstraintViolationException $e) {
reportDependentRows($id);
} catch (DriverException $e) {
if ($e->getSQLState() !== '23001') {
throw $e;
}
reportDependentRows($id); // Temporary PostgreSQL 18 compatibility path.
}Keep the fallback next to the normal Doctrine exception and preserve the original exception for logging. Remove the special branch after the deployed DBAL release converts 23001 itself.
Confirm the server code, not the message text
The PostgreSQL 18 error-code appendix identifies 23001 as restrict_violation and 23503 as foreign_key_violation. Applications should branch on SQLSTATE because localized message text and constraint names can vary.
A current Doctrine DBAL report documents the conversion gap for PostgreSQL 18. Verify your exact DBAL version and inspect its release notes or tests before adding the fallback; a maintained release may already contain the mapping.
Keep the compatibility layer safe
Catch the specific Doctrine foreign-key exception first. In the generic driver catch, handle only SQLSTATE 23001 and rethrow every other database error. Do not translate all class-23 integrity errors into the same user message because not-null, unique, check, and exclusion failures require different responses.
Add integration tests for both RESTRICT and NO ACTION against PostgreSQL 18, and run the older supported server version if the application spans an upgrade window. Log the SQLSTATE, constraint name, DBAL version, and server version without logging bound secrets. Once the official converter passes both tests, delete the temporary branch so application behavior again depends on Doctrine’s typed exception hierarchy.