Skip to content
Advertisement

Remove Child does not contain attribute value

I am practicing with XML DOM PHP parsing. I have such XML file (shorter version):

<?xml version="1.0" encoding="UTF-8"?>
<tabela_kursow typ="A" uid="21a184">
   <numer_tabeli>184/A/NBP/2021</numer_tabeli>
   <data_publikacji>2021-09-22</data_publikacji>
   <pozycja>
      <nazwa_waluty>bat (Tajlandia)</nazwa_waluty>
      <przelicznik>1</przelicznik>
      <kod_waluty>THB</kod_waluty>
      <kurs_sredni>0,1181</kurs_sredni>
   </pozycja>
   <pozycja>
      <nazwa_waluty id="2">dolar amerykański</nazwa_waluty>
      <przelicznik>1</przelicznik>
      <kod_waluty>USD</kod_waluty>
      <kurs_sredni>3,9460</kurs_sredni>
   </pozycja>
</tabela_kursow>

I created attribute with name “id” and value “2” for two Tag elements.

<?php
$doc = new DOMDocument();
$doc->load("kursy_walut.xml");
$doc->preserveSpace = false;
$doc->formatOutput = true;


$a = $doc->getElementsByTagName("nazwa_waluty");
$b = count($a);
for ($i=0; $i<$b; $i++){
    $c = $a[$i]->childNodes[0]->nodeValue;
    if ($c === 'dolar amerykański'){
        $a[$i]->setAttribute('id', '2');
    }
    if ($c === 'euro'){
        $a[$i]->setAttribute('id', '2');
    }
}
$doc->save('output.xml');

Now I would like to:

  1. Get only Tag elements which did not contain attribute ‘id=”2″‘;
  2. Get only Tag elements which contain only ‘id=”2″‘.

For first case I created such code. Don’t know exactly how to create second case.

$doc3 = new DOMDocument();
$doc3->preserveWhiteSpace = false;
$doc3->load('output.xml');
$xpath2 = new DOMXPath($doc3);
$id3 = $xpath2->query('//*/pozycja/nazwa_waluty[@id="2"]');
foreach($id3 as $attr){
    $attr->parentNode->removeChild($attr);
}
$doc3->formatOutput = true;
$doc3->save('output3.xml');

Maybe someone can help me with second case? Thank you.

Advertisement

Answer

If I understand you source then you would like to add an id depending on the currency string. However your are using a language specific string. The language code would be a better option. It is defined and unique already.

DOMNode::getElementsByTagName() returns a node list, you could use foreach to iterate it. However the node list is “LIVE”. It reacts to changes on the nodes. This is an issue if you add/remove nodes. Using Xpath expressions avoids this and allows for more specific fetches.

More important, take a look at the DOMNode::$textContent property. It reads/writes the whole text inside of a node. For elements this includes any descendant text node.

Using this you can simplify the code:

$document = new DOMDocument();
// configure the parser - need to be set before load
$document->preserveWhiteSpace = false;
$document->loadXML($xml);
$xpath = new DOMXpath($document);

$currencyMap = [
    "USD" => 2,
    "EUR" => 4
];

// iterate the "nazwa_waluty" elements
foreach ($xpath->evaluate('//pozycja/kod_waluty') as $kodWaluty) {
    // get the currency code
    $currencyCode = trim($kodWaluty->textContent);
    if (isset($currencyMap[$currencyCode])) {
        // add id for currency
        $kodWaluty->setAttribute('id', $currencyMap[$currencyCode]);
    }
}

// configure the serializer - need to be set before save
$document->formatOutput = true;
echo $document->saveXML();

Output:

<?xml version="1.0" encoding="UTF-8"?>
<tabela_kursow typ="A" uid="21a184">
  <numer_tabeli>184/A/NBP/2021</numer_tabeli>
  <data_publikacji>2021-09-22</data_publikacji>
  <pozycja>
    <nazwa_waluty>bat (Tajlandia)</nazwa_waluty>
    <przelicznik>1</przelicznik>
    <kod_waluty>THB</kod_waluty>
    <kurs_sredni>0,1181</kurs_sredni>
  </pozycja>
  <pozycja>
    <nazwa_waluty>dolar amerykański</nazwa_waluty>
    <przelicznik>1</przelicznik>
    <kod_waluty id="2">USD</kod_waluty>
    <kurs_sredni>3,9460</kurs_sredni>
  </pozycja>
</tabela_kursow>

Now to fetch the data with an specific id. I would expect that you would want the pozycja elements and its children.

DOMXpath::evaluate() can not just return node lists, but scalar values. It is not that much code:

foreach ($xpath->evaluate('//pozycja[kod_waluty/@id = "2"]') as $pozycja) {
    var_dump(
        [
            'przelicznik' => $xpath->evaluate('string(przelicznik)', $pozycja),
            'kurs_sredni' => $xpath->evaluate('string(kurs_sredni)', $pozycja),
        ]
    );
}

Output:

array(2) {
  ["przelicznik"]=>
  string(1) "1"
  ["kurs_sredni"]=>
  string(6) "3,9460"
}

However because the language code is already defined and unique, you could use it directly:

foreach ($xpath->evaluate('//pozycja[normalize-space(kod_waluty) = "USD"]') as $pozycja) {
    var_dump(
        [
            'przelicznik' => $xpath->evaluate('string(przelicznik)', $pozycja),
            'kurs_sredni' => $xpath->evaluate('string(kurs_sredni)', $pozycja),
        ]
    );
}

And if you need the “kurs_serdni” content for a specific currency code then you can do this with a single Xpath expression:

var_dump(
    $xpath->evaluate(
        'string(//pozycja[normalize-space(kod_waluty) = "USD"]/kurs_sredni)'
    )
);

Output:

string(6) "3,9460"
Advertisement