diff --git a/src/batch/docs/domain/item-job/item-writer.md b/src/batch/docs/domain/item-job/item-writer.md index 7b32c3af..e2e7e955 100644 --- a/src/batch/docs/domain/item-job/item-writer.md +++ b/src/batch/docs/domain/item-job/item-writer.md @@ -17,6 +17,8 @@ It can be any class implementing [ItemWriterInterface](../../../src/Job/Item/Ite route writing to different writer based on your logic. - [SummaryWriter](../../../src/Job/Item/Writer/SummaryWriter.php): write items to a job summary value. +- [TransformingWriter](../../../src/Job/Item/Writer/TransformingWriter.php): + perform items transformation before delegating to another writer. **Item writers from bridges:** - [DoctrineDBALInsertWriter (`doctrine/dbal`)](https://github.com/yokai-php/batch-doctrine-dbal/blob/0.x/src/DoctrineDBALInsertWriter.php): diff --git a/src/batch/src/Job/Item/Writer/TransformingWriter.php b/src/batch/src/Job/Item/Writer/TransformingWriter.php new file mode 100644 index 00000000..22fc5427 --- /dev/null +++ b/src/batch/src/Job/Item/Writer/TransformingWriter.php @@ -0,0 +1,81 @@ + $item) { + if (!\is_string($index) && !\is_int($index)) { + throw UnexpectedValueException::type('string|int', $index); + } + + try { + $transformedItems[] = $this->processor->process($item); + } catch (SkipItemException $exception) { + $this->jobExecution->getLogger()->debug( + \sprintf('Skipping item in writer transformation %s.', $index), + $exception->getContext() + ['item' => $exception->getItem()] + ); + + $cause = $exception->getCause(); + if ($cause) { + $cause->report($this->jobExecution, $index, $exception->getItem()); + } + + continue; + } + } + + if (count($transformedItems) > 0) { + $this->writer->write($transformedItems); + } + } + + public function initialize(): void + { + $this->configureElementJobContext($this->processor, $this->jobExecution); + $this->initializeElement($this->processor); + $this->configureElementJobContext($this->writer, $this->jobExecution); + $this->initializeElement($this->writer); + } + + public function flush(): void + { + $this->flushElement($this->processor); + $this->flushElement($this->writer); + } +} diff --git a/src/batch/tests/Job/Item/Writer/TransformingWriterTest.php b/src/batch/tests/Job/Item/Writer/TransformingWriterTest.php new file mode 100644 index 00000000..0f7e8aac --- /dev/null +++ b/src/batch/tests/Job/Item/Writer/TransformingWriterTest.php @@ -0,0 +1,84 @@ + \strtoupper($string))), + $debugWriter = new TestDebugWriter($innerWriter = new InMemoryWriter()) + ); + + $writer->setJobExecution(JobExecution::createRoot('123', 'test.transforming_writer')); + $writer->initialize(); + $writer->write(['one', 'two', 'three']); + $writer->flush(); + + $debugProcessor->assertWasConfigured(); + $debugProcessor->assertWasUsed(); + $debugWriter->assertWasConfigured(); + $debugWriter->assertWasUsed(); + self::assertSame(['ONE', 'TWO', 'THREE'], $innerWriter->getItems()); + } + + public function testSkipItems(): void + { + $writer = new TransformingWriter( + $debugProcessor = new TestDebugProcessor( + new CallbackProcessor( + fn ($item) => throw SkipItemException::withWarning($item, 'Skipped for test purpose') + ) + ), + $debugWriter = new TestDebugWriter($innerWriter = new InMemoryWriter()) + ); + + $writer->setJobExecution($execution = JobExecution::createRoot('123', 'test.transforming_writer')); + $writer->initialize(); + $writer->write(['one', 'two', 'three']); + $writer->flush(); + + $debugProcessor->assertWasConfigured(); + $debugProcessor->assertWasUsed(); + $debugWriter->assertWasConfigured(); + $debugWriter->assertWasNotUsed(true, true); + self::assertSame([], $innerWriter->getItems()); + self::assertCount(3, $warnings = $execution->getWarnings()); + self::assertSame('Skipped for test purpose', $warnings[0]->getMessage()); + self::assertSame(['itemIndex' => 0, 'item' => 'one'], $warnings[0]->getContext()); + self::assertSame('Skipped for test purpose', $warnings[1]->getMessage()); + self::assertSame(['itemIndex' => 1, 'item' => 'two'], $warnings[1]->getContext()); + self::assertSame('Skipped for test purpose', $warnings[2]->getMessage()); + self::assertSame(['itemIndex' => 2, 'item' => 'three'], $warnings[2]->getContext()); + self::assertStringContainsString('Skipping item in writer transformation 0.', (string)$execution->getLogs()); + self::assertStringContainsString('Skipping item in writer transformation 1.', (string)$execution->getLogs()); + self::assertStringContainsString('Skipping item in writer transformation 2.', (string)$execution->getLogs()); + } + + public function testInvalidIndexType(): void + { + $this->expectException(UnexpectedValueException::class); + $this->expectExceptionMessage('Expecting argument to be string|int, but got null.'); + + $writer = new TransformingWriter(new NullProcessor(), new InMemoryWriter()); + $generator = function () { + yield null => null; + }; + + $writer->write($generator()); + } +}