Понадобилось на одном простеньком внутреннем проекте на Yii добавить возможность прикреплять к сущностям файлы. Быстрое гугление не привело к результату (наверное, задача такая банальная, что никто не выкладывает :-) Что ж, рискну я.

Итак, суть поведения (behavior) следующая: описываем его у нужной модели, передаём путь до сохранения файлов и имя поля, которое укажем в форме модели у виджета CMultiFileUpload. Поведение будет типа CActiveRecordBehavior будет срабатывать на событие afterSave у модели.

Код поведения

<?php
 
/**
 * Created by olegpro.ru.
 * User: Oleg Maksimenko <oleg.39style@gmail.com>
 * Date: 21.08.2015. Time: 16:42
 */
class OMultiFileBehavior extends CActiveRecordBehavior
{
 
    /**
     * @var string
     */
    public $filesPath = '';
 
    /**
     * @var string
     */
    public $filesField = 'files';
 
    /**
     * @param CEvent $event
     */
    public function afterSave($event)
    {
        $files = CUploadedFile::getInstancesByName($this->filesField);
 
        if (isset($files) && is_array($files) && sizeof($files) > 0) {
 
            $dirFiles = $this->prepareFilesPath($event->sender);
 
            if (!is_dir($dirFiles)) {
                @mkdir($dirFiles, 0775, true);
            }
 
            if (is_dir($dirFiles)) {
 
                /** @var CUploadedFile $file */
                foreach ($files as $fileNum => $file) {
                    if ($file->saveAs(sprintf('%s/%s', $dirFiles, $file->name))) {
 
                    }
                }
            }
 
        }
 
    }
 
    /**
     * @return array
     */
    public function getAttachedFiles()
    {
        $files = array();
        $dirFiles = $this->prepareFilesPath($this->owner);
        if (is_dir($dirFiles) && $handle = opendir($dirFiles)) {
            $i = 1;
            while (false !== ($entry = readdir($handle))) {
                if ($entry != '.' && $entry != '..') {
                    $files[] = array(
                        'id' => $i++,
                        'name' => $entry,
                        'path' => str_replace(Yii::getPathOfAlias('webroot'), '', sprintf('%s/%s', $dirFiles, $entry))
                    );
                }
            }
            closedir($handle);
        }
 
        return $files;
    }
 
    /**
     * @param CActiveRecord $model
     * @return mixed
     */
    private function prepareFilesPath ($model){
        return str_replace(
            array(
                '{id}'
            ),
            array(
                $model->id
            ),
            Yii::getPathOfAlias('webroot') . $this->filesPath
        );
    }
 
}

«Прикрепляем» наше поведение OMultiFileBehavior к нужной модели.

<?php
 
// Model
class MyModel extends CActiveRecord
{
 
    public function behaviors()
    {
        return array(
            'OMultiFileBehavior' => array(
                'class' => 'OMultiFileBehavior',
                'filesPath' => '/files/software/{id}',
                'filesField' => 'files',
            )
        );
    }
}

Важно! В представление формы модели у модели надо добавить параметр enctype равный multipart/form-data. Это важно. Без этого CUploadedFile::getInstancesByName будет возвращать всегда пустой массив.

$form=$this->beginWidget('bootstrap.widgets.TbActiveForm',array(
    // ...
    'htmlOptions' => array('enctype'=>'multipart/form-data'), 
));

Так же нужно добавить вызов виджета CMultiFileUpload, которое выведет поле типа «файл» и будет показывать список уже прикрёпленных файлов в момент выбора.

// Form view
echo CHtml::label('Files', '');
$this->widget('CMultiFileUpload', array(
    'name' => 'files',
    'accept' => 'xml|html|pdf|jpeg|jpg|gif|png|doc|docx|xls|xlsx',
    'duplicate' => 'File is not existed!',
    'denied' => 'Incorrect file type',
    'htmlOptions'=>array(
        'class'=>'form-control',
    ),
));

В детальное представление нашей модели добавим вывод уже прикреплённых файлов. $model->attachedFiles съинициирует вызов OMultiFileBehavior->getAttachedFiles() у нужной модели.

// Detail view
if(isset($model->attachedFiles) && is_array($model->attachedFiles) && !empty($model->attachedFiles)){ ?>
    <br />
    <h3>Файлы</h3>
    <?php $this->widget('bootstrap.widgets.TbGridView',array(
        'dataProvider'=> new CArrayDataProvider($model->attachedFiles),
        'type'=>'striped bordered',
        'template' => '{items} {pager}',
        'enableSorting' => false,
        'enablePagination' => false,
        'hideHeader' => true,
        'columns'=>array(
            array(
                'class'=>'CLinkColumn',
                'labelExpression' => '$data["name"]',
                'urlExpression' => '$data["path"]',
                'linkHtmlOptions' => array('target' => '_blank')
            ),
        )
 
    ));
 
} 
?>

Ну вот и всё. Домашним заданием будет: добавить возможность удаления файлов, прикреплённых к модели.