Skip to content

Commit

Permalink
Improve exception message at ModelManager::batchDelete()
Browse files Browse the repository at this point in the history
  • Loading branch information
phansys committed Apr 2, 2023
1 parent 87a84a1 commit 15c3ae2
Show file tree
Hide file tree
Showing 2 changed files with 167 additions and 2 deletions.
31 changes: 29 additions & 2 deletions src/Model/ModelManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ final class ModelManager implements ModelManagerInterface, LockInterface, ProxyR
{
public const ID_SEPARATOR = '~';

private const BATCH_SIZE = 20;

/**
* @var EntityManagerInterface[]
*/
Expand Down Expand Up @@ -362,22 +364,47 @@ public function batchDelete(string $class, BaseProxyQueryInterface $query): void

$entityManager = $this->getEntityManager($class);
$i = 0;
$confirmedDeletionsCount = 0;

try {
foreach ($query->getDoctrineQuery()->toIterable() as $object) {
$entityManager->remove($object);

if (0 === (++$i % 20)) {
if (0 === (++$i % self::BATCH_SIZE)) {
$entityManager->flush();
$confirmedDeletionsCount = $i;
$entityManager->clear();
}
}

$entityManager->flush();
$entityManager->clear();
} catch (\PDOException|Exception $exception) {
$id = null;

if (isset($object)) {
$id = $this->getNormalizedIdentifier($object);
}

if (null === $id) {
throw new ModelManagerException(
sprintf('Failed to perform batch deletion for "%s" objects', $class),
(int) $exception->getCode(),
$exception
);
}

$msg = 'Failed to delete object "%s" (id: %s) while performing batch deletion';
if ($i > self::BATCH_SIZE) {
$msg .= sprintf(' (%u objects were successfully deleted before this error)', $confirmedDeletionsCount);
}

throw new ModelManagerException(
sprintf('Failed to delete object: %s', $class),
sprintf(
$msg,
$class,
$id
),
(int) $exception->getCode(),
$exception
);
Expand Down
138 changes: 138 additions & 0 deletions tests/Model/ModelManagerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,18 @@
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\Type;
use Doctrine\ORM\AbstractQuery;
use Doctrine\ORM\Configuration;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Internal\Hydration\SimpleObjectHydrator;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\ClassMetadataFactory;
use Doctrine\ORM\OptimisticLockException;
use Doctrine\ORM\Query;
use Doctrine\ORM\QueryBuilder;
use Doctrine\ORM\UnitOfWork;
use Doctrine\Persistence\ManagerRegistry;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\MockObject\Stub\Exception as ExceptionStub;
use PHPUnit\Framework\TestCase;
use Sonata\AdminBundle\Exception\LockException;
use Sonata\AdminBundle\Exception\ModelManagerException;
Expand Down Expand Up @@ -559,6 +565,138 @@ public function testRemove(\Throwable $exception): void
$this->modelManager->delete(new VersionedEntity());
}

/**
* @return iterable<int|string, array<int, string|array<int, object|null>|null>>
*
* @phpstan-return iterable<int|string, array{0: string, 1: ?array<int, object>, 2: array<int, ?ExceptionStub>}>
*/
public function failingBatchDeleteProvider(): iterable
{
yield [
'Failed to delete object "Sonata\DoctrineORMAdminBundle\Tests\Fixtures\Entity\VersionedEntity" (id: 42) while'
.' performing batch deletion (20 objects were successfully deleted before this error)',
array_fill(0, 21, new VersionedEntity()),
[null, static::throwException(new Exception())],
];

yield [
'Failed to delete object "Sonata\DoctrineORMAdminBundle\Tests\Fixtures\Entity\VersionedEntity" (id: 42) while'
.' performing batch deletion',
[new VersionedEntity(), new VersionedEntity()],
[static::throwException(new Exception())],
];

yield [
'Failed to perform batch deletion for "Sonata\DoctrineORMAdminBundle\Tests\Fixtures\Entity\VersionedEntity" objects',
null,
[null],
];
}

/**
* @param array<int, object>|null $result
* @param array<int, ExceptionStub|null> $onConsecutiveFlush
*
* @dataProvider failingBatchDeleteProvider
*/
public function testFailingBatchDelete(string $expectedExceptionMessage, ?array $result, array $onConsecutiveFlush): void
{
$classMetadata = $this->createMock(ClassMetadata::class);
$classMetadata
->method('getIdentifierValues')
->willReturn([
'id' => 42,
]);
$classMetadata->expects(static::once())
->method('getIdentifierFieldNames')
->willReturn(['id']);
$classMetadata->table['name'] = 'versioned_entity';

$em = $this->setGetMetadataExpectation(VersionedEntity::class, $classMetadata);
$em
->expects(static::exactly(null === $result ? 0 : \count($result)))
->method('remove');
$em
->expects(static::exactly(null === $result ? 0 : (int) ceil(\count($result) / 20)))
->method('flush')
->will(static::onConsecutiveCalls(
...$onConsecutiveFlush
));
$em
->method('getConfiguration')
->willReturn(new Configuration());

$uow = $this->createMock(UnitOfWork::class);
$uow
->expects(static::exactly(null === $result ? 0 : 1))
->method('getEntityState')
->willReturn(UnitOfWork::STATE_MANAGED);

$em
->expects(static::exactly(null === $result ? 0 : 1))
->method('getUnitOfWork')
->willReturn($uow);

$connection = $this->createMock(Connection::class);
$connection
->method('getDatabasePlatform')
->willReturn($this->createStub(AbstractPlatform::class));
$connection
->method('getParams')
->willReturn([]);

$em
->method('getConnection')
->willReturn($connection);

$hydrator = $this->createMock(SimpleObjectHydrator::class);
$hydrator
->expects(static::once())
->method('toIterable')
->willReturnCallback(static function () use ($result): iterable {
if (null === $result) {
throw new Exception();
}

return $result;
});

$em
->expects(static::once())
->method('newHydrator')
->willReturn($hydrator);

$queryBuilder = new QueryBuilder($em);
$queryBuilder
->select('ve')
->from(VersionedEntity::class, 've');

$query = new Query($em);
$query->setDQL($queryBuilder->getDQL());

$em
->expects(static::once())
->method('createQuery')
->willReturn($query);

$cmf = $this->createMock(ClassMetadataFactory::class);
$cmf
->expects(static::once())
->method('getMetadataFor')
->with(VersionedEntity::class)
->willReturn($classMetadata);

$em
->expects(static::once())
->method('getMetadataFactory')
->willReturn($cmf);

$this->expectException(ModelManagerException::class);
$this->expectExceptionMessage($expectedExceptionMessage);

$this->modelManager->batchDelete(VersionedEntity::class, new ProxyQuery($queryBuilder));
}

/**
* @param string[] $expectedParameters
* @param string[] $identifierFieldNames
Expand Down

0 comments on commit 15c3ae2

Please sign in to comment.