diff --git a/.docker/php/Dockerfile b/.docker/php/Dockerfile
index 33dd796..82be215 100644
--- a/.docker/php/Dockerfile
+++ b/.docker/php/Dockerfile
@@ -1,4 +1,4 @@
-FROM php:8.4-fpm-alpine
+FROM php:8.5-fpm-alpine
ARG UID
ARG GID
@@ -13,8 +13,7 @@ RUN apk update && apk add \
bash \
icu-dev \
&& docker-php-ext-configure intl \
- && docker-php-ext-install intl opcache \
- && docker-php-ext-enable opcache
+ && docker-php-ext-install intl
RUN ln -s /usr/share/zoneinfo/Europe/Paris /etc/localtime \
&& sed -i "s/^;date.timezone =.*/date.timezone = Europe\/Paris/" $PHP_INI_DIR/php.ini
diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml
index a07ce8b..f4e4f5b 100644
--- a/.github/workflows/quality.yml
+++ b/.github/workflows/quality.yml
@@ -15,11 +15,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@v6
- name: Install PHP with extensions
uses: shivammathur/setup-php@v2
with:
- php-version: '8.4'
+ php-version: '8.5'
coverage: none
tools: composer:v2
- name: Install Composer dependencies (locked)
@@ -32,11 +32,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@v6
- name: Install PHP with extensions
uses: shivammathur/setup-php@v2
with:
- php-version: '8.4'
+ php-version: '8.5'
coverage: none
tools: composer:v2
- name: Install Composer dependencies (locked)
@@ -49,11 +49,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@v6
- name: Install PHP with extensions
uses: shivammathur/setup-php@v2
with:
- php-version: '8.4'
+ php-version: '8.5'
coverage: none
tools: composer:v2
- name: Install Composer dependencies (locked)
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 0e07db0..1e0baed 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -23,13 +23,14 @@ jobs:
- '8.2'
- '8.3'
- '8.4'
- dependencies: [highest]
+ - '8.5'
+ dependencies: [highest, lowest]
allowed-to-fail: [false]
symfony-require: ['']
variant: [normal]
include:
- - php-version: '8.1'
- dependencies: highest
+ - php-version: '8.2'
+ dependencies: lowest
allowed-to-fail: false
symfony-require: 6.4.*
variant: symfony/symfony:"6.4.*"
@@ -41,8 +42,8 @@ jobs:
- php-version: '8.2'
dependencies: highest
allowed-to-fail: false
- symfony-require: 7.3.*
- variant: symfony/symfony:"7.3.*"
+ symfony-require: 7.4.*
+ variant: symfony/symfony:"7.4.*"
- php-version: '8.3'
dependencies: highest
allowed-to-fail: false
@@ -51,8 +52,8 @@ jobs:
- php-version: '8.3'
dependencies: highest
allowed-to-fail: false
- symfony-require: 7.3.*
- variant: symfony/symfony:"7.3.*"
+ symfony-require: 7.4.*
+ variant: symfony/symfony:"7.4.*"
- php-version: '8.4'
dependencies: highest
allowed-to-fail: false
@@ -61,12 +62,31 @@ jobs:
- php-version: '8.4'
dependencies: highest
allowed-to-fail: false
- symfony-require: 7.3.*
- variant: symfony/symfony:"7.3.*"
-
+ symfony-require: 7.4.*
+ variant: symfony/symfony:"7.4.*"
+ - php-version: '8.4'
+ dependencies: highest
+ allowed-to-fail: false
+ symfony-require: 8.*
+ variant: symfony/symfony:"8.*"
+ - php-version: '8.5'
+ dependencies: highest
+ allowed-to-fail: false
+ symfony-require: 6.4.*
+ variant: symfony/symfony:"6.4.*"
+ - php-version: '8.5'
+ dependencies: highest
+ allowed-to-fail: false
+ symfony-require: 7.4.*
+ variant: symfony/symfony:"7.4.*"
+ - php-version: '8.5'
+ dependencies: highest
+ allowed-to-fail: false
+ symfony-require: 8.*
+ variant: symfony/symfony:"8.*"
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@v6
- name: Install PHP with extensions
uses: shivammathur/setup-php@v2
with:
diff --git a/.gitignore b/.gitignore
index ca08796..f9e680f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,3 +7,4 @@
.phpunit.cache
.php-cs-fixer.cache
coverage-report
+GEMINI.md
diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php
index 273e26d..9222ff1 100644
--- a/.php-cs-fixer.dist.php
+++ b/.php-cs-fixer.dist.php
@@ -24,9 +24,8 @@
return (new PhpCsFixer\Config())
->setRules([
- '@PHP71Migration' => true,
- '@PHP82Migration' => true,
- '@PHPUnit75Migration:risky' => true,
+ '@PHP8x2Migration' => true,
+ '@PHPUnit7x5Migration:risky' => true,
'@Symfony' => true,
'@Symfony:risky' => true,
'@DoctrineAnnotation' => true,
@@ -35,7 +34,7 @@
'header_comment' => ['header' => $fileHeaderComment],
'modernize_strpos' => true,
'get_class_to_class_keyword' => true,
- 'phpdoc_to_comment' => ['ignored_tags' => ['var']], // Fix issue on initializeStatement method $params variable
+ 'phpdoc_to_comment' => ['ignored_tags' => ['var']], // Fix an issue on initializeStatement method $params variable
])
->setRiskyAllowed(true)
->setFinder(
diff --git a/composer.json b/composer.json
index 10e1643..b3462c7 100644
--- a/composer.json
+++ b/composer.json
@@ -49,21 +49,20 @@
}
},
"require": {
- "php": ">=8.1",
- "cleverage/process-bundle": "^4.0",
- "doctrine/common": "^3.0",
- "doctrine/dbal": "^2.9 || ^3.0",
- "doctrine/doctrine-bundle": "^2.5",
- "doctrine/doctrine-migrations-bundle": "^3.2",
- "doctrine/orm": "^2.9 || ^3.0"
+ "php": ">=8.2",
+ "cleverage/process-bundle": "^5.0",
+ "doctrine/common": "^3.5",
+ "doctrine/doctrine-bundle": "^2.18 || ^3.1",
+ "doctrine/doctrine-migrations-bundle": "^3.7 || ^4",
+ "doctrine/orm": "^2.20 || ^3.5"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "*",
"phpstan/extension-installer": "*",
"phpstan/phpstan": "*",
- "phpstan/phpstan-doctrine": "^2.0",
+ "phpstan/phpstan-doctrine": "*",
"phpstan/phpstan-symfony": "*",
- "phpunit/phpunit": "<10.0",
+ "phpunit/phpunit": "*",
"rector/rector": "*",
"roave/security-advisories": "dev-latest",
"symfony/test-pack": "^1.1"
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 766495c..c3e7947 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -1,27 +1,22 @@
+ failOnWarning="true">
tests
-
-
+
src
-
-
+
+
\ No newline at end of file
diff --git a/rector.php b/rector.php
index 9f9d327..8725ef8 100644
--- a/rector.php
+++ b/rector.php
@@ -8,18 +8,18 @@
use Rector\ValueObject\PhpVersion;
return RectorConfig::configure()
- ->withPhpVersion(PhpVersion::PHP_84)
+ ->withPhpVersion(PhpVersion::PHP_85)
->withPaths([
__DIR__.'/src',
__DIR__.'/tests',
])
- ->withPhpSets(php81: true)
- // here we can define, what prepared sets of rules will be applied
+ ->withPhpSets(php82: true)
+ // here we can define what prepared sets of rules will be applied
->withComposerBased(doctrine: true)
->withPreparedSets(deadCode: true, codeQuality: true, doctrineCodeQuality: true, symfonyCodeQuality: true)
->withAttributesSets(symfony: true, doctrine: true)
->withSets([
- LevelSetList::UP_TO_PHP_81,
+ LevelSetList::UP_TO_PHP_82,
SymfonySetList::SYMFONY_64,
SymfonySetList::SYMFONY_CODE_QUALITY,
SymfonySetList::SYMFONY_CONSTRUCTOR_INJECTION,
diff --git a/src/Task/Database/DatabaseReaderTask.php b/src/Task/Database/DatabaseReaderTask.php
index 639f2c0..9e1e430 100644
--- a/src/Task/Database/DatabaseReaderTask.php
+++ b/src/Task/Database/DatabaseReaderTask.php
@@ -17,7 +17,9 @@
use CleverAge\ProcessBundle\Model\FinalizableTaskInterface;
use CleverAge\ProcessBundle\Model\IterableTaskInterface;
use CleverAge\ProcessBundle\Model\ProcessState;
+use Doctrine\DBAL\ArrayParameterType;
use Doctrine\DBAL\Connection;
+use Doctrine\DBAL\ParameterType;
use Doctrine\DBAL\Result;
use Doctrine\DBAL\Types\Type;
use Doctrine\Persistence\ManagerRegistry;
@@ -37,7 +39,7 @@
* 'offset': ?int,
* 'input_as_params': bool,
* 'params': array,
- * 'types': array|array
+ * 'types': array|string, ArrayParameterType|ParameterType|Type|string>
* }
*/
class DatabaseReaderTask extends AbstractConfigurableTask implements IterableTaskInterface, FinalizableTaskInterface
diff --git a/src/Task/Database/DatabaseUpdaterTask.php b/src/Task/Database/DatabaseUpdaterTask.php
index 1d6df62..65aebd4 100644
--- a/src/Task/Database/DatabaseUpdaterTask.php
+++ b/src/Task/Database/DatabaseUpdaterTask.php
@@ -15,8 +15,10 @@
use CleverAge\ProcessBundle\Model\AbstractConfigurableTask;
use CleverAge\ProcessBundle\Model\ProcessState;
+use Doctrine\DBAL\ArrayParameterType;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Exception;
+use Doctrine\DBAL\ParameterType;
use Doctrine\DBAL\Types\Type;
use Doctrine\Persistence\ManagerRegistry;
use Psr\Log\LoggerInterface;
@@ -31,7 +33,7 @@
* 'sql': string,
* 'input_as_params': bool,
* 'params': array,
- * 'types': array|array
+ * 'types': array|string, ArrayParameterType|ParameterType|Type|string>
* }
*/
class DatabaseUpdaterTask extends AbstractConfigurableTask
diff --git a/src/Task/EntityManager/DoctrineBatchWriterTask.php b/src/Task/EntityManager/DoctrineBatchWriterTask.php
index f26bdf5..0fcd49f 100644
--- a/src/Task/EntityManager/DoctrineBatchWriterTask.php
+++ b/src/Task/EntityManager/DoctrineBatchWriterTask.php
@@ -72,7 +72,7 @@ protected function writeBatch(ProcessState $state): void
throw new \UnexpectedValueException("No manager found for class {$class}");
}
$entityManager->persist($entity);
- $entityManagers->attach($entityManager);
+ $entityManagers->offsetSet($entityManager);
}
foreach ($entityManagers as $entityManager) {
diff --git a/tests/Task/Database/DatabaseReaderTaskTest.php b/tests/Task/Database/DatabaseReaderTaskTest.php
new file mode 100644
index 0000000..82b7c98
--- /dev/null
+++ b/tests/Task/Database/DatabaseReaderTaskTest.php
@@ -0,0 +1,477 @@
+logger = $this->createStub(LoggerInterface::class);
+ $this->doctrine = $this->createStub(ManagerRegistry::class);
+ }
+
+ public function testExecute(): void
+ {
+ $state = $this->createMock(ProcessState::class);
+ $options = [
+ 'table' => 'my_table',
+ 'sql' => 'SELECT * FROM my_table',
+ 'limit' => null,
+ 'offset' => null,
+ 'paginate' => null,
+ 'input_as_params' => false,
+ 'params' => [],
+ 'types' => [],
+ 'empty_log_level' => LogLevel::WARNING,
+ 'connection' => null,
+ ];
+
+ $resultData = ['id' => 1, 'name' => 'test'];
+ $result = $this->createStub(Result::class);
+ $result->method('fetchAssociative')->willReturnOnConsecutiveCalls($resultData, false);
+
+ $connection = $this->createStub(Connection::class);
+ $connection->method('executeQuery')->willReturn($result);
+
+ $task = new class($this->logger, $this->doctrine, $options, $connection) extends DatabaseReaderTask {
+ /**
+ * @param array $testOptions
+ */
+ public function __construct(LoggerInterface $logger, ManagerRegistry $doctrine, private readonly array $testOptions, private readonly Connection $testConnection)
+ {
+ parent::__construct($logger, $doctrine);
+ }
+
+ /**
+ * @return array
+ */
+ protected function getOptions(?ProcessState $state = null): array
+ {
+ return $this->testOptions;
+ }
+
+ protected function getConnection(?ProcessState $state = null): Connection
+ {
+ return $this->testConnection;
+ }
+ };
+
+ $state->expects($this->once())->method('setOutput')->with($resultData);
+
+ $task->initialize($state);
+ $task->execute($state);
+ }
+
+ public function testExecuteWithPagination(): void
+ {
+ $state = $this->createMock(ProcessState::class);
+ $options = [
+ 'table' => 'my_table',
+ 'sql' => 'SELECT * FROM my_table',
+ 'limit' => null,
+ 'offset' => null,
+ 'paginate' => 2,
+ 'input_as_params' => false,
+ 'params' => [],
+ 'types' => [],
+ 'empty_log_level' => LogLevel::WARNING,
+ 'connection' => null,
+ ];
+
+ $resultData1 = ['id' => 1, 'name' => 'test1'];
+ $resultData2 = ['id' => 2, 'name' => 'test2'];
+ $result = $this->createStub(Result::class);
+ $result->method('fetchAssociative')->willReturnOnConsecutiveCalls($resultData1, $resultData2, false);
+
+ $connection = $this->createStub(Connection::class);
+ $connection->method('executeQuery')->willReturn($result);
+
+ $task = new class($this->logger, $this->doctrine, $options, $connection) extends DatabaseReaderTask {
+ /**
+ * @param array $testOptions
+ */
+ public function __construct(LoggerInterface $logger, ManagerRegistry $doctrine, private readonly array $testOptions, private readonly Connection $testConnection)
+ {
+ parent::__construct($logger, $doctrine);
+ }
+
+ /**
+ * @return array
+ */
+ protected function getOptions(?ProcessState $state = null): array
+ {
+ return $this->testOptions;
+ }
+
+ protected function getConnection(?ProcessState $state = null): Connection
+ {
+ return $this->testConnection;
+ }
+ };
+ $state->expects($this->once())->method('setOutput')->with([$resultData1, $resultData2]);
+
+ $task->initialize($state);
+ $task->execute($state);
+ }
+
+ public function testFinalize(): void
+ {
+ $task = new DatabaseReaderTask($this->logger, $this->doctrine);
+
+ $state = $this->createStub(ProcessState::class);
+
+ $result = $this->createMock(Result::class);
+ $result->expects($this->once())->method('free');
+
+ $reflection = new \ReflectionClass(DatabaseReaderTask::class);
+ $statementProperty = $reflection->getProperty('statement');
+ $statementProperty->setValue($task, $result);
+
+ $task->finalize($state);
+ }
+
+ public function testExecuteWithEmptyResult(): void
+ {
+ $logger = $this->createMock(LoggerInterface::class); // Use createMock to assert on logger
+ $state = $this->createMock(ProcessState::class);
+ $options = [
+ 'table' => 'my_table',
+ 'sql' => 'SELECT * FROM my_table',
+ 'limit' => null,
+ 'offset' => null,
+ 'paginate' => null,
+ 'input_as_params' => false,
+ 'params' => [],
+ 'types' => [],
+ 'empty_log_level' => LogLevel::WARNING,
+ 'connection' => null,
+ ];
+
+ $result = $this->createStub(Result::class);
+ $result->method('fetchAssociative')->willReturn(false);
+
+ $connection = $this->createStub(Connection::class);
+ $connection->method('executeQuery')->willReturn($result);
+
+ $task = new class($logger, $this->doctrine, $options, $connection) extends DatabaseReaderTask {
+ /**
+ * @param array $testOptions
+ */
+ public function __construct(LoggerInterface $logger, ManagerRegistry $doctrine, private readonly array $testOptions, private readonly Connection $testConnection)
+ {
+ parent::__construct($logger, $doctrine);
+ }
+
+ /**
+ * @return array
+ */
+ protected function getOptions(?ProcessState $state = null): array
+ {
+ return $this->testOptions;
+ }
+
+ protected function getConnection(?ProcessState $state = null): Connection
+ {
+ return $this->testConnection;
+ }
+ };
+ $logger->expects($this->once())->method('log')->with(LogLevel::WARNING, 'Empty resultset for query', ['options' => $options]);
+ $state->expects($this->once())->method('setSkipped')->with(true);
+
+ $task->initialize($state);
+ $task->execute($state);
+ }
+
+ public function testExecuteWithInputAsParams(): void
+ {
+ $state = $this->createMock(ProcessState::class);
+ $state->method('getInput')->willReturn(['id' => 1]);
+
+ $options = [
+ 'table' => 'my_table',
+ 'sql' => 'SELECT * FROM my_table WHERE id = :id',
+ 'limit' => null,
+ 'offset' => null,
+ 'paginate' => null,
+ 'input_as_params' => true,
+ 'params' => [],
+ 'types' => [],
+ 'empty_log_level' => LogLevel::WARNING,
+ 'connection' => null,
+ ];
+
+ $resultData = ['id' => 1, 'name' => 'test'];
+ $result = $this->createStub(Result::class);
+ $result->method('fetchAssociative')->willReturnOnConsecutiveCalls($resultData, false);
+
+ $connection = $this->createMock(Connection::class);
+ $connection->expects($this->once())->method('executeQuery')->with($options['sql'], ['id' => 1], $options['types'])->willReturn($result);
+
+ $task = new class($this->logger, $this->doctrine, $options, $connection) extends DatabaseReaderTask {
+ /**
+ * @param array $testOptions
+ */
+ public function __construct(LoggerInterface $logger, ManagerRegistry $doctrine, private readonly array $testOptions, private readonly Connection $testConnection)
+ {
+ parent::__construct($logger, $doctrine);
+ }
+
+ /**
+ * @return array
+ */
+ protected function getOptions(?ProcessState $state = null): array
+ {
+ return $this->testOptions;
+ }
+
+ protected function getConnection(?ProcessState $state = null): Connection
+ {
+ return $this->testConnection;
+ }
+ };
+ $state->expects($this->once())->method('setOutput')->with($resultData);
+
+ $task->initialize($state);
+ $task->execute($state);
+ }
+
+ public function testNext(): void
+ {
+ $state = $this->createMock(ProcessState::class);
+ $options = [
+ 'table' => 'my_table',
+ 'sql' => 'SELECT * FROM my_table',
+ 'limit' => null,
+ 'offset' => null,
+ 'paginate' => null,
+ 'input_as_params' => false,
+ 'params' => [],
+ 'types' => [],
+ 'empty_log_level' => LogLevel::WARNING,
+ 'connection' => null,
+ ];
+
+ $resultData1 = ['id' => 1, 'name' => 'test1'];
+ $resultData2 = ['id' => 2, 'name' => 'test2'];
+ $result = $this->createStub(Result::class);
+ $result->method('fetchAssociative')->willReturnOnConsecutiveCalls($resultData1, $resultData2, false, false);
+
+ $connection = $this->createStub(Connection::class);
+ $connection->method('executeQuery')->willReturn($result);
+
+ $task = new class($this->logger, $this->doctrine, $options, $connection) extends DatabaseReaderTask {
+ /**
+ * @param array $testOptions
+ */
+ public function __construct(LoggerInterface $logger, ManagerRegistry $doctrine, private readonly array $testOptions, private readonly Connection $testConnection)
+ {
+ parent::__construct($logger, $doctrine);
+ }
+
+ /**
+ * @return array
+ */
+ protected function getOptions(?ProcessState $state = null): array
+ {
+ return $this->testOptions;
+ }
+
+ protected function getConnection(?ProcessState $state = null): Connection
+ {
+ return $this->testConnection;
+ }
+ };
+
+ $task->initialize($state);
+
+ $state->expects($this->once())->method('setOutput')->with($resultData1);
+ $task->execute($state);
+
+ // The statement is now initialized, we can test next()
+ $this->assertTrue($task->next($state));
+ $this->assertFalse($task->next($state));
+ }
+
+ public function testInitializeStatementWithSql(): void
+ {
+ $state = $this->createStub(ProcessState::class);
+ $options = [
+ 'table' => 'my_table',
+ 'sql' => 'SELECT * FROM my_table',
+ 'limit' => null,
+ 'offset' => null,
+ 'paginate' => null,
+ 'input_as_params' => false,
+ 'params' => [],
+ 'types' => [],
+ 'empty_log_level' => LogLevel::WARNING,
+ 'connection' => null,
+ ];
+
+ $connection = $this->createMock(Connection::class);
+ $connection->expects($this->once())->method('executeQuery')->with($options['sql'], [], [])->willReturn($this->createStub(Result::class));
+
+ $task = new class($this->logger, $this->doctrine, $options, $connection) extends DatabaseReaderTask {
+ /**
+ * @param array $testOptions
+ */
+ public function __construct(LoggerInterface $logger, ManagerRegistry $doctrine, private readonly array $testOptions, private readonly Connection $testConnection)
+ {
+ parent::__construct($logger, $doctrine);
+ }
+
+ /**
+ * @return array
+ */
+ protected function getOptions(?ProcessState $state = null): array
+ {
+ return $this->testOptions;
+ }
+
+ protected function getConnection(?ProcessState $state = null): Connection
+ {
+ return $this->testConnection;
+ }
+ };
+
+ $this->callInitializeStatement($task, $state);
+ }
+
+ public function testInitializeStatementWithoutSql(): void
+ {
+ $state = $this->createStub(ProcessState::class);
+ $options = [
+ 'table' => 'my_table',
+ 'sql' => null,
+ 'limit' => 10,
+ 'offset' => 5,
+ 'paginate' => null,
+ 'input_as_params' => false,
+ 'params' => [],
+ 'types' => [],
+ 'empty_log_level' => LogLevel::WARNING,
+ 'connection' => null,
+ ];
+
+ $qb = $this->createMock(QueryBuilder::class);
+ $qb->expects($this->once())->method('select')->with('tbl.*')->willReturnSelf();
+ $qb->expects($this->once())->method('from')->with('my_table', 'tbl')->willReturnSelf();
+ $qb->expects($this->once())->method('setMaxResults')->with(10)->willReturnSelf();
+ $qb->expects($this->once())->method('setFirstResult')->with(5)->willReturnSelf();
+ $qb->expects($this->once())->method('getSQL')->willReturn('SELECT tbl.* FROM my_table LIMIT 10 OFFSET 5');
+
+ $connection = $this->createMock(Connection::class);
+ $connection->method('createQueryBuilder')->willReturn($qb);
+ $connection->expects($this->once())->method('executeQuery')->with('SELECT tbl.* FROM my_table LIMIT 10 OFFSET 5', [], [])->willReturn($this->createStub(Result::class));
+
+ $task = new class($this->logger, $this->doctrine, $options, $connection) extends DatabaseReaderTask {
+ /**
+ * @param array $testOptions
+ */
+ public function __construct(LoggerInterface $logger, ManagerRegistry $doctrine, private readonly array $testOptions, private readonly Connection $testConnection)
+ {
+ parent::__construct($logger, $doctrine);
+ }
+
+ /**
+ * @return array
+ */
+ protected function getOptions(?ProcessState $state = null): array
+ {
+ return $this->testOptions;
+ }
+
+ protected function getConnection(?ProcessState $state = null): Connection
+ {
+ return $this->testConnection;
+ }
+ };
+
+ $this->callInitializeStatement($task, $state);
+ }
+
+ public function testInitializeStatementWithInvalidParams(): void
+ {
+ $this->expectException(\UnexpectedValueException::class);
+
+ $state = $this->createStub(ProcessState::class);
+ $state->method('getInput')->willReturn('not an array');
+
+ $options = [
+ 'table' => 'my_table',
+ 'sql' => 'SELECT * FROM my_table',
+ 'limit' => null,
+ 'offset' => null,
+ 'paginate' => null,
+ 'input_as_params' => true,
+ 'params' => [],
+ 'types' => [],
+ 'empty_log_level' => LogLevel::WARNING,
+ 'connection' => null,
+ ];
+
+ $connection = $this->createStub(Connection::class);
+
+ $task = new class($this->logger, $this->doctrine, $options, $connection) extends DatabaseReaderTask {
+ /**
+ * @param array $testOptions
+ */
+ public function __construct(LoggerInterface $logger, ManagerRegistry $doctrine, private readonly array $testOptions, private readonly Connection $testConnection)
+ {
+ parent::__construct($logger, $doctrine);
+ }
+
+ /**
+ * @return array
+ */
+ protected function getOptions(?ProcessState $state = null): array
+ {
+ return $this->testOptions;
+ }
+
+ protected function getConnection(?ProcessState $state = null): Connection
+ {
+ return $this->testConnection;
+ }
+ };
+
+ $this->callInitializeStatement($task, $state);
+ }
+
+ private function callInitializeStatement(DatabaseReaderTask $task, ProcessState $state): Result
+ {
+ $reflection = new \ReflectionClass(DatabaseReaderTask::class);
+ $method = $reflection->getMethod('initializeStatement');
+
+ /** @var Result $result */
+ $result = $method->invoke($task, $state);
+
+ return $result;
+ }
+}
diff --git a/tests/Task/Database/DatabaseUpdaterTaskTest.php b/tests/Task/Database/DatabaseUpdaterTaskTest.php
new file mode 100644
index 0000000..748d3d6
--- /dev/null
+++ b/tests/Task/Database/DatabaseUpdaterTaskTest.php
@@ -0,0 +1,168 @@
+createStub(ManagerRegistry::class);
+ $logger = $this->createStub(LoggerInterface::class);
+ $state = $this->createMock(ProcessState::class);
+ $options = [
+ 'sql' => 'UPDATE my_table SET name = :name WHERE id = :id',
+ 'input_as_params' => false,
+ 'params' => ['id' => 1, 'name' => 'test'],
+ 'types' => [],
+ 'connection' => null,
+ ];
+
+ $connection = $this->createMock(Connection::class);
+ $connection->expects($this->once())->method('executeStatement')
+ ->with($options['sql'], $options['params'], $options['types'])
+ ->willReturn(1);
+
+ $task = new class($doctrine, $logger, $options, $connection) extends DatabaseUpdaterTask {
+ /**
+ * @param array $testOptions
+ */
+ public function __construct(ManagerRegistry $doctrine, LoggerInterface $logger, private readonly array $testOptions, private readonly Connection $testConnection)
+ {
+ parent::__construct($doctrine, $logger);
+ }
+
+ /**
+ * @return array
+ */
+ protected function getOptions(?ProcessState $state = null): array
+ {
+ return $this->testOptions;
+ }
+
+ protected function getConnection(?ProcessState $state = null): Connection
+ {
+ return $this->testConnection;
+ }
+ };
+
+ $state->expects($this->once())->method('setOutput')->with(1);
+
+ $task->initialize($state);
+ $task->execute($state);
+ }
+
+ public function testExecuteWithInputAsParams(): void
+ {
+ $doctrine = $this->createStub(ManagerRegistry::class);
+ $logger = $this->createStub(LoggerInterface::class);
+ $state = $this->createMock(ProcessState::class);
+ $state->method('getInput')->willReturn(['id' => 1, 'name' => 'test']);
+
+ $options = [
+ 'sql' => 'UPDATE my_table SET name = :name WHERE id = :id',
+ 'input_as_params' => true,
+ 'params' => [],
+ 'types' => [],
+ 'connection' => null,
+ ];
+
+ $connection = $this->createMock(Connection::class);
+ $connection->expects($this->once())->method('executeStatement')
+ ->with($options['sql'], ['id' => 1, 'name' => 'test'], $options['types'])
+ ->willReturn(1);
+
+ $task = new class($doctrine, $logger, $options, $connection) extends DatabaseUpdaterTask {
+ /**
+ * @param array $testOptions
+ */
+ public function __construct(ManagerRegistry $doctrine, LoggerInterface $logger, private readonly array $testOptions, private readonly Connection $testConnection)
+ {
+ parent::__construct($doctrine, $logger);
+ }
+
+ /**
+ * @return array
+ */
+ protected function getOptions(?ProcessState $state = null): array
+ {
+ return $this->testOptions;
+ }
+
+ protected function getConnection(?ProcessState $state = null): Connection
+ {
+ return $this->testConnection;
+ }
+ };
+
+ $state->expects($this->once())->method('setOutput')->with(1);
+
+ $task->initialize($state);
+ $task->execute($state);
+ }
+
+ public function testExecuteWithInvalidParams(): void
+ {
+ $this->expectException(\UnexpectedValueException::class);
+
+ $doctrine = $this->createStub(ManagerRegistry::class);
+ $logger = $this->createStub(LoggerInterface::class);
+ $state = $this->createStub(ProcessState::class);
+ $state->method('getInput')->willReturn('not an array');
+
+ $options = [
+ 'sql' => 'UPDATE my_table SET name = :name WHERE id = :id',
+ 'input_as_params' => true,
+ 'params' => [],
+ 'types' => [],
+ 'connection' => null,
+ ];
+
+ $connection = $this->createStub(Connection::class);
+
+ $task = new class($doctrine, $logger, $options, $connection) extends DatabaseUpdaterTask {
+ /**
+ * @param array $testOptions
+ */
+ public function __construct(ManagerRegistry $doctrine, LoggerInterface $logger, private readonly array $testOptions, private readonly Connection $testConnection)
+ {
+ parent::__construct($doctrine, $logger);
+ }
+
+ /**
+ * @return array
+ */
+ protected function getOptions(?ProcessState $state = null): array
+ {
+ return $this->testOptions;
+ }
+
+ protected function getConnection(?ProcessState $state = null): Connection
+ {
+ return $this->testConnection;
+ }
+ };
+
+ $task->initialize($state);
+ $task->execute($state);
+ }
+}
diff --git a/tests/Task/EntityManager/AbstractDoctrineQueryTaskTest.php b/tests/Task/EntityManager/AbstractDoctrineQueryTaskTest.php
new file mode 100644
index 0000000..d3d3e3a
--- /dev/null
+++ b/tests/Task/EntityManager/AbstractDoctrineQueryTaskTest.php
@@ -0,0 +1,39 @@
+expectException(\UnexpectedValueException::class);
+
+ $task = $this->createStub(AbstractDoctrineQueryTask::class);
+ $repository = $this->createStub(EntityRepository::class);
+ $repository->method('createQueryBuilder')->willReturn(new QueryBuilder($this->createStub(EntityManagerInterface::class)));
+
+ $reflection = new \ReflectionClass(AbstractDoctrineQueryTask::class);
+ $method = $reflection->getMethod('getQueryBuilder');
+
+ $method->invoke($task, $repository, ['e.field; DROP TABLE dummy;' => 'value'], []);
+ }
+}
diff --git a/tests/Task/EntityManager/ClearEntityManagerTaskTest.php b/tests/Task/EntityManager/ClearEntityManagerTaskTest.php
new file mode 100644
index 0000000..b343ca2
--- /dev/null
+++ b/tests/Task/EntityManager/ClearEntityManagerTaskTest.php
@@ -0,0 +1,52 @@
+createMock(EntityManagerInterface::class);
+ $entityManager->expects($this->once())
+ ->method('clear');
+
+ $managerRegistry = $this->createMock(ManagerRegistry::class);
+ $managerRegistry->expects($this->once())
+ ->method('getManager')
+ ->willReturn($entityManager);
+
+ $task = $this->getMockBuilder(ClearEntityManagerTask::class)
+ ->setConstructorArgs([$managerRegistry])
+ ->onlyMethods(['getOption']) // Mock only the getOption method
+ ->getMock();
+
+ // Ensure getOption returns null for 'entity_manager', mimicking default behavior
+ $task->expects($this->once())
+ ->method('getOption')
+ ->with(self::anything(), 'entity_manager')
+ ->willReturn(null);
+
+ $processState = $this->createStub(ProcessState::class);
+
+ $task->execute($processState);
+ }
+}
diff --git a/tests/Task/EntityManager/DoctrineBatchWriterTaskTest.php b/tests/Task/EntityManager/DoctrineBatchWriterTaskTest.php
new file mode 100644
index 0000000..7a13d4a
--- /dev/null
+++ b/tests/Task/EntityManager/DoctrineBatchWriterTaskTest.php
@@ -0,0 +1,210 @@
+ $options
+ */
+ protected function getTask(array $options = [], ?ManagerRegistry $managerRegistry = null): DoctrineBatchWriterTask
+ {
+ if (!$managerRegistry instanceof ManagerRegistry) {
+ $managerRegistry = $this->createStub(ManagerRegistry::class);
+ }
+ $task = new DoctrineBatchWriterTask($managerRegistry);
+
+ $state = $this->createStub(ProcessState::class);
+ $state->method('getContextualizedOptions')->willReturn($options);
+ $task->initialize($state);
+
+ return $task;
+ }
+
+ public function testExecuteAddsEntityToBatchAndSkipsWhenBatchCountNotReached(): void
+ {
+ $entity1 = new \stdClass();
+ $state = $this->createMock(ProcessState::class);
+ $state->method('getInput')->willReturn($entity1);
+
+ $task = $this->getTask(['batch_count' => 2]);
+
+ $state->expects($this->once())->method('setSkipped')->with(true);
+ $task->execute($state);
+
+ $reflection = new \ReflectionClass(DoctrineBatchWriterTask::class);
+ $batchProperty = $reflection->getProperty('batch');
+ $this->assertCount(1, (array) $batchProperty->getValue($task));
+ $this->assertContains($entity1, (array) $batchProperty->getValue($task));
+ }
+
+ public function testExecuteFlushesBatchWhenBatchCountReached(): void
+ {
+ $entity1 = new \stdClass();
+ $entity2 = new \stdClass();
+
+ $state = $this->createMock(ProcessState::class);
+ $state->method('getInput')->willReturnOnConsecutiveCalls($entity1, $entity2);
+ $state->expects($this->once())->method('setOutput')->with([$entity1, $entity2]);
+
+ $entityManager = $this->createMock(EntityManagerInterface::class);
+ $entityManager->expects($this->exactly(2))->method('persist'); // Called for entity1 and entity2
+ $entityManager->expects($this->once())->method('flush');
+
+ $managerRegistry = $this->createStub(ManagerRegistry::class);
+ $managerRegistry->method('getManagerForClass')->willReturn($entityManager);
+
+ $task = $this->getTask(['batch_count' => 2], $managerRegistry);
+
+ // Simulate filling the batch
+ $reflection = new \ReflectionClass(DoctrineBatchWriterTask::class);
+ $batchProperty = $reflection->getProperty('batch');
+ // Add first entity
+ $task->execute($state); // batch has 1 entity, not flushed
+ $this->assertCount(1, (array) $batchProperty->getValue($task));
+ $this->assertContains($entity1, (array) $batchProperty->getValue($task));
+
+ // Add second entity, should trigger flush
+ $task->execute($state); // batch has 2 entities, should be flushed
+ $this->assertCount(0, (array) $batchProperty->getValue($task)); // Batch should be empty after flush
+ }
+
+ public function testFlushCallsWriteBatch(): void
+ {
+ $state = $this->createStub(ProcessState::class);
+ $task = $this->getMockBuilder(DoctrineBatchWriterTask::class)
+ ->disableOriginalConstructor()
+ ->onlyMethods(['writeBatch'])
+ ->getMock();
+
+ $task->expects($this->once())->method('writeBatch')->with($state);
+ $task->flush($state);
+ }
+
+ public function testWriteBatchWithEmptyBatchSkipsState(): void
+ {
+ $state = $this->createMock(ProcessState::class);
+ $state->expects($this->once())->method('setSkipped')->with(true);
+
+ $managerRegistry = $this->createStub(ManagerRegistry::class);
+ $task = new DoctrineBatchWriterTask($managerRegistry);
+
+ // Ensure batch is empty
+ $reflection = new \ReflectionClass(DoctrineBatchWriterTask::class);
+ $batchProperty = $reflection->getProperty('batch');
+ $batchProperty->setValue($task, []);
+
+ // Call writeBatch directly
+ $writeBatchMethod = $reflection->getMethod('writeBatch');
+ $writeBatchMethod->invoke($task, $state);
+ }
+
+ public function testWriteBatchPersistsAndFlushesEntities(): void
+ {
+ $entity1 = new \stdClass();
+ $entity2 = new \stdClass();
+ $state = $this->createMock(ProcessState::class); // Use mock to set expectation on setSkipped
+ $state->expects($this->never())->method('setSkipped'); // Should not be skipped if batch is not empty
+
+ $entityManager = $this->createMock(EntityManagerInterface::class);
+ $entityManager->expects($this->exactly(2))->method('persist'); // Called for entity1 and entity2
+ $entityManager->expects($this->once())->method('flush');
+
+ $managerRegistry = $this->createStub(ManagerRegistry::class);
+ $managerRegistry->method('getManagerForClass')->willReturn($entityManager);
+
+ $task = new DoctrineBatchWriterTask($managerRegistry);
+
+ // Manually set batch
+ $reflection = new \ReflectionClass(DoctrineBatchWriterTask::class);
+ $batchProperty = $reflection->getProperty('batch');
+ $batchProperty->setValue($task, [$entity1, $entity2]);
+
+ // Call writeBatch directly
+ $writeBatchMethod = $reflection->getMethod('writeBatch');
+ $writeBatchMethod->invoke($task, $state);
+ }
+
+ public function testWriteBatchClearsBatchAfterFlushing(): void
+ {
+ $entity = new \stdClass();
+ $state = $this->createStub(ProcessState::class);
+
+ $entityManager = $this->createStub(EntityManagerInterface::class);
+ $managerRegistry = $this->createStub(ManagerRegistry::class);
+ $managerRegistry->method('getManagerForClass')->willReturn($entityManager);
+
+ $task = new DoctrineBatchWriterTask($managerRegistry);
+
+ $reflection = new \ReflectionClass(DoctrineBatchWriterTask::class);
+ $batchProperty = $reflection->getProperty('batch');
+ $batchProperty->setValue($task, [$entity]); // Add an entity to the batch
+
+ // Call writeBatch directly
+ $writeBatchMethod = $reflection->getMethod('writeBatch');
+ $writeBatchMethod->invoke($task, $state);
+
+ $this->assertCount(0, (array) $batchProperty->getValue($task)); // Batch should be empty
+ }
+
+ public function testWriteBatchSetsOutput(): void
+ {
+ $entity = new \stdClass();
+ $state = $this->createMock(ProcessState::class); // Use mock to set expectation on setOutput
+ $state->expects($this->once())->method('setOutput')->with([$entity]);
+
+ $entityManager = $this->createStub(EntityManagerInterface::class);
+ $managerRegistry = $this->createStub(ManagerRegistry::class);
+ $managerRegistry->method('getManagerForClass')->willReturn($entityManager);
+
+ $task = new DoctrineBatchWriterTask($managerRegistry);
+
+ $reflection = new \ReflectionClass(DoctrineBatchWriterTask::class);
+ $batchProperty = $reflection->getProperty('batch');
+ $batchProperty->setValue($task, [$entity]); // Add an entity to the batch
+
+ // Call writeBatch directly
+ $writeBatchMethod = $reflection->getMethod('writeBatch');
+ $writeBatchMethod->invoke($task, $state);
+ }
+
+ public function testWriteBatchThrowsExceptionWhenNoManagerFound(): void
+ {
+ $this->expectException(\UnexpectedValueException::class);
+
+ $entity = new \stdClass();
+ $state = $this->createStub(ProcessState::class);
+
+ $managerRegistry = $this->createStub(ManagerRegistry::class);
+ $managerRegistry->method('getManagerForClass')->willReturn(null); // Simulate no manager found
+
+ $task = new DoctrineBatchWriterTask($managerRegistry);
+
+ $reflection = new \ReflectionClass(DoctrineBatchWriterTask::class);
+ $batchProperty = $reflection->getProperty('batch');
+ $batchProperty->setValue($task, [$entity]); // Add an entity to the batch
+
+ // Call writeBatch directly
+ $writeBatchMethod = $reflection->getMethod('writeBatch');
+ $writeBatchMethod->invoke($task, $state);
+ }
+}
diff --git a/tests/Task/EntityManager/DoctrineCleanerTaskTest.php b/tests/Task/EntityManager/DoctrineCleanerTaskTest.php
new file mode 100644
index 0000000..09d3cec
--- /dev/null
+++ b/tests/Task/EntityManager/DoctrineCleanerTaskTest.php
@@ -0,0 +1,69 @@
+createStub(ProcessState::class);
+ $state->method('getInput')->willReturn($entity);
+
+ $entityManager = $this->createMock(EntityManagerInterface::class);
+ $entityManager->expects($this->once())->method('clear');
+
+ $managerRegistry = $this->createStub(ManagerRegistry::class);
+ $managerRegistry->method('getManagerForClass')->willReturn($entityManager);
+
+ $task = new DoctrineCleanerTask($managerRegistry);
+ $task->execute($state);
+ }
+
+ public function testExecuteWithNullInput(): void
+ {
+ $this->expectException(\RuntimeException::class);
+
+ $state = $this->createStub(ProcessState::class);
+ $state->method('getInput')->willReturn(null);
+
+ $managerRegistry = $this->createStub(ManagerRegistry::class);
+
+ $task = new DoctrineCleanerTask($managerRegistry);
+ $task->execute($state);
+ }
+
+ public function testExecuteWithNoManager(): void
+ {
+ $this->expectException(\UnexpectedValueException::class);
+
+ $entity = new \stdClass();
+ $state = $this->createStub(ProcessState::class);
+ $state->method('getInput')->willReturn($entity);
+
+ $managerRegistry = $this->createStub(ManagerRegistry::class);
+ $managerRegistry->method('getManagerForClass')->willReturn(null);
+
+ $task = new DoctrineCleanerTask($managerRegistry);
+ $task->execute($state);
+ }
+}
diff --git a/tests/Task/EntityManager/DoctrineDetacherTaskTest.php b/tests/Task/EntityManager/DoctrineDetacherTaskTest.php
new file mode 100644
index 0000000..0770d69
--- /dev/null
+++ b/tests/Task/EntityManager/DoctrineDetacherTaskTest.php
@@ -0,0 +1,84 @@
+ $options
+ */
+ protected function getTask(array $options = [], ?ManagerRegistry $managerRegistry = null): DoctrineDetacherTask
+ {
+ if (!$managerRegistry instanceof ManagerRegistry) {
+ $managerRegistry = $this->createStub(ManagerRegistry::class);
+ }
+ $task = new DoctrineDetacherTask($managerRegistry);
+
+ $state = $this->createStub(ProcessState::class);
+ $state->method('getContextualizedOptions')->willReturn($options);
+ $task->initialize($state);
+
+ return $task;
+ }
+
+ public function testExecuteDetachesEntity(): void
+ {
+ $entity = new \stdClass();
+ $state = $this->createStub(ProcessState::class);
+ $state->method('getInput')->willReturn($entity);
+
+ $entityManager = $this->createMock(EntityManagerInterface::class);
+ $entityManager->expects($this->once())->method('detach')->with($entity);
+
+ $managerRegistry = $this->createStub(ManagerRegistry::class);
+ $managerRegistry->method('getManagerForClass')->willReturn($entityManager);
+
+ $task = $this->getTask([], $managerRegistry);
+ $task->execute($state);
+ }
+
+ public function testExecuteThrowsExceptionOnNullInput(): void
+ {
+ $this->expectException(\RuntimeException::class);
+
+ $state = $this->createStub(ProcessState::class);
+ $state->method('getInput')->willReturn(null);
+
+ $task = $this->getTask();
+ $task->execute($state);
+ }
+
+ public function testExecuteThrowsExceptionWhenNoManagerFound(): void
+ {
+ $this->expectException(\UnexpectedValueException::class);
+
+ $entity = new \stdClass();
+ $state = $this->createStub(ProcessState::class);
+ $state->method('getInput')->willReturn($entity);
+
+ $managerRegistry = $this->createStub(ManagerRegistry::class);
+ $managerRegistry->method('getManagerForClass')->willReturn(null);
+
+ $task = $this->getTask([], $managerRegistry);
+ $task->execute($state);
+ }
+}
diff --git a/tests/Task/EntityManager/DoctrineReaderTaskTest.php b/tests/Task/EntityManager/DoctrineReaderTaskTest.php
new file mode 100644
index 0000000..267a323
--- /dev/null
+++ b/tests/Task/EntityManager/DoctrineReaderTaskTest.php
@@ -0,0 +1,260 @@
+createStub(LoggerInterface::class);
+ $doctrine = $this->createStub(ManagerRegistry::class);
+ $state = $this->createMock(ProcessState::class);
+ $options = [
+ 'class_name' => 'App\Entity\MyEntity',
+ 'criteria' => ['id' => 1],
+ 'order_by' => ['createdAt' => 'DESC'],
+ 'limit' => 10,
+ 'offset' => 0,
+ 'empty_log_level' => LogLevel::WARNING,
+ ];
+
+ $entity = new \stdClass();
+ $query = $this->createStub(Query::class);
+ $query->method('toIterable')->willReturn(new \ArrayIterator([$entity]));
+
+ $qb = $this->createStub(QueryBuilder::class);
+ $qb->method('getQuery')->willReturn($query);
+ $qb->method('select')->willReturn($qb);
+ $qb->method('from')->willReturn($qb);
+ $qb->method('andWhere')->willReturn($qb);
+ $qb->method('orderBy')->willReturn($qb);
+ $qb->method('setFirstResult')->willReturn($qb);
+ $qb->method('setMaxResults')->willReturn($qb);
+ $qb->method('setParameter')->willReturn($qb);
+
+ $repository = $this->createStub(EntityRepository::class);
+ $repository->method('createQueryBuilder')->willReturn($qb);
+
+ $em = $this->createStub(EntityManagerInterface::class);
+ $em->method('getRepository')->willReturn($repository);
+
+ $doctrine->method('getManagerForClass')->willReturn($em);
+
+ $task = new class($logger, $doctrine, $options) extends DoctrineReaderTask {
+ /**
+ * @param array $testOptions
+ */
+ public function __construct(LoggerInterface $logger, ManagerRegistry $doctrine, private readonly array $testOptions)
+ {
+ parent::__construct($logger, $doctrine);
+ }
+
+ /**
+ * @return array
+ */
+ protected function getOptions(?ProcessState $state = null): array
+ {
+ return $this->testOptions;
+ }
+ };
+
+ $state->expects($this->once())->method('setOutput')->with($entity);
+ $state->expects($this->never())->method('setSkipped');
+
+ $task->initialize($state);
+ $task->execute($state);
+ }
+
+ public function testExecuteEmpty(): void
+ {
+ $logger = $this->createMock(LoggerInterface::class);
+ $doctrine = $this->createStub(ManagerRegistry::class);
+ $state = $this->createMock(ProcessState::class);
+ $options = [
+ 'class_name' => 'App\Entity\MyEntity',
+ 'criteria' => ['id' => 1],
+ 'order_by' => [],
+ 'limit' => null,
+ 'offset' => null,
+ 'empty_log_level' => LogLevel::WARNING,
+ ];
+
+ $query = $this->createStub(Query::class);
+ $query->method('toIterable')->willReturn(new \ArrayIterator([]));
+
+ $qb = $this->createStub(QueryBuilder::class);
+ $qb->method('getQuery')->willReturn($query);
+ $qb->method('select')->willReturn($qb);
+ $qb->method('from')->willReturn($qb);
+ $qb->method('andWhere')->willReturn($qb);
+ $qb->method('setParameter')->willReturn($qb);
+
+ $repository = $this->createStub(EntityRepository::class);
+ $repository->method('createQueryBuilder')->willReturn($qb);
+
+ $em = $this->createStub(EntityManagerInterface::class);
+ $em->method('getRepository')->willReturn($repository);
+
+ $doctrine->method('getManagerForClass')->willReturn($em);
+
+ $task = new class($logger, $doctrine, $options) extends DoctrineReaderTask {
+ /**
+ * @param array $testOptions
+ */
+ public function __construct(LoggerInterface $logger, ManagerRegistry $doctrine, private readonly array $testOptions)
+ {
+ parent::__construct($logger, $doctrine);
+ }
+
+ /**
+ * @return array
+ */
+ protected function getOptions(?ProcessState $state = null): array
+ {
+ return $this->testOptions;
+ }
+ };
+
+ $logger->expects(self::once())->method('log')->with(LogLevel::WARNING, 'Empty resultset for query');
+ $state->expects($this->once())->method('setSkipped')->with(true);
+
+ $task->initialize($state);
+ $task->execute($state);
+ }
+
+ public function testNext(): void
+ {
+ $logger = $this->createStub(LoggerInterface::class);
+ $doctrine = $this->createStub(ManagerRegistry::class);
+ $state = $this->createMock(ProcessState::class);
+ $options = [
+ 'class_name' => 'App\Entity\MyEntity',
+ 'criteria' => [],
+ 'order_by' => [],
+ 'limit' => null,
+ 'offset' => null,
+ 'empty_log_level' => LogLevel::WARNING,
+ ];
+
+ $entity1 = new \stdClass();
+ $entity2 = new \stdClass();
+ $query = $this->createStub(Query::class);
+ $query->method('toIterable')->willReturn(new \ArrayIterator([$entity1, $entity2]));
+
+ $qb = $this->createStub(QueryBuilder::class);
+ $qb->method('getQuery')->willReturn($query);
+ $qb->method('select')->willReturn($qb);
+ $qb->method('from')->willReturn($qb);
+
+ $repository = $this->createStub(EntityRepository::class);
+ $repository->method('createQueryBuilder')->willReturn($qb);
+
+ $em = $this->createStub(EntityManagerInterface::class);
+ $em->method('getRepository')->willReturn($repository);
+
+ $doctrine->method('getManagerForClass')->willReturn($em);
+
+ $task = new class($logger, $doctrine, $options) extends DoctrineReaderTask {
+ /**
+ * @param array $testOptions
+ */
+ public function __construct(LoggerInterface $logger, ManagerRegistry $doctrine, private readonly array $testOptions)
+ {
+ parent::__construct($logger, $doctrine);
+ }
+
+ /**
+ * @return array
+ */
+ protected function getOptions(?ProcessState $state = null): array
+ {
+ return $this->testOptions;
+ }
+ };
+
+ $task->initialize($state);
+
+ $call = 0;
+ $state->expects($this->exactly(2))->method('setOutput')
+ ->with($this->callback(function ($output) use (&$call, $entity1, $entity2) {
+ if (0 === $call) {
+ $this->assertSame($entity1, $output);
+ }
+ if (1 === $call) {
+ $this->assertSame($entity2, $output);
+ }
+ ++$call;
+
+ return true;
+ }));
+
+ $task->execute($state);
+ $this->assertTrue($task->next($state));
+ $task->execute($state);
+ $this->assertFalse($task->next($state));
+ }
+
+ public function testExecuteThrowsExceptionWhenNoManagerFound(): void
+ {
+ $this->expectException(\UnexpectedValueException::class);
+
+ $logger = $this->createStub(LoggerInterface::class);
+ $doctrine = $this->createStub(ManagerRegistry::class);
+ $state = $this->createStub(ProcessState::class);
+ $options = [
+ 'class_name' => 'App\Entity\MyEntity',
+ 'criteria' => [],
+ 'order_by' => [],
+ 'limit' => null,
+ 'offset' => null,
+ 'empty_log_level' => LogLevel::WARNING,
+ ];
+
+ $doctrine->method('getManagerForClass')->willReturn(null);
+
+ $task = new class($logger, $doctrine, $options) extends DoctrineReaderTask {
+ /**
+ * @param array $testOptions
+ */
+ public function __construct(LoggerInterface $logger, ManagerRegistry $doctrine, private readonly array $testOptions)
+ {
+ parent::__construct($logger, $doctrine);
+ }
+
+ /**
+ * @return array
+ */
+ protected function getOptions(?ProcessState $state = null): array
+ {
+ return $this->testOptions;
+ }
+ };
+
+ $task->initialize($state);
+ $task->execute($state);
+ }
+}
diff --git a/tests/Task/EntityManager/DoctrineRefresherTaskTest.php b/tests/Task/EntityManager/DoctrineRefresherTaskTest.php
new file mode 100644
index 0000000..3d68093
--- /dev/null
+++ b/tests/Task/EntityManager/DoctrineRefresherTaskTest.php
@@ -0,0 +1,85 @@
+ $options
+ */
+ protected function getTask(array $options = [], ?ManagerRegistry $managerRegistry = null): DoctrineRefresherTask
+ {
+ if (!$managerRegistry instanceof ManagerRegistry) {
+ $managerRegistry = $this->createStub(ManagerRegistry::class);
+ }
+ $task = new DoctrineRefresherTask($managerRegistry);
+
+ $state = $this->createStub(ProcessState::class);
+ $state->method('getContextualizedOptions')->willReturn($options);
+ $task->initialize($state);
+
+ return $task;
+ }
+
+ public function testExecuteRefreshesEntity(): void
+ {
+ $entity = new \stdClass();
+ $state = $this->createMock(ProcessState::class);
+ $state->method('getInput')->willReturn($entity);
+ $state->expects($this->once())->method('setOutput')->with($entity);
+
+ $entityManager = $this->createMock(EntityManagerInterface::class);
+ $entityManager->expects($this->once())->method('refresh')->with($entity);
+
+ $managerRegistry = $this->createStub(ManagerRegistry::class);
+ $managerRegistry->method('getManagerForClass')->willReturn($entityManager);
+
+ $task = $this->getTask([], $managerRegistry);
+ $task->execute($state);
+ }
+
+ public function testExecuteThrowsExceptionOnNullInput(): void
+ {
+ $this->expectException(\RuntimeException::class);
+
+ $state = $this->createStub(ProcessState::class);
+ $state->method('getInput')->willReturn(null);
+
+ $task = $this->getTask();
+ $task->execute($state);
+ }
+
+ public function testExecuteThrowsExceptionWhenNoManagerFound(): void
+ {
+ $this->expectException(\UnexpectedValueException::class);
+
+ $entity = new \stdClass();
+ $state = $this->createStub(ProcessState::class);
+ $state->method('getInput')->willReturn($entity);
+
+ $managerRegistry = $this->createStub(ManagerRegistry::class);
+ $managerRegistry->method('getManagerForClass')->willReturn(null);
+
+ $task = $this->getTask([], $managerRegistry);
+ $task->execute($state);
+ }
+}
diff --git a/tests/Task/EntityManager/DoctrineRemoverTaskTest.php b/tests/Task/EntityManager/DoctrineRemoverTaskTest.php
new file mode 100644
index 0000000..7d689b8
--- /dev/null
+++ b/tests/Task/EntityManager/DoctrineRemoverTaskTest.php
@@ -0,0 +1,70 @@
+createStub(ProcessState::class);
+ $state->method('getInput')->willReturn($entity);
+
+ $entityManager = $this->createMock(EntityManagerInterface::class);
+ $entityManager->expects($this->once())->method('remove')->with($entity);
+ $entityManager->expects($this->once())->method('flush');
+
+ $managerRegistry = $this->createStub(ManagerRegistry::class);
+ $managerRegistry->method('getManagerForClass')->willReturn($entityManager);
+
+ $task = new DoctrineRemoverTask($managerRegistry);
+ $task->execute($state);
+ }
+
+ public function testExecuteWithNullInput(): void
+ {
+ $this->expectException(\TypeError::class); // ClassUtils::getClass expects an object, null will cause a TypeError
+
+ $state = $this->createStub(ProcessState::class);
+ $state->method('getInput')->willReturn(null);
+
+ $managerRegistry = $this->createStub(ManagerRegistry::class);
+
+ $task = new DoctrineRemoverTask($managerRegistry);
+ $task->execute($state);
+ }
+
+ public function testExecuteWithNoManager(): void
+ {
+ $this->expectException(\UnexpectedValueException::class);
+
+ $entity = new \stdClass();
+ $state = $this->createStub(ProcessState::class);
+ $state->method('getInput')->willReturn($entity);
+
+ $managerRegistry = $this->createStub(ManagerRegistry::class);
+ $managerRegistry->method('getManagerForClass')->willReturn(null);
+
+ $task = new DoctrineRemoverTask($managerRegistry);
+ $task->execute($state);
+ }
+}
diff --git a/tests/Task/EntityManager/DoctrineWriterTaskTest.php b/tests/Task/EntityManager/DoctrineWriterTaskTest.php
new file mode 100644
index 0000000..aa8ea13
--- /dev/null
+++ b/tests/Task/EntityManager/DoctrineWriterTaskTest.php
@@ -0,0 +1,86 @@
+ $options
+ */
+ protected function getTask(array $options = [], ?ManagerRegistry $managerRegistry = null): DoctrineWriterTask
+ {
+ if (!$managerRegistry instanceof ManagerRegistry) {
+ $managerRegistry = $this->createStub(ManagerRegistry::class);
+ }
+ $task = new DoctrineWriterTask($managerRegistry);
+
+ $state = $this->createStub(ProcessState::class);
+ $state->method('getContextualizedOptions')->willReturn($options);
+ $task->initialize($state);
+
+ return $task;
+ }
+
+ public function testExecuteWritesAndFlushesEntity(): void
+ {
+ $entity = new \stdClass();
+ $state = $this->createMock(ProcessState::class);
+ $state->method('getInput')->willReturn($entity);
+ $state->expects($this->once())->method('setOutput')->with($entity);
+
+ $entityManager = $this->createMock(EntityManagerInterface::class);
+ $entityManager->expects($this->once())->method('persist')->with($entity);
+ $entityManager->expects($this->once())->method('flush');
+
+ $managerRegistry = $this->createStub(ManagerRegistry::class);
+ $managerRegistry->method('getManagerForClass')->willReturn($entityManager);
+
+ $task = $this->getTask([], $managerRegistry);
+ $task->execute($state);
+ }
+
+ public function testExecuteThrowsExceptionOnNullInput(): void
+ {
+ $this->expectException(\RuntimeException::class);
+
+ $state = $this->createStub(ProcessState::class);
+ $state->method('getInput')->willReturn(null);
+
+ $task = $this->getTask();
+ $task->execute($state);
+ }
+
+ public function testExecuteThrowsExceptionWhenNoManagerFound(): void
+ {
+ $this->expectException(\UnexpectedValueException::class);
+
+ $entity = new \stdClass();
+ $state = $this->createStub(ProcessState::class);
+ $state->method('getInput')->willReturn($entity);
+
+ $managerRegistry = $this->createStub(ManagerRegistry::class);
+ $managerRegistry->method('getManagerForClass')->willReturn(null);
+
+ $task = $this->getTask([], $managerRegistry);
+ $task->execute($state);
+ }
+}