PHP рекурсия


Сегодня я бы хотел поговорить на тему рекурсии в  PHP.

Не одна сотня начинающих PHP программистов ломала голову над показами многоуровневых структур каталогов. Такие системы часто используются на большинстве сайтов имеющих меню. Пример можно посмотреть на рисунке.

 

На данном рисунке она представлена в виде иерархии директорий. Разработку подобной системы каталогов на сервере я описал в статье о сканировании дирректорий сервера.  Ещё можно её представить в следующем виде:

На этом рисунке показано меню интернет магазина, сформированное все тем-же методом рекурсии.

Сейчас мы более подробно поговорим о том, как делается такое меню для сайта.

Работа с рекурсиями только на первый взгляд кажется сложной. На самом деле, все намного проще чем кажется, нужно только помнить несколько правил:

  1. Всегда необходимо предусматривать условие для выхода из рекурсии. Иначе ваш сервер зависнет. А это не из самых приятных последствий.
  2. Не нужно её усложнять, наполнять её условиями, проверками и т.д. Это необходимо продумать заранее, т.к. рекурсия - это ресурсоёмкая процедура.
  3. Необходимо четко понимать в голове что делает программа в определённый момент времени.

Теперь давайте создадим тестовую базу данных для нашего выдуманного интернет магазина. Для этого выполним следующий запрос:

CREATE TABLE  `tree` (

`id` INT UNSIGNED NOT NULL AUTO_INCREMENT ,
`pid` INT DEFAULT  '0' NOT NULL ,
`name` VARCHAR( 255 ) NOT NULL ,
PRIMARY KEY (  `id` )
)

Сразу после создания таблицы, выполним следующий запрос, который наполнит её данными для примера работы рекурсии в php:

INSERT INTO `tree` VALUES (1, 0, 'Категории');
INSERT INTO `tree` VALUES (2, 1, 'Подкатегория 1');
INSERT INTO `tree` VALUES (3, 1, 'Подкатегория 2');
INSERT INTO `tree` VALUES (4, 1, 'Подкатегория 3');
INSERT INTO `tree` VALUES (6, 3, 'Подкатегория 2.1');
INSERT INTO `tree` VALUES (7, 3, 'Подкатегория 2.2');
INSERT INTO `tree` VALUES (8, 3, 'Подкатегория 2.3');
INSERT INTO `tree` VALUES (9, 3, 'Подкатегория 2.4');
INSERT INTO `tree` VALUES (10, 7, 'Подкатегория 2.2.1');
INSERT INTO `tree` VALUES (11, 7, 'Подкатегория 2.2.2');
INSERT INTO `tree` VALUES (12, 7, 'Подкатегория 2.2.3');
INSERT INTO `tree` VALUES (13, 7, 'Подкатегория 2.2.4');

Сейчас разберём получившуюся таблицу, заодно поговорим о схеме работы подобных систем рекурсий.

В результате мы получили таблицу, которая показана на следующем рисунке:

Подобная структура таблицы - минимальный пример для разбора работы рекурсии в php.

Начнём по порядку.

В таблице находится список рубрик (так будем называть пункты меню на сайте). Что где должно в результате находиться, понятно из названия рубрик (за категорией 2 следует подкатегория 2.1 и т.д.). 

Поле "id" - это уникальный идентификатор каждой рубрики. Он нам нужен для того, что-бы мы могли сделать этот пункт полноценной ссылкой (например для списка товаров, но это будет дальше). Именно для контроля его уникальности, при создании этого поля ему был задан параметр "AUTO_INCREMENT". Он включает автоматическое увеличение этого ключа на единицу (инкрементирование).

Теперь "гвоздь программы" - поле "pid".

Данная аббревиатура PID - взята из сокращения "parent ID". Многие наверное уже догадались что она значит.

Для остальных поясню:

Для использования рекурсии выбран метод "родителей". Каждая запись имеет своего родителя. Именно поле "pid" указывает на ID родителя. Если-же "pid" равен нулю, это означает что у элемента нет родителя, и он находится в корне "дерева рубрик". Вообще-то можно брать не ноль, а что угодно, но 0 будет удобнее, да и вообще так принято в рядах программистов... Теперь в таблице можно строить иерархические деревья рубрик с бесконечным (сколько позволит вам ваша база данных) количеством уровней. Для правильной рекурсии это не проблема.

С базой данных надеюсь понятно, если нет, можете задать вопрос в комментах.

Теперь перейдем к самому интересному - программирование рекурсии на php.

Начнем с цитаты определения рекурсии на википедии: 

Рекурсия — процесс повторения элементов самоподобным образом.

Именно этим и займемся. Напишем функцию которая будет "самоповторяться".

<?
function menu() {
   // какие-то действия
   // ...
   menu();
}
?>

Вот как-то так...

Теперь сделаем следующее: передадим в функцию параметр, который будет указывать на наш "корень" структуры (в нашем случае это ноль). Это даст нам возможность в будущем отображать дерево начиная с любого узла. После этого покажем все "дочерние" узлы указанного параметра (который мы отправляем в функцию).

Выводить все на экран будем в списке "ul", хотя это каждый делает как хочет, но чаще всего на сайтах встречается именно этот вариант.

<?
menu(0);

function menu($id)
{
    $res = mysql_query(" SELECT * FROM tree WHERE pid = '$id' ");
    if (mysql_numrows($res)) {
        echo '<ul>';
        while ($dat = mysql_fetch_array($res)) {
            echo '<li>' . $dat['name'] . '</li>';
            menu();
        }
        echo '</ul>';
    }
}
?>

Теперь вся "фишка" задумки данной рекурсии: параметром для следующего шага мы указываем не ноль, как при старте, а ID следующего элемента. В результате получаем вот что:

<?
menu(0);

function menu($id)
{
    $res = mysql_query(" SELECT * FROM tree WHERE pid = '$id' ");
    if (mysql_numrows($res)) {
        echo '<ul>';
        while ($dat = mysql_fetch_array($res)) {
            echo '<li>' . $dat['name'] . '</li>';
            menu($dat['id']);
        }
        echo '</ul>';
    }
}
?>

Вот мы и получили в результате полное меню в правильной структуре. Результат я показал на рисунке:

Теперь чутка доработаем его чтобы получить ссылки вместо простых пунктов. Сразу отмечу, что ссылки я напишу выдуманные. Вы-же можете написать какие вам угодно.

Ещё хочу написать небольшой мой вариант того, как можно оформлять CSS выделениями весь путь до выбранного пункта. С этой проблемой тоже часто сталкиваются начинающие программисты. Приправлю всё это дело простым CSS стилем

<style>
.active a {font-weight:bold; text-decoration:none; color:red;}
</style>
<?
$array_with_path_id[] = 0;
if (!empty($_GET['id'])) {
    $current_id = (int)$_GET['id'];
    while ($current_id != 0) {
        $res = mysql_query(" SELECT * FROM tree WHERE id = '$current_id' ");
        $dat = mysql_fetch_array($res);
        $array_with_path_id[] = $dat['id'];
        $current_id = $dat['pid'];
    }
}


menu(0, $array_with_path_id);

function menu($id, $array_with_path_id)
{
    $res = mysql_query(" SELECT * FROM tree WHERE pid = '$id' ");
    if (mysql_numrows($res)) {
        echo '<ul>';
        while ($dat = mysql_fetch_array($res)) {

            if (in_array($dat['id'], $array_with_path_id))
                $class = ' class="active" ';
            else
                $class = '';


            echo '<li ' . $class . '><a href="?id=' . $dat['id'] . '">' . $dat['name'] .
                '</a></li>';
            menu($dat['id'], $array_with_path_id);
        }
        echo '</ul>';
    }
}
?>

Здесь перед выводом на экран дерева рубрик, я сначала формирую массив со списком всех id, находящихся в пути от корня списка (нулевой элемент) до необходимого id (который передан параметром $_GET['id']). Делается это спомощью цикла while(). Тут думаю ничего сложного нету.

Затем перед выводом на экран li, я проверяю наличие текущего id в массиве (он кстати передается параметром в функцию)подготовленном нами заранее. Если он там есть, то переменной $class задаю строку содержащую класс active. Иначе пустую строку. Этот класс вставляется в li. Если посмотреть внимательнее код, это можно заметить.

В результате вот что мы получили:

Теперь видно полный путь до текущего элемента. Но я пойду дальше и усовершенствую скрипт таким образом, чтобы ненужные пункты скрывались. Сделаю я это на яваскрипте (JQuery). Этот способ я считаю самый приемлемый, т.к. ссылки все равно проиндексируется поисковиком, но пользователю они не будут показываться.

Для этого я "немного" модифицировал нашу функцию.

<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.0/jquery.min.js"></script>
<script>
$(document).ready(function() {
	$('#main_ul ul').each(function(i) { // Check each submenu:

		if ($(this).attr("class").indexOf('active')) 
			{$(this).hide().prev().removeClass('expanded').addClass('collapsed'); }
		$(this).prev().addClass('collapsible').click(function() { // Attach an event listener
			if ($(this).next().css('display') == 'none') {
				$(this).next().slideDown(200, function () { // Show submenu:
					$(this).prev().removeClass('collapsed').addClass('expanded');
				});
			}else {
				$(this).next().slideUp(200, function () { // Hide submenu:
					$(this).prev().removeClass('expanded').addClass('collapsed');
					$(this).find('ul').each(function() {
						$(this).hide().prev().removeClass('expanded').addClass('collapsed');
					});
				});
			}
		return false; // Prohibit the browser to follow the link address
		});

	});
});
</script>

<style>
a.active {font-weight:bold; text-decoration:none; color:red;}
</style>
<?
$array_with_path_id[] = 0;
if (!empty($_GET['id'])) {
    $current_id = (int)$_GET['id'];
    while ($current_id != 0) {
        $res = mysql_query(" SELECT * FROM tree WHERE id = '$current_id' ");
        $dat = mysql_fetch_array($res);
        $array_with_path_id[] = $dat['id'];
        $current_id = $dat['pid'];
    }
}


menu(0, $array_with_path_id, '');

function menu($id, $array_with_path_id, $class)
{
    $res = mysql_query(" SELECT * FROM tree WHERE pid = '$id' ");
    if (mysql_numrows($res)) {
        if ($id == 0)
            echo '<ul id="main_ul" ' . $class . '>';
        else
            echo '<ul ' . $class . '>';
        while ($dat = mysql_fetch_array($res)) {

            if (in_array($dat['id'], $array_with_path_id))
                $class = ' class="active" ';
            else
                $class = ' class=" " ';

            echo '<li><a ' . $class . ' href="?id=' . $dat['id'] . '">' . $dat['name'] .
                '</a></li>';
            menu($dat['id'], $array_with_path_id, $class);
        }
        echo '</ul>';
    }
}
?>

Если вы внимательно посмотрите, то увидите изменения:

  1. Ввел ещё одну проверку на первый ul
  2. Подключил JQuery из библиотеки гугла
  3. Вставил функцию сворачивающую все ul не имеющие класс active
  4. Передаю в параметре к функции класс для определения текущего в структуре
  5. Остальное по мелочам. Можете сами их увидеть в функции

Код функции на JQuery стоит повнимательнее посмотреть. Он не особо сложный, но зато подойдет практически к любой структуре меню построенной на UL. Плюс, как я уже написал выше, он легко индексируется поисковиком.

Последний код в статье полностью рабочий и не требует никаких подключений дополнительных файлов. Если у вас есть готовая таблица в базе данных, вы можете прямо сейчас отправить этот скрипт на ваш сайт.

Вот такая вот рекурсия на PHP.

На все возникшие вопросы, с удовольствием отвечу в комментариях.

 

 


Тэги:

Комментарии: 12

Прокомментировать »

 
 
Сергей
18.09.2013
 

Здравствуйте, очень интересная статья, перебрал и перепробовал очень много вариантов, ваш мне показался более интересный. Я из начинающих и пытаюсь ваш вариант перенести в codeigniter, конечно если не составит для вас большего труда, не могли бы вы все выше описанное попробовать перенести на codeigniter. за ранее большее спасибо.

lavrik
18.09.2013
 

Если вы хотите написать функцию для показа меню на сайте (под управление вашего движка) то тут немного посложнее чем в данной статье, т.к. будет работа в пределах управляющих классов... С данным движком я дела не имел, пишу на yii, но помочь думаю смогу (т.к. принципы работы MVC у этих движков одинаковые). Если интересует моя помощь, обращайтесь на valery-lavrik@yandex.ru.

Сергей
9.12.2013
 

как сделать чтобы только одно подменю было открыто, если открываешь 2 подменю, то 1 закрывается

Lavrik
9.12.2013
 

Сергей, для этого вам необходимо добавить после строчки:

$(this).prev().addClass('collapsible').click(function() { // Attach an event listener

Следующую:
$("#main_ul ul").hide().prev().removeClass('expanded').addClass('collapsed');

И тогда открыто будет только одно меню....

Денис
4.01.2014
 

Добрый день. Очень хорошая статья. Возник вопрос. Как изменить код, чтобы и в узловых подкатегориях подгружалась страница? Например при нажатии "Подкатегория 2" и подгрузилась новая страница и раскрылся список подкатегорий "Подкатегории 2"?

Lavrik
4.01.2014
 

Денис: В вашем случае есть 2 пути:
1) это использовать Ajax (но это вряд ли вам подойдет)
2) это просто из последнего, финального скрипта убрать строчку return false;. В этом случае сначала раскроется меню, затем произойдет перезагрузка страницы (как вы уже догадались эта строчка останавливает загрузку страницы). Когда страница обновится, в $_GET['id'] будет содержаться идентификатор нужной вам страницы. Дальше все довольно просто (идёте в базу данных - в таблицу со страницами и по id вытаскиваете нужные вам записи).

ghjkkklis
19.02.2017
 

заказать прогон хрумером логин скайпа pokras7777

Robertcab
27.07.2017
 

Мы ценим ваше время и делим с вами общие цели. Ваши продажи для нас главный приоритет. smm продвижение заказать логин скайпа SEO2000 оращайтесь договримся есть примеры работ логин скайпа SEO2000

CurtisCouby
19.08.2017
 

Мы ценим ваше время и делим с вами общие цели. Ваши продажи для нас главный приоритет. прогон сайта логин скайпа SEO2000 оращайтесь договримся есть примеры работ логин скайпа SEO2000

Gilbertwhano
31.08.2017
 

Хорошо переносят влажную уборку, однако так как обычно разряд ламината достаточно искусный, то лучше быть его чистке не извлекать абразивных веществ. Фрезерованная вагонка Только правило, он используется в элитном строительстве. Здесь паки нуждаться оборачивать забота на то, какое наполнение имеет само дверное полотно. Урывками более дешевые блоки оказываются намного дороже присутствие полной калькуляции расходов на их покупку и затрат для монтаж и дополнительные операции. Вагонка из сосны. Профнастил чтобы забора - который лучше выбрать учитывая условия эксплуатации Профнастил С-10 kupitikirpich.ru/derevyannyie-izdeliya/ - душ деревянный <a href=kupitikirpich.ru/armatura-steklokompozitnaya/>стекло арматура</a> <a href=kupitikirpich.ru/czement/>цены на цемент</a> Вариант ячеистого бетона. Впрочем для этой же поверхности лучше только заметны различные потожировые следы. Ее теорема такая же – обеспечить движение воздуха в ванной комнате, для постоянно перемешивались горячие и холодные слои. Допустимы: Двери покрытые шпоном Представляет собой бумагу наклеенную для основание двери и покрытую смолами. Двери из массива склееных брусков Выбирая промеж шпонированными, ламинированными, покрытыми экошпоном либо ПВХ пленкой дверями, надо учитывать большое число факторов.

ander#12
2.09.2017
 

Изготовлениесветодиодных электронных табло, бегущих строк, табло для АЗС

Paulabrona
2.10.2017
 

<b>Пополение баланса Авито (Avito) за 50%</b> | <b>Телеграмм @a1garant</b> <b>Мое почтение, дорогие друзья!</b> Готовы предоставить Всем вам сервис по пополнению баланса на действующие активные аккаунты Avito (а также, абсолютно новые). Если Вам требуются конкретные балансы - пишите, будем решать. Потратить можно на турбо продажи, любые платные услуги Авито (Avito). <b>Аккаунты не Брут. Живут долго.</b> Процент пополнения в нашу сторону и стоимость готовых аккаунтов: <b>50% от баланса на аккаунте.</b> Если необходим залив на ваш аккаунт, в этом случае требуются логин и пароль Вашего акка для доступа к форме оплаты, пополнения баланса. Для постоянных клиентов гибкая система бонусов и скидок! <b>Гарантия: </b> <b>И, конечно же ничто не укрепляет доверие, как - Постоплата!!!</b> Вперед денег не просим... Рады сотрудничеству! <b>Заливы на балансы Авито</b> ________ что означает аккаунт в авито пополнить авито с киви кошелька обманули на деньги на авито авито аккаунты продажа продажа аккаунт авито

 

Прокомментировать

 
 
Сообщение *
 
Проверочный код *
 
 
 
Яндекс.Метрика