Symfony 2.8 Jobeet Day 10: the Forms

Any website has forms, from the simple contact form to the complex ones with lots 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…

In Day 3 of this tutorial we used the doctrine:generate:crud command to generate a simple CRUD controller for out Job entity. This also generated a Job form that you can find in /src/AppBundle/Form/JobType.php file.

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 (app/Resources/views/base.html.twig):

Then, change the job_show route parameters in createAction of the JobController to match the new route we created in Day 5 of this tutorial:

By default, the Doctrine generated form displays fields for all the table columns. But for the Job form, some of them must not be editable by the end user. Edit the Job form as you see below:

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. We already talked about validation in Day 3 of this tutorial and we know that, in Symfony 2, validation is applied to the underlying object (e.g. Job). In other words, the question isn’t whether the “form” is valid, but whether or not the Job object is valid after the form has applied the submitted data to it. Using annotations, we will add an email validation constraint to our model:

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:

For this to work, add the following methods in the Job entity:

The getTypes method is used in the form to get the possible types for a Job and getTypeValues will be used in the validation to get the valid values for the type field:

For each field, symfony automatically generates a label (which will be used in the rendered <label> tag). This can be changed with the label option. Also, Symfony adds a required HTML attribute to all the fields that defaults to true. We need to change this for the logo and url fields:

In the end, remove the NotBlank() constraints from the logo, url, expiresAt, createdAt and updatedAt fields that we added back in Day 3:

Handling File Uploads in Symfony 2

To handle the actual file upload in the form, we will need to change the logo field type to file in the form:

->add('logo', 'file', array('required' => false, 'label' => 'Company logo'))

Also we need to add an Image constraint to the logo property of the Job entity:

/**
* @ORM\Column(type="string", length=255, nullable=true)
* @Assert\Image()
*/
private $logo;

When the form is submitted, the logo field will be an instance of UploadedFile. It can be used to move the file to a permanent location. After this we will set the job logo property to the uploaded file name.

For this to work we need to add a new parameter, jobs_directory, in our config file:

jobs_directory: '%kernel.root_dir%/../web/uploads/jobs'

Even if this implementation works, let’s do this in a better way, using a Symfony service and Doctrine lifecycle callbacks.

To create a Symfony service, first create a new FileUploader class in src/AppBundle/Utils folder:

Then, define a service for this class in app/config/services.yml:

Now create a Doctrine listener to automatically upload the file when persisting the entity (src/AppBundle/EventListener/JobUploadListener.php):

Next, register this class as a Doctrine listener (app/config/services.yml):

This listener is now automatically executed when persisting a new Job entity. This way, you can remove everything related to uploading from the controller.

The Form Template

Now that the form class has been customized, we need to display it. Open the new.html.twig template and edit it:

We could render the form by just using the following line of code, but as we need more customization, we choose to render each form field by hand.

{{ form_widget(form) }}

By printing form_widget(form), each field in the form is rendered, along with a label and error message (if there is one). As easy as this is, it’s not very flexible. Usually, you’ll want to render each form field individually so you can control how the form looks.

We also used a technique named form theming to customize how the form errors will be rendered. You can read more about this in the official Symfony documentation.

Do the same thing with the edit.html.twig template:

We also need to make a small addition to the editAction because we changed the logo input type from text to file earlier and the form now expects a File type instead of the text that is stored by the logo property of the Job:

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 four methods in the JobController:

  • newAction: Displays a blank form to create a new job, processes the submitted form (validation, form repopulation) and creates a new job with the user submitted values
  • editAction: Displays a form to edit an existing job, processes the submitted form (validation, form repopulation) and updates an existing job with the user submitted values

When you browse to the /job/new page, a form instance for a new job object is created by calling the createForm method and passed to the template (new.html.twig). When the user submits the form, the form is bound (handleRequest() method) with the user submitted values and the validation is triggered.

Once the form is submitted, 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 ($em->persist($job)), and the user is redirected to the job preview page; if not, the new.html.twig template is displayed again with the user submitted values and the associated 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 second argument of the createForm method. This object will be used for default widget values in the template.

You can also define default values for the creation form. For this we will pass a pre-modified Job object to the createForm method to set the type default value to full-time:

Protecting the Job Form with a Token

Everything must work fine by now. As of now, the user must enter the token for the job. But the job token must be generated automatically when a new job is created, as we don’t want to rely on the user to provide a unique token. Create a new setTokenValue() method of the Job entity to add the logic that generates the token before a new job is saved:

You can now remove the token field from the form by deleting the add(‘token’) line and the NotBlank token constraint from the Job entity. Also remove it from the new.html.twig and edit.html.twig templates.

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.

Let’s change the routes so you can edit or delete a job only if you now the secret token. Also change the url generation for the job_delete route to use the token instead of the id when creating the delete form:

In the job show and edit templates (show.html.twig and edit.html.twig), change the job_edit route parameter:

{{ path('job_edit', { 'token': job.token }) }}

Now, all routes related to the jobs, except the job_show one, embed the token. For instance, the route to edit a job is now of the following pattern:

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

The Preview Page

The preview page is the same as the job page display. The only difference is that the job preview page will be accessed using the job token instead of the job id:

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

Then, create the admin.html.twig template:

There is a lot of code, but most of the code is simple to understand.

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

The admin bar displays the different actions depending on the job status:

We now need redirect the new and edit actions of the JobController to the new preview page:

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:

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

We also need to change the preview action to send the publish form to the template:

We can now change the link of the “Publish” link (we will use a form here, like when deleting a job, so we will have a POST request):

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. We need to edit the JobRepository methods to add this requirement:

The same for CategoryRepository getWithJobs() method:

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 token URL. In that case, the job preview will show up with the admin bar.

As always, find the code from day 10 here: https://github.com/dragosholban/jobeet-sf2.8/tree/day10.


About the Author

Passionate about web & mobile development. Doing this at IntelligentBee for the last 5 years. If you are looking for custom web and mobile app development, please contact us here and let’s have a talk.