Показаны сообщения с ярлыком forms. Показать все сообщения
Показаны сообщения с ярлыком forms. Показать все сообщения

среда, 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

вторник, 16 сентября 2008 г.

Symfony: Создаем форму обратной связи 2/3

Приступим к генерации формы.

Начиная с версии 1.1 в Symfony для генерации форм используется система виджетов. Это означает что Вам надо будет в основном заниматься описанием полей и их свойств, и не заботиться о пользовательском вводе и проверке.

Теперь к практике.
В прошлой статье мы сгенерировали базовые классы форм командой >symfony propel:build-forms
Небольшое объяснение, Symfony генерирует базовые классы, в каталоге например
/lib/forms/base/BaseFeedbackLogForm.class.php
мы их не трогаем, мы в них будем "подглядывать", т.к. в следующий раз при выполнении
>symfony propel:build-forms
все классы в каталоге /lib/forms/base/ будут регенерированы заново, в соответствии в моделью, описанной в schema.yml, и все изменения внесенные в них будут уничтожены.

Для всех опытов и модернизаций нам предоставлены классы в каталоге /lib/forms/
Открываем класс /lib/forms/FeedbackLogForm.class.php, видим пустой класс, отложим его на время.

Открываем actions.class.php, и меняем метод executeIndex

public function executeIndex($request){
$this->form = new FeedbackLogForm(FeedbackLogPeer::retrieveByPk($request->getParameter('id')));
}

Запускаем http://minisite/frontend_dev.php/feedback
ops, error

Class "FeedbackUsers" must implement a "__toString" method to be rendered in a "sfWidgetFormPropelSelect" widget

Что произошло?
Symfony попыталась создать листбокс со списком получателей, для него используется "магический метод" __toString, но он не определен в модели, исправим это.

class FeedbackUsers extends BaseFeedbackUsers
{
public function __toString(){
return $this->getName();
}
}


Пробуем еще, и получаем такой экран:


Для начала неплохо, немного наведем красоту, открываем файл /lib/form/FeedbackLogForm.class.php и вносим следующие изменения:

class FeedbackLogForm extends BaseFeedbackLogForm
{
public function configure()
{
$this->setWidgets(array(
'id' => new sfWidgetFormInputHidden(),
'feedback_user_id' => new sfWidgetFormPropelSelect(array('model' => 'FeedbackUsers', 'add_empty' => true)),
'name' => new sfWidgetFormInput(),
'email' => new sfWidgetFormInput(),
'subject' => new sfWidgetFormInput(),
'content' => new sfWidgetFormTextarea(),
'get_answer' => new sfWidgetFormInputCheckbox(),
));

$this->widgetSchema->setLabels(array(
'feedback_user_id' => 'Кому',
'name' => 'Ваше Ф.И.О.',
'content' => 'Вопрос',
'email' => 'Ваш Email',
'subject' => 'Заголовок письма',
'get_answer' => 'Получить ответ на Email',
));
}//configure
}//class

Обратите внимание на $this->widgetSchema->setLabels тут мы меняем подписи к полям.

Что бы листбокс не пустовал, добавим пару записей.
ПО большому счету сайт начинается с админки.
Но мы пока не будем ей уделять много времени.
symfony propel:init-admin backend feedback_users FeedbackUsers
symfony propel:init-admin backend feedback_log FeedbackLog
Открываем http://minisite/backend_dev.php/feedback_users и вносим пару получателей.

Обновляем страницу http://minisite/frontend_dev.php/feedback и видим



Форма обрела более понятый вид, но нас не устраивает вид подписей к полям, мы хотим их выровнять по левому краю, также мы хотим поля снабдить коментариями, для обязательных полей поставим "*", и рядом с флажком напишем "да"
Для этого мы напишем воспользуемся директивой $this->widgetSchema->setHelps

$this->widgetSchema->setHelps(array(
'feedback_user_id' => ' * ',
'name' => ' * ',
'content' => ' * ',
'email' => ' * ',
'subject' => ' * ',
'get_answer' => ' да ',
));
$myDecorator = new sfFeedbackFormDecorator($this->getWidgetSchema());
$this->getWidgetSchema()->addFormFormatter('custom', $myDecorator);
$this->widgetSchema->setFormFormatterName('custom');

и напишем свой декоратор для строчек формы.

/*
* CUSTOM DECORATOR
*/
class sfFeedbackFormDecorator extends sfWidgetFormSchemaFormatter {
protected
$rowFormat = "
%hidden_fields%

%label%
%field% %help%
\n",
$helpFormat = '%help%',
$errorRowFormat = "
\n%errors%
\n",
$errorListFormatInARow = " \n%errors% \n",
$errorRowFormatInARow = " %error%\n",
$namedErrorRowFormatInARow = "
  • %name%: %error%
  • \n",
    $decoratorFormat = "";
    }// decorator class


    Обновляем страницу и видим:


    На сегодня хватит.
    Что мы пока имеем? Никаких очевидных преимуществ ;)
    Такую формочку с использованием классического HTML мы бы сделали гораздо быстрее, но не будем торопиться, завтра попробуем проверять введенные данные и сохранять их в БД.