Приветствую всех моих читателей, я решил не откладывать в долгий ящик то, что обещал вам вчера поэтому продолжим дальше разбирать тему загрузки файлов в 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 загрузку…