Skip to content

Commit c2c5000

Browse files
authored
Merge pull request #11646 from greg0ire/finally-fix-bug
Run risky code in finally block
2 parents 6281c2b + b6137c8 commit c2c5000

File tree

4 files changed

+133
-19
lines changed

4 files changed

+133
-19
lines changed

Diff for: src/EntityManager.php

+22-11
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,24 @@ 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+
if ($this->conn->isTransactionActive()) {
263+
$this->conn->rollBack();
264+
}
265+
}
261266
}
262267
}
263268

@@ -268,18 +273,24 @@ public function wrapInTransaction(callable $func)
268273
{
269274
$this->conn->beginTransaction();
270275

276+
$successful = false;
277+
271278
try {
272279
$return = $func($this);
273280

274281
$this->flush();
275282
$this->conn->commit();
276283

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

282-
throw $e;
286+
return $return;
287+
} finally {
288+
if (! $successful) {
289+
$this->close();
290+
if ($this->conn->isTransactionActive()) {
291+
$this->conn->rollBack();
292+
}
293+
}
283294
}
284295
}
285296

Diff for: src/UnitOfWork.php

+11-8
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();

Diff for: tests/Tests/ORM/EntityManagerTest.php

+62
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,21 @@
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;
32+
use PHPUnit\Framework\Assert;
2933
use stdClass;
3034
use TypeError;
3135

3236
use function get_class;
3337
use function random_int;
38+
use function sprintf;
3439
use function sys_get_temp_dir;
3540
use function uniqid;
3641

@@ -382,4 +387,61 @@ public function testDeprecatedFlushWithArguments(): void
382387

383388
$this->entityManager->flush($entity);
384389
}
390+
391+
/** @dataProvider entityManagerMethodNames */
392+
public function testItPreservesTheOriginalExceptionOnRollbackFailure(string $methodName): void
393+
{
394+
$entityManager = new EntityManagerMock(new class extends ConnectionMock {
395+
public function rollBack(): bool
396+
{
397+
throw new Exception('Rollback exception');
398+
}
399+
});
400+
401+
try {
402+
$entityManager->transactional(static function (): void {
403+
throw new Exception('Original exception');
404+
});
405+
self::fail('Exception expected');
406+
} catch (Exception $e) {
407+
self::assertSame('Rollback exception', $e->getMessage());
408+
self::assertNotNull($e->getPrevious());
409+
self::assertSame('Original exception', $e->getPrevious()->getMessage());
410+
}
411+
}
412+
413+
/** @dataProvider entityManagerMethodNames */
414+
public function testItDoesNotAttemptToRollbackIfNoTransactionIsActive(string $methodName): void
415+
{
416+
$entityManager = new EntityManagerMock(
417+
new class extends ConnectionMock {
418+
public function commit(): bool
419+
{
420+
throw new Exception('Commit exception that happens after doing the actual commit');
421+
}
422+
423+
public function rollBack(): bool
424+
{
425+
Assert::fail('Should not attempt to rollback if no transaction is active');
426+
}
427+
428+
public function isTransactionActive(): bool
429+
{
430+
return false;
431+
}
432+
}
433+
);
434+
435+
$this->expectExceptionMessage('Commit exception');
436+
$entityManager->$methodName(static function (): void {
437+
});
438+
}
439+
440+
/** @return Generator<string, array{string}> */
441+
public function entityManagerMethodNames(): Generator
442+
{
443+
foreach (['transactional', 'wrapInTransaction'] as $methodName) {
444+
yield sprintf('%s()', $methodName) => [$methodName];
445+
}
446+
}
385447
}

Diff for: tests/Tests/ORM/UnitOfWorkTest.php

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