среда, 8 апреля 2009 г.

Symfony 1.2 tabular(embedded) subforms

Довольно часто при редактировании требуется вывести N дочерних записей в виде таблицы.
К сожалению стандартными средствами Symfony этого сделать не получилось, в первую очередь из за того, что Symfony в каждой строке оставляет место для сообщений об ошибках валидации.
Постараемся с минимумом усилий сделать таблицу с подчиненными записями.
Для примера возьмем очень простую ситуацию - в нашем интернет-магазине каждый товар может иметь 0-N рекомендованных товаров.

Кусок схемы:

propel:
pc_product:
_attributes: { phpName: Product }
id: ~
name: varchar(255)
price1: float
price2: float
description: longvarchar
pic: varchar(255)
is_published: boolean
category_id: { type: integer, foreignReference: id, foreignTable: pc_category, onDelete: cascade, onUpdate: cascade, required: true }

pc_product_recommended:
_attributes: { phpName: ProductRecommended }
id: ~
product_id: { type: integer, foreignReference: id, foreignTable: pc_product, onDelete: cascade, onUpdate: cascade, required: true }
recommend_product_id: { type: integer, foreignReference: id, foreignTable: pc_product, onDelete: cascade, onUpdate: cascade, required: true }
note: { type: varchar(512), default: null }
pos: integer

pc_category:
_attributes: { phpName: Category, treeMode: NestedSet }
id: ~
name: varchar(255)
alias: varchar(1024)
lft: { type: integer, required: false, default: 0, nestedSetLeftKey: 'true'}
rgt: { type: integer, required: false, default: 0, nestedSetRightKey: 'true'}
scope: { type: integer, required: false, default: 0, treeScopeKey: 'true'}
parent: { type: integer }
is_published: { type: boolean }
deleted_at: { type: date }


Изменяем /lib/forms/ProductForm.class.php

class ProductForm extends BaseProductForm
{
public function configure()
{
$this->setWidgets(array(
'category_id' => new sfWidgetFormPropelChoice(array('model' => 'Category', 'add_empty' => false)),
'id' => new sfWidgetFormInputHidden(),
'name' => new sfWidgetFormInput(),
'price1' => new sfWidgetFormInput(),
'price2' => new sfWidgetFormInput(),
'pic' => new myWidgetFormInputFileEditable(),
'is_published' => new sfWidgetFormInputCheckbox(),
));


$this->setValidators(array(
'id' => new sfValidatorPropelChoice(array('model' => 'Product', 'column' => 'id', 'required' => false)),
'name' => new sfValidatorString(array('max_length' => 255, 'required' => false)),
'price1' => new sfValidatorNumber(array('required' => false)),
'price2' => new sfValidatorNumber(array('required' => false)),
'is_published' => new sfValidatorBoolean(array('required' => false)),
'category_id' => new sfValidatorPropelChoice(array('model' => 'Category', 'column' => 'id')),
'pic' => new sfValidatorFile(array(
'required' => false,
'max_size' => '102400', // bytes (1MB)
'mime_types' => array('image/png', 'image/jpeg', 'image/gif',)
)),
));

$this->widgetSchema->setNameFormat('product[%s]');

$this->errorSchema = new sfValidatorErrorSchema($this->validatorSchema);

$this->validatorSchema->setOption('allow_extra_fields', true);
$this->validatorSchema->setOption('filter_extra_fields', false);

//получим дочерние записи
$c = new Criteria();
$c->add(ProductRecommendedPeer::PRODUCT_ID, $this->getObject()->getId());
$c->addAscendingOrderByColumn(ProductRecommendedPeer::ID);
$rps = ProductRecommendedPeer::doSelect($c);
//цикл по дочерним записям
foreach ($rps as $index => $rProduct)
{
$rProductForm = new ProductRecommendedForm($rProduct);
$rProductForm->setWidget('product_id', new sfWidgetFormInputHidden());
$rProductForm->setWidget('del_me', new sfWidgetFormInputCheckbox());
$rProductForm->setValidator('del_me', new sfValidatorBoolean(array('required' => false)));
$rProductForm->widgetSchema->moveField('del_me', sfWidgetFormSchema::FIRST);

$this->embedForm('rProduct-'.$index, $rProductForm);
$this->widgetSchema->setLabel('rProduct-'.$index, $index+1);
}
$rProductForm = new ProductRecommendedForm();

$rProductForm->setWidget('product_id', new sfWidgetFormInputHidden());
//добавляем дополнительный чекбокс, для удаления записи
$rProductForm->setWidget('del_me', new sfWidgetFormInputCheckbox(
array(),
array('disabled' => true)
));
if (!$this->getObject()->isNew())
{
//добавим пустой элемент в списке, на случай если мы не хотим добавлять новую запись
$rProductForm->setWidget('recommend_product_id', new sfWidgetFormPropelChoice(
array('model' => 'Product', 'add_empty' => true)));

$rProductForm->setDefaults(array('product_id' => $this->getObject()->getId()));
//отключим обязательный выбор значения листбокса
$rProductForm->setValidator('recommend_product_id', new sfValidatorInteger(array('required' => false)));
//переносим чекбокс для удаления записи наверх
$rProductForm->widgetSchema->moveField('del_me', sfWidgetFormSchema::FIRST);

$this->embedForm('new-rec', $rProductForm);
$this->widgetSchema->setLabel('new-rec', 'new record');
}


}

public function updateObject($values = null)
{
$object = parent::updateObject();
$path = sfConfig::get('sf_root_dir').DIRECTORY_SEPARATOR.
sfConfig::get('sf_web_dir_name').DIRECTORY_SEPARATOR.sfConfig::get('sf_upload_dir');
$object->setPic(str_replace($path.'/', '', $object->getPic()));
return $object;
}
/*
* do save
*/
protected function doSave($con = null){
$values = $this->getValues();

$path = sfConfig::get('sf_root_dir').'/'.
sfConfig::get('sf_web_dir_name').'/'.sfConfig::get('sf_upload_dir');

if (isset($values['pic_delete'])){
$currentFile = $path.'/'.$this->getObject()->getPic();
if (is_file($currentFile)){
unlink($currentFile);
}
$this->getObject()->setPic('');
}

$file = $values['pic'];

if(!empty($file)){
if (file_exists(sfConfig::get('sf_upload_dir').'/'.$this->getObject()->getPic())){
@unlink($this->getObject()->getPic());
}
$filename = sha1($file->getOriginalName()).$file->getExtension($file->getOriginalExtension());
$path = sfConfig::get('sf_root_dir').DIRECTORY_SEPARATOR.sfConfig::get('sf_web_dir_name').DIRECTORY_SEPARATOR.sfConfig::get('sf_upload_dir');
$file->save($path.'/'.$filename);
}

return parent::doSave($con);
}

}



Если в /generator.yml настройки форм по умолчанию:

form: ~
edit: ~
new: ~

мы видим внизу формы редактирования доп.форму с добавлением записи

она даже работает ;) но внешний вид нас не устраивает, и по работе тоже есть нарекания, при каждом обновлении родительской формы без указания нового рекомендованного товара, у нас добавляется пустая запись, а так же еще не работает удаление записей.

Давайте исправим внешний вид и логику добавления-удаления.

Редактируем в config/generator.yml настройки формы указываем поля для отображения:

form:
display: [ name, price1, price2, pic, is_published ]

Такая небольшая настройка приводит к тому, что исчезают наши встроенные формы, подробнее об этом явлении как нибудь позже...
При этом форма перестает работать, что неудивительно, т.к. мы к форме прикрепили массу валидаторов для подчиненных форм.
Теперь мы в ручную отобразим подформы в табличном виде.
Заходим в кэш и копируем
/cache/backend/.../autoProduct/templates/_form.php в
apps/backend/modules/product/templates/_form.php

Далее заменяем содежимое этого файла на:

<div class="sf_admin_form">
<?php echo form_tag_for($form, '@product') ?>
<?php echo $form->renderHiddenFields() ?>

<?php if ($form->hasGlobalErrors()): ?>
<?php echo $form->renderGlobalErrors() ?>
<?php endif; ?>

<?php foreach ($configuration->getFormFields($form, $form->isNew() ? 'new' : 'edit') as $fieldset => $fields): ?>
<?php include_partial('product/form_fieldset', array('product' => $product, 'form' => $form, 'fields' => $fields, 'fieldset' => $fieldset)) ?>
<?php endforeach; ?>

<?php if ($form->getEmbeddedForms()): ?>
<fieldset id="sf_fieldset_recommended_products">
<h2>Recommended products</h2>
<table>
<tr>
<th>#</th>
<th>del</th>
<th>product</th>
<th>note</th>
<th>pos</th>
</tr>
<?php foreach ($form->getEmbeddedForms() as $index => $embForm): ?>
<tr>
<?php echo $form[$index]['id']->render() ?>
<?php echo $form[$index]['product_id']->render() ?>
<td><?php echo $form[$index]->renderLabel() ?></td>
<td><?php echo $form[$index]['del_me']->renderError() ?><?php echo $form[$index]['del_me']->render() ?></td>
<td><?php echo $form[$index]['recommend_product_id']->renderError() ?><?php echo $form[$index]['recommend_product_id']->render() ?></td>
<td><?php echo $form[$index]['note']->renderError() ?><?php echo $form[$index]['note']->render() ?></td>
<td><?php echo $form[$index]['pos']->renderError() ?><?php echo $form[$index]['pos']->render() ?></td>
</tr>
<?php endforeach; ?>
</table>
</fieldset>
<?php endif; ?>

<?php include_partial('product/form_actions', array('product' => $product, 'form' => $form, 'configuration' => $configuration, 'helper' => $helper)) ?>
</form>
</div>



Подформа преобрела более приятный вид, остается разобраться с обновлением и удалением.
Добавляем новый метод в /lib/forms/ProductForm.class.php

public function updateObjectEmbeddedForms($values, $forms = null)
{
if (is_null($forms))
{
$forms = $this->embeddedForms;
}

foreach ($forms as $name => $form)
{
if (!is_array($values[$name]))
{
continue;
}

if ($form instanceof sfFormPropel)
{
if (isSet($values[$name]['del_me'])){
if ($values[$name]['del_me'] == 1)
{
$form->getObject()->delete();
unset($this->embeddedForms[$name]);
continue;
}
} else {
if (strstr($name, 'new-'))
{
//именно тут мы сами выступаем в роли валидатора, и если одно из обязательных полей пустое,
//мы игнорируем новую запись
$rProduct = $values[$name]['recommend_product_id'];
$rPos = $values[$name]['pos'];
if ( empty($rProduct) OR empty($rPos))
{
unset($this->embeddedForms[$name]);
continue;
}
}
}//if !isset del me
$form->updateObject($values[$name]);
}
else
{
$this->updateObjectEmbeddedForms($values[$name], $form->getEmbeddedForms());
}
}
//die();
}//updateObjectEmbeddedForms

3 комментария:

zaARTix комментирует...

в последней функции print_r видимо забыл убрать )

Анонимный комментирует...

а у меня удаление не работает!
The item has not been saved due to some errors.
и все тут!

Vit228 комментирует...

Исправлено и проверено, спасибо!