Compare commits

...

6 Commits

Author SHA1 Message Date
Grégoire Paris
c2c500077b Merge pull request #11646 from greg0ire/finally-fix-bug
Run risky code in finally block
2024-10-10 11:46:49 +02:00
Grégoire Paris
6281c2b79f Merge pull request #11655 from greg0ire/submodule-cleanup
Submodule cleanup
2024-10-10 11:12:12 +02:00
Grégoire Paris
bac1c17eab Remove submodule remnant
This should make a warning we have in the CI go away.

>  fatal: No url found for submodule path 'docs/en/_theme' in .gitmodules
2024-10-10 11:07:38 +02:00
Grégoire Paris
b6137c8911 Add guard clause
It maybe happen that the SQL COMMIT statement is successful, but then
something goes wrong. In that kind of case, you do not want to attempt a
rollback.

This was implemented in UnitOfWork::commit(), but for some reason not in
the similar EntityManager methods.
2024-10-10 10:58:24 +02:00
Grégoire Paris
51be1b1d52 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 #7545
2024-10-10 10:06:12 +02:00
Matthias Pigulla
16a8f10fd2 Remove a misleading comment (#11644) 2024-10-09 15:37:04 +02:00
6 changed files with 134 additions and 23 deletions

Submodule docs/en/_theme deleted from 6f1bc8bead

View File

@@ -32,7 +32,6 @@ use Doctrine\ORM\Repository\RepositoryFactory;
use Doctrine\Persistence\Mapping\MappingException;
use Doctrine\Persistence\ObjectRepository;
use InvalidArgumentException;
use Throwable;
use function array_keys;
use function class_exists;
@@ -246,18 +245,24 @@ class EntityManager implements EntityManagerInterface
$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();
}
}
}
}
@@ -268,18 +273,24 @@ class EntityManager implements EntityManagerInterface
{
$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();
}
}
}
}

View File

@@ -14,8 +14,6 @@ use Doctrine\ORM\Query\SqlWalker;
* that are mapped to a single table.
*
* @link www.doctrine-project.org
*
* @todo This is exactly the same as SingleSelectExecutor. Unify in SingleStatementExecutor.
*/
class SingleTableDeleteUpdateExecutor extends AbstractSqlExecutor
{

View File

@@ -49,7 +49,6 @@ use Doctrine\Persistence\PropertyChangedListener;
use Exception;
use InvalidArgumentException;
use RuntimeException;
use Throwable;
use UnexpectedValueException;
use function array_chunk;
@@ -427,6 +426,8 @@ class UnitOfWork implements PropertyChangedListener
$conn = $this->em->getConnection();
$conn->beginTransaction();
$successful = false;
try {
// Collection deletions (deletions of complete collections)
foreach ($this->collectionDeletions as $collectionToDelete) {
@@ -478,16 +479,18 @@ class UnitOfWork implements PropertyChangedListener
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();
if ($conn->isTransactionActive()) {
$conn->rollBack();
}
$this->afterTransactionRolledBack();
}
$this->afterTransactionRolledBack();
throw $e;
}
$this->afterTransactionComplete();

View File

@@ -21,16 +21,21 @@ use Doctrine\ORM\QueryBuilder;
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;
@@ -382,4 +387,61 @@ class EntityManagerTest extends OrmTestCase
$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];
}
}
}

View File

@@ -41,6 +41,7 @@ use Doctrine\Tests\Models\GeoNames\City;
use Doctrine\Tests\Models\GeoNames\Country;
use Doctrine\Tests\OrmTestCase;
use Doctrine\Tests\PHPUnitCompatibility\MockBuilderCompatibilityTools;
use Exception;
use PHPUnit\Framework\MockObject\MockObject;
use stdClass;
@@ -971,6 +972,43 @@ class UnitOfWorkTest extends OrmTestCase
$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 */