Yii: работа с множественной загрузкой фото

  • 04, 14, 2013
  •  
  •  php
  • Комментарии к записи Yii: работа с множественной загрузкой фото отключены

Для генерации превьюшек разных размеров понадобится 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/ — про правильную запись отношений между моделями.

Comments are closed.