Как отфильтровать дублирующие поля записей, выгруженные из БД MySQL для построения DOMDocument с целью последующего сохранения в XML-файл?

Добрый день!
Информация из БД приходит в следующем формате (дублируются товары по ID):
Array
(
    [id] => 517
    [Код] => 201
    [Название] => Бумага А4
    [Тип] => Базовая
    [Цена] => 11.50
    [Свойства] => Плотность
    [значение свойства] => 100
    [Раздел] => Бумага
)
Array
(
    [id] => 517
    [Код] => 201
    [Название] => Бумага А4
    [Тип] => Москва
    [Цена] => 12.50
    [Свойства] => Плотность
    [значение свойства] => 100
    [Раздел] => Бумага
)
Array
(
    [id] => 517
    [Код] => 201
    [Название] => Бумага А4
    [Тип] => Базовая
    [Цена] => 11.50
    [Свойства] => Белизна ЕдИзм=%
    [значение свойства] => 100
    [Раздел] => Бумага
)
Array
(
    [id] => 517
    [Код] => 201
    [Название] => Бумага А4
    [Тип] => Москва
    [Цена] => 12.50
    [Свойства] => Белизна ЕдИзм=%
    [значение свойства] => 100
    [Раздел] => Бумага
)
Array
(
    [id] => 518
…


Пытаюсь собрать эту информацию в DOMDocument для последующего сохранения в XML-файл, но не получается отфильтровать дублирующие поля записей.
Результат, которого удалось добиться:
<?xml version="1.0" encoding="windows-1251"?>
<Товары>
  <Товар Код="201" Название="Бумага А4">
    <Цена Тип="Базовая">11.50</Цена>
    <Цена Тип="Москва">12.50</Цена>
    <Цена Тип="Базовая">11.50</Цена>
    <Цена Тип="Москва">12.50</Цена>
  </Товар>
  <Товар Код="202" Название="Бумага А3">
    <Цена Тип="Базовая">18.50</Цена>
    <Цена Тип="Москва">22.50</Цена>
    <Цена Тип="Базовая">18.50</Цена>
    <Цена Тип="Москва">22.50</Цена>
  </Товар>
…
</Товары>


То есть цель – сохранить под каждым ID = узел <Товар Код="…" Название="…" >…Товар > весь перечень его цен и свойств, а не дублировать узлы с одинаковым ID (Код). Ожидаемая структура на выходе:
<?xml version="1.0" encoding="windows-1251"?>
<Товары>
	<Товар Код="201" Название="Бумага А4">
		<Цена Тип="Базовая">11.50</Цена>
		<Цена Тип="Москва">12.50</Цена>
		<Свойства>
			<Плотность>100</Плотность>
			<Белизна ЕдИзм="%">150</Белизна>
		</Свойства>
		<Разделы>
			<Раздел>Бумага</Раздел>
		</Разделы>
	</Товар>
	<Товар Код="202" Название="Бумага А3">
		<Цена Тип="Базовая">18.50</Цена>
		<Цена Тип="Москва">22.50</Цена>
		<Свойства>
			<Плотность>90</Плотность>
			<Белизна ЕдИзм="%">100</Белизна>
		</Свойства>
		<Разделы>
			<Раздел>Бумага</Раздел>
		</Разделы>
	</Товар>
…
</Товары>


Идея реализации состояла в следующем:
1) создание узла <Товар>
2) сохранение каждой полученной из БД записи в массив записей $rows[], а каждого созданного узла <Товар> ($node) в массив $elements[]
3) и последующей проверки при добавлении узлов и атрибутов к созданному узлу на дубли в массиве записей $rows[] (пояснение – сравнение проводится в цикле, то есть в массив записей $rows[] в этот момент загружена только текущая запись и предыдущие (ранее загруженные)).

Для упрощения задачи пока закомментировал все составляющие узла кроме
<Цена Тип="…">…Цена>
Прошу проконсультировать по проблеме реализации в п.3 – проверке и отфильтровывании дублирующих полей при сохранении информации о товаре в узел DOMDocument (в приложенном коде начинается сразу в теле while($row = $result->fetch_assoc()) )
Буду также признателен в случае более комплексного и элегантного решения.
Используемый код:
<?php
//Подключение к серверу БД
…
} else {
	$result = mysqli_query(
	$connect,
	"SELECT a_product.id, a_product.код AS `Код`, a_product.название AS `Название`, a_price.`тип цены` AS `Тип`, a_price.`цена` AS `Цена`, a_property.`свойство` AS `Свойства`, a_property.`значение свойства`, a_category.`название`AS `Раздел`
	FROM a_product 
	INNER JOIN a_price ON название = a_price.товар
	INNER JOIN a_property ON a_product.название = a_property.товар
	INNER JOIN a_category ON a_product.код = a_category.код
	;");
};

//Создание DOM-документа
$dom = new DOMDocument('1.0', 'windows-1251'); //создал дескриптор на новый DOM-документ (объект)
$dom->formatOutput = True; //Форматирует вывод, добавляя отступы и дополнительные пробелы. Не работает, если документ был загружен с включённым параметром preserveWhitespace. https://www.php.net/manual/ru/class.domdocument.php

//Создание корневого элемента (узла) <ТОВАРЫ>
$root = $dom->createElement("Товары"); //создаем корневой элемент
$dom->appendChild($root); //присоединяем элемент к корню документа				

//===================================================================================
		//Функция создания элементов/узлов второго уровня (дочерние по отношению к <Товар>)
		//(на входе: $dom, $node, $child, Имя узла ($tempNameElement), Текстовое содержимое узла ($tempCreate = NULL), Имя атрибута ($tempSetNameAttribute = NULL), Значение атрибута ($tempSetAttribute = NULL))
		function addChild2($dom, $node, $tempNameElement, $tempCreate = NULL, $tempSetNameAttribute = NULL, $tempSetAttribute = NULL)
		{
			$child = $dom->createElement($tempNameElement); //создали <элемент>
			$node->appendChild($child); //добавили <элемент> к элементу <Товар> в качестве дочернего
			
			if($tempCreate !== NULL) {
				$child->appendChild($dom->createTextNode($tempCreate)); //(вывели текст в элементе <>...</>) создали дочерний Текстовый узел для текущего узла.
			}
			
			if($tempSetNameAttribute != NULL && $tempSetAttribute != NULL) {
				$child->setAttribute($tempSetNameAttribute,$tempSetAttribute); //присваиваем атрибут "тип цены" элементу <Цена>
			}
			
			return $child;
		};
//===================================================================================

//===================================================================================
		//Функция создания элементов/узлов третьего уровня 
		//(на входе: $dom, $node, $child, Имя узла ($tempNameElement), Текстовое содержимое узла ($tempCreate = NULL), Имя атрибута ($tempSetNameAttribute = NULL), Значение атрибута ($tempSetAttribute = NULL))
		function addChild3($dom, $node, $child, $tempNameElement, $tempCreate = NULL, $tempSetNameAttribute = NULL, $tempSetAttribute = NULL)
		{
			$child2 = $dom->createElement($tempNameElement); //создали <элемент>
			$child->appendChild($child2); //добавили <элемент> к элементу <...> в качестве дочернего
			
			if($tempCreate !== NULL) {
			$child2->appendChild($dom->createTextNode($tempCreate)); //(вывели значение элемента в теге <>...</>) создали дочерний Текстовый узел для текущего узла В КОНЕЦ СПИСКА.
			}
			
			if($tempSetNameAttribute != NULL && $tempSetAttribute != NULL) {
			$child2->setAttribute($tempSetNameAttribute,$tempSetAttribute); //присваиваем атрибут (наименование="значение") текущему дочернему элементу
			}
			
			return $child2;
		};
//=========================================================================================================
		
	//ЛОГИКА. Получаем записи из БД MySQL и формируем из них дерево DOM
		$i = 0; //инициализация индекса массива $rows[$i] (полученных из БД MySQL записей)
		
		$elements = []; //Массив $node (узлов DOM)
		$rows = []; //Массив полученных из БД MySQL записей

while($row = $result->fetch_assoc()) { //получение записей из таблицы 'a_product' БД MySQL

																					//ДЛЯ ОТЛАДКИ - УДАЛИТЬ !!!
																					echo '<pre>';
																					print_r($row);
																					echo '</pre>';
				
	//Если такой ID пока не сохранен в массиве узлов $elements[] (избавляемся от дублей по ID)
	if(FALSE === ($key = array_search($row['id'], array_column($rows, 'id')))) {	//$key - индекс строки в массиве записей из БД MySQL ($rows[]) проверяем на FALSE, потому что array_search возвращает FALSE или ключ найденного элемента
																					
		$rows[$i] = $row; 															//массив записей из БД MySQL
		
		//создаем элементы <Товар> (получаем наименование для элемента <Товар> из функци fetch_assoc() в вышестоящем цикле while пока не закончатся записи из БД)
		$node = $dom->createElement('Товар'); //создаем элемент <Товар> (дочерний элемент корневого элемента "Товары")
		$root->appendChild($node); //и добавляем его в корневой элемент <Товары>

		$elements[$i] = $node; 														//Массив узлов
		
		//Атрибуты <ТОВАР>
		$elements[$i]->setAttribute("Код", $rows[$i]['Код']); //присваиваем атрибуты 'код' и 'название' элементу <Товар>
		$elements[$i]->setAttribute("Название", $rows[$i]['Название']); //присваиваем атрибуты 'код' и 'название' элементу <Товар>

		//<ЦЕНА> - узел 1 уровня
		addChild2($dom, $elements[$i], 'Цена', $rows[$i]['Цена'], 'Тип', $rows[$i]['Тип']);
		
		$i++; //увеличиваем индекс
		
	}else{	//если <ТОВАР> есть в массиве записей из БД MySQL ($rows[]) (и, соответственно, есть $key с ключом его ID, а также есть соответствующий в массиве узлов ($elements[]))- отсеиваем дубли по атрибутам и проводим запись

		//<ЦЕНА> - узел 1 уровня
		foreach ($elements[$key]->childNodes as $childN) {	//Перебираем в массиве узлов ($elements[]) детей найденого в массиве записей из БД MySQL ($rows[]) узла <ТОВАР>

			if($childN->nodeName == 'Цена') {	//Если атрибут тега <ЦЕНА> не совпадает с атрибутом текущей (проверяемой) записи из БД MySQL ($rows[])
				if($childN->getAttribute('Тип') == $rows[$i]['Тип']) {
					$addPrice = FALSE;
					break;	
				} else {
					$addPrice = TRUE;
				}
			}
			
		}
			
			//Результат проверки == FALSE или TRUE
		if($addPrice === TRUE) {
			addChild2($dom, $elements[$key], 'Цена', $row['Цена'], 'Тип', $row['Тип']); // то записываем в найденный в массиве узлов ($elements[]) узел <ТОВАР> - новый узел <ЦЕНА>
		}
		//}
	}
};
	$dom->save( 'target.xml' ); //сохранение объекта (DOM-документа) в файл.
…
  • Вопрос задан
  • 79 просмотров
Решения вопроса 1
@VadimFox
В предложенный вами код особо не вчитывался, но предложу эту реализацию:
Код


<?php
$inputData = 'SELECT ....'; //Получаем данные из БД

$outputData = [];
foreach ($inputData as $row){//Собираем данные в нужном формате
    if(!isset($outputData[ $row['Код'] ])){//Собираем основыне данные
        $outputData[ $row['Код'] ]['Свойства'] = $row['Свойства'];
        $outputData[ $row['Код'] ]['Раздел'] = $row['Раздел'];
    }

    $outputData[ $row['Код'] ]['Цены'][] = [//Собираем цены
        'Цена' => $row['Цена'],
        'Тип' => $row['Тип']
    ];
}
unset($inputData);//Удаляем оригинал данных

//По итогу перебираем $outputData и формируем из имеющегося xml
echo '<Товары>';
foreach ($outputData as $key => $row){//Тут уже под свои нужды пишем =)
    echo '<Товар Код="'.$key.'">';

    foreach ($row as $rowInner){
        echo '<Цена Тип="'.$rowInner['Тип'].'">'.$rowInner['Цена'].'</Цена>';
    }
    
    /*
     * ...
     * ...
     */
    
    echo '</Товар>';
}
echo '</Товары>';




В общем суть в том, что, проще сначала привести в порядок данные, сформировать их в более приятный для работы вид и уже потом проводить какие-то манипуляции
Ответ написан
Пригласить эксперта
Ответы на вопрос 1
@mobcomm Автор вопроса
Итоговое решение в результате доработки исходного кода:
//Создание DOM-документа
$dom = new DOMDocument('1.0', 'windows-1251'); 
$dom->formatOutput = True; 
//Создание корневого элемента <ТОВАРЫ>
$root = $dom->createElement("Товары"); 
$dom->appendChild($root); 

//=========================================================================================================
		//Функция создания элементов/узлов второго уровня (дочерние по отношению к <Товар>)
		function addChild2($dom, $node, $tempNameElement, $tempCreate = NULL, $tempSetNameAttribute = NULL, $tempSetAttribute = NULL)
		{
			$child = $dom->createElement($tempNameElement); //создали <элемент>
			$node->appendChild($child); //добавили <элемент> к элементу <Товар> в качестве дочернего
			
			if($tempCreate !== NULL) {
				$child->appendChild($dom->createTextNode($tempCreate)); //(вывели текст в элементе <>...</>) создали дочерний Текстовый узел для текущего узла.
			}
			
			if($tempSetNameAttribute != NULL && $tempSetAttribute != NULL) {
				$child->setAttribute($tempSetNameAttribute,$tempSetAttribute); //присваиваем атрибут "тип цены" элементу <Цена>
			}
			
			return $child;
		};
//=========================================================================================================

		//Функция создания элементов/узлов третьего уровня 
		function addChild3($dom, $node, $child, $tempNameElement, $tempCreate = NULL, $tempSetNameAttribute = NULL, $tempSetAttribute = NULL)
		{
			$child2 = $dom->createElement($tempNameElement); //создали <элемент>
			$child->appendChild($child2); //добавили <элемент> к элементу <...> в качестве дочернего
			
			if($tempCreate !== NULL) {
			$child2->appendChild($dom->createTextNode($tempCreate)); //вывели значение элемента в теге <>...</>
			}
			
			if($tempSetNameAttribute != NULL && $tempSetAttribute != NULL) {
			$child2->setAttribute($tempSetNameAttribute,$tempSetAttribute); //присваиваем атрибут (наименование="значение") текущему дочернему элементу
			}
			
			return $child2;
		};
//=========================================================================================================

//Функция проверки элементов на дубли
function checkDuplicate2($childKey, $checkMassive, $row)
{
	//Перебираем в массиве узлов ($elements[$key]) детей найденного в массиве записей из БД MySQL ($rows[]) узла <ТОВАР>
	foreach($checkMassive->childNodes as $childN) { //на каждой итерации цикла проверяем один очередной дочерний узел узла <Товар> (например: Товар->Цена или Товар->Свойства или Товар->Разделы)

		//Проверка на дубли по наименованию узла
			switch ($childKey) {
				case 'Цена':
					if($childN->getAttribute('Тип') == $row['Тип']) { //проверяем атрибут 'Тип' узла <Цена>
						$add = FALSE; //Индикатор записи (если записывали в массив ранее)
						break 2;	//выходим из switch и из foreach. 
					} else {
						$add = TRUE;
						break; //выходим только из switch
					}

				case 'Свойства':
					if('Свойства' == $childN->nodeName) {
						$add = FALSE; //Индикатор записи (если записывали в массив ранее)
						break 2;	//выходим из switch и из foreach. 
					} else {
						$add = TRUE;
						break; //выходим только из switch
					}

				//case 'Разделы':
				case 'Раздел':
					if('Разделы' == $childN->nodeName) {
						$add = FALSE; //Индикатор записи (если записывали в массив ранее)
						break 2;	//выходим из switch и из foreach.
					} else {
						$add = TRUE;
						break; //выходим только из switch (так как требуется продолжать проверку по всем детям текущего узла, пока не будет совпадение или конец списка)
					}
			}
	}
	return $add;
}

//=========================================================================================================

//Функция проверки элементов 3го уровня на дубли
function checkDuplicate3($childKey, $childValue, $checkMassive, $row)
{
	//Перебираем в массиве узлов ($elements[$key]) детей найденного в массиве записей из БД MySQL ($rows[]) узла <ТОВАР>
	foreach($checkMassive->childNodes as $childN) { //на каждой итерации цикла проверяем один очередной дочерний узел узла <Товар> (например: Товар->Цена)

		foreach($childN->childNodes as $childValue3) { //<Разделы> -> <Раздел> или...

			switch ($childKey) {
				case 'Раздел':
					if($childValue == $childValue3->nodeValue) { //$childValue3->nodeValue -это значение (содержимое) узла <Раздел>
						$add = FALSE; //Индикатор записи (если записывали в массив ранее)
						break 3;	//выходим из switch и из обоих foreach.
					} else {
						$add = TRUE;
						break; //выходим только из switch (так как требуется продолжать проверку по всем детям текущего узла, пока не будет совпадение или конец списка)
					}

				case 'Свойства':
					if($childValue == $childValue3->nodeName && $row['значение свойства'] == $childValue3->nodeValue) { //$childValue3->nodeName -это наименование (ключ) дочернего узла <Свойства>, а $childValue3->nodeValue -значение (содержимое) этого дочернего узла
						$add = FALSE;
						break 3;	//выходим из switch и из обоих foreach.
					} else {
						$add = TRUE;
						break; //выходим только из switch
					}
			}
		}
	}
	return $add;
}

//=========================================================================================================


	//ЛОГИКА. Получаем записи из таблиц и формируем из них дерево DOM
		$i = 0; //инициализация индекса Массива $rows[$i] (полученных из БД MySQL записей)
		$elements = []; //Массив $node (узлов DOM)
		$rows = []; //Массив полученных из БД MySQL записей

while($row = $result->fetch_assoc()) { //получение записей из таблиц БД MySQL
	//Если такой ID пока не сохранён в Массиве записей из БД MySQL (избавляемся от дублей по ID)
	if(FALSE === ($key = array_search($row['id'], array_column($rows, 'id')))) {	//$key - индекс строки в Массиве записей из БД MySQL ($rows[])
																					
		$rows[$i] = $row; //Массив записей из БД MySQL
		
		//Создаем элемент <Товар>
		$node = $dom->createElement('Товар');
		$root->appendChild($node);

		$elements[$i] = $node; //Массив узлов
		
		//Получаем наименования для дочерних узлов узла <Товар> из функции fetch_assoc()
		
		//АТРИБУТЫ <ТОВАРА>
		$elements[$i]->setAttribute("Код", $rows[$i]['Код']); //присваиваем атрибуты 'код' и 'название' элементу <Товар>
		$elements[$i]->setAttribute("Название", $rows[$i]['Название']);

		//<ЦЕНА> - узел 1 уровня
		addChild2($dom, $elements[$i], 'Цена', $row['Цена'], 'Тип', $row['Тип']);		
		
		$i++; //увеличиваем индекс
		
	}else{	//Если <ТОВАР> есть в Массиве записей из БД MySQL ($rows[]) (и, соответственно, есть $key с ключом его ID, а также есть соответствующий в Массиве узлов ($elements[]))
		foreach ($row as $childKey => $childValue) { //на каждой итерации цикла проверяем один очередной элемент записи из БД MySQL (например: $row['Цена'] или $row['Свойства'] или $row['Раздел'])

			$add = checkDuplicate2($childKey, $elements[$key], $row);
			
			//Результат проверки == FALSE или TRUE для узлов 2-го уровня(<ЦЕНА> или <Свойства> или <Раздел>)
			if($add === TRUE) {

				if($childKey == 'Цена') {
					addChild2($dom, $elements[$key], $childKey, $childValue, 'Тип', $row['Тип']); //то записываем в найденный в Массиве узлов ($elements[]) узел <ТОВАР> - новый узел <ЦЕНА>
				}

				if($childKey == 'Раздел') {
					$childR = addChild2($dom, $elements[$key], 'Разделы');
				}
				
				if($childKey == 'Свойства') {
					$childP = addChild2($dom, $elements[$key], 'Свойства');
				}
			}

			//Результат проверки == FALSE или TRUE для узлов 3-го уровня(<Плотность> или <Белизна> или <Формат> или <Тип> или <Раздел>)
			if($childKey == 'Раздел') {
				$add3 = checkDuplicate3($childKey, $childValue, $elements[$key], $row);
				
				if($add3 === TRUE) {
					$child2 = addChild3($dom, $elements[$key], $childR, $childKey, $childValue);
				}
			}

			if($childKey == 'Свойства') {
				$add4 = checkDuplicate3($childKey, $childValue, $elements[$key], $row);

				if($add4 === TRUE) {
					$child2 = addChild3($dom, $elements[$key], $childP, $childValue, $row['значение свойства']);
				}
			}
		} //end foreach
	}
};
foreach ($root->childNodes as $q) {
	if ($q->lastChild->nodeName == $q->firstChild->nodeName) {
		//public DOMNode::replaceChild ( DOMNode $node , DOMNode $child ) : DOMNode|false. Возвращает старый узел или false в случае возникновения ошибки.
		$w = $q->replaceChild($q->lastChild, $q->childNodes[1]); //функция DOMNode::replaceChild заменяет дочерний узел child новым узлом. 
		$q->appendChild($w);
		$w = $q->replaceChild($q->lastChild, $q->childNodes[2]);
		$q->appendChild($w);
	}
}
	$dom->save( 'target.xml' ); //сохранение объекта (DOM-документа) в файл.

//Закрываем подключение к серверу БД
mysqli_close($connect);


Полученный результат:
<?xml version="1.0" encoding="windows-1251"?>
<Товары>
  <Товар Код="201" Название="Бумага А4">
    <Цена Тип="Базовая">11.50</Цена>
    <Цена Тип="Москва">12.50</Цена>
    <Свойства>
      <Плотность>100</Плотность>
      <Белизна>100</Белизна>
    </Свойства>
    <Разделы>
      <Раздел>Бумага</Раздел>
    </Разделы>
  </Товар>
  <Товар Код="302" Название="Принтер Canon">
    <Цена Тип="Базовая">3010.00</Цена>
    <Цена Тип="Москва">3500.00</Цена>
    <Свойства>
      <Тип>Лазерный</Тип>
      <Формат>A4</Формат>
      <Формат>A3</Формат>
    </Свойства>
    <Разделы>
      <Раздел>МФУ</Раздел>
      <Раздел>Принтеры</Раздел>
    </Разделы>
  </Товар>
  ...
</Товары>
Ответ написан
Комментировать
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Войти через центр авторизации
Похожие вопросы