Задача сводится к следующему. Нужен удобный механизм загрузки файлов и картинок на сервер (включая необходимые проверки), создание миниатюр для картинок и автоматическая генерация тегов img
и ссылок на скачивание файла. В расширениях ничего подходящего не нашлось. Наиболее близким по смыслу является расширение upload, но и оно не предоставляет ряда необходимых функций. Так что решил писать сам. Прямо перед публикацией статьи случайно увидел рецепт, в котором рассматривается похожая идея.
Примеры кода даны только для ознакомления, я вставлял их из работающего проекта, но мог что-нибудь перепутать. Если нужен работающий код, в конце статьи есть ссылка на проект. Итак, вперед!
Первая итерация. Дополняем стандартный функционал.
Что предлагает Yii? Во-первых, класс CUploadedFile, предоставляющий информацию о загруженном файле и позволяющий сохранять его на сервер. Во-вторых, валидатор CFileValidator, выполняющий проверку загруженного файла. Вот как официальная документация рекомендует загружать файлы:
// Модель class MyModel extends CActiveRecord { public $image; public function rules(){ return array( array('image', 'file', 'types'=>'jpg, gif, png'), ); } } // Контроллер class MyModelController extends CController { public function actionCreate(){ $modMyModel=new MyModel; if(isset($_POST['MyModel'])){ $modMyModel->attributes=$_POST['MyModel']; $modMyModel->id_image=CUploadedFile::getInstance($modMyModel,'image'); if($modMyModel->save()){ $modMyModel->id_image->saveAs('path/to/localFile'); // перенаправляем на страницу, где выводим сообщение об // успешной загрузке } } $this->render('create', array('model'=>$modMyModel)); } } // Форма <?php echo CHtml::form('','post',array('enctype'=>'multipart/form-data')); ?> ... <?php echo CHtml::activeFileField($modMyModel, 'image'); ?> ... <?php echo CHtml::endForm(); ?>
У такого подхода есть ряд недостатков:
- Во фреймворке нет специально выделенной папки для загрузки файлов
- Загрузку файлов приходится каждый раз описывать в контроллере
- Указанный подход не может быть перенесен на
actionUpdate()
, поскольку ожидает загрузки файла при каждом вызове. А с файлами было бы удобно работать как с обычными свойствами — загрузить при создании модели и при необходимости перезагрузить при ее изменении. - Нет рекомендаций относительно последующего обращения к файлу. Впрочем,
path/to/localFile
можно хранить в свойстве модели.
Разумеется, я говорю об этих недостатках только в контексте собственных проектов. И вот что хочу предложить.
Для начала определимся с местом для сохранения файлов. На мой взгляд, для хранения файлов лучше всего подойдет директория .../protected/data/files
. Вообще говоря, для файлов, создающихся в процессе работы, существует папка .../protected/runtime
, но по смыслу директория data
больше подходит для этих целей. Имя файла будем генерировать случайным образом (uniqid()
) и сохранять в свойстве модели $modMyModel->id_image
, в следующих абзацах расскажу как. Правда у такого подхода есть один подводный камень — директория data
закрыта для обращений из браузера. Как быть с этим — чуть позже. Забегая вперед, файлы для скачивания предлагаю выдавать динамически через readfile()
, а картинки (точнее, миниатюры картинок) — публиковать в папке assets
.
С папкой и именованием файлов разобрались. Теперь разберемся с валидацией и загрузкой. Начнем с формы. Сделаем так:
<?php echo $modMyModel->id_image ?> <?php echo CHtml::activeFileField($modMyModel, 'id_image_file'); ?>
Так мы будем видеть, загружен ли файл. А сам файл будет грузиться с именем id_image_file
. Вместо echo $modMyModel->id_image
можно будет вставить ссылку для скачивания файла или миниатюру картинки.
Теперь валидация. Идея такова: валидатор должен проверить, есть ли в $_FILES
загруженный файл с именем id_image_file
. Если есть, то создать объект CUploadedFile
и записать его в $modMyModel->id_image
. После чего выполнить валидацию $modMyModel->id_image
стандартным способом. Для этого создадим свой валидатор DFileValidator
, унаследованный от CFileValidator
. И сразу еще один — DImageValidator
, унаследованный от DFileValidator
, в котором укажем типы файлов по умолчанию для картинок.
И, наконец, загрузка файла и сохранение модели. После валидации загруженный файл будет находиться в $modMyModel->id_image
, причем в виде CUploadedFile
. Для того чтобы загрузка не была привязана к конкретному свойству, нужно перед сохранением модели проверить, являются ли какие-либо ее свойства объектами класса CUploadedFile
, и если являются — загрузить их и сохранить адреса. Теперь модель будет выглядеть так:
class MyModel extends DActiveRecord { public $id_image; public function rules(){ return array( array('id_image', 'DImageValidator'), ); } public function beforeSave() { foreach ($this->attributes as $key => $attribute) if ($attribute instanceof CUploadedFile) { $strSource = uniqid(); if ($attribute->saveAs(Yii::getPathOfAlias('application.data.files') . '/' . $strSource)) $this->$key = $strSource; } return parent::beforeSave(); } }
Свойство image было заменено свойством id_image сознательно. Дальше будет понятно почему.
Подведем промежуточный итог
- Все файлы сохраняются в папке
.../protected/data/files
со случайными именами. - Загузка файла выполняется в модели, перед сохранением в базе данных.
- Чтобы пометить свойство как файл, нужно:
- В
rules()
модели назначить этому свойству валидаторDFileValidator
. - В форме переименовать инпут для этого свойства, дописав к нему
'_file'
.
- В
- Методы
actionCreate()
иactionUpdate()
контроллера можно оставить без изменений.
Вторая итерация. Подключаем базу данных.
Мы разобрались, как загрузить файл. Но пока непонятно как к нему обращаться. Что писать в параметре src
тэга img
? Как отдавать файл для скачивания? На мой взгляд, для работы с файлами было бы удобно использовать функционал модели Yii. В самом деле, если каждому загруженному файлу будет соответствовать модель, все низкоуровневые операции, включая загрузку, можно будет поручить ей. А в свойстве $modMyModel->id_image
хранить первичный ключ этой модели (теперь понятна суть имени этого свойства). Тогда для $modMyModel
можно будет определить соответствующие связи и писать, например, так:
// В MyModel: public function relations() { return array( 'image' => array(self::BELONGS_TO, 'DImage', 'id_image'), ); } // Где угодно: $modMyModel = new MyModel; echo $modMyModel->id_image->image($htmlOptions); // Подготовит картинку к публикации и выведет тэг img echo $modMyModel->file->downloadLink(); // Вернет ссылку для скачивания файла
Кроме того, при использовании модели сам собой решается вопрос хранения оригинального имени файла (которое теряется при сохранении).
Поехали.
Создадим таблицу tbl_files
с полями id, source, name
. Определим модель DFile
, связанную с этой таблицей. В ней определим статический метод upload
:
class DFile extends DActiveRecord { public $uploadPath; // Путь к папке загрузки public function init() { $this->uploadPath = Yii::getPathOfAlias('application.data.files'); } public static function upload($objFile) { $modFile = new DFile; $modFile->name = $objFile->name; $modFile->source = uniqid(); if ($objFile->saveAs($modFile->uploadPath . '/' . $modFile->source)) if ($modFile->save()) return $modFile; return null; } }
И сразу создадим пустой класс DImage extends DFile
. Он понадобится нам позже.
Теперь немного изменим нашу модель. Определим обещанную связь с картинкой и немного подправим метод beforeSave()
:
class MyModel extends DActiveRecord { public $id_image; public function rules(){ return array( array('id_image', 'DImageValidator', 'allowEmpty' => true), ); } public function relations() { return array( 'image' => array(self::BELONGS_TO, 'DImage', 'id_image'), ); } public function beforeSave() { foreach ($this->attributes as $key => $attribute) if ($attribute instanceof CUploadedFile) { $modFile = DFile::upload($attribute); // Загрузку отдали DFile $this->$key = $modFile->id; } return parent::beforeSave(); } }
В объекте $modMyModel
модели можно обращаться к файлу через $modMyModel->image
. Как это выгодно использовать — читайте дальше.
Третья итерация. Обращения к загруженным файлам.
До этого момента мы почти не разделяли файлы и картинки. В самом деле, их загрузка выполняется абсолютно идентично. Единственная разница — проверки перед загрузкой. Но с этим отлично справятся валидаторы DFileValidator
и DImageValidator
, в которых можно указать все необходимые правила.
В отличие от загрузки, обращения к загруженным файлам и картинкам осуществляются по-разному. Файлы грузятся чтобы их потом скачивать, а картинки — чтобы их смотреть. Начнем с файлов.
Работа с файлами
Повторюсь, файлы загружаются для того, чтобы их скачивать. При этом часто требуется проверка прав доступа. Всего нужно решить две задачи, а именно — предоставление ссылки на скачивание и, собственно, выдача файла для скачивания.
Генерацию ссылки удобно делать в DFile
. Примерно так:
class DFile extends DActiveRecord { public function downloadLink($htmlOptions = array()) { return CHtml::link($this->name, array('/files/file/download', 'id' => $this->id), $htmlOptions); } }
Ссылка указывает на контроллер FileController
. Определим его:
class FileController extends DcController { public function actionDownload($id) { $modFile = $this->loadModel($id); header("Content-Type: application/force-download"); header("Content-Type: application/octet-stream"); header("Content-Type: application/download"); header("Content-Disposition: attachment; filename=" . $modFile->name); header("Content-Transfer-Encoding: binary "); readfile($modFile->uploadPath . '/' . $modFile->source); } public function loadModel($id) { $modFile = DFile::model()->findByPk($id); if($modFile === null) throw new CHttpException(404,'The requested page does not exist.'); return $modFile; } }
Думаю, здесь все понятно. Метод actionDownload()
не делает никаких дополнительных проверок, но их вполне можно включить при необходимости. В модели теперь, определив соответствующую связь, можно писать $modMyModel->file->downloadLink()
. Конечно, такой подход будет менее производителен, чем выдача прямых ссылок на файлы. Если производительность является критичной, можно заказчивать файлы не в защищенную директорию data
, а в другую (открытую) директорию, и выдавать прямые ссылки.
Работа с картинками
С картинками ситуация немного сложнее. Картинки требуют создания миниатюр. Кроме того, с картинками мы уж точно не можем позволить себе выдавать динамические ссылки. К счастью, Yii предоставляет удобный механизм публикации ресурсов, который можно использовать в наших целях. Идея такова: миниатюры будем создавать и публиковать как ресурсы при генерации ссылки на картинку. Тут, правда, есть пара неприятностей. Во-первых, если нужен доступ к исходной картинке, ее тоже придется копировать в папку assets
. Во-вторых, публикацию не получится осуществить стандартными средствами Yii. Дело в том, что для каждого опубликованного файла Yii создаст собственную папку, что будет перебором. Да и создание миниатюр сразу в папку assets
стандартными средствами сделать не получится.
Первая проблема может быть не актуальна, если картинок закачивается не очень много и доступ к исходной картинке не требуется. Исходные картинки хранятся в хорошем разрешении, их можно скачать используя описанный выше механизм, а для вывода на экран используются только миниатюры. Если же такая проблема имеет место, то, как вариант, можно не копировать исходную картинку в папку assets
, а создать ссылку (стандартный механизм публикации в Yii предлагает публикацию с созданием ссылок).
Что касается второй проблемы, придется писать публикацию самостоятельно. Впрочем, не так уж много писать…
Итак, поехали. Класс DImage
, унаследованный от DFile
у нас уже есть. Опишем создание миниатюр и публикацию:
class DImage extends DFile { public $assetsPath; // Путь к папке с ресурсами public $assetsUrl; // URL папки с ресурсами public $thumbs = array( 'min' => array('width' => 150, 'height' => 150), 'mid' => array('width' => 250), 'big' => array('width' => 600), ); // Определим настройки public function init() { $this->assetsUrl = Yii::app()->assetManager->baseUrl . '/files'; $this->assetsPath = Yii::app()->assetManager->basePath . '/files'; if (!is_dir($this->assetsPath)) mkdir($this->assetsPath); } // Все миниатюры должны находиться в $this->assetsPath public function getIsPublished() { foreach ($this->thumbs as $kThumb => $vThumb) if (!is_file($this->assetsPath . '/' . $this->source . '_' . $kThumb)) return false; return true; } // Публикация миниатюр public function publish() { if (!$this->isPublished) foreach ($this->thumbs as $kThumb => $vThumb) $this->createThumb($this->uploadPath . '/' . $this->source, $this->assetsPath . '/' . $this->source . '_' . $kThumb, $kThumb); return $this->assetsUrl . '/' . $this->source; } // Создание миниатюр function createThumb($strSrcFile, $strDstFile, $strThumb) { // Создает миниатюру картинки $strSrcFile, сохраняет в $strDstFile } }
Для публикации миниатюр предлагаю создать в папке assets
подпапку files
. Учитывая то, что папку assets
рекомендуется периодически чистить, существование папки assets/files
необходимо каждый раз проверять. И создавать если нужно. Имя миниатюры равно имени изображения, дополненному идентификатором миниатюры. Изображение считается опубликованным, если все миниатюры находятся на своих местах. Проверять совпадение даты исходного и опубликованного файлов не имеет смысла, поскольку загруженный файл не может изменяться. Функция publish()
возвращает URL опубликованной картинки (правда, без указания миниатюры), что не противоречит идее публикации ресурсов в Yii.
И, наконец, рассмотрим обращения к загруженной картинке. Дополним класс DImage
методом image()
:
public function image($strThumb = 'min', $htmlOptions = array()) { return CHtml::image($this->publish() . '_' . $strThumb, $this->name, $htmlOptions); }
Теперь, по аналогии с файлами, в модели можно писать $modMyModel->image->image()
. Кстати, если размер миниатюр вдруг необходимо изменить или добавить новый (у меня такое как-то раз случилось), а все файлы уже закачаны, достаточно будет поменять размер в настройках и очистить папку assets.
Последние штрихи
Все работает. Картинки загружаются, выводятся. Файлы закачиваются и скачиваются. Осталось немного причесать код. Например, метод beforeSave() можно вынести из класса MyModel в класс DActiveRecord, от которого, как Вы успели заметить, наследуются все модели. Кроме этого, отображение инпутов для файлов и картинок можно перенести в класс DActiveForm extends CActiveForm
. Хранение настроек можно поручить модулю files
.
Ну и, по хорошей традиции, ссылка на скачивание работающего проекта. Выкладывать отдельные файлы оказалось проблематично из-за большого количества зависимостей, поэтому выкладываю проект целиком. Дамп БД лежит в protected/data/dump.sql. Из настроек — указать путь к Yii, прописать доступ к БД. Базовые классы и валидаторы лежат в папке protected/components, все что касается файлов — в модуле files.
Заключение
Итак, вот что мы имеем на выходе:
- Централизованное управление загрузкой и хранением файлов
- Удобный интерфейс для создания в моделях свойств-файлов
- Высокоуровневую генерацию ссылок на скачивание файлов и тэгов IMG
Идею можно развить. Например, практически все WYCIWYG — редакторы предлагают интерфейс для загрузки файлов и изображений. Для этого требуется лишь указать адрес загрузки. Обработчик загрузки можно включить в контроллер FileController
. Но как тогда публиковать миниатюры?
Или еще, можно использовать предложения, описанные в статьях Безопасная загрузка изображений на сервер. Часть первая и Безопасная загрузка изображений на сервер. Часть вторая. Можно дополнить упомянутое выше расширение upload. Можно перенести функционал в поведения.
Одним словом, считать предложенное решение готовым пока рано. Но если описанная идея окажется полезной, готов довести работу до конца и опубликовать соответствующее расширение. Спасибо всем, кто дочитал!
ссылка на оригинал статьи http://habrahabr.ru/post/156293/
Добавить комментарий