Symfony: иерархический URL

  • Наверняка многие имеют некие иерархические данные в своем проекте, будь то категории с подкатегориями, страницы с подстраницами и все такое прочее. Как подобные объекты выводятся пользователю? Обычно с помощью роута типа /category/1, или /page/28, где число — это id объекта, или даже /page/delivery, где используется slug. Но все это не отражает иерархичность наших данных. Иногда же хочется /page/delivery/small-items и /page/delivery/large-items. Никто не мешает сделать дополнительный роут с двумя слагами, но ведь их может быть не два, а три, и не три, а пять.Сразу оговорюсь, речь опять пойдет о плагине. Всю функциональность проекта я стараюсь реализовывать в плагинах, если есть хоть малейший намек на то, что это может потребоваться где-то еще. В конкретном случае модуль для статических страниц (в схеме: заголовок, текст, слаг, метаполя). И вот важный момент, для плагинов можно создать файл PLUGINNAMEConfiguration.class.php. Нам это пригодится тем, что мы будем создавать роуты на лету, начиная как раз с этого файла.Обычно я убираю названия плагинов/модулей из листингов, но в данном случае ради наглядности и простоты понимания я их оставлю. Плагин называется wePages, модуль wePages_frontend. И вот значит что мы будем делать в конфигурации:1 2 3 4 5 public function initialize() { if (in_array(‘wePages_frontend’, sfConfig::get(‘sf_enabled_modules’, array()))) { $this->dispatcher->connect(‘routing.load_configuration’, array(‘wePagesRouting’, ‘listenToRoutingLoadConfigurationEvent’)); } }Если наш модуль числится в списке подключенных, то подключаем listener по имени listenToRoutingLoadConfigurationEvent из файла wePagesRouting к событию routing.load_configuration. Проще говоря, когда загрузится конфигурация роутов будет вызвана наша функция.Создадим файл /lib/routing/wePagesRouting.class.php, куда поместим нашу функцию.1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 class wePagesRouting {   /** * Listens to the routing.load_configuration event. * * @param sfEvent An sfEvent instance * @static */ static public function listenToRoutingLoadConfigurationEvent(sfEvent $event) { $r = $event->getSubject();   // Узнаем максимальную глубину дерева $maxlvl = Doctrine_Query::create()-> select(‘MAX(level) as maxlvl’)-> from(‘wePages’)-> execute()-> getFirst()-> getMaxlvl();   // В конфигурации мы можем указать префикс для роута, // например ‘/page’, начинаться же он должен со слеша, // что мы и проверяем // prefix must have ‘/’ if (trim(sfConfig::get(‘app_we_pages_plugin_route_prefix’, ”))) { $route_pattern = ‘/’.trim( sfConfig::get(‘app_we_pages_plugin_route_prefix’, ”), ‘/’); } else { $route_pattern = ”; }   // Здесь мы создаем строчку, на соответствие которой в роутинге // будет проверятся URL, тут-то и кроется вся магия. // Мы создаем роут с количеством слагов равным максимальной // глубине дерева, чтобы охватить все страницы, а в качестве // значения по умолчанию ставим false, чтобы роут срабатывал // и на меньшем количестве слагов $defaults = array(); for ($i = 0; $i <= $maxlvl; $i++) { $route_pattern .= ‘/:slug’.$i; $defaults[‘slug’.$i] = false; }   // осталось теперь только подключить роут к уже загруженным, // важно чтобы он был первым // preprend our routes $route = new wePagesRoute( $route_pattern, array_merge( array( ‘module’ => ‘wePages_frontend’, ‘action’ => ‘show’ ), $defaults ) ); $r->prependRoute(‘wePages_frontend’, $route); }   }Надеюсь, что в комментариях я обьяснил достаточно подробно что мы делаем. Создали роут, который охватывает все страницы сразу, с большим и маленьким количеством слагов.Обратите внимание, что мы создали объет не sfRoute, а wePagesRoute. Этот класс лежит в соседнем файле wePagesRoute.class.php. Нам ведь надо проверять довольно специфические условия, поэтому стандартный класс не подходит.1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 class wePagesRoute extends sfRoute { // функция проверяет подходит ли данный роут под указанный URL public function matchesUrl($url, $context = array()) { // если шаблон роута не совпадает – дальше даже не смотрим if (false === $parameters = parent::matchesUrl( rtrim($url, ‘/’) , $context)) { return false; } // преобразуем наши сгенерированные слаги в массив, // где ключи – это глубина элемента в дереве, // а значения – сами слаги. // почему так? у нас же рекурсивный роут и начинается // с верхнего уровня, который имеет level = 0 // соответственно следующие слаги имеют level = 1,2,3.. // make slug1, slug2.. into array [0] => ‘slug1’, [1] => ‘slug2’ $i = 0; $pages = array(); while (isset($parameters[‘slug’.$i]) && strlen(trim($parameters[‘slug’.$i]))) { $pages[$i] = $parameters[‘slug’.$i]; $i++; }   // если выяснилось, что слагов не указано – бросаем это дело // If we dont have any slugs – we dont want this anymore if (empty ($pages)) { return false; } // Второй важный момент // Мы берем слаги и уровни, и ищем страницы подходящие под условия. // Данный способ не работает, если slug не уникальны. // Groupby объединяет элементы с одинаковым уровнем // Get pages with these slugs and levels $data = Doctrine_Query::create() ->from(‘wePages’) ->andWhereIn(‘slug’, array_values($pages)) ->andWhereIn(‘level’, array_keys($pages)) ->orderBy(‘level ASC, lft ASC’) // this group by removes entries with same level ->groupBy(‘level’) ->execute();   // теперь внимательно смотрим, если нашли страниц меньше, // чем было указано слагов то URL не совпадает с иерархией в БД, // а значит, извините, но вы не по адресу. // искали 3 слага – надо найти 3 страницы, и чтобы их level // были равны 0,1,2 и не повторялись // проблемы начнутся, если слаги могут совпадать, // но другого быстрого и простого способа проверить я не придумал // If some url parts dont fit – dont want this anymore if (count($data) < count($pages)) { return false; }   // если мы не указали в конфигурации префикс // и страницы вызываются, как www.site.com/page1 // то этот роут будет выполнятся довольно часто, // поэтому раз уж мы выполнили поиск страниц // было бы глупо не передать уже найденный id в метод show, // или что там у вас для показа $parameters[‘page_id’] = $data->getLast()->getId();   return $parameters; }У меня этот метод прекрасно заработал, хотя и не является идеальным решением. Если я хочу создать страницы /team/men, /team/women, то мне придется создавать и страницу /team, и она будет открываться, хотя я, например, хотел бы чтобы она редиректила на /team/men. То есть надо было бы создавать тип страниц «редирект». Что порядком костыльно. Так же, вероятно, будут проблемы с link_to и url_for. Стоит написать свой хелпер для генерации урлов.Что еще можно сделать? Можно при создании страницы указывать URL напрямую, и хранить его в БД. И точно так же в роуте проверять не соответствие слагов и уровней, а просто искать по полю url. Позволяет обойти проблему с /team/men, хотя и не очень красиво, страница /team, будет выдавать 404. Хотя как я понимаю, проблему с link_to оно все равно не решит.Все описанные способы являются экспериментальными, где-то может вылезти проблема о которой я не догадывался. Так что воспринимайте это как концепцию, а если вы найдете более удачный способ и напишете мне — я буду благодарен.
    • dkХранение url в модели — не такая уж плохая мысль, если ее немного развить.Естественно, поле должно заполняться автоматически из slug-ов страницы и родителей.Можно сделать, например, так:wePages::preSave при изменении slug скидывает url страницы и подстраниц в NULL.В wePages добавляем метод getInternalUrl, проверяющий что url IS NOT NULL, собирающий урлы объекта и родителей при необходимости и возвращающий что-то вроде «@pages?id=123″.weRoute::matchesUrl первым делом проверяет совпадение по полному url, если нет-проверяем slug-и от корня, если страница нашлась — вызываем wePages::getInternalUrl и возвращаем параметры фабрике.weRoute::generate вызывает getInternalUrl для верности и возвращает wePages[‘url’]Проблема с /team — тоже вполне решаема — просто добавляем флаг is_index в модель, если последняя часть урла не нашлась по slug — ищем дочернюю страницу с is_index=1

Leave a Reply

Your email address will not be published. Required fields are marked *