Итак начнем, будем считать что у вас уже есть настроенный проект на 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.
Комментариев нет:
Отправить комментарий