Магазин на Yii2, часть 30. Админка: WYSIWYG-редактор и изображение для товара
11.09.2019
Теги: CRUD • Web-разработка • Yii2 • Изображение • ИнтернетМагазин • КаталогТоваров • ПанельУправления • Практика • Редактор • Установка • Фреймворк
Теперь займемся формой для добавления и редактирования товара. Установим расширение CKEditor, чтобы добавить WYSIWYG-редактор для удобной работы с описанием товара. И организуем загрузку картинки товара с использованием класса yii\web\UploadedFile
.
WYSIWYG-редактор для товаров
Начнем с установки расширения CKEditor:
> composer require --prefer-dist mihaildev/yii2-ckeditor "*" ./composer.json has been updated Loading composer repositories with package information Updating dependencies (including require-dev) Package operations: 1 install, 0 updates, 0 removals - Installing mihaildev/yii2-ckeditor (1.0.1): Downloading (100%) Package phpunit/phpunit-mock-objects is abandoned, you should avoid using it. No replacement was suggested. Writing lock file Generating autoload files
Пример использования расширения:
use mihaildev\ckeditor\CKEditor; CKEditor::widget([ 'editorOptions' => [ // разработанны стандартные настройки basic, standard, full 'preset' => 'basic', 'inline' => false, // по умолчанию false ] ]); // или c ActiveForm echo $form->field($post, 'content')->widget(CKEditor::className(),[ 'editorOptions' => [ // разработанны стандартные настройки basic, standard, full 'preset' => 'basic', 'inline' => false, // по умолчанию false ], ]);
Открываем на редактирование файл view-шаблона с формой добавления-редактироваия товара:
<?php /* * Форма для добавления и редактирования товара, файл modules/admin/views/product/_form.php */ use mihaildev\ckeditor\CKEditor; use app\modules\admin\models\Brand; use app\modules\admin\models\Category; use yii\helpers\ArrayHelper; use yii\helpers\Html; use yii\widgets\ActiveForm; /* @var $this yii\web\View */ /* @var $model app\modules\admin\models\Product */ /* @var $form yii\widgets\ActiveForm */ ?> <?php $form = ActiveForm::begin(); ?> <?= $form->field($model, 'name')->textInput(['maxlength' => true]); ?> <?php $category = Yii::$app->request->get('category') ?: 0; $param = ['options' => [$category => ['selected' => true]]]; echo $form->field($model, 'category_id')->dropDownList(Category::getTree(), $param); ?> <?= $form->field($model, 'brand_id')->dropDownList( ArrayHelper::map(Brand::find()->all(), 'id', 'name') ); ?> <?= $form->field($model, 'price')->textInput(['maxlength' => true]); ?> <?= $form->field($model, 'image')->textInput(['maxlength' => true]); ?> <?= $form->field($model, 'content')->widget( CKEditor::class, [ 'editorOptions' => [ // разработанны стандартные настройки basic, standard, full 'preset' => 'basic', 'inline' => false, // по умолчанию false ], ] ); ?> <?= $form->field($model, 'keywords')->textarea(['rows' => 2, 'maxlength' => true]); ?> <?= $form->field($model, 'description')->textarea(['rows' => 2, 'maxlength' => true]); ?> <?= $form->field($model, 'hit')->checkbox(); ?> <?= $form->field($model, 'new')->checkbox(); ?> <?= $form->field($model, 'sale')->checkbox(); ?> <div class="form-group"> <?= Html::submitButton('Сохранить', ['class' => 'btn btn-success']) ?> </div> <?php ActiveForm::end(); ?>
Загрузка и резайз изображений
Добавляем два вспомогательных свойства для класса модели Product
, изменяем правила валидации. Кроме того, нам потребуются несколько новых методов — для загрузки изображения при добавлении или обновлении товара и для удаления старого изображения.
<?php namespace app\modules\admin\models; use Yii; use yii\db\ActiveRecord; use yii\imagine\Image; /** * Это модель для таблицы БД `product` * * @property int $id Уникальный идентификатор * @property int $category_id Родительская категория * @property int $brand_id Идентификатор бренда * @property string $name Наименование товара * @property string $content Описание товара * @property string $price Цена товара * @property string $keywords Мета-тег keywords * @property string $description Мета-тег description * @property string $image Имя файла изображения * @property int $hit Лидер продаж? * @property int $new Новый товар? * @property int $sale Распродажа? */ class Product extends ActiveRecord { /** * Вспомогательный атрибут для загрузки изображения товара */ public $upload; /** * Вспомогательный атрибут для удаления изображения товара */ public $remove; /** * Возвращает имя таблицы базы данных */ public static function tableName() { return 'product'; } /** * Возвращает данные о родительской категории */ public function getCategory() { return $this->hasOne(Category::class, ['id' => 'category_id']); } /** * Возвращает наименование родительской категории */ public function getCategoryName() { $parent = $this->category; return $parent ? $parent->name : ''; } /** * Возвращает данные о бренде товара */ public function getBrand() { return $this->hasOne(Brand::class, ['id' => 'brand_id']); } /** * Возвращает наименование бренда товара */ public function getBrandName() { $brand = $this->brand; return $brand ? $brand->name : ''; } /** * Правила валидации полей формы при создании и редактировании товара */ public function rules() { return [ [['category_id', 'brand_id', 'name', 'price'], 'required'], [['category_id', 'brand_id', 'hit', 'new', 'sale'], 'integer'], ['content', 'string'], ['price', 'number', 'min' => 1], [['name', 'keywords', 'description'], 'string', 'max' => 255], // атрибут image проверяем с помощью валидатора image ['image', 'image', 'extensions' => 'png, jpg, gif'], // вспомогательный атрибут remove помечаем как безопасный ['remove', 'safe'] ]; } /** * Возвращает имена полей формы для создания и редактирования товара */ public function attributeLabels() { return [ 'id' => 'ID', 'category_id' => 'Категория', 'brand_id' => 'Бренд', 'name' => 'Наименование', 'content' => 'Описание', 'price' => 'Цена', 'keywords' => 'Мета-тег keywords', 'description' => 'Мета-тег description', 'image' => 'Изображение', 'hit' => 'Лидер продаж', 'new' => 'Новинка', 'sale' => 'Распродажа', 'remove' => 'Удалить изображение' ]; } /** * Загружает файл изображения товара */ public function uploadImage() { if ($this->upload) { // только если был выбран файл для загрузки $name = md5(uniqid(rand(), true)) . '.' . $this->upload->extension; // сохраняем исходное изображение в директории source $source = Yii::getAlias('@webroot/images/products/source/' . $name); if ($this->upload->saveAs($source)) { // выполняем resize, чтобы получить еще три размера $large = Yii::getAlias('@webroot/images/products/large/' . $name); Image::thumbnail($source, 1000, 1000)->save($large, ['quality' => 100]); $medium = Yii::getAlias('@webroot/images/products/medium/' . $name); Image::thumbnail($source, 500, 500)->save($medium, ['quality' => 95]); $small = Yii::getAlias('@webroot/images/products/small/' . $name); Image::thumbnail($source, 250, 250)->save($small, ['quality' => 90]); return $name; } } return false; } /** * Удаляет старое изображение при загрузке нового */ public static function removeImage($name) { if (!empty($name)) { $source = Yii::getAlias('@webroot/images/products/source/' . $name); if (is_file($source)) { unlink($source); } $large = Yii::getAlias('@webroot/images/products/large/' . $name); if (is_file($large)) { unlink($large); } $medium = Yii::getAlias('@webroot/images/products/medium/' . $name); if (is_file($medium)) { unlink($medium); } $small = Yii::getAlias('@webroot/images/products/small/' . $name); if (is_file($small)) { unlink($small); } } } /** * Удаляет изображение при удалении товара */ public function afterDelete() { parent::afterDelete(); self::removeImage($this->image); } }
Для ресайза изображений используем расширение yii2-imagine, которое установим через Composer:
> composer require --prefer-dist yiisoft/yii2-imagine Using version ^2.2 for yiisoft/yii2-imagine ./composer.json has been updated Loading composer repositories with package information Updating dependencies (including require-dev) Package operations: 2 installs, 0 updates, 0 removals - Installing imagine/imagine (1.2.2): Downloading (100%) - Installing yiisoft/yii2-imagine (2.2.0): Downloading (100%) imagine/imagine suggests installing ext-imagick (to use the Imagick implementation) imagine/imagine suggests installing ext-gmagick (to use the Gmagick implementation) Package phpunit/phpunit-mock-objects is abandoned, you should avoid using it. No replacement was suggested. Writing lock file Generating autoload files
Вносим изменения в методы контроллера actionCreate()
и actionUpdate()
:
<?php namespace app\modules\admin\controllers; use Yii; use app\modules\admin\models\Product; use yii\data\ActiveDataProvider; use yii\web\NotFoundHttpException; use yii\filters\VerbFilter; use yii\web\UploadedFile; /** * Класс ProductController реализует CRUD для товаров */ class ProductController extends AdminController { public function behaviors() { return [ 'verbs' => [ 'class' => VerbFilter::class, 'actions' => [ 'delete' => ['POST'], ], ], ]; } /** * Список всех товаров с постраничной навигацией */ public function actionIndex() { $dataProvider = new ActiveDataProvider([ 'query' => Product::find(), ]); return $this->render('index', [ 'dataProvider' => $dataProvider, ]); } /** * Просмотр данных существующего товара */ public function actionView($id) { return $this->render('view', [ 'model' => $this->findModel($id), ]); } /** * Создание нового товара */ public function actionCreate() { $model = new Product(); if ($model->load(Yii::$app->request->post()) && $model->validate()) { // загружаем изображение и выполняем resize исходного изображения $model->upload = UploadedFile::getInstance($model, 'image'); if ($name = $model->uploadImage()) { // если изображение было загружено // сохраняем в БД имя файла изображения $model->image = $name; } $model->save(); return $this->redirect(['view', 'id' => $model->id]); } return $this->render( 'create', ['model' => $model] ); } /** * Обновление существующего товара */ public function actionUpdate($id) { $model = $this->findModel($id); // старое изображение, которое надо удалить, если загружено новое $old = $model->image; if ($model->load(Yii::$app->request->post()) && $model->validate()) { // если отмечен checkbox «Удалить изображение» if ($model->remove) { // удаляем старое изображение if (!empty($old)) { $model::removeImage($old); } // сохраняем в БД пустое имя $model->image = ''; // чтобы повторно не удалять $old = ''; } else { // оставляем старое изображение $model->image = $old; } // загружаем изображение и выполняем resize исходного изображения $model->upload = UploadedFile::getInstance($model, 'image'); if ($new = $model->uploadImage()) { // если изображение было загружено // удаляем старое изображение if (!empty($old)) { $model::removeImage($old); } // сохраняем в БД новое имя $model->image = $new; } $model->save(); return $this->redirect(['view', 'id' => $model->id]); } return $this->render('update', [ 'model' => $model, ]); } /** * Удаление существующего товара */ public function actionDelete($id) { $this->findModel($id)->delete(); return $this->redirect(['index']); } /** * Поиск товара по идентификатору */ protected function findModel($id) { if (($model = Product::findOne($id)) !== null) { return $model; } throw new NotFoundHttpException('The requested page does not exist.'); } }
Изменяем форму добавления и редактирования товара:
<?php /* * Форма для добавления и редактирования товара, файл modules/admin/views/product/_form.php */ use mihaildev\ckeditor\CKEditor; use app\modules\admin\models\Brand; use app\modules\admin\models\Category; use yii\helpers\ArrayHelper; use yii\helpers\Html; use yii\widgets\ActiveForm; /* @var $this yii\web\View */ /* @var $model app\modules\admin\models\Product */ /* @var $form yii\widgets\ActiveForm */ ?> <?php $form = ActiveForm::begin(); ?> <?= $form->field($model, 'name')->textInput(['maxlength' => true]); ?> <?php $category = Yii::$app->request->get('category') ?: 0; $param = ['options' => [$category => ['selected' => true]]]; echo $form->field($model, 'category_id')->dropDownList(Category::getTree(), $param); ?> <?= $form->field($model, 'brand_id')->dropDownList( ArrayHelper::map(Brand::find()->all(), 'id', 'name') ); ?> <?= $form->field($model, 'price')->textInput(['maxlength' => true]); ?> <fieldset> <legend>Загрузить изображение</legend> <?= $form->field($model, 'image')->fileInput(); ?> <?php if (!empty($model->image)) { $img = Yii::getAlias('@webroot') . '/images/products/source/' . $model->image; if (is_file($img)) { $url = Yii::getAlias('@web') . '/images/products/source/' . $model->image; echo 'Уже загружено ', Html::a('изображение', $url, ['target' => '_blank']); echo $form->field($model,'remove')->checkbox(); } } ?> </fieldset> <?= $form->field($model, 'content')->widget( CKEditor::class, [ 'editorOptions' => [ // разработанны стандартные настройки basic, standard, full 'preset' => 'basic', 'inline' => false, // по умолчанию false ], ] ); ?> <?= $form->field($model, 'keywords')->textarea(['rows' => 2, 'maxlength' => true]); ?> <?= $form->field($model, 'description')->textarea(['rows' => 2, 'maxlength' => true]); ?> <?= $form->field($model, 'hit')->checkbox(); ?> <?= $form->field($model, 'new')->checkbox(); ?> <?= $form->field($model, 'sale')->checkbox(); ?> <div class="form-group"> <?= Html::submitButton('Сохранить', ['class' => 'btn btn-success']) ?> </div> <?php ActiveForm::end(); ?>
До версии 2.0.8 для формы, которая загружает файлы, обязательно нужно было добавлять атрибут enctype
со значением multipart/form-data
:
<?php $form = ActiveForm::begin(['options' => ['enctype' => 'multipart/form-data']]); ?>
- Магазин на Yii2, часть 31. Админка: загрузка изображений для категорий и брендов
- Магазин на Yii2, часть 35. Админка: загрузка картинок для страниц и страница 404
- Магазин на Yii2, часть 33. Админка: приводим в порядок CRUD-код для страниц
- Магазин на Yii2, часть 32. Админка: удаление категорий и CRUD для страниц
- Магазин на Yii2, часть 29. Админка: добавляем список товаров категории
- Магазин на Yii2, часть 28. Админка: выбор родителя и список всех категорий
- Магазин на Yii2, часть 27. Админка: приводим в порядок сгенерированный код
Поиск: Web-разработка • Yii2 • Изображение • Интернет магазин • Каталог товаров • Редактор • Установка • Фреймворк • Панель управления • UploadedFile • Практика • CRUD