
Приветствую всех моих читателей, я решил не откладывать в долгий ящик то, что обещал вам вчера поэтому продолжим дальше разбирать тему загрузки файлов в Kohana.
Сегодня мы рассмотрим загрузку файлов с последующим сохранением имени файла и комментария к нему в БД, и реализуем небольшое файловое хранилище которое будет позволять нам загружать, просматривать список загруженных файлов, а так же скачивать следующие файловые форматы: jpg, jpeg, png, gif, zip, pdf, doc, docx, xls (думаю этого для примера будет достаточно).
1. Активация необходимых модулей
Для того, что бы реализовать то, что мы задумали нам понадобятся модуль database и orm которые нужно активировать, для этого нам нужно отредактировать параметры вызова метода Kohana::modules в файле application/bootstrap.php, то что получилось у меня ниже:
/** * Enable modules. Modules are referenced by a relative or absolute path. */ Kohana::modules(array( // 'auth' => MODPATH.'auth', // Basic authentication // 'cache' => MODPATH.'cache', // Caching with multiple backends // 'codebench' => MODPATH.'codebench', // Benchmarking tool 'database' => MODPATH.'database', // Database access // 'image' => MODPATH.'image', // Image manipulation 'orm' => MODPATH.'orm', // Object Relationship Mapping // 'unittest' => MODPATH.'unittest', // Unit testing // 'userguide' => MODPATH.'userguide', // User guide and API documentation ));
2. Настройка соединения с БД
Для начала настройки соединения с БД скопируйте файл modules/database/config/database.php в каталог application/config/ и отредактируйте секцию с именем default вот что получилось у меня:
<?php defined('SYSPATH') or die('No direct access allowed.');
return array
(
'default' => array
(
'type' => 'mysql',
'connection' => array(
/**
* The following options are available for MySQL:
*
* string hostname server hostname, or socket
* string database database name
* string username database username
* string password database password
* boolean persistent use persistent connections?
* array variables system variables as "key => value" pairs
*
* Ports and sockets may be appended to the hostname.
*/
'hostname' => 'localhost',
'database' => 'dev_file_upload',
'username' => 'root',
'password' => FALSE,
'persistent' => FALSE,
),
'table_prefix' => '',
'charset' => 'utf8',
'caching' => FALSE,
'profiling' => TRUE,
),
);
3. Создание таблицы в БД
CREATE TABLE IF NOT EXISTS `files` ( `id` int(11) NOT NULL AUTO_INCREMENT, `file` text NOT NULL, `type` varchar(4) NOT NULL, `size` bigint(20) unsigned NOT NULL, `description` text, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;
4. Создание модели
Заранее прошу рассмотреть внимательно код модели т.к. всю основную работу по валидации данных и загрузке файла выполняет именно она, я как и обычно добавил комментарии практически к каждой строчки модели, вот что у меня получилось:
<?php defined('SYSPATH') or die('No direct script access.');
/**
* Model file class
*
* @author Novichkov Sergey(Radik) <novichkovsergey@yandex.ru>
* @copyright Copyrights (c) 2012 Novichkov Sergey
*
* @property integer $id
* @property string $file
* @property string $type
* @property integer $size
* @property string $description
*/
class Model_File extends ORM {
/**
* Table columns
*
* Field name => Label
*
* @var array
*/
protected $_table_columns = array(
'id' => 'id',
'file' => 'file',
'type' => 'type',
'size' => 'size',
'description' => 'description',
);
/**
* Label definitions for validation
*
* @return array
*/
public function labels()
{
return array(
'file' => 'File',
'description' => 'Description',
);
}
/**
* Filter definitions for validation
*
* @return array
*/
public function filters()
{
return array(
TRUE => NULL,
'description', array(
array('trim'),
),
);
}
/**
* Rule definitions for validation
*
* @return array
*/
public function rules()
{
return array(
'file' => array(
array('Upload::valid'),
array('Upload::not_empty'),
array('Upload::type', array(':value', array('jpg', 'jpeg', 'png', 'gif', 'zip', 'pdf', 'doc', 'docx', 'xls'))),
array(array($this, 'file_save'), array(':value'))
),
);
}
/**
* Uploads directory
*
* @return string
*/
private function uploads_dir()
{
return DOCROOT . 'uploads' . DIRECTORY_SEPARATOR;
}
/**
* Upload file in upload directory and setup valid filename
*
* @param array $file
*
* @return boolean
*/
public function file_save($file)
{
// upload file
$uploaded = Upload::save($file, $file['name'], $this->uploads_dir());
// if uploaded set file name to save to database
if ($uploaded)
{
// set file name
$this->set('file', $file['name']);
// set file type
$this->set('type', strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)));
// set file size
$this->set('size', $file['size']);
}
// return result
return $uploaded;
}
} // end Model File class
Особого внимания от вас в коде модели требует нестандартное, наше собственное правило валидации:
array(array($this, 'file_save'), array(':value'))
Разберем более детально, что оно делает: первоначально запись такой конструкции правила сообщает объекту валидации что ему в процессе проверки правила необходимо вызвать метод file_save экземпляра модели Model_File и в качестве первого параметра передать в метод значение свойства file, а в качестве результата получил логическое значение сообщающие о результате валидации (TRUE — пройдена, FALSE — провалена). В свою очередь метод file_save попытается скопировать загруженный файл из временного каталога системы в каталог загрузок и в случае успеха установит необходимые значения объекта, вот в этом то и заключается вся магия :)
Вы наверное спросите, зачем я сделал именно так, а не оставил весь код обработки загрузки файла в контроллере?
Я сделал это исключительно по эстетическим соображениям, т.к. мы с вами в процессе работы с Kohana должны придерживаться идеологии MVC, а файл который мы загружаем это ни что иное как данные, то если память мне не изменяет данными должна управлять модель, а не контроллер!
5. Создание вида
<?php defined('SYSPATH') or die('No direct script access.');
/**
* @var Database_Result $files
* @var array $errors
* @var string $message
*
* @author Novichkov Sergey(Radik) <novichkovsergey@yandex.ru>
* @copyright Copyrights (c) 2012 Novichkov Sergey
*/
?><!DOCTYPE html>
<html>
<head>
<title>Files</title>
<style type="text/css">
*{ margin: 0; padding: 0; }
html, body{ width: 100%; height: 100%; }
a:hover{ text-decoration: none; }
#wrap{ margin: 0 auto; padding-top: 20px; width: 960px; }
#wrap h2{ margin-bottom: 15px; }
#wrap table{ width: 100%; border-collapse: collapse; margin-bottom: 20px; }
#wrap td, #wrap th{ padding: 5px; border: 1px solid #000; }
#wrap th{ background: #000; color: #fff; }
#wrap .type{ text-align: center; width: 1%; }
#wrap .type, #wrap .name, #wrap .size, { white-space: nowrap; }
#wrap textarea{ width: 100%; height: 100px; resize: vertical; }
#wrap .message{ padding: 5px; border: 3px solid #00f; color: #00f; margin-bottom: 20px; }
#wrap .error{ padding: 5px; border: 3px solid #f00; color: #f00; margin-bottom: 20px; }
#wrap .row, #wrap label{ display: block; margin-bottom: 5px; }
#wrap .controls{ text-align: right; }
</style>
</head>
<body>
<div id="wrap">
<h2>Files</h2>
<table id="files">
<tr>
<th>Type</th>
<th>Name</th>
<th>Size</th>
<th>Description</th>
</tr>
<?php if ($files->count() === 0) : ?>
<tr>
<td colspan="4">Uploaded files not found</td>
</tr>
<?php else : ?>
<?php foreach ($files as $file) : /** @var Model_File $file **/ ?>
<tr>
<td class="type"><img src="<?php echo URL::base('http') ?>public/icons/16px/<?php echo $file->type ?>.png"></td>
<td class="name"><a href="<?php echo URL::base('http') ?>uploads/<?php echo $file->file ?>"><?php echo $file->file ?></a></td>
<td class="size"><?php echo Text::bytes($file->size) ?></td>
<td class="desc"><?php echo HTML::chars($file->description) ?></td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</table>
<h2>Upload</h2>
<?php if ($message) : ?>
<div class="message"><?php echo HTML::chars($message) ?></div>
<?php endif; ?>
<?php foreach ($errors as $error) : ?>
<div class="error"><?php echo HTML::chars($error) ?></div>
<?php endforeach; ?>
<form action="<?php echo Route::url('default', array('controller' => 'files', 'action' => 'upload')) ?>" method="post" enctype="multipart/form-data">
<label for="file_control">File</label>
<div class="row"><input type="file" name="file" id="file_control"></div>
<label for="description_control">Description</label>
<div class="row"><textarea rows="10" cols="10" name="description" id="description_control"></textarea></div>
<div class="controls"><input type="submit" value="Upload"></div>
</form>
</div>
</body>
</html>
6. Создание контроллера
<?php defined('SYSPATH') or die('No direct script access.');
/**
* Files controller class
*
* @author Novichkov Sergey(Radik) <novichkovsergey@yandex.ru>
* @copyright Copyrights (c) 2012 Novichkov Sergey
*/
class Controller_Files extends Controller_Template {
/**
* @var View
*/
public $template = 'files';
/**
* View all tree action
*/
public function action_view()
{
// set values to template
$this->template->set(array(
// files list
'files' => ORM::factory('File')->find_all(),
// errors from user session
'errors' => Session::instance()->get_once('errors', array()),
// message from user session
'message' => Session::instance()->get_once('message'),
));
}
/**
* Upload file action
*/
public function action_upload()
{
// check request method
if ($this->request->method() === Request::POST)
{
$file = ORM::factory('File')->values(Arr::merge($_FILES, $this->request->post()));
// try upload and save file and file info
try
{
// save
$file->save();
// set user message
Session::instance()->set('message', 'File is successfully uploaded');
}
catch (ORM_Validation_Exception $e)
{
// prepare errors
$errors = $e->errors('upload');
$errors = Arr::merge($errors, Arr::get($errors, '_external', array()));
// remove external errors
unset($errors['_external']);
// set user errors
Session::instance()->set('errors', $errors);
}
}
// redirect to home page
$this->request->redirect('/');
}
} // End Controller Files
7 Сообщения валидации
И снова по тем же самым соображениям, что и в первой части статьи немного рассширим список ранее созданных сообщений валидации сообщениями для новых правил:
<?php defined('SYSPATH') or die('No direct script access.');
return array(
'Upload::valid' => ':field data is invalid',
'Upload::not_empty' => ':field not selected',
'Upload::type' => ':field invalid file format',
'Upload::image' => ':field not valid image file',
);
8 Результат

Ну вот собственно и все, на скриншоте вы можете увидеть то, что у меня получилось :) На этом я думаю цикл статей можно закончить и я оставлю не освященным (вам как домашнее задание) только один момент в загрузке файлов: ajax загрузка нескольких файлов, но если вам дорогие мои читатели это будет интересно, то я с радостью могу вам рассказать как такое можно сделать….
Мне нужно сохранять путь к загруженной картинке в БД, но картинки сохраняются под произвольным именем, не таким, каким оно было до загрузки. Как вытащить конечное имя картинки?
Здравствуйте, если у вас есть путь к файлу на диске то имя файла можно получить так pathinfo($file_path, PATHINFO_FILENAME).
«…данными должна управлять модель, а не контроллер!…»
хотелось бы узнать что делать если данные беруться из нескольких моделей? В какой модели это описывать?
Здравствуйте, я думаю вы описываете ситуацию в случае которой вы хотите сохранить модель с зависимостями. Такое возможно сделать. Достаточно добавить к основной модели соответствующий метод.
Я новичек в Kohana, поэтому не сталкивался с моделями, которые данными управляют, всегда делал это через паттерн «Репозиторий»
На сколько я знаю в классическом представлении паттерн «Репозиторий» принято использовать в совокупности с паттерном «Data Mapper» как это делается к примеру в Doctrine 2. В Kohana 3 ORM в основу заложен паттерн «Active Record» поэтому я просто добавляю методы к модели и в методе уже описываю всю логику обработки данных. Чисто теоретически можно добавить «Репоизитории», но вот вопрос стоит ли?
хм, раз данными управляет модель, че orm делает в контроллере?
ORM::factory это фабричный метод модели, то что он размещается в контроллере это нормально т.к. по идеологии MVC контроллер управляет данными, а так же связывает данные и представление.
эм, по тойже идеологии работа с бд должна осуществляться в моделе. А orm — предоставляет работу с бд.
Почему бы не спрятать orm в модель и вызывать методы вот так Model::factory(‘Files’)->name_method() ?
Вы наверное не читали документацию Kohana. ORM это базовый класс для всех моделей, поэтому через него они и создаются, то что вы написали можно записать в таком виде ORM::factory(‘model’)->some_method(), это будет правильно с точки зрения Kohana.
совсем запутался … как тут происходит валидация данных. Ведь /files/upload controller то и делает, что записывает данные из POST и FILES.
А в try {} catch(){} заключен метод save() для того чтобы выловить ошибку валидации.
Верно?
Тогда как вызывается то, что заставляет все эти данные проверять?
Дефолтный метод rules() описывает общую валидацию для всей модели? И вызывается сам каждый раз когда идет обращение к через ORM::factory(‘File’)… ?
Насчет пред. ответа, покопался лучше, вы правы:
http://kohanaframework.org/3.3/guide/orm/examples/simple
Походу я стремясь делать правильно, все делал не правильно. Мда. Ужс.
Принцип работы валидации примерно следующий:
1. Устанавливаются значения модели
2. После установки значений запускаются фильтры из filters
3. При вызове метода save запускаются правила валидации из rules
По этому метод save и обернут в блок try catch для того, что бы отловить ошибку валидации данных.
Хорошо, теперь валидация вся прописана в Модели. Использую ORM в контроллере для CRUD. Какая еще логика должна быть описана в Модели?
Например, я раньше там писал весь CRUD с помощью ORM + подготовка данных к определенному формату и вызывал все это дело через Model::factory(‘Model_Name’)->name_method() где получив массив в контроллере, кидал в вид.
Эх, жаль нет целикового примера «проектом» на кохане, который решает тестовые задачи, что-то типа демонстрации, как делать правильно …
Какие методы должны быть в модели все зависит от вас, в рамках MVC есть несколько подходов к проектированию:
1. «толстая» модель и «тонкий» контроллер — в данном походе подразумевается, что все запросы на получение и обновление информации размещаются в модели путем создания соответствующих методов, а контроллер вызывает эти методы, что бы получить данные или изменить из как ему требуется.
2. «тонкая» модель и «толстый» контроллер — в данном подходе подразумевается, что контроллер сам с помощью модели строит запросы какие ему надо, а модель только предоставляет более удобный интерфейс для работы с БД.
Какой из этих подходов вам больше нравится тот и используйте, как говорится «на вкус и цвет…».
Большое спасибо за ответы!
Здравствуйте.
Сначала хочу сказать спасибо за статью, было интересно читать и пробовать.
Но обнаружил, что русские литеры сохраняются в имени файлов кракозябрами, а при попытки загрузить/просмотреть их возникает резонная 404. не подскажите как заставить кохану сохранять файлы с тем именем и кодировкой которой они были загружены
Здравствуйте, тут дело не в самой так каковой Kohanа, а в кодировке файловой системы. По сути принцип следующий:
1. Нужно узнать в какой кодировке у вас на сервере сохраняются имена файлов
2. При создании файла нужно конвертировать кодировку из utf8 в кодировку файловой системы.
Примерно как то так, но думаю не стоит с этим заморачиваться, если это не очень критично и лучше использовать случайные имена для файлов, а информацию о реальных именах хранить в БД, если это необходимо.
Ясно.
Пожалуй идея о хранении «человеческого» имени файла как ни с чем не связной записи бд. весьма здравая.
Помимо возможности спокойно загружать файлы с русскими именами будет проще изменять им имена потом (переименование самого файла не потребуется).
Еще раз спасибо.
Автор кажется издевается, сохранение файла в модели? Что за бред! Люди, не делайте так.
Ок, готов выслушать вашу точку зрения почему так не стоит делать?
Полностью с этим согласен, но в этом есть свой недостаток именно в случае загрузки файла. Точнее, если после загрузки о нем не просто забываешь, а еще можешь редактировать описание, название и прочую сопроводительную чушь.
Присоединяюсь к вышесказаному! Хорошо, что еще есть блоги, где пишут чтото новое, а не копипастят доки.
Спасибо за статью, очень познавательно, хотелось бы лицезреть ajax загрузку…