Kohana file upload — Загрузка файлов Kohana — Часть 2

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

Скачать исходные коды урока

23 комментария

  1. Alina:

    Мне нужно сохранять путь к загруженной картинке в БД, но картинки сохраняются под произвольным именем, не таким, каким оно было до загрузки. Как вытащить конечное имя картинки?

    • radik:

      Здравствуйте, если у вас есть путь к файлу на диске то имя файла можно получить так pathinfo($file_path, PATHINFO_FILENAME).

  2. Денис:

    «…данными должна управлять модель, а не контроллер!…»
    хотелось бы узнать что делать если данные беруться из нескольких моделей? В какой модели это описывать?

    • radik:

      Здравствуйте, я думаю вы описываете ситуацию в случае которой вы хотите сохранить модель с зависимостями. Такое возможно сделать. Достаточно добавить к основной модели соответствующий метод.

      • Денис:

        Я новичек в Kohana, поэтому не сталкивался с моделями, которые данными управляют, всегда делал это через паттерн «Репозиторий»

        • radik:

          На сколько я знаю в классическом представлении паттерн «Репозиторий» принято использовать в совокупности с паттерном «Data Mapper» как это делается к примеру в Doctrine 2. В Kohana 3 ORM в основу заложен паттерн «Active Record» поэтому я просто добавляю методы к модели и в методе уже описываю всю логику обработки данных. Чисто теоретически можно добавить «Репоизитории», но вот вопрос стоит ли?

  3. hmm:

    хм, раз данными управляет модель, че orm делает в контроллере?

    • radik:

      ORM::factory это фабричный метод модели, то что он размещается в контроллере это нормально т.к. по идеологии MVC контроллер управляет данными, а так же связывает данные и представление.

      • hmm:

        эм, по тойже идеологии работа с бд должна осуществляться в моделе. А orm — предоставляет работу с бд.

        Почему бы не спрятать orm в модель и вызывать методы вот так Model::factory(‘Files’)->name_method() ?

        • radik:

          Вы наверное не читали документацию Kohana. ORM это базовый класс для всех моделей, поэтому через него они и создаются, то что вы написали можно записать в таком виде ORM::factory(‘model’)->some_method(), это будет правильно с точки зрения Kohana.

      • hmm:

        совсем запутался … как тут происходит валидация данных. Ведь /files/upload controller то и делает, что записывает данные из POST и FILES.
        А в try {} catch(){} заключен метод save() для того чтобы выловить ошибку валидации.
        Верно?
        Тогда как вызывается то, что заставляет все эти данные проверять?
        Дефолтный метод rules() описывает общую валидацию для всей модели? И вызывается сам каждый раз когда идет обращение к через ORM::factory(‘File’)… ?

        Насчет пред. ответа, покопался лучше, вы правы:
        http://kohanaframework.org/3.3/guide/orm/examples/simple

        Походу я стремясь делать правильно, все делал не правильно. Мда. Ужс.

        • radik:

          Принцип работы валидации примерно следующий:
          1. Устанавливаются значения модели
          2. После установки значений запускаются фильтры из filters
          3. При вызове метода save запускаются правила валидации из rules

          По этому метод save и обернут в блок try catch для того, что бы отловить ошибку валидации данных.

      • hmm:

        Хорошо, теперь валидация вся прописана в Модели. Использую ORM в контроллере для CRUD. Какая еще логика должна быть описана в Модели?
        Например, я раньше там писал весь CRUD с помощью ORM + подготовка данных к определенному формату и вызывал все это дело через Model::factory(‘Model_Name’)->name_method() где получив массив в контроллере, кидал в вид.

        Эх, жаль нет целикового примера «проектом» на кохане, который решает тестовые задачи, что-то типа демонстрации, как делать правильно …

        • radik:

          Какие методы должны быть в модели все зависит от вас, в рамках MVC есть несколько подходов к проектированию:
          1. «толстая» модель и «тонкий» контроллер — в данном походе подразумевается, что все запросы на получение и обновление информации размещаются в модели путем создания соответствующих методов, а контроллер вызывает эти методы, что бы получить данные или изменить из как ему требуется.
          2. «тонкая» модель и «толстый» контроллер — в данном подходе подразумевается, что контроллер сам с помощью модели строит запросы какие ему надо, а модель только предоставляет более удобный интерфейс для работы с БД.

          Какой из этих подходов вам больше нравится тот и используйте, как говорится «на вкус и цвет…».

  4. Григорий:

    Здравствуйте.
    Сначала хочу сказать спасибо за статью, было интересно читать и пробовать.
    Но обнаружил, что русские литеры сохраняются в имени файлов кракозябрами, а при попытки загрузить/просмотреть их возникает резонная 404. не подскажите как заставить кохану сохранять файлы с тем именем и кодировкой которой они были загружены

    • radik:

      Здравствуйте, тут дело не в самой так каковой Kohanа, а в кодировке файловой системы. По сути принцип следующий:

      1. Нужно узнать в какой кодировке у вас на сервере сохраняются имена файлов
      2. При создании файла нужно конвертировать кодировку из utf8 в кодировку файловой системы.

      Примерно как то так, но думаю не стоит с этим заморачиваться, если это не очень критично и лучше использовать случайные имена для файлов, а информацию о реальных именах хранить в БД, если это необходимо.

      • Григорий:

        Ясно.
        Пожалуй идея о хранении «человеческого» имени файла как ни с чем не связной записи бд. весьма здравая.
        Помимо возможности спокойно загружать файлы с русскими именами будет проще изменять им имена потом (переименование самого файла не потребуется).
        Еще раз спасибо.

  5. Вася:

    Автор кажется издевается, сохранение файла в модели? Что за бред! Люди, не делайте так.

  6. Дмитрий:

    «…данными должна управлять модель, а не контроллер!…»

    Полностью с этим согласен, но в этом есть свой недостаток именно в случае загрузки файла. Точнее, если после загрузки о нем не просто забываешь, а еще можешь редактировать описание, название и прочую сопроводительную чушь.

  7. Дмитрий (другой):

    Присоединяюсь к вышесказаному! Хорошо, что еще есть блоги, где пишут чтото новое, а не копипастят доки.

  8. Дмитрий:

    Спасибо за статью, очень познавательно, хотелось бы лицезреть ajax загрузку…

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *