Skip to content

Commit

Permalink
Merge pull request #11646 from greg0ire/finally-fix-bug
Browse files Browse the repository at this point in the history
Run risky code in finally block
  • Loading branch information
greg0ire authored Oct 10, 2024
2 parents 6281c2b + b6137c8 commit c2c5000
Show file tree
Hide file tree
Showing 4 changed files with 133 additions and 19 deletions.
33 changes: 22 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,24 @@ 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();
if ($this->conn->isTransactionActive()) {
$this->conn->rollBack();
}
}
}
}

Expand All @@ -268,18 +273,24 @@ 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();
if ($this->conn->isTransactionActive()) {
$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
62 changes: 62 additions & 0 deletions tests/Tests/ORM/EntityManagerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,21 @@
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 PHPUnit\Framework\Assert;
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 +387,61 @@ 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());
}
}

/** @dataProvider entityManagerMethodNames */
public function testItDoesNotAttemptToRollbackIfNoTransactionIsActive(string $methodName): void
{
$entityManager = new EntityManagerMock(
new class extends ConnectionMock {
public function commit(): bool
{
throw new Exception('Commit exception that happens after doing the actual commit');
}

public function rollBack(): bool
{
Assert::fail('Should not attempt to rollback if no transaction is active');
}

public function isTransactionActive(): bool
{
return false;
}
}
);

$this->expectExceptionMessage('Commit exception');
$entityManager->$methodName(static function (): void {
});
}

/** @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 c2c5000

Please sign in to comment.