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

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

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

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

Добавим туда метод set(), перекрывающий родительский метод. Без него, мы не смогли бы сохранить файл.
<pre lang="php">public function set($value) { return (array) $value; }</pre> <p>
А теперь создадим новый класс для работы с изображениями.
Он должен располагаться по адресу: /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) &#038;&#038; $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) &#038;&#038; 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) &#038;&#038; 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’“)); ?>

`

И код для страницы только для просмотра
<pre lang="php"> <?php if (isset($field->thumbnails) &#038;&#038; 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)) . "' />"; } ?> </pre> <p>

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

Итак, пробуем создать модель с полем «изображение». Редактируем свою модель, и добавляем новое поле.
<pre lang="php">'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")), ) )),</pre> <p>
Я надеюсь что всё понятно из комментариев. Основное отличие типа «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;
}

}

`

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

Read more posts by this author.