В прошлых двух заметках я рассказал о том как производить установку модулей в Yii и про организацию backend обработчиков. Сегодня поговорим о возможности динамического формирования меню в backend для каждого установленного модуля, на основании конфигурационных файлов.
Для формирования меню в администратировной части будем использовать дополнительное расширение mbmenu. Этот виджет позволяет строить меню с выпадающими элементами на основании переданного массива элементов.
$this->widget('application.extensions.mbmenu.MbMenu',array( 'items'=>array( array('label'=>'Home', 'url'=>array('/site/index')), array('label'=>'About', 'url'=>array('/site/page', 'view'=>'about'), 'items'=>array( array('label'=>'Contact', 'url'=>array('/site/contact')), array('label'=>'sub 2 contact', 'url'=>array('/admin/invoice/')), ) ), array('label'=>'Login', 'items'=>array( array('label'=>'Login', 'url'=>array('/site/login'), 'visible'=>Yii::app()->user->isGuest), array('label'=>'Logout ('.Yii::app()->user->name.')', 'url'=>array('/site/logout'), 'visible'=>!Yii::app()->user->isGuest) ), ), array('label'=>'Content', 'items'=>array( array('label'=>'Categories', 'url'=>array('/content/category')), array('label'=>'Articles', 'url'=>array('/content/articles')) ), ), ), 'htmlOptions' => array('class' => 'my-class') ));
Добавим в каждый xml конфигурационный файл модуля раздел содержащий пункты меню для административной части
<?xml version="1.0"?> <config> <name>content</name> <version>1.0.0</version> <active>true</active> <creationDate>2009-03-27</creationDate> <author>SpiRi7</author> <authorEmail>alex@spiri7.net</authorEmail> <authorUrl>http://spiri7.net</authorUrl> <copyright></copyright> <license></license> <description>Content module</description> <adminhtml> <menu> <content> <label>Content</label> <sort_order>5</sort_order> <items> <categories> <label>Categories Manager</label> <sort_order>1</sort_order> <url>/admin/content/category</url> </categories> <articles> <label>Articles Manager</label> <sort_order>2</sort_order> <url>/admin/content/articles</url> </articles> </items> </content> </menu> </adminhtml> </config>
Разберем структуру xml файла.
Содержимое меню находится в ноде adminhtml->menu. Каждый узел представляет собой один пункт меню. “content” — уникальный идентификатор пункта меню, является верхним уровнем дерева меню. “label” — текст пункта, “sort_order” — сквозной порядок сортировки, “items” — дочерние элементы меню, “categories” и “articles” дочернии идентификаторы меню, “url” — действие выполняемое по щелчку мышки (если не указано, действие не выполняется).
Для построения меню добавим layout административной части ( у меня он называется backend.php) следующий код
$backendMenu = $this->getBackendMenu(); $this->widget('application.extensions.mbmenu.MbMenu',$backendMenu);
Метод getBackendMenu находится в базовом контроллере для всех контроллеров административной части приложения. Он возвращает массив элементов меню. Этот метод мы рассмотрим чуть позже.
Перейдем к части анализа xml файла конфигурации и генерация кеша элементов меню. Для этого добавим следующий код в метод actionIndex контроллера InstallController модуля core.
$model = new Resource(); // Refresh backend menu manager $model->refreshBackendMenu();
метод производит обновление кеша меню установленных модулей. Рассмотрим его содержимое. Файл модели Resource.php
/** * Force refresh backend menu elements. * Read all xml configuration file, analize and create cache */ public function refreshBackendMenu() { $totalBackendMenuArray = array(); // Get all modules configuration files (configuration stored as xml file) // For each file recive backend menu and merge to total backend menu // @todo get config file only for installed modules (not for all config into dirrectory) $configFileList = glob(YiiBase::getPathOfAlias('application.config.modules')."/*.xml"); foreach ($configFileList as $singleConfigFile) { $config = new SimpleXMLElement($singleConfigFile, NULL, true); $nodes = $config->xpath('/config/adminhtml/menu/*'); $menuItemsForModule = $this->parsingXmlMenu($nodes); $totalBackendMenuArray = CMap::mergeArray($totalBackendMenuArray, $menuItemsForModule); } // Sorting total menu items array by key "sort_order" $this->sortingMenuItems($totalBackendMenuArray); $outputMenu['items'] = $this->convertXmlMenuFormatToOutputFormat($totalBackendMenuArray); $fh = fopen(YiiBase::getPathOfAlias('application.cache')."/backendmenu.ser", "w"); fwrite($fh, serialize($outputMenu)); fclose($fh); } protected function convertXmlMenuFormatToOutputFormat($xmlMenuFormat) { $outputMenu = array(); foreach ($xmlMenuFormat as $single) { $menuItem = array(); $menuItem['label'] = $single['label']; if (isset($single['url'])) { $menuItem['url'] = array($single['url']); } if (isset($single['items'])) { $menuItem['items'] = $this->convertXmlMenuFormatToOutputFormat($single['items']); } $outputMenu[] = $menuItem; } return $outputMenu; } /** * Recursive function for parsing XML Object with menu information * * @param SimpleXMLElement $nodeElements xml element of current level */ protected function parsingXmlMenu($nodeElements) { $returnArray = array(); foreach ($nodeElements as $element) { $nodeName = $element->getName(); $returnArray[$nodeName] = array(); if ($element->label) { $returnArray[$nodeName]['label'] = $element->label.""; } if ($element->sort_order) { $returnArray[$nodeName]['sort_order'] = $element->sort_order.""; } if ($element->url) { $returnArray[$nodeName]['url'] = $element->url.""; } if ($element->items) { $returnArray[$nodeName]['items'] = $this->parsingXmlMenu($element->xpath("items/*")); } } return $returnArray; } /** * Recursive function for sorting all menu for backend * including children items. Return nothing. Result in input param * $menuItems * * @param array $menuItems */ protected function sortingMenuItems(&$menuItems) { uasort($menuItems, "Resource::sortingByKeySortOrder"); foreach ($menuItems as $key => $item) { if (isset($item['items'])) { $this->sortingMenuItems($menuItems[$key]['items']); } } } /** * User definated array sorting function * @param array $a item of array * @param array $b item of array * @return result of compare 0, 1, -1 */ public static function sortingByKeySortOrder ($a, $b) { if ($a['sort_order'] == $b['sort_order']) return 0; return ($a['sort_order'] > $b['sort_order']) ? 1 : -1; }
Рассмотрим созданные методы
refreshBackendMenu производит считывание всех конфигурационных файлов модулей, парсит xml (метод parsingXmlMenu) содержимое и формирует общий массив меню админки ($totalBackendMenuArray). Обратите внимание что для объединения массивов используется функция CMap::mergeArray которая производит слияние элементов дополняя и заменяя дублирующие. После этого производится сортировка элементов согласно ключу sort_order (метод sortingMenuItems). Далее производится отсечение не нужных элементов массива(метод convertXmlMenuFormatToOutputFormat). И запись новых элементов меню в кеш файл (как serialize данные).
Рассмотрим метод который производит считывание кеша и возвращает массив элементов меню.
Данный код помещает в AdminController
public function getBackendMenu() { // File with serialize menu data $fname = YiiBase::getPathOfAlias('application.cache')."/backendmenu.ser"; if (!file_exists($fname)) { // When file not exits // Create it by calling method to refresh menu $model = new Resource(); // Refresh backend menu manager $model->refreshBackendMenu(); // Show notification to user Yii::app()->user->setFlash('success',"Create backend menu success."); } // Read file content and return array of menu $fh = fopen($fname, "r"); $outputMenu = fread($fh, filesize($fname)); $outputMenu = unserialize($outputMenu); fclose($fh); return $outputMenu; }
Обратите внимание, если файла с кешем не существует проивозится вызов метода который создает файл с данными о меню.
Немного подробнее остановится на формировании xml файлов.
<adminhtml> <menu> <system> <items> <invoice> <label>Manage Invoice</label> <sort_order>1</sort_order> <url>/admin/invoice/index</url> </invoice> </items> </system> </menu> </adminhtml>
Такая структура позволит нам добавить еще один дочерний элемент в меню с идентификатором system, который будет размещен первым (sort_order = 1)
Результатом работы структуры созданной в рамках статьи будет

Спасибо за внимание. Буду рад услышать Ваши комментарии и отзывы.

Спасибо. Классная заметка.
Спасибо, классная идея, сделал себе такое