В дополнение к отличной статье о 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;
    }

}

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