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');