Yii: работа с множественной загрузкой фото
Table of Contents
Для генерации превьюшек разных размеров понадобится http://www.yiiframework.com/extension/image/ (В описании установки есть ошибка: CArray.php нужно скопировать в корень protected/components проекта).
Постановка
Задача такая:
Есть разные объекты разных классов (для примера, квартиры и многоквартирные дома), каждый из них может иметь неограниченное количество фотографий.
Все фотографии представляются объектом Photo, а принадлежность к разным типам объектов зависит от атрибута ref_type.
Нужно иметь превью фотографий разных размеров (генерировать при сохранении фото).
Достаточно сделать загрузку файлов только в формате jpg (поэтому расширение зашито жёстко).
SQL
Вот SQL-код моделей (SQLite; часть полей опущена):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
/** * 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
<? $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
1 2 3 4 5 6 7 8 9 10 11 12 13 |
... '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/ — про правильную запись отношений между моделями.
Similar Posts
- None Found