webew
Войти » Регистрация
 
PHP
MySQL/MariaDB :: хранимые процедуры

Как сделать иерархическое меню?

9 февраля 2009, 17:40

Нередко возникает задача формирования меню, обладающего иерархической структурой. Другими словами, у пунктов такого меню могут быть подпункты (практически каждый раз это приходится делать, например, при разработке веб-сайтов). При этом часто некоторый элемент является "активным" и меню должно быть соответствующим образом "раскрыто".

В настоящей статье предлагается решение этой проблемы средствами языка PHP, а также альтернативное решение с использованием процедур MySQL.

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

Структура данных.

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

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

+----+----------+------+--------------+
| id | parentid | sort | text         |
+----+----------+------+--------------+
|  1 |        0 |    0 | Главная      |
|  2 |        0 |    0 | Продукция    |
|  3 |        0 | -100 | Контакты     |
|  4 |        0 | -200 | О нас        |
|  5 |        2 |   20 | Балки        |
|  6 |        2 |   50 | Швеллеры     |
|  7 |        2 |    0 | Черепица     |
|  9 |        6 |    0 | 16 П         |
| 10 |        6 |    0 | 16 У         |
| 11 |        6 |    0 | 18 П         |
| 12 |        6 |    0 | 10 П         |
| 13 |        6 |    0 | 12 П         |
| 14 |        6 |    0 | 14 П         |
| 15 |        5 |    0 | 12 б         |
| 16 |        5 |    0 | 30 Б1        |
| 17 |        5 |    0 | 35 б1        |
+----+----------+------+--------------+

Хранить такую таблицу удобно в какой-нибудь СУБД, например, MySQL (которая и будет использоваться далее), где структура данных приводимой таблицы описывается так:

CREATE TABLE menu (
    id INT unsigned NOT NULL AUTO_INCREMENT,
    parentid INT UNSIGNED NOT NULL DEFAULT 0,
    sort INT NOT NULL DEFAULT 0,
    text VARCHAR(255) DEFAULT NULL,
    PRIMARY KEY (id),
    KEY parentid (parentid)
)

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

Несколько слов о сортировке.

Возможно, вызывает вопросы поле sort. Нужно оно вот для чего: часто бывает необходимо жёстко контролировать порядок отображения элементов меню одного уровня, и при этом сортировка ни по алфавиту, ни по id не подходит (например, хочется, чтобы элемент "Швеллеры" всегда показывался над элементом "Балки"). Для этого и вводится дополнительный параметр, по которому сортируют.

Следует отметить, что в конечном итоге sort повлияет лишь на относительное расположение тех элементов, у которых один и тот же родитель (parentid). То есть, элемент "Балки" никогда не окажется выше элемента "Продукция" или "Главная", хотя sort у него больше.

Код для PHP

Для формирования готовых для работы данных меню требуется вызывать функцию getmenu(), которая формирует массив вида

Array
(
    [1] => Array
        (
            [id] => 1
            [text] => Главная
            [level] => 1
            [active] =>
        )

    [2] => Array
        (
            [id] => 2
            [text] => Продукция
            [level] => 1
            [active] => 1
        )

    [6] => Array
        (
            [id] => 6
            [text] => Швеллеры
            [level] => 2
            [active] =>
        )
    ...
)

Параметр active показывает, является ли данный пункт меню активным; это бывает нужно, например, чтобы подсветить его жирным, кроме того, в меню веб-сайтов активный пункт, в отличие от всех остальных, обычно не является ссылкой. Из такого массива легко сделать HTML-код:

<?php

function drawmenu($menudata) {
    // упрощенный вариант без URL для пунктов меню
    $html = '';
    foreach ($menudata as $a) {
        extract($a);
        $html .= '<div style="padding-left: '.(($level-1)*25).'px;">';
        if ($active) $html.= "<strong>$text</strong>";
        else $html .= $text;
        $html .= '</div>';
    }
    return $html;
}

// соединяемся с базой данных и т.п.

// пусть меню хранится в таблице mainmenu
$menutable = 'mainmenu',
$menuid = 11;
echo drawmenu(getmenu($menutable, $menuid));

?>

Получится так:

Главная
Продукция
Швеллеры
14 П
12 П
10 П
18 П
16 У
16 П
Балки
Черепица
Контакты
О нас

Необходимый код представлен ниже:

<?php


function getmenu($tablename, $activeid) {
    $parents = menuparents($tablename);
    $chain = makechain($parents, $activeid);
    $parentid = 0; $level = 1;
    $list = build_hierarchy($parentid, $level, $chain, $parents);
    $menudata = menudata($tablename, $list, $activeid);
    return $menudata;
}


function menuparents($tablename) {
    $parents = array();
    $sql = "SELECT id, parentid FROM $tablename ORDER BY sort DESC";
    $result = mysql_query($sql) OR die (
        "<p><strong>Error at line ".__LINE__."</strong>:<br/>"
        .mysql_error().
        "<br/>
        <strong>SQL was</strong>: $sql</p>"

        );
    while ($row = mysql_fetch_assoc($result)) {
        $parents[$row{'id'}] = $row['parentid'];
    }
    return $parents;
}


function makechain($hierachy, $activeid) {
    $chain = array();
    if (!isset($hierachy[$activeid])) {
        return array();
    }
   
    $current = $activeid;
    do {
        $chain[] = $current;
        $current = $hierachy[$current];
    }
    while ($current);
   
    return $chain;
}


function build_hierarchy($parentid, $level, $chain, $hierarchy) {
    $list = array();
    $brothers = array_keys($hierarchy, $parentid);
    foreach ($brothers as $value) {
        $list[$value] = $level;
        if (in_array($value, $chain)) {
            $children = build_hierarchy($value, $level+1, $chain, $hierarchy);
            $list = $list + $children;
        }
    }
    return $list;
}


function menudata($tablename, $list, $activeid) {
    $ids = array_keys($list); // вспомним, что id элементов хранятся в ключах массива
    $in = implode(',', $ids);
    $sql = "SELECT id, text FROM $tablename WHERE id IN ($in)";
    $result = mysql_query($sql) OR die (
        "<p><b>Error at line ".__LINE__."</b>: "
        .mysql_error().
        "<br/>
        <b>SQL was</b>: $sql</p>"

        );
    $tempdata = array();
    while ($row = mysql_fetch_assoc($result)) {        
        $id = $row['id'];
        $level = $list[$id];
        $active = ($id == $activeid) ? TRUE : FALSE;
        $a = $row + compact('level', 'active');
        $tempdata[$row{'id'}] = $a;
    }
    foreach ($list as $key => $value) {
        $data[$key] = $tempdata[$key];
    }
    return $data;
}


?>

Далее предложенный код подробно разбирается. Те, кого разбор кода не интересует, могут перейти к описанию реализации на MySQL или — если есть желание оставить комментарий — к концу статьи.

function getmenu($tablename, $activeid) {
    /*
    getmenu() является интерфейсом для работы с предлагаемым пакетом функций (т.е., чтобы всё заработало, нужно вызывать именно её).
   
    Функция принимает два аргумента:
    1. имя таблицы в БД, в которой хранится информация о пунктах меню
    2. id "активного" элемента меню; если активных элементов меню нет, следует передавать любое число, не совпадающее ни с одним id из имеющихся (например, 0)
    */

    $parents = menuparents($tablename);
    $chain = makechain($parents, $activeid);
    $parentid = 0; $level = 1;
    $list = build_hierarchy($parentid, $level, $chain, $parents);
    $menudata = menudata($tablename, $list, $activeid);
    return $menudata;
}

function menuparents($tablename) {
    /*
    menuparents() формирует массив, хранящий информацию об иерархии элементов меню.
    Массив одномерный, каждому пункту меню соответствует один элемент массива, ключом при этом будет id пункта меню, значением - id родительского пункта меню (для корневых пунктов это будет 0).
    Для работы функции требуется имя таблицы в БД, где хранится меню.
    */

    $parents = array();
    $sql = "SELECT id, parentid FROM $tablename ORDER BY sort DESC";
    /*
    SQL-инъекций давайте в этом месте бояться не будем, т.к. аргумент с именем таблицы должен формироваться программистом (а не пользователями и т.п.)
    В случае, если какие-то опасности все-таки есть, следует обработать их до передачи аргументов функции getmenu().
     */

    $result = mysql_query($sql) OR die (
        "<p><strong>Error at line ".__LINE__."</strong>:<br/>"
        .mysql_error().
        "<br/>
        <strong>SQL was</strong>: $sql</p>"

        );
    while ($row = mysql_fetch_assoc($result)) {
        $parents[$row{'id'}] = $row['parentid'];
    }
    return $parents;
}

function makechain($hierachy, $activeid) {
    /*
    Меню может быть развёрнуто: если есть некий "активный" элемент, то должны также отображаться все его дочерние, если они имеются. Также раскрыт должен быть любой элемент, если его дочерний элемент является "активным", причем родство может быть как прямое, так и через несколько уровней (например, когда активен дочерний элемент дочернего элемента и т.д).
    Задача этой функции - составить список элементов меню, которые должны быть раскрыты (т.е. чьи дочерние элементы нужно показать).
    */

    $chain = array();
    if (!isset($hierachy[$activeid])) {
        /*
        Так проверяется существование элемента меню с данным id.
        Если в массиве $hierachy нет соответствующего элемента, значит, элемента меню с таким id нет вообще.
        В этом случае логично сразу завершить работу функции, вернув пустой массив.     
        */

        return array();
    }
   
    // если же элемент найден в $parents - можно начинать построение цепочки
    $current = $activeid; // цепочка начинается с активного элемента
    do {
        /*     
        Следует отметить, что, поскольку id каждого элемента уникален, нет нужды указывать у каждого из членов цепочки его уровень или же строить цепочку в каком-то специальном порядке - важно лишь  наличие элемента в возвращаемом списке
        */

        $chain[] = $current;
        $current = $hierachy[$current]; // переходим на уровень выше - к "родителю" текущего элемента  
    }
    // если оказалось, что id элемента равен 0 (отсюда условие ($current == FALSE) верно) - значит, мы добрались до самого верхнего уровня, цепочка построена и выполнение цикла нужно прекратить
    while ($current);
   
    return $chain;
}


function build_hierarchy($parentid, $level, $chain, $hierarchy) {
    /*
    Эта функция выполняет ключевую роль в построении правильно упорядоченного списка элементов меню. Фактически требуется просто список id элементов (поскольку id элемента однозначно его идентифицирует). В этот список должны войти только отображаемые элементы, причем список должен быть сформирован именно в том порядке, в котором элементы будут следовать в меню.
   
    Наиболее удобным оказалось построить функцию, отталкиваясь от id родительского элемента (который передается первым аргументом) - в меню в любом случае отображаются все пункты самого верхнего уровня (parentid = 0), следовательно, нужно их все включить в список, а далее просто для каждого элемента проверить, не раскрыт ли он (это проверяется по наличию текущего элемента в цепочке раскрытых, которая передается третьим аргументом - $chain). В случае, если раскрыт, настоящая функция просто вызывается так, что текущий элемент выступает уже в качестве родительского, и так далее, пока цепочка раскрытых не закончится.
   
    Бывает нужен также уровень элемента, поэтому функция построена с учетом такой необходимости и возвращает массив, ключи которого принимают значения id элементов меню (id уникальны, поэтому никаких конфликтов внутри массива не будет), а значения - уровень этих элементов. Для расчета уровня служит второй аргумент функции ($level); хотелось бы отметить, однако, что он несет лишь прикладную функцию и для построения меню без информации об уровнях не требуется. Вообще уровень можно было бы вычислять и отдельно -  считать количество элементов в цепочке, возвращаемой функцией chain(), но это потребовало бы дополнительных действий, основная часть которых итак выполняется в настоящей функции.
   
    Массив $hierarchy, передающийся в функцию четвертым аргументом, является продуктом вышеописанной функции menuparents() и требуется для получения дочерних элементов по id родительского.
    */

    $list = array();
    $brothers = array_keys($hierarchy, $parentid);
    foreach ($brothers as $value) {
        $list[$value] = $level;
        /*
        Если элемент есть среди раскрытых, значит, нужно получить его дочерние элементы
        причем эти дочерние элементы должны быть добавлены перед тем, как переходить к следующим, поскольку в меню именно в таком порядке они и должны быть:
        */

        if (in_array($value, $chain)) {
            $children = build_hierarchy($value, $level+1, $chain, $hierarchy);
            $list = $list + $children;
        }
    }
    return $list;
}


function menudata($tablename, $list, $activeid) {
    /*
    menudata() получает данные о нужных элементах меню, возвращая готовый отсортированный в нужном порядке массив со всей информацией.
   
    Устроена функция несколько более сложно, чем могла бы быть: обычно в таких случаях из БД запрашивают информацию обо всех пунктах меню и потом выбирают нужные. Однако хочется все же выбрать только те данные, которые необходимы - зачем вытаскивать всю таблицу меню, если нужны сведения только о некоторых из элементов (хотя это вряд ли скажется на производительности, поскольку обычно таблицы с меню очень маленькие). Поэтому функции нужно передать список элементов, которые будут показаны в меню; делается это с помощью аргумента $list - результата работы функции build_hierarchy().
    */

    $ids = array_keys($list); // вспомним, что id элементов хранятся в ключах массива
    $in = implode(',', $ids);
    $sql = "SELECT id, text FROM $tablename WHERE id IN ($in)";
    $result = mysql_query($sql) OR die (
        "<p><strong>Error at line ".__LINE__."</strong>:<br/>"
        .mysql_error().
        "<br/>
        <strong>SQL was</strong>: $sql</p>"

        );
    $tempdata = array();
    // сначала выберем данные в двумерный массив с ключами, равными id соотв. элементов
    // нужно помнить, что в результате запроса хотя и содержатся только нужные элементы, отсортированы они там неизвестно как
    while ($row = mysql_fetch_assoc($result)) {        
        $id = $row['id'];
        $level = $list[$id];
        $active = ($id == $activeid) ? TRUE : FALSE;
        $a = $row + compact('level', 'active');
        $tempdata[$row{'id'}] = $a;
    }
    // а теперь воспользуемся тем обстоятельством, что в массиве из функции build_hierarchy() элементы отсортированы именно в том порядке, в котором их следует выводить
    // сохранить этот порядок в результирующем массиве легко:
    foreach ($list as $key => $value) {
        $data[$key] = $tempdata[$key];
    }
    return $data;
}

Код для MySQL

Реализация средствами MySQL приводится больше для сравнения с остальными способами, чем для реальной жизни: как правило, логику построения меню целесообразно переносить на клиент. Реализовывать на SQL менее удобно, чем на многих других современных языках. В MySQL, например, нет массивов и нельзя обращаться к идентификаторам (именам столбцов и таблиц) через переменные — для всего этого приходится использовать всяческие ухищрения типа временных таблиц и PREPARED STATEMENTS. Кроме того, из-за необходимости создания временных таблиц всё это еще и медленно работает.

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

Код, пригодный для MySQL версии 5.0 и выше, выглядит так:

DELIMITER $$

CREATE PROCEDURE getmenu (IN tablename VARCHAR(255), IN activeid INT)
BEGIN
    DECLARE current, counter, depth INT;
    DECLARE tmptablename VARCHAR(64);
   
    SET depth = @@session.max_sp_recursion_depth;
    SET @@session.max_sp_recursion_depth = 255;
   
    SET tmptablename = 'menutmptable';
    IF tmptablename = tablename THEN SET tmptablename = 'menutmptable2'; END IF;
    SET @q = CONCAT('CREATE TEMPORARY TABLE ', tmptablename, ' SELECT * FROM ', tablename);
    PREPARE s FROM @q;
    EXECUTE s;
   
    CREATE TEMPORARY TABLE chain (id INT);
   
    SELECT id FROM menutmptable WHERE id = activeid INTO current;
   
    IF current IS NOT NULL THEN BEGIN
        forchain: LOOP
            INSERT INTO chain VALUES (current);
            SELECT parentid FROM menutmptable WHERE id = current INTO current;
            IF current = 0 THEN LEAVE forchain; END IF;
        END LOOP;
    END;    END IF;

    CREATE TEMPORARY TABLE hierarchy (id INT, level INT, counter INT);
   
    SET counter = 1;
    CALL build_hierarchy(0, 1, counter);
    SELECT m.*, h.level, IF(h.id = activeid, 1, 0) AS active FROM menutmptable m INNER JOIN hierarchy h USING(id) ORDER BY h.counter ASC;
   
    SET @@session.max_sp_recursion_depth = depth;
   
    DROP TEMPORARY TABLE chain;
    DROP TEMPORARY TABLE hierarchy;
   
    SET @q2 = CONCAT('DROP TEMPORARY TABLE ', tmptablename);
    PREPARE s2 FROM @q2;
    EXECUTE s2;

END $$


CREATE PROCEDURE build_hierarchy (IN parent INT, IN level INT, INOUT counter INT)
BEGIN
    DECLARE currentid INT;
    DECLARE chk, exit_flag BOOL DEFAULT 0;
    DECLARE c CURSOR FOR SELECT id FROM menutmptable WHERE parentid = parent;
    DECLARE CONTINUE HANDLER FOR SQLSTATE '02000' SET exit_flag = 1;
    OPEN c;
    fetch_loop: LOOP
        FETCH c INTO currentid;
        IF exit_flag = 1 THEN LEAVE fetch_loop; END IF;
        INSERT INTO hierarchy (id, level, counter) VALUES (currentid, level, counter);
        SET counter = counter + 1;
        SELECT IF(COUNT(*)>0, 1, 0) FROM chain WHERE id = currentid INTO chk;
        IF chk != 0 THEN CALL build_hierarchy(currentid, level + 1, counter); END IF;
    END LOOP;
    CLOSE c;
END $$

DELIMITER ;

Разберем предложенный код (для читателей, не нуждающихся в разборе, дальнейшее содержание статьи, по всей видимости, не будет интересным; если хочется что-то обсудить в комментариях, то можно быстро перейти ближе к концу статьи).

DELIMITER $$

CREATE PROCEDURE getmenu (IN tablename VARCHAR(255), IN activeid INT)
BEGIN
    DECLARE current, counter, depth INT;
    DECLARE tmptablename VARCHAR(64); -- переменная для имени временной таблицы - все равно больше 64 символов нельзя
   
    /*
    MySQL не позволяет бесконечную рекурсию в процедурах и функциях. Для регулировки уровня рекурсии существует специальная переменная под названием max_sp_recursion_depth. Подробнее см.
    http://dev.mysql.com/doc/refman/5.1/en/server-system-variables.html#sysvar_max_sp_recursion_depth
    http://dev.mysql.com/doc/refman/5.1/en/stored-routines-syntax.html
    */

    SET depth = @@session.max_sp_recursion_depth; -- сохраним текущее значение переменой, чтобы потом вернуть его обратно
    SET @@session.max_sp_recursion_depth = 255; -- чтобы чувствовать себя свободнее, поставим максимально возможное значение
   
    /*
    Теперь нужно как-то добиться того, чтобы работа процедуры не зависела от конкретного имени таблицы, где хранится меню.
    Фактически требуется, чтобы можно было обращаться к требуемым данным, используя какой-то один фиксированный идентификатор (имя). Для этого подходит либо VIEW, либо временная таблица. Однако, VIEW будет виден и за пределами сессии, поэтому лучше работать с временой таблицей (временные VIEW, к сожалению, создавать нельзя).
    Не стоит забывать, что при создании временной таблицы делается копия данных и при больших объемах это может потребовать значительных ресурсов.
   
    При конфликте имен с другими таблицами временная таблица перекроет существующую; если при этом окажется, что имя, под которым создаётся временная таблица, совпадёт  с именем таблицы, где хранится меню, последняя перестанет быть видна процедуре. Чтобы избежать такого, сделаем дополнительную проверку:
    */


    SET tmptablename = 'menutmptable';
    IF tmptablename = tablename THEN SET tmptablename = 'menutmptable2'; END IF;
   
    /*
    Теперь нужно, собственно, временную таблицу создать. MySQL не позволяет вставлять переменные в те места запросов, где должны быть имена таблиц. Обойти это ограничение можно только с помощью PREPARE STATEMENT, которые позволяют выполнить в качестве запроса любую строку.
    Мало того, для создания PREPARE STATEMENT еще и придется использовать глобальную переменную сессии, а не внутреннюю: PREPARE STATEMENTS не принимают внутреннюю переменную в качестве строки запроса.  
    */

    SET @q = CONCAT('CREATE TEMPORARY TABLE ', tmptablename, ' SELECT * FROM ', tablename); -- составляем текст запроса
    PREPARE s FROM @q; -- готовим выражение
    EXECUTE s; -- выполняем выражение - после этого временная таблица будет создана
   
    CREATE TEMPORARY TABLE chain (id INT); -- сделаем таблицу для хранения раскрытых элементов
   
    SELECT id FROM menutmptable WHERE id = activeid INTO current;
   
    /*
    Такое выражение позволяет одновременно и проверить наличие элемента меню с заданным id, и, в случае наличия такового, сразу же присвоить это значение вспомогательной переменной, которая будет использоваться далее.
    Если в переменную current попало значение - стало быть, запись с таковым id существует, и можно начинать построение цепочки.
    В противном случае таблица с цепочкой останется пустой, т.к. активных элементов нет.
    */

    IF current IS NOT NULL THEN BEGIN
        forchain: LOOP
            INSERT INTO chain VALUES (current);
            SELECT parentid FROM menutmptable WHERE id = current INTO current;
            IF current = 0 THEN LEAVE forchain; END IF; -- выбираем, пока не достигнем верхнего уровня меню
        END LOOP;
    END;END IF;

   
    /*
    Теперь сделаем еще одну вспомогательную временную таблицу. Основная информация, которая будет храниться в ней - это id нужных нам элементов (тех, которые будут отображаться в меню).
   
    Не менее важная информация - это порядок, в котором элементы будут следовать в меню в конечном итоге. Чтобы иметь возможность установить этот порядок, понадобится специальная колонка - пусть она будет называться counter.
   
    На всякий случай получим и уровень каждого элемента (чтобы, при надобности, всё по многу раз не пересчитывать).
    */

    CREATE TEMPORARY TABLE hierarchy (id INT, level INT, counter INT);
   
    /*
    Обратите внимание, что в процедуре build_hierarchy() третий аргумент имеет тип INOUT (это означает, что изменения, которые этот аргумент претерпевает во время её работы, сохранятся и после вызова процедуры этот аргумент будет хранить уже измененное значение). По этой причине третий аргумент обязательно должен быть в виде переменной, поскольку новое значение (полученное в результате работы процедуры) должно быть куда-то записано. Первый и второй аргументы имеют тип IN и потому могут передаваться в виде констант.
   
    Начнем с корневых элементов - первый аргумент (id родительского элемента) - равен 0. Уровени меню (здесь - второй аргумент) пусть начинаются с первого. Счетчик начнем с единицы.
    */

    SET counter = 1;
    CALL build_hierarchy(0, 1, counter);
   
    -- после вызова процедуры build_hierarchy() таблица hierarchy будет нужным образом заполнена, и остаётся только выбрать остальные параметры элементов (такие как название и др.): 
    SELECT
        m.*, h.level, IF(h.id = activeid, 1, 0) AS active
        FROM menutmptable m INNER JOIN hierarchy h USING(id) -- USING(id) - то же, что ON h.id = m.id
        ORDER BY h.counter ASC;
   
    -- "уберёмся" за собой: вернём переменной прежнее значение и удалим временные таблицы  
    SET @@session.max_sp_recursion_depth = depth;
    DROP TEMPORARY TABLE chain;
    DROP TEMPORARY TABLE hierarchy;
    -- поскольку имя таблицы в переменной, опять приходится использовать PREPARE STATEMENT и глобальные переменные
    SET @q2 = CONCAT('DROP TEMPORARY TABLE ', tmptablename);
    PREPARE s2 FROM @q2;
    EXECUTE s2;

END $$


CREATE PROCEDURE build_hierarchy (IN parent INT, IN level INT, INOUT counter INT)
    /*
    Эта процедура занимается заполнением таблицы hierarchy, на основе которой потом строится список элементов меню, причем строится именно в том порядке, в котором они потом будут отображаться. Для каждого из этих элементов процедура должна получить:
    1. id
    2. уровень
    3. порядковый номер (требуется для правильной сортировки)
    */


BEGIN
    DECLARE currentid INT;
    DECLARE chk, exit_flag BOOL DEFAULT 0; -- BOOL - то же, что TINYINT
    DECLARE c CURSOR FOR SELECT id FROM menutmptable WHERE parentid = parent;
    DECLARE CONTINUE HANDLER FOR SQLSTATE '02000' SET exit_flag = 1;
    OPEN c;
    fetch_loop: LOOP
        FETCH c INTO currentid;
    IF exit_flag = 1 THEN LEAVE fetch_loop; END IF;
        INSERT INTO hierarchy (id, level, counter) VALUES (currentid, level, counter);
        SET counter = counter + 1;
        SELECT IF(COUNT(*)>0, 1, 0) FROM chain WHERE id = currentid INTO chk;
        IF chk != 0 THEN CALL build_hierarchy(currentid, level + 1, counter); END IF; -- проверяем, есть ли текущий элемент среди раскрытых; если есть - вызываем build_hierarchy() уже для него
    END LOOP;
    CLOSE c;
END $$

DELIMITER ;

© Все права на данную статью принадлежат порталу webew.ru. Перепечатка в интернет-изданиях разрешается только с указанием автора и прямой ссылки на оригинальную статью. Перепечатка в печатных изданиях допускается только с разрешения редакции.
Добавить комментарий
Отображение комментариев: Древовидное | Плоское

bur

Вау!
19.02.2009, 11:17
Ответить

1234ru

Спасибо :)
(я так понял, тебя впечатлили, в основном, SQL-неистовства).

Кстати. А как на твоём языке меню делается? (мне вот всегда было немного неуютно, что я этого не знаю, т.к. на сайтах с глубокой и сложной иерархией его часто нужно делать).
У тебя нет ли, случайно, аналогичного кода?
То, что не убивает нас, делает нас инвалидами.
19.02.2009, 22:53
Ответить

bur

Да, SQL-часть заставила очередной раз поразиться мощи MySQL. Несмотря на все подводные камни, задача была успешно решена. Не знал что мускуль на такое способен. Отдельное спасибо за мелочи типа "PREPARE STATEMENTS не принимают внутреннюю переменную" и др.

Как правило, JavaScript работает с уже выстроенными массивами, содержащими данные о иерархии и текущем состоянии меню. Так что задача сводится к написанию простой ф-ии, аналогичной drawmenu. Хотя никаких проблем в построении меню на JavaScript-t не вижу, все PHP-функции из статьи можно с небольшими изменениями перевести на рельсы JavaScript, заставив клиента считать вместо сервера.
20.02.2009, 11:32
Ответить

bur

Нашел даже пример собственной реализации выпадающего меню.
Хотя этот код морально устарел. Не нужны глобальные переменные, громадные обработчики и прочие некрасивости.
20.02.2009, 11:40
Ответить
NO USERPIC

regul

Господа, я конечно не до конца понял всей глубины Вашей реализации, когда я вижу структуру типа id,parent_id и понятие меню, у меня это миинмум вызывает недоверие. Меню чаще всего читается и делать чтение таким трудоемким расточительно.

Что насчет терева "Вложенные множества" (Nested Sets)? Или вот ещё ссылка.
Почему не была придложена данная технология? По-моему, она себя оправдывает сполна. А чем оправдывает себя такая реализация

В дополнение, хорошо бы использовать memcache или кеширование. Это все-таки меню.
20.02.2009, 19:37
Ответить
NO USERPIC

victor

function drawmenu($menudata) {
// упрощенный вариант без URL для пунктов меню
$html = '';
foreach ($menudata as $a) {
extract($a);
$html .= '<div style="padding-left: '.(($level-1)*25).'px;">';
if ($active) $html.= "<strong>$text</strong>";
else $html .= $text;
$html .= '</div>';
}
return $html;
}

не судите строго , только начинающий ещё
не въехал куда прописывать пути URL страниц собственно.
должно быть это в БД ? или как реализуется вывод строки при нажатии на соответствующий пункт меню
2121
20.07.2009, 11:13
Ответить

1234ru

Цитата:
не въехал куда прописывать пути URL страниц собственно.

В таблице меню (в БД) должна быть соответствующая колонка - pageid.
Код у Вас будет вида

if ($active) $html.= "<strong>$text</strong>";
else $html .= "<a href=\"$url\">$text</a>";

Соответственно, при нажатии на пункты меню будут открываться разные страницы, каждой из которых соответствует определенный пункт. Активный пункт при этом, очевидно, будет меняться.
То, что не убивает нас, делает нас инвалидами.
21.07.2009, 04:41
Ответить
NO USERPIC

victor

спасибо за быстрый ответ , НО
у меня не получается, что-то упустил, наверное.

В таблице меню (в БД) должна быть соответствующая колонка - pageid.
Код у Вас будет вида

if ($active) $html.= "<strong>$text</strong>";
else $html .= "<a href=\"$url\">$text</a>";


вопрос:
в таблице меню - добавляю поле pageid ,
туда прописываю путь:
например: одна страница - /папка/страница.php

другая страница - /папка/папка/страница3.php

это правильно ?

теперь вопрос:
$url - наверное нужно прописать в коде каким образом она будет получать данные из pageid ????
или я ошибаюсь ???
Еще раз большое спасибо
2121
22.07.2009, 16:07
Ответить

1234ru

Цитата:
В таблице меню (в БД) должна быть соответствующая колонка - pageid.


Предполагается, что есть таблица pages, где у каждой страницы свой id. Этот id и должен храниться в колонке pageid таблицы menu.
Можно и так, как Вы сказали (хотя так менее удобно).

Цитата:
$url - наверное нужно прописать в коде каким образом она будет получать данные из pageid ????


Когда выбираете таблицу меню, для каждого пункта нужно цеплять JOIN'ом id cоответствующей ему страницы

SELECT m.*, p.url FROM menu m INNER JOIN pages p ON p.id = m.pageid


(по смыслу это примерно то же самое, что для каждого пункта сделать SELECT url FROM pages WHERE id = тут_id_страницы , только лучше)
То, что не убивает нас, делает нас инвалидами.
22.07.2009, 17:34
Ответить
NO USERPIC

victor

блин, наверное, я безмозглый какой-то.
Переделал базу, сделал таблицу pages. прописал там все страницы
потом id-шки страниц перенес в поле pageid таблицы menu.

у меня меню отражается след образом.
Свернутое- показывается только первый уровень , при клике на ссылку - не открывается - идет ссылка этой страницы саму на себя. Т.е. если открываешь index - показывается индекс по всем пунктам(ссылкам) меню.
Если набираешь ручками путь страницы (например второго уровня - http://XXX.net/papka/file.php . то ссылка подсвечивается ровно такая же http://XXX.net/papka/file.php

Могут мне гении = PHP-исты подсказать , что я делаю не так.

И еще - я все же не могу въехать , как прога разберется в каком месте у меня файл лежит . у меня они не все в корне лежат
Если со мной вопрос не решаем в виду тугодумости, прошу честно сказать, чтобы не тратить Вашего времени
Еще раз спасибо
2121
24.07.2009, 11:01
Ответить

1234ru

(я тут без интернета был, поэтому долго не отвечал)

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


Наверняка у Вас неправильно определяется id активного элемента меню (из-за этого проблемы правильным раскрытием меню).

Цитата:
я все же не могу въехать , как прога разберется в каком месте у меня файл лежит


В смысле?
У Вас адрес страницы есть в таблице pages. Нужно брать переменную $_SERVER['REQUEST_URI'], (а точнее, parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH)), и искать соотв. адрес в таблице pages - так будет определена открытая в данный момент страница.

Посмотрите, кстати, соседнюю статью (возможно, найдете полезную для себя информацию):
http://webew.ru/articles/2291.webew
То, что не убивает нас, делает нас инвалидами.
30.07.2009, 15:59
Ответить
NO USERPIC

nike

Скажите пожалуйста , каким образом можно раскрыть все елементы меню ,а не толко активный.Заранее благодарю
16.02.2010, 08:24
Ответить

1234ru

Нужно просто достраивать меню не только в случае, если элемент "открыт", а в любом случае.
Для этого нужно код, который присоединяет элементы меню следующего уровня (т.е. вызов build_hierarchy()) вынести из if и выполнять в любом случае.

В PHP код

if (in_array($value, $chain)) {
    $children = build_hierarchy($value, $level+1, $chain, $hierarchy);
    $list = $list + $children;
}

меняется на
$children = build_hierarchy($value, $level+1, $chain, $hierarchy);
$list = $list + $children;


В MySQL вместо

SELECT IF(COUNT(*)>0, 1, 0) FROM chain WHERE id = currentid INTO chk;
IF chk != 0 THEN CALL build_hierarchy(currentid, level + 1, counter); END IF;


пишете просто
CALL build_hierarchy(currentid, level + 1, counter);

То, что не убивает нас, делает нас инвалидами.
16.02.2010, 13:28
Ответить
NO USERPIC

nike

все гениально просто )
а я пытался цыкле менять активный елемент ((((( Боьшое спасибо , отличное меню )
16.02.2010, 13:58
Ответить
NO USERPIC

sikon

Привет, пытаюсь разобраться. Появилась такая проблема. Программа выводит только верхние пункты независимо от того, какой activeid я выбираю. В чём может быть проблема?
Заранее благодарю. Приложу на всякий случай файл:http://webew.ru/f/9rI2YatR.txt
22.03.2010, 22:11
Ответить

1234ru

А у Вас точно все parentid правильно проставлены?

С первого взгляда сложно понять, какая часть кода в прилагаемом файле отличается от оригинального. Вы только класс добавили, остальное не трогали?
То, что не убивает нас, делает нас инвалидами.
22.03.2010, 22:50
Ответить
NO USERPIC

sikon

Parentid правильно расставлены. Я ничего не менял. Вот вставил в каждую функцию print_r и получил такие результаты(как понял неправильно работает menudata и почему-то build_hierahy сначала выводит пустой массив):

menuparents

Array
(
[6] => 2
[5] => 2
[1] => 0
[13] => 5
[12] => 6
[11] => 6
[10] => 6
[9] => 6
[8] => 6
[7] => 2
[2] => 0
[14] => 5
[3] => 0
[4] => 0
)

makechain

Array
(
[0] => 13
[1] => 5
[2] => 2
)

build_hierarhy

Array
(
)

build_hierarhy

Array
(
[13] => 3
[14] => 3
)

build_hierarhy

Array
(
[6] => 2
[5] => 2
[7] => 2
)

build_hierarhy

Array
(
[1] => 1
[2] => 1
[3] => 1
[4] => 1
)

menudata

Array
(
[1] => Array
(
[id] => 1
[text] => Главная
[level] => 1
[active] =>
)

[2] => Array
(
[id] => 2
[text] => Продукты
[level] => 1
[active] =>
)

[3] => Array
(
[id] => 3
[text] => Контакты
[level] => 1
[active] =>
)

[4] => Array
(
[id] => 4
[text] => О нас
[level] => 1
[active] =>
)

)

Главная
Продукты
Контакты
О нас
23.03.2010, 00:05
Ответить

1234ru

У Вас довольно странно всё выглядит...

С одной стороны, $active_id Вы передаете равный 4:

$menu_html = $this->drawmenu($this->getmenu('menu',4));

Однако, при этом в результирующем массиве $menudata активным он не является.
С другой стороны, массив $chain выглядит так, как будто $active_id у Вас - 13..

Вы точно такой код используете, как привели в файле? Не ошиблись нигде в начале при вызове getmenu?

(Вообще я щас вижу, что для PHP надо было это на классах сделать... Надо будет потом всё это переписать)
То, что не убивает нас, делает нас инвалидами.
29.03.2010, 00:26
Ответить
NO USERPIC

sikon

Проблема была где-то в ООП. Попробовал без него - получилось.
Спасибо.
30.03.2010, 21:19
Ответить
NO USERPIC

kukaramba

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

http://webew.ru/f/bDGbaTkb.txt
06.05.2010, 17:38
Ответить

1234ru

А в чем неполадки? (неправильно формируется меню?)
Или с БД соединиться не получается?
То, что не убивает нас, делает нас инвалидами.
07.05.2010, 00:22
Ответить
NO USERPIC

kukaramba

Соединение с базой есть. Проверял. Не выводится само меню.
07.05.2010, 07:19
Ответить
NO USERPIC

kukaramba

Вот, пытаюсь подконнектить таким образом: http://webew.ru/f/Qif9VEHt.txt
У меня есть строка :

$link = pg_pconnect("host=$host dbname=$db user=$user password=$pass");
if (!$link)
{
die("Could not open connection to database server");
}
Где при ошибки подключения - отображается информация. Проверял. У меня есть поля id,note,id_parent для прорисовки меню. Что у меня неправильно в коде? Уже весь перерыл. Я новичок в этом всем. Подскажите пожалуйста.
07.05.2010, 09:50
Ответить

1234ru

Вы таки напишите, как у Вас конкретно не работает.

Что выводит ваш скрипт? Корневые элементы показывает? Или вообще ничего
То, что не убивает нас, делает нас инвалидами.
11.05.2010, 20:44
Ответить
NO USERPIC

kukaramba

Пустой экран.
11.05.2010, 21:25
Ответить

1234ru

Если есть echo что-то и при этом выводит пустой экран - значит, где-то в скрипте ошибка (либо синтаксис, либо не установлена какая-то переменная, значение которой необходимо для правильной работы), просто сервер работает в таком режиме, что она не выводится.
Чтобы их увидеть, добавьте в начало скрипта:

error_reporting(E_ALL);
ini_set('display_errors', 1);



И еще у вас в функции menuparents() в запросе статически написано SELECT ... FROM project. Судя по тому, что таблица у Вас называется project, работать из-за этого неправильно не будет, но зачем Вы убрали оттуда переменную $tablename и заменили её на project - непонятно.
То, что не убивает нас, делает нас инвалидами.
12.05.2010, 00:43
Ответить
NO USERPIC

kukaramba

Я экспериментировал. Запрос к таблице написан правильно. Изначально я корректировал $tablename. Были мысли что присвоение переменной вне функции не работают.
17.05.2010, 14:11
Ответить

1234ru

Вы ошибки-то всё-таки включите.
У Вас написано echo. Безусловно (т.е. не внутри if, поэтому выполняться должно точно). При этом ничего не происходит. Это не есть нормальная ситуация. Ошибки и warning'и помогут понять, в чем дело и, вероятнее всего, сделать так, чтобы код заработал.
То, что не убивает нас, делает нас инвалидами.
19.05.2010, 02:04
Ответить
NO USERPIC

j3ff

всё тоже самое только немного подругому, мой вариант таков. таблицу можно юзать вашу, только parentid = pid, text = title

$catalogue = mysql_query("SELECT * FROM catalogue WHERE visible = 1 ORDER BY rownum");

if (isset($_GET['id'])) {
if (mysql_num_rows($catalogue) > 0) {
$counter = 0;
$arr_id = 1;
$curr_id = intval($_GET['id']);
$ids[0] =  intval($_GET['id']);
do {
$parent = getParents($catalogue, $curr_id);
if ($parent == "" || $parent[1] == 0) $counter = 999;
else {
$ids[$arr_id] = $parent[1];
$arr_id++;
$curr_id = $parent[1];
}
} while ($counter != 999);
//print_r($ids);

echo "<table width=\"100%\" style=\"font-family: Tahoma, Arial; font-size: 12px;\">";
echo "<tr>";
echo "<td align=\"center\">";
echo "<b>:: Навигация ::</b>";
echo "</td>";
echo "</tr>";
echo "<tr>";
echo "<td>";
echo "&nbsp";
echo "</td>";
echo "</tr>";
tree_selected($catalogue, 0, 0, 0, $ids);
echo "</table>";
} else echo "Нет записей!!";
} else {
echo "<table width=\"100%\" style=\"font-family: Tahoma, Arial; font-size: 12px;\">";
echo "<tr>";
echo "<td align=\"center\">";
echo "<b>:: Навигация ::</b>";
echo "</td>";
echo "</tr>";
echo "<tr>";
echo "<td>";
echo "&nbsp";
echo "</td>";
echo "</tr>";
tree_selected($catalogue, 0, 0, 0, 0);
echo "</table>";
}

function getParents($catalogue, $id) {
    mysql_data_seek($catalogue, 0);
    $ice = "";
    for ($i = 0; $i < mysql_num_rows($catalogue); $i++) {
        $row =  mysql_fetch_assoc($catalogue);
        if ($id == $row['id']) {
            $ice[0] = $row['id'];
            $ice[1] = $row['pid'];
        }
    }
    return $ice;
}

function tree_selected($result, $id, $pid, $depth, $ids) {
    $depth++;
    for ($i = 0; $i < mysql_num_rows($result); $i++) {
        mysql_data_seek($result, $i);
        $row = mysql_fetch_assoc($result);
        if (isset($_GET['id']) && $row['id'] == intval($_GET['id'])) $comma = " id=\"sellink\"";
        else $comma = "";
        if ($id == 0 && $row['pid'] == $pid) {
            echo "<tr>";
                echo "<td id=\"menuitems\" style=\"padding: 0px 0px 0px ".($depth*10)."px\"><a $comma href=\"index.php?id=".$row['id']."\">".$row['title']."</a><hr size=\"1\"></td>";
            echo "</tr>";
           
            if ($ids != 0 && in_array($row['id'],$ids)) tree_selected($result, $row['id'], $row['pid'], $depth, $ids);
        } else if ($row['pid'] == $id) {
            echo "<tr>";
                echo "<td id=\"menuitems\" style=\"padding: 0px 0px 0px ".($depth*10)."px\"><a $comma href=\"index.php?id=".$row['id']."\">".$row['title']."</a><hr size=\"1\"></td>";
            echo "</tr>";
            if ($ids != 0 && in_array($row['id'],$ids)) tree_selected($result, $row['id'], $row['pid'], $depth, $ids);
        }
    }
}
прохожий
27.07.2010, 14:44
Ответить
Добавить комментарий
Отображение комментариев: Древовидное | Плоское
© 2008—2017 webew.ru, связаться: x собака webew.ru
Сайт использует Flede и соответствует стандартам WAI-WCAG 1.0 на уровне A.
Rambler's Top100