INTRODUCTION

This project aims to adapt Symfony1's Jobeet tutorial for Symfony2. You can read the original Jobeet tutorial at <symfony.com/legacy/doc/jobeet?orm=Doctrine>.

PREREQUISITES

Symfony uses a dependency manager called Composer. We'll use Composer to download Symfony itself plus any third-party libraries Symfony depends on. Get Composer at <getcomposer.org/download>.

Many of the libraries are available only through Git repositories. We'll need Git to pull down copies of those libraries. Get Git at <git-scm.com/downloads>.

SYMFONY INSTALLATION

Open the console and run:

php /path/to/composer.phar create-project symfony/framework-standard-edition /path/to/project 2.2.0

After installation, change your console's working directory to your new project. All further console commands shown here will assume this.
WEB SERVER CONFIGURATION

<VirtualHost *:80>
    ServerName jobeet.com.localhost
    # Declare the domain name in your
    # hosts file with the following line:
    # 127.0.0.1 jobeet.com.localhost

    DocumentRoot "/path/to/project/web"
    <Directory "/path/to/project/web">
        AllowOverride All
        Require all granted
    </Directory>

    ErrorLog "/path/to/project/error.log"
    CustomLog "/path/to/project/access.log" common
</VirtualHost>

PHP CONFIGURATION

Use the following URL to check your configuration:

http://jobeet.com.localhost/config.php

If there are any issues, correct them now before moving on.

SYMFONY CONFIGURATION

Click the link to "Configure your Symfony Application online." In step 1, enter your database connection info and credentials. In step 2, the randomly generated default secret can be left as is.
BUNDLE GENERATION

A bundle is a directory that houses everything related to a specific feature, including PHP classes, configuration, and even CSS and JS files.

To generate a typical bundle directory structure with sample code, run the command:

php app/console generate:bundle

Bundle namespace: Jobeet/JobBoardBundle
Bundle name: [default]
Target directory: [default]
Configuration format: annotation
Generate whole directory structure: [default]
Confirm: [default]
Confirm update Kernal: [default]
Confirm update Routing: [default]
DOCTRINE EXTENSIONS

Before we create the schema, we're going to install the DoctrineExtensionsBundle. This gives us common behaviors such as Sluggable, Timestampable, Sortable, Softdeleteable, and others. Edit composer.json as shown in this commit, then run the command:

php /path/to/composer.phar update

Other dependencies may also update themselves during this step.
DOCTRINE EXTENSIONS CONT.

A Symfony application won't load a bundle just because it exists on the filesystem. Our app will load only those bundles that we tell it to load, so we need to tell it to load the DoctrineExtensionsBundle.
DOCTRINE EXTENSIONS CONT.

DoctrineExtensions is configurable, and it will enable only those behaviors that we explicitly ask for. For now, we're only going to use Timestampable and Sluggable.
ENTITIES & ANNOTATIONS

Creating the schema happens in two parts: First, creating the entities, which are just simple classes. And second, telling the ORM how to persist those entities. There are several ways we can tell Doctrine how to persist entities. We can use plain PHP, YAML, XML, or docblock annotations. Here we use docblock annotations since it seems to be the prevailing choice.

With the entities and annotations in place, we can generate the SQL to sync our database schema with the persistence mapping using the command:

php app/console doctrine:schema:update --dump-sql

And we can execute that SQL using the command:

php app/console doctrine:schema:update --force
GETTERS & SETTERS

As a convenience, Doctrine can generate getters and setters for us. (Doctrine uses the Reflection API to access data from entities, so we don’t have to take Doctrine into account when designing access to our objects.)

We can generate getters and setters for entities in our bundle using the command:

php app/console doctrine:generate:entities JobeetJobBoardBundle
GETTERS & SETTERS CONT.

English is inconsistent, and the getter/setter generator isn't perfect. Let's clean the generated code a bit.
FIXTURES

Next, we want to fill our database with dummy data for development, called fixtures. We'll want to install the DoctrineFixturesBundle. Edit composer.json as shown in this commit, then run the composer update command like we did for the DoctrineExtensionsBundle.

Also like we did for the DoctrineExtensionsBundle, we need to explicitly ask our application to load this bundle. But notice this time that we load the bundle within an if block. That means we're loading this bundle only in our dev and test environments. In the production environment, this bundle won't be loaded.
FIXTURES CONT.

Follow the example in this commit to create objects and persist them to the database. When that's done, run the command:

php app/console doctrine:fixtures:load
CRUD

Much of the code to create, retrieve, update, and delete (CRUD) an entity is repetitive. Instead, we can generate that code. To generate CRUD code for the Job entity, use the command:

php app/console doctrine:generate:crud

Entity name: JobeetJobBoardBundle:Job
Generate write actions: yes
Configuration format: [default]
Routes prefix: [default]
Confirm: [default]

Now you have some pages you can browse. Use the following URL to view these pages in the dev environment:

jobeet.com.localhost/app_dev.php/job/

Also take a moment to explore Symfony's toolbar at the bottom of these pages.
FORM CHOICE FIELD

While browsing, you may have noticed an error on pages that would have a form:

"A __toString() method was not found on the objects of type Jobeet\JobBoardBundle\Entity\Category passed to the choice field."

In order to render a menu of categories, the form needs to know how to stringify category objects. The error message informs us of two possible solutions. We can define a __toString method in the Category class, or we can set a form option to define what getter the form should use. For this example, let's set the form option.
STYLESHEETS, IMAGES, AND JAVASCRIPTS

Since this tutorial is not about web design, we have already prepared all the needed assets we will use for Jobeet.

After importing the files as shown in this commit, you can install your bundle's public assets to the web directory using the command:

php app/console assets:install web
TEMPLATE INHERITANCE AND LAYOUTS

More often than not, templates in a project share common elements, like the header, footer, sidebar, and more. In Symfony2, this problem is thought about differently. A template can be decorated by another one. Template inheritance allows you to build a base "layout" template that contains all the common elements of your site. A child template can extend the base layout and override any of its blocks.

Let's build a base layout file as shown in this commit.
THE TEMPLATE

The generated index template is an HTML table for all the jobs. Let's clean this up a bit to display only a sub-set of the available columns.
THE TEMPLATE CONT.

Next, let's clean up the show template.
THE TEMPLATE CONT.

Right now, the title of all pages is defined in the <title> tag of the layout. But for the job page, we want to provide more useful information, like the company name and the job position. In Symfony, when a zone of the layout depends on the template to be displayed, you need to define a block. Now, in the show template, we can define the content of the title block. For other pages, like the homepage, we just need a generic title. The default title in the layout will be used with any template that does not override the title block.
ROUTE CUSTOMIZATIONS

Let's change the job page URL to something more meaningful. Edit the Job entity to add a slug field. The DoctrineExtensions Sluggable behavior can automatically populate the value of this field for us. Then edit the Job show route to use the slug rather than the ID.
PARAM CONVERTER

The Symfony Standard Edition comes with the SensioFrameworkExtraBundle, which adds sweet conventions and annotations. We can use the ParamConverter to automatically convert request parameters into objects. The converter tries to get a Job object from the request attributes. (Request attributes come from route placeholders.) If no Job object is found, a 404 response is generated. But if a Job object is found, then a new "job" request attribute is defined. As with any other request attribute, it is automatically injected in the controller when present in the method signature.