Symfony form handling recipe

Date November 3, 2007

Symfony is a really great and well-thought-out framework, but I think its form handling support needs some more improvements. 5-6 years ago I developed an MVC framework as my thesis which has the following features related to form handling:

  • You can define forms logically (including labels, validation rules, error message parameters etc.) via form configuration files. The developer module provides a web based front-end to edit these files in a convenient way.
  • You have an API to create or modify forms from program code (add extra validations rule, add/modify/freeze elements etc.), this could be very useful.
  • A form can render itself via renderer objects, so you can insert a form to a template adding only one line to it. Generally you should only set up a few renderer object per project, they can be reused.
  • The framework validates forms automatically, it can generate client side validation code too. It redisplays the form on errors with the error messages.

It would be great if symfony came up with something similar, I found these features very handy. After this little introduction (feature request ;) ) let me show you a solution how you can effectively handle a form for creating and modifying something.

This tutorial is not an introduction to symfony, you should have some knowledge about it. It has a really great documentation and a super tutorial, so you can learn the basics easily.

Imagine a social site, where users can get points after their activities. There are events (creating forum topic, answering a question etc.). When a user for example creates a forum topic we record a new_forum_topic event to him/her. We will now build a form for creating and editing the events in the system.

Database layout

An event has the following attributes:

  • id: primary key, auto generated.
  • code: we can refer to an event with it in program code, which improves the readability of the code.
  • type: we differentiate public and admin actions.
  • weight: how many points worth is the event.
  • title: we show this text on the administration area.
  • description: the description of the event.

We store the events in the following database table (you may notice that it would be nice to use an enum for the type field, but unfortunately Doctrine (a PHP ORM tool) does not support it yet):

1
2
3
4
5
6
7
8
9
10
CREATE TABLE award_event(    
    id INT UNSIGNED AUTO_INCREMENT,
    code VARCHAR(50) NOT NULL,
    type VARCHAR(20) NOT NULL,
    weight SMALLINT UNSIGNED DEFAULT 1 NOT NULL,
    title VARCHAR(255) NOT NULL,
    description TEXT,
    PRIMARY KEY(id),
    UNIQUE (code)
) ENGINE = INNODB;

Setting up the controller

We have two actions from the user’s point of view: creating a new event and editing an existing one), but with a little trick we can use the same symfony action and view for both of them. First let us find out the interface of the two actions. We can create an event with the URL http://example.com/award/event/add and edit it with http://example.com/award/edit/id. To achieve this we have to add the following to our routing.yml:

1
2
3
4
5
6
7
8
award_edit_event:
  url:   /award/event/edit/:id
  param: { module: award, action: editEvent }
  requirements: { id: \d+ }

award_add_event:
  url:   /award/event/add
  param: { module: award, action: editEvent, id: ~ }

You can see that we mapped both functions to the same editEvent action. In the first rule we ensure that id parameter should be an integer, in the second rule we ensure that the value of the id parameter will be constantly null. Now we should add to award module’s actions.class.php the executeEditEvents method:

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
class awardActions extends sfActions {
    /**
     * Editing award events.
     */

    public function executeEditEvent() {
        $request = $this->getRequest();
        if ($request->getMethod() != sfRequest::POST) {
            // Are we editing an existing event?
            if ($id = $request->getParameter('id')) {
                $event = sfDoctrine::getTable('AwardEvent')->find($id);
                $this->forward404Unless($event);
                // The template always gets the value of the inputs from the request object.
                $request->setParameter('code', $event->code);
                $request->setParameter('type', $event->type);
                $request->setParameter('weight', $event->weight);
                $request->setParameter('title', $event->title);
                $request->setParameter('description', $event->description);
            }
        } else {
            // Are we editing an existing event?
            if ($id = $request->getParameter('id')) {
                $event = sfDoctrine::getTable('AwardEvent')->find($id);
                $this->forward404Unless($event);
            // No, so create a new event.
            } else{
                $event = new AwardEvent();
            }

            $event->code = $request->getParameter('code');
            $event->type = $request->getParameter('type');
            $event->weight = $request->getParameter('weight');
            $event->title = $request->getParameter('title');
            $event->description = $request->getParameter('description');
            $event->save();

            $this->setFlash('appMessage', 'Your modifications was saved.');
            $this->redirect('@award_list_events');
        }
    }
}

Let’s walk through the code. When the page is first loaded we have one thing to do: if we are on edit phase we have to read the given event’s data from the database, and set its attributes into the request object. Maybe this seems a little strange, but I think it is a good idea, because this way the template can use transparently the request object to get the values of the input fields. Don’t worry about the line sfDoctrine::getTable('AwardEvent')->find($id); if you are not familiar with Doctrine, it loads the event from the database via the primary key, and returns an AwardEvent object filled with these data.

After posting the form we check if we are creating a new event or editing an existing one. In the first case we create a new AwardEvent object, in the second case we load it from the database. Then we set up the object’s properties from the request and save the event. Finally we assign a flash message to the user and make a redirect to the list page of the events.

Creating the view

We are almost ready with the controller part of our program, let’s make now the view. We should create a file named editEventSuccess.php in the module’s templates directory with the following content:

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
<?php use_helper('Validation') ?><?php if($id = $sf_params->get('id')): ?>
    <h1>Edit event</h1>
<?php else: ?>
    <h1>Create event</h1>
<?php endif; ?>

<br />

<?php if($id): ?>
    <?= form_tag('@award_edit_event?id='.$id) ?>
<?php else: ?>
    <?= form_tag('@award_add_event') ?>
<?php endif; ?>
    <?= form_error('code') ?>
    <label for="code">Code:</label>
    <?= input_tag('code', $sf_params->get('code')) ?>
    <br /><br />

    <?= form_error('type') ?>
    <label for="type">Type:</label>
    <?= select_tag('type', options_for_select(array('admin' => 'admin', 'user' => 'user'), $sf_params->get('type'))) ?>
    <br /><br />
   
    <?= form_error('weight') ?>
    <label for="weight">Weight:</label>
    <?= input_tag('weight', $sf_params->get('weight')) ?>
    <br /><br />

    <?= form_error('title') ?>
    <label for="title">Title:</label>
    <?= input_tag('title', $sf_params->get('title')) ?>
    <br /><br />

    <?= form_error('description') ?>
    <label for="code">Description:</label>
    <?= textarea_tag('description', $sf_params->get('description')) ?>
    <br /><br />

    <?= submit_tag("Save") ?>
</form>

There is nothing special here, we display the form, the only important thing is to set the proper action to the form tag. You can see that we use the form_error() helper (that’s why we include the Validation helper), which displays the error message related to the given field. Maybe you are wondering a little, because we haven’t done any error handling yet. You are right, so let’s do it.

Input validation

To make symfony validate the user input you should create an editEvent.yml file in the module’s validate directory with following content:

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
methods:         [post]
fields:
  code:
    required:
      msg:       The code field cannot be left blank
    sfStringValidator:
      max:       50
      max_error: This field is too long (50 characters maximum)
    sfDoctrineUniqueValidator:
      class:     AwardEvent
      column:    code
      unique_error: An event with this code already exists. Please choose another one.
  type:
    required:
      msg:       The type field cannot be left blank
    sfRegexValidator:
      match:       Yes
      match_error: The type field should be "admin" or "user"
      pattern:     /^(admin|user)$/
  weight:
    required:
      msg:       The weight field cannot be left blank
    sfNumberValidator:
      nan_error:  This field should be an integer
      type:       int
      type_error: This field should be an integer
  title:
    required:
      msg:       The title field cannot be left blank
    sfStringValidator:
      max:       255
      max_error: This field is too long (255 characters maximum)

I won’t introduce symfony’s input validation, you can read about it in the previously mentioned symfony book. The structure of the file is straightforward, the only interesting thing is the sfDoctrineUniqueValidator which ensures that the value of code field should be unique in the award_event table. If we define this file in the proper directory, symfony will automatically validate the input data. If everything is okey it calls the executeEditEvent method of our action class, but if there is any error, it tries to call the handleErrorEditEvent method. If this method is not presented in the class symfony would use the sfView::ERROR template. While we want to show the error messages in the same template (which is the sfView::SUCCESS), we have to add the following method to the awardAction class:

1
2
3
public function handleErrorEditEvent() {
    return sfView::SUCCESS;
}

And now we have the fully functional create/edit form. I hope you have found this tutorial useful, I believe this is a good solution to reuse as much code as possible.

Leave a Reply

XHTML: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>