Вступление
Многие начинающие веб-разработчики сталкиваются с необходимостью создания меню, каталогов или рубрикаторов для своего проекта на Yii2, которые бы имели иерархическую структуру, но при этом поддерживали мультиязычность. Задача довольно простая, но не совсем очевидная в рамках данного фреймворка. Есть большое количество готовых расширений для создания древовидных структур (меню, каталогов итд.), но довольно сложно найти решение, которое бы поддерживало полноценную работу с несколькими языками. Причём речь тут идёт не о переводе интерфейса штатными средствами фреймворка, а про хранение данных в базе на нескольких языках. Также достаточно сложно найти удобный и полностью работоспособный виджет для управления деревом, который мог бы также работать с многоязычным контентом без сложных манипуляций с кодом.
Я хотел бы поделиться рецептом того, как можно создавать подобные модули на примере реализации модуля меню. Для примера я буду использовать шаблон приложения Yii2 App Basic, но вы можете адаптировать всё под свой шаблон, если он отличается от базового.
Подготовка
Для реализации задачи нам понадобится несколько замечательных расширений, а именно:
- Adjacency List — для хранения древовидной структуры
меню в БД; - Yii2 Bootstrap Treeview — виджет
для удобного отображения меню в виде дерева; - Translateable Behavior — поведение для
поддержки мультиязычности в моделях;
Устанавливаем данные расширения через composer:
composer require paulzi/yii2-adjacency-list
composer require execut/yii2-widget-bootstraptreeview
composer require creocoder/yii2-translateable
Для реализации меню в виде модуля, с помощью Gii генератора (либо вручную) создаём новый модуль menu и подключаем его в настройках приложения.
В проекте также должен быть настроен механизм переключения языков. Я предпочитаю использовать вот это расширение для Yii2.
Создание моделей
Для хранения меню (либо другой сущности, которая имеет мультиязычность) в базе данных нам необходимо создать две таблицы. На самом деле, для хранения мультиязычных данных могут использоваться разные методики, но вариант с двумя таблицами, одна из которых хранит саму сущность, а вторая — её языковые вариации, мне нравится больше остальных. Для создания таблиц удобно использовать миграции. Вот пример такой миграции:
<?php
use yii\db\Schema;
use yii\db\Migration;
class m180819_083502_menu_init extends Migration
{
public function init()
{
$this->db = 'db';
parent::init();
}
public function safeUp()
{
$tableOptions = 'ENGINE=InnoDB';
$this->createTable('{{%menu}}', [
'id'=> $this->primaryKey(11),
'parent_id'=> $this->integer(11)->null()->defaultValue(null),
'link'=> $this->string(255)->notNull()->defaultValue('#'),
'link_attributes'=> $this->text()->notNull(),
'icon_class'=> $this->string(255)->notNull(),
'sort'=> $this->integer(11)->notNull()->defaultValue(0),
'status'=> $this->tinyInteger(1)->notNull()->defaultValue(1),
], $tableOptions);
$this->createIndex('parent_sort', '{{%menu}}', ['parent_id','sort'], false);
$this->createTable('{{%menu_lang}}', [
'owner_id'=> $this->integer(11)->notNull(),
'language'=> $this->string(2)->notNull(),
'name'=> $this->string(255)->notNull(),
'title'=> $this->text()->notNull(),
], $tableOptions);
$this->addPrimaryKey('pk_on_menu_lang', '{{%menu_lang}}', ['owner_id','language']);
$this->addForeignKey(
'fk_menu_lang_owner_id',
'{{%menu_lang}}',
'owner_id',
'{{%menu}}',
'id',
'CASCADE',
'CASCADE'
);
// Insert sample data
$this->batchInsert(
'{{%menu}}',
['id', 'parent_id', 'link', 'link_attributes', 'icon_class', 'sort', 'status'],
[
[
'id' => '1',
'parent_id' => null,
'link' => '#',
'link_attributes' => '',
'icon_class' => '',
'sort' => '0',
'status' => '0',
],
[
'id' => '2',
'parent_id' => '1',
'link' => '/',
'link_attributes' => '',
'icon_class' => 'fa fa-home',
'sort' => '0',
'status' => '1',
],
]
);
$this->batchInsert(
'{{%menu_lang}}',
['owner_id', 'language', 'name', 'title'],
[
[
'owner_id' => '1',
'language' => 'ru',
'name' => 'Главное меню',
'title' => '',
],
[
'owner_id' => '1',
'language' => 'en',
'name' => 'Main menu',
'title' => '',
],
[
'owner_id' => '2',
'language' => 'ru',
'name' => 'Главная',
'title' => 'Главная страница сайта',
],
[
'owner_id' => '2',
'language' => 'en',
'name' => 'Home',
'title' => 'Site homepage',
],
]
);
}
public function safeDown()
{
$this->truncateTable('{{%menu}} CASCADE');
$this->dropForeignKey('fk_menu_lang_owner_id', '{{%menu_lang}}');
$this->dropTable('{{%menu}}');
$this->dropPrimaryKey('pk_on_menu_lang', '{{%menu_lang}}');
$this->dropTable('{{%menu_lang}}');
}
}
Поместим данный файл миграции в папку /migrations нашего проекта, и
выполним в консоли команду:
php yii migrate
После того, как мы создали необходимые таблицы и добавили в них новое меню с помощью миграции, нам нужно создать модели. Так как в проекте мультиязычность и деревья могут встречаться не только в меню, но и в других сущностях (например, страницы сайта), я предлагаю вынести методы, которые реализуют механизм мультиязычности и организацию дерева, в отдельные трейты, чтобы в дальнейшем мы могли легко использовать их в других моделях без дублирования кода. Создадим в корне приложения папочку traits (если её там ещё нет) и поместим туда два файла:
<?php
namespace app\traits;
use Yii;
use yii\behaviors\SluggableBehavior;
use creocoder\translateable\TranslateableBehavior;
trait LangTrait
{
public static function langClass()
{
return self::class . 'Lang';
}
public static function langTableName()
{
return self::tableName() . '_lang';
}
public function langBehaviors($translationAttributes)
{
return [
'translateable' => [
'class' => TranslateableBehavior::class,
'translationAttributes' => $translationAttributes,
'translationRelation' => 'translations',
'translationLanguageAttribute' => 'language',
],
];
}
public function transactions()
{
return [
self::SCENARIO_DEFAULT => self::OP_INSERT | self::OP_UPDATE,
];
}
public function getLang()
{
return $this->hasOne(self::langClass(), ['owner_id' => 'id'])->where([self::langTableName() . '.language' => Yii::$app->language]);
}
public function getTranslations()
{
return $this->hasMany(self::langClass(), ['owner_id' => 'id']);
}
}
<?php
namespace app\traits;
use Yii;
use yii\helpers\Html;
use paulzi\adjacencyList\AdjacencyListBehavior;
trait TreeTrait
{
private static function getQueryClass()
{
return self::class . 'Query';
}
public function treeBehaviors()
{
return [
'tree' => [
'class' => AdjacencyListBehavior::class,
'parentAttribute' => 'parent_id',
'sortable' => [
'step' => 10,
],
'checkLoop' => false,
'parentsJoinLevels' => 5,
'childrenJoinLevels' => 5,
],
];
}
public static function find()
{
$queryClass = self::getQueryClass();
return new $queryClass(get_called_class());
}
public static function listTree($node = null, $level = 1, $nameAttribute = 'name', $prefix = '-->')
{
$result = [];
if (!$node) {
$node = self::find()->roots()->one()->populateTree();
}
if ($node->isRoot()) {
$result[$node['id']] = mb_strtoupper($node[$nameAttribute ?: 'slug']);
}
if ($node['children']) {
foreach ($node['children'] as $child) {
$result[$child['id']] = str_repeat($prefix, $level) . $child[$nameAttribute];
$result = $result + self::listTree($child, $level + 1, $nameAttribute);
}
}
return $result;
}
public static function treeViewData($node = null)
{
if ($node === null) {
$node = self::find()->roots()->one()->populateTree();
}
$result = null;
$items = null;
$children = null;
if ($node['children']) {
foreach ($node['children'] as $child) {
$items[] = self::treeViewData($child);
}
$children = call_user_func_array('array_merge', $items);
}
$result[] = [
'text' => Html::a($node['lang']['name'] ?: $node['id'], ['update', 'id' => $node['id']], ['title' => Yii::t('app', 'Редактировать элемент')]),
'tags' => [
Html::a(
'<i class="glyphicon glyphicon-arrow-down"></i>',
['move-down', 'id' => $node['id']],
['title' => Yii::t('app', 'Передвинуть вниз')]
),
Html::a(
'<i class="glyphicon glyphicon-arrow-up"></i>',
['move-up', 'id' => $node['id']],
['title' => Yii::t('app', 'Передвинуть вверх')]
)
],
'backColor' => $node['status'] == 0 ? '#ccc' : '#fff',
'selectable' => false,
'nodes' => $children,
];
return $result;
}
}
Теперь создадим непосредственно сами модели для работы с меню, в которых подключим трейты для дерева и мультиязычности. Модели помещаем в /modules/menu/models:
<?php
namespace app\modules\menu\models;
use Yii;
class Menu extends \yii\db\ActiveRecord
{
use \app\traits\TreeTrait;
use \app\traits\LangTrait;
const STATUS_ACTIVE = 1;
const STATUS_INACTIVE = 0;
public function behaviors()
{
$behaviors = [];
return array_merge(
$behaviors,
$this->treeBehaviors(),
$this->langBehaviors(['name', 'title'])
);
}
public static function tableName()
{
return 'menu';
}
public function rules()
{
return [
[['parent_id', 'sort', 'status'], 'integer'],
[['link', 'icon_class'], 'string', 'max' => 255],
[['link_attributes'], 'string'],
[['link'], 'default', 'value' => '#'],
[['link_attributes', 'icon_class'], 'default', 'value' => ''],
[['parent_id'], 'exist', 'skipOnError' => true, 'targetClass' => self::class, 'targetAttribute' => ['parent_id' => 'id']],
];
}
public function attributeLabels()
{
return [
'id' => Yii::t('app', 'ID'),
'parent_id' => Yii::t('app', 'Родитель'),
'link' => Yii::t('app', 'Ссылка'),
'link_attributes' => Yii::t('app', 'Атрибуты ссылки (JSON массив)'),
'icon_class' => Yii::t('app', 'Класс иконки'),
'sort' => Yii::t('app', 'Сортировка'),
'status' => Yii::t('app', 'Опубликован'),
];
}
public static function menuItems($node = null)
{
if ($node === null) {
$node = self::find()->roots()->one()->populateTree();
}
$result = null;
$items = null;
$children = null;
if ($node['children']) {
foreach ($node['children'] as $child) {
$items[] = self::menuItems($child);
}
$children = call_user_func_array('array_merge', $items);
}
$result[] = [
'label' => ($node['icon_class'] ? '<i class="' . $node['icon_class'] . '"></i> ' . ($node['lang']['name'] ?: $node['id']) : ($node['lang']['name'] ?: $node['id'] )),
'encode' => ($node['icon_class'] ? false : true),
'url' => [$node['link'], 'language' => Yii::$app->language],
'active' => $node['link'] == Yii::$app->request->url ? true : false,
'linkOptions' => ($node['link_attributes'] ? array_merge(json_decode($node['link_attributes'], true), ['title' => ($node['lang']['title'] ?: $node['lang']['name'])]) : ['title' => ($node['lang']['title'] ?: $node['lang']['name'])]),
'items' => $children,
];
return $result;
}
}
<?php
namespace app\modules\menu\models;
use Yii;
class MenuLang extends \yii\db\ActiveRecord
{
public static function tableName()
{
return 'menu_lang';
}
public function rules()
{
return [
[['name'], 'required'],
[['name', 'title'], 'string', 'max' => 255],
];
}
public function attributeLabels()
{
return [
'owner_id' => Yii::t('app', 'Владелец'),
'language' => Yii::t('app', 'Язык'),
'name' => Yii::t('app', 'Название'),
'title' => Yii::t('app', 'Всплывающая подсказка'),
];
}
public function getOwner()
{
return $this->hasOne(Menu::class, ['id' => 'owner_id']);
}
}
<?php
namespace app\modules\menu\models;
use paulzi\adjacencyList\AdjacencyListQueryTrait;
class MenuQuery extends \yii\db\ActiveQuery
{
use AdjacencyListQueryTrait;
}
<?php
namespace app\modules\menu\models;
use Yii;
use yii\base\Model;
use yii\data\ActiveDataProvider;
use app\modules\menu\models\Menu;
class MenuSearch extends Menu
{
public $name;
public function rules()
{
return [
[['id', 'parent_id', 'sort', 'status'], 'integer'],
[['link', 'link_attributes', 'icon_class'], 'safe'],
[['name'], 'safe'],
];
}
public function scenarios()
{
return Model::scenarios();
}
public function search($params)
{
$query = parent::find()->joinWith(['lang']);
$dataProvider = new ActiveDataProvider([
'query' => $query,
'sort' => ['defaultOrder' => ['sort' => SORT_ASC]]
]);
$dataProvider->sort->attributes['name'] = [
'asc' => [
'menu_lang.name' => SORT_ASC,
],
'desc' => [
'menu_lang.name' => SORT_DESC,
],
];
$this->load($params);
if (!$this->validate()) {
return $dataProvider;
}
$query->andFilterWhere([
'id' => $this->id,
'parent_id' => $this->parent_id,
'sort' => $this->sort,
'status' => $this->status,
]);
$query->andFilterWhere(['like', 'link', $this->link]);
$query->andFilterWhere(['like', 'link_attributes', $this->link_attributes]);
$query->andFilterWhere(['like', 'icon_class', $this->icon_class]);
$query->andFilterWhere(['like', 'name', $this->name]);
return $dataProvider;
}
}
Создание контроллеров
Для осуществления CRUD операций над мультиязычными деревьями нам нужен контроллер. Чтобы упростить себе жизнь в будущем, мы создадим один базовый контроллер, в котором будут все необходимые действия, а для разных сущностей, будь то меню, или каталог, или страницы — будем наследоваться от него.
Классы нашего проекта, которые мы будем использовать как базовые, мы разместим в папке /base. Создадим файл /base/controllers/AdminLangTreeController.php. Этот контроллер у нас будет базовым для CRUD всех сущностей, в которых реализовано дерево и мультиязычность:
<?php
namespace app\base\controllers;
use Yii;
use yii\web\Controller;
use yii\web\NotFoundHttpException;
use yii\filters\VerbFilter;
use yii\helpers\Url;
class AdminLangTreeController extends Controller
{
public $modelClass;
public $modelClassSearch;
public $modelName;
public $modelNameLang;
public function behaviors()
{
return [
'verbs' => [
'class' => VerbFilter::class,
'actions' => [
'delete' => ['POST'],
],
],
];
}
public function actionIndex()
{
// Если корневой элемент дерева отсутствует, он будет создан автоматически
if (count($this->modelClass::find()->roots()->all()) == 0) {
$model = new $this->modelClass;
$model->makeRoot()->save();
Yii::$app->session->setFlash('info', Yii::t('app', 'Корневой элемент создан автоматически'));
return $this->redirect(['index']);
}
$searchModel = new $this->modelClassSearch;
$dataProvider = $searchModel->search(Yii::$app->request->queryParams);
$dataProvider->pagination = false;
return $this->render('index', [
'searchModel' => $searchModel,
'dataProvider' => $dataProvider,
]);
}
public function actionCreate()
{
// Проверка наличия корневого элемента
if (count($this->modelClass::find()->roots()->all()) == 0) {
return $this->redirect(['index']);
}
// Создание новой записи и привязка её к дереву
$model = new $this->modelClass;
$root = $model::find()->roots()->one();
$model->parent_id = $root->id;
// Загрузка моделей из формы
if ($model->load(Yii::$app->request->post()) && $model->validate()) {
$parent = $model::findOne($model->parent_id);
$model->appendTo($parent)->save();
// Сохраняем мультиязычные данные
foreach (Yii::$app->request->post($this->modelNameLang, []) as $language => $data) {
foreach ($data as $attribute => $translation) {
$model->translate($language)->$attribute = $translation;
}
}
$model->save();
Yii::$app->session->setFlash('success', Yii::t('app', 'Создание прошло успешно'));
return $this->redirect(['update', 'id' => $model->id]);
} else {
return $this->render('create', [
'model' => $model,
]);
}
}
public function actionUpdate($id)
{
// Находим нужную модель
$model = $this->modelClass::find()->with('translations')->where(['id' => $id])->one();
if ($model === null) {
throw new NotFoundHttpException(Yii::t('app', 'Страница не найдена'));
}
// Загрузка данных из формы
if ($model->load(Yii::$app->request->post()) && $model->save()) {
foreach (Yii::$app->request->post($this->modelNameLang, []) as $language => $data) {
foreach ($data as $attribute => $translation) {
$model->translate($language)->$attribute = $translation;
}
}
$model->save();
Yii::$app->session->setFlash('success', Yii::t('app', 'Обновление произведено успешно'));
if (Yii::$app->request->post('save') !== null) {
return $this->redirect(['index']);
}
return $this->redirect(['update', 'id' => $model->id]);
} else {
return $this->render('update', [
'model' => $model,
]);
}
}
public function actionDelete($id)
{
$model = $this->findModel($id);
// Запрещаем удаление узла, если у него есть потомки
if (count($model->children) > 0) {
Yii::$app->session->setFlash('error', Yii::t('app', 'Элемент не может быть удалён, так как содержит дочерние элементы. Сначала нужно удалить все дочерние элементы'));
return $this->redirect(['index']);
}
// Запрещаем удаление корневого элемента
if ($model->isRoot()) {
Yii::$app->session->setFlash('error', Yii::t('app', 'Нельзя удалять корневой элемент'));
return $this->redirect(['index']);
}
// Удаляем элемент
if ($model->delete()) {
Yii::$app->session->setFlash('success', Yii::t('app', 'Удаление произведено успешно'));
}
return $this->redirect(['index']);
}
public function actionMoveUp($id)
{
$model = $this->findModel($id);
if ($prev = $model->getPrev()->one()) {
$model->moveBefore($prev)->save();
$model->reorder(false);
} else {
Yii::$app->session->setFlash('error', Yii::t('app', 'Невозможно передвинуть элемент вверх'));
}
return $this->redirect(Yii::$app->request->referrer);
}
public function actionMoveDown($id)
{
$model = $this->findModel($id);
if ($next = $model->getNext()->one()) {
$model->moveAfter($next)->save();
$model->reorder(false);
} else {
Yii::$app->session->setFlash('error', Yii::t('app', 'Невозможно передвинуть элемент вниз'));
}
return $this->redirect(Yii::$app->request->referrer);
}
protected function findModel($id)
{
if (($model = $this->modelClass::findOne($id)) !== null) {
return $model;
} else {
throw new NotFoundHttpException(Yii::t('app', 'Страница не найдена'));
}
}
}
Теперь в модуле создадим файл /modules/menu/controllers/AdminController.php. Это будет основной контроллер для управления меню, и, так как он реализует дерево и мультиязычность, будет наследоваться от базового, который мы уже создали в предыдущем шаге:
<?php
namespace app\modules\menu\controllers;
use app\base\controllers\AdminLangTreeController as BaseController;
class AdminController extends BaseController
{
public $modelClass = \app\modules\menu\models\Menu::class;
public $modelClassSearch = \app\modules\menu\models\MenuSearch::class;
public $modelName = 'Menu';
public $modelNameLang = 'MenuLang';
}
Как видите, код данного контроллера содержит лишь названия моделей и их классов. Тоесть, для создания CRUD контроллеров других модулей (каталога, рубрикатора итд.), которые также будут использовать дерево и мультиязычность, можно поступать аналогичным способом — расширять базовый контроллер.
Создание интерфейса для управления меню
Завершающий этап — создание интерфейса для управления мультиязычным деревом. С задачей отображения дерева отлично справляется расширение Bootstrap Treeview, которое можно достаточно гибко настроить и оно поддерживает множество удобных функций (например, поиск по дереву). Создадим индексный view для отображения самого дерева, и поместим его в /modules/menu/views/admin/index.php:
<?php
use yii\helpers\Html;
use yii\grid\GridView;
use yii\widgets\ActiveForm;
use execut\widget\TreeView;
$this->title = Yii::t('app', 'Меню сайта');
$this->params['breadcrumbs'][] = $this->title;
?>
<div class="row">
<div class="col-md-6">
<div class="panel panel-primary">
<div class="panel-heading">
<?= Html::a(Yii::t('app', 'Создать'), ['create'], ['class' => 'btn btn-success btn-flat']) ?>
</div>
<div class="panel-body">
<?= TreeView::widget([
'id' => 'tree',
'data' => $searchModel::treeViewData($searchModel::find()->roots()->one()),
'header' => Yii::t('app', 'Меню сайта'),
'searchOptions' => [
'inputOptions' => [
'placeholder' => Yii::t('app', 'Поиск по дереву') . '...'
],
],
'clientOptions' => [
'selectedBackColor' => 'rgb(40, 153, 57)',
'borderColor' => '#fff',
'levels' => 10,
'showTags' => true,
'tagsClass' => 'badge',
'enableLinks' => true,
],
]) ?>
</div>
</div>
</div>
</div>
Вот мы дошли до самого интересного этапа данного кейса: как правильно создать форму для создания/редактирования мультиязычных данных. Создаём в папке /modules/menu/views/admin три файла:
<?php
use yii\helpers\Html;
$this->title = Yii::t('app', 'Создать');
$this->params['breadcrumbs'][] = ['label' => Yii::t('app', 'Меню сайта'), 'url' => ['index']];
$this->params['breadcrumbs'][] = $this->title;
echo $this->render('_form', [
'model' => $model,
]);
<?php
use yii\helpers\Html;
$this->title = Yii::t('app', 'Обновить') . ': ' . $model->name;
$this->params['breadcrumbs'][] = ['label' => Yii::t('app', 'Меню сайта'), 'url' => ['index']];
$this->params['breadcrumbs'][] = ['label' => $model->name, 'url' => ['update', 'id' => $model->id]];
$this->params['breadcrumbs'][] = Yii::t('app', 'Обновить');
echo $this->render('_form', [
'model' => $model,
]);
<?php
use yii\helpers\Html;
use yii\widgets\ActiveForm;
if ($model->isNewRecord) {
$model->status = true;
}
?>
<div class="panel panel-primary">
<?php $form = ActiveForm::begin(); ?>
<div class="panel-body">
<fieldset>
<legend><?= Yii::t('app', 'Общие настройки') ?></legend>
<div class="row">
<div class="col-md-4">
<?php if (!$model->isRoot()) { ?>
<?= $form->field($model, 'parent_id')->dropDownList($model::listTree()) ?>
<?php } ?>
<?= $form->field($model, 'link')->textInput(['maxlength' => true]) ?>
<?= $form->field($model, 'link_attributes')->textInput(['maxlength' => true]) ?>
<?= $form->field($model, 'icon_class')->textInput(['maxlength' => true]) ?>
<?= $form->field($model, 'status')->checkbox() ?>
</div>
</div>
</fieldset>
<fieldset>
<legend><?= Yii::t('app', 'Содержание') ?></legend>
<!-- Nav tabs -->
<ul class="nav nav-tabs" role="tablist">
<?php foreach (Yii::$app->urlManager->languages as $key => $language) { ?>
<li role="presentation" <?= $key == 0 ? 'class="active"' : '' ?>>
<a href="#tab-content-<?= $language ?>" aria-controls="tab-content-<?= $language ?>" role="tab" data-toggle="tab"><?= $language ?></a>
</li>
<?php } ?>
</ul>
<!-- Tab panes -->
<div class="tab-content">
<?php foreach (Yii::$app->urlManager->languages as $key => $language) { ?>
<div role="tabpanel" class="tab-pane <?= $key == 0 ? 'active' : '' ?>" id="tab-content-<?= $language ?>">
<?= $form->field($model->translate($language), "[$language]name")->textInput() ?>
<?= $form->field($model->translate($language), "[$language]title")->textInput() ?>
</div>
<?php } ?>
</div>
</fieldset>
</div>
<div class="box-footer">
<?= Html::submitButton($model->isNewRecord ? '<i class="fa fa-plus"></i> ' . Yii::t('app', 'Создать') : '<i class="fa fa-refresh"></i> ' . Yii::t('app', 'Обновить'), ['class' => $model->isNewRecord ? 'btn btn-primary' : 'btn btn-success']) ?>
<?= !$model->isNewRecord ? Html::submitButton('<i class="fa fa-save"></i> ' . Yii::t('app', 'Сохранить'), ['class' => 'btn btn-warning', 'name' => 'save']) : ''; ?>
<?= !$model->isNewRecord ? Html::a('<i class="fa fa-trash"></i> ' . Yii::t('app', 'Удалить'), ['delete', 'id' => $model->id], ['class' => 'btn btn-danger', 'data' => ['confirm' => Yii::t('app', 'Вы уверены, что хотите удалить этот элемент?'), 'method' => 'post']]) : ''; ?>
</div>
<?php ActiveForm::end(); ?>
</div>
Не забываем, что в приложении должен быть указан язык по умолчанию (параметр language), а в параметрах UrlManager — массив со списком языков (languages), которые мы будем использовать. Язык по умолчанию должен быть первым в это массиве.
Заключение
В итоге мы должны получить следующее:
- Готовый модуль для мультиязычного древовидного меню сайта с удобным и настраиваемым интерфейсом;
- Базовый CRUD контроллер, который можно наследовать при создании других модулей, в которых используется дерево и мультиязычность;
- Два трейта (мультиязычность и дерево), которые можно подключать к моделям для имплементации соответствующих функций.
Я надеюсь, что данная статья станет полезной и поможет вам в разработке новых хороших проектов на Yii2.
Mylistryx
Я может не совсем понял, но что мешало просто использовать i18n и не городить это хрен-пойми что? Нет переводов по умолчанию? Ну так ловить непереведенные фразы в нужной локали и сохранять в БД, где можно и хранить переводы, а не в файлах…
Какая то поделка из серии лишь бы было, точнее пример того, как делать ненадо. Кто плюсует? Объясните свою позицию…
kwazaro Автор
Вы видимо не совсем поняли суть данного решения. Это не переводы интерфейса, как предусматривает механизм i18n, и не переводы фреймворка. Это механизм хранения мультиязычного контента (это может быть и содержимое динамических страниц, мета-данные для SEO итд.). К тому же, тут показан рабочий вариант виджета по управлению деревом с поддержкой мультиязычности (я, к сожалению, не нашёл такого, когда столкнулся с подобной задачей). Если у Вас есть предложения или готовые решения по способу организации мультиязычного контента в Yii2 — можете поделиться. Как говориться, «критикуешь — предлагай».