В дополнение к отличной статье о 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; } }
Надеюсь что этот текст окажется полезным для вас и буду рад сообщениям об ошибкам или вопросам :)
Поправьте теги в коде, а то куча всяких смиволов >
Спасибо за сообщение, поправил.
Гм… А зачем править метод set() в родительском классе Field, если это можно (и ИМХО нужно) делать в дочернем (Field_Image)?
Это ошибка не класса Image, а класса Field_File.
Поэтому я и предположил что её лучше пофиксить в одном месте, после чего и файлы загружаются и изображения :)
Тьфу, неправильно прочел имя поля — показалось, что исправляется Field_Core :)
А не было ли опыта сравнения родной ORM и Jelly, кто из них по шустрее работает ?
Как-то писал для себя маленький тестик, вроде как идентичный результат по скорости получился. Интересно, как эти ORM-ки ведут себя на рабочих проектах, так же одинково или разрыв в производительности все же есть?
Нет, я никогда не сравнивал. Поэтому не могу ничего толком сказать о скорости. Понятно что без ORM, обычными SQL запросами скорость будет намного выше.
Хотя, по сути это одни и те же обращения к БД, которые, кстати, можно кэшировать.
Pingback: PHP Code Sniffer для Kohana 3 | Записочки
Pingback: Развитие Jelly ORM | Изучаем Web