Как стать автором
Обновить

Динамическое добавление связанных форм без AJAX в Symfony 1.4

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

Думаю, эта тема будет интересна начинающим изучать Symfony.

Имеем таблицу с анкетами, каждая из которых может иметь некоторое количество прикрепленных изображений. А может и не иметь.
Хотелось бы, чтобы добавление изображений происходило в той же форме, в которой заполняется информация о пользователе.

// config/doctrine/schema.yml
---
detect_relations: true
options:
  charset: utf8
  type: InnoDB

People:
  columns:
    name:
      type: string(250)
      default: null

Images:
  columns:
    id_person:
      type: integer(8)
      notnull: true
    file:
      type: string(250)
      notnull: true
  relations:
    people:
      class: People
      local: id_person
      foreign: id
      foreignAlias: images
      foreignType: many
      owningSide: true
  indexes:
    fk_person:
      fields: [id_person]


В классе формы для изображения настраиваем валидатор и виджет для поля «file»
// lib/form/doctrine/ImageForm.class.php
class ImageForm extends BaseImageForm
{
  public function configure()
  {
    $this->useFields(array('file'));

    $this->widgetSchema['file'] = new sfWidgetFormInputFile(array(
        'label' => 'Файл',
      ));
    $this->validatorSchema['file'] = new sfValidatorFile(array(
        'required' => false,
        'path' => sfConfig::get('sf_upload_dir'),
        'mime_types' => 'web_images'
      ));
  }
}

В класс формы пользователей надо добавить следующий код:
// lib/form/doctrine/PeopleForm.class.php
class PeopleForm extends BasePeopleForm
{
  public function configure()
  {
    // ...

    // Создаем новое изображение и указываем человека
    $picture = new Image();
    $picture->people = $this->getObject();
    $pic_form = new ImageForm($picture);
    $pictures_forms = new SfForm();
    $pictures_forms->embedForm('picture_0', $pic_form);

    // Подцепляем форму с изображениями к основной форме
    $this->embedForm('pictures', $pictures_forms);
    // В форме это будет отображаться инпутом с именем people[pictures][picture_0][file]
  }
}

Далее обновим шаблон формы модели «people», добавив в него отображение связанной формы:
// apps/frontend/modules/people/templates/_form.php
<ul id="images">
  <li><h3>Копии документов</h3></li>
<?php foreach($form['pictures'] AS $eForm) : ?>
  <li><?php echo $eForm['file']->render() ?></li>
<?php endforeach ?>
</ul>

Добавлять новые поля выбора файла будем javascript`ом. В источниках вдохновения добавление новой связанной формой реализовывалось через ajax. Отправлялся запрос на сервер, там создавалась новая форма, дальше она возвращалась в виде ответа и встраивалась в контейнер. В общем, было что-то вроде «кручу-верчу, запутать хочу».
Я решил не нагружать лишний раз сервер, а просто скопировать уже существующее поле, изменив у него имя. В итоге это работает гораздо быстрее и не нагружает сервер лишний раз.
Я использую в своей работе jQuery.
function appendNewInputFile()
{
  $('#images li:last input').one('change', function()
    {
      var input = $(this)
      var reg = /\d+/
      var num = reg.exec(input.attr('name'))
      var new_name = input.attr('name').replace(reg, parseInt(num[0])+1)
      $('#images').append('<li><input type="file"></li>')
      $('#images li:last input').attr('name', new_name)
      appendNewInputFile()
      return false
    })
}

Теперь возвращаемся к нашему файлу PeopleForm.class.php. Надо добавить в него несколько функций.
// lib/form/doctrine/PeopleForm.class.php

// ...

protected function doBind(array $values)
{
  foreach($values['pictures'] AS $key => $picture)
  {
    // Удаляем из формы пустые поля для прикрепления изображений
    if(empty($picture['file']['name']))
      unset($values['pictures'][$key],
          $this->embeddedForms['pictures'][$key],
          $this->taintedValues['pictures'][$key],
          $this->validatorSchema['pictures'][$key]
        );
  }

  parent::doBind($values);
}

public function bind(array $taintedValues = null, array $taintedFiles = null)
{
  // Добавляем связанные формы, которые мы создали с помощью JS, к основной
  // Без этого валидацию мы не пройдем.
  foreach($taintedFiles['pictures'] AS $key => $newPicture)
    if(!isset($this['pictures'][$key]))
      $this->addNewField($key);

  parent::bind($taintedValues, $taintedFiles);
}

public function addNewField($name)
{
  $picture = new Image();
  $picture->people = $this->getObject();
  $this->embeddedForms['pictures']->embedForm($name, new ImageForm($picture));
  $this->embedForm('pictures', $this->embeddedForms['pictures']);
}

Думаю, пустые поля можно было отсеять еще в функции bind, но меня поджимали сроки и катастрофически не хватало опыта с symfony. Поэтому реализовал эту схему таким образом.

После всех махинаций в анкете появляется поле для выбора файла. После того как выбирается какой-нибудь файл, появляется следующее. И так до бесконечности.
Правда у нашей новой формы есть одна особенность. Если вдруг форма не пройдет валидацию, то во время её повторного отображения с сообщениями об ошибках отрисуется столько полей для выбора файлов, со сколькими мы форму и отправили.
Одинаково работает как при редактировании анкеты, так и при добавлении новой.
Теги:
Хабы:
Данная статья не подлежит комментированию, поскольку её автор ещё не является полноправным участником сообщества. Вы сможете связаться с автором только после того, как он получит приглашение от кого-либо из участников сообщества. До этого момента его username будет скрыт псевдонимом.