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());
+ }
+
}