Archives


Master Symfony2 fundamentals

Be trained by SensioLabs experts (2 to 6 day sessions -- French or English).
trainings.sensiolabs.com

Discover the SensioLabs Support

Access to the SensioLabs Competency Center for an exclusive and tailor-made support on Symfony
sensiolabs.com

Fabien Potencier
Jobeet - Day 10: The Forms
by Fabien Potencier – December 10, 2008 – 17 comments

This post was published as part of the symfony 2008 advent calendar. As this tutorial might have been updated since then, you are advised to read the last version from the symfony 1.2 documentation (for Propel or Doctrine).

Previously on Jobeet

The second week of Jobeet got off to a flying start with the introduction of the symfony test framework. We will continue today with the form framework.

The Form Framework

Any website has forms; from the simple contact form to the complex ones with tens of fields. Writing forms is also one of the most complex and tedious task for a web developer: you need to write the HTML form, implement validation rules for each field, process the values to store them in a database, display error messages, repopulate fields in case of errors, and much more...

Of course, instead of reinventing the wheel over and over again, symfony provides a framework to ease form management. The form framework is made of three parts:

  • validation: The validation sub-framework provides classes to validate inputs (integer, string, email address, ...)
  • widgets: The widget sub-framework provides classes to output HTML fields (input, textarea, select, ...)
  • forms: The form classes represent forms made of widgets and validators and provide methods to help manage the form. Each form field has its own validator and widget.

Forms

A symfony form is a class made of fields. Each field has a name, a validator, and a widget. A simple ContactForm can be defined with the following class:

class ContactForm extends sfForm
{
  public function configure()
  {
    $this->setWidgets(array(
      'email'   => new sfWidgetFormInput(),
      'message' => new sfWidgetFormTextarea(),
    ));
 
    $this->setValidators(array(
      'email'   => new sfValidatorEmail(),
      'message' => new sfValidatorString(array('max_length' => 255)),
    ));
  }
}
 

Form fields are configured in the configure() method, by using the setValidators() and setWidgets() methods.

The form framework comes bundled with a lot of widgets and validators. The API describes them quite extensively with all the options, errors, and default error messages.

The widget and validator class names are quite explicit: the email field will be rendered as an HTML <input> tag (sfWidgetFormInput) and validated as an email address (sfValidatorEmail). The message field will be rendered as a <textarea> tag (sfWidgetFormTextarea), and must be a string of no more than 255 characters (sfValidatorString).

By default all fields are required, as the default value for the required option is true. So, the validation definition for email is equivalent to new sfValidatorEmail(array('required' => true)).

You can merge a form in another one by using the mergeForm() method, or embed one by using the embedForm() method:

$this->mergeForm(new AnotherForm());
$this->embedForm('name', new AnotherForm());
 

Propel Forms

Most of the time, a form has to be serialized to the database. As symfony already knows everything about your database model, it can automatically generate forms based on this information. In fact, when you launched the propel:build-all task during day 3, symfony automatically called the propel:build-forms task:

$ php symfony propel:build-forms

The propel:build-forms task generates form classes in the lib/form/ directory. The organization of these generated files is similar to that of lib/model. Each model class has a related form class (for instance JobeetJob has JobeetJobForm), which is empty by default as it inherits from a base class:

// lib/form/JobeetJobForm.class.php
class JobeetJobForm extends BaseJobeetJobForm
{
  public function configure()
  {
  }
}
 

By browsing the generated files under the lib/form/base/ sub-directory, you will see a lot of great usage examples of symfony built-in widgets and validators.

Customizing the Job Form

The job form is a perfect example to learn form customization. Let's see how to customize it, step by step.

First, change the "Post a Job" link in the layout to be able to check changes directly in your browser:

<!-- apps/frontend/templates/layout.php -->
<a href="<?php echo url_for('@job_new') ?>">Post a Job</a>
 

By default, a Propel form displays fields for all the table columns. But for the job form, some of them must not be editable by the end user. Removing fields from a form is as simple as unsetting them:

// lib/form/JobeetJobForm.class.php
class JobeetJobForm extends BaseJobeetJobForm
{
  public function configure()
  {
    unset(
      $this['created_at'], $this['updated_at'],
      $this['expires_at'], $this['is_activated']
    );
  }
}
 

Unsetting a field means that both the field widget and validator are removed.

The form configuration must sometimes be more precise than what can be introspected from the database schema. For example, the email column is a varchar in the schema, but we need this column to be validated as an email. Let's change the default sfValidatorString to a sfValidatorEmail:

// lib/form/JobeetJobForm.class.php
public function configure()
{
  // ...
 
  $this->validatorSchema['email'] = new sfValidatorEmail();
}
 

Even if the type column is also a varchar in the schema, we want its value to be restricted to a list of choices: full time, part time, or freelance.

First, let's define the possible values in JobeetJobPeer:

// lib/model/JobeetJobPeer.php
class JobeetJobPeer extends BaseJobeetJobPeer
{
  static public $types = array(
    'full-time' => 'Full time',
    'part-time' => 'Part time',
    'freelance' => 'Freelance',
  );
 
  // ...
}
 

Then, use sfWidgetFormChoice for the type widget:

$this->widgetSchema['type'] = new sfWidgetFormChoice(array(
  'choices'  => JobeetJobPeer::$types,
  'expanded' => true,
));
 

sfWidgetFormChoice represents a choice widget which can be rendered by a different widget according to some configuration options (expanded and multiple):

  • Dropdown list (<select>): array('multiple' => false, 'expanded' => false)
  • Dropdown box (<select multiple="multiple">): array('multiple' => true, 'expanded' => false)
  • List of radio buttons: array('multiple' => false, 'expanded' => true)
  • List of checkboxes: array('multiple' => true, 'expanded' => true)

If you want one of the radio button to be selected by default (full-time for instance), you can change the default value in the database schema.

Even if you think nobody can submit a non-valid value, a hacker can easily bypass the widget choices by using tools like curl or the Firefox Web Developer Toolbar. Let's change the validator to restrict the possible choices:

$this->validatorSchema['type'] = new sfValidatorChoice(array(
  'choices' => array_keys(JobeetJobPeer::$types),
));
 

As the logo column will store the filename of the logo associated with the job, we need to change the widget to a file input tag:

$this->widgetSchema['logo'] = new sfWidgetFormInputFile(array(
  'label' => 'Company logo',
));
 

For each field, symfony automatically generate a label (which will be used in the rendered <label> tag). This can be changed with the label option.

You can also change labels in a batch with the setLabels() method of the widget array:

$this->widgetSchema->setLabels(array(
  'category_id'    => 'Category',
  'is_public'      => 'Public?',
  'how_to_apply'   => 'How to apply?',
));
 

We also need to change the default validator:

$this->validatorSchema['logo'] = new sfValidatorFile(array(
  'required'   => false,
  'path'       => sfConfig::get('sf_upload_dir').'/jobs',
  'mime_types' => 'web_images',
));
 

sfValidatorFile is quite interesting as it does a number of things:

  • Validates that the uploaded file is an image in a web format (mime_types)
  • Renames the file to something unique
  • Stores the file in the given path
  • Updates the logo column with the generated name

You need to create the logo directory (web/uploads/jobs) and check that it is writable by the web server.

As the validator will save the relative path in the database, change the path used in the showSuccess template:

// apps/frontend/modules/job/template/showSuccess.php
<img src="/uploads/jobs/<?php echo $job->getLogo() ?>" alt="<?php echo $job->getCompany() ?> logo" />
 

If a generateLogoFilename() method exists in the form, it will be called by the validator and the result will override the default generated logo filename. The method is given the sfValidatedFile object as an argument.

Just as you can override the generated label of any field, you can also define a help message. Let's add one for the is_public column to better explain its significance:

$this->widgetSchema->setHelp('is_public', 'Whether the job can also be published on affiliate websites or not.');
 

The final JobeetJobForm class read as follow:

// lib/form/JobeetJobForm.class.php
class JobeetJobForm extends BaseJobeetJobForm
{
  public function configure()
  {
    unset(
      $this['created_at'], $this['updated_at'],
      $this['expires_at'], $this['is_activated']
    );
 
    $this->validatorSchema['email'] = new sfValidatorEmail();
 
    $this->widgetSchema['type'] = new sfWidgetFormChoice(array(
      'choices' => JobeetJobPeer::$types,
      'expanded' => true,
    ));
    $this->validatorSchema['type'] = new sfValidatorChoice(array(
      'choices' => array_keys(JobeetJobPeer::$types),
    ));
 
    $this->widgetSchema['logo'] = new sfWidgetFormInputFile(array(
      'label' => 'Company logo',
    ));
    $this->validatorSchema['logo'] = new sfValidatorFile(array(
      'required'   => false,
      'path'       => sfConfig::get('sf_upload_dir').'/jobs',
      'mime_types' => 'web_images',
    ));
 
    $this->widgetSchema->setHelp('is_public', 'Whether the job can also be published on affiliate websites or not.');
  }
}
 

The Form Template

Now that the form class has been customized, we need to display it. The template for the form is the same whether you want to create a new job or edit an existing one. In fact, both newSuccess.php and editSuccess.php templates are quite similar:

<!-- apps/frontend/modules/job/templates/newSuccess.php -->
<?php use_stylesheet('job.css') ?>
 
<h1>Post a Job</h1>
 
<?php include_partial('form', array('form' => $form)) ?>
 

The form itself is rendered in the _form partial. Replace the content of the generated _form partial with the following code:

<!-- apps/frontend/modules/job/templates/_form.php -->
<?php include_stylesheets_for_form($form) ?>
<?php include_javascripts_for_form($form) ?>
 
<?php echo form_tag_for($form, '@job') ?>
  <table id="job_form">
    <tfoot>
      <tr>
        <td colspan="2">
          <input type="submit" value="Preview your job" />
        </td>
      </tr>
    </tfoot>
    <tbody>
      <?php echo $form ?>
    </tbody>
  </table>
</form>
 

The include_javascripts_for_form() and include_stylesheets_for_form() helpers include JavaScript and stylesheet dependencies needed for the form widgets.

Even if the job form does not need any JavaScript or stylesheet file, it is a good habit to keep these helper calls "just in case". It can save your day later if you decide to change a widget that needs some JavaScript or a specific stylesheet.

The form_tag_for() helper generate a <form> tag for the given form and route and changes the HTTP methods to POST or PUT depending on whether the object is new or not. It also takes care of the multipart attribute if the form has any file input tags.

Eventually, the <?php echo $form ?> renders the form widgets.

The Form Action

We now have a form class and a template that renders it. Now, it's time to actually make it work with some actions.

The job form is managed by five methods in the job module:

  • new: Displays a blank form to create a new job
  • edit: Displays a form to edit an existing job
  • create: Creates a new job with the user submitted values
  • update: Updates an existing job with the user submitted values
  • processForm: Called by create and update, it processes the form (validation, form repopulation, and serialization to the database)

All forms have the following life-cycle:

Form flow

As we have created a Propel route collection 5 days ago for the job module, we can simplify the code for the form management methods:

// apps/frontend/modules/job/actions/actions.class.php
public function executeNew(sfWebRequest $request)
{
  $this->form = new JobeetJobForm();
}
 
public function executeCreate(sfWebRequest $request)
{
  $this->form = new JobeetJobForm();
  $this->processForm($request, $this->form);
  $this->setTemplate('new');
}
 
public function executeEdit(sfWebRequest $request)
{
  $this->form = new JobeetJobForm($this->getRoute()->getObject());
}
 
public function executeUpdate(sfWebRequest $request)
{
  $this->form = new JobeetJobForm($this->getRoute()->getObject());
  $this->processForm($request, $this->form);
  $this->setTemplate('edit');
}
 
protected function processForm(sfWebRequest $request, sfForm $form)
{
  $form->bind(
    $request->getParameter($form->getName()),
    $request->getFiles($form->getName())
  );
 
  if ($form->isValid())
  {
    $job = $form->save();
 
    $this->redirect($this->generateUrl('job_show', $job));
  }
}
 

When you browse to the /job/new page, a new form instance is created and passed to the template (new action).

When the user submits the form (create action), it is bound (bind() method) with the user submitted values and the validation is triggered.

Once the form is bound, it is possible to check its validity using the isValid() method: If the form is valid (returns true), the job is saved to the database ($form->save()), and the user is redirected to the job preview page; if not, the newSuccess.php template is displayed again with the user submitted values and error messages.

The setTemplate() method changes the template used for a given action. If the submitted form is not valid, the create and update methods use the same template as the new and edit action respectively to re-display the form with error messages.

The modification of an existing job is quite similar. The only difference between the new and the edit action is that the job object to be modified is passed as the first argument of the form constructor. This object will be used for default widget values in the template (default values are an object for Propel forms, but a plain array for simple forms).

You can also define default values for the creation form. One way is to declare the values in the database schema. Another one is to pass a pre-modified Job object to the form constructor:

// apps/frontend/modules/job/actions/actions.class.php
public function executeNew(sfWebRequest $request)
{
  $job = new JobeetJob();
  $job->setType('full-time');
 
  $this->form = new JobeetJobForm($job);
}
 

When the form is bound, the default values are replaced with the user submitted ones. The use submitted values will be used for form repopulation when the form is redisplayed in case of validation errors.

Protecting the Job Form with a Token

Everything must work fine by now. But there is a problem. First, the job token must be generated automatically when a new job is created, as we don't want the user to provide a unique token. Update the save() method of JobeetJob:

// lib/model/JobeetJob.php
public function save(PropelPDO $con = null)
{
  // ...
 
  if (!$this->getToken())
  {
    $this->setToken(sha1($this->getEmail().rand(11111, 99999)));
  }
 
  return parent::save($con);
}
 

We can remove the token field from the form:

// lib/form/JobeetJobForm.class.php
class JobeetJobForm extends BaseJobeetJobForm
{
  public function configure()
  {
    unset(
      $this['created_at'], $this['updated_at'],
      $this['expires_at'], $this['is_activated'],
      $this['token']
    );
 
    // ...
  }
 
  // ...
}
 

If you remember the user stories from day 2, a job can be edited only if the user knows the associated token. Right now, it is pretty easy to edit or delete any job, just by guessing the URL. That's because the edit URL is like /job/ID/edit, where ID is the primary key of the job.

By default, a sfPropelRouteCollection route generates URLs with the primary key, but it can be changed to any unique column by passing the column option:

# apps/frontend/config/routing.yml
job:
  class:        sfPropelRouteCollection
  options:      { model: JobeetJob, column: token }
  requirements: { token: \w+ }
 

Now, all routes, except the job_show_user one, embed the token. For instance, the route to edit a job is now:

http://jobeet.localhost/job/TOKEN/edit

You will also need to change the "Edit" link in the showSuccess template:

<!-- apps/frontend/modules/job/templates/showSuccess.php -->
<a href="<?php echo url_for('job_edit', $job) ?>">Edit</a>
 

We have also changed the requirements for the token column as the symfony default requirements is \d+ for the primary key.

The Preview Page

The preview page is the same as the job page display. Thanks to the routing, if the user comes with the right token, it will be accessible in the token request parameter.

If the user comes in with the tokenized URL, we will add an admin bar at the top. At the beginning of the showSuccess template, add a partial to host the admin bar and remove the edit link at the bottom:

<!-- apps/frontend/modules/job/templates/showSuccess.php -->
<?php if ($sf_request->getParameter('token') == $job->getToken()): ?>
  <?php include_partial('job/admin', array('job' => $job)) ?>
<?php endif; ?>
 

Then, create the _admin partial:

<!-- apps/frontend/modules/job/templates/_admin.php -->
<div id="job_actions">
  <h3>Admin</h3>
  <ul>
    <?php if (!$job->getIsActivated()): ?>
      <li><?php echo link_to('Edit', 'job_edit', $job) ?></li>
      <li><?php echo link_to('Publish', 'job_edit', $job) ?></li>
    <?php endif; ?>
    <li><?php echo link_to('Delete', 'job_delete', $job, array('method' => 'delete', 'confirm' => 'Are you sure?')) ?></li>
    <?php if ($job->getIsActivated()): ?>
      <li<?php $job->expiresSoon() and print ' class=" expires_soon"' ?>>
        <?php if ($job->isExpired()): ?>
          Expired
        <?php else: ?>
          Expires in <strong><?php echo $job->getDaysBeforeExpires() ?></strong> days
        <?php endif; ?>
 
        <?php if ($job->expiresSoon()): ?>
         - <a href="">Extend</a> for another <?php echo sfConfig::get('app_active_days') ?> days
        <?php endif; ?>
      </li>
    <?php else: ?>
      <li>
        [Bookmark this <?php echo link_to('URL', 'job_show', $job, true) ?> to manage this job in the future.]
      </li>
    <?php endif; ?>
  </ul>
</div>
 

There is a lot of code, but most of the code is simple to understand. The admin bar is different, depending on the job status:

Not activated job

Activated job

To make the template more readable, we have added a bunch of shortcut methods in the JobeetJob class:

// lib/model/JobeetJob.php
public function getTypeName()
{
  return $this->getType() ? JobeetJobPeer::$types[$this->getType()] : '';
}
 
public function isExpired()
{
  return $this->getDaysBeforeExpires() < 0;
}
 
public function expiresSoon()
{
  return $this->getDaysBeforeExpires() < 5;
}
 
public function getDaysBeforeExpires()
{
  return floor(($this->getExpiresAt('U') - time()) / 86400);
}
 

Job Activation and Publication

In the previous section, there is a link to publish the job. The link needs to be changed to point to a new publish action. Instead of creating a new route, we can just configure the existing job route:

# apps/frontend/config/routing.yml
job:
  class:   sfPropelRouteCollection
  options:
    model:          JobeetJob
    column:         token
    object_actions: { publish: put }
  requirements:
    token: \w+
 

The object_actions takes an array of additional actions for the given object. We can now change the link of the "Publish" link:

<!-- apps/frontend/modules/job/templates/_admin.php -->
<li>
  <?php echo link_to('Publish', 'job_publish', $job, array('method' => 'put')) ?>
</li>
 

The last step is to create the publish action:

// apps/frontend/modules/job/actions/actions.class.php
public function executePublish(sfWebRequest $request)
{
  $request->checkCSRFProtection();
 
  $job = $this->getRoute()->getObject();
  $job->publish();
 
  $this->getUser()->setFlash('notice', sprintf('Your job is now online for %s days.', sfConfig::get('app_active_days')));
 
  $this->redirect($this->generateUrl('job_show_user', $job));
}
 

As we have enabled the CSRF protection, the link_to() helper embeds a CSRF token in the link and the checkCSRFProtection() method of the request object checks the validity of it on submision.

The executePublish() method uses a new publish() method that can be defined as follows:

// lib/model/JobeetJob.php
public function publish()
{
  $this->setIsActivated(true);
  $this->save();
}
 

You can now test the new publish feature in your browser.

But we still have something to fix. The non-activated jobs must not be accessible, which means that they must not show up on the Jobeet homepage, and must not be accessible by their URL. As we have created an addActiveJobsCriteria() method to restrict a Criteria to active jobs, we can just edit it and add the new requirements at the end:

// lib/model/JobeetJobPeer.php
static public function addActiveJobsCriteria(Criteria $criteria = null)
{
  // ...
 
  $criteria->add(self::IS_ACTIVATED, true);
 
  return $criteria;
}
 

That's all. You can test it now in your browser. All non-activated jobs have disappeared from the homepage; even if you know their URLs, they are not accessible anymore. They are, however, accessible if one knows the job's tokent URL. In that case, the job preview will show up with the admin bar.

That's one of the great advantage of the MVC pattern and the refactorisation we have done along the way. Only a single change in one method was needed to add the new requirement.

When we created the getWithJobs() method, we forgot to use the addActiveJobsCriteria() method. So, we need to edit it and add the new requirement:

class JobeetCategoryPeer extends BaseJobeetCategoryPeer
{
  static public function getWithJobs()
  {
    // ...
 
    $criteria->add(JobeetJobPeer::IS_ACTIVATED, true);
 
    return $criteria;
  }
 

See you Tomorrow

Today's tutorial was packed with a lot of new information, but hopefully you now have a better understanding of symfony's form framework.

I know that some of you noticed that we forgot something today... We have not implemented any test for the new features. Because writing tests is an important part of developing an application, this is the first thing we will do tomorrow.

Comments RSS

  • gravatar
    #1 Harro said on the 2008/12/10 at 15:32
    I justed wanted to verify that what I did so far was ok (it works, but just wanted to do a diff) but the svn server doesn't seem accessable without username and password. If I change to https I come into the normal symfony svn.
  • gravatar
    #2 Sha1dy said on the 2008/12/10 at 15:34
    Please somebody update the links on the Jobeet page because currently link to this day isn't there
  • gravatar
    #3 Markus.Staab said on the 2008/12/10 at 15:52
    very nice!

    little typo in the template:
    class=" expires_soon"

    there is a preceding whitespace inside the class attribute
  • gravatar
    #4 Gianko said on the 2008/12/10 at 17:44
    A couple of question:

    1. in the JobeetCategoryPeer.php I kept the "return self::doSelect($criteria);" instead of the "return $criteria;" otherwise it gives me a Fatal Error.


    2.- after the Publish but before the Show it gives me this Error for like a second

    "Warning: Cannot modify header information - headers already sent by (output started at C:\dev\sf\jobeet\apps\frontend\lib\Jobeet.class.php:1) in C:\dev\sf\jobeet\lib\vendor\symfony\lib\response\sfWebResponse.class.php on line 335

    Warning: Cannot modify header information - headers already sent by (output started at C:\dev\sf\jobeet\apps\frontend\lib\Jobeet.class.php:1) in C:\dev\sf\jobeet\lib\vendor\symfony\lib\response\sfWebResponse.class.php on line 349

    Warning: Cannot modify header information - headers already sent by (output started at C:\dev\sf\jobeet\apps\frontend\lib\Jobeet.class.php:1) in C:\dev\sf\jobeet\lib\vendor\symfony\lib\response\sfWebResponse.class.php on line 349"

    any helps?
  • gravatar
    #5 lking said on the 2008/12/10 at 18:06
    2 Gianko
    about 1-st i left doSelect too.
    about 2-nd take look to line 1 of
    C:\dev\sf\jobeet\apps\frontend\lib\Jobeet.class.php
    looks like here left spaces or other chars before tag <?php
  • gravatar
    #6 lking said on the 2008/12/10 at 18:13
    2 Gianko

    actually strange path for Jobeet class.
    expected that it will be in the
    C:\dev\sf\jobeet\lib\Jobeet.class.php
    not in the app libs, as this class probably will be used on backend too.
  • gravatar
    #7 Ujoux said on the 2008/12/10 at 18:25
    Doctrine version is missing, right ?
  • gravatar
    #8 Gianko said on the 2008/12/10 at 19:03
    @Iking:

    it seems that i have that file duplicated, but if i delete the C:\dev\sf\jobeet\apps\frontend\lib\Jobeet.class.php
    it throws me a fatal error.

    well... i'm lost now :(
  • gravatar
    #9 Harro said on the 2008/12/10 at 22:24
    @Gianko: Have you tried a 'symfony cc' after deleting the extra one?
  • gravatar
    #10 mad said on the 2008/12/11 at 00:08
    god...i actually now think that making a form with plain html and some php it's much simpler...i;m really happy it;s just a subframework and i can just not use it
  • gravatar
    #11 Gianko said on the 2008/12/11 at 02:35
    yeah! that was it... i forget the CC

    now everythng is Ok.

    thanks @Iking and @Harro

    This tutorial Rocks!
  • gravatar
    #12 Harro said on the 2008/12/11 at 09:02
    @Mad: For simple websites it's a bit overkill, but for big projects with over 200 forms, 2 designers and 5 programmers all working on them And then customer calling after 6 months wanting something changed that requires a whole different set of validators in a form someone else made. Then you'll be happy that you have the forms framework.

  • gravatar
    #13 Piwaï said on the 2008/12/11 at 10:55
    This was a lot of information at once, but it is still OK. 2 remarks :

    1) Some of you might want to use <?php echo '</form>' ?> instead of writing directly </form>, in order to avoid validation problems in your IDE (PHPEclipse for instance checks HTML and I hate warnings :P).

    2) Humm.. Not sure but, with this way of writing forms, it seems that the framework has all the informations needed to also provide client-side input validators (ie Javascript validation). Which would of course be great and give a lovely feeling to the users (if provided with graceful degradation...). Is there anything in symfony 1.2 that enables it without writing a lot more code ? Any plugin maybe ?
  • gravatar
    #14 COil said on the 2008/12/11 at 13:45
    I like the sfPropelRouteCollection and the concept of admin bar when accessing the object with its token. :)
  • gravatar
    #15 Lynx said on the 2008/12/11 at 19:27
    Does this limit you to having your forms in a table instead of div statements and using CSS to style them? Also what about attaching javascripts to certain fields. For example on my symfony 1.0 site, I am using auto complete in several fields, and others I create more fields based on the fact if all the fields are in use and another field is needed.
  • gravatar
    #16 Francisco Vacas said on the 2008/12/12 at 16:27
    This is an awesome tutorial. Keep up the good work.

    For those who want their delete function to work i've changed following things:

    # apps/frontend/config/routing.yml
    job:
    class: sfPropelRouteCollection
    options:
    model: JobeetJob
    column: token
    object_actions: { publish: put, delete: delete }
    requirements:
    token: \w+

    an then in the actions.class.php (executeDelete)

    $request->checkCSRFProtection();
    $job = $this->getRoute()->getObject();
    $job->delete();
    $this->getUser()->setFlash('notice', sprintf('Your job has been deleted.'));
    $this->redirect('job/index');

    and then override the method delete from the BaseJobeetJob in JobeetJob

    $logo = sfConfig::get('sf_upload_dir').'/jobs'.$this->getLogo();
    unlink($logo);
    return parent::delete($con);

  • gravatar
    #17 lukasz said on the 2008/12/12 at 17:34
    What about Doctrine version of this article? Why Doctrine version is so out of date?