From 2c2a9b08c0f6ecd26849be1393158fb06ff58690 Mon Sep 17 00:00:00 2001
From: Sammyjo20 <29132017+Sammyjo20@users.noreply.github.com>
Date: Sun, 5 Nov 2023 21:57:16 +0000
Subject: [PATCH] Use method instead for nicer DX
---
.github/README.md | 77 +++++++++++++++++++++++----------
src/XmlReader.php | 34 ++++++++++++---
tests/Feature/XmlReaderTest.php | 29 ++++++++-----
3 files changed, 99 insertions(+), 41 deletions(-)
diff --git a/.github/README.md b/.github/README.md
index 3234554..95c3528 100644
--- a/.github/README.md
+++ b/.github/README.md
@@ -181,17 +181,6 @@ $reader->value('song')->get(); // ['Luke Combs - When It Rains It Pours', 'Sam R
$reader->value('song.2')->sole(); // 'London Symfony Orchestra - Starfield Suite'
```
-#### Reading Specific Values via XPath
-You can use the `xpathValue` method to find a specific element's value with an [XPath](https://devhints.io/xpath) query. This method will return a `Query` class which has different methods to retrieve the data.
-```php
-xpathValue('//person/favourite-songs/song[3]')->sole(); // 'London Symfony Orchestra - Starfield Suite'
-```
->**Warning**
->This method is not memory safe as XPath requires all the XML to be loaded in memory at once.
#### Reading Specific Elements
You can use the `element` method to search for a specific element. You can use dot-notation to search for child elements. You can also use whole numbers to find specific positions of multiple elements. This method searches through the whole XML body in a memory efficient way.
@@ -215,18 +204,6 @@ $reader->element('song')->get(); // [Element('Luke Combs - When It Rains It Pour
$reader->element('song.2')->sole(); // Element('London Symfony Orchestra - Starfield Suite')
```
-#### Reading Specific Elements via XPath
-You can use the `xpathElement` method to find a specific element with an [XPath](https://devhints.io/xpath) query. This method will return a `Query` class which has different methods to retrieve the data.
-```php
-xpathElement('//person/favourite-songs/song[3]')->sole(); // Element('London Symfony Orchestra - Starfield Suite')
-```
->**Warning**
->This method is not memory safe as XPath requires all the XML to be loaded in memory at once.
-
#### Lazily Iterating
When searching a large file, you can use the `lazy` or `collectLazy` methods which will return a generator of results only keeping one item in memory at a time.
```php
@@ -243,7 +220,61 @@ $names = $reader->value('name')->collect();
$names = $reader->value('name')->collectLazy();
```
+#### Searching for specific elements
+Sometimes you might want to search for a specific element or value where the element contains a specific attribute. You can do this by providing a second argument to the `value` or `element` method. This will search the last element for the attributes and will return if they match.
+```php
+$reader = XmlReader::fromString('
+
+
+ Sammyjo20
+
+ Luke Combs - When It Rains It Pours
+ Sam Ryder - SPACE MAN
+ London Symfony Orchestra - Starfield Suite
+
+
+');
+
+$reader->element('song', ['recent' => 'true'])->sole(); // Element('London Symfony Orchestra - Starfield Suite')
+
+$reader->value('song', ['recent' => 'true'])->sole(); // 'London Symfony Orchestra - Starfield Suite'
+```
+### Reading with XPath
+XPath is a fantastic way to search through XML. With one string, you can search for a specific element, with specific attributes or indexes. If you are interested in learning XPath, you can [click here for a useful cheatsheet](https://devhints.io/xpath).
+
+#### Reading Specific Elements via XPath
+You can use the `xpathElement` method to find a specific element with an [XPath](https://devhints.io/xpath) query. This method will return a `Query` class which has different methods to retrieve the data.
+```php
+xpathElement('//person/favourite-songs/song[3]')->sole(); // Element('London Symfony Orchestra - Starfield Suite')
+```
+#### Reading Specific Values via XPath
+You can use the `xpathValue` method to find a specific element's value with an [XPath](https://devhints.io/xpath) query. This method will return a `Query` class which has different methods to retrieve the data.
+```php
+xpathValue('//person/favourite-songs/song[3]')->sole(); // 'London Symfony Orchestra - Starfield Suite'
+```
+>**Warning**
+>Due to limitations with XPath - the above methods used to query with XPath are not memory safe and may not be suitable for large XML documents.
+#### XPath and un-prefixed namespaces
+You might found yourself with an XML document that contains an un-prefixed `xmlns` attribute - like this:
+```xml
+
+```
+When this happens, XML Wrangler will automatically remove these un-prefixed namespaces to improve compatability. If you would like to keep these namespaces, you can use `setXpathNamespaceMap` to map each un-prefixed XML namespace.
+```php
+$reader = XmlReader::fromString(...);
+$reader->setXpathNamespaceMap([
+ 'root' => 'http://example.com/xml-wrangler/person',
+]);
+
+$reader->xpathValue('//root:person/root:favourite-songs/root:song[3]')->sole();
+```
### Writing XML
This section on the documentation is for using the XML writer.
#### Basic Usage
diff --git a/src/XmlReader.php b/src/XmlReader.php
index b744fce..29ed2a3 100644
--- a/src/XmlReader.php
+++ b/src/XmlReader.php
@@ -36,6 +36,15 @@ class XmlReader
*/
protected mixed $streamFile = null;
+ /**
+ * XPath namespace map
+ *
+ * Used to map un-prefixed namespaces
+ *
+ * @var array
+ */
+ protected array $xpathNamespaceMap = [];
+
/**
* Constructor
*
@@ -269,16 +278,16 @@ public function element(string $name, array $withAttributes = []): LazyQuery
/**
* Search for an element with xpath
*
- * @param array $namespaceMap
* @throws \Throwable
* @throws \VeeWee\Xml\Encoding\Exception\EncodingException
*/
- public function xpathElement(string $query, array $namespaceMap = []): Query
+ public function xpathElement(string $query): Query
{
try {
$xml = $this->reader->provide(Matcher\document_element())->current();
$xpathConfigurators = [];
+ $namespaceMap = $this->xpathNamespaceMap;
// When the namespace map is empty we will remove the root namespaces
// because if they are not mapped then you cannot search on them.
@@ -345,15 +354,14 @@ public function value(string $name, array $attributes = []): LazyQuery
/**
* Find and retrieve value of element
*
- * @param array $namespaceMap
* @throws \Saloon\XmlWrangler\Exceptions\XmlReaderException
* @throws \Throwable
* @throws \VeeWee\Xml\Encoding\Exception\EncodingException
*/
- public function xpathValue(string $query, array $namespaceMap = []): Query
+ public function xpathValue(string $query): Query
{
- $generator = function () use ($query, $namespaceMap) {
- yield from $this->xpathElement($query, $namespaceMap)->get();
+ $generator = function () use ($query) {
+ yield from $this->xpathElement($query)->get();
};
return new Query($query, $this->convertElementArrayIntoValues($generator()));
@@ -435,6 +443,20 @@ protected function convertArrayIntoElements(?string $key, mixed $value): array|E
return [$key => $element];
}
+ /**
+ * Set the XPath namespace map
+ *
+ * Used to map un-prefixed namespaces
+ *
+ * @param array $xpathNamespaceMap
+ */
+ public function setXpathNamespaceMap(array $xpathNamespaceMap): XmlReader
+ {
+ $this->xpathNamespaceMap = $xpathNamespaceMap;
+
+ return $this;
+ }
+
/**
* Handle destructing the reader
*
diff --git a/tests/Feature/XmlReaderTest.php b/tests/Feature/XmlReaderTest.php
index 2f11386..41d88a9 100644
--- a/tests/Feature/XmlReaderTest.php
+++ b/tests/Feature/XmlReaderTest.php
@@ -487,9 +487,10 @@
// Or we can map them and they will be searchable
+ $reader->setXpathNamespaceMap(['root' => 'http://symfony.com/schema/dic/services']);
+
$mappedXpathElement = $reader->xpathElement(
- query: '/root:container/root:services/root:service[@id="service_container"]',
- namespaceMap: ['root' => 'http://symfony.com/schema/dic/services']
+ '/root:container/root:services/root:service[@id="service_container"]',
)->sole();
expect($mappedXpathElement)->toEqual(
@@ -513,6 +514,8 @@
// Test we can query xpath element
+ $reader->setXpathNamespaceMap([]);
+
$xpathTags = $reader->xpathElement('/container/services/service/tag')->get();
expect($xpathTags)->toEqual([
@@ -522,13 +525,14 @@
// Test we can query xpath elements with mapping
+ $reader->setXpathNamespaceMap([
+ 'root' => 'http://symfony.com/schema/dic/services',
+ 'tag-1' => 'http://symfony.com/schema/dic/tag-1',
+ 'tag-2' => 'http://symfony.com/schema/dic/tag-2',
+ ]);
+
$mappedXpathTag = $reader->xpathElement(
- query: '/root:container/root:services/root:service/tag-1:tag',
- namespaceMap: [
- 'root' => 'http://symfony.com/schema/dic/services',
- 'tag-1' => 'http://symfony.com/schema/dic/tag-1',
- 'tag-2' => 'http://symfony.com/schema/dic/tag-2',
- ],
+ '/root:container/root:services/root:service/tag-1:tag',
)->sole();
expect($mappedXpathTag)->toEqual(Element::make('1')
@@ -536,12 +540,13 @@
// Works with XPath Element
+ $reader->setXpathNamespaceMap([
+ 'root' => 'http://symfony.com/schema/dic/services',
+ 'tag-1' => 'http://symfony.com/schema/dic/tag-1',
+ ]);
+
$mappedXpathTag = $reader->xpathValue(
query: '/root:container/root:services/root:service/tag-1:tag',
- namespaceMap: [
- 'root' => 'http://symfony.com/schema/dic/services',
- 'tag-1' => 'http://symfony.com/schema/dic/tag-1',
- ],
)->sole();
expect($mappedXpathTag)->toBe('1');