Árvores multilíngues no Yii2 como um exemplo de criação de um módulo de menu

Entrada


Muitos desenvolvedores da Web iniciantes enfrentam a necessidade de criar menus, diretórios ou categorias para seu projeto Yii2, que teriam uma estrutura hierárquica, mas, ao mesmo tempo, suportariam o multilinguismo. A tarefa é bastante simples, mas não muito óbvia dentro da estrutura dessa estrutura. Há um grande número de extensões prontas para a criação de estruturas em árvore (menus, diretórios etc.), mas é bastante difícil encontrar uma solução que suporte o trabalho completo com vários idiomas. Além disso, não estamos falando de traduzir a interface usando ferramentas de estrutura padrão, mas de armazenar dados em um banco de dados em vários idiomas. Também é bastante difícil encontrar um widget conveniente e totalmente funcional para gerenciar a árvore, que também pode funcionar com conteúdo multilíngue, sem manipulações complicadas de código.


Gostaria de compartilhar uma receita sobre como criar módulos semelhantes usando um exemplo de implementação de módulo de menu. Por exemplo, usarei o modelo de aplicativo Yii2 App Basic, mas você pode adaptar tudo ao seu modelo se ele diferir do modelo base.


Preparação


Para realizar a tarefa, precisamos de algumas extensões maravilhosas, a saber:



Instale os dados de extensão através do compositor:


composer require paulzi/yii2-adjacency-list composer require execut/yii2-widget-bootstraptreeview composer require creocoder/yii2-translateable 

Para implementar o menu como um módulo, usando o gerador Gii (ou manualmente), crie um novo módulo de menu e conecte-o nas configurações do aplicativo.


O projeto também deve ter um mecanismo de troca de idioma configurado. Eu prefiro usar esta extensão para o Yii2 .


Criação de modelo


Para armazenar o menu (ou outra entidade que possui multilinguismo) no banco de dados, precisamos criar duas tabelas. De fato, métodos diferentes podem ser usados ​​para armazenar dados multilíngues, mas eu gosto da opção com duas tabelas, uma das quais armazena a essência em si e a segunda - suas variações de linguagem, mais do que as outras. É conveniente usar migrações para criar tabelas. Aqui está um exemplo dessa migração:


m180819_083502_menu_init.php
 <?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}}'); } } 

Coloque esse arquivo de migração na pasta / migrations do nosso projeto e
execute o comando no console:


 php yii migrate 

Depois de criar as tabelas necessárias e adicionar um novo menu a elas usando a migração, precisamos criar modelos. Como o multilinguismo e as árvores podem ser encontrados não apenas no menu, mas também em outras entidades (por exemplo, a página do site) no projeto, sugiro que os métodos que implementam o mecanismo do multilinguismo e a organização das árvores sejam colocados em traços separados, para que possamos usá-los facilmente mais tarde em outros modelos sem duplicação de código. Crie uma pasta de características na raiz do aplicativo (se ainda não estiver lá) e coloque dois arquivos lá:


LangTrait.php
 <?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']); } } 

TreeTrait.php
 <?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; } } 

Agora, criaremos diretamente os modelos para trabalhar com o menu, no qual conectamos as características da árvore e o multilinguismo. Colocamos os modelos em / modules / menu / models:


Menu.php
 <?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; } } 

MenuLang.php
 <?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']); } } 

MenuQuery.php
 <?php namespace app\modules\menu\models; use paulzi\adjacencyList\AdjacencyListQueryTrait; class MenuQuery extends \yii\db\ActiveQuery { use AdjacencyListQueryTrait; } 

MenuSearch.php
 <?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; } } 

Criando controladores


Para operações CRUD em árvores multilíngues, precisamos de um controlador. Para simplificar nossa vida no futuro, criaremos um controlador básico no qual haverá todas as ações necessárias e, para diferentes entidades, seja um menu, um diretório ou páginas, herdaremos dele.


As classes do nosso projeto que usaremos como classes base serão colocadas na pasta / base. Crie o arquivo /base/controllers/AdminLangTreeController.php. Este controlador será a base para o CRUD de todas as entidades nas quais a árvore e o multilinguismo são implementados:


AdminLangTreeController.php
 <?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', '  ')); } } } 

Agora no módulo, crie o arquivo /modules/menu/controllers/AdminController.php. Este será o principal controlador para gerenciar o menu e, como implementa a árvore e o multilinguismo, herdará da base que já criamos na etapa anterior:


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'; } 

Como você pode ver, o código deste controlador contém apenas os nomes dos modelos e suas classes. Ou seja, para criar controladores CRUD para outros módulos (catálogo, rubricador etc.), que também usarão a árvore e o multilinguismo, você pode fazer o mesmo da mesma maneira - expanda o controlador base.


Criando uma interface para gerenciar o menu


A etapa final é a criação de uma interface para gerenciar uma árvore multilíngue. A extensão Bootstrap Treeview lida com a tarefa de exibir uma árvore, que pode ser configurada com bastante flexibilidade e suporta muitas funções convenientes (por exemplo, pesquisar em uma árvore). Crie uma exibição de índice para exibir a própria árvore e coloque-a em /modules/menu/views/admin/index.php:


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> 

Então chegamos à fase mais interessante deste caso: como criar um formulário para criar / editar dados multilíngues. Crie três arquivos na pasta / modules / menu / views / admin:


create.php
 <?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, ]); 

update.php
 <?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, ]); 

_form.php
 <?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> 

Não esqueça que o idioma padrão deve ser especificado no aplicativo (parâmetro de idioma) e nos parâmetros UrlManager - uma matriz com uma lista de idiomas (idiomas) que usaremos. O idioma padrão deve ser o primeiro nesta matriz.


Conclusão


Como resultado, devemos obter o seguinte:


  • Um módulo pronto para um menu multilíngue em formato de árvore de um site com uma interface conveniente e personalizável;
  • Um controlador CRUD básico que pode ser herdado ao criar outros módulos que usam madeira e multilinguismo;
  • Dois traços (multilíngüe e árvore) que podem ser conectados aos modelos para implementar as funções correspondentes.

Espero que este artigo seja útil e ajude você a desenvolver novos bons projetos no Yii2.

Source: https://habr.com/ru/post/pt426625/


All Articles