Skip to content

Commit 51be1b1

Browse files
committed
Run risky code in finally block
catch blocks are not supposed to fail. If you want to do something despite an exception happening, you should do it in a finally block. Closes doctrine#7545
1 parent bc37f75 commit 51be1b1

File tree

4 files changed

+101
-19
lines changed

4 files changed

+101
-19
lines changed

src/EntityManager.php

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@
3232
use Doctrine\Persistence\Mapping\MappingException;
3333
use Doctrine\Persistence\ObjectRepository;
3434
use InvalidArgumentException;
35-
use Throwable;
3635

3736
use function array_keys;
3837
use function class_exists;
@@ -246,18 +245,22 @@ public function transactional($func)
246245

247246
$this->conn->beginTransaction();
248247

248+
$successful = false;
249+
249250
try {
250251
$return = $func($this);
251252

252253
$this->flush();
253254
$this->conn->commit();
254255

255-
return $return ?: true;
256-
} catch (Throwable $e) {
257-
$this->close();
258-
$this->conn->rollBack();
256+
$successful = true;
259257

260-
throw $e;
258+
return $return ?: true;
259+
} finally {
260+
if (! $successful) {
261+
$this->close();
262+
$this->conn->rollBack();
263+
}
261264
}
262265
}
263266

@@ -268,18 +271,22 @@ public function wrapInTransaction(callable $func)
268271
{
269272
$this->conn->beginTransaction();
270273

274+
$successful = false;
275+
271276
try {
272277
$return = $func($this);
273278

274279
$this->flush();
275280
$this->conn->commit();
276281

277-
return $return;
278-
} catch (Throwable $e) {
279-
$this->close();
280-
$this->conn->rollBack();
282+
$successful = true;
281283

282-
throw $e;
284+
return $return;
285+
} finally {
286+
if (! $successful) {
287+
$this->close();
288+
$this->conn->rollBack();
289+
}
283290
}
284291
}
285292

src/UnitOfWork.php

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@
4949
use Exception;
5050
use InvalidArgumentException;
5151
use RuntimeException;
52-
use Throwable;
5352
use UnexpectedValueException;
5453

5554
use function array_chunk;
@@ -427,6 +426,8 @@ public function commit($entity = null)
427426
$conn = $this->em->getConnection();
428427
$conn->beginTransaction();
429428

429+
$successful = false;
430+
430431
try {
431432
// Collection deletions (deletions of complete collections)
432433
foreach ($this->collectionDeletions as $collectionToDelete) {
@@ -478,16 +479,18 @@ public function commit($entity = null)
478479

479480
throw new OptimisticLockException('Commit failed', $object);
480481
}
481-
} catch (Throwable $e) {
482-
$this->em->close();
483482

484-
if ($conn->isTransactionActive()) {
485-
$conn->rollBack();
486-
}
483+
$successful = true;
484+
} finally {
485+
if (! $successful) {
486+
$this->em->close();
487487

488-
$this->afterTransactionRolledBack();
488+
if ($conn->isTransactionActive()) {
489+
$conn->rollBack();
490+
}
489491

490-
throw $e;
492+
$this->afterTransactionRolledBack();
493+
}
491494
}
492495

493496
$this->afterTransactionComplete();

tests/Tests/ORM/EntityManagerTest.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,20 @@
2121
use Doctrine\ORM\UnitOfWork;
2222
use Doctrine\Persistence\Mapping\Driver\MappingDriver;
2323
use Doctrine\Persistence\Mapping\MappingException;
24+
use Doctrine\Tests\Mocks\ConnectionMock;
25+
use Doctrine\Tests\Mocks\EntityManagerMock;
2426
use Doctrine\Tests\Models\CMS\CmsUser;
2527
use Doctrine\Tests\Models\GeoNames\Country;
2628
use Doctrine\Tests\OrmTestCase;
29+
use Exception;
2730
use Generator;
2831
use InvalidArgumentException;
2932
use stdClass;
3033
use TypeError;
3134

3235
use function get_class;
3336
use function random_int;
37+
use function sprintf;
3438
use function sys_get_temp_dir;
3539
use function uniqid;
3640

@@ -382,4 +386,34 @@ public function testDeprecatedFlushWithArguments(): void
382386

383387
$this->entityManager->flush($entity);
384388
}
389+
390+
/** @dataProvider entityManagerMethodNames */
391+
public function testItPreservesTheOriginalExceptionOnRollbackFailure(string $methodName): void
392+
{
393+
$entityManager = new EntityManagerMock(new class extends ConnectionMock {
394+
public function rollBack(): bool
395+
{
396+
throw new Exception('Rollback exception');
397+
}
398+
});
399+
400+
try {
401+
$entityManager->transactional(static function (): void {
402+
throw new Exception('Original exception');
403+
});
404+
self::fail('Exception expected');
405+
} catch (Exception $e) {
406+
self::assertSame('Rollback exception', $e->getMessage());
407+
self::assertNotNull($e->getPrevious());
408+
self::assertSame('Original exception', $e->getPrevious()->getMessage());
409+
}
410+
}
411+
412+
/** @return Generator<string, array{string}> */
413+
public function entityManagerMethodNames(): Generator
414+
{
415+
foreach (['transactional', 'wrapInTransaction'] as $methodName) {
416+
yield sprintf('%s()', $methodName) => [$methodName];
417+
}
418+
}
385419
}

tests/Tests/ORM/UnitOfWorkTest.php

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
use Doctrine\Tests\Models\GeoNames\Country;
4242
use Doctrine\Tests\OrmTestCase;
4343
use Doctrine\Tests\PHPUnitCompatibility\MockBuilderCompatibilityTools;
44+
use Exception;
4445
use PHPUnit\Framework\MockObject\MockObject;
4546
use stdClass;
4647

@@ -971,6 +972,43 @@ public function testItThrowsWhenApplicationProvidedIdsCollide(): void
971972

972973
$this->_unitOfWork->persist($phone2);
973974
}
975+
976+
public function testItPreservesTheOriginalExceptionOnRollbackFailure(): void
977+
{
978+
$this->_connectionMock = new class extends ConnectionMock {
979+
public function commit(): bool
980+
{
981+
return false; // this should cause an exception
982+
}
983+
984+
public function rollBack(): bool
985+
{
986+
throw new Exception('Rollback exception');
987+
}
988+
};
989+
$this->_emMock = new EntityManagerMock($this->_connectionMock);
990+
$this->_unitOfWork = new UnitOfWorkMock($this->_emMock);
991+
$this->_emMock->setUnitOfWork($this->_unitOfWork);
992+
993+
// Setup fake persister and id generator
994+
$userPersister = new EntityPersisterMock($this->_emMock, $this->_emMock->getClassMetadata(ForumUser::class));
995+
$userPersister->setMockIdGeneratorType(ClassMetadata::GENERATOR_TYPE_IDENTITY);
996+
$this->_unitOfWork->setEntityPersister(ForumUser::class, $userPersister);
997+
998+
// Create a test user
999+
$user = new ForumUser();
1000+
$user->username = 'Jasper';
1001+
$this->_unitOfWork->persist($user);
1002+
1003+
try {
1004+
$this->_unitOfWork->commit();
1005+
self::fail('Exception expected');
1006+
} catch (Exception $e) {
1007+
self::assertSame('Rollback exception', $e->getMessage());
1008+
self::assertNotNull($e->getPrevious());
1009+
self::assertSame('Commit failed', $e->getPrevious()->getMessage());
1010+
}
1011+
}
9741012
}
9751013

9761014
/** @Entity */

0 commit comments

Comments
 (0)