
Данному виджету необходимо передать массив элементов в определенном формате, например:
echo \yii\widgets\Menu::widget([ 'items' => [ ['label' => 'Home', 'url' => ['site/index']], ['label' => 'Products', 'url' => ['product/index'], 'items' => [ ['label' => 'New Arrivals', 'url' => ['product/index', 'tag' => 'new']], ['label' => 'Most Popular', 'url' => ['product/index', 'tag' => 'popular']], ]], ['label' => 'Login', 'url' => ['site/login'], 'visible' => Yii::$app->user->isGuest], ], ]);
Для формирования массива из данных хранимых в БД по методу Nested Sets используем специальный класс NestedSetsTree, который можно найти на разных сайтах в слегка измененном виде. Я так же внес некоторые коррективы в его работу. Разместить класс можно в common/services/NestedSetsTree.php т.к. он может потребовать для меню и backend и frontend (да и не только для меню). Для фреймворка версии basic можно просто создать общий каталог «services» в корне проекта.
<?php /* * Создание дерева элементов в виде массива */ namespace common\services; class NestedSetsTree { /** * @var string */ public $leftAttribute = 'lft'; /** * @var string */ public $depthAttribute = 'depth'; /** * @var string */ public $labelAttribute = 'name'; /** * @var string */ public $childrenOutAttribute = 'children'; /** * Построение дерева Nested Sets в виде массива * * @param array $collection Массив строк из БД * @return array */ public function tree(array $collection) { $trees = []; // Дерево if (count($collection) > 0) { //Добавляем свои элементы foreach ($collection as &$col) { $col = $this->addItem($col); } // Узел. Используется для создания иерархии $stack = array(); foreach ($collection as $node) { $item = $node; $item[$this->childrenOutAttribute] = array(); // Количество элементов узла $l = count($stack); // Проверка имеем ли мы дело с разными уровнями while($l > 0 && $stack[$l - 1][$this->depthAttribute] >= $item[$this->depthAttribute]) { array_pop($stack); $l--; } // Если это корень if ($l == 0) { // Создание корневого элемента $i = count($trees); $trees[$i] = $item; $stack[] = &$trees[$i]; } else { // Добавление элемента в родительский $i = count($stack[$l - 1][$this->childrenOutAttribute]); $stack[$l - 1][$this->childrenOutAttribute][$i] = $item; $stack[] = &$stack[$l - 1][$this->childrenOutAttribute][$i]; } } } return $trees; } /** * Добавляет в массив дополнительные элементы * @param $node array Текущий элемент массива (строка из БД) * @return array */ protected function addItem($node) { $newNode = []; return array_merge($node, $newNode); } }
В результате работы метода tree получаем готовый массив, тем не менее его нужно слегка подкорректировать, т.к. у виджета yii\widgets\Menu есть свои требования.
Например элемент массива содержащий заголовок пункта меню должен называться «label», а у нас в БД, например «name». Дочерние пункты меню должны содержаться в элементе с ключем «items», тогда как данный класс по-умолчанию его называет «children».
Кроме того виджет предусматривает использование дополнительных полей, например «visible» - для включения/выключения вывода определенных элементов меню и «active» для назначения отдельного класса активному пункту меню.
Для корректировки создадим свой класс отнаследовав его от NestedSetsTree. Для примера я создам меню frontend, поэтому разместим в frontend/services/NestedSetsTreeMenu.php:
<?php namespace frontend\services; use common\services\NestedSetsTree; use Yii; class NestedSetsTreeMenu extends NestedSetsTree { /** * @var string */ public $childrenOutAttribute = 'items'; //children /** * @var string */ public $labelOutAttribute = 'label'; //title /** * Добавляет в массив дополнительные элементы * @param $node * @return array */ protected function addItem($node) { $node = $this->renameTitle($node); //переименование элемента массива $node = $this->visible($node); //видимость элементов меню $node = $this->makeActive($node); //выделение активного пункта меню return $node; } /** * Переименовываем элемент "name" в "label" (создаем label, удаляем name) * требуется для yii\widgets\Menu * @param $node * @return array */ protected function renameTitle($node) { $newNode = [ $this->labelOutAttribute => $node[$this->labelAttribute], ]; unset($node[$this->labelAttribute]); return array_merge($node, $newNode); } /** * Видимость пункта меню (visible = false - скрыть элемент) * @param $node * @return array */ protected function visible($node) { $newNode = []; //Гость if (Yii::$app->user->isGuest) { //Действие logout по-умолчанию проверяется на метод POST. //При использовании подкорректировать VerbFilter в контроллере (удалить это действие или добавить GET). if ($node['url'] === '/logout') { $newNode = [ 'visible' => false, ]; } //Авторизованный пользователь } else { if ($node['url'] === '/login' || $node['url'] === '/signup') { $newNode = [ 'visible' => false, ]; } } return array_merge($node, $newNode); } /** * Добавляет элемент "active" в массив с url соответствующим текущему запросу * для назначения отдельного класса активному пункту меню * * @param $node * @return array */ private function makeActive($node) { //URL без хоста, слэша спереди и параметров запроса $path = Yii::$app->request->getPathInfo(); //считается, что поле url в БД содержит слэш спереди, например "/about" if('/' . $path === $node['url']){ $newNode = [ 'active' => true, ]; return array_merge($node, $newNode); } return $node; } }
Метод addItem, который переопределяется в данном классе, вызывается для каждого элемента дерева (для каждой строки из базы данных) и позволяет вносить любые изменения. В данном случае я:
- переименовал поле children в items;
- скрыл некоторые элементы меню в зависимости от статуса пользователя (для авторизованного скрыл пункты меню «вход» и «регистрация», а для гостя скрыл пункт «выход»). Касательно ссылки «/logout» еще будет ниже;
- добавил в массив содержащий в поле url значение соответствующее url текущей страницы элемент 'active' со значением true, чтобы виджет присвоил активному пункту меню дополнительный стиль (для выделения его на странице с помощью CSS).
Для работы класса NestedSetsTree ему необходимо передать коллекцию – массив строк из БД. Таким образом необходимо создать вспомогательный класс, который будет формировать коллекцию и передавать ее уже непосредственно виджету.
Файл frontend/services/MenuArray.php:
<?php namespace frontend\services; use common\models\Menu; class MenuArray { static function getData() { $collection = Menu::find()->orderBy('lft')->asArray()->all(); $menu = []; if($collection){ $nsTree = new NestedSetsTreeMenu(); $dataMenu = $nsTree->tree($collection); //создаем дерево в виде массива $menu = $dataMenu[0]['items']; //убираем корневой элемент } return $menu; } }
Т.к.корневой элемент в меню не используется и создавался только для хранения данных в БД методом Nested Sets, тут мы его удаляем строкой
$menu = $dataMenu[0]['items'];
Осталось только вывести виджет в нужном месте, например где-то в frontend/views/layouts/main.php:
<?= \yii\widgets\Menu::widget([ 'items' => \frontend\services\MenuArray::getData(), 'options' => ['id'=>'main-menu', 'class' => 'navbar'], 'encodeLabels'=>false, 'activateParents'=>true, 'activeCssClass'=>'active', ]); ?>
Строкой
'items' => \frontend\services\MenuArray::getData()передаем массив данных для меню и далее я указал некоторые опции, которые можно поменять или удалить. Подробнее про них можно почитать непосредственно в файле виджета vendor/yiisoft/yii2/widgets/Menu.php
Указание метода запроса для пункта меню.
Т.к. меню это ссылки с названиями элементов, обычно осуществляется GET запрос для перехода на нужную страницу, но иногда необходимо отправить запрос типа POST или другой.
В yii2 используется VerbFilter, который указывает каким типом запроса должен вызываться тот или иной метод. Так, например, в frontend/controllers/SiteController.php в методе behaviors можно увидить:
'verbs' => [ 'class' => VerbFilter::className(), 'actions' => [ 'logout' => ['post'], ], ],
Т.е. при вызове действия 'logout' необходим POST запрос. Просто перейдя по ссылке «/logout» получим исключение «MethodNotAllowedHttpException».
Для отправки запроса типа POST, можно тегу <a> присвоить атрибут data-method="post", тогда yii2 создаст форму c POST запросом и не придется корректировать VerbFilter.
К сожалению yii\widgets\Menu не позволяет без изменения своего кода добавить данный атрибут, но мы можем наследовать его класс и немного изменить всего один метод.
Файл common/widgets/Menu.php:
<?php namespace common\widgets; use yii\helpers\ArrayHelper; use yii\helpers\Html; use yii\helpers\Url; use \yii\widgets\Menu as MenuYii; class Menu extends MenuYii { protected function renderItem($item) { if (isset($item['url'])) { $template = ArrayHelper::getValue($item, 'template', $this->linkTemplate); return strtr($template, [ '{url}' => Html::encode(Url::to($item['url'])), '{label}' => $item['label'], '{method}' => isset($item['method']) ? $item['method'] : 'get', //добавляем атрибут data-method ]); } $template = ArrayHelper::getValue($item, 'template', $this->labelTemplate); return strtr($template, [ '{label}' => $item['label'], ]); } }
Тут я добавил всего одну строку
'{method}' => isset($item['method']) ? $item['method'] : 'get',Теперь когда код виджета будет создавать ссылку, он заменит {method} на значение указанное в элементе массива «method».
Добавим этот элемент в массив, для этого создадим еще один метод в классе NestedSetsTreeMenu:
- подключаем его в addItem:
protected function addItem($node) { $node = $this->establishMethod($node); $node = $this->renameTitle($node); //переименование элемента массива $node = $this->visible($node); //видимость элементов меню $node = $this->makeActive($node); //выделение активного пункта меню return $node; }
- добавляем ниже внутри класса:
/** * Добавление элемента method со значением post для формирования атрибута data-method="post" * @param $node * @return array */ protected function establishMethod($node) { if($node['url'] === '/logout'){ $newNode = [ 'method' => 'post', ]; return array_merge($node, $newNode); } return $node; }
Итак, в массив у которого в поле url будет «/logout» добавится новый элемент 'method' со значением 'post'.
Осталось подключить свой класс виджета, который был переопределен:
<?= \common\widgets\Menu::widget([ 'items' => \frontend\services\MenuArray::getData(), 'options' => ['id'=>'main-menu', 'class' => 'navbar'], 'encodeLabels'=>false, 'activateParents'=>true, 'activeCssClass'=>'active', //добавление атрибута data-method 'linkTemplate' => '<a href="{url}" data-method="{method}">{label}</a>', ]); ?>
Тут я указал новое пространство имен класса Menu и дописал свойство «linkTemplate» виджета, которое определяет как должна выглядеть ссылка, добавил в нее атрибут «data-method».
Итак, в зависимости от структуры вашего меню, вывод виджета без оформления стилей CSS может выглядеть, например, так:
