2

Yii: динамическое меню для backend приложения

Posted апреля 14, 2010 in Yii and tagged , , , by SpiRi7

В прошлых двух заметках я рассказал о том как производить установку модулей в 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)

Результатом работы структуры созданной в рамках статьи будет

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

Похожие посты:

2 Responses so far.

  1. snnwolf пишет:

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

  2. Konstantin пишет:

    Спасибо, классная идея, сделал себе такое :)

Leave a Reply