Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Run risky code in finally block #11646

Merged
merged 2 commits into from
Oct 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading