SERVICE CONTAINER

In the Job prePersist method, we have hardcoded the number of days for the job to expire. It would have been better to make the 30 days configurable.

This is a spot where Symfony2 diverges significantly from Symfony1. In Symfony1, settings were available through a global sfConfig class. In Symfony2, settings and other dependencies are passed in through arguments. Dependency injection makes our classes less dependent on a global environment. Though, it also makes their construction more complicated; we need to know about and create all the dependencies before creating the objects we need. To solve this problem, Symfony2 provides a dependency injection container. A dependency injection container is an object that knows how to instantiate and configure objects.

Let's change the Job entity to accept an activeDays argument. Then in the dependency injection container, we'll define an active_days parameter and register a job factory service to create and configure job objects. Then any place where we had created a Job object, instead we'll use the job factory service to construct a Job object for us.
SERVICE CONTAINER CONT.

Now we're using a service to construct Job objects for us. But there's one caveat: Doctrine doesn't use our factory service, so the Job objects we fetch from the repository won't be configured. Fortunately, Doctrine packages a rich event system that fires events when almost anything happens. We can have Doctrine invoke our service whenever a certain action happens (e.g. postLoad).
REPOSITORY CLASSES

In order to isolate, test, and reuse database queries, it's a good idea to create a repository class for your entity and add methods with your query logic there.
QUERY BUILDER CONT.

Let's sort the jobs by the expiresAt field.
CATEGORIES ON THE HOMEPAGE

Until now, we haven't  taken the job category into account. We want the homepage to display jobs by category. First we need to get all categories with at least one active job. Let's create a CategoryRepository class for that query logic.
CATEGORIES ON THE HOMEPAGE CONT.

Let's change the job index action to get the categories, then in the template, we'll iterate through all categories and display the active jobs. For this to work, we need to add the getActiveJobsByCategory() method to the JobRepository class. Notice how we reuse the active jobs query builder.

This is a spot where Symfony2 diverges significantly from Symfony1. In Symfony1, we would execute database queries from an entity (e.g., $category->getActiveJobs()). In Symfony2, entities are meant to be unaware and independent of any persistence mechanism. To execute a query from within a template and in the context of a particular entity, we'll render the output from another controller action.
LIMIT THE RESULTS

For each category, let's make the list show only the first 10 jobs.
LIMIT THE RESULTS CONT.

The appropriate LIMIT clause is now hard-coded into the Model, but it is better for this value to be configurable. Let's set the value in the dependency injection container parameters and change the controller to pass the parameter to the model. For convenience, controllers can directly access the container.
FIXTURES CONT.

Unless you lower the max_jobs_on_homepage parameter to one, you won't see any difference. We need to add a bunch of jobs to the fixture.
PARAM CONVERTER CONT.

When a job expires, even if you know the URL, it must not be possible to access it anymore. Try the URL for the expired job:

jobeet.com.localhost/job/sensio-labs-paris-france-web-developer-expired

Instead of displaying the job, we need to forward the user to a 404 page. But how can we do this as the job is retrieved automatically by the param converter? Previously, we were able to omit the @ParamConverter annotation because we used type hinting. Let's add the @ParamConverter annotation so that we can set the repository_method option. Now, if you try to get an expired job, you will be forwarded to a 404 page.
CATEGORY PAGE

Now let's create the category page and add a link on the homepage to the category page. By now you should have enough knowledge to implement this all by yourself!

The next several commits shows a possible implementation.

First we generate the category controller and templates.
CATEGORY PAGE CONT.

Next we customize the category route to use a slug field, and we retrieve the category object using the ParamConverter.
CATEGORY PAGE CONT.

Next we change the job index action and template to count the number of jobs in each category, and we link to the category show page.
CATEGORY PAGE CONT.

Finally we customize the category show template and render the list of jobs. Notice how we shared the job list markup with the listByCategory template.
PAGINATION

We'd like to paginate the category jobs at 20 jobs per page. To paginate a list of Doctrine objects, we can use a popular bundle called KnpPaginatorBundle. Now in the category action, instead of passing the job objects to the show template, we'll pass a pagination object.
FORMS

Most of the time, a form has to be serialized to the database. Since Symfony already knows everything about your database model, it can automatically generate forms based on this information. In fact, when we generated CRUD code, form classes were automatically created as well.

The job form is a perfect example to learn to customize forms. By default, the 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. Removing fields from a form is as simple as removing them from the JobType class.
FORMS CONT.

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. The goal of validation is to tell you whether or not the data of an object is valid. For this to work, we'll configure a list of rules (called constraints) that the object must follow in order to be valid.
FORMS CONT.

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. We also want to render this field as a choice widget.
FORMS CONT.

The logoPath column will store the filename of the logo associated with the job. We need to validate that the uploaded file is an image in a web format. We'll also need to change the widget to a file input. Though, we won't need to do this last part explicitly. Symfony can "guess" the type of your field from the validation rules.
FORMS CONT.

When the form is submitted, the logoPath field will be an instance of UploadedFile. It can be used to move the logo file to a permanent location. But we want the logoPath property to store the relative path to the file. To handle the actual file upload in the form, we're going to use a new logoFile field.

We can integrate the file upload into the entity lifecycle (creation, update, and removal) so that the database operation and the moving of the file become atomic. If there is a problem persisting the entity or if the file cannot be moved, then nothing should happen.

The class now does everything we need: It generates a unique filename before persisting, and it moves the file after persisting.