Skip to content

Commit 591d7fc

Browse files
committed
[FEATURE] Allow XPath functions to directly return a result, resolves #389
1 parent b26ab67 commit 591d7fc

4 files changed

Lines changed: 64 additions & 17 deletions

File tree

ChangeLog

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
2026-01-07 Francois Suter (Idéative) <typo3@ideative.ch>
2+
3+
* Allow XPath functions to directly return a result, resolves #389
4+
15
2026-01-04 Francois Suter (Idéative) <typo3@ideative.ch>
26

37
* Expand test coverage of XmlHandler, references #389

Classes/Handler/XmlHandler.php

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use Cobweb\ExternalImport\Event\SubstructurePreprocessEvent;
2222
use Cobweb\ExternalImport\Exception\XpathSelectionFailedException;
2323
use Cobweb\ExternalImport\Importer;
24+
use DOMNodeList;
2425
use Psr\EventDispatcher\EventDispatcherInterface;
2526
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
2627

@@ -69,10 +70,16 @@ public function handleData($rawData, Importer $importer): array
6970
$records = [];
7071
if (array_key_exists('nodepath', $generalConfiguration)) {
7172
try {
72-
$records = $this->selectNodeWithXpath(
73+
$records = $this->selectWithXpath(
7374
$xPathObject,
7475
$generalConfiguration['nodepath']
7576
);
77+
// Only a node list makes sense in this context, log an error if anything else was returned
78+
if (!($records instanceof \DomNodeList)) {
79+
$importer->addMessage(
80+
'No nodes could be selected with existing configuration',
81+
);
82+
}
7683
} catch (\Exception $e) {
7784
$importer->addMessage(
7885
$e->getMessage(),
@@ -201,11 +208,15 @@ public function getValue(\DOMNode $record, array $columnConfiguration, \DOMXPath
201208
$selectedNode = $nodeList->item(0);
202209
// If an XPath expression is defined, apply it (relative to currently selected node)
203210
if (!empty($columnConfiguration['xpath'])) {
204-
$nodes = $this->selectNodeWithXpath(
211+
$nodes = $this->selectWithXpath(
205212
$xPathObject,
206213
$columnConfiguration['xpath'],
207214
$selectedNode
208215
);
216+
// If result is a string, return it as is
217+
if (is_string($nodes)) {
218+
return $nodes;
219+
}
209220
$selectedNode = $nodes->item(0);
210221
}
211222
$value = $this->extractValueFromNode(
@@ -222,11 +233,15 @@ public function getValue(\DOMNode $record, array $columnConfiguration, \DOMXPath
222233
} else {
223234
// If an XPath expression is defined, apply it (relative to current node)
224235
if (!empty($columnConfiguration['xpath'])) {
225-
$nodes = $this->selectNodeWithXpath(
236+
$nodes = $this->selectWithXpath(
226237
$xPathObject,
227238
$columnConfiguration['xpath'],
228239
$record
229240
);
241+
// If result is a string, return it as is
242+
if (is_string($nodes)) {
243+
return $nodes;
244+
}
230245
$selectedNode = $nodes->item(0);
231246
$value = $this->extractValueFromNode(
232247
$selectedNode,
@@ -267,7 +282,7 @@ public function getNodeList(\DOMNode $record, array $columnConfiguration, \DOMXP
267282

268283
if ($nodeList->length > 0 && !empty($columnConfiguration['xpath'])) {
269284
$selectedNode = $nodeList->item(0);
270-
$nodeList = $this->selectNodeWithXpath(
285+
$nodeList = $this->selectWithXpath(
271286
$xPathObject,
272287
$columnConfiguration['xpath'],
273288
$selectedNode
@@ -282,7 +297,7 @@ public function getNodeList(\DOMNode $record, array $columnConfiguration, \DOMXP
282297
} else {
283298
// If an XPath expression is defined, apply it (relative to current node)
284299
if (!empty($columnConfiguration['xpath'])) {
285-
$nodeList = $this->selectNodeWithXpath(
300+
$nodeList = $this->selectWithXpath(
286301
$xPathObject,
287302
$columnConfiguration['xpath'],
288303
$record
@@ -291,13 +306,17 @@ public function getNodeList(\DOMNode $record, array $columnConfiguration, \DOMXP
291306
// Create a DOMNodeList by querying the current node (record) itself with XPath
292307
// (weird, but the alternative is to create a new DOMDocument and import the node into it,
293308
// which is not really any better)
294-
$nodeList = $this->selectNodeWithXpath(
309+
$nodeList = $this->selectWithXpath(
295310
$xPathObject,
296311
'.',
297312
$record
298313
);
299314
}
300315
}
316+
// Only a node list makes sense in this context, create an empty list if anything else was returned
317+
if (!($nodeList instanceof \DomNodeList)) {
318+
$nodeList = new \DomNodeList();
319+
}
301320
return $nodeList;
302321
}
303322

@@ -361,10 +380,10 @@ public function getXmlValue(\DOMNode $node): string
361380
* @param \DOMXPath $xPathObject Instantiated DOMXPath object
362381
* @param string $xPath XPath query to evaluate
363382
* @param \DOMNode|null $context Node giving the context of the XPath query (null for root node)
364-
* @return \DOMNodeList List of found nodes
383+
* @return \DOMNodeList|string List of found nodes
365384
* @throws XpathSelectionFailedException
366385
*/
367-
public function selectNodeWithXpath(\DOMXPath $xPathObject, string $xPath, ?\DOMNode $context = null): \DOMNodeList
386+
public function selectWithXpath(\DOMXPath $xPathObject, string $xPath, ?\DOMNode $context = null): \DOMNodeList|string
368387
{
369388
$resultNodes = $xPathObject->evaluate($xPath, $context);
370389
if ($resultNodes === false) {
@@ -373,7 +392,7 @@ public function selectNodeWithXpath(\DOMXPath $xPathObject, string $xPath, ?\DOM
373392
1767541086
374393
);
375394
}
376-
if ($resultNodes->length > 0) {
395+
if (($resultNodes instanceof \DOMNodeList && $resultNodes->length > 0) || is_string($resultNodes)) {
377396
return $resultNodes;
378397
}
379398
throw new XpathSelectionFailedException(

Documentation/Administration/Columns/Index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,7 @@ Description
218218
The value will be taken from the first node returned by the query.
219219
If the :ref:`attribute <administration-columns-properties-attribute>` property is
220220
also defined, it will be applied to the node returned by the XPath query.
221+
If the XPath query is just a function (without selector), the resulting string will be returned.
221222

222223
Please see the :ref:`namespaces <administration-general-tca-properties-namespaces>`
223224
property for declaring namespaces to use in a XPath query.

Tests/Unit/Handler/XmlHandlerTest.php

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,16 @@ public static function selectWithXpathValidProvider(): array
7777
7878
XML,
7979
],
80+
'string selection' => [
81+
'structure' => <<<'XML'
82+
<items>
83+
<item id="foo1" quality="poor">foo</item>
84+
</items>
85+
XML,
86+
'xpath' => 'concat(@id, \'-\', @quality)',
87+
'context' => 'item',
88+
'result' => 'foo1-poor',
89+
],
8090
];
8191
}
8292

@@ -90,16 +100,21 @@ public function selectWithXpathReturnsNodeListOrStringWithValidPath(string $stru
90100
if (!empty($context)) {
91101
$contextNode = $dom->getElementsByTagName($context)->item(0);
92102
}
93-
$nodeList = $this->subject->selectNodeWithXpath($xPathObject, $xpath, $contextNode);
94-
$resultingDocument = new \DOMDocument();
95-
// Test the result by writing the selected nodes to a new document
96-
foreach ($nodeList as $node) {
97-
$node = $resultingDocument->importNode($node, true);
98-
$resultingDocument->appendChild($node);
103+
$nodeList = $this->subject->selectWithXpath($xPathObject, $xpath, $contextNode);
104+
if (is_string($nodeList)) {
105+
$effectiveResult = $nodeList;
106+
} else {
107+
$resultingDocument = new \DOMDocument();
108+
// Test the result by writing the selected nodes to a new document
109+
foreach ($nodeList as $node) {
110+
$node = $resultingDocument->importNode($node, true);
111+
$resultingDocument->appendChild($node);
112+
}
113+
$effectiveResult = $resultingDocument->saveXML();
99114
}
100115
self::assertSame(
101116
$result,
102-
$resultingDocument->saveXML()
117+
$effectiveResult,
103118
);
104119
}
105120

@@ -144,7 +159,7 @@ public function selectWithXpathThrowsExceptionWithInvalidPath(string $structure,
144159
if (!empty($context)) {
145160
$contextNode = $dom->getElementsByTagName($context)->item(0);
146161
}
147-
$this->subject->selectNodeWithXpath($xPathObject, $xpath, $contextNode);
162+
$this->subject->selectWithXpath($xPathObject, $xpath, $contextNode);
148163
}
149164

150165
public static function getValueSuccessProvider(): array
@@ -200,6 +215,14 @@ public static function getValueSuccessProvider(): array
200215
],
201216
'result' => 'foo',
202217
],
218+
'xpath value, with function' => [
219+
'structure' => '<item id="bar1" name="Foo">foo</item>',
220+
'configuration' => [
221+
'field' => 'item',
222+
'xpath' => 'concat(@id, \'-\', @name)',
223+
],
224+
'result' => 'bar1-Foo',
225+
],
203226
'substructure as string' => [
204227
'structure' => '<item><foo>me</foo><bar>you</bar></item>',
205228
'configuration' => [

0 commit comments

Comments
 (0)