| Известный вопрос, и известны решения (ссылки на которые есть и здесь) - рекурсия, без рекурсии обходом вправо, NestedSet ... Не было времени, да и лень мешала писать много, теперь пока лень спит, можно и возразить по этому поводу.
Действительно, создаем сложные запросы объединяя их в один, и вроде бы в порядке вещей, но почему-то, если надо получить дерево каталогов, то забываем об этой возможности. А можно ведь получить дерево одним запросом без рекурсий, множества запросов, левых и правых ключей (NestedSet), да и вообще каких либо прочих ключей. Потребуется минимальное - идентификатор записи и ее владелец (родитель), и это все. Для того чтобы это реализовать, нужно сформировать должных образом тело запроса, с соблюдением единственного условия, о котором ниже.
Сразу оговорюсь - будет "тезисное" изложение вопроса на примере фиксированного уровня вложения. Кому надо, домозгуют, если интересно "добить" это, дополнения и решения можно тут добавить.
Чтобы не возвращаться к частям повествования, лучше начать его с описания аргументов функции получения дерева, которую условно назовем tree():
<?
function tree($db, //условимся, что PDO, и это созданное подключение
$tbl, //имя таблицы
$fld, //массив содержащий поля таблицы, значения которых нужно включить в вывод
$lvl, //уровень вложения как величина максимального вложения минус 1 (пояснение показано на максимуме 3)
$srt=false //поля таблицы по которым нужно производить сортировку результата
) {
//Теперь об упомянутом условии - в аргументе $fld, должны быть перечислены "поименно" необходимые поля таблицы,
//при этом первый элемент этого массива должен обязательно быть именем поля идентификатора записи,
//а второй элемент должен обязательно быть именем поля ее владельца (родителя),
//указывать в этом аргументе поля как (*) НЕЛЬЗЯ
//Формируем тело запроса
$second = $fld[0]; //имя поля идентификатора категории
$parent = $fld[1]; //имя поля категории владельца
//Формируем поля запроса и их псевдонимы
//для этого просто будем к именам таблиц и именам полей добавлять число от 0 до $lvl
//реально псевдонимы использоваться будут только в запросе, в дальнейшем они учитываться не будут
$sql = 'SELECT ' . implode(',', array_map(function($n) use($fld) {
foreach($fld as $v) $m[] = 't'.$n.'.`'.$v.'` '.$v.$n;
return implode(',', $m);
}, range(0, $lvl))).',';
//Подключаем к телу запрос к первичной таблице содержащей записи не имеющих родителя - $parent = 0,
//то есть это будут корневые каталоги дерева
$sql = rtrim($sql,',').' FROM '.$tbl.' t0 ';
//Формируем вложенные запросы, добавляя к псевдонимам имен таблиц и именам полей условия выборки число от 0 до $lvl
//таким образом каждое последующее объединение будет выбирать каталог,
//идентификатор родителя которого равен идентификатору предыдущего запроса/объединения
for($i=0; $i<$lvl; $i++) $sql .= 'LEFT JOIN '.$tbl.' t'.($i+1).' ON t'.($i+1).'.`'.$parent.'`=t'.$i.'.`'.$second.'` ';
//Подключаем условия выборки и сортировки, если она определена
//условие выборки одно - получить для первичного запроса все идентификаторы не имеющие родителей, то есть корневых записей
//Какие либо иные условия можно добавить или формировать по условиям передаваемых как аргумент функции,
//но эти индивидуальности можно учесть и потом, в результирующем наборе, по соответствующим полям
//Сортировку можно тоже формировать здесь, но лучше передавать уже готовый набор в функцию,
//ели будет необходимость получать в итоге различные наборы
$sql .= 'WHERE t0.`'.$second.'` IN(SELECT `'.$second.'` FROM '.$tbl.' WHERE `'.$parent.'`=0) '.($srt ? 'ORDER BY '.$srt : null);
//Делаем запрос и обрабатываем результат
if($q = $db->query($sql)) {
if($q->rowCount()) {
//Возвращаемое сформированное дерево
$tree = [];
//Корневой идентификатор
$id = 0;
//Если необходимо хранить идентификаторы вложений (от корня до последнего вложения), для каждого идентификатора,
//например для того, чтобы по ним из результирующего дерева получить путь для навигатора
//или иметь возможность сформировать список каталогов для администрирования, учитывающий уровни их вложения,
//наличие и глубину субкаталогов, для предотвращения переноса каталога, например самого на себя, и т.п.,
//то сохраним эту вложения в этот массив
$dir = [];
//Число полей в запросе
$f = count($fld);
//Удаляем из массива имен полей имена идентификатора записи и владельца,
//если они могут потребоваться в дальнейшем, например для рекурсивного обхода с помощью array_walk_recursive(),
//то оставляем для последующего объединения весь массив имен $fld, а не его срез
$fld = array_slice($fld, 2);
while($r = $q->fetch(PDO::FETCH_NUM)) {
//Разбиваем поля выборки на группы по количеству числа полей запроса
$r = array_chunk($r, $f);
//Удаляем пустые значения выборки у записей с вложениями менее $lvl
$r = array_intersect_key($r, array_diff(array_column($r, 0), [null]));
//Запоминаем идентификаторы вложений
$dir[] = array_column($r, 0);
//Получаем корневой идентификатор
if($id!=$r[0][0]) $id = $r[0][0];
for($i=0, $k=count($r); $i<$k; $i++) {
if(!$r[$i][1]) { //если корневой каталог
//если корневой каталог еще не добавлен в результат, добавить
//результатом ветки дерева будут имена полей запроса ($fld, за вычетом id и родителя), как ключи
//и соответствующие им индексы выборки как значения
if(!array_key_exists($id, $tree)) $tree[$id] = array_combine($fld, array_slice($r[$i], 2))+['level'=>$i];//если нужны будут уровни вложения, то добавляем их
} else if($i+1<$k) { //если не достигли максимального вложения
//если еще не добавлялась категория первого вложения в результат
if(@!array_key_exists($r[$i][0], $tree[$id]['sub'])) $tree[$id]['sub'][$r[$i][0]] = array_combine($fld, array_slice($r[$i], 2))+['level'=>$i];
} else { //достигнут максимальный уровень вложения
//если уровень 1
if($i==1) $tree[$id]['sub'][$r[$i][0]] = array_combine($fld, array_slice($r[$i], 2))+['level'=>$i];
//если последний уровень
else $tree[$id]['sub'][$r[$i][1]]['sub'][$r[$i][0]] = array_combine($fld, array_slice($r[$i], 2))+['level'=>$i];
}
}
}
return [$tree, $dir];
} else return 0; //нет записей
} else return null; //ошибка запроса
}
//Например, получить дерево из таблицы tbl, с именами идентификатора id и владельца prt, включив в результат название (name), число записей (cnt), и отсортировать результат по полю name:
if(!is_bool($tree = tree($db, 'tbl', ['id', 'prt', 'name', 'cnt'], 2, 'name0, name1, name2'))) {
if($tree) {
//формирование нужного
//дерево $tree[0] имеет структуру:
/*
[1] => array(
[name] => value
[cnt] => value
[level] => value
[sub] = array( субкаталог, если есть
....
*/
} else echo 'Нет записей';
} else echo 'Ошибка запроса';
|
| |