Магазин на Yii2, часть 30. Админка: WYSIWYG-редактор и изображение для товара

11.09.2019

Теги: CRUDWeb-разработка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']]); ?>

Поиск: Web-разработка • Yii2 • Изображение • Интернет магазин • Каталог товаров • Редактор • Установка • Фреймворк • Панель управления • UploadedFile • Практика • CRUD

Каталог оборудования
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Производители
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Функциональные группы
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.