Как сделать фильтрацию элементов по уровню иерархии?

На многих сайтах используются каталоги с фильтрацией элементов по уровню иерархии. Как сделать подобный каталог на G-Drive, можно прочитать в данной статье.

Каталог будет двухуровневым с отражением иерархии в адресах. В G-Drive такая иерархия поддерживается автоматически. Кроме того, в каталог будет включена поддержка вывода списка всех элементов при его подключении к определенной странице сайта. Для распознавания этой страницы в представленном ниже коде используется условие strlen($p0) == 0, что соответствует главной странице сайта. Для подключения каталога к отличной от главной странице можно использовать какое-то другое условие вывода списка всех элементов, либо простой хак вида:

<?php

$p0 = '';
require PATH.($r0['module'] = 'filterl2').'.d.php';

Связь элементов с иерархической структурой осуществляется по одному полю вне зависимости от поддерживаемого уровня вложенности узловых элементов. Для определенности назовем узловые элементы первого уровня разделами, а второго – категориями (не путать с категориями в терминологии G-Drive, где они занимают первый уровень иерархии). Фильтруемые элементы связываются с категориями, а категории – с разделами. Прямая связь между фильтруемыми элементами и разделами не поддерживается, т.е. элементы не могут находиться непосредственно в разделах. Если возникает затруднение с выбором категории для отдельных элементов раздела, для них можно создать категорию наподобие «Другое» в этом разделе. Для элементов, категорий и разделов используются три отдельные таблицы. Кроме того, таблицу категорий можно разделить на отдельные таблицы по одной для каждого раздела (в качестве альтернативы использованию в запросе условия для выбора раздела), однако в коде отдано предпочтение использованию единой таблицы категорий, а для обеспечения автоматического роутинга G-Drive задействованы представления, разделяющие таблицу категорий по разделам.

Создание таблицы элементов и примерных записей этой таблицы:

CREATE TABLE `site_items` (
  `item` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `cat` tinyint(3) unsigned NOT NULL,
  `id` varchar(16) NOT NULL,
  `name` tinytext NOT NULL,
  PRIMARY KEY (`item`),
  KEY (`cat`),
  UNIQUE (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=5;

INSERT INTO `site_items` (`item`, `cat`, `id`, `name`) VALUES
(1, 1, 'item-1', 'Элемент 1'),
(2, 2, 'item-2', 'Элемент 2'),
(3, 3, 'item-3', 'Элемент 3'),
(4, 4, 'item-4', 'Элемент 4');

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

CREATE TABLE `site_category` (
  `cat` tinyint(3) unsigned NOT NULL AUTO_INCREMENT,
  `div` tinyint(3) unsigned NOT NULL,
  `id` varchar(16) NOT NULL,
  `name` tinytext NOT NULL,
  PRIMARY KEY (`cat`),
  UNIQUE (`div`, `id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=5;

INSERT INTO `site_category` (`cat`, `div`, `id`, `name`) VALUES
(1, 1, 'cat-1', 'Категория 1 раздела 1'),
(2, 1, 'cat-2', 'Категория 2 раздела 1'),
(3, 2, 'cat-1', 'Категория 1 раздела 2'),
(4, 2, 'cat-2', 'Категория 2 раздела 2');

CREATE ALGORITHM=MERGE VIEW `site_div_1` AS
  SELECT `cat`, `id`, `name` FROM `site_category` WHERE `div`=1;
CREATE ALGORITHM=MERGE VIEW `site_div_2` AS
  SELECT `cat`, `id`, `name` FROM `site_category` WHERE `div`=2;

Создание таблицы разделов и примерных записей этой таблицы (таблица должна быть совмещена с таблицей категорий в терминологии G-Drive):

CREATE TABLE `site_categories` (
  `div` tinyint(3) unsigned NOT NULL,
  `id` varchar(16) NOT NULL,
  `name` tinytext NOT NULL,
  `module` varchar(16) NOT NULL,
  `bits` tinyint(3) unsigned NOT NULL,
  PRIMARY KEY (`id`),
  KEY (`div`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO `site_categories` (`div`, `id`, `name`, `module`, `bits`) VALUES
(0, '', 'Home', 'filterl2', 52),
(1, 'div-1', 'Раздел 1', 'filterl2', 62),
(2, 'div-2', 'Раздел 2', 'filterl2', 62);

Поле div объявлено обычным ключом, чтобы можно было идентифицировать не относящиеся к разделам каталога записи одним и тем же значением этого поля, например нулевым. Если использовать какой-то другой признак, отделяющий относящиеся к разделам каталога записи от всех прочих (например условие `module`='filterl2' AND `bits`=62), данное поле можно сделать и первичным ключом, и автоинкрементальным.

Основной код модуля (filterl2.d.php):

<?php

require PATH.'include/getrow.php';
require PATH.'include/pagelink.php';

$pp = 10;
$fields = '`items`.*';
$tables = '`items`';
$where = '1';

if (strlen($p1))
{
  $fields .= ",'{$r1['id']}' `cat_id`,'{$link->escape_string($r1['name'])}' `cat_name`";
  $where .= " AND `cat`={$r1['cat']}";
}
else
{
  $fields .= ',`category`.`id` `cat_id`,`category`.`name` `cat_name`';
  $tables .= ' LEFT JOIN `category` USING(`cat`)';
}

if (strlen($p0))
{
  $fields .= ",'{$r0['id']}' `div_id`,'{$link->escape_string($r0['name'])}' `div_name`";
  if (empty($r1)) $where .= " AND `div`={$r0['div']}";
}
else
{
  $fields .= ',`categories`.`id` `div_id`,`categories`.`name` `div_name`';
  $tables .= ' LEFT JOIN `categories` USING(`div`)';
}

if ($result = $link->query('SELECT FLOOR((COUNT(*)+'.($pp-1).')/'.$pp.') FROM '.(strlen($p0) ? $tables : '`items`').' WHERE '.$where))
{
  list($pc) = $result->fetch_row();
  $result->free();

  rotate01();
  if ($pn == 0 || $pn > $pc) error(404);

  elseif (!$result = $link->query("SELECT $fields FROM $tables WHERE $where ORDER BY `item` LIMIT ".(($pn-1)*$pp).','.$pp)) error(503);
}
else error(503);

В приведенном коде опущен табличный префикс (site_). Код ротатора номеров страниц 0 и 1 (функции rotate01) описан в заключительной части статьи Как сделать пагинацию? и в первом комментарии к ней. Содержимое включаемого файла include/getrow.php (оно будет использовано в шаблоне страниц каталога) представлено в статье Как сделать вывод списка статей?

Простейший шаблон страниц каталога (filterl2.php):

<h1><?= $page['name'] ?> (стр. <?= $pn ?> из <?= $pc ?>)</h1>

<?php while ($row = getrow($result)): ?>
<h2><?= $row['name'] ?></h2>
<p>
  <a href="/<?= $row['div_id'] ?>"><?= $row['div_name'] ?></a>
/
  <a href="/<?= $row['div_id'] ?>/<?= $row['cat_id'] ?>"><?= $row['cat_name'] ?></a>
</p>
<?php endwhile; ?>

Демонстрация: /filter-l2 (для наглядности в шаблон был добавлен вывод текста основного запроса к базе данных для конкретной страницы, т.к. формированию подобных запросов в статье было уделено не слишком много внимания).

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

  1. Михаил

    Страницу с отключенной фильтрацией можно определять непосредственно по значению $p0, например по условию $p0==$nofilter, в котором $nofilter содержит слаг страницы с отключенной фильтрацией. Обратите внимание, что подобное условие используется в коде дважды, второй раз при формировании запроса на выборку значения $pc.

  2. Михаил

    При определенных условиях использовать представления не совсем удобно. Чтобы G-Drive не пытался выполнять запросы к таблице site_category через представления site_div_1, site_div_2 и т.п., нужно изменить режим разрешений с 2 на 1 в записях разделов (младшие два бита поля bits) и взять на себя часть работы, выполняемой G-Drive, а именно добавить код для выборки записи запрошенной категории при появлении двухкомпонентного пути:

    if (strlen($p1))
    {
      if (!($result=$link->query("SELECT * FROM `category` WHERE `div`={$r0['div']} AND `id`='$p1'")))
      {
        error(503);
        return;
      }
    
      $r1=$result->fetch_assoc();
      $result->free();
    
      if (!$r1)
      {
        error(404);
        return;
      }
    
      $page=&$r1;
    }
    
  3. Михаил

    Обновил код, сделав конструкцию WHERE обязательной для любого запроса, в том числе выбирающего все элементы, т.к. в реальности практически всегда присутствует какое-то дополнительное условие, например постоянно ограничивающее видимость определенных элементов. Так вот это условие можно указать в качестве начального значения переменной $where вместо 1. Обратите внимание, что если в этом условии содержится операция OR, то может потребоваться заключить его в скобки.

    Также обратите внимание на дополнительное PHP-условие перед командой $where.=" AND `div`={$r0['div']}". Условие с `div` – лишнее при выборе элементов одной категории, т.к. мы используем единую таблицу категорий для всех разделов. Кроме того, условие с `div` нельзя добавлять без присоединения таблицы категорий, т.к. это поле хранится именно в таблице категорий, а не в таблице элементов.

Отправить комментарий

Ваш адрес E-mail не будет опубликован.