This article will examine the concept of how to save related models in Yii2 using the example of the relation Product->hasMany(ProductColor).
Yii2 Array of Models: Table Columns
Start with Migration
The command for creating migration:
php yii
migrate/create <name>
We create tables with the up() method. We delete with down().
Product table:
$this->createTable('{{%product}}', [
'id' => $this->primaryKey(),
'title' => $this->string()->notNull(),
'status' => $this->boolean(),
'created_at' => $this->integer(),
'updated_at' => $this->integer(),
'image' => $this->string(),
]);
Quite common fields: ID, name, status (published/draft 1/0), product creation time, product update time, an image, for the name of a saved image.
ProductColor table:
$this->createTable('{{%product_color}}', [
'id' => $this->primaryKey(),
'product_id' => $this->integer()->notNull(),
'image' => $this->string(),
]);
$this->addForeignKey("fk_product_color_product", "{{%product_color}}", "product_id", "{{%product}}", "id", "CASCADE", "CASCADE");
Here, the product_id is a foreign key referencing Product->id, and the “image” field is similar to the previous one.
How to Create Models in Yii2
We can use Gii to make Yii2 create models, views, and controllers for these tables. For convenience, you can use the following commands in the terminal:
php yii gii/model --tableName=table_name --modelClass=ModelName --ns='common\models' --enableI18N=1
php yii gii/crud --enableI18N=1 --modelClass='common\models\ModelName' --controllerClass='backend\controllers\ModelController' --searchModelClass='backend\models\SearchModel' --viewPath='backend/views/model'
Change all names in bold to the name of your model. Namespaces are also can be changed, if necessary. These commands will generate all the required files.
Next, we add UploadImageBehavior to the Product and ProductColor. It is an extension that serves for saving images. Install it with the following command:
composer require mongosoft/yii2-upload-behavior "*"
Yii Model Rules
We also add it to behaviors and rules.
Product Model
The Product model will look as follows:
namespace commonmodels;
use mongosoftfileUploadImageBehavior;
use Yii;
use yiibehaviorsTimestampBehavior;
class Product extends yiidbActiveRecord
{
const ENTITY_DRAFT = 0;
const ENTITY_PUBLISHED = 1;
public static function tableName()
{
return 'product';
}
public function behaviors()
{
return [
TimestampBehavior::className(),
'image' => [
'class' => UploadImageBehavior::className(),
'attribute' => 'image',
'path' => '@frontend/web/uploads/Product/{id}',
'url' => '/frontend/web/uploads/Product/{id}',
'unlinkOnDelete' => true,
'scenarios' => ['insert', 'update'],
'thumbs' => [
'thumbnail' => ['width' => 120, 'height' => 80, 'quality' => 90],
'900x600' => ['width' => 900, 'height' => 600, 'quality' => 90],
]
]
];
}
public function rules()
{
return [
[['title'], 'required'],
[['status', 'created_at', 'updated_at'], 'integer'],
[['title'], 'string', 'max' => 255],
['image', 'file', 'skipOnEmpty' => true, 'extensions' => ['png', 'jpg', 'gif'], 'on' => ['insert', 'update']]
];
}
public static function getStatusList()
{
return [
self::ENTITY_DRAFT => Yii::t('app', 'Draft'),
self::ENTITY_PUBLISHED => Yii::t('app', 'Published'),
];
}
public function getProductColors()
{
return $this->hasMany(ProductColor::className(), ['product_id' => 'id']);
}
}
Product Color
ProductColor will not differ by much:
namespace commonmodels;
use mongosoftfileUploadImageBehavior;
use Yii;
class ProductColor extends yiidbActiveRecord
{
public static function tableName()
{
return 'product_color';
}
public function behaviors()
{
return [
'image' =>
[
'class' => UploadImageBehavior::className(),
'attribute' => 'image',
'path' => '@frontend/web/uploads/ProductColor/{id}',
'url' => '/frontend/web/uploads/ProductColor/{id}',
'unlinkOnDelete' => true,
'scenarios' => ['insert', 'update'],
'thumbs' => [
'thumbnail' => ['width' => 120, 'height' => 80, 'quality' => 90],
'300x200' => ['width' => 300, 'height' => 200, 'quality' => 90],
]
]
];
}
public function rules()
{
return [
[['product_id'], 'required'],
[['product_id'], 'integer'],
['image', 'file', 'skipOnEmpty' => true, 'extensions' => ['png', 'jpg', 'gif'], 'on' => ['insert', 'update']],
[['product_id'], 'exist', 'skipOnError' => true, 'targetClass' => Product::className(), 'targetAttribute' => ['product_id' => 'id']],
];
}
public function getProduct()
{
return $this->hasOne(Product::className(), ['id' => 'product_id']);
}
}
ProductController
Let’s move to ProductController:
namespace backendcontrollers;
use commonmodelsProductColor;
use Yii;
use commonmodelsProduct;
use backendmodelsProductSearch;
use yiidbActiveRecord;
use yiiwebController;
use yiiwebNotFoundHttpException;
use yiiwebUploadedFile;
class ProductController extends Controller
{
public function behaviors()
{
return [
'verbs' => [
'class' => 'yiifiltersVerbFilter',
'actions' => [
'delete' => ['POST'],
],
],
// since v 2.0.13
'ajax' => [
'class' => 'yiifiltersAjaxFilter',
'only' => ['update-colors', 'delete-product-color']
]
];
}
public function actionIndex()
{
$searchModel = new ProductSearch();
$dataProvider = $searchModel->search(Yii::$app->request->queryParams);
return $this->render('index', [
'searchModel' => $searchModel,
'dataProvider' => $dataProvider,
]);
}
public function actionCreate()
{
$model = new Product();
$model->setScenario('insert');
$colorModel = new ProductColor();
if ($model->load(Yii::$app->request->post()) && $model->save()) {
return $this->redirect(['update', 'id' => $model->id]);
} else {
return $this->render('create', [
'model' => $model,
'colorModel' => $colorModel
]);
}
}
public function actionUpdate($id)
{
$model = $this->findModel(Product::className(), $id);
$model->setScenario('update');
$colorModel = new ProductColor();
if ($model->load(Yii::$app->request->post()) && $model->save()) {
return $this->redirect(['index']);
} else {
return $this->render('update', [
'model' => $model,
'colorModel' => $colorModel
]);
}
}
public function actionUpdateColors($productId)
{
$model = $this->findModel(Product::className(), $productId);
$items = $model->productColors;
$items[] = new ProductColor();
foreach ($items as $item) {
$item->setScenario('update'); // for UploadImageBehavior
}
if (ProductColor::loadMultiple($items, Yii::$app->request->post()) &&
ProductColor::validateMultiple($items, ['image'])){
foreach ($items as $key => $item) {
$item->image = UploadedFile::getInstance($item, "[$key]image");
$item->save();
if ($item->isNewRecord && $item->image instanceof UploadedFile) {
$model->link('productColors', $item);
}
}
}
return $this->renderAjax('_color_form', [
'model' => $model,
'colorModel' => new ProductColor(),
]);
}
public function actionDeleteProductColor($productColorId)
{
/** @var ProductColor $productColor */
$productColor = $this->findModel(ProductColor::className(), $productColorId);
$model = $productColor->product;
$productColor->delete();
$colorModel = new ProductColor();
return $this->renderAjax('_color_form', [
'model' => $model,
'colorModel' => $colorModel
]);
}
public function actionDeleteImage($id)
{
if ($model = $this->findModel(Product::className(), $id)) {
$model->image = null;
$model->save();
}
return $this->redirect(['/product/update', 'id' => $id]);
}
public function actionDelete($id)
{
$model = $this->findModel(Product::className(), $id);
// For UploadImageBehavior
foreach ($model->productColors as $productColor) {
$productColor->delete();
}
$model->delete();
return $this->redirect(['index']);
}
protected function findModel($className, $id)
{
if (class_exists($className)) {
$model = new $className();
if ($model instanceof ActiveRecord) {
$model = $model::findOne($id);
if (!is_null($model)) {
return $model;
}
}
}
throw new NotFoundHttpException();
}
}
A Bit About Actions in ProductController
- Add a scenario to the standard actionCreate and actionUpdate for UploadImageBehavior and to $colorModel variable for view;
- actionUpdateColors is for output/saving a color with AJAX (also for updating several colors at once);
- actionDeleteProductColor is for deleting one color;
- actionDeleteImage is for deleting a product image;
- In actionDelete, we delete all of a product’s colors for UploadImageBehavior to work correctly.
Product Views
create/update are standard, add only one variable in the render
<?= $this->render('_form', [
'model' => $model,
'colorModel' => $colorModel,
]) ?>
Form view (with styling for clarity):
<div class="product-form">
<div class="col-md-6">
<?php $form = ActiveForm::begin(); ?>
<?= $form->field($model, 'title')->textInput(['maxlength' => true]) ?>
<?= $form->field($model, 'status')->dropDownList($model::getStatusList()) ?>
<?php if ($path = $model->getThumbUploadUrl('image', 'thumbnail')): ?>
<div class="js-input-wrapper">
<div class="form-group">
<?= Html::img($path, ['class' => 'img-thumbnail']) ?>
<a href="#" class= "js-edit btn btn-warning glyphicon glyphicon-pencil"></a>
<a href="<?= Url::to(['/product/delete-image', 'id' => $model->id]) ?>" class="btn btn-danger glyphicon glyphicon-remove"
data-action="<?= Url::to(['/product/delete-image', 'id' => $model->id]) ?>"></a>
</div>
<?= $form->field($model, "image")->fileInput(['accept' => 'image/*', 'class' => 'js-input hidden'])->label(false) ?>
</div>
<?php else: ?>
<?= $form->field($model, 'image')->fileInput(['accept' => 'image/*', 'class' => 'form-control']) ?>
<?php endif ?>
<div class="form-group">
<?= Html::submitButton($model->isNewRecord ? Yii::t('app', 'Create') : Yii::t('app', 'Update'),
['class' => $model->isNewRecord ? 'btn btn-success' : 'btn btn-primary']) ?>
</div>
<?php ActiveForm::end(); ?>
</div>
<div class="col-md-6">
<h2><?= Yii::t('app', 'Product Colors') ?></h2>
<?php if (!$model->isNewRecord): ?>
<?php Pjax::begin(['enablePushState' => false]); ?>
<?= $this->render('_color_form', [
'model' => $model,
'colorModel' => $colorModel,
]) ?>
<?php Pjax::end(); ?>
<?php else: ?>
<div class="alert-warning alert fade in"><?= Yii::t('app', 'You need to create a product in order to add colors.'); ?>
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
</div>
<?php endif ?>
</div>
</div>
<?php $this->registerJsFile('@web/js/product-color.js', ['depends' => 'yiiwebJqueryAsset']);
product-color.js:
$(document).ready( function() {
$('.product-form').on('click', '.js-remove', function(e) {
e.preventDefault();
var $this = $(this);
var data = $this.data();
var action = data.action;
var $form = $this.closest('form');
if ($form && action) {
$form.attr('action', action).submit();
}
});
$('.product-form').on('click', '.js-edit', function(){
var $this = $(this);
var $wrapper = $this.closest('.js-input-wrapper');
var $input = $wrapper.find('.js-input');
$input.click();
});
});
_color_form view:
<div class="color-form">
<?php $form = ActiveForm::begin([
'action' => Url::to(['/product/update-colors', 'productId' => $model->id]),
'options' => [
'data-pjax' => true
]
]); ?>
<?php foreach ($model->productColors as $key => $productColor): ?>
<div class="js-input-wrapper form-group">
<?php if ($path = $productColor->getThumbUploadUrl('image', 'thumbnail')): ?>
<?= $form->field($productColor, "[$key]image")->fileInput(['accept' => 'image/*', 'class' => 'js-input hidden'])->label(false) ?>
<?= Html::img($path, ['class' => 'img-thumbnail']) ?>
<a href="#" class= "js-edit btn btn-warning glyphicon glyphicon-pencil"></a>
<?php endif; ?>
<a href="#" class="js-remove btn btn-danger glyphicon glyphicon-remove"
data-action="<?= Url::to(['/product/delete-product-color', 'productColorId' => $productColor->id]) ?>"></a>
</div>
<?php endforeach ?>
<div class="row">
<div class="col-md-6">
<?php $key = isset($key) ? $key 1 : 0 ?>
<?= $form->field($colorModel, "[$key]image")->fileInput(['accept' => 'image/*', 'class' => 'js-input form-control'])->label(Yii::t('app', 'Add Image')) ?>
<?= Html::submitButton(Yii::t('app', 'Upload'), ['class' => 'btn btn-primary']) ?>
</div>
</div>
<?php ActiveForm::end(); ?>
</div>
The main feature of this view is the output, saving, and deletion of a product color using PJAX. It will look as follows:
Index view.
For illustration purposes, let’s add a column with the product image into the grid.
<?= GridView::widget([
'dataProvider' => $dataProvider,
'filterModel' => $searchModel,
'columns' => [
'id',
[
'label' => Yii::t('app', 'Image'),
'format' => 'image',
'value' => function($model) {
return $model->getThumbUploadUrl('image', 'thumbnail');
}
],
'title',
'status:boolean',
'created_at:datetime',
'updated_at:datetime',
['class' => 'yiigridActionColumn', 'template' => '{update} {delete}'],
],
]); ?>
After this, index view will look as follows:
That’s all you need to know. Please, keep in mind that this solution is written for mongosoft/yii2—upload—behavior. You may use any other extension for saving/cropping images.