Tuesday, September 20, 2011

Symfony Forms Framework: Merge 2 forms

Recently I had to create a form to create/update users in our system. Some time ago we decided to save are users in 2 tables. The first table would contain all login information and the second his personal information. This is a simple example of the DB design:

User db design

User db design

I would not recommend doing this for so little fields. But in our system we have a lot more fields, and it helps us to optimize our queries.

Wouldn't it be better if we could merge the 2 forms? The answer is yes. And it's pretty easy to do so in Symfony … too bad that it's not documented on the Symfony website.

When you generate the forms with Symfony, you'll get a UserForm and a UserInfoForm.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* USER FORM.
*
* @PACKAGE    FORM
*/
class UserForm extends BaseUserForm {
 
public function configure() {
 
}
 
}
 
/**
* USERINFO FORM.
*
* @PACKAGE    FORM
*/
class UserInfoForm extends BaseUserInfoForm {
 
public function configure() {
 
}
 
}

The idea is to display one form to insert/update those 2 tables. If you are familiar to creating forms with Symfony you know that you could achieve it by instantiating the 2 forms in an action and send them to the templates:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/**
* ACTION CLASS
*/
class userActions extends sfActions
{
public function executeCreate($request) {
$this->userForm = new UserForm();
$this->userInfoForm = new UserInfoForm();
$this->setTemplate('edit');
}
 
public function executeEdit($request) {
$this->userForm = new UserForm(UserPeer::retrieveByPK($request->getParameter('id')));
$this->userInfoForm = new UserInfoForm(UserInfoPeer::retrieveByPK($request->getParameter('id')));
}
 
public function executeUpdate($request) {
$this->userForm = new UserForm(UserPeer::retrieveByPK($request->getParameter('id')));
$this->userInfoForm = new UserInfoForm(UserInfoPeer::retrieveByPK($request->getParameter('id')));
 
$this->userForm->bind($request->getParameter($this->userForm->getName()));
$this->userInfoForm->bind($request->getParameter($this->userInfoForm->getName()));
 
// ETC ...
}
}
 
// TEMPLATE CODE EDITSUCCESS.PHP
<?php $user = $userForm->getObject(); ?>
 
<form action="<?php echo url_for('user/update'.(!$user->isNew() ? '?id='.$user->getId() : '')) ?>" method="post">
<table>
<tfoot>
<tr>
<td colspan="2">
<input type="submit" value="Save" />
</td>
</tr>
</tfoot>
<tbody>
<?php echo $userForm ?>
<?php echo $userInfoForm ?>
</tbody>
</table>
</form>

This is really annoying to work this way. You always have to instance 2 forms in the action and the templates. Wouldn't it be better if we could merge the 2 forms? The answer is yes. And it's pretty easy to do so in Symfony … too bad that it's not documented on the Symfony website.

Here is how you do it:

1. UserForm.class.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/**
* USER FORM.
*
* @PACKAGE    FORM
*/
class UserForm extends BaseUserForm {
 
public function configure() {
$this->mergeForm(new UserInfoForm(UserInfoPeer::retrieveByPK($this->getObject()->getId())));
}
 
/**
* OVERRIDE THE SAVE METHOD TO SAVE THE MERGED USER INFO FORM.
*/
public function save($con = null) {
parent::save();
 
$this->updateUserInfo();
 
return $this->object;
}
 
/**
* UPDATES THE USER INFO MERGED FORM.
*/
protected function updateUserInfo() {
// UPDATE USER INFO
if (!is_null($userInfo = $this->getUserInfo())) {
 
$values = $this->getValues();
if ( $userInfo->isNew() ) {
$values['user_id'] = $this->object->getId();
}
 
$userInfo->fromArray($values, BasePeer::TYPE_FIELDNAME);
 
$userInfo->save();
}
}
 
/**
* RETURNS THE USER INFO OBJECT. IF IT DOES
* NOT EXIST RETURN A NEW INSTANCE.
*
* @RETURN USERINFO
*/
protected function getUserInfo() {
 
if (!$this->object->getUserInfo()) {
return new UserInfo();
}
 
return $this->object->getUserInfo();
}
}

2. actions.class.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public function executeCreate($request) {
$this->form = new UserForm();
$this->setTemplate('edit');
}
 
public function executeEdit($request) {
$this->form = new UserForm(UserPeer::retrieveByPK($request->getParameter('id')));
}
 
public function executeUpdate($request) {
 
$this->forward404Unless($request->isMethod('post'));
 
$this->form = new UserForm(UserPeer::retrieveByPK($request->getParameter('id')));
 
$this->form->bind($request->getParameter($this->form->getName()));
if ($this->form->isValid()) {
$user = $this->form->save();
$this->redirect('user/edit?id='.$user->getId());
}
 
$this->setTemplate('edit');
}

3. editSuccess.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php $user = $userForm->getObject(); ?>
 
<form action="<?php echo url_for('user/update'.(!$user->isNew() ? '?id='.$user->getId() : '')) ?>" method="post">
<table>
<tfoot>
<tr>
<td colspan="2">
<input type="submit" value="Save" />
</td>
</tr>
</tfoot>
<tbody>
<?php echo $userForm ?>
</tbody>
</table>
</form>

As you can see, it's the UserForm that will handle all the business of the UserInfoForm. This is great because the code in the action and template will be much more lightened and if needed it can easily be reused somewhere else.

This was a simple example on how to merge 2 forms, but since it's not documented on the symfony website, it took me some time to fully understand on how to make it work. Now you can do much more advanced operations. ;)

No comments: