Yii: работа с множественной загрузкой фото
Table of Contents
Для генерации превьюшек разных размеров понадобится http://www.yiiframework.com/extension/image/ (В описании установки есть ошибка: CArray.php нужно скопировать в корень protected/components проекта).
Постановка
Задача такая:
Есть разные объекты разных классов (для примера, квартиры и многоквартирные дома), каждый из них может иметь неограниченное количество фотографий.
Все фотографии представляются объектом Photo, а принадлежность к разным типам объектов зависит от атрибута ref_type.
Нужно иметь превью фотографий разных размеров (генерировать при сохранении фото).
Достаточно сделать загрузку файлов только в формате jpg (поэтому расширение зашито жёстко).
SQL
Вот SQL-код моделей (SQLite; часть полей опущена):
CREATE TABLE tbl_apartment_house ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, description TEXT NOT NULL, address VARCHAR(255), floors INTEGER ); CREATE TABLE tbl_flat ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, description TEXT NOT NULL, floor INTEGER, apartment_house_id INTEGER NOT NULL, CONSTRAINT FK_comment_post FOREIGN KEY (apartment_house_id) REFERENCES tbl_apartment_house (id) ON DELETE CASCADE ON UPDATE RESTRICT ); CREATE TABLE tbl_photo ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, slug VARCHAR(50) NOT NULL, // сгенерированное уникальное название файла description TEXT, ref_id INTEGER NOT NULL, ref_type INTEGER NOT NULL );
Код моделей, контроллеров и отображений генерировался с помощью Gii, так что начальный код приводить смысла нет.
Модели
Photo
class Photo extends CActiveRecord
{
const REF_TYPE_APARTMENT_HOUSE = 1;
const REF_TYPE_FLAT = 2;
public static $PHOTO_SIZES = array(
'big' => 1000,
'medium' => 200,
'thumb' => 100
);
/**
* Для новых объектов генерируем название файла
*/
public function afterConstruct()
{
$res = parent::afterConstruct();
if (! $this->slug) {
$this->slug = uniqid(); // $this->slug = uniqid('', true);
}
return $res;
}
public static function model($className=__CLASS__)
{
return parent::model($className);
}
...
public function search()
{
$criteria=new CDbCriteria;
return new CActiveDataProvider($this, array(
'criteria'=>$criteria,
));
}
/**
* Удаляем файлы после удаления модели
*
* @since 14.04.13
* @author bullgare
*/
public function afterDelete()
{
foreach (self::$PHOTO_SIZES as $key => $size)
{
$name = ucfirst($key);
$path = $this->{'getPathTo' . $name}();
unlink($path);
}
unlink(self::getPathToOrig());
rmdir(self::getPathToDirAbs($this->slug));
parent::afterDelete();
}
/**
* Перед сохранением модели сохраняем файл и все превью
*
* @since 13.04.13
* @author bullgare
*/
public function save(CUploadedFile $photoFile, $runValidation = true, $attributes = null)
{
$resSave = $photoFile->saveAs($this->getPathToOrig());
$resResize = $this->saveResized();
return $resSave && $resResize && parent::save($runValidation, $attributes);
}
/**
* Создаём превью
*
* @since 14.04.13
* @author bullgare
*/
public function saveResized()
{
Yii::import('application.extensions.image.Image');
$res = true;
foreach (self::$PHOTO_SIZES as $key => $size)
{
$image = new Image($this->getPathToOrig());
$name = ucfirst($key);
$path = $this->{'getPathTo' . $name}();
$image->resize($size, $size)->quality(90);
$res = $image->save($path) && $res;
}
return $res;
}
/**
* Если нет необходимых директорий, создаём
*
* @since 13.04.13
* @author bullgare
* @static
*/
public static function checkAndCreateDirs($pathToDir)
{
$pathWebRoot = Yii::getPathOfAlias('webroot');
$pathSuffix = substr($pathToDir, strlen($pathWebRoot));
$pathCHunks = explode('/',$pathSuffix);
$pathCur = $pathWebRoot;
foreach ($pathCHunks as $chunk)
{
if (strlen($chunk))
{
$pathCur .= '/' . $chunk;
if (! is_dir($pathCur))
{
mkdir($pathCur);
chmod($pathCur, 0755);
}
}
}
}
/**
* Абсолютный путь к директории с фотографиями
*
* @since 13.04.13
* @author bullgare
*/
public static function getPathToDirAbs($slug, $checkAccess = false)
{
$path = Yii::getPathOfAlias('webroot') . self::getPathToDir($slug);
if ($checkAccess)
{
self::checkAndCreateDirs($path);
if (! is_dir($path)) {
return false;
}
}
return $path;
}
/**
* Путь к директории без webroot
*
* @since 14.04.13
* @author bullgare
* @static
*/
public static function getPathToDir($slug)
{
$prefix = substr('' . $slug, 0, 2);
return '/images/obj_photos/' . $prefix . '/' . $slug;
}
/**
* Путь к загруженному фото
*
* @since 13.04.13
* @author bullgare
*/
public function getPathToOrig($web = false)
{
return ($web ? self::getPathToDir($this->slug) : self::getPathToDirAbs($this->slug)) . '/orig.jpg';
// return $this->getPathToDir() . '/orig.' . $this->getExtensionName();
}
public function getPathToBig($web = false)
{
return ($web ? self::getPathToDir($this->slug) : self::getPathToDirAbs($this->slug)) . '/big.jpg';
}
public function getPathToMedium($web = false)
{
return ($web ? self::getPathToDir($this->slug) : self::getPathToDirAbs($this->slug)) . '/medium.jpg';
}
public function getPathToThumb($web = false)
{
return ($web ? self::getPathToDir($this->slug) : self::getPathToDirAbs($this->slug)) . '/thumb.jpg';
}
}
В контроллере нужно будет для каждой фотографии вызвать метод save(), который сохранит на диск файл и все превью.
Flat
/**
* This is the model class for table "{{flat}}".
* ...
* The followings are the available model relations:
* @property ApartmentHouse $apartmentHouse
* @property Photo $photo
*/
class Flat extends CActiveRecord
{
public static function model($className=__CLASS__)
{
return parent::model($className);
}
public function relations()
{
return array(
'apartmentHouse' => array(self::BELONGS_TO, 'ApartmentHouse', 'apartment_house_id'),
// связь с фотографиями
'photos' => array(
self::HAS_MANY,
'Photo',
'ref_id',
'on' => 'photos.ref_type = :type',
'params' => array(':type'=>Photo::REF_TYPE_FLAT)
),
);
}
public function attributeLabels()
{
return array(
'description' => 'Описание',
'floor' => 'Этаж',
'apartment_house_id' => 'Дом',
'photos' => 'Фото',
);
}
Контроллеры
FlatController
public function actionCreate()
{
$model=new Flat;
if(isset($_POST['Flat']))
{
$model->attributes=$_POST['Flat'];
if($model->save())
{
$this->savePhotos($model->id);
$this->redirect(array('view','id'=>$model->id));
}
}
$this->render('create',array(
'model'=>$model,
));
}
public function actionUpdate($id)
{
$model=$this->loadModel($id);
if(isset($_POST['Flat']))
{
$model->attributes=$_POST['Flat'];
if($model->save())
{
$this->savePhotos($id);
$this->redirect(array('view','id'=>$model->id));
}
}
$this->render('update',array(
'model'=>$model,
));
}
/**
* Сохраняем фотографии
*
* @since 14.04.13
* @author bullgare
*/
public function savePhotos($flatId)
{
$photoFiles = CUploadedFile::getInstancesByName('photo');
if (! empty($photoFiles))
{
foreach ($photoFiles as $photoFile)
{
$photoObj = new Photo();
$photoObj->ref_id = $flatId;
$photoObj->ref_type = Photo::REF_TYPE_FLAT;
if (Photo::getPathToDirAbs($photoObj->slug, true))
{
if (! $photoObj->save($photoFile)) {
// TODO error handling saving original OR creating thumbnails
}
}
else {
// TODO error handling - no permissions
}
}
}
}
Пока тут нет обработки ошибок, только размечены места, где она будет.
Всё, что нужно — это вызвать метод savePhotos().
view
/views/flat/_form.php
<? $form=$this->beginWidget('CActiveForm', array(
'id'=>'flat-form',
'enableAjaxValidation'=>false,
'htmlOptions' => array('enctype' => 'multipart/form-data'),
)); ?>
<div class="row">
<?php echo $form->labelEx($model,'apartment_house_id'); ?>
<?php echo $form->dropDownList($model,'apartment_house_id', CHtml::listData(ApartmentHouse::model()->findAll(), 'id', 'address')); ?>
<?php //echo $form->textField($model,'apartment_house_id'); ?>
<?php echo $form->error($model,'apartment_house_id'); ?>
</div>
<div class="row">
<?php echo $form->labelEx($model,'photos'); ?>
<ul>
<? foreach ($model->photos as $photo) {?>
<li>
<?= CHtml::image($photo->getPathToThumb(true)); ?>
<?= CHtml::link(CHtml::encode('удалить'), array('photo/delete', 'id' => $photo->id), array('class' => 'js-delete-photo')); ?>
</li>
<? } ?>
</ul>
Добавить
<? $this->widget('CMultiFileUpload', array(
'name' => 'photo',
'accept' => 'jpeg|jpg', // jpeg|jpg|gif|png // useful for verifying files
'duplicate' => 'Дублирующиеся фото', // useful, i think
'denied' => 'Только jpeg', // useful, i think
)); ?>
<?php echo $form->error($model,'photo'); ?>
</div>
<script type="text/javascript">
$(document).ready(function () {
$('.js-delete-photo').on('click', function deletePhoto(e) {
e.preventDefault();
var url = $(this).attr('href');
$.post(url).
success(setTimeout(function () {location.reload()}, 500));
});
});
</script>
Не надо забывать про multipart/form-data у формы.
Отображение фотографий на странице просмотра оставим на факультатив:)
Вот собственно и всё.
На всякий случай можно добавить в конфиг image:
/config/main.php
... 'components'=>array( 'user'=>array( // enable cookie-based authentication 'allowAutoLogin'=>true, 'loginUrl'=>null, // 403 вместо редиректа на страницу авторизации ), 'image' => array( 'class' => 'application.extensions.image.CImageComponent', // GD or ImageMagick 'driver' => 'GD', ), ...
Полезные ссылки:
http://sudwebdesign.com/yii-uploading-and-saving-images/541
http://www.yiiframework.com/wiki/176/uploading-multiple-images-with-cmultifileupload/
http://www.yiiframework.com/forum/index.php/topic/6392-fancyupload/page__view__findpost__p__91605
http://stackoverflow.com/questions/10891294/condition-while-making-relation-in-yii/16000313#16000313 и http://www.yiiframework.com/forum/index.php/topic/10185-using-relations-and-conditions/ — про правильную запись отношений между моделями.