Хорошей практикой считается создание тонких контроллеров и толстых моделей
всегда, когда это возможно. Это позволяет сделать код приложения более пригодным
для повторного использования.
Данный рецепт повторяет то, что уже было описано в рецепте
«Как загрузить файл используя модель»
с той лишь разницей, что теперь мы не будем писать код, связанный с работой с файлами,
в контроллере, а вынесем его в модель.
Кроме того, в рецепте будут показаны модель и контроллер для того случая,
когда нам может пригодится функционал не только добавления объекта модели с файлом
(C
в CRUD
), но и обновления файла при редактировании уже существующего объекта модели
(U
в CRUD
), который был создан ранее и хранится в базе данных.
Контроллер стандартный, почти такой же, какой генерируется при помощи
Gii. В данном случае контроллер
не выполняет действий, связанных с загружаемыми файлами напрямую. Всё, чем он
занимается — это обработка HTTP запросов, передача данных модели
и отображение представления.
class ItemController extends Controller{
public function actionUpdate($id=null){
// в зависимости от аргумента создаем модель или ищем уже существующую
if($id===null)
$model=new Item();
else if(!$model=Item::model()->findByPk($id))
throw new CHttpException(404);
if(isset($_POST['Item'])){
$model->attributes=$_POST['Item'];
if($model->save()){
// отображаем успешное сообщение, обновляем страницу
// или перенаправляем куда-либо ещё
$this->refresh();
}
}
$this->render('update',array('model'=>$model));
}
}
Форму в представлении можно создать при помощи класса CHtml, но так как это уже было показано в рецепте
«Как загрузить файл используя модель», то мы
сделаем это через CActiveForm.
<?php
/* @var $this ItemController */
/* @var $model Item */
/* @var $form CActiveForm */
?>
<?php $form=$this->beginWidget('CActiveForm',array(
'htmlOptions'=>array('enctype'=>'multipart/form-data'),
)); ?>
<?php /* текстовое поле названия элемента */ ?>
<div class="field">
<?php echo $form->labelEx($model,'title'); ?>
<?php echo $form->textField($model,'title'); ?>
<?php echo $form->error($model,'title'); ?>
</div>
<?php /* поле для загрузки файла */ ?>
<div class="field">
<?php if($model->document): ?>
<p><?php echo CHtml::encode($model->document); ?></p>
<?php endif; ?>
<?php echo $form->labelEx($model,'document'); ?>
<?php echo $form->fileField($model,'document'); ?>
<?php echo $form->error($model,'document'); ?>
</div>
<?php /* кнопка отправки */ ?>
<div class="button">
<?php echo CHtml::submitButton($model->isNewRecord ? 'Создать' : 'Сохранить'); ?>
</div>
<?php $this->endWidget(); ?>
Структура формы стандартная. Отображаем две подписи (<label />
), одно текстовое поле,
одно файловое поле, название файла (если он был загружен ранее) и кнопку отправки формы.
Не забываем указать атрибут enctype
со значением multipart/form-data
в открывающем теге формы.
Основная часть рецепта — это модель, в которой находится код, отвечающий за работу с файлом:
валидация, обработка файла и его сохранение происходит в ней.
Переопределяем метод CActiveRecord::beforeSave(), выполняемый перед сохранением AR-модели.
В нём получаем экземпляр класса загруженного файла и сохраняем его в нужное место на диске.
Под обновлением понимается редактирование модели, то есть тогда, когда
CActiveRecord::getIsNewRecord()
возвращает false
и активным сценарием валидации
по умолчанию является update
.
/**
* @property integer $id
* @property string $title
* @property string $document
*/
class Item extends CActiveRecord{
public $document;
public static function model($className=__CLASS__){
return parent::model($className);
}
public function tableName(){
return '{{item}}';
}
public function rules(){
return array(
array('title','required','on'=>'insert,update'),
array('document','file','types'=>'doc,docx,xls,xlsx,odt,pdf',
'allowEmpty'=>true,'on'=>'insert,update'),
);
}
protected function beforeSave(){
if(!parent::beforeSave())
return false;
if(($this->scenario=='insert' || $this->scenario=='update') &&
($document=CUploadedFile::getInstance($this,'document'))){
$this->deleteDocument(); // старый документ удалим, потому что загружаем новый
$this->document=$document;
$this->document->saveAs(
Yii::getPathOfAlias('webroot.media').DIRECTORY_SEPARATOR.$this->document);
}
return true;
}
protected function beforeDelete(){
if(!parent::beforeDelete())
return false;
$this->deleteDocument(); // удалили модель? удаляем и файл
return true;
}
public function deleteDocument(){
$documentPath=Yii::getPathOfAlias('webroot.media').DIRECTORY_SEPARATOR.
$this->document;
if(is_file($documentPath))
unlink($documentPath);
}
}
DDL
таблицы для приведённой выше AR-модели:
DROP TABLE IF EXISTS `tbl_item`;
CREATE TABLE `tbl_item` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`title` varchar(250) NOT NULL DEFAULT '',
`document` varchar(250) NOT NULL DEFAULT '',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Код для работы с файлом при необходимости можно вынести в поведение (CActiveRecordBehavior).
Ниже представлен рабочий пример такого поведения:
/**
* @property string $savePath путь к директории, в которой сохраняем файлы
*/
class UploadableFileBehavior extends CActiveRecordBehavior{
/**
* @var string название атрибута, хранящего в себе имя файла и файл
*/
public $attributeName='document';
/**
* @var string алиас директории, куда будем сохранять файлы
*/
public $savePathAlias='webroot.media';
/**
* @var array сценарии валидации к которым будут добавлены правила валидации
* загрузки файлов
*/
public $scenarios=array('insert','update');
/**
* @var string типы файлов, которые можно загружать (нужно для валидации)
*/
public $fileTypes='doc,docx,xls,xlsx,odt,pdf';
/**
* Шорткат для Yii::getPathOfAlias($this->savePathAlias).DIRECTORY_SEPARATOR.
* Возвращает путь к директории, в которой будут сохраняться файлы.
* @return string путь к директории, в которой сохраняем файлы
*/
public function getSavePath(){
return Yii::getPathOfAlias($this->savePathAlias).DIRECTORY_SEPARATOR;
}
public function attach($owner){
parent::attach($owner);
if(in_array($owner->scenario,$this->scenarios)){
// добавляем валидатор файла
$fileValidator=CValidator::createValidator('file',$owner,$this->attributeName,
array('types'=>$this->fileTypes,'allowEmpty'=>true));
$owner->validatorList->add($fileValidator);
}
}
// имейте ввиду, что методы-обработчики событий в поведениях должны иметь
// public-доступ начиная с 1.1.13RC
public function beforeSave($event){
if(in_array($this->owner->scenario,$this->scenarios) &&
($file=CUploadedFile::getInstance($this->owner,$this->attributeName))){
$this->deleteFile(); // старый файл удалим, потому что загружаем новый
$this->owner->setAttribute($this->attributeName,$file->name);
$file->saveAs($this->savePath.$file->name);
}
return true;
}
// имейте ввиду, что методы-обработчики событий в поведениях должны иметь
// public-доступ начиная с 1.1.13RC
public function beforeDelete($event){
$this->deleteFile(); // удалили модель? удаляем и файл, связанный с ней
}
public function deleteFile(){
$filePath=$this->savePath.$this->owner->getAttribute($this->attributeName);
if(@is_file($filePath))
@unlink($filePath);
}
}
В поведении мы делаем тоже самое, что и в модели ранее. Код для работы с файлом мы просто вынесли
из модели в поведение, тем самым увеличили повторную используемость кода.
Код модели теперь совсем простой:
/**
* @property integer $id
* @property string $title
* @property string $document
*/
class Item extends CActiveRecord{
public static function model($className=__CLASS__){
return parent::model($className);
}
public function tableName(){
return '{{item}}';
}
public function rules(){
return array(
array('title','required','on'=>'insert,update'),
// после генерации модели при помощи Gii нужно убрать валидацию
// у атрибута $document — если этого не сделать, то оно будет
// препятствовать правильной работе валидации загружаемого файла
);
}
public function behaviors(){
return array(
// наше поведение для работы с файлом
'uploadableFile'=>array(
'class'=>'application.components.UploadableFileBehavior',
// конфигурируем нужные свойства класса UploadableFileBehavior
// ...
),
);
}
}
Стоит иметь ввиду, что данный рецепт является всего лишь примером. Так, например, мы не учли
ситуацию, когда две разные модели могут иметь два файла с одним и тем-же именем.
Автор
: resurtmДополнения
: mc-bearОригинальный рецепт
: как загрузить файл используя модельАнглийский рецепт
: how to upload a file using a modelОбсуждение и комментарии
: тема на форуме