Symfony 2.8 Jobeet Day 5: The Routing

URLs

If you click on a job on the Jobeet homepage, the URL looks like this: /job/1. If you have already developed PHP websites, you are probably more accustomed to URLs like /job.php?id=1. How does Symfony make it work? How does Symfony determine the action to call based on this URL? Why is the job retrieved with the $job parameter in the action? Here, we will answer all these questions.

You have already seen the following code in the app/Resources/views/job/index.html.twig template: {{ path(‘job_show’, { ‘id’: job.id }) }}

This uses the path template helper function to generate the url for the job which has the id 1. The job_show is the name of the route used, defined in the configuration as you will see below.

Routing Configuration

In Symfony, routing configuration is usually done in the app/config/routing.yml. This imports specific bundle routing configuration. In our case, the routing is imported from all AppBundle’s controllers, using annotations:

app:
resource: '@AppBundle/Controller/'
type: annotation

In the JobController you will find every job related route defined:

/**
* Job controller.
*
* @Route("job")
*/
class JobController extends Controller
{
/**
* Lists all job entities.
*
* @Route("/", name="job_index")
* @Method("GET")
*/
public function indexAction()
...

/**
* Creates a new job entity.
*
* @Route("/new", name="job_new")
* @Method({"GET", "POST"})
*/
public function newAction(Request $request)
...

/**
* Finds and displays a job entity.
*
* @Route("/{id}", name="job_show")
* @Method("GET")
*/
public function showAction(Job $job)
...

/**
* Displays a form to edit an existing job entity.
*
* @Route("/{id}/edit", name="job_edit")
* @Method({"GET", "POST"})
*/
public function editAction(Request $request, Job $job)
...

/**
* Deletes a job entity.
*
* @Route("/{id}", name="job_delete")
* @Method("DELETE")
*/
public function deleteAction(Request $request, Job $job)
...
}

Let’s have a closer look to the job_show route. The pattern defined by the this route acts like /job/* where the wildcard is given the name id (the /job part comes from the @Route(“job”) definition found at the beginning of the class that acts like a prefix for all the routes defined in that class). For the URL /job/1, the id variable gets a value of 1, which is used by the Doctrine Converter to retrieve the corresponding job and then made available for you to use in your controller.

The route parameters are especially important because each is made available as an argument to the controller method.

Routing Configuration in Dev Environment

The dev environment loads the app/config/routing_dev.yml file that contains the routes used by the Web Debug Toolbar, among others. This file loads, at the end, the main routing.yml configuration file:

_wdt:
resource: '@WebProfilerBundle/Resources/config/routing/wdt.xml'
prefix: /_wdt
_profiler:
resource: '@WebProfilerBundle/Resources/config/routing/profiler.xml'
prefix: /_profiler
_errors:
resource: '@TwigBundle/Resources/config/routing/errors.xml'
prefix: /_error
_main:
resource: routing.yml

Route Customizations

For now, when you request the / URL in a browser, you will get the default Symfony page, decorated by our changed layout. That’s because this URL does match a route defined in the DefaultController (@Route(“/”, name=”homepage”)). Let’s remove this controller and change the job_index route from the JobController it to match the / URL. To make this work we will need to make several changes:

  • we need to remove the @Route(“job”) annotation from the beginning of the JobController class that sets a /job prefix for all the routes defined below
  • we need to add the /job prefix to all the routes (because we just removed it and they will not work anymore) except the job_index route, that we want to work without the prefix.
/**
* Job controller.
*/
class JobController extends Controller
{
/**
* Lists all job entities.
*
* @Route("/", name="job_index")
* @Method("GET")
*/
public function indexAction()
...
    /**
* Creates a new job entity.
*
* @Route("/job/new", name="job_new")
* @Method({"GET", "POST"})
*/
public function newAction(Request $request)
...
   /**
* Finds and displays a job entity.
*
* @Route("/job/{id}", name="job_show")
* @Method("GET")
*/
public function showAction(Job $job)
...
    /**
* Displays a form to edit an existing job entity.
*
* @Route("/job/{id}/edit", name="job_edit")
* @Method({"GET", "POST"})
*/
public function editAction(Request $request, Job $job)
...
    /**
* Deletes a job entity.
*
* @Route("/job/{id}", name="job_delete")
* @Method("DELETE")
*/
public function deleteAction(Request $request, Job $job)
...
}

Now, if you clear the cache and go to http://jobeet.local from your browser, you will see the Job homepage.

For something a bit more involved, let’s change the job page URL to something more meaningful:

/job/sensio-labs/paris-france/1/web-developer

Without knowing anything about Jobeet, and without looking at the page, you can understand from the URL that Sensio Labs is looking for a Web developer to work in Paris, France.

The following pattern matches such a URL:

/job/{company}/{location}/{id}/{position}

Edit the job_show route from the JobController.php file:

/**
* Finds and displays a job entity.
*
* @Route("/job/{company}/{location}/{id}/{position}", name="job_show")
* @Method("GET")
*/
public function showAction(Job $job)

Now, we need to pass all the parameters for the changed route for it to work (app/Resources/views/job/index.html.twig):

<a href="{{ path('job_show', { 'id': job.id, 'company': job.company, 'location': job.location, 'position': job.position }) }}">
{{ job.position }}
</a>

If you have a look at generated URLs, they are not quite yet as we want them to be:

http://jobeet.local/app_dev.php/job/Sensio%20Labs/Paris,%20France/1/Web%20Developer

We need to “slugify” the column values by replacing all non ASCII characters by a -. Open the Job.php file and add the following methods to the class:

use AppBundle\Utils\Jobeet as Jobeet;
// ...
class Job
{
// ...
public function getCompanySlug()
{
return Jobeet::slugify($this->getCompany());
}

public function getPositionSlug()
{
return Jobeet::slugify($this->getPosition());
}

public function getLocationSlug()
{
return Jobeet::slugify($this->getLocation());
}
}

Then, create the src/AppBundle/Utils/Jobeet.php file and add the slugify method in it:

namespace AppBundle\Utils;
class Jobeet
{
static public function slugify($text)
{
// replace all non letters or digits by -
$text = preg_replace('/\W+/', '-', $text);

// trim and lowercase
$text = strtolower(trim($text, '-'));

return $text;
}
}

We have defined three new “virtual” accessors: getCompanySlug(), getPositionSlug(), and getLocationSlug(). They return their corresponding column value after applying it the slugify() method. Now, you can replace the real column names by these virtual ones in the template:

<a href="{{ path('job_show', { 'id': job.id, 'company': job.companySlug, 'location': job.locationSlug, 'position': job.positionSlug }) }}">
{{ job.position }}
</a>

You will now have the expected URLs:

http://jobeet.local/app_dev.php/job/sensio-labs/paris-france/1/web-developer

Route Requirements

The routing system has a built-in validation feature. Each pattern variable can be validated by a regular expression defined using the requirements entry of a route definition:

/**
* Finds and displays a job entity.
*
* @Route("/job/{company}/{location}/{id}/{position}", name="job_show", requirements={"id" = "\d+"})
* @Method("GET")
*/

The above requirements entry forces the id to be a numeric value. If not, the route won’t match.

Route Debugging

While adding and customizing routes, it’s helpful to be able to visualize and get detailed information about your routes. A great way to see every route in your application is via the debug:router console command. Execute the command by running the following from the root of your project:

php app/console debug:router

The command will print a helpful list of all the configured routes in your application. You can also get very specific information on a single route by including the route name after the command:

php app/console debug:router job_show

Final Thoughts

That’s all for today! To learn more about the Symfony routing system read the Routing chapter form the documentation.

You can find the code from today here: https://github.com/dragosholban/jobeet-sf2.8/tree/day5


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.