В прошлой части мы написали систему по добавлению частей речи на сайт через админку. Самое время приступить к реализации основной задачи. Напомню,
Нужен словарь, куда администратор сайта будет добавлять, собственно, слова, а также их словоформы. Затем мы будем вытаскивать по нужной словоформе слово в базовой форме и давать его перевод, а также позволим пользователям добавлять его в свой собственный словарь для дальнейшего изучения.
Немного уточню: наш сервис подразумевает, что учиться будут носители русского и английского языка (с возможностью дальнейшего расширения количества языков), а учить они будут третий. Неважно, филиппинский язык, креольский или греческий.
Приступим к реализации словаря.
Учитываем, что переводиться на несколько языков будут только поля comment и translate.
public function safeUp() { $tableOptions = null; if ($this->db->driverName === 'mysql') { $tableOptions = 'CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE=InnoDB'; } $this->createTable('dictionary', [ 'id' => $this->primaryKey(), 'word' => $this->string(64), // слово, для которого будем делать переводы 'parent_id' => $this->integer(5), // если есть родитель, значит, что текущее слово - словоформа 'part_of_speech' => $this->integer(5), // к какой части речи относится данное слово. Связь с таблицей parts_of_speech ], $tableOptions); $this->createTable('dictionary_translate', [ 'id' => $this->primaryKey(), 'word_id' => $this->integer(), // для какого слова перевод? Связь с таблицей dictionary_translate 'language' => $this->string(), // тут будет лежать код языка перевода 'translate' => $this->string(), // перевод слова 'comment' => $this->string(), // если нужно объяснение значения слова, оно будет в этой строке ], $tableOptions); // Создадим индексы нужных полей $this->createIndex( 'idx-dictionary_translate-word_id', 'dictionary_translate', 'word_id' ); $this->createIndex( 'idx-dictionary_translate-language', 'dictionary_translate', 'language' ); /* Cоздадим внешний ключ для связи с таблицей parts_of_speech */ // См. документацию https://www.yiiframework.com/doc/guide/2.0/en/db-migrations, блок "add foreign key for table `user`" $this->addForeignKey( 'fk-dictionary-part_of_speech', 'dictionary', // какую таблицу связываем 'part_of_speech', // по какому полю 'parts_of_speech', // с какой таблицей 'id', // по какому её полю 'CASCADE' // тип ); /* Аналогично, создадим внешний ключ в таблице переводов, связав её с основной таблицей словаря */ $this->addForeignKey( 'fk-dictionary_translate-word_id', 'dictionary_translate', // какую таблицу связываем 'word_id', // по какому полю 'dictionary', // с какой таблицей 'id', // по какому её полю 'CASCADE' // тип ); }
Забавная штука: ключ translation оказался зарезервированным, поэтому пришлось поле переименовать в translate.
В случае некорректной миграции будем удалять также обе созданные таблицы и внешние ключи. Индексы дропать не будем, ну их.
public function safeDown() { //Добавляем удаление внешних ключей $this->dropForeignKey( 'fk-dictionary_translate-word_id', // имя ключа 'dictionary_translate' // к какой таблице он был добавлен ); $this->dropForeignKey( 'fk-dictionary-part_of_speech', // имя ключа 'dictionary' // к какой таблице он был добавлен ); $this->dropTable('dictionary'); $this->dropTable('dictionary_translate'); }
Создадим модель по тем же принципам, что и в прошлом посте.
Настроим внутри неё мультиязычное поведение и правила валидации:
public function behaviors() { return [ 'ml' => [ 'class' => MultilingualBehavior::className(), 'languages' => [ 'ru' => 'Russian', 'en-US' => 'English', ], 'defaultLanguage' => 'ru', 'tableName' => "{{%dictionary_translate}}", 'langForeignKey' => 'word_id', 'attributes' => [ 'comment', 'translation', // Передаем массив переводимых полей ] ], ]; } public function rules() { return [ [['parent_id', 'part_of_speech'], 'integer'], [['word'], 'string', 'max' => 64], [['part_of_speech'], 'exist', 'skipOnError' => true, 'targetClass' => PartsOfSpeech::className(), 'targetAttribute' => ['part_of_speech' => 'id']], [['comment', 'translation'], 'string'] ]; }
Также изменим метод find() и добавим use.
Аналогично прошлому уроку, сгенерируем крудЪ и пропишем в контроллере правильный метод findModel($id).
Откорректируем форму в виде для заполнения нужных полей (и удалим лишнее поле parent_id, его в дальнейшем мы будем заполнять автоматически).
<?php $form = ActiveForm::begin(); ?> <?= $form->field($model, 'word')->textInput(['maxlength' => true]) ?> <?= $form->field($model, 'part_of_speech')->textInput() ?> <div class="row"> <div class="col-sm-6"> <h3>Перевод на русский язык</h3> <?= $form->field($model, 'translation') ?> <?= $form->field($model, 'comment') ?> </div> <div class="col-sm-6"> <h3>English version</h3> <?= $form->field($model, 'translation_en') ?> <?= $form->field($model, 'comment_en') ?> </div> </div> <div class="form-group"> <?= Html::submitButton('Сохранить', ['class' => 'btn btn-success']) ?> </div> <?php ActiveForm::end(); ?>
Также заполним лейблы для полей и получим такую форму:
Самое время воспользоваться написанным в прошлой части кодом и вывести выпадающим списком возможные варианты частей речи. Для этого подключим модель частей речи в наш контроллер, а также поключим ArrayHelper для формирования правильного списка:
#backend\controllers\DictionaryController use common\models\PartsOfSpeech; use yii\helpers\ArrayHelper;
И напишем метод контроллера, который будет возвращать обработанный массив, состоящий из всех частей речи, добавленных через админку.
public function getAllPartsOfSpeech(){ $parts = PartsOfSpeech::find()->asArray()->all(); $array = ArrayHelper::map($parts, 'id', 'name'); return $array; }
Передадим части речи в вид внутри экшнов (на примере экшна create):
public function actionCreate() { $model = new Dictionary(); if ($model->load(Yii::$app->request->post()) && $model->save()) { return $this->redirect(['view', 'id' => $model->id]); } return $this->render('create', [ 'model' => $model, 'parts_of_speech' => $this->getAllPartsOfSpeech() ]); }
Не забудьте передать эти данные в update, а также прокинуть их через вид в форму.
И, наконец, в виде делаем вот такую штуку:
# /backend/views/dictionary/_form.php $params = ['selection' => $model->part_of_speech ]; echo $form->field($model, 'part_of_speech')->dropDownList($parts_of_speech, $params);
И вот у нас готовая и работающая форма добавления нового слова в словарь.
Самое время реализовать возможность добавления словоформы к родительскому слову через тот же CRUD. По сути, механизм будет все тот же, за исключением передачи параметра с ID родителя.
Для начала, добавим кнопку добавления потомка в GridView:
# /backend/views/dictionary/index.php <?= GridView::widget([ 'dataProvider' => $dataProvider, 'columns' => [ ['class' => 'yii\grid\SerialColumn'], 'id', 'word', 'parent_id', 'part_of_speech' [ 'class' => \yii\grid\ActionColumn::class, /* Кусок кода с добавленной кнопкной */ 'template' => '{view} {add} {update} {delete} ', 'buttons' => [ 'add' => function ($url, $model, $key) { $iconName = "duplicate"; $title = 'Добавить словоформу'; $id = 'add-'.$key; $options = [ 'title' => $title, 'id' => $id ]; $url = Url::current(['dictionary/add', 'id' => $key]); $icon = Html::tag('span', '', ['class' => "glyphicon glyphicon-$iconName"]); return Html::a($icon, $url, $options); }, ], /* Конец куска */ ], ], ]); ?>
Подробнее о том, как и почему это сделано, смотрите тут.
Теперь, когда у нас есть ссылка на экшн, опишем его:
# /backend/controllers/DictionaryController.php public function actionAdd($id) { $model = new Dictionary(); if($this->parentExist($id)){ $model->parent_id = $id; } else { throw new NotFoundHttpException('Родительская форма слова не найдена или Вы пытаетесь добавить словоформу другой словоформе.'); } if ($model->load(Yii::$app->request->post()) && $model->save()) { return $this->redirect(['view', 'id' => $model->id]); } return $this->render('create', [ 'model' => $model, 'parts_of_speech' => $this->getAllPartsOfSpeech() ]); }
Фактически, это немного усовершенствованный код из стандартного экшна create, лишь с тем различием, что в нем мы проверяем, действительно ли в нашей базе существует запись с переданным id, и если это так, делаем присвоение модели:
$model->parent_id = $id;
Помимо этого, нужно написать в нашем контроллере метод для проверки существования родителя:
public function parentExist($id){ // Если запись существует if (($model = Dictionary::find()->where(['id' => $id])->multilingual()->one()) !== null) { // и у неё нет родителя if( $model->parent_id == null ){ return true; } return false; } return false; }
Что важно: для избежания круговых ссылок слов друг на друга мы запрещаем ссылаться на словоформы как на родителей. Благо, сам язык (под изучение которого разрабатывается сайт) нам это позволяет. Ну и в контроллере бросаем 404 исключение, если родителя с переданным id не существует или он сам является потомком.
Теоретически, можно написать рекурсивную функцию, которая будет определять родителя первого уровня и проставлять его id-шник, но в данном случае реализация не требует такого подхода, да и сегодня мне уже лень это делать.
В заключительном на сегодня блоке выведем часть речи строкой вместо её идентификатора.
В виджете
# /backend/views/dictionary/view.php <?= DetailView::widget([ 'model' => $model, 'attributes' => [ 'id', 'word', 'parent_id', /* Выведем поле с нужным лейблом и значением */ [ 'label' => 'Часть речи', 'value' => $model->getPartOfSpeech($model->part_of_speech), ], ], ]) ?>
Также нужно написать функцию внутри модели, которая будет возвращать строкой значение по переданному id.
# /common/models/Dictionary.php public function getPartOfSpeech($id) { $part_of_speech = PartsOfSpeech::find()->where(['id' => $id])->asArray()->one(); return $part_of_speech['name']; }
Как мне кажется, получилось более чем наглядно.
И делаем аналогичную весчь в GridView общего списка.
[ 'attribute' => 'part_of_speech', 'label' => 'Часть речи', 'format' => 'text', 'content' => function($data){ return $data->getPartOfSpeech($data->part_of_speech); } ],
Время прерваться на сон, чем я, пожалуй, и займусь.