Jelly — Добавляем тип поля «Изображение»

В дополнение к отличной статье о Jelly я решил рассказать о реализации типа поля «Image».

На самом деле я не изобретаю тут абсолютно ничего нового, я просто адаптировал оригинальную библиотеку от нестабильной версии Jelly к текущей, самой популярной, но уже не развивающейся версии.

Перед созданием нового типа, нужно немного изменить класс Field_File в файле /modules/jelly/classes/field/file.php

Добавим туда метод set(), перекрывающий родительский метод. Без него, мы не смогли бы сохранить файл.

public function set($value)
{
	return (array) $value;
}


А теперь создадим новый класс для работы с изображениями.
Он должен располагаться по адресу: /modules/jelly/classes/field/image.php или, если вы не хотите затрагивать чужие файлы, его путь должен быть таким: /application/classes/field/image.php

<?php defined('SYSPATH') or die('No direct script access.');
 
/**
 * Handles image uploads and optionally creates thumbnails of different sizes from the uploaded image
 * (as specified by the $thumbnails array).
 *
 * Each thumbnail is specified as an array with the following properties: path, resize, crop, and driver.
 * 
 *  * **path** is the only required property. It must point to a valid, writable directory.
 *  * **resize** is the arguments to pass to Image->resize(). See the documentation for that method for more info.
 *  * **crop** is the arguments to pass to Image->crop(). See the documentation for that method for more info.
 *
 * For example:
 *
 *     "thumbnails" => array (
 *         // 1st thumbnail
 *         array(
 *             'path'   => DOCROOT.'upload/images/my_thumbs/', // where to save the thumbnails
 *             'resize' => array(500, 500, Image::AUTO),       // width, height, resize type
 *             'crop'   => array(100, 100, NULL, NULL),        // width, height, offset_x, offset_y
 *             'driver' => 'ImageMagick',                      // NULL defaults to Image::$default_driver
 *         ),
 *         // 2nd thumbnail
 *         array(
 *             // ...
 *         ),
 *     )
 *
 * @see      Image::resize
 * @see      Image::crop
 * @author   Kelvin Luck
 * @package  Jelly
 */
class Field_Image extends Jelly_Field_File
{
	protected static $defaults = array(
		// The path to save to
		'path'   => NULL, 
		 // An array to pass to resize(). e.g. array($width, $height, Image::AUTO)
		'resize' => NULL,
		// An array to pass to crop(). e.g. array($width, $height, $offset_x, $offset_y)
		'crop'   => NULL,
		// The driver to use, defaults to Image::$default_driver
		'driver' => NULL,
	);
 
	/**
	 * @var  array  Specifications for all of the thumbnails that should be automatically generated when a new image is uploaded.
	 *  
	 */
	public $thumbnails = array();
 
	/**
	 * @var  array  Allowed file types
	 */
	public $types = array('jpg', 'gif', 'png', 'jpeg');
 
	/**
	 * Ensures there we have validation rules restricting file types to valid image filetypes and
	 * that the paths for any thumbnails exist and are writable
	 *
	 * @param  array  $options
	 */
	public function __construct($options = array())
	{
		parent::__construct($options);
 
		// Check that all thumbnail directories are writable...
		foreach ($this->thumbnails as $key => $thumbnail) 
		{
			// Merge defaults to prevent array access errors down the line
			$thumbnail += Field_Image::$defaults;
 
			// Ensure the path is normalized and writable
			$thumbnail['path'] = $this->_check_path($thumbnail['path']);
 
			// Merge back in
			$this->thumbnails[$key] = $thumbnail;
		}
	}
 
	/**
	 * Uploads a file if we have a valid upload
	 *
	 * @param   Jelly_Model $model
	 * @param   mixed  		$value
	 * @param   bool   		$loaded
	 * @return  string|NULL
	 */
	public function save($model, $value, $loaded)
	{
	 	$filename = parent::save($model, $value, $loaded);
		$image_option = @$_POST['imageoption' . $this->name];
 
		// Has our source file changed?
		if ($model->changed($this->name) && $image_option != 'leave')
		{
			$source   = $this->path.$filename;
 
			foreach ($this->thumbnails as $thumbnail)
			{
				$dest = $thumbnail['path'].$filename;
 
				// Delete old file if necessary
				$this->delete_old_file($this->path, $this->_original($model));
 
				if ($filename) {
					// Let the Image class do its thing
					$image = Image::factory($source, $thumbnail['driver'] ? $thumbnail['driver'] : Image::$default_driver);
 
					// This little bit of craziness allows us to call resize
					// and crop in the order specifed by the config array
					foreach ($thumbnail as $method => $args)
					{
						if (($method === 'resize' OR $method === 'crop') AND $args)
						{
							call_user_func_array(array($image, $method), $args);
						}
					}
 
					// Save
					$image->save($dest);
				}
			}
			return $filename;
		}
		return $this->_original($model);
	}
 
	/**
	 * Возвращает сохранённое в модели имя файла, до изменения
	 * @param Jelly_Model $model
	 * @return string
	 */
	public function _original(Jelly_Model $model)
	{
		$original_filename = $model->get($this->name, FALSE);
		return array_pop($original_filename);
	}
 
	/**
	 * Проверяет путь, на записываемость и возвращает путь в нормальном виде
	 *
	 * @param string $path
	 * @return string
	 */
	public function _check_path($path)
	{
		// Normalize the path
		$path = realpath(str_replace('\\', '/', $path));
 
		// Ensure we have a trailing slash
		if (!empty($path) AND is_writable($path))
		{
			$path = rtrim($path, '/').'/';
		}
		else
		{
			throw new Kohana_Exception(get_class($this).' must have a `path` property set that points to a writable directory');
		}
		return $path;
	}
 
    /**
     * Функция для удаления старого файла.
     *
     * @param string $path
     * @param string $name
     * @return void
     */
	public function delete_old_file($path, $name)
	{
		$path = $path . $name;
		if (file_exists($path) && is_file($path)) {
			unlink($path);
		}
	}
}


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

Создаём новый файл c вьюшкой для изображения. Вьюшку можно положить к файлам всех вьюшек библиотеки /modules/jelly/views/jelly/field/image.php, или как сделал я, положить его в каталог админки /application/views/admin/fields/form/image.php

У меня есть два каталога для генератора форм. В первом хранятся вьюшки позволяющие модифицировать код, во втором выводят страницы только для чтения. На всякий случай, я приведу вьюшки из обоих каталогов.

Вот код для вьюшки форм. За этот код мне стыдно и я его переделаю чуть позже.. Обещаю :)

<?php
	$filename 	= array_pop($value);
	$field_name = 'imageoption'.$name;
 
	if ($filename) {
		if (isset($field->thumbnails) && count($field->thumbnails) > 0) {
			$path = str_replace(DOCROOT, '', $field->thumbnails[0]['path']);
			echo "<p><img src='" . URL::site($path . $filename) . "' /></p>";
		} else {
			$path = str_replace(DOCROOT, '', $field->path);
			echo "<p><img src='" . URL::site($path . $filename) . "' /></p>";
		}
		echo '<p>
			<label for="'.$field_name.'1"><input id="'.$field_name.'1" type="radio" checked="checked" value="leave" name="'.$field_name.'"> Оставить</label>
			<label for="'.$field_name.'2"><input id="'.$field_name.'2" type="radio" value="delete" name="'.$field_name.'"> Удалить</label>
			<label for="'.$field_name.'3"><input id="'.$field_name.'3" type="radio" value="replace" name="'.$field_name.'"> Изменить на</label>
			</p>';
	}
?>
<?php echo Form::file($name, $attributes + array('id' => 'field-'.$name, 'onClick' => "this.form.{$field_name}3.checked='checked'")); ?>


И код для страницы только для просмотра

<?php
	if (isset($field->thumbnails) && count($field->thumbnails) > 0) {
		$path = str_replace(DOCROOT, '', $field->thumbnails[0]['path']);
		echo "<img src='" . URL::site($path . array_pop($value)) . "' />";
	} else {
		$path = str_replace(DOCROOT, '', $field->path);
		echo "<img src='" . URL::site($path . array_pop($value)) . "' />";
	}
?>

Теперь у нас всё готово что бы создать новый тип поля в модели.

Итак, пробуем создать модель с полем «изображение». Редактируем свою модель, и добавляем новое поле.

'photo'				=> new Field_Image(array(
	'label'			=> 'Картинка',
	'path'			=> DOCROOT . 'userdata/news/full/',
	'delete_old_file'=> true,
	'resize'		=> array(
		'width'	=> 10,
		'm_dim'	=> Image::WIDTH,
	),
	'thumbnails'	=> array(
		array(
			'path'   => DOCROOT.'userdata/news/small/',  // where to save the thumbnails
			'resize' => array(100, 100, Image::AUTO),    // width, height, resize type
			'crop'   => array(100, 100, NULL, NULL),     // width, height, offset_x, offset_y
			'driver' => 'ImageMagick',                   // NULL defaults to Image::$default_driver
		),
	),
	'rules'			=> array(
		'Upload::valid'	=> 	null,
		'Upload::size'	=>	array("5M"),
		'Upload::type'	=>	array(array("jpeg","jpg","gif","png")),
	)
)),


Я надеюсь что всё понятно из комментариев. Основное отличие типа «Field_Image» от «Field_File» в том что у него появился массив thumbnails, в котором могут находится подмассивы. Каждый подмассив — это настройки для новой превьюшки изображения. Т.е. для одной реально загруженной картинки можно автоматически создавать несколько превьюшек с различными пропорциями.

Тут можно долго объяснять, но проще будет посмотреть на официальное API.

И последнее о чём мне необходимо вам напомнить, так это о том, что в контроллере мы должны для валидации указывать сборным массив из $_POST и $_FILES. Сделать это можно примерно так:

$post = array_merge($_FILES, $_POST);
if ($post) {
 
	try {
 
		$object->set($post);
		$object->save();
		$this->request->redirect(Helper_Admin::get_admin_url($object, 'list'));
 
	} catch (Validate_Exception $e) {
		$errors = $e->array;
	}
 
}

Надеюсь что этот текст окажется полезным для вас и буду рад сообщениям об ошибкам или вопросам :)

This entry was posted in Разработка and tagged , , . Bookmark the permalink.

9 Responses to Jelly — Добавляем тип поля «Изображение»

  1. icp says:

    Поправьте теги в коде, а то куча всяких смиволов >

  2. biakaveron says:

    Гм… А зачем править метод set() в родительском классе Field, если это можно (и ИМХО нужно) делать в дочернем (Field_Image)?

    • ukko says:

      Это ошибка не класса Image, а класса Field_File.

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

  3. xbagir says:

    А не было ли опыта сравнения родной ORM и Jelly, кто из них по шустрее работает ?

    Как-то писал для себя маленький тестик, вроде как идентичный результат по скорости получился. Интересно, как эти ORM-ки ведут себя на рабочих проектах, так же одинково или разрыв в производительности все же есть?

    • ukko says:

      Нет, я никогда не сравнивал. Поэтому не могу ничего толком сказать о скорости. Понятно что без ORM, обычными SQL запросами скорость будет намного выше.

      Хотя, по сути это одни и те же обращения к БД, которые, кстати, можно кэшировать.

  4. Pingback: PHP Code Sniffer для Kohana 3 | Записочки

  5. Pingback: Развитие Jelly ORM | Изучаем Web