Skip to content

Commit

Permalink
Run risky code in finally block
Browse files Browse the repository at this point in the history
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 #7545
  • Loading branch information
greg0ire committed Oct 10, 2024
1 parent bc37f75 commit 51be1b1
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 19 deletions.
29 changes: 18 additions & 11 deletions src/EntityManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@
use Doctrine\Persistence\Mapping\MappingException;
use Doctrine\Persistence\ObjectRepository;
use InvalidArgumentException;
use Throwable;

use function array_keys;
use function class_exists;
Expand Down Expand Up @@ -246,18 +245,22 @@ public function transactional($func)

$this->conn->beginTransaction();

$successful = false;

try {
$return = $func($this);

$this->flush();
$this->conn->commit();

return $return ?: true;
} catch (Throwable $e) {
$this->close();
$this->conn->rollBack();
$successful = true;

throw $e;
return $return ?: true;
} finally {
if (! $successful) {
$this->close();
$this->conn->rollBack();
}
}
}

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

$successful = false;

try {
$return = $func($this);

$this->flush();
$this->conn->commit();

return $return;
} catch (Throwable $e) {
$this->close();
$this->conn->rollBack();
$successful = true;

throw $e;
return $return;
} finally {
if (! $successful) {
$this->close();
$this->conn->rollBack();
}
}
}

Expand Down
19 changes: 11 additions & 8 deletions src/UnitOfWork.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@
use Exception;
use InvalidArgumentException;
use RuntimeException;
use Throwable;
use UnexpectedValueException;

use function array_chunk;
Expand Down Expand Up @@ -427,6 +426,8 @@ public function commit($entity = null)
$conn = $this->em->getConnection();
$conn->beginTransaction();

$successful = false;

try {
// Collection deletions (deletions of complete collections)
foreach ($this->collectionDeletions as $collectionToDelete) {
Expand Down Expand Up @@ -478,16 +479,18 @@ public function commit($entity = null)

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

if ($conn->isTransactionActive()) {
$conn->rollBack();
}
$successful = true;
} finally {
if (! $successful) {
$this->em->close();

$this->afterTransactionRolledBack();
if ($conn->isTransactionActive()) {
$conn->rollBack();
}

throw $e;
$this->afterTransactionRolledBack();
}
}

$this->afterTransactionComplete();
Expand Down
34 changes: 34 additions & 0 deletions tests/Tests/ORM/EntityManagerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,20 @@
use Doctrine\ORM\UnitOfWork;
use Doctrine\Persistence\Mapping\Driver\MappingDriver;
use Doctrine\Persistence\Mapping\MappingException;
use Doctrine\Tests\Mocks\ConnectionMock;
use Doctrine\Tests\Mocks\EntityManagerMock;
use Doctrine\Tests\Models\CMS\CmsUser;
use Doctrine\Tests\Models\GeoNames\Country;
use Doctrine\Tests\OrmTestCase;
use Exception;
use Generator;
use InvalidArgumentException;
use stdClass;
use TypeError;

use function get_class;
use function random_int;
use function sprintf;
use function sys_get_temp_dir;
use function uniqid;

Expand Down Expand Up @@ -382,4 +386,34 @@ public function testDeprecatedFlushWithArguments(): void

$this->entityManager->flush($entity);
}

/** @dataProvider entityManagerMethodNames */
public function testItPreservesTheOriginalExceptionOnRollbackFailure(string $methodName): void
{
$entityManager = new EntityManagerMock(new class extends ConnectionMock {
public function rollBack(): bool
{
throw new Exception('Rollback exception');
}
});

try {
$entityManager->transactional(static function (): void {
throw new Exception('Original exception');
});
self::fail('Exception expected');
} catch (Exception $e) {
self::assertSame('Rollback exception', $e->getMessage());
self::assertNotNull($e->getPrevious());
self::assertSame('Original exception', $e->getPrevious()->getMessage());
}
}

/** @return Generator<string, array{string}> */
public function entityManagerMethodNames(): Generator
{
foreach (['transactional', 'wrapInTransaction'] as $methodName) {
yield sprintf('%s()', $methodName) => [$methodName];
}
}
}
38 changes: 38 additions & 0 deletions tests/Tests/ORM/UnitOfWorkTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
use Doctrine\Tests\Models\GeoNames\Country;
use Doctrine\Tests\OrmTestCase;
use Doctrine\Tests\PHPUnitCompatibility\MockBuilderCompatibilityTools;
use Exception;
use PHPUnit\Framework\MockObject\MockObject;
use stdClass;

Expand Down Expand Up @@ -971,6 +972,43 @@ public function testItThrowsWhenApplicationProvidedIdsCollide(): void

$this->_unitOfWork->persist($phone2);
}

public function testItPreservesTheOriginalExceptionOnRollbackFailure(): void
{
$this->_connectionMock = new class extends ConnectionMock {
public function commit(): bool
{
return false; // this should cause an exception
}

public function rollBack(): bool
{
throw new Exception('Rollback exception');
}
};
$this->_emMock = new EntityManagerMock($this->_connectionMock);
$this->_unitOfWork = new UnitOfWorkMock($this->_emMock);
$this->_emMock->setUnitOfWork($this->_unitOfWork);

// Setup fake persister and id generator
$userPersister = new EntityPersisterMock($this->_emMock, $this->_emMock->getClassMetadata(ForumUser::class));
$userPersister->setMockIdGeneratorType(ClassMetadata::GENERATOR_TYPE_IDENTITY);
$this->_unitOfWork->setEntityPersister(ForumUser::class, $userPersister);

// Create a test user
$user = new ForumUser();
$user->username = 'Jasper';
$this->_unitOfWork->persist($user);

try {
$this->_unitOfWork->commit();
self::fail('Exception expected');
} catch (Exception $e) {
self::assertSame('Rollback exception', $e->getMessage());
self::assertNotNull($e->getPrevious());
self::assertSame('Commit failed', $e->getPrevious()->getMessage());
}
}
}

/** @Entity */
Expand Down

0 comments on commit 51be1b1

Please sign in to comment.