:-)
  • PHP 06.03.2009

    Этот пост не совсем про симфони. На самом деле я опишу, как реализовывал категории и подкатегории для сайта над которым тружусь уже около месяца. Но так как сам сайт создается на symfony, то без нее тут никуда. Задача стояла примерно такая: для удобной навигации и поиска по товарам их нужно отнести к различным категориям и подкатегориям. Количество как первых, так и вторых может быть бесконечно. Единственное ограничение - уровень вложенности, он равен двум. В тех.задании категории и подкатегории были описаны двумя таблицами. Я думаю, что если бы последовал этому - я бы сэкономил пару-тройку часов, однако же, мне пришла в голову идея сделать все в одной таблице. Ведь по сути эти сущности ничем не отличаются, только что подкатегории имеют родительские категории.

    По моему плану в поле, обозначающем предка в базе данных у всех должен стоять id родительской категории, а у собственно родительских - 0. Сказано - сделано, создана таблица, заполнена тестовыми данными. Заказчик обрисовал в ТЗ форму редактирования примерно так:

    Стрелочки вверх и вниз редактируют порядок категорий. Плюсики - добавление категории или подкатегории.

    Обрисовав примерную форму в Admin Generator'e в симфони, я скопировал все сгенерированные файлы в папку модуля, удалил generator.yml, то есть дальше модуль был полностью под моим контролем. Конечно, симфони генерит много необязательного кода, чтобы его убрать требуется время. Например мне сейчас совсем не нужна интернационализация, поэтому я старательно убрал все __() функции и подключения хелперов. Так же сократил количество файлов, мне не нужны были партиаллы header, footer, а некоторые я просто объединил. В итоге у меня оказалось всего 3 партиалла и 2 файла темплейтов.

    Тут, собственно, и начинается самое интересное. Чтобы не переписывать шаблоны полностью, я не хотел терять функционала Propel'a и доставал все значения из базы именно им. Но вот тут возник вопрос: "Как вывести массив объектов, возвращенных Пропелом, так, чтобы подкатегории шли сразу за своей категорией?". Решение я придумал довольно быстро - нужно было построить дерево. И вот что я сделал:

    $tree = array();
    foreach ($array as $element) {
       $parentId = $element->getParentId();
       if ($parentId == 0) {
          $tree['root'][$element->getCatId()] = $element;
       } else {
          $tree['branches'][$parentId][] = $element;
       }
    }

    Поясню. Если в массиве встречается элемент, у которого $parentId == 0, значит это родительская категория и мы кладем ее среди корневых элементов в $tree['root'][-ид-категории-]. Если же $parentId не 0, значит это подкатегория, и мы кладем ее в одну из веток $tree['branches']['-ид-родителя'][]. Поэтому теперь в темплейте, я могу писать 2 вложенных foreach. Во внешнем я перебираю $tree['root'], а во внутреннем $tree['branches'][-ид-текущего-родителя-]. Код приводить не буду - там много лишних деталей, суть же, надеюсь, я передал достаточно понятно.
    Отступ для подкатегорий, кстати, делается так:

    #sf_admin_container .sf_admin_subcat_row_0 td:first-child {
      padding-left: 20px;
    }
    #sf_admin_container .sf_admin_subcat_row_1 td:first-child {
      padding-left: 20px;
    }
    #sf_admin_container .sf_admin_subcat_row_0 td {
      background-color: #eef;
    }

    То есть применяем padding-left: 20px только для :first-child.

    Следующим трудным моментом стало редактирование/сохранение/создание категорий. Дело все в том, что в данном модуле используется одна форма для редактирования существующих категорий или создания новой и используется функция get..OrCreate (.. - название модели). Так делается в админ генераторе по умолчанию. Идея эта хороша, однако тут мне надо было обрабатывать создание новой категории особенным способом. Если в запросе создания передается cat_id, значит мы создаем подкатегорию, а эта cat_id будет родительской, а если не передаем, значит создается родительская категория. Все было хорошо до тех пор, пока мне не пришлось передавать тот самый параметр cat_id. Проблема была в том, что он терялся среди редиректов и сабмитов. Поэтому при запросе на создание я делал setFlash('force_create', $cat_id). Причем тут 'force_create'? При том, что если я хочу создать категорию и передаю id - симфони ловит его и пытается послать меня не подкатегорию создавать, а родительскую редактировать. Потому что так работает get..OrCreate. Мне пришлось ее редактировать, чтобы при hasFlash('force_create') она создавала новую категорию. Ну и потом при сохранении я достаю этот Flash и вставляю как родительский id. Звучит, наверное, просто, но нервов пришлось потратить много, пока разобрался почему терялся id, как его не терять и как избежать конфликтов. Трудно все-таки, когда одна функция создает категории, подкатегории и редактирует существующие.

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

    $query = "UPDATE categories SET order_num = CASE
           WHEN cat_id = '".$cat_id."' THEN '".$demoting_num."'
           WHEN parent_id='{$promoting_parent}'
              AND order_num='{$demoting_num}' THEN '".$promoting_num."'
           END
           WHERE cat_id IN (".$cat_id.")
              OR (parent_id={$promoting_parent} AND order_num={$demoting_num})";

    Вот так весело прошел день моей работы. В результате я получил вот такую симпатичную формочку

    Если вам понравился этот рассказ - пожалуйста, обязательно напишите комментарий. Дайте мне знать, что вам хочется чтобы я писал что-то подобное еще. Ну или не пишите, если не хотите, чтобы я продолжал.

    А еще я писал про:

    1. Symfony: переключатели
    2. Symfony: динамический роутинг
    3. Symfony: теги

    Tags: , ,

  • 10 комментариев

    WP_Modern_Notepad
    • Snowcore пишет:

      Симпатичненько.
      Помнится, я тоже когда-то делал подобную вещь. Нужно было сделать редактируемый javascript tree view .
      При чем jQuery тогда еще не знал – писал на чистом javascript.

    • [YS.PRO] пишет:

      Интересный пост. Все никак не доберусь до админ-генератора. А иерархия rootbranches напоминает связный список. Еще интереснее было бы описать универсальный метод для неограниченной вложенности.

    • CharnaD пишет:

      Для неограниченной вложенности стоит попробовать SPL Recursive Iterator. Вчера про них прочитал – интересная штуковина

    • Steward пишет:

      А может не надо для категорий, у которых есть хоть один потомок, показывать кнопочку удалить? Да и иконка «красный минус» смотрелась бы эффектнее в данном случае.
      Я бы сделал так:
      http://clip2net.com/clip/m11645/1236352899-clip-6kb.png

    • CharnaD пишет:

      Иконку я не сам рисовал, это стандартная иконка симфони. То, что минус смотрелся бы лучше – я согласен. Но дизайн еще будет меняться, я же – только программист.

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

    • Роман пишет:

      Хотел спросить, а как реализована сортировка и стрелочки вверх-вниз?

    • CharnaD пишет:

      Спасибо за вопрос. Я думаю, что напишу пост по этому поводу на ближайшей неделе, так что следите за обновлениями)

    • Роман пишет:

      Спасибо

    • BabyWolf пишет:

      Заинтересовался симфонией. Ваш блог первый, на котором я увидел простые понятные вещи про симфонию.

    • BceмиpныйAнгeл пишет:

      Круто написано! Надобыотметить на ХабрХабр. :)

    Trackbacks