Доброго времени суток! Большую часть проектов мы пишем на Yii2, потому что он клёвый и мы его любим.
Однако, всегда есть что улучшить (благо этого не препятствует архитектура Yii). Хочу поделиться решением, которое упрощает прописывание навигации в приложениях на Yii2.
Проблема
Когда мы добавляем в приложение страницу, нам нужно прописать для неё следующие вещи (после создания контроллера и вьюшки):
- Заголовок страницы (
$this->title = ...
); - Хлебные крошки (
$this->params['breadcrumbs'][] = ...
); - Права для действия в контроллере (
\yii\base\ActionFilter
вbehaviors
контроллера); - Параметр
visible
с проверкой доступа во всех меню, где есть ссылка на эту страницу; - Добавить правило в
\yii\web\UrlManager::rules
для красивой ссылки; - Добавить страницу в sitemap.xml.
Не жирновато ли для "ещё одной страницы"? Самое плохое в этом то, что все эти пункты нужно держать в голове и не забывать. А если навигация в проекте начинает меняться, то что-то сломать становится еще проще, чаще всего забываешь про хлебные крошки и они становятся просто не рабочими.
Решение
Мы предположили, что любая страница приложения должна входить в общую карту сайта. А значит, если создать такую карту сайта (в виде многоуровнего дерева) с исчерпывающей информацией о странице (см. пункты из раздела "Проблема"), то добавление страницы сведётся к описанию её в карте сайте, всего лишь в одном месте! Мы можем прописать там и заголовки, и права и правила ссылки, а имея карту сайта легко получить хлебные крошки и sitemap.xml.
Таким образом получился компонент [MegaMenu](), который и представляю хабрасообществу.
Установка
Устанавливается компонент через Composer:
$ composer require ExtPoint/yii2-megamenu
Далее нам нужно добавить компонент в конфигурацию приложения:
Как компонент приложения:
'components' => [
'megaMenu'=> [
'class' => '\extpoint\megamenu\MegaMenu',
'items' => [
// You sitemap
[
'label' => 'Главная',
'url' => ['/site/index'],
'urlRule' => '/',
],
...
],
],
...
],
И подгружать его до запуска приложения (для добавления правил в UrlManager
):
...
'bootstrap' => ['log', 'megamenu'],
...
API
АПИ компонента создавалось максимально приближенным к Yii2, часто повторяя его 1 в 1.
Формат описания страницы (параметр \extpoint\megamenu\MegaMenu::items
)
Каждый item в большинстве соответствует формату задания навигации для \yii\bootstrap\Nav::items
, где каждый item имеет атрибуты label
, url
, visible
, active
, encode
, items
, options
, linkOptions
. Каждый item задается в виде массива, из которого затем создается экземпляр класса \extpoint\megamenu\MegaMenuItem
.
Ниже перечислим нововведенные параметры, которых нет в \yii\bootstrap\Nav::items
:
urlRule
(строка, массив или экземпляр\yii\rest\UrlRule
). Формат соответствует правилу из\yii\web\UrlManager::rules
;roles
(строка или массив строк). Формат идентичен\yii\filters\AccessRule::roles
. Поддерживаются значения"?"
,"@"
и указание роли в виде строки.order
(число) Каждый уровень меню сортируется согласно этому параметру. Значение по-умолчанию — 0.
Методы компонента \extpoint\megamenu\MegaMenu
setItems(array $items)
Добавляет элементы меню в конец списка;addItems()
Добавляет элементы меню;getItems()
Возвращает элементы меню;getActiveItem()
Возвращает текущий роут, аналогично\Yii::$app->requestedRoute
, но с распарсеными параметрами;getMenu(array $item, $custom)
Находит вложенный элемент меню (null
= корень) и возвращает вложенное меню с дочерними элементами. В параметре custom можно переопределять конфигурацию меню, если задать его как массив. Если задать числом — то это укажет на возвращаемую вложенность меню. Например,\Yii::$app->megaMenu->getMenu(null, 2)
вернет двухуровневое меню, даже если само меню имеет большее число вложенности.getTitle($url = null)
Находит item для указанногоurl
(по-умолчанию — текущая страница) и возвращает его заголовокgetFullTitle($url = null, $separator = ' — ')
Аналогично предыдущему, но так же добавляет все родительские названия item'овgetBreadcrumbs($url = null)
Возвращает хлебные крошки для виджета\yii\widgets\Breadcrumbs::links
getItem($item, &$parents = [])
Находит item по url/роуту, в parents добавляет item'ы всех родителей для найденного item'аgetItemUrl($item)
Находит item и возвращает его url
Логика поиска item'а
Логика сравнения двух item реализована в методе \extpoint\megamenu\MegaMenu::isUrlEquals
. Сравнение ссылок ведется путем сравнения двух строк.
Роуты сравниваются немного сложнее: сперва они нормализуются (получение полного роута, с указанием модуля, контроллера и экшена), затем сравниваются только роуты. Если роуты совпали, то сравниваются параметры.
Если параметр отличается от null, то сравнивается как его ключ, так и значение. Если значение указано как null, это означает, что может быть любое значение, сравнивается только наличие ключей.
Примеры:
- isUrlEquals('http://ya.ru', 'http://ya.ru') // true
- isUrlEquals(['qq/ww/ee'], ['aa/bb/cc']) // false
- isUrlEquals(['aa/bb/cc', 'foo' => null], ['aa/bb/cc']) // false
- isUrlEquals(['aa/bb/cc', 'foo' => null], ['aa/bb/cc', 'foo' => null]) // true
- isUrlEquals(['aa/bb/cc', 'foo' => 'qwe'], ['aa/bb/cc', 'foo' => null]) // true
- isUrlEquals(['aa/bb/cc', 'foo' => 'qwe'], ['aa/bb/cc', 'foo' => '555']) // false
Пример
Пример маленького веб-приложения с установленным MegaMenu можно найти в папке тестов:
Да ладно, это в реальных проектах не будет работать!
Однако, будет. MegaMenu уже успешно используется в нескольких крупных проектах. В наших проектах мы всегда разбиваем функционал на модули и MegaMenu этому не сопротивляется.
Пример такой разбивки и более реальный пример можно увидеть в нашем бойлерплейте. Меню по кусочкам собирается из модулей или контроллеров.
TODO
Компонент ещё развивается, вот некоторые фичи, которые стоит ждать в ближайшем будущем:
- Проверка доступа для контроллера (behaviors, анализирующий карту сайта для проверки доступа);
- Получение карты сайта для sitemap.xml;
- UI для кастомизации карты сайта с сохранением изменений в БД.
End
Спасибо всем, кто дочитал/пролистал до конца. Любые предложения и пожелания пишите на affka@affka.ru
Ставьте звезды на гитхабе — ExtPoint/yii2-megamenu
Всем удачного дня!
Комментарии (25)
karminski
09.06.2016 09:32Что-то у вас с namespace не всё хорошо. В одном месте \app\core\components\MegaMenu, в другом \extpoint\megamenu\MegaMenu. На гитхабе у вас вроде всё с этим в порядке. По сабжу — для меня проблема кажется надуманной. Не вижу никаких проблем и сложностей в решении указанных задач для каждой отдельно взятой страницы.
ilyaplot
09.06.2016 10:39Вы, видимо, просто не видели массив rules для UrlManager размером в 2 экрана. Есть некоторые удобства в данном решении, но я не буду его использовать для небольших проектов.
zelenin
09.06.2016 12:38+1правильно я понимаю: при инициализации компонента он для своих целей инициализирует все 100 (ой, 1000) модулей моего проекта?
affka
09.06.2016 14:161. Это могут быть статичные методы модулей/контроллеров
2. Это все можно кешировать
Само мегаменю не решает эти задачи, решение возлагается на приложение.zelenin
09.06.2016 14:38+21. Вы на чей-то чужой вопрос ответили?
2. Про существование кэша я в курсе. И есть понимание, что кэш при архитектурных проблемах — костыль, а в данном случае и проблему не решает, т.к. объекты остаются в памяти.
Я вижу, что модули, которые по умолчанию lazy и инициализируются только во время непосредственного обращения к их контроллерам, инициализируются вами принудительно, для извлечения информации из — внимание — метода coreMenu, который есть только на ваших проектах.
Это огромнейший фейл, ошибка в проектировании расширения и вообще сама по себе непонятная вещь — завязаться на какую-то специфичную для вас функциональность, подпортив производительность всем.
Решение: предусмотреть в компоненте интерфейс MenuProvider — для всех, а конкретно вам не завязываться на методы модулей, требующих их инициализации (статика как вы правильно подметили, но я бы в конфиг какой-то выносил — модуль не место для хранения конфигурационных данных).affka
09.06.2016 15:38-11. На Ваш. Я имел ввиду, что если меню «хранится» в статичных методах, то не нужно инициализировать (создавать инстанс) всех модулей
В любом случае все решает кеш.
Реализацию модульности в boilerplate не навязываю, она не включается в MegaMenu. Это (принудительное создание экземпляра) действительно может повлиять на производительность, пересмотрю свое решение.zelenin
09.06.2016 15:47+11. «Если меню хранится», но я то писал про конкретный кусок реализации.
Это серьезно может повлиять. И опять же не надо придумывать способ хранения меню — предоставьте интферфейс и одну-две реализации. Не надо хардкодить, навязывая. Во многих проектах уже есть меню и не в том месте, где вы его захардкодите.
hlogeon
10.06.2016 02:59Может быть я не понимаю ООП, но по моему использование правильных абстракций само по себе избавляет от ВСЕХ перечисленных Вами проблем без необходимости тащит внешнюю зависимость, да еще такую.
affka
10.06.2016 04:24Т.е. вы предлагаете реализовывать подобие мегаменю в рамках приложения, а не отдельной библиотеки?) Как использование «правильных абстракций» избавляет от всех проблем, если архитектура Yii по-умолчанию диктует где что прописывать
Radik_Wind
Посмотрел исходники и огорчен… где же PSR CodeStyle?
affka
В чем именно отличия от PSR? В том, что "{" не переносится на новую строку в классах и методах?=)
ilyaplot
И в расстановке пробелов. Но лично я не вижу ничего критического в данной ситуации.
KlimovDm
Нормальный код, прилично читается. PSR — набор рекомендаций, не более. Не огорчайтесь.
samizdam
+1 Судя по всему, автор уже привёл в соответствие с PSR.
Да, формально, PSR — всего лишь набор рекомендаций.
Но, де-факто это стандарт для open-source проектов, ибо альтернатив, достаточно распространённых чтобы быть достойными внимания, в современном PHP, лично я не вижу. Раньше, да у каждого более менее крупного фреймворка был свой стандарт.
Но php — язык интерпретируемый, с широкой практикой использования вендорных библиотек и переиспользования кода вообще. Мало приятного в чтении исходников или использовании кода от третей стороны, когда в каждой либе свой огород. Всем проще и удобнее если это будет один стиль, один формат.
Вообще, PSR, на мой взгляд, один из важных и удачных аспектов развития языка.
PS: Не важно какое соглашение вы примите, важно что оно было принято©… Чистый / Совершенный Код (Мартин / Макконнелл)??
KlimovDm
Я как-то упустил тот момент, когда на хабре следование PSR с точностью до пробела в стиле кодирования стало религией.
Как мне кажется, вы неправильно трактуйте цитату Макконнелла.
Соглашение важно принять для себя или для проекта, в котором вы участвуйте. Важно, чтобы ВАШ код (код проекта) был написан в едином стиле. Вот о каком соглашении идет речь.
Пример по аналогии: C(Си) изначально повезло больше, чем PHP, у C были Керниган-Ричи. Однако ядро Lunux написано совсем не в К&Р стайл. Тем не менее, для сообщества разработчиков ядра Linux были разработаны и написаны свои рекомендации по стилю написания кода. Соглашение было принято, но это не значит, что все C программисты в мире должны им следовать в других проектах.
Следствие: просто прекрасно, что наконец-то принят PSR по стилю написания кода. Фактически — это готовое соглашение. Его можно взять и применять. Более того, если в случае кода, который мы обсуждаем, среди разработчиков приложений для Yii2 принято соглашение о кодировании в стиле PSR (я просто не знаю) — желательно бы ему следовать. Но «расстраиваться» из за того, что автор скобку не в том месте поставил?
Код у автора был написан хорошо, в едином стиле и легко читался. В общем, вполне соответствовал «принятому соглашению» для разработки этого отдельно взятого решения.
Более того, в стиле кодирования всегда читается индивидуальность. Это как в литературе — одну книгу читать легко и интересно, другую хочется отложить в сторону после первого абзаца. А вроде обе написаны на одном языке (PHP:) — русском. Берем третью книгу, и она тоже читается легко и интересно. Но значит ли это, что она написана тем же стилем, что и предыдущая интересная книга? Конечно же — нет! Если брать конкретный пример, то, навскидку, это как стихи Маяковского и Пушкина.
Коротко резюмирую свою мысль.
— PSR — вещь хорошая и полезная.
— Расстраиваться из за того, что кто-то делает что-то полезное, но при этом не ходит строем — не стоит.
samizdam
Пользуясь предложенной литературной метафорой, я бы определил PSR не как разновидность авторского стиля, а скорее как общепринятую для языка пунктуацию — как мы переносим слова, ставим запятые, большие буквы в начале предложения и т.п.
Ожидать и желать чтобы повсеместно был PSR, это что-то сродни Grammar-nazi. Но не вижу в этом ничего плохого.
Авторский же стиль, это из области дизайна — нагородить абстрактных фабрик, фасадов или других абстракций, обеспечить всё публичными интерфейсами или запилить одним статическим божественным классом. Вот где стоит искать (и проявлять) индивидуальность, а не на уровне пунктуации =)
У Брукса приводится мысль, что жесткие рамки стиля не мешают, а наоборот помогают настоящему мастеру.
KlimovDm
Можно сказать, что истина где-то посередине. Пример с пунктуацией не корректен, потому что правила русского языка достаточно строго определены и в общем случае — это не рекомендации. Человек пишет или грамотно или нет (делает ошибки). Эта аналогия ближе к синтаксису языка (ключевые слова, переменные и т.п.) А PSR это все-таки как ни крути — визуализация. Следование или не следование PSR не отменяет наличие общей грамотности и отсутствия ошибок.
samizdam
Для ошибок на уровне синтаксиса языка, я бы привёл аналогию с грамматикой, орфографией — она однозначна как в естественных, так и в искусственных языках.
Пунктуация же, строго говоря, есть формальная, для любого языка, но может быть и авторской, что распространено в литературе.
PSR при таком взгляде, это официальная пунктуация, e.g. рекомендованная «госпожой Вербицкой» (a.k.a FIG) сообщества PHP =)
PS: это как разговорный PHP и литературный. Вроде бы, да многие делают не по PSR, но в «обществе», так говорить — моветон.