Итак начнем, будем считать что у вас уже есть настроенный проект на symfony (я использую symfony 1.4.6). В качестве ORM будем использовать Propel. Итак создадим полигон для наших манипуляций:
1. Создадим frontend приложение
Заходим в консоль и пишем заветную строку
php symfony generate:app frontend
В результате получим структуру папок для нашего полигона.
2. Опишем обьекты которыми собираемся оперировать: 1) author - автор, будет иметь дату рождения(birthday) и имя (name).
2) book - книга, будет иметь дату публикации(pdate), название (title) и ссылаться на своего автора (author_id)
# config/schema.yml
propel:
author:
id: ~
name: { type: varchar(255), required: true }
birthday: { type: date, required: true }
books:
id: ~
title: { type: varchar(255), required: true }
pdate: { type: date, required: true }
author_id: { type: integer, foreignTable: author, foreignReference: id, required: true }
Перед следующим шагом, те кто не настроил доступ к своей БД может сделать это командой :
php ./symfony configure:database 'mysql:host=localhost;dbname=test_dev' myName myPassword
3. Итак, сгенерим классы и добавим в нашу базу таблицы по описанным выше обьектам:
php ./symfony propel:build-all4. Создадим модуль в котором и будет потом писать код
php ./symfony propel:generate-module frontend author Author
Теперь продя по ссылке http://localhost/author Вы должны увидеть уже вполне рабочий модуль с базовыми CRUD возможностями.
![]() |
| http://localhost/author |
![]() |
| http://localhost/author/new |
Начнем с первого екшена - который будет отображать сохраненные данные. На этом етапе он сделан табличной версткой и показует лишь сохраненных авторов, заставим его отображать, так же, количество книг которые написал автор...
В итоге получим следующий шаблон :
apps/frontend/modules/author/templates/indexSuccess.php
<h1>Authors List</h1>
<table>
<thead>
<tr>
<th>Id</th>
<th>Name</th>
<th>Birthday</th>
<th>Books</th>
</tr>
</thead>
<tbody>
<?php foreach ($Authors as $Author): ?>
<tr>
<td><a href="<?php echo url_for('author/edit?id='.$Author->getId()) ?>"><?php echo $Author->getId() ?></a></td>
<td><?php echo $Author->getName() ?></td>
<td><?php echo $Author->getBirthday() ?></td>
<td><?php echo $Author->countBooks() ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<a href="<?php echo url_for('author/new') ?>">New</a>
Две выделенные строки - это дописанный код для отображения количества написанных книг.Теперь можно потестить (посоздавать авторов и подобавлять книги) . Ой, у нас же нету функционала для добавления книг :(. Не беда, создадим модуль для этого, лишь для того что б убедиться что все работает.
php ./symfony propel:generate-module frontend books Book
(Не забудьте определить магический метод __toString() для класса Author )
В моем случае он просто выводит имя автора :
public function __toString() {
return $this->getName();
}
Итак, убедившысь что все работает как надо приступим к описанию задания.
Задание : разработать функционал для добавления авторов и написанных ними книг через 1 форму.
Настроим контроллер что бы он создавал 2 формы и немного настроим их.
Изначально екшен New создавал только форму для авторов и помещал ее в переменную $this->form которая в последствии будет доступна во вьюхе как $form. Теперь же мы получим кроме переменной $form переменную $book_form (обьект класса BookForm). Я допускаю что подобную операцию можно было бы выполнить и непосредственно во вьюхе, но мы также проводим настройку форм что при использовании в файле шаблона протеворечило бы идеологии MVC.
В строках 12, 15 мы задаем вывод форм в виде елементов списка(по умолчанию поля форма выводятся в виде елементов таблицы ). Далее мы удаляем поле author_id и задаем новый формат имен для полей книжной формы. Обратите внимание на участок place_for_index, мы будем использовать его далее.
В нормальном режиме работы экшн executeNew вызывает шаблон successNew из папки templates своего модуля. При генерации propel модуля мы получили уже готовый шаблон который использует партиал _form, но мы не станем им пользоваться так как он используется и при редактировании. Поетому и для наглядности внесем весь клиентский код в successNew.
Первая и вторая строки подгружают на страницу файлы CSS и JS указанные в классе формы (методы: getJavaScripts() и getStylesheets() должны вернуть списки файлов для подключения). В моем случае подгружаются файлы jquery.min.js, jquery.validate.pack.js которые я предварительно положил в папку web/js проекта. Для облегчения клиентского джаваскрипта валидацию натравим на классы - указав их в при конфигурировании форм:
Добавление параметра allow_extra_fields равного истине позволит добавить кастомные поля в форму и не напоротся на унылую морду нерпойденного валидатора.
Вернемся к файлу шаблона:
Теперь серверная часть.
Созданый для нас метод executeCreate() вполне нас устраивает, а вот вызываемый им метод processForm() - мы перепишем:
В строках с 6 по 13 мы узнаем количество добавленныех книжных форм и пачкой клеим их к нашей форме. Функцию sort используем что бы реиндексить массив на случай если значения в нем будут идти с пропусками (к примеру : 1,2,3,5,7). В строке 15 парсим в форму значения, потом проверяем ее и сохраняем.
Попробуйте и у вас не сработает ;)
Остался последний штрих - класс формы.
Первые 2 метода мы уже рассматривали, идем дальше:
Настроим контроллер что бы он создавал 2 формы и немного настроим их.
//...
public function executeNew(sfWebRequest $request)
{
$this->form = new AuthorForm();
$this->form->getWidgetSchema()->setFormFormatterName('list');
$this->book_form = new BookForm();
$this->book_form->getWidgetSchema()->setFormFormatterName('list');
$schema = $this->book_form->getWidgetSchema();
unset($schema['author_id']);
$schema->setNameFormat($this->form->getName().'['.$this->book_form->getName().'][place_for_index][%s]');
}
//...
Изначально екшен New создавал только форму для авторов и помещал ее в переменную $this->form которая в последствии будет доступна во вьюхе как $form. Теперь же мы получим кроме переменной $form переменную $book_form (обьект класса BookForm). Я допускаю что подобную операцию можно было бы выполнить и непосредственно во вьюхе, но мы также проводим настройку форм что при использовании в файле шаблона протеворечило бы идеологии MVC.
В строках 12, 15 мы задаем вывод форм в виде елементов списка(по умолчанию поля форма выводятся в виде елементов таблицы ). Далее мы удаляем поле author_id и задаем новый формат имен для полей книжной формы. Обратите внимание на участок place_for_index, мы будем использовать его далее.
В нормальном режиме работы экшн executeNew вызывает шаблон successNew из папки templates своего модуля. При генерации propel модуля мы получили уже готовый шаблон который использует партиал _form, но мы не станем им пользоваться так как он используется и при редактировании. Поетому и для наглядности внесем весь клиентский код в successNew.
<?php use_stylesheets_for_form($form) ?>
<?php use_javascripts_for_form($form) ?>
<h1>New Author</h1>
<script type="text/javascript" >
$('document').ready(function(){
$('#main_form').validate();
var books_count = 0;
var remover = $('input.remover');
remover.bind('click', function(){
$(this).parents('ul.book').remove();
});
$('#add_book').bind('click', function(){
var book_form = $('.book_prototype').clone();
$(book_form[0].elements).each(function (){
var field_name = $(this).attr('name');
$(this).attr('name', field_name.replace('place_for_index', books_count));
})
books_count++;
var form_content = book_form.find('ul.book');
form_content.append(remover.clone(true));
form_content.insertBefore(this);
});
});
</script>
<form id="main_form" action="<?php echo url_for('author/create') ?>" method="post" >
<ul>
<?php echo $form->render(); ?>
<li id="books"> Books:
<input type="button" id="add_book" value="Add Book" />
</li>
</ul>
<a href="<?php echo url_for('author/index') ?>">Back to list</a>
<input type="submit" value="Save" />
</form>
<div style="display:none;">
<form class="book_prototype" action="">
<ul class="book">
<?php echo $book_form?$book_form->render():''; ?>
</ul>
</form>
<input type="button" class="remover" value="Remove Book" />
</div>
Первая и вторая строки подгружают на страницу файлы CSS и JS указанные в классе формы (методы: getJavaScripts() и getStylesheets() должны вернуть списки файлов для подключения). В моем случае подгружаются файлы jquery.min.js, jquery.validate.pack.js которые я предварительно положил в папку web/js проекта. Для облегчения клиентского джаваскрипта валидацию натравим на классы - указав их в при конфигурировании форм:
// lib/form/AuthorForm.class.php
class AuthorForm extends BaseAuthorForm
{
public function configure()
{
$this->validatorSchema->addOption('allow_extra_fields', true);
$this->widgetSchema['name']->setAttribute('class', 'required');
$this->widgetSchema['birthday']->setAttribute('class', 'required');
}
public function getJavaScripts() {
return array('jquery.min.js', 'jquery.validate.pack.js');
}
//...
// lib/form/BookForm.class.php
class BookForm extends BaseBookForm
{
public function configure()
{
$this->validatorSchema->addOption('allow_extra_fields', true);
$this->widgetSchema['title']->setAttribute('class', 'required');
$this->widgetSchema['pdate']->setAttribute('class', 'required');
}
}
//...
Добавление параметра allow_extra_fields равного истине позволит добавить кастомные поля в форму и не напоротся на унылую морду нерпойденного валидатора.
Вернемся к файлу шаблона:
- строки с 33 по 44 - отображение главной формы (формы авторов), контейнер для форм книжек (37-39) с кнопокой добавления книг;
- строки с 46 по 53 - контейнер для прототипа форм книжек с кнопкой их удаления;
- строки с 6 по 31 - немного магии на джава-скрипте
<script type="text/javascript">
$('document').ready(function(){
$('#main_form').validate();
var books_count = 0;
var remover = $('input.remover');
remover.bind('click', function(){
$(this).parents('ul.book').remove();
});
$('#add_book').bind('click', function(){
var book_form = $('.book_prototype').clone();
$(book_form[0].elements).each(function (){
var field_name = $(this).attr('name');
$(this).attr('name', field_name.replace('place_for_index', books_count));
})
books_count++;
var form_content = book_form.find('ul.book');
form_content.append(remover.clone(true));
form_content.insertBefore(this);
});
});
</script>
- строка 8 - вешаем валидацию формы;
- строка 9 - устанавливаем начальное значение счетчика книг;
- строки с 11 по 15 - вешаем удаление формы книги при нажатии на ее кнопу (кнопку удаления);
- строки с 17 по 28 - вешаем добавление форм при клике на кнопку "Add book".
Теперь серверная часть.
Созданый для нас метод executeCreate() вполне нас устраивает, а вот вызываемый им метод processForm() - мы перепишем:
// apps/frontend/modules/author/actions/actions.class.php
protected function processForm(sfWebRequest $request, sfForm $form)
{
$params = $request->getParameter($form->getName());
$FormForConfiguration = new BookForm();
if(count($params[$FormForConfiguration->getName()])){
$form->embedFormForEach($FormForConfiguration->getName(),
$FormForConfiguration,
count($params[$FormForConfiguration->getName()])
);
sort($params[$FormForConfiguration->getName()]);
}
$form->bind($params, $request->getFiles($form->getName()));
if ($form->isValid())
{
$Author = $form->save();
$this->redirect('author/edit?id='.$Author->getId());
}
}
//...
В строках с 6 по 13 мы узнаем количество добавленныех книжных форм и пачкой клеим их к нашей форме. Функцию sort используем что бы реиндексить массив на случай если значения в нем будут идти с пропусками (к примеру : 1,2,3,5,7). В строке 15 парсим в форму значения, потом проверяем ее и сохраняем.
Попробуйте и у вас не сработает ;)
Остался последний штрих - класс формы.
<?php
class AuthorForm extends BaseAuthorForm
{
public function configure()
{
$this->validatorSchema->addOption('allow_extra_fields', true);
$this->widgetSchema['name']->setAttribute('class', 'required');
$this->widgetSchema['birthday']->setAttribute('class', 'required');
}
public function getJavaScripts() {
return array('jquery.min.js', 'jquery.validate.pack.js');
}
protected function setAuthorIdForEmbeddedForms($eForms)
{
if(!is_array($eForms))
return false;
foreach ($eForms as $Form)
{
if(method_exists($Form, 'getObject'))
{
$obj = $Form->getObject();
if(method_exists($obj, 'setAuthorId'))
$obj->setAuthorId($this->getObject()->getId());
}
if($Form->getEmbeddedForms())
$this->setAuthorIdForEmbeddedForms ($Form->getEmbeddedForms());
}
return true;
}
public function saveEmbeddedForms($con = null, $forms = null)
{
$this->setAuthorIdForEmbeddedForms($this->getEmbeddedForms());
parent::saveEmbeddedForms($con, $forms);
}
public function embedForm($name, sfForm $form, $decorator = null)
{
$form->getWidgetSchema()->setNameFormat($this->getName().'['.$name.'][%s]');
parent::embedForm($name, $form, $decorator);
$this->validatorSchema[$name]['author_id'] = new sfValidatorPass();
}
public function embedFormForEach($name, sfForm $form, $n, $decorator = null, $innerDecorator = null, $options = array(), $attributes = array(), $labels = array())
{
if (true === $this->isBound() || true === $form->isBound())
{
throw new LogicException('A bound form cannot be embedded');
}
$this->embeddedForms[$name] = new sfForm();
$form = clone $form;
unset($form[self::$CSRFFieldName]);
$widgetSchema = $form->getWidgetSchema();
$class = $form->getModelName().'Form';// Get name of embeded form class
// generate default values
$defaults = array();
for ($i = 0; $i < $n; $i++)
{
$defaults[$i] = $form->getDefaults();
$this->embeddedForms[$name]->embedForm($i, new $class());// Use new instance of form instead of clonned one - cause clonned forms has links to single object
}
$this->setDefault($name, $defaults);
$decorator = null === $decorator ? $widgetSchema->getFormFormatter()->getDecoratorFormat() : $decorator;
$innerDecorator = null === $innerDecorator ? $widgetSchema->getFormFormatter()->getDecoratorFormat() : $innerDecorator;
$this->widgetSchema[$name] = new sfWidgetFormSchemaDecorator(new sfWidgetFormSchemaForEach(new sfWidgetFormSchemaDecorator($widgetSchema, $innerDecorator), $n, $options, $attributes), $decorator);
$this->validatorSchema[$name] = new sfValidatorSchemaForEach($form->getValidatorSchema(), $n);
// generate labels
for ($i = 0; $i < $n; $i++)
{
if (!isset($labels[$i]))
{
$labels[$i] = sprintf('%s (%s)', $this->widgetSchema->getFormFormatter()->generateLabelName($name), $i);
}
}
$this->widgetSchema[$name]->setLabels($labels);
$this->resetFormFields();
//Now we need to configure validators for passing non-existing id (we didn't have Author object for passing id)
for($i = 0; $i < $n; $i++)
{
$this->validatorSchema[$name][$i]['author_id'] = new sfValidatorPass();
}
}
}
Итак последнее разьяснение. Первые 2 метода мы уже рассматривали, идем дальше:
- setAuthorIdForEmbeddedForms - мотод для установления значения id сохраненного автора в прикрепленные к нему формы (формы книг);
- saveEmbeddedForms - собственно метот в котором мы используем предидущий метод;
- embedForm - метод добавления 1 вложенной формы, в него мы добавили генерации принятого формата имени и удаления валидации поля author_id которое мы просто не можем получить не сохранив обьект автора;
- embedFormForEach - не пугайтесь что он такой большой - необходимо было внести правки в середину кода метода и потому пришлось его полностью скопировать. Строки 63, 70 - Узанем имя класса для формы и используем новый обьект для вставки (в оригинале форма клонировалась и использовались клоны, но при этом возникала проблема что после сохранения в базу попадали значения лиш одной формы продублированые несколько раз, было это из-за того что форма хранит обьект класса который после и будет сохранен, а при клонировании обьект (в нашем случае обьект отдельной книжки) не клонировался как форма, а передавался по ссылке, PHP5 - однако ). И в последних строчках мы проходимся по добавленным формам и удаляем валидацию для поля author_id.


Комментариев нет:
Отправить комментарий