How to build a basic contact form in symfony

Forms in symfony bring some fun to the table. They’re very, very powerful, but you have to know how to ask them, and figuring that out isn’t always the easiest thing. I have worked with forms extensively for the past several weeks, and primed with this knowledge, I believe I can show you a pretty solid way to start building customized forms.

We’re going to have six pieces building our contact form:

  1. Setup — We make our module
  2. Preparation — I have a helpful function for you to add to your BaseForm class that makes things easier later on
  3. Form — We build our form
  4. Action — We build the action to handle our form
  5. Templates — We build two templates to handle our contact form, the form page, and the thank you page.
  6. Routing — All this hard work, let’s make it look nice!

So with that being said, let’s get on the road to enlightenment!

1. Setup

At this point, I assume the following:

  • You have a working symfony install
  • You have an app named ‘frontend’

If you haven’t made a module before, you’re about to expand your horizons!

./symfony generate:module frontend contact_form

This will create apps/frontend/modules/contact_form, and related subdirectories.

2. Preparation

I promised you a helpful addition to your BaseForm class, and along with that, I will offer an explanation.

The sfForm class (and it’s children) will allow you to easily get only hidden fields, but it doesn’t offer you a simple way to allow you to easily get only visible fields. This little function solves that inequity.

  public function getVisibleFields()
  {
    $hidden_fields = $this->getFormFieldSchema()->getHiddenFields();
    $fields = array();
    foreach($this as $name => $field)
    {
      if (!in_array($field, $hidden_fields))
        $fields[$name] = $field;
    }
    return $fields;
  }

If you have nothing else added to your BaseForm.class.php file (lib/form/BaseForm.class.php), it will look like:

<?php
 
/**
 * Base project form.
 *
 * @package    your-package
 * @subpackage form
 * @author     YourNameHere
 * @version    SVN: $Id: BaseForm.class.php 20147 2009-07-13 11:46:57Z FabianLange $
 */
class BaseForm extends sfFormSymfony
{
  public function getVisibleFields()
  {
    $hidden_fields = $this->getFormFieldSchema()->getHiddenFields();
    $fields = array();
    foreach($this as $name => $field)
    {
      if (!in_array($field, $hidden_fields))
        $fields[$name] = $field;
    }
    return $fields;
  }
}

3. Form

For our contact form, we’ll have Name, Email, and Comments. Name and Email will be input fields, and Comments will be a text area. All fields will be required. Let’s take a look at what that form looks like:

lib/form/PublicContactForm.class.php

<?php
 
class PublicContactForm extends BaseForm
{
  public function setup()
  {
    // this is required, don't forget this!
    $this->widgetSchema->setNameFormat('contact_form[%s]');
 
    $this->setWidget('name', new sfWidgetFormInput());
    $this->setValidator('name', new sfValidatorString(array('required' => true)));
 
    $this->setWidget('email', new sfWidgetFormInput());
    $this->setValidator('email', new sfValidatorEmail(array('required' => true)));
 
    $this->setWidget('comments', new sfWidgetFormTextarea());
    $this->setValidator('comments', new sfValidatorString(array('required' => true)));
 
    parent::setup();
  }
}

Why did I use Public in the name? It reminds me that it’s a public facing form, most likely using a custom-ish template, which I may need to update if I change the form much.

4. The Action

Here’s a hearty chunk of where we’re going to end up. Let’s start by taking a look at the final form of the file, and then we’ll highlight portions.

apps/frontend/modules/contact_form/action/action.class.php

<?php
 
/**
 * contact_form actions.
 *
 * @package    your-project
 * @subpackage contact_form
 * @author     Jacob Mather
 * @version    SVN: $Id: actions.class.php 23810 2009-11-12 11:07:44Z Kris.Wallsmith $
 */
class contact_formActions extends sfActions
{
 /**
  * Executes index action
  *
  * @param sfRequest $request A request object
  */
  public function executeIndex(sfWebRequest $request)
  {
    $this->form = new PublicContactForm();
    if ($request->hasParameter('contact_form'))
    {
      // we're getting the parameters for the form, let's bind and see what happens
      $this->form->bind($request->getParameter('contact_form'));
 
      if ($this->form->isValid())
      {
        $values = $this->form->getValues();
        $text_msg = 'You have received a message!'."\r\n\r\n";
        $html_msg = 'You have received a message!<br /><br />';
        foreach($values as $name => $value)
        {
          $text_msg .= $name.':'."\r\n".$value."\r\n\r\n";
          $html_msg .= $name.':<br />'.$value.'<br /><br />';
        }
        $text_msg .= 'Thanks!';
        $html_msg .= 'Thanks!';
 
        $from = array([email protected]' => 'My Application');
        $to = array([email protected]');
 
        $message = $this->getMailer()->compose($from, $to, 'Contact Form Submission');
        $message->setBody($html_msg, 'text/html');
        $message->addPart($text_msg, 'text/plain');
        $this->getMailer()->send($message);
 
        $this->getResponse()->setTitle('Thanks for contacting me');
        return 'Thanks';
      }
    }
    $this->getResponse()->setTitle('Contact Me');
    return 'Form';
  }
}

I think you’ll find the form pretty self explanatory, though I’ll cover a couple things.

The first thing to point out, I didn’t return sfView::SUCCESS, or sfView::ERROR. Why not? Simple. I wanted to keep the names of the files within the context of what they’re doing. We have two views we will use, the form, and the thank you page, so we will use two templates, indexForm.php, and indexThanks.php. I think that keeps things pretty obvious, how about you?

The next thing to point out, is we’re just using a single action! Why? Well, why do we need more than one action? We need the form object to display an error, and we can easily detect if someone has submitted the form (that is what $request->hasParameter(‘contact_form’) does), so why not just roll it into one compact bit.

Lastly, yes, I put a lot of effort into the outgoing emails. I always send an HTML and a plaintext part. It’s my personal preference, but it takes so little effort to deliver a professional solution even in a simple example, it feels sloppy not to.

5. The Templates

As I mentioned in the last section, we need two templates. indexForm.php and indexThanks.php.

apps/frontend/modules/contact_form/templates/indexForm.php

<style>
  .contact-form li { list-style: none; margin-bottom: 10px; }
  .error_list { padding: 0px; }
  .error_list li { background-color: red; color: white; font-weight: bold; padding: 3px;}
</style>
<?php echo $form->renderFormTag(url_for('contact_form/index')); ?>
<?php echo $form->renderGlobalErrors(); ?>
<?php echo $form->renderHiddenFields(); ?>
<ul class="contact-form form">
<?php foreach($form->getVisibleFields() as $name => $field): ?>
  <li>
    <?php echo $field->renderError(); ?>
    <?php echo $field->renderLabel(); ?>
    <div class="value"><?php echo $field->render(); ?>
    <span class="help"><?php echo $field->renderHelp(); ?></span></div>
  </li>
<?php endforeach; ?>
  <li><input type="submit" value="Submit Contact Form" /></li>
</ul>
</form>

apps/frontend/modules/contact_form/templates/indexThanks.php

<p>Thanks for contacting me!</p>

As you can see, it’s a pretty simple setup. I did some inline styles for the form, though in a larger project you would want those to be in a stylesheet include. The indexForm.php should serve you well as a base for further forms, as it includes support for a few things we don’t use in this form (help, global errors), which should help you make things ‘just work’ in the future.

6. Routing

Ok, so we’ve made everything, set it up, made it look visually nice enough to not want to stab our eyes out too much, now it’s time to put the icing on the cake: the route.

This is real simple. Open apps/frontend/config/routing.yml

contact_form:
  url: /contact-me
  param: { module: contact_form, action: index }

And you’re all set! Clear your cache, and you’re ready to go:

./symfony cc

Now navigate to your /contact-me page, and you will see your contact form!

Let’s try something a little new, and show a screencast of the finished product! Let me know how you like it!

5 Comments

  1. Anonymous

    […] […]

    Reply

  2. orit

    Hi
    Thanks for this.
    What if instead of renderFormTag(url_for(‘contact_form/index’)); ?>
    I want to put the routing itself? contact-me
    I did my own form (which also required an object) and when I click submit, it tells me:
    “Empty module and/or action after parsing the URL …”
    But the routing does work when I link TO the form with this route. Why doesn’t it work also for the submit action? 🙁

    Reply

    • Jacob Mather

      I’d have to see your form and routing configuration to help you more. There’s too many variables to say for sure.

      Reply

      • orit

        Thanks,
        I already did it. I just had to add sf_method: [get,post] to the routing rule which looks like this now:
        price_quote:
        url: /:id/:name_slug/ask-for-price-quote
        class: sfDoctrineRoute
        param: { module: priceQuote, action: index }
        options: { model: Tour, type: object }
        requirements:
        id: \d+
        sf_method: [get,post]

        Reply

  3. machau

    thanks dude!
    exectly what i needed.
    cheers

    Reply

Leave a Reply

*

twitter