13 min read

The signup module

We want to provide the users with the functionality to enter their name, email address, and how they found our web site. We want all this stored in a database and to have an email automatically sent out to the users thanking them for signing up.

To start things off, we must first add some new tables to our existing database schema.

The structure of our newsletter table will be straightforward. We will need one table to capture the users’ information and a related table that will hold the names of all the places where we advertised our site. I have constructed the following entity relationship diagram to show you a visual relationship of the tables:

User Interaction and Email Automation in Symfony 1.3: Part1

All the code used in this article can be accessed here.

Let’s translate this diagram into XML and place it in the config/schema.xml file:

<table name="newsletter_adverts" idMethod="native" phpName="NewsletterAds">
   <column name="newsletter_adverts_id" type="INTEGER"
          required="true" autoIncrement="true"
          primaryKey="true" />
   <column name="advertised" type="VARCHAR" size="30"
          required="true" />
</table>
<table name="newsletter_signups" idMethod="native"
       phpName="NewsletterSignup">
   <column name="id" type="INTEGER" required="true"
           autoIncrement="true" primaryKey="true" />
   <column name="first_name" type="VARCHAR" size="20"
           required="true" />
   <column name="surname" type="VARCHAR" size="20"
           required="true" />
   <column name="email" type="VARCHAR" size="100"
           required="true" />
   <column name="activation_key" type="VARCHAR" size="100"
           required="true" />
   <column name="activated" type="BOOLEAN" default="0"
           required="true" />
   <column name="newsletter_adverts_id" type="INTEGER"
           required="true"/>
   <foreign-key foreignTable="newsletter_adverts"
           onDelete="CASCADE">
   <reference local="newsletter_adverts_id"
           foreign="newsletter_adverts_id" />
   </foreign-key>
   <column name="created_at" type="TIMESTAMP" required="true" />
   <column name="updated_at" type="TIMESTAMP" required="true" />
</table>

We will need to populate the newsletter_adverts table with some test data as well. Therefore, I have also appended the following data to the fixtures.yml file located in the data/fixtures/ directory:

NewsletterAds:
  nsa1:
    advertised: Internet Search
  nsa2:
    advertised: High Street
  nsa3:
    advertised: Poster

With the database schema and the test data ready to be inserted into the database, we can once again use the Symfony tasks. As we have added two new tables to the schema, we will have to rebuild everything to generate the models using the following command:

$/home/timmy/workspace/milkshake>symfony propel:build-all-load --no-confirmation

Now we have populated the tables in the database, and the models and forms have been generated for use too.

Binding a form to a database table

Symfony contains a whole framework just for the development of forms. The forms framework makes building forms easier by applying object-oriented methods to their development. Each form class is based on its related table in the database. This includes the fields, the validators, and the way in which the forms and fields are rendered.

A look at the generated base class

Rather than starting off with a simple form, we are going to look at the base form class that has already been generated for us as a part of the build task we executed earlier. Because the code is generated, it will be easier for you to see the initial flow of a form. So let’s open the base class for the NewsletterSignupForm form. The file is located at lib/form/base/BaseNewsletterSignupForm.class.php:

class BaseNewsletterSignupForm extends BaseFormPropel
{
  public function setup()
  {
    $this->setWidgets(array(
      'id' => new sfWidgetFormInputHidden(),
      'first_name' => new sfWidgetFormInput(),
      'surname' => new sfWidgetFormInput(),
      'email' => new sfWidgetFormInput(),
      'activation_key' => new sfWidgetFormInput(),
      'activated' => new sfWidgetFormInputCheckbox(),
      'newsletter_adverts_id' => new sfWidgetFormPropelChoice
         (array('model' => 'NewsletterAds', 'add_empty' => false)),
      'created_at' => new sfWidgetFormDateTime(),
      'updated_at' => new sfWidgetFormDateTime(),
    ));
  $this->setValidators(array(
   'id' => new sfValidatorPropelChoice(array
              ('model' => 'NewsletterSignup', 'column' => 'id',
                                             'required' => false)),
   'first_name' => new sfValidatorString(array('max_length' => 20)),
   'surname' => new sfValidatorString(array('max_length' => 20)),
   'email' => new sfValidatorString(array('max_length' => 100)),
   'activation_key' => new sfValidatorString(array('max_length' => 100)),
   'activated' => new sfValidatorBoolean(),
   'newsletter_adverts_id'=> new sfValidatorPropelChoice(array
                                ('model' => 'NewsletterAds',
                                 'column' => 'newsletter_adverts_id')),
   'created_at' => new sfValidatorDateTime(),
   'updated_at' => new sfValidatorDateTime(),
  ));
  $this->widgetSchema->setNameFormat('newsletter_signup[%s]');
  $this->errorSchema = new sfValidatorErrorSchema
                           ($this->validatorSchema);
  parent::setup();
}

There are five areas in this base class that are worth noting:

  • This base class extends the BaseFormPropel class, which is an empty class. All base classes extend this class, which allows us to add global settings to all our forms.
  • All of the columns in our table are treated as fields in the form, and are referred to as widgets. All of these widgets are then attached to the form by adding them to the setWidgets() method. Looking over the widgets in the array, you will see that they are pretty standard, such as sfWidgetFormInputHidden(), sfWidgetFormInput().
  • However, there is one widget added that follows the relationship between the newsletter_sigups table and the newsletter_adverts table. It is the sfWidgetFormPropelChoice widget. Because there is a 1:M relation between the tables, the default behavior is to use this widget, which creates an HTML drop-down box and is populated with the values from the newsletter_adverts table. As a part of the attribute set, you will see that it has set the model needed to retrieve the values to NewsletterAds and the newsletter_adverts_id column for the actual values of the drop-down box.
  • All the widgets on the form must be validated by default. To do this, we have to call the setValidators() method and add the validation requirements to each widget. At the moment, the generated validators reflect the attributes of our database as set in the schema. For example, the first_name field in the statement ‘first_name’ => new sfValidatorString(array(‘max_length’ => 20)) demonstrates that the validator checks if the maximum length is 20. If you remember, in our schema too, the first_name column is set to 20 characters.
  • The final part calls the parent’s setup() function.

The base class BaseNewsletterSignupForm contains all the components needed to generate the form for us. So let’s get the form on a page and take a look at the method to customize it.

There are many widgets that Symfony provides for us. You can find the classes for them inside the widget/ directory of your Symfony installation. The Symfony propel task always generates a form class and its corresponding base class. Of course, not all of our tables will need to have a form bound to them. Therefore, delete all the form classes that are not needed.

Rendering the form

Rendering this basic form requires us to instantiate the form object in the action. Assigning the form object to the global $this variable means that we can pass the form object to the template just like any other variable. So let’s start by implementing the newsletter signup module. In your terminal window, execute the generate:module task like this:

$/home/timmy/workspace/milkshake>symfony generate:module frontend signup

Now we can start with the application logic. Open the action class from apps/frontend/modules/signup/actions/actions.class.php for the signup module and add the following logic inside the index action:

public function executeIndex(sfWebRequest $request)
{
  $this->form = new NewsletterSignupForm();
return sfView::SUCCESS; }

As I had mentioned earlier, the form class deals with the form validation and rendering. For the time being, we are going to stick to the default layout by allowing the form object to render itself. Using this method initially will allow us to create rapid prototypes. Let’s open the apps/frontend/signup/templates/indexSuccess.php template and add the following view logic:

<form action="<?php echo url_for('signup/submit') ?>" method="POST">
   <table><?php echo $form ?></table>
   <input type="submit" />
</form>

The form class is responsible for rendering of the form elements only. Therefore, we have to include the <form> and submit HTML tags that wrap around the form. Also, the default format of the form is set to ‘table’. Again, we must also add the start and end tags of the <table>.

At this stage, we would normally be able to view the form in the browser. But doing so will raise a Symfony exception error. The cause of this is that the results retrieved from the newsletter_adverts table are in the form of an array of objects. These results need to populate the select box widget. But in the current format, this is not possible. Therefore, we have to convert each object into its string equivalent. To do this, we need to create a PHP magic function of __toString() in the DAO class NewsletterAds.

The DAO class for NewlsetterAds is located at lib/model/NewsletterAds.php just as all of the other models. Here we need to represent each object as its name, which is the value in the advertised column. Remember that we need to add this method to the DAO class as this represents a row within the results, unlike the peer class that represents the entire result set. Let’s add the function to the NewsletterAds class as I have done here:

class NewsletterAds extends BaseNewsletterAds
{
  public function __toString()
{
return $this->getAdvertised();
} }

We are now ready to view the completed form. In your web browser, enter the URL http://milkshake/frontend_dev.php/signup and you will see the result shown in the following screenshot:

User Interaction and Email Automation in Symfony 1.3: Part1

As you can see, although the form has been rendered according to our table structure, the fields which we do not want the user to fill in are also included. Of course, we can change this quiet easily. But before we take a look at the layout of the form, let’s customize the widgets and widget validators. Now we can begin working on the application logic for submitting the form.

Customizing form widgets and validators

All of the generated form classes are located in the lib/form and the lib/form/base directories. The latter is where the default generated classes are located, and the former is where the customizable classes are located. This follows the same structure as the models.

Each custom form class inherits from its parent. Therefore, we have to override some of the functions to customize the form.

Let’s customize the widgets and validators for the NewsletterSignupForm. Open the lib/forms/NewsletterSignupForm.class.php file and paste the following code inside the configure() method:

//Removed unneeded widgets
    unset(
      $this['created_at'], $this['updated_at'],
      $this['activation_key'], $this['activated'], $this['id']
    );
    //Set widgets
    //Modify widgets
     $this->widgetSchema['first_name'] = new sfWidgetFormInput();
     $this->widgetSchema['newsletter_adverts_id'] = new
       sfWidgetFormPropelChoice(array('model' => 'NewsletterAds',
        'add_empty' => true, 'label'=>'Where did you find us?'));
     $this->widgetSchema['email'] = new sfWidgetFormInput
                              (array('label' => 'Email Address'));
    //Add validation
    $this->setValidators(array
     ('first_name'=> new sfValidatorString(array
        ('required' => true), array('required' => 'Enter your
                                                  firstname')),
      'surname'=> new sfValidatorString(array('required' => true),
                        array('required' => 'Enter your surname')),
      'email'=> new sfValidatorString(array('required' => true),
                           array('invalid' => 'Provide a valid email',
                                   'required' => 'Enter your email')),
      'newsletter_adverts_id' => new
                sfValidatorPropelChoice(array('model' => 'NewsletterAds',
                   'column' => 'newsletter_adverts_id'),
                   array('required' => 'Select where you found us')),
    ));
    //Set post validators
    $this->validatorSchema->setPostValidator(
      new sfValidatorPropelUnique(array('model' =>
       'NewsletterSignup', 'column' => array('email')),
         array('invalid' => 'Email address is already registered'))
    );
//Set form name
$this->widgetSchema->setNameFormat('newsletter_signup[%s]');
//Set the form format
$this->widgetSchema->setFormFormatterName('list');

Let’s take a closer look at the code.

Removing unneeded fields

To remove the fields that we do not want to be rendered, we must call the PHP unset() method and pass in the fields to unset. As mentioned earlier, all of the fields that are rendered need a corresponding validator, unless we unset them. Here we do not want the created_at and activation_key fields to be entered by the user. To do so, the unset() method should contain the following code:

unset(
  $this['created_at'], $this['updated_at'],
  $this['activation_key'], $this['activated'], $this['id']
);

Modifying the form widgets

Although it’ll be fine to use the remaining widgets as they are, let’s have a look at how we can modify them:

//Modify widgets
    $this->widgetSchema['first_name'] = new sfWidgetFormInput();
    $this->widgetSchema['newsletter_adverts_id'] = new
      sfWidgetFormPropelChoice(array('model' =>
      'AlSignupNewsletterAds', 'add_empty' => true,
      'label'=>'Where did you find us?'));
    $this->widgetSchema['email'] = new
      sfWidgetFormInput(array('label' => 'Email Address'));

There are several types of widgets available, but our form requires only two of them. Here we have used the sfWidgetFormInput() and sfWidgetFormPropelChoice() widgets. Each of these can be initialized with several values. We have initialized the email and newsletter_adverts_id widgets with a label. This basically renders the label field associated to the widget on the form. We do not have to include a label because Symfony adds the label according to the column name.

Adding form validators

Let’s add the validators in a similar way as we have added the widgets:

//Add validation
$this->setValidators(array(
  'first_name'=> new sfValidatorString(array('required' => true),
                   array('required' => 'Enter your firstname')),
  'surname'=> new sfValidatorString(array('required' => true),
                   array('required' => 'Enter your surname')),
  'email'=> new sfValidatorEmail(array('required' => true),
                   array('invalid' => 'Provide a valid email',
                            'required' => 'Enter your email')),
  'newsletter_adverts_id' => new sfValidatorPropelChoice(array
                  ('model' => 'NewsletterAds',
                   'column' => 'newsletter_adverts_id'),
                   array('required' => 'Select where you found us')),
  ));
//Set post validators
$this->validatorSchema->setPostValidator(new
        sfValidatorPropelUnique(array('model' => 'NewsletterSignup',
        'column' => array('email')),
        array('invalid' => 'Email address is already registered'))
  );

Our form will need four different types of validators:

    • sfValidatorString: This checks the validity of a string against a criteria. It takes four arguments—required, trim, min_length, and max_length.
    • SfValidatorEmail: This validates the input against the pattern of an email address.
    • SfValidatorPropelChoice: It validates the value with the values in the newsletter_adverts table. It needs the model and column that are to be used.

 

  • SfValidatorPropelUnique: Again, this validator checks the value against the values in a given table column for uniqueness. In our case, we want to use the NewsletterSignup model to test if the email column is unique.

As mentioned earlier, all the fields must have a validator. Although it’s not recommended, you can allow extra parameters to be passed in. To achieve this, there are two steps:

  1. You must disable the default option of having all fields validated by $this->validatorSchema->setOption(‘allow_extra_fields’, true).
  2. Although the above step allows the values to bypass validation, they will be filtered out of the results. To prevent this, you will have to set $this->validatorSchema->setOption(‘filter_extra_fields’, false).

Form naming convention and setting its style

The final part we added is the naming convention for the HTML attributes and the style in which we want the form rendered. The HTML output will use our naming convention. For example, in the following code, we have set the convention to newsletter_signup[fieldname] for each input field’s name.

//Set form name
$this->widgetSchema->setNameFormat('newsletter_signup[%s]');
//Set the form format
$this->widgetSchema->setFormFormatterName('list');

Two formats are shipped with Symfony that we can use to render our form. We can either render it in an HTML table or an unordered list. As we have seen, the default is an HTML table. But by setting this as list, the form is now rendered as an unordered HTML list, just like the following screenshot. (Of course, I had to replace the <table> tags with the <ul> tags.)

User Interaction and Email Automation in Symfony 1.3: Part1

LEAVE A REPLY

Please enter your comment!
Please enter your name here