Symfony 2.8 Jobeet Day 4: The Controller and the View
Today, we are going to customize the basic job controller we created yesterday. It already has most of the code we need for Jobeet:
- A page to list all jobs
- A page to create a new job
- A page to update an existing job
- A page to delete a job
Although the code is ready to be used as is, we will refactor the templates to match closer to the Jobeet mockups.
The MVC Architecture
For web development, the most common solution for organizing your code nowadays is the MVC design pattern. In short, the MVC design pattern defines a way to organize your code according to its nature. This pattern separates the code into three layers:
- The Model layer defines the business logic (the database belongs to this layer). You already know that Symfony stores all the classes and files related to the Model in the
Entity/
directory of your bundles. - The View is what the user interacts with (a template engine is part of this layer). In Symfony, the View layer is mainly made of Twig templates. They are stored in various
Resources/views/
directories as we will see later. - The Controller is a piece of code that calls the Model to get some data that it passes to the View for rendering to the client. When we installed Symfony at the beginning of this tutorial, we saw that all requests are managed by front controllers (
app.php
andapp_dev.php
). These front controllers delegate the real work to actions.
The Layout
If you have a closer look at the mockups, you will notice that much of each page looks the same. You already know that code duplication is bad, whether we are talking about HTML or PHP code, so we need to find a way to prevent these common view elements from resulting in code duplication.
One way to solve the problem is to define a header and a footer and include them in each template. A better way is to use another design pattern to solve this problem: the decorator design pattern. The decorator design pattern resolves the problem the other way around: the template is decorated after the content is rendered by a global template, called a layout.
If you take a look in the app/Resources/views
folder, you will find there a base.html.twig
template. That is the default layout that decorates our job pages right now. Open it and replace it’s content with the following:
<!DOCTYPE html>
<html>
<head>
<title>
{% block title %}
Jobeet - Your best job board
{% endblock %}
</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
{% block stylesheets %}
<link rel="stylesheet" href="{{ asset('css/main.css') }}" type="text/css" media="all" />
{% endblock %}
{% block javascripts %}
{% endblock %}
<link rel="shortcut icon" href="{{ asset('images/favicon.ico') }}" />
</head>
<body>
<div id="container">
<div id="header">
<div class="content">
<h1><a href="{{ path('job_index') }}">
<img src="{{ asset('images/logo.jpg') }}" alt="Jobeet Job Board" />
</a></h1>
<div id="sub_header">
<div class="post">
<h2>Ask for people</h2>
<div>
<a href="{{ path('job_index') }}">Post a Job</a>
</div>
</div>
<div class="search">
<h2>Ask for a job</h2>
<form action="" method="get">
<input type="text" name="keywords" id="search_keywords" />
<input type="submit" value="search" />
<div class="help">
Enter some keywords (city, country, position, ...)
</div>
</form>
</div>
</div>
</div>
</div>
<div id="content">
{% if app.session.flashbag.has('notice') %}
{% for message in app.session.flashBag.get('notice') %}
<div class="flash_notice">
{{ message }}
</div>
{% endfor %}
{% endif %}
{% if app.session.flashbag.has('error') %}
{% for message in app.session.flashBag.get('error') %}
<div class="flash_error">
{{ message }}
</div>
{% endfor %}
{% endif %}
<div class="content">
{% block body %}
{% endblock %}
</div>
</div>
<div id="footer">
<div class="content">
<span class="symfony">
<img src="{{ asset('images/jobeet-mini.png') }}" />
powered by <a href="http://www.symfony.com/">
<img src="{{ asset('images/symfony.gif') }}" alt="symfony framework" />
</a>
</span>
<ul>
<li><a href="">About Jobeet</a></li>
<li class="feed"><a href="">Full feed</a></li>
<li><a href="">Jobeet API</a></li>
<li class="last"><a href="">Affiliates</a></li>
</ul>
</div>
</div>
</div>
</body>
</html>
Twig Blocks
In Twig, the default Symfony template engine, you can define blocks as we did above. A twig block can have a default content (look at the title block
for example) that can be replaced or extended in the child template as you will see in a moment.
To make use of the base layout we just edited, all the job templates (edit, index, new and show from app/Resources/views/job/
) extend the parent template (the layout) and overwrite the body block defined in it:
{% extends 'base.html.twig' %}
{% block body %}
<!-- template code goes here -->
{% endblock %}
The Stylesheets, Images, and JavaScripts
As this tutorial is not about web design, we have already prepared all the needed assets we will use for Jobeet. You can find the image files here: https://github.com/dragosholban/jobeet-sf2.8/tree/day4/web/images and the stylesheet files here: https://github.com/dragosholban/jobeet-sf2.8/tree/day4/web/css. Get them and put them in the corresponding folders (web/images
and web/css
).
If you look in the css folder you will notice that we have 4 css files: admin.css
, job.css
, jobs.css
and main.css
. The main.css
is needed in all Jobeet pages so we included it in the layout in the stylesheets
twig block. The rest are more specialized css files and we need them only in specific pages.
To add a new css file in a template we will overwrite the stylesheets block, but call the parent before adding the new css file (so we would have the main.css file and the additional css files we need).
app/Resources/views/job/index.html.twig
{% extends 'base.html.twig' %}
{% block stylesheets %}
{{ parent() }}
<link rel="stylesheet" href="{{ asset('css/jobs.css') }}" type="text/css" media="all" />
{% endblock %}
<!-- the rest of the code -->
app/Resources/views/job/show.html.twig
{% extends 'base.html.twig' %}
{% block stylesheets %}
{{ parent() }}
<link rel="stylesheet" href="{{ asset('css/job.css') }}" type="text/css" media="all" />
{% endblock %}
<!-- the rest of the code -->
The Job Homepage Action
Each action is represented by a method of a class. For the job homepage, the class is JobController
and the method is indexAction()
. It retrieves all the jobs from the database:
public function indexAction()
{
$em = $this->getDoctrine()->getManager(); $jobs = $em->getRepository('AppBundle:Job')->findAll(); return $this->render('job/index.html.twig', array(
'jobs' => $jobs,
));
}
Let’s have a closer look at the code: the indexAction() method gets the Doctrine entity manager object, which is responsible for handling the process of persisting and fetching objects to and from the database, and then the repository, that will create a query to retrieve all the jobs. It returns a Doctrine ArrayCollection
of Job objects that are passed to the template (the View).
The Job Homepage Template
The index.html.twig
template generates an HTML table for all the jobs. Here is the current template code:
{% extends 'base.html.twig' %}
{% block stylesheets %}
{{ parent() }}
<link rel="stylesheet" href="{{ asset('css/jobs.css') }}" type="text/css" media="all" />
{% endblock %}{% block body %}
<h1>Jobs list</h1> <table>
<thead>
<tr>
<th>Id</th>
<th>Type</th>
<th>Company</th>
<th>Logo</th>
<th>Url</th>
<th>Position</th>
<th>Location</th>
<th>Description</th>
<th>Howtoapply</th>
<th>Token</th>
<th>Ispublic</th>
<th>Isactivated</th>
<th>Email</th>
<th>Expiresat</th>
<th>Createdat</th>
<th>Updatedat</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for job in jobs %}
<tr>
<td><a href="{{ path('job_show', { 'id': job.id }) }}">{{ job.id }}</a></td>
<td>{{ job.type }}</td>
<td>{{ job.company }}</td>
<td>{{ job.logo }}</td>
<td>{{ job.url }}</td>
<td>{{ job.position }}</td>
<td>{{ job.location }}</td>
<td>{{ job.description }}</td>
<td>{{ job.howToApply }}</td>
<td>{{ job.token }}</td>
<td>{% if job.isPublic %}Yes{% else %}No{% endif %}</td>
<td>{% if job.isActivated %}Yes{% else %}No{% endif %}</td>
<td>{{ job.email }}</td>
<td>{% if job.expiresAt %}{{ job.expiresAt|date('Y-m-d H:i:s') }}{% endif %}</td>
<td>{% if job.createdAt %}{{ job.createdAt|date('Y-m-d H:i:s') }}{% endif %}</td>
<td>{% if job.updatedAt %}{{ job.updatedAt|date('Y-m-d H:i:s') }}{% endif %}</td>
<td>
<ul>
<li>
<a href="{{ path('job_show', { 'id': job.id }) }}">show</a>
</li>
<li>
<a href="{{ path('job_edit', { 'id': job.id }) }}">edit</a>
</li>
</ul>
</td>
</tr>
{% endfor %}
</tbody>
</table> <ul>
<li>
<a href="{{ path('job_new') }}">Create a new job</a>
</li>
</ul>
{% endblock %}
Let’s clean this up a bit to only display a sub-set of the available columns. Replace the twig block body
with the one below:
{% block body %}
<div id="jobs">
<table class="jobs">
{% for job in jobs %}
<tr class="{{ cycle(['even', 'odd'], loop.index) }}">
<td class="location">{{ job.location }}</td>
<td class="position">
<a href="{{ path('job_show', { 'id': job.id }) }}">
{{ job.position }}
</a>
</td>
<td class="company">{{ job.company }}</td>
</tr>
{% endfor %}
</table>
</div>
{% endblock %}
The Job Page Template
Now let’s customize the template of the job page. Open the show.html.twig
file and replace its content with the following code:
{% extends 'base.html.twig' %}
{% block title %}
{{ job.company }} is looking for a {{ job.position }}
{% endblock %}
{% block stylesheets %}
{{ parent() }}
<link rel="stylesheet" href="{{ asset('css/job.css') }}" type="text/css" media="all" />
{% endblock %}
{% block body %}
<div id="job">
<h1>{{ job.company }}</h1>
<h2>{{ job.location }}</h2>
<h3>
{{ job.position }}
<small> - {{ job.type }}</small>
</h3>
{% if job.logo %}
<div class="logo">
<a href="{{ job.url }}">
<img src="/uploads/jobs/{{ job.logo }}"
alt="{{ job.company }} logo" />
</a>
</div>
{% endif %}
<div class="description">
{{ job.description|nl2br }}
</div>
<h4>How to apply?</h4>
<p class="how_to_apply">{{ job.howtoapply }}</p>
<div class="meta">
<small>posted on {{ job.createdat|date('m/d/Y') }}</small>
</div>
<div style="padding: 20px 0">
<a href="{{ path('job_edit', { 'id': job.id }) }}">
Edit
</a>
</div>
</div>
{% endblock %}
The Job Page Action
The job page is generated by the show action, defined in the showAction()
method of the JobController:
public function showAction(Job $job)
{
$deleteForm = $this->createDeleteForm($job); return $this->render('job/show.html.twig', array(
'job' => $job,
'delete_form' => $deleteForm->createView(),
));
}
Here the parameter of the showAction method is actually the Job object. If the job does not exist in the database, a 404 Response is generated. This is done automatically by Symfony using a Doctrine converter that convert request parameters to objects. You can read more about this here: http://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/converters.html
As for exceptions (the 404 No Found in our case), the page displayed to the user is different in the prod environment and in the dev environment:
As always, find the code from day 4 here: https://github.com/dragosholban/jobeet-sf2.8/tree/day4.
Next Steps
Continue this tutorial here: Symfony 2.8 Jobeet Day 5: The Routing
Previous post is available here: Symfony 2.8 Jobeet Day 3: The Data Model
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.