メニューモジュールの作成例としてのYii2の多言語ツリー

エントリー



多くの初心者のWeb開発者は、階層構造を持ちながら多言語をサポートするYii2プロジェクトのメニュー、ディレクトリ、またはカテゴリを作成する必要に直面しています。 タスクは非常に単純ですが、このフレームワークのフレームワーク内ではあまり明確ではありません。 ツリー構造(メニュー、ディレクトリなど)を作成するための既製の拡張機能が多数ありますが、いくつかの言語で本格的な作業をサポートするソリューションを見つけるのはかなり困難です。 そして、これは通常のフレームワークツールを使用してインターフェイスを翻訳することではなく、いくつかの言語でデータベースにデータを保存することです。 また、ツリーを管理するための便利で完全に機能するウィジェットを見つけることは非常に難しく、複雑なコード操作なしで多言語コンテンツを扱うこともできます。







メニューモジュールの実装例を使用して、同様のモジュールを作成する方法に関するレシピを共有したいと思います。 たとえば、Yii2 App Basicアプリケーションテンプレートを使用しますが、ベーステンプレートと異なる場合は、すべてをテンプレートに適合させることができます。







準備する



このタスクを達成するには、すばらしい拡張機能が必要です。









composerを介して拡張データをインストールします。







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





メニューをモジュールとして実装するには、Giiジェネレーターを使用して(または手動で)新しいメニューモジュールを作成し、アプリケーション設定で接続します。







プロジェクトには、言語切り替えメカニズムも構成する必要があります。 この拡張機能をYii2に使用することを好みます。







モデル作成



メニュー(または多言語対応の別のエンティティ)をデータベースに保存するには、2つのテーブルを作成する必要があります。 実際、 異なる方法を使用して多言語データを保存できますが、私は2つのテーブルのオプションが好きです。1つはエッセンス自体を保存し、2つ目は他のものよりも言語のバリエーションを保存します。 移行を使用してテーブルを作成すると便利です。 そのような移行の例を次に示します。







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





この移行ファイルをプロジェクトの/ migrationsフォルダーに配置し、

コンソールでコマンドを実行します。







 php yii migrate
      
      





必要なテーブルを作成し、移行を使用して新しいメニューを追加した後、モデルを作成する必要があります。 多言語とツリーはメニューだけでなく、プロジェクトの他のエンティティ(サイトページなど)にもあるため、多言語メカニズムとツリー編成を実装するメソッドは、後で簡単に使用できるように別々の特性に配置することをお勧めしますコードを重複させることなく、他のモデルで使用できます。 アプリケーションルートに特性フォルダーを作成し(まだない場合)、そこに2つのファイルを配置します。







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





次に、メニューを操作するためのモデル自体を直接作成します。このメニューでは、ツリーの特性と多言語性を結び付けます。 モデルを/モジュール/メニュー/モデルに配置します。







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





コントローラーの作成



多言語ツリーでCRUD操作を実装するには、コントローラーが必要です。 将来の生活を簡素化するために、必要なすべてのアクションが存在する1つの基本コントローラーを作成し、さまざまなエンティティに対して、それがメニュー、ディレクトリ、またはページであるかどうかにかかわらず、それから継承します。







基本クラスとして使用するプロジェクトのクラスは、/ baseフォルダーに配置されます。 ファイル/base/controllers/AdminLangTreeController.phpを作成します。 このコントローラは、ツリーと多言語が実装されているすべてのエンティティのCRUDのベースになります。







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





モジュールで、/ modules / menu / controllers / AdminController.phpファイルを作成します。 これはメニューを管理するためのメインコントローラーになり、ツリーと多言語を実装するため、前の手順で作成したベースコントローラーから継承します。







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





ご覧のとおり、このコントローラーのコードにはモデルとそのクラスの名前のみが含まれています。 つまり、ツリーと多言語を使用する他のモジュール(カタログ、rubricatorなど)のCRUDコントローラーを作成するには、同じ方法で同じことを実行できます-ベースコントローラーを拡張します。







メニューを管理するためのインターフェースを作成する



最後の段階は、多言語ツリーを管理するためのインターフェースの作成です。 Bootstrap Treeview拡張機能は、ツリーを表示するのに非常に役立ちます。ツリーは非常に柔軟に構成でき、多くの便利な機能(ツリーの検索など)をサポートしています。 ツリー自体を表示するインデックスビューを作成し、/ 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>
      
      





ここで、このケースで最も興味深い段階に進みます。多言語データを作成/編集するためのフォームを作成する方法です。 / modules / menu / views / adminフォルダーに3つのファイルを作成します。







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>
      
      





アプリケーション(言語パラメーター)およびUrlManagerパラメーター(使用する言語(言語)のリストを含む配列)でデフォルト言語を指定することを忘れないでください。 デフォルトの言語は、この配列の最初の言語でなければなりません。







おわりに



その結果、次のものが得られます。









この記事が有用であり、Yii2の新しい良いプロジェクトの開発に役立つことを願っています。








All Articles