From b1391cb357360d01bb172c53e66907fb549270d7 Mon Sep 17 00:00:00 2001 From: Sascha Date: Fri, 17 Jul 2026 07:52:37 +0200 Subject: [PATCH] feat: allow custom data and filename on XmlProcessorContext processFile() now accepts an optional data array that is stored on the XmlProcessorContext alongside the currently processed filename, so node processors can access per-run context via getXmlProcessorContext(). Co-Authored-By: Claude Sonnet 5 --- README.md | 21 ++++++++++ src/XmlProcessor.php | 7 +++- src/XmlProcessorContext.php | 55 ++++++++++++++++++++++++++ tests/Unit/XmlProcessorContextTest.php | 45 +++++++++++++++++++++ tests/Unit/XmlProcessorTest.php | 23 +++++++++++ 5 files changed, 150 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8dad80c..c472075 100644 --- a/README.md +++ b/README.md @@ -129,6 +129,27 @@ array(3) { [`TextNodeProcessorInterface`]: src/NodeProcessor/TextNodeProcessorInterface.php +## Passing custom data to `processFile` + +`XmlProcessor::processFile()` accepts an optional associative array of custom data. +It is stored on the [`XmlProcessorContext`], together with the filename currently being processed, +and is available to every node processor via `$context->getXmlProcessorContext()`. + +```php +$processor->processFile('file.xml', ['tenant' => 'acme']); +``` + +```php +public function openElement(OpenContext $context) +{ + $xmlProcessorContext = $context->getXmlProcessorContext(); + $filename = $xmlProcessorContext->getFilename(); + $tenant = $xmlProcessorContext->get('tenant'); +} +``` + +[`XmlProcessorContext`]: src/XmlProcessorContext.php + [`NodeProcessorContext`]: src/NodeProcessor/Context/NodeProcessorContext.php [`OpenContext]: src/NodeProcessor/Context/OpenContext.php diff --git a/src/XmlProcessor.php b/src/XmlProcessor.php index d17bc60..953a27e 100644 --- a/src/XmlProcessor.php +++ b/src/XmlProcessor.php @@ -100,8 +100,13 @@ function getProcessor(string $processorName): ?NodeProcessorInterface return $this->context->getProcessor($processorName); } - public function processFile(string $filename): void + /** + * @param array $data + */ + public function processFile(string $filename, array $data = []): void { + $this->context->setFilename($filename); + $this->context->setData($data); $this->xml->open($filename); foreach ($this->parserProperties as $parserProperty => $value) { $this->xml->setParserProperty($parserProperty, $value); diff --git a/src/XmlProcessorContext.php b/src/XmlProcessorContext.php index 4d00d70..029fe6e 100644 --- a/src/XmlProcessorContext.php +++ b/src/XmlProcessorContext.php @@ -18,6 +18,13 @@ class XmlProcessorContext private Closure $skipNode; + private string $filename = ''; + + /** + * @var array + */ + private array $data = []; + public function __construct(XMLReader $xml, iterable $processors, Closure $skipNode) { $this->xml = $xml; @@ -45,4 +52,52 @@ public function getXMLReader(): XMLReader { return $this->xml; } + + public function setFilename(string $filename): void + { + $this->filename = $filename; + } + + public function getFilename(): string + { + return $this->filename; + } + + /** + * @param array $data + */ + public function setData(array $data): void + { + $this->data = $data; + } + + /** + * @return array + */ + public function getData(): array + { + return $this->data; + } + + /** + * @param mixed $default + * @return mixed + */ + public function get(string $key, $default = null) + { + return $this->data[$key] ?? $default; + } + + /** + * @param mixed $value + */ + public function set(string $key, $value): void + { + $this->data[$key] = $value; + } + + public function has(string $key): bool + { + return \array_key_exists($key, $this->data); + } } diff --git a/tests/Unit/XmlProcessorContextTest.php b/tests/Unit/XmlProcessorContextTest.php index 65e4be6..80cdb20 100644 --- a/tests/Unit/XmlProcessorContextTest.php +++ b/tests/Unit/XmlProcessorContextTest.php @@ -67,6 +67,51 @@ function skipCurrentNodeDataProvider(): iterable yield [false]; } + function testGetFilename(): void + { + $context = new XmlProcessorContext($this->getXMLReaderMock(), [], fn() => true); + self::assertSame('', $context->getFilename()); + + $context->setFilename('foo.xml'); + self::assertSame('foo.xml', $context->getFilename()); + } + + function testGetData(): void + { + $context = new XmlProcessorContext($this->getXMLReaderMock(), [], fn() => true); + self::assertSame([], $context->getData()); + + $context->setData(['foo' => 'bar']); + self::assertSame(['foo' => 'bar'], $context->getData()); + } + + function testGet(): void + { + $context = new XmlProcessorContext($this->getXMLReaderMock(), [], fn() => true); + self::assertNull($context->get('foo')); + self::assertSame('default', $context->get('foo', 'default')); + + $context->setData(['foo' => 'bar']); + self::assertSame('bar', $context->get('foo')); + } + + function testSet(): void + { + $context = new XmlProcessorContext($this->getXMLReaderMock(), [], fn() => true); + $context->set('foo', 'bar'); + self::assertSame('bar', $context->get('foo')); + self::assertSame(['foo' => 'bar'], $context->getData()); + } + + function testHas(): void + { + $context = new XmlProcessorContext($this->getXMLReaderMock(), [], fn() => true); + self::assertFalse($context->has('foo')); + + $context->set('foo', 'bar'); + self::assertTrue($context->has('foo')); + } + private function getXMLReaderMock(): XMLReader { return $this->getMockBuilder(XMLReader::class)->getMock(); diff --git a/tests/Unit/XmlProcessorTest.php b/tests/Unit/XmlProcessorTest.php index bb01908..699358b 100644 --- a/tests/Unit/XmlProcessorTest.php +++ b/tests/Unit/XmlProcessorTest.php @@ -8,6 +8,7 @@ use Generator; use Netlogix\XmlProcessor\NodeProcessor\CloseNodeProcessorInterface; use Netlogix\XmlProcessor\NodeProcessor\Context\CloseContext; +use Netlogix\XmlProcessor\NodeProcessor\Context\NodeProcessorContext; use Netlogix\XmlProcessor\NodeProcessor\Context\OpenContext; use Netlogix\XmlProcessor\NodeProcessor\Context\TextContext; use Netlogix\XmlProcessor\NodeProcessor\NodeProcessorInterface; @@ -82,6 +83,28 @@ public function testProcessFile() $xmlProcessor->processFile(__DIR__ . '/../Fixtures/XmlProcessorTest/test.xml'); } + public function testProcessFile_fillsContextWithFilenameAndData(): void + { + $filename = __DIR__ . '/../Fixtures/XmlProcessorTest/test.xml'; + $nodeProcessor = $this->getMockForAbstractClass(NodeProcessorInterface::class); + + $nodeProcessor + ->method('getSubscribedEvents') + ->will($this->returnCallback( + fn() => yield from [ + XmlProcessor::EVENT_OPEN_FILE => function (NodeProcessorContext $context) use ($filename): void { + $xmlProcessorContext = $context->getXmlProcessorContext(); + self::assertSame($filename, $xmlProcessorContext->getFilename()); + self::assertSame(['foo' => 'bar'], $xmlProcessorContext->getData()); + self::assertSame('bar', $xmlProcessorContext->get('foo')); + } + ] + )); + + $xmlProcessor = new XmlProcessor([$nodeProcessor]); + $xmlProcessor->processFile($filename, ['foo' => 'bar']); + } + public function testProcessFile_skipCurrentNode() { $nodeProcessor = $this->getMockForAbstractClass(NodeProcessorInterface::class);