Skip to content

Commit

Permalink
Use method instead for nicer DX
Browse files Browse the repository at this point in the history
  • Loading branch information
Sammyjo20 committed Nov 5, 2023
1 parent 0f4a5ea commit 2c2a9b0
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 41 deletions.
77 changes: 54 additions & 23 deletions .github/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<?php

$reader = XmlReader::fromString(...);

$reader->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.

Expand All @@ -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
<?php

$reader = XmlReader::fromString(...);

$reader->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
Expand All @@ -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('
<?xml version="1.0" encoding="utf-8"?>
<person>
<name>Sammyjo20</name>
<favourite-songs>
<song>Luke Combs - When It Rains It Pours</song>
<song>Sam Ryder - SPACE MAN</song>
<song recent="true">London Symfony Orchestra - Starfield Suite</song>
</favourite-songs>
</person>
');

$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
<?php

$reader = XmlReader::fromString(...);

$reader->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
<?php
$reader = XmlReader::fromString(...);

$reader->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
<container xmlns="http://example.com/xml-wrangler/person" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" />
```
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
Expand Down
34 changes: 28 additions & 6 deletions src/XmlReader.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,15 @@ class XmlReader
*/
protected mixed $streamFile = null;

/**
* XPath namespace map
*
* Used to map un-prefixed namespaces
*
* @var array<string, string>
*/
protected array $xpathNamespaceMap = [];

/**
* Constructor
*
Expand Down Expand Up @@ -269,16 +278,16 @@ public function element(string $name, array $withAttributes = []): LazyQuery
/**
* Search for an element with xpath
*
* @param array<string, string> $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.
Expand Down Expand Up @@ -345,15 +354,14 @@ public function value(string $name, array $attributes = []): LazyQuery
/**
* Find and retrieve value of element
*
* @param array<string, string> $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()));
Expand Down Expand Up @@ -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<string, string> $xpathNamespaceMap
*/
public function setXpathNamespaceMap(array $xpathNamespaceMap): XmlReader
{
$this->xpathNamespaceMap = $xpathNamespaceMap;

return $this;
}

/**
* Handle destructing the reader
*
Expand Down
29 changes: 17 additions & 12 deletions tests/Feature/XmlReaderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -513,6 +514,8 @@

// Test we can query xpath element

$reader->setXpathNamespaceMap([]);

$xpathTags = $reader->xpathElement('/container/services/service/tag')->get();

expect($xpathTags)->toEqual([
Expand All @@ -522,26 +525,28 @@

// 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')
->setAttributes(['name' => 'controller.service_arguments', 'xmlns' => 'http://symfony.com/schema/dic/tag-1']));

// 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');
Expand Down

0 comments on commit 2c2a9b0

Please sign in to comment.