Несколько изображений к 1 записи

21.07.2013

Несколько изображений к одной записи

В последнее время в скайпе и комментах промелькнул один и тот же вопрос - как организовать управлениние несколькими изображениями, относящимися к одной записи. Это очень простой вопрос и в этом посте покажу, как это делаю я. Речь пойдет только об административной части сайта.

Пускай нужно сделать CRUD объявлений, причем у каждого объявления должна быть 1 главная фотография и несколько дополнительных. Главную отображаем везде, по-дефолту.

01. Структура таблиц

Используем 2 таблицы: advert и advert_image со следующими полями

CREATE TABLE `advert` (
 `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
 `title` VARCHAR(255) NOT NULL,
 `text` TEXT NOT NULL,
 `main_image` VARCHAR(255) NOT NULL,
 `created_date` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
 PRIMARY KEY (`id`)
 );

 CREATE TABLE `advert_image` (
 `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
 `advert_id` INT(11) UNSIGNED NOT NULL,
 `image` VARCHAR(255) NOT NULL,
 PRIMARY KEY (`id`),
 CONSTRAINT `fk_advert_id1` FOREIGN KEY (`advert_id`) REFERENCES `advert` (`id`) ON UPDATE CASCADE ON DELETE CASCADE
 );

Из структуры видно, что в главной таблице в поле main_image будем хранить изображение по-умолчанию и связь между advert и advert_image 1 ко многим.

02. Генерируем модели

Генерируем модели при помощи gii, немного подправим правила валидации и лейблы полей. Код можно посмотреть в репозитории на гитхабе.
В дополнение давайте подключим к модели Advert поведение, отвечающее за сохранение изображения из поля main_image. Опять же весь код поведения тут выкладывать не буду, смотрим на гите (в нагрузку можно почитать отличную статью про поведения от Дмитрия Елисеева - http://www.elisdn.ru/blog/41/usage-of-behaviors-in-yii)

public function behaviors() {
    return array(
        'imageUploadBehavior' => array(
            'class' => 'ext.ImageUploadBehavior',
            'imagePath' => 'uploads',
            'imageField' => 'main_image',
        ),
    );
}

Как видно из кода выше - указываем папку для хранения изображений, в нашем случае uploads/id-записи/ и название поля main_image. Подключение этого поведения позволяет нам использовать стандартный CRUD для добавления объявлений, а код для загруженных изображений будет находиться отдельно, в файле бехавиора.

03. Пишем CRUD для работы с объявлениями

Теперь давайте сгенерируем стандартный КРУД для модели advert, в нашем случае будет только 1 контроллер AdvertController и несколько действий внутри него. Единственное - не забываем исправить в форме инпут для main_image на fileField и указать 'enctype' => 'multipart/form-data' для формы.

Для удобства в форму, в блок с изображением добавили следующий код:

if(!$model->isNewRecord) {
    echo '
'.CHtml::image($model->getImageUrl(), $model->title, array('style' => 'height: 200px'));
}

Метод $model->getImageUrl() - находится в нашем поведении и возвращает УРЛ изображения.
Несколько человек хотели сделать вывод изображения в гриде при помощи расширения, как вариант конечно можно, но проще использовать то что уже есть, поэтому приведу небольшой пример, как определить столбец с изображениями в CGridView:

array(
  'name' => 'main_image',
  'type' => 'raw',    // главное указать тип колонки
  'filter' => false,
  'value' => function($data) {
    return CHtml::image($data->getImageUrl(), $data->title, array('style' => 'height: 100px;'));
  }
),

04. Добавляем несколько изображений

Первое, что нам нужно, в модели Advert определить открытое свойство images (public $images). И добавить ещё 1 элемент в нашу форму - CMultiFileUpload:

echo $form->labelEx($model, 'images');
$this->widget('CMultiFileUpload', array(
  'model' => $model,
  'attribute' => 'images',
  'accept' => 'jpg',
  'duplicate' => 'Этот файл уже выбран!',
  'denied' => 'Недопустимый тип файла',
  'htmlOptions' => array(
    'multiple' => 'multiple',
  ),
));
if ($model->advertImages) {
  $this->renderPartial('_imageGrid', array('model' => $model));
}

$model->advertImages - реляция до таблицы с изображениями. Соответственно если у нас есть связанные записи, то выводим грид с этими изображениями. Сам файл _imageGrid:

$images = new CActiveDataProvider('AdvertImage', array(
  'criteria' => array(
    'condition' => 'advert_id=' . $model->id,
  ),
  'pagination' => array(
    'pageSize' => 20,
  ),
));

$this->widget('zii.widgets.grid.CGridView', array(
  'id' => 'image-grid',
  'dataProvider' => $images,
  'template' => "{items}",
  'columns' => array(
    array(
      'header' => 'Изображение',
      'type' => 'raw',
      'value' => function ($data) {
        return CHtml::image($data->getImageUrl(), $data->title, array('style' => 'height: 100px;'));
      }
    ),
    array(
      'header' => 'Название',
      'filter' => false,
      'value' => function ($data) {
        return $data->image;
      }
    ),
    array(
      'class' => 'CButtonColumn',
      'template' => '{delete}',
      'deleteButtonUrl' => 'Yii::app()->createUrl("/advert/deleteImage", array("id" => $data->id))',
    ),
  ),
));

Может быть не самое лучше решение для выбоки изображений, и вы что-то предложите сами.
Осталось дописать пару методов.
1. Допишем метод сохранения изображений, переданных из CMultiFileUpload (метод будет находиться прямо в модели Advert):

protected function afterSave() {
    parent::afterSave();

    $images = CUploadedFile::getInstances($this, 'images');
    if (isset($images) && count($images) > 0) {
        foreach($images as $k => $img) {
            $imageName = md5(microtime()) . '.jpg';
            if($img->saveAs($this->getFolder().$imageName)) {
                $advImg = new AdvertImage();
                $advImg->advert_id = $this->getPrimaryKey();
                $advImg->image = $imageName;
                $advImg->save();
            }
        }
    }
}

protected function getFolder() {
    $folder = Yii::getPathOfAlias('webroot') . '/uploads/' . $this->getPrimaryKey() . '/images/';
    if (is_dir($folder) == false)
       mkdir($folder, 0755, true);
     return $folder;
}

В методе afterSave() получаем экземпляры изображений, генерируем им имя рандомом, и далее сохраняем изображения в папку uploads/$id/images/. Кроме этого не забываем добавить images в правила валидации.

Ну и самое последнее, что осталось сделать - реализовать удаление прикрепленных изображений, а конкретнее написать экшен actionDeleteImage в AdvertController:

public function actionDeleteImage($id) {
    $model = AdvertImage::model()->findByPk((int)$id);
    if($model) {
        $model->delete();
    } else {
        throw new CHttpException(404, 'The requested page does not exist.');
    }
}

Не забываем при этом удалить файлы изображений с диска, для этого в AdvertImage:

protected function afterDelete() {
    parent::afterDelete();
    $file = Yii::getPathOfAlias('webroot') . '/uploads/' . $this->advert->id . '/images/' . $this->image;
    if (file_exists($file) && !is_dir($file)) {
        unlink($file);
    }
}

05. Заключение

Примерно так я осуществляю работу с несколькими файлами для 1 модели. Весь код можно посмотреть в репозитории на гитхабе.

yii
blog comments powered by Disqus
Наверх