Overview
In the controller:
$post->load(Yii::$app->request->post() loads the model. Yii::$app->request->post() returns an array of the form ['MyFormName[key]' => 'value] . In the load for each property of the model with the name key , the value is set if they have validation rules and this field is written in the script.
In the model:
getTags is relaying. Serves for ActiveRecord model connections. setTags looks like a setter (a setter, this is a function called when accessing a non-existent property), but in this case it is just a function. It saves the associated model with populateRelation and increments the current $this->tags_count . Speaking about the problem of public properties, the counter can be increased directly, without calling the setTags method.
How do setters work in this example? Where do the values passed to setters come from ($ tags, $ cover, $ images ...)?
setCover , setTags , setImages are called inside the model with the usual passing of parameters. I suppose they should be private and not called from client code (controller).
At what point is the data written from related models (tags, images, main image) to the database?
At the time of the call of the populateRelation in the model, the associated relaying is filled. Saving occurs in the controller at the moment of save model.
What is missing in this code to make it work?
Write it all over again, using this code as an example only. You do need experience, and this will save you from having to think about functions like setTagsString , which are not used anywhere and only confuse you. It will also be clear that for the getRelatedRecords method to work, you need to have the images and tags tables filled in.
I recommend taking this section of the documentation and go through each function in turn. This is a long time, I haven’t walked through everything myself, but this will allow you to get the most complete picture of the capabilities of the ActiveRecord framework without digging into the code of dubious quality.
Update: An example of the working code of this example.
Despite the fact that this is just a quick refactoring is ready in the codream and questions in the comments.
Migration:
<?php use yii\db\Migration; class m151122_155133_create_tables extends Migration { public function up() { $tableOptions = null; if ($this->db->driverName === 'mysql') { // http://stackoverflow.com/questions/766809/whats-the-difference-between-utf8-general-ci-and-utf8-unicode-ci $tableOptions = 'CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE=InnoDB'; } $this->createTable('{{%post}}', [ 'id' => $this->primaryKey(), 'message' => $this->text(), 'tags_count' => $this->integer(2)->notNull()->defaultValue(0) ], $tableOptions); $this->createTable('{{%tag}}', [ 'id' => $this->primaryKey(), 'name' => $this->string(32)->notNull(), 'UNIQUE INDEX `UNQ_tag__name` (`name`)', ], $tableOptions); $this->batchInsert('{{%tag}}', ['name'], [ ['tag1'], ['tag2'], ]); $this->createTable('{{%post_tag}}', [ 'id' => $this->primaryKey(), 'post_id' => $this->integer(11)->notNull(), 'tag_id' => $this->integer(11)->notNull(), 'FOREIGN KEY `FK_post_tag__post_id` (post_id) REFERENCES post(id) ON UPDATE RESTRICT ON DELETE RESTRICT', 'FOREIGN KEY `FK_post_tag__tag_id` (tag_id) REFERENCES tag(id) ON UPDATE RESTRICT ON DELETE RESTRICT', ], $tableOptions); $this->createTable('{{%post_image}}', [ 'id' => $this->primaryKey(), 'post_id' => $this->integer(11)->notNull(), 'image' => $this->string(128)->notNull(), 'is_cover' => $this->boolean()->defaultValue(0), 'FOREIGN KEY `FK_post_image__post_id` (post_id) REFERENCES post(id) ON UPDATE RESTRICT ON DELETE RESTRICT', ], $tableOptions); } public function down() { $this->dropTable('{{%post_image}}'); $this->dropTable('{{%post_tag}}'); $this->dropTable('{{%tag}}'); $this->dropTable('{{%post}}'); } }
Controller:
<?php namespace frontend\controllers; use frontend\models\CreatePostForm; use frontend\models\Post; use frontend\models\PostSearch; use Yii; use yii\base\Exception; use yii\web\Controller; use yii\web\NotFoundHttpException; /** * PostController implements the CRUD actions for Post model. */ class PostController extends Controller { /** * Lists all Post models. * * @return mixed */ public function actionIndex() { $searchModel = new PostSearch(); $dataProvider = $searchModel->search(Yii::$app->request->queryParams); return $this->render('index', [ 'searchModel' => $searchModel, 'dataProvider' => $dataProvider, ]); } /** * Displays a single Post model. * * @param integer $id * * @return mixed */ public function actionView($id) { return $this->render('view', [ 'model' => $this->findModel($id), ]); } /** * Creates a new Post model. * If creation is successful, the browser will be redirected to the 'view' page. * * @return mixed */ public function actionCreate() { $model = new CreatePostForm(); if ($model->load(Yii::$app->request->post()) && $model->validate()) { if (!$model->createNewPost()) { throw new Exception('Failed to save CreatePostForm'); } return $this->redirect(['view', 'id' => $model->id]); } return $this->render('create', [ 'model' => $model, ]); } /** * Finds the Post model based on its primary key value. * If the model is not found, a 404 HTTP exception will be thrown. * * @param integer $id * * @return Post the loaded model * @throws NotFoundHttpException if the model cannot be found */ protected function findModel($id) { if (($model = Post::findOne($id)) !== null) { return $model; } else { throw new NotFoundHttpException('The requested page does not exist.'); } } }
View index.php
<?php use yii\helpers\Html; use yii\grid\GridView; /* @var $this yii\web\View */ /* @var $searchModel frontend\models\PostSearch */ /* @var $dataProvider yii\data\ActiveDataProvider */ $this->title = 'Posts'; $this->params['breadcrumbs'][] = $this->title; ?> <div class="post-index"> <h1><?= Html::encode($this->title) ?></h1> <p> <?= Html::a('Create Post', ['create'], ['class' => 'btn btn-success']) ?> </p> <?= GridView::widget([ 'dataProvider' => $dataProvider, 'filterModel' => $searchModel, 'columns' => [ ['class' => 'yii\grid\SerialColumn'], 'id', 'message:ntext', ['class' => 'yii\grid\ActionColumn'], ], ]); ?> </div>
View create.php
<?php use yii\helpers\Html; use yii\widgets\ActiveForm; /* @var $this yii\web\View */ /* @var $model frontend\models\Post */ /* @var $form yii\widgets\ActiveForm */ $this->title = 'Create Post'; $this->params['breadcrumbs'][] = ['label' => 'Posts', 'url' => ['index']]; $this->params['breadcrumbs'][] = $this->title; ?> <div class="post-create"> <h1><?= Html::encode($this->title) ?></h1> <div class="post-form"> <?php $form = ActiveForm::begin(['options' => ['enctype' => 'multipart/form-data']]); ?> <?= $form->field($model, 'message')->textarea(['rows' => 6]) ?> <?= $form->field($model, 'tagString')->input('text') ?> <?= $form->field((new \frontend\models\PostImage()), 'image[]')->fileInput(['multiple' => true, 'accept' => 'image/*']) ?> <div class="form-group"> <?= Html::submitButton($model->isNewRecord ? 'Create' : 'Update', ['class' => $model->isNewRecord ? 'btn btn-success' : 'btn btn-primary']) ?> </div> <?php ActiveForm::end(); ?> </div> </div>
View view.php
<?php use yii\helpers\Html; use yii\widgets\DetailView; /* @var $this yii\web\View */ /* @var $model frontend\models\Post */ $this->title = $model->id; $this->params['breadcrumbs'][] = ['label' => 'Posts', 'url' => ['index']]; $this->params['breadcrumbs'][] = $this->title; ?> <div class="post-view"> <h1><?= Html::encode($this->title) ?></h1> <p> <?= Html::a('Update', ['update', 'id' => $model->id], ['class' => 'btn btn-primary']) ?> <?= Html::a('Delete', ['delete', 'id' => $model->id], [ 'class' => 'btn btn-danger', 'data' => [ 'confirm' => 'Are you sure you want to delete this item?', 'method' => 'post', ], ]) ?> </p> <?= DetailView::widget([ 'model' => $model, 'attributes' => [ 'id', 'message:ntext', ], ]) ?> </div>
Model CreatePostForm
<?php namespace frontend\models; use Yii; use yii\helpers\ArrayHelper; use yii\web\UploadedFile; class CreatePostForm extends Post { /** * Храним тут строку. Приватная потому что нам нужен сеттер, и мы за компанию прописываем геттер чтобы нельзя было писать напрямую в свойство, минуя сеттер. * * @var string */ private $_tagString; /** * Load отработает только для тех полей для которых прописаны правила валидации. * * @return array */ public function rules() { return ArrayHelper::merge([ ['tagString', 'string'] ], parent::rules()); } /** * Стараемся в контроллере не работать напрямую с ActiveRecord. * * @return bool */ public function createNewPost() { $this->loadUploadedImages(); return $this->save(); } /** * Просто получаем теги, которые ввел пользователь на форме * * @return string */ public function getTagString() { return $this->_tagString; } /** * Сохраняем картинки. Тут только запись названий в бд, запись файлов не производится. * * @see http://www.yiiframework.com/doc-2.0/guide-input-file-upload.html */ private function loadUploadedImages() { $images = []; foreach (UploadedFile::getInstances(new PostImage(), 'image') as $file) { $image = new PostImage(); $image->image = $file->name; $images[] = $image; } $this->setImages($images); } /** * Сохряняем связи картинок и для первой картинки устанавливаем флаг is_cover. Тут в примере только создание, но если бы было обновление вызов setCover бы * не произошел. * * @param PostImage[] $images */ private function setImages($images) { $this->populateRelation('images', $images); if (!$this->isRelationPopulated('cover') && !$this->getCover()->one()) { $this->setCover(reset($images)); } } /** * Сохраняем главную картинку поста. * * @param PostImage $cover */ private function setCover($cover) { $cover->is_cover = true; $this->populateRelation('cover', $cover); } /** * Записываем строчку тегов, полученную от пользователя в связанную таблицу. * * @param string $tagString */ public function setTagString($tagString) { $this->_tagString = $tagString; $this->saveTagsToRelation(); } /** * Сохраняем теги в связанной таблице и увеличиваем счетчик */ private function saveTagsToRelation() { $tags = []; /** * Пример с viaTable в релейшене Post::getTags подразумевал что теги только выбираются, но не создаются. */ foreach (explode(',', $this->_tagString) as $name) { $tag = Tag::find()->where(['name' => trim($name)])->one(); if (!$tag) { continue; } $tags[] = $tag; } $this->populateRelation('tags', $tags); $this->tags_count = count($tags); } }
Post model
<?php namespace frontend\models; use Yii; /** * This is the model class for table "post". * * @property integer $id * @property string $message * @property integer $tags_count * * @property PostImage $image * @property Tag[] $tags */ class Post extends \yii\db\ActiveRecord { /** * @inheritdoc */ public static function tableName() { return 'post'; } /** * @inheritdoc */ public function rules() { return [ ['tags_count', 'integer'], [['message'], 'string'], ]; } /** * @inheritdoc */ public function attributeLabels() { return [ 'id' => 'ID', 'message' => 'Message', ]; } /** * @inheritdoc */ public function transactions() { return [ self::SCENARIO_DEFAULT => self::OP_INSERT | self::OP_UPDATE, ]; } /** * @return \yii\db\ActiveQuery */ public function getCover() { return $this->hasOne(PostImage::className(), ['post_id' => 'id']) ->andWhere(['is_cover' => true]); } /** * @return \yii\db\ActiveQuery */ public function getImages() { return $this->hasMany(PostImage::className(), ['post_id' => 'id']); } /** * @return \yii\db\ActiveQuery */ public function getTags() { return $this->hasMany(Tag::className(), ['id' => 'tag_id']) ->viaTable('post_tag', ['post_id' => 'id']); } /** * В afterSave мы сохраняем связанные модели, которые нужно сохранять после основной модели, т.к. нужен ее ИД. * * @param bool $true * @param array $changedAttributes */ public function afterSave($true, $changedAttributes) { $relatedRecords = $this->getRelatedRecords(); if (isset($relatedRecords['cover'])) { $this->link('cover', $relatedRecords['cover']); } if (isset($relatedRecords['tags'])) { foreach ($relatedRecords['tags'] as $tag) { $this->link('tags', $tag); } } if (isset($relatedRecords['images'])) { foreach ($relatedRecords['images'] as $image) { $this->link('images', $image); } } } }
PostImage model
<?php namespace frontend\models; use Yii; /** * This is the model class for table "post_image". * * @property integer $id * @property integer $post_id * @property string $image * @property integer $is_cover * * @property Post $post */ class PostImage extends \yii\db\ActiveRecord { /** * @inheritdoc */ public static function tableName() { return 'post_image'; } /** * @inheritdoc */ public function rules() { return [ [['post_id', 'image'], 'required'], [['post_id', 'is_cover'], 'integer'], [['image'], 'string', 'max' => 128], [['post_id'], 'unique'], [['post_id'], 'exist', 'skipOnError' => true, 'targetClass' => Post::className(), 'targetAttribute' => ['post_id' => 'id']], ]; } /** * @inheritdoc */ public function attributeLabels() { return [ 'id' => 'ID', 'post_id' => 'Post ID', 'image' => 'Image', 'is_cover' => 'Is Cover', ]; } /** * @return \yii\db\ActiveQuery */ public function getPost() { return $this->hasOne(Post::className(), ['id' => 'post_id']); } }
PostTag model
<?php namespace frontend\models; use Yii; /** * This is the model class for table "post_tag". * * @property integer $id * @property integer $post_id * @property integer $tag_id * * @property Post $post * @property Tag $post0 */ class PostTag extends \yii\db\ActiveRecord { /** * @inheritdoc */ public static function tableName() { return 'post_tag'; } /** * @inheritdoc */ public function rules() { return [ [['post_id', 'tag_id'], 'required'], [['post_id', 'tag_id'], 'integer'], [['post_id'], 'exist', 'skipOnError' => true, 'targetClass' => Post::className(), 'targetAttribute' => ['post_id' => 'id']], [['tag_id'], 'exist', 'skipOnError' => true, 'targetClass' => Tag::className(), 'targetAttribute' => ['tag_id' => 'id']], ]; } /** * @inheritdoc */ public function attributeLabels() { return [ 'id' => 'ID', 'post_id' => 'Post ID', 'tag_id' => 'Tag ID', ]; } /** * @return \yii\db\ActiveQuery */ public function getPost() { return $this->hasOne(Post::className(), ['id' => 'post_id']); } /** * @return \yii\db\ActiveQuery */ public function getPost0() { return $this->hasOne(Tag::className(), ['id' => 'post_id']); } }
Tag Model
<?php namespace frontend\models; use Yii; /** * This is the model class for table "tag". * * @property integer $id * @property string $name * * @property PostTag[] $postTags */ class Tag extends \yii\db\ActiveRecord { /** * @inheritdoc */ public static function tableName() { return 'tag'; } /** * @inheritdoc */ public function rules() { return [ [['name'], 'required'], [['name'], 'string', 'max' => 32], [['name'], 'unique'], ]; } /** * @inheritdoc */ public function attributeLabels() { return [ 'id' => 'ID', 'name' => 'Name', ]; } /** * @return \yii\db\ActiveQuery */ public function getPostTags() { return $this->hasMany(PostTag::className(), ['post_id' => 'id']); } }
PostSearch Model
<?php namespace frontend\models; use Yii; use yii\base\Model; use yii\data\ActiveDataProvider; use frontend\models\Post; /** * PostSearch represents the model behind the search form about `frontend\models\Post`. */ class PostSearch extends Post { /** * @inheritdoc */ public function rules() { return [ [['id'], 'integer'], [['message'], 'safe'], ]; } /** * @inheritdoc */ public function scenarios() { // bypass scenarios() implementation in the parent class return Model::scenarios(); } /** * Creates data provider instance with search query applied * * @param array $params * * @return ActiveDataProvider */ public function search($params) { $query = Post::find(); // add conditions that should always apply here $dataProvider = new ActiveDataProvider([ 'query' => $query, ]); $this->load($params); if (!$this->validate()) { // uncomment the following line if you do not want to return any records when validation fails // $query->where('0=1'); return $dataProvider; } // grid filtering conditions $query->andFilterWhere([ 'id' => $this->id, ]); $query->andFilterWhere(['like', 'message', $this->message]); return $dataProvider; } }