В дополнение к отличной статье о 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;
}
}
Надеюсь что этот текст окажется полезным для вас и буду рад сообщениям об ошибкам или вопросам :)