Skip to content

Commit

Permalink
Merge pull request #23 from saloonphp/feature/v1-remove-namespaces
Browse files Browse the repository at this point in the history
Feature | Remove Namespaces From Reader
  • Loading branch information
Sammyjo20 authored Dec 5, 2023
2 parents fa9f5b0 + 1803f5f commit c26b2f4
Show file tree
Hide file tree
Showing 3 changed files with 149 additions and 4 deletions.
46 changes: 42 additions & 4 deletions src/XmlReader.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ class XmlReader
*/
protected array $xpathNamespaceMap = [];

/**
* Should XML Wrangler keep namespaces?
*/
protected bool $keepNamespaces = true;

/**
* Constructor
*
Expand Down Expand Up @@ -217,8 +222,16 @@ public function element(string $name, array $withAttributes = []): LazyQuery
$matchers = [];

foreach ($searchTerms as $index => $searchTerm) {
// When the search term is not numeric we must perform an element name
// search which will limit the results by a given element name. If
// namespaces were removed we must use element_local_name as this
// matches on elements no matter the prefix.

if (! is_numeric($searchTerm)) {
$matchers[$index] = Matcher\element_name($searchTerm);
$matchers[$index] = $this->keepNamespaces === true
? Matcher\element_name($searchTerm)
: Matcher\element_local_name($searchTerm);

continue;
}

Expand Down Expand Up @@ -302,13 +315,20 @@ public function xpathElement(string $query): Query
$xml = $this->reader->provide(Matcher\document_element())->current();

$xpathConfigurators = [];
$keepNamespaces = $this->keepNamespaces;
$namespaceMap = $this->xpathNamespaceMap;

if ($keepNamespaces === false && ! empty($namespaceMap)) {
throw new XmlReaderException('XPath namespace map cannot be used when namespaces are removed.');
}

// When the namespace map is empty we will remove the root namespaces
// because if they are not mapped then you cannot search on them.

if (empty($namespaceMap)) {
$xml = Document::fromXmlString($xml, traverse(RemoveNamespaces::unprefixed()))->toXmlString();
$namespaceFilter = $keepNamespaces ? RemoveNamespaces::unprefixed() : RemoveNamespaces::all();

$xml = Document::fromXmlString($xml, traverse($namespaceFilter))->toXmlString();
} else {
$xpathConfigurators[] = namespaces($namespaceMap);
}
Expand Down Expand Up @@ -344,8 +364,8 @@ public function xpathElement(string $query): Query
/**
* Convert the XML into an array
*
* @throws \Throwable
* @return array<string, mixed>
* @throws \Throwable
*/
public function values(): array
{
Expand Down Expand Up @@ -413,7 +433,13 @@ protected function convertElementArrayIntoValues(array|Generator $elements): arr
*/
protected function parseXml(string $xml): Element|array
{
$decoded = xml_decode($xml);
$xmlConfigurators = [];

if ($this->keepNamespaces === false) {
$xmlConfigurators[] = traverse(RemoveNamespaces::all());
}

$decoded = xml_decode($xml, ...$xmlConfigurators);

$firstKey = array_key_first($decoded);

Expand Down Expand Up @@ -474,6 +500,18 @@ public function setXpathNamespaceMap(array $xpathNamespaceMap): XmlReader
return $this;
}

/**
* Remove XML namespaces
*
* @return $this
*/
public function removeNamespaces(): static
{
$this->keepNamespaces = false;

return $this;
}

/**
* Handle destructing the reader
*
Expand Down
74 changes: 74 additions & 0 deletions tests/Feature/XmlReaderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -553,3 +553,77 @@

expect($mappedXpathTag)->toBe('1');
});

test('can remove namespaces and prefixes from xml', function () {
$reader = XmlReader::fromFile('tests/Fixtures/prefixed-breakfast-menu.xml');

expect($reader->element('food.0')->first())->toBeNull();

expect($reader->element('bkfst:food.0')->first())->toEqual(
Element::make()
->setAttributes([
'soldOut' => 'false',
'bestSeller' => 'true',
'xmlns:bkfst' => 'http://breakfast.test/example/doesnt-exist',
])
->setContent([
'bkfst:name' => Element::make('Belgian Waffles'),
'bkfst:price' => Element::make('$5.95'),
'bkfst:description' => Element::make('Two of our famous Belgian Waffles with plenty of real maple syrup'),
'bkfst:calories' => Element::make('650'),
])
);

// Now we'll remove the namespaces

$reader->removeNamespaces();

expect($reader->element('bkfst:food.0')->first())->toBeNull();

expect($reader->element('food.0')->first())->toEqual(
Element::make()
->setAttributes([
'soldOut' => 'false',
'bestSeller' => 'true',
])
->setContent([
'name' => Element::make('Belgian Waffles'),
'price' => Element::make('$5.95'),
'description' => Element::make('Two of our famous Belgian Waffles with plenty of real maple syrup'),
'calories' => Element::make('650'),
])
);

// We'll also make sure XPath works okay

expect($reader->xpathValue('//food[1]/name')->sole())->toEqual('Belgian Waffles');
expect($reader->xpathElement('//food[1]/name')->sole())->toEqual(Element::make('Belgian Waffles'));

// Test converting all values

$unprefixedNodes = 0;
$values = $reader->values();

expect($values)->toHaveKey('breakfast_menu');
expect($values['breakfast_menu'])->toHaveKey('food');
expect($values['breakfast_menu']['food'])->toHaveCount(5);

array_walk_recursive($values['breakfast_menu']['food'], function (mixed $value, string $key) use (&$unprefixedNodes) {
if (str_contains($key, ':') === false) {
$unprefixedNodes++;
}
});

expect($unprefixedNodes)->toBe(20);
});

test('cannot use an xpath namespace map without namespaces', function () {
$reader = XmlReader::fromFile('tests/Fixtures/prefixed-breakfast-menu.xml');

$reader->removeNamespaces();
$reader->setXpathNamespaceMap([
'bkfast' => 'http://breakfast.test/example/doesnt-exist',
]);

$reader->xpathValue('//food[1]/name')->sole();
})->throws(XmlReaderException::class, 'XPath namespace map cannot be used when namespaces are removed.');
33 changes: 33 additions & 0 deletions tests/Fixtures/prefixed-breakfast-menu.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<bkfst:breakfast_menu name="Big G's Breakfasts" xmlns:bkfst="http://breakfast.test/example/doesnt-exist">
<bkfst:food soldOut="false" bestSeller="true">
<bkfst:name>Belgian Waffles</bkfst:name>
<bkfst:price>$5.95</bkfst:price>
<bkfst:description>Two of our famous Belgian Waffles with plenty of real maple syrup</bkfst:description>
<bkfst:calories>650</bkfst:calories>
</bkfst:food>
<bkfst:food soldOut="false" bestSeller="false">
<bkfst:name>Strawberry Belgian Waffles</bkfst:name>
<bkfst:price>$7.95</bkfst:price>
<bkfst:description>Light Belgian waffles covered with strawberries and whipped cream</bkfst:description>
<bkfst:calories>900</bkfst:calories>
</bkfst:food>
<bkfst:food soldOut="false" bestSeller="true">
<bkfst:name>Berry-Berry Belgian Waffles</bkfst:name>
<bkfst:price>$8.95</bkfst:price>
<bkfst:description>Light Belgian waffles covered with an assortment of fresh berries and whipped cream</bkfst:description>
<bkfst:calories>900</bkfst:calories>
</bkfst:food>
<bkfst:food soldOut="true" bestSeller="false">
<bkfst:name>French Toast</bkfst:name>
<bkfst:price>$4.50</bkfst:price>
<bkfst:description>Thick slices made from our homemade sourdough bread</bkfst:description>
<bkfst:calories>600</bkfst:calories>
</bkfst:food>
<bkfst:food soldOut="false" bestSeller="false">
<bkfst:name>Homestyle Breakfast</bkfst:name>
<bkfst:price>$6.95</bkfst:price>
<bkfst:description>Two eggs, bacon or sausage, toast, and our ever-popular hash browns</bkfst:description>
<bkfst:calories>950</bkfst:calories>
</bkfst:food>
</bkfst:breakfast_menu>

0 comments on commit c26b2f4

Please sign in to comment.