Как обновить проект с symfony-2.0 до symfony-2.1

Изначально я создавал свой проект на версии symfony 2.0 без vendors (standard edition). Все зависимости были установлены через

php bin/vendors install

Проект находился в git-репозитории.

Сразу оговорка — на момент написания статьи версия symfony 2.1 находилась в стадии бета-тестирования.
Тем не менее, основная тенденция уже была понятна и значительных изменений в АПИ не предвиделось.
Одно из ключевых нововведений в symfony 2.1 — использование Composer.
На вебсайте фреймворка Symfony можно найти описание процесса создания нового проекта, но нет информации как обновить существующий.

Итак, начнем-с.
Изначально я попытался просто скопировать содержимое файла composer.json, находящегося в директории фреймворка Symnofy на github, и выполнить команду

php composer.phar update

но там нет всех зависимостей, необходимых для нормального функционирования проекта.
Потому я просто решил создать новый «пустой» проект:

php composer.phar create-project symfony/framework-standard-edition path/

в отдельной ветке git-репозитория и потом перенести изменения из ветки с версией symfony-2.0.

Признаюсь, что пришлось порешать конфликты в файлах apps/AppKernel.php, файлах config.yml, security.yml и других.
Но это не было слишком сложным заданием.
Также необходимо проапгрейдить все дополнительные bundle, которые вы используете до соответствующих версий под symfony-2.1
К примеру, для FOSUserBundle (https://github.com/FriendsOfSymfony/FOSUserBundle/blob/master/Resources/doc/index.md#installation)

{
    "require": {
        "friendsofsymfony/user-bundle": "*"
    }
}

и далее выполнить команду

php composer.phar update

Таже необходимо сделать изменения согласно этой инструкции в файлах форм и других местах. Тут уже дело персональное — смотря кто какие фичи использовал в своем проекте.

Таким образом, после всех проделанных манипуляций проект запустился успешно.
Из видимых изменений — добавили красивостей к web-debug-toolbar — появились тултипы со сводной информацией по каждому из разделов, которые показываются при наведении мышью на соответствующую иконку.

П.С.: Вот на днях встретил другой вариант апгрейда до symfony-2.1: http://buildthedamnproduct.com/upgrading-symfony-to-21-10415

Composer — minimum-stability по-умолчанию считается stable

Сегодня (4 июля 2012) Composer изменяет значение по-умолчанию свойства stability с dev на stable.
Подробнее здесь и здесь.
Что это значит? Что по-умолчанию Composer будет игнорировать пакеты RC, beta, alpha или dev.
Столкнулся я с этим, когда попытался проапдейтить зависимости проекта, использующего symfony-2.1.x, которая на момент написания заметки находится на стадии бета-тестирования.


rodush@debian:~/gtrs$ php composer.phar update
Updating dependencies
Your requirements could not be solved to an installable set of packages.

Вот что подсказал скрипт:

Potential causes:
— A typo in the package name
— The package is not available in a stable-enough version according to your minimum-stability setting
see https://groups.google.com/d/topic/composer-dev/_g3ASeIFlrc/discussion for more details.

Чтобы обойти данную проблему, самый простой способ — это в файл composer.json добавить свойство
"minimum-stability": "dev"

Symfony, Yaml отключение файла

Бывает так, что в некоторых конфигурационных файлах для представления view.yml необходимо отключить некоторые из файлов — css или javascript. Как известно, массив в yml можно определить как

key:
  - value1.css
  - value2.css
  - value3.css

Отлючается определенный файл с помощью директивы «-» (обычный дефис).
Иногда при отключении файла можно случайно либо по незнанию некорректно поставить прочерк (между знаком отключения и название файла появится пробел), т.е., к примеру при желании отключить второй файл value2.css мы напишем:

key:
  - value1.css
  - - value2.css
  - value3.css

В таком случае наша директива не сработает.
Корректно писать так:

key:
  - value1.css
  - -value2.css
  - value3.css

Об этом явно говорится в документации. Но часто бывает так, что кто ж её читает, когда есть «гугль в помощь»!

Symfony sfThumbnailPlugin как виджет формы

В процессе разработки одного веб-сайта в админ-панели мне понадобилось реализовать возможность загрузки фотографии к создаваемой записи. Задача, само собой разумеется частая и востребованная. Так как разработку сайта на делал на основе фреймворка symfony, то решил первым делом порыскать среди готовых плагинов.

Поставленную задачу с успехом должен был решить плагин sfThumbnailPlugin. В симфони (>=1.2), как известно, для загрузки файлов служит виджет sfWidgetFormInputFileEditable. Данный виджет представляет привычное поле выбора файла из файловой системы. За саму же загрузку файла на сервер отвечает валидатор для указанного виджета. Таким образом, задача для меня состояла в том, чтобы «прикрутить» плагин создания «превьюшек» к стандартному виджету.

После углубленного изучения жизненного пути файла в недрах виджетов и валидаторов symfony (от момента его выбора до загрузки на сервер) мне показалось самым логичным использовать функциональность sfThumbnailPlugin именно в валидаторе загружаемого файла. То-есть, после нажатия кнопки сохранения какой-либо записи загруженный файл передается на обработку собственноручно написаного валидатора. Свой файл валидатора я назвал sfValidatorFileThumb и поместил в папку lib/validators проекта.

Класс конфигурации формы выглядит следующим образом (для примера*):

//................................
public function configure()
{
  $this->widgetSchema['photo'] = new sfWidgetFormInputFileEditable(
    array(
      'file_src'  => '/uploads/doctors/thumb/' . $this->getObject()->getPhoto(),
      'is_image'  => true,
      'with_delete' => true,
      'delete_label'  => "Удалить фото"
    )
);

Валидатор под этот виджет:

$this->validatorSchema['photo'] = new sfValidatorFileThumb(
  array(
    'path'        => sfConfig::get('sf_upload_dir') . DIRECTORY_SEPARATOR . 'doctors',
    'required'    => false,
    'mime_types'  => 'web_images',
    // Part below concerning a thumbnail
    'with_thumb'  => true,
    'thumb_path'  => sfConfig::get('sf_upload_dir') . DIRECTORY_SEPARATOR . 'doctors/thumb',
    'thumb_scale' => true,
    'thumb_inflate' => true,
    'thumb_quality' => 80, // must be in a range [0-100]
    'thumb_dimensions'  => array('width' => 116, 'height' => 154)
)
);

Сам же файл валидатора имеет следующий код:

class sfValidatorFileThumb extends sfValidatorFile
/**
* Configures the current validator.
*
* @access protected
*
* @param array $options   An array of options
* @param array $messages  An array of error messages
*/
protected function configure($options = array(), $messages = array())
{
// First configure the parent object
parent::configure($options, $messages);
 
// Add options
$this->addOption('with_thumb', isset($options['with_thumb']) ? $options['with_thumb'] : false);
$this->addOption('thumb_path', isset($options['thumb_path']) ? $options['thumb_path'] : sfConfig::get('sf_upload_dir') . DIRECTORY_SEPARATOR . 'assets');
$this->addOption('thumb_dimensions', isset($options['thumb_dimensions']) ? $options['thumb_dimensions'] : null);
$this->addOption('thumb_scale', isset($options['thumb_scale']) ? $options['thumb_scale'] : false);
$this->addOption('thumb_inflate', isset($options['thumb_inflate']) ? $options['thumb_inflate'] : false);
$this->addOption('thumb_quality', isset($options['thumb_quality']) ? $options['thumb_quality'] : false);
 
// Add messages:
//    $this->addMessage('thumb_path', 'Path does not exists or directory is not writeble!');
 
// Add our custom class that will handle uploaded file
$this->addOption('validated_file_class', 'withThumbnailValidatedFile');
}
 
/**
* Make validation for a file
*
* @access protected
* @author Roman Dushko
*
* @see sfValidatorFile
*/
protected function doClean($value)
{
// Validated file object
$validatedFileObj = parent::doClean($value);
 
if ($this->getOption('with_thumb'))
{
// Check if plugin sfThumpnailPlugin exists
if (!class_exists('sfThumbnail')) // @todo: Could we not to check if plugin is enabled? Or should we check?
{
throw new Exception(sprintf('A class for making the thumbnails "%s" is not found', 'sfThumbnail'));
}
 
// Check if a path where thumbnail will be stored is provided
if(null === $this->getOption('thumb_path'))
{
throw new Exception('Path to thumbnails is not provided!');
}
 
// Set needed validated file properties
$validatedFileObj->setWithThumbnail(true);
$validatedFileObj->setThumbProperties(
array(
'thumb_path' => $this->getOption('thumb_path'),
'thumb_dimensions' => $this->getOption('thumb_dimensions'),
'thumb_scale' => $this->getOption('thumb_scale'),
'thumb_inflate' => $this->getOption('thumb_inflate'),
'thumb_quality' => $this->getOption('thumb_quality')
)
);
}
 
return $validatedFileObj;
}
 
}
 
class withThumbnailValidatedFile extends sfValidatedFile
{
protected $withTumbnail = false;
protected $thumbProperties = array(
'thumb_path' => null,
'thumb_dimensions' => null,
'thumb_scale' => false,
'thumb_inflate' => false,
'thumb_quality' => null
);
 
public function save($file = null, $fileMode = 0666, $create = true, $dirMode = 0777)
{
// Save original image
$savedFile = parent::save($file, $fileMode, $create, $dirMode);
 
// Check if we need to upload a thumbnail
if ($this->withTumbnail)
{
// Get dimension for thumbnail
$thumbDimensions = $this->getThumbProperty('thumb_dimensions');
if ($thumbDimensions)
{
$thumbWidth = isset($thumbDimensions['width']) ? $thumbDimensions['width'] : null;
$thumbHeight = isset($thumbDimensions['height']) ? $thumbDimensions['height'] : null;
}
 
// Create instance of thumbnailer
/**
* @todo: make it possible to define 'adapterClass' for sfThumbnail (and it's options) in factories.yml
*/
$thumnailer = new sfThumbnail(
$thumbWidth,
$thumbHeight,
$this->getThumbProperty('thumb_scale'),
$this->getThumbProperty('thumb_inflate'),
$this->getThumbProperty('thumb_quality')
);
// Load saved file
$thumnailer->loadFile($this->getSavedName());
// Save a thumbnail
$thumnailer->save($this->getThumbProperty('thumb_path') . DIRECTORY_SEPARATOR . basename($this->getSavedName()));
}
 
return $savedFile;
}
 
public function setWithThumbnail($with_thumb = false)
{
$this->withTumbnail = $with_thumb;
}
 
public function getWithThumbnail()
{
return $this->withTumbnail;
}
 
public function setThumbProperties($properties)
{
$this->thumbProperties = $properties;
}
 
public function getThumbProperties()
{
return $this->thumbProperties;
}
 
public function getThumbProperty($propName)
{
return $this->thumbProperties[$propName];
}
 
}

Собственно, за создание «превьюшки» и загрузки файлов на сервер отвечает класс withThumbnailValidatedFile, расширяющий sfValidatedFile, а именно его метод save, переопределяющий родительский метод.

В самом классе валидатора файла-превьюшки мы указываем, кто будет отвечать за обработку провалидированного файла:

$this->addOption('validated_file_class', 'withThumbnailValidatedFile');

Вот и всё! Естественно возможны дальнейшие улучшения, но для меня данное решение оказалось подходящим. При выборе файла и сохранении записи мы делаем две операции:

  1. Сохраняем оригинальное изображение
  2. Создаем и сохраняем thumbnail с помощью «допиленного» валидатора.
sfThumbnailPlugin