diff --git a/features/SkipNode.feature b/features/SkipNode.feature new file mode 100644 index 0000000..ac7cb11 --- /dev/null +++ b/features/SkipNode.feature @@ -0,0 +1,61 @@ +Feature: run XMLProcessor with TextNodeProcessor + + Scenario: run XMLProcessor + Given initialize XMLProcessor with "Netlogix\XmlProcessor\Behat\NodeProcessor\ArrayNodeProcessor" + When set skipNode to: + """ + category + """ + When process xml with current XMLProcessor instance: + """ + + foo + + + baz + + + + bar + + """ + Then NodeProcessor "Netlogix\XmlProcessor\Behat\NodeProcessor\ArrayNodeProcessor" should return: + """ +[ + { + "node": "root", + "level": 1, + "attributes": { + "name": "main" + }, + "children": [ + { + "node": "product", + "level": 2, + "attributes": { + "id": "1" + }, + "children": [], + "text": "foo" + }, + { + "node": "category", + "level": 2, + "attributes": { + "id": "1" + }, + "children": [] + }, + { + "node": "product", + "level": 2, + "attributes": { + "id": "2" + }, + "children": [], + "text": "bar" + } + ] + } +] + """ \ No newline at end of file diff --git a/features/bootstrap/FeatureContext.php b/features/bootstrap/FeatureContext.php index 53883d5..637f33d 100644 --- a/features/bootstrap/FeatureContext.php +++ b/features/bootstrap/FeatureContext.php @@ -40,6 +40,14 @@ public function iRunXmlProcessor(PyStringNode $content): void $this->xmlProcessor->processFile($fileName); } + /** + * @When set skipNode to: + */ + public function iSetSkipNode(PyStringNode $skipNode): void + { + $this->xmlProcessor->setSkipNodes($skipNode->getStrings()); + } + /** * @Then NodeProcessor :nodeProcessorClass should return: */ @@ -52,7 +60,7 @@ function nodeProcessorShouldReturn(string $nodeProcessorClass, PyStringNode $con throw new \Exception(sprintf('Class %s does not extend %s', $nodeProcessorClass, NodeProcessorInterface::class)); } /** @var InvokeNodeProcessorInterface $nodeProcessor */ - $nodeProcessor = $this->xmlProcessor->getProcessorContext()->getProcessor($nodeProcessorClass); + $nodeProcessor = $this->xmlProcessor->getProcessor($nodeProcessorClass); $expected = json_decode($content->getRaw(), true); if (json_last_error() !== JSON_ERROR_NONE) { throw new \Exception(sprintf('Could not decode expected result: %s', json_last_error_msg())); diff --git a/src/NodeProcessor/AbstractNodeProcessor.php b/src/NodeProcessor/AbstractNodeProcessor.php index c0ac117..1544709 100644 --- a/src/NodeProcessor/AbstractNodeProcessor.php +++ b/src/NodeProcessor/AbstractNodeProcessor.php @@ -3,6 +3,7 @@ namespace Netlogix\XmlProcessor\NodeProcessor; +use Netlogix\XmlProcessor\XmlProcessor; use Netlogix\XmlProcessor\XmlProcessorContext; class AbstractNodeProcessor implements NodeProcessorInterface @@ -34,16 +35,6 @@ public function getSubscribedEvents(string $nodePath, XmlProcessorContext $conte public function isNode(string $nodePath): bool { - $expected = $this->getNodePath(); - if ($expected === '/' . $nodePath) { - return true; - } - - return $nodePath === $this->getNodePath() - || ( - function_exists('str_end_with') - ? str_end_with($nodePath, $expected) : - substr_compare($nodePath, $expected, -strlen($expected)) === 0 - ); + return XmlProcessor::checkNodePath($nodePath, $this->getNodePath()); } } diff --git a/src/XmlProcessor.php b/src/XmlProcessor.php index b15d098..885c686 100644 --- a/src/XmlProcessor.php +++ b/src/XmlProcessor.php @@ -17,37 +17,57 @@ class XmlProcessor private array $nodePath = []; private string $currentValue = ''; + private ?array $skipNodes = NULL; + private \XMLReader $xml; private XmlProcessorContext $context; /** @var iterable */ private iterable $processors; + /** @var iterable */ + private iterable $parserProperties; + /** * @param iterable $processors - * @param iterable $options + * @param iterable $parserProperties */ public function __construct( iterable $processors, - iterable $options = [] + iterable $parserProperties = [] ) { $this->xml = new \XMLReader(); - foreach ($options as $option => $value) { - $this->xml->setParserProperty($option, $value); - } $this->processors = $processors; - $this->context = new XmlProcessorContext($this->xml, $this->processors); + $this->parserProperties = $parserProperties; + $this->context = new XmlProcessorContext( + $this->xml, + $this->processors, + fn() => $this->skipNode() + ); } - function getProcessorContext(): XmlProcessorContext + function setSkipNodes(?array $skipNodes = NULL): void { - return $this->context; + $this->skipNodes = $skipNodes; + } + + function getSkipNodes(): ?array + { + return $this->skipNodes; + } + + function getProcessor(string $processorName): ?NodeProcessorInterface + { + return $this->context->getProcessor($processorName); } public function processFile(string $filename): void { $this->xml->open($filename); + foreach ($this->parserProperties as $parserProperty => $value) { + $this->xml->setParserProperty($parserProperty, $value); + } $this->getProcessorEvents(self::EVENT_OPEN_FILE); while ($this->xml->read()) { switch ($this->xml->nodeType) { @@ -57,6 +77,10 @@ public function processFile(string $filename): void case \XMLReader::ELEMENT: $selfClosing = $this->xml->isEmptyElement; $this->eventOpenElement(); + if ($this->shouldSkipNode()) { + $this->skipNode(); + break; + } if ($selfClosing) { $this->eventCloseElement(); } @@ -73,6 +97,28 @@ public function processFile(string $filename): void $this->xml->close(); } + private function skipNode(): bool + { + $result = $this->xml->next(); + $this->eventCloseElement(); + return $result; + } + + private function shouldSkipNode(): bool + { + if ($this->skipNodes === NULL) { + return false; + } + $nodePath = implode('/', $this->nodePath); + foreach ($this->skipNodes as $skipNode) { + if (self::checkNodePath($nodePath, $skipNode)) { + return true; + } + } + + return false; + } + private function eventOpenElement(): void { $this->pushNodePath(); @@ -148,4 +194,15 @@ private function createContext(string $contextClass): NodeProcessorContext } return $context; } + + static function checkNodePath(string $nodePath, string $expected): bool + { + return + $expected === '/' . $nodePath || + $nodePath === $expected || ( + function_exists('str_end_with') + ? str_end_with($nodePath, $expected) : + substr_compare($nodePath, $expected, -strlen($expected)) === 0 + ); + } } diff --git a/src/XmlProcessorContext.php b/src/XmlProcessorContext.php index 842a9b1..0e91d01 100644 --- a/src/XmlProcessorContext.php +++ b/src/XmlProcessorContext.php @@ -3,6 +3,7 @@ namespace Netlogix\XmlProcessor; +use Netlogix\XmlProcessor\NodeProcessor\NamedNodeProcessorInterface; use Netlogix\XmlProcessor\NodeProcessor\NodeProcessorInterface; class XmlProcessorContext @@ -13,16 +14,24 @@ class XmlProcessorContext */ private iterable $processors; - public function __construct(\XMLReader $xml, iterable $processors) + private \Closure $skipNode; + + public function __construct(\XMLReader $xml, iterable $processors, \Closure $skipNode) { $this->xml = $xml; $this->processors = $processors; + $this->skipNode = $skipNode; + } + + public function skipCurrentNode(): bool + { + return ($this->skipNode)(); } public function getProcessor(string $class): ?NodeProcessorInterface { foreach ($this->processors as $processor) { - if ($processor instanceof $class) { + if (class_exists($class) && $processor instanceof $class) { return $processor; } } diff --git a/tests/Fixtures/AbstractNodeProcessorTest/TestNodeProcessor.php b/tests/Fixtures/AbstractNodeProcessorTest/TestNodeProcessor.php index 4fea789..bccc615 100644 --- a/tests/Fixtures/AbstractNodeProcessorTest/TestNodeProcessor.php +++ b/tests/Fixtures/AbstractNodeProcessorTest/TestNodeProcessor.php @@ -3,8 +3,26 @@ namespace Netlogix\XmlProcessor\Tests\Fixtures\AbstractNodeProcessorTest; use Netlogix\XmlProcessor\NodeProcessor\AbstractNodeProcessor; +use Netlogix\XmlProcessor\NodeProcessor\CloseNodeProcessorInterface; +use Netlogix\XmlProcessor\NodeProcessor\Context\CloseContext; +use Netlogix\XmlProcessor\NodeProcessor\Context\OpenContext; +use Netlogix\XmlProcessor\NodeProcessor\Context\TextContext; +use Netlogix\XmlProcessor\NodeProcessor\OpenNodeProcessorInterface; +use Netlogix\XmlProcessor\NodeProcessor\TextNodeProcessorInterface; -class TestNodeProcessor extends AbstractNodeProcessor +class TestNodeProcessor extends AbstractNodeProcessor implements OpenNodeProcessorInterface, TextNodeProcessorInterface, CloseNodeProcessorInterface { const NODE_PATH = 'test'; + + function openElement(OpenContext $context): void + { + } + + function textElement(TextContext $context): void + { + } + + function closeElement(CloseContext $context): void + { + } } \ No newline at end of file diff --git a/tests/Fixtures/XmlProcessorTest/test.xml b/tests/Fixtures/XmlProcessorTest/test.xml index d458015..ead37af 100644 --- a/tests/Fixtures/XmlProcessorTest/test.xml +++ b/tests/Fixtures/XmlProcessorTest/test.xml @@ -1,4 +1,8 @@ - hallo + + text + + hallo +
\ No newline at end of file diff --git a/tests/Unit/Behat/NodeProcessor/ArrayNodeProcessorTest.php b/tests/Unit/Behat/NodeProcessor/ArrayNodeProcessorTest.php index ada431e..e8f85f1 100644 --- a/tests/Unit/Behat/NodeProcessor/ArrayNodeProcessorTest.php +++ b/tests/Unit/Behat/NodeProcessor/ArrayNodeProcessorTest.php @@ -1,9 +1,11 @@ ['foo', 'bar'], 'attributes' => ['name' => 'me'], + 'text' => 'test' ], [ 'nodePath' => ['foo', 'bar'], 'attributes' => ['name' => 'you'], + 'text' => 'test2' ], [ 'nodePath' => ['foo'], @@ -57,6 +61,11 @@ function testOpenElement(): void $context = new OpenContext($xmlProcessorContext, $item['nodePath']); $context->setAttributes($item['attributes']); $nodeProcessor->openElement($context); + if (isset($item['text'])) { + $textContext = new TextContext($xmlProcessorContext, $item['nodePath']); + $textContext->setText($item['text']); + $nodeProcessor->textElement($textContext); + } } self::assertEquals([ @@ -70,12 +79,14 @@ function testOpenElement(): void 'level' => 2, 'attributes' => ['name' => 'me'], 'children' => [], + 'text' => 'test' ], [ 'node' => 'bar', 'level' => 2, 'attributes' => ['name' => 'you'], 'children' => [], + 'text' => 'test2' ], ], diff --git a/tests/Unit/Behat/NodeProcessor/TextNodeProcessorTest.php b/tests/Unit/Behat/NodeProcessor/TextNodeProcessorTest.php index 112f5bd..888018a 100644 --- a/tests/Unit/Behat/NodeProcessor/TextNodeProcessorTest.php +++ b/tests/Unit/Behat/NodeProcessor/TextNodeProcessorTest.php @@ -1,4 +1,5 @@ assertEquals($expectedResult, $nodeProcessor->isNode($nodePath)); + $nodeProcessor = $this->getMockForAbstractClass( + TestNodeProcessor::class + ); + $context = $this->createMock(XmlProcessorContext::class); + $events = []; + foreach ($nodeProcessor->getSubscribedEvents('test', $context) as $event => $action) { + $events[] = $event; + self::assertIsCallable($action); + } + + self::assertEquals([ + 'NodeType_' . \XMLReader::ELEMENT, + 'NodeType_' . \XMLReader::END_ELEMENT, + 'NodeType_' . \XMLReader::TEXT, + ], $events); } public static function isNodeDataProvider(): \Generator diff --git a/tests/Unit/NodeProcessor/Context/CloseContextTest.php b/tests/Unit/NodeProcessor/Context/CloseContextTest.php index aaa2b7b..be439a2 100644 --- a/tests/Unit/NodeProcessor/Context/CloseContextTest.php +++ b/tests/Unit/NodeProcessor/Context/CloseContextTest.php @@ -1,4 +1,5 @@ getXMLReaderMock(), []); + $context = new XmlProcessorContext($this->getXMLReaderMock(), [], fn() => true); $this->assertInstanceOf(XmlProcessorContext::class, $context); } function testGetXMLReader(): void { $xmlReader = $this->getXMLReaderMock(); - $context = new XmlProcessorContext($xmlReader, []); + $context = new XmlProcessorContext($xmlReader, [], fn() => true); $this->assertSame($xmlReader, $context->getXMLReader()); } @@ -35,7 +39,7 @@ function testGetXMLReader(): void */ function testGetProcessor($processor, $expected): void { - $context = new XmlProcessorContext($this->getXMLReaderMock(), [$processor]); + $context = new XmlProcessorContext($this->getXMLReaderMock(), [$processor], fn() => true); $this->assertSame($expected, $context->getProcessor(NodeProcessorInterface::class)); } @@ -45,6 +49,23 @@ public static function getProcessorDataProvider(): \Generator yield [[], NULL]; } + /** + * @dataProvider skipCurrentNodeDataProvider + */ + public function testSkipCurrentNode(bool $return): void + { + $skipNodeMock = $this->getMockBuilder(\stdClass::class)->addMethods(['skipNode'])->getMock(); + $skipNodeMock->expects($this->atLeastOnce())->method('skipNode')->willReturn($return); + $context = new XmlProcessorContext($this->getXMLReaderMock(), [], fn() => $skipNodeMock->skipNode()); + self::assertEquals($context->skipCurrentNode(), $return); + } + + function skipCurrentNodeDataProvider(): iterable + { + yield [true]; + yield [false]; + } + private function getXMLReaderMock(): \XMLReader { return $this->getMockBuilder(\XMLReader::class)->getMock(); diff --git a/tests/Unit/XmlProcessorTest.php b/tests/Unit/XmlProcessorTest.php index 83369b6..73a8ee6 100644 --- a/tests/Unit/XmlProcessorTest.php +++ b/tests/Unit/XmlProcessorTest.php @@ -1,4 +1,5 @@ getMockForAbstractClass(NodeProcessorInterface::class) - ]); + $xmlProcessor = new XmlProcessor( + [ + $this->getMockForAbstractClass(NodeProcessorInterface::class) + ], + [ + \XMLReader::SUBST_ENTITIES => true + ] + ); self::assertInstanceOf(XmlProcessor::class, $xmlProcessor); } - public function testGetProcessorContext(): void + public function testGetProcessor(): void { - $xmlProcessor = new XmlProcessor([ - $this->getMockForAbstractClass(NodeProcessorInterface::class) - ]); - $context = $xmlProcessor->getProcessorContext(); - self::assertInstanceOf(XmlProcessorContext::class, $context); + $nodeProcessor = $this->getMockForAbstractClass(TestNodeProcessor::class); + $xmlProcessor = new XmlProcessor([$nodeProcessor]); + + self::assertInstanceOf(TestNodeProcessor::class, $xmlProcessor->getProcessor(TestNodeProcessor::class)); + self::assertInstanceOf(get_class($nodeProcessor), $xmlProcessor->getProcessor(TestNodeProcessor::class)); + self::assertNull($xmlProcessor->getProcessor(OpenNodeProcessorInterface::class)); } public function testProcessFile() @@ -58,5 +64,62 @@ public function testProcessFile() ]); $xmlProcessor->processFile(__DIR__ . '/../Fixtures/XmlProcessorTest/test.xml'); + + $xmlProcessor->setSkipNodes(['foo']); + $xmlProcessor->processFile(__DIR__ . '/../Fixtures/XmlProcessorTest/test.xml'); + + $xmlProcessor = new XmlProcessor( + [$nodeProcessor], + [\XMLReader::SUBST_ENTITIES => true] + ); + $xmlProcessor->processFile(__DIR__ . '/../Fixtures/XmlProcessorTest/test.xml'); + + if(!function_exists('str_end_with')){ + function str_end_with(string $nodePath, string $expected){ + return substr_compare($nodePath, $expected, -strlen($expected)) === 0; + } + } + $xmlProcessor->processFile(__DIR__ . '/../Fixtures/XmlProcessorTest/test.xml'); + } + + /** + * @dataProvider checkNodePathDataProvider + */ + function testCheckNodePath(string $nodePath, string $expected, bool $result): void + { + self::assertSame(XmlProcessor::checkNodePath($nodePath, $expected), $result); } + + public static function checkNodePathDataProvider(): iterable + { + yield ['/foo/bar', 'foo/bar', true]; + yield ['/foo', 'foo/bar', false]; + yield ['foo', 'foo/bar', false]; + yield ['bar', 'foo/bar', false]; + yield ['foo/bar', 'bar', true]; + yield ['foo/bar', 'foo/bar', true]; + yield ['foo/bar/baz', 'foo/bar', false]; + } + + function testSetSkipNodes(): void + { + $xmlProcessor = new XmlProcessor([ + $this->getMockForAbstractClass(NodeProcessorInterface::class) + ]); + $xmlProcessor->setSkipNodes(['foo']); + self::assertSame(['foo'], $xmlProcessor->getSkipNodes()); + } + + function testGetSkipNodes(): void + { + $xmlProcessor = new XmlProcessor([ + $this->getMockForAbstractClass(NodeProcessorInterface::class) + ]); + self::assertNull($xmlProcessor->getSkipNodes()); + $xmlProcessor->setSkipNodes([]); + self::assertSame([], $xmlProcessor->getSkipNodes()); + $xmlProcessor->setSkipNodes(['foo']); + self::assertSame(['foo'], $xmlProcessor->getSkipNodes()); + } + }