I'm trying to create a search function allowing partial matching by song title or genre using Xpath.
This is my XML file:
<?xml version="1.0" encoding="UTF-8"?>
<playlist>
<item>
<songid>USAT29902236</songid>
<songtitle>I Say a Little Prayer</songtitle>
<artist>Aretha Franklin</artist>
<genre>Soul</genre>
<link>https://www.amazon.com/I-Say-a-Little-Prayer/dp/B001BZD6KO</link>
<releaseyear>1968</releaseyear>
</item>
<item>
<songid>GBAAM8300001</songid>
<songtitle>Every Breath You Take</songtitle>
<artist>The Police</artist>
<genre>Pop/Rock</genre>
<link>https://www.amazon.com/Every-Breath-You-Take-Police/dp/B000008JI6</link>
<releaseyear>1983</releaseyear>
</item>
<item>
<songid>GBBBN7902002</songid>
<songtitle>London Calling</songtitle>
<artist>The Clash</artist>
<genre>Post-punk</genre>
<link>https://www.amazon.com/London-Calling-Remastered/dp/B00EQRJNTM</link>
<releaseyear>1979</releaseyear>
</item>
</playlist>
and this is my search function so far:
function searchSong($words){
global $xml;
if(!empty($words)){
foreach($words as $word){
//$query = "//playlist/item[contains(songtitle/genre, '{$word}')]";
$query = "//playlist/item[(songtitle[contains('{$word}')]) and (genre[contains('{$word}')])]";
$result = $xml->xpath($query);
}
}
print_r($result);
}
Calling the function searchSong(array("take", "soul"))
should return the second and first song from XML file, but the array is always empty.
A few errors here: use of and
instead of or
, assuming searches are case-insensitive, and passing incorrect number of parameters to contains
. The last would have triggered PHP warnings if you were looking for them. Also, you're only ever returning the last item you search for.
Case insensitive searches in XPath 1.0 (which is all PHP supports) are a huge pain to do:
$result = $xml->query(
"//playlist/item[(songtitle[contains(translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), '{$word}')]) or (genre[contains(translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), '{$word}')])]"
);
This assumes you've taken your search terms and converted them to lower-case already. For example:
<?php
function searchSong($xpath, ...$words)
{
$return = [];
foreach($words as $word) {
$word = strtolower($word);
$q = "//playlist/item[(songtitle[contains(translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), '{$word}')]) or (genre[contains(translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), '{$word}')])]";
$result = $xpath->query($q);
foreach($result as $node) {
$return[] = $node;
}
}
return $return;
}
In DOM you have another option, you can register PHP functions and use them in Xpath expressions.
So write a function that does the matching logic:
function contentContains($nodes, ...$needles) {
// ICUs transliterator is really convenient,
// lets get one for lowercase and replacing umlauts
$transliterator = \Transliterator::create('Any-Lower; Latin-ASCII');
foreach ($nodes as $node) {
$haystack = $transliterator->transliterate($node->nodeValue);
foreach ($needles as $needle) {
if (FALSE !== strpos($haystack, $needle)) {
return TRUE;
}
}
}
return FALSE;
}
Now you can register it on an DOMXpath instance:
$document = new DOMDocument();
$document->loadXML($xml);
$xpath = new DOMXpath($document);
$xpath->registerNamespace("php", "http://php.net/xpath");
$xpath->registerPHPFunctions(['contentContains']);
$expression = "//item[
php:function('contentContains', songtitle, 'take', 'soul') or
php:function('contentContains', genre, 'take', 'soul')
]";
$result = [];
foreach ($xpath->evaluate($expression) as $node) {
// read values as strings
$result[] = [
'title' => $xpath->evaluate('string(songtitle)', $node),
'gerne' => $xpath->evaluate('string(genre)', $node),
// ...
];
}
var_dump($result);