Symfony2 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 Arhitecture

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 Symfony2 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 Symfony2, the View layer is mainly made of Twig templates. They are stored in various Resources/views/ directories as we will see later in these lines.
  • 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 and app_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.

Unlike Symfony 1.x, Symfony2 does not came with a default layout but we will create one and use it to decorate our application pages.

Create a new file layout.html.twig in the src/Ens/JobeetBundle/Resources/views/ directory and put in the following code:

<!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('bundles/ensjobeet/css/main.css') }}" type="text/css" media="all" />
    {% endblock %}
    {% block javascripts %}
    {% endblock %}
    <link rel="shortcut icon" href="{{ asset('bundles/ensjobeet/images/favicon.ico') }}" />
  </head>
  <body>
    <div id="container">
      <div id="header">
        <div class="content">
          <h1><a href="{{ path('ens_job') }}">
            <img src="{{ asset('bundles/ensjobeet/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('ens_job') }}">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.hasFlash('notice') %}
          <div class="flash_notice">
            {{ app.session.flash('notice') }}
          </div>
        {% endif %}

        {% if app.session.hasFlash('error') %}
          <div class="flash_error">
            {{ app.session.flash('error') }}
          </div>
        {% endif %}

        <div class="content">
            {% block content %}
            {% endblock %}
        </div>
      </div>

      <div id="footer">
        <div class="content">
          <span class="symfony">
            <img src="{{ asset('bundles/ensjobeet/images/jobeet-mini.png') }}" />
            powered by <a href="http://www.symfony.com/">
              <img src="{{ asset('bundles/ensjobeet/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 Symfony2 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.

Now, to make use of the new layout we created, we will need to edit all the job templates (edit, index, new and show from src/En/JobeetBundle/Resources/views/Job/) to extend the parent template (the layout) and to overwrite the content block we defined in it:

{% extends 'EnsJobeetBundle::layout.html.twig' %}

{% block content %}
  <!-- original 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: download the image files archive and put them into the src/Ens/JobeetBundle/Resources/public/images/ directory; download the stylesheet files archive and put them into the src/Ens/JobeetBundle/Resources/public/css/ directory.

Now run

php app/console assets:install web

to tell Symfony to make them available to the public.

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).

<!-- src/Ens/JobeetBundle/Resources/views/Job/index.html.twig -->
{% extends 'EnsJobeetBundle::layout.html.twig' %}

{% block stylesheets %}
  {{ parent() }}
  <link rel="stylesheet" href="{{ asset('bundles/ensjobeet/css/jobs.css') }}" type="text/css" media="all" />
{% endblock %}

<!-- the rest of the code -->

 

<!-- src/Ens/JobeetBundle/Resources/views/Job/show.html.twig -->
{% extends 'EnsJobeetBundle::layout.html.twig' %}

{% block stylesheets %}
  {{ parent() }}
  <link rel="stylesheet" href="{{ asset('bundles/ensjobeet/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()->getEntityManager();

    $entities = $em->getRepository('EnsJobeetBundle:Job')->findAll();

    return $this->render('EnsJobeetBundle:Job:index.html.twig', array(
        'entities' => $entities
    ));
}

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:

<!-- src/Ens/JobeetBundle/Resources/views/Job/index.html.twig -->
{% extends 'EnsJobeetBundle::layout.html.twig' %}

{% block stylesheets %}
  {{ parent() }}
  <link rel="stylesheet" href="{{ asset('bundles/ensjobeet/css/jobs.css') }}" type="text/css" media="all" />
{% endblock %}

{% block content %}
    <h1>Job list</h1>

    <table class="records_list">
        <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>How_to_apply</th>
                <th>Token</th>
                <th>Is_public</th>
                <th>Is_activated</th>
                <th>Email</th>
                <th>Expires_at</th>
                <th>Created_at</th>
                <th>Updated_at</th>
                <th>Actions</th>
            </tr>
        </thead>
        <tbody>
        {% for entity in entities %}
            <tr>
                <td><a href="{{ path('ens_job_show', { 'id': entity.id }) }}">{{ entity.id }}</a></td>
                <td>{{ entity.type }}</td>
                <td>{{ entity.company }}</td>
                <td>{{ entity.logo }}</td>
                <td>{{ entity.url }}</td>
                <td>{{ entity.position }}</td>
                <td>{{ entity.location }}</td>
                <td>{{ entity.description }}</td>
                <td>{{ entity.howtoapply }}</td>
                <td>{{ entity.token }}</td>
                <td>{{ entity.ispublic }}</td>
                <td>{{ entity.isactivated }}</td>
                <td>{{ entity.email }}</td>
                <td>{% if entity.expiresat %}{{ entity.expiresat|date('Y-m-d H:i:s') }}{% endif%}</td>
                <td>{% if entity.createdat %}{{ entity.createdat|date('Y-m-d H:i:s') }}{% endif%}</td>
                <td>{% if entity.updatedat %}{{ entity.updatedat|date('Y-m-d H:i:s') }}{% endif%}</td>
                <td>
                    <ul>
                        <li>
                            <a href="{{ path('ens_job_show', { 'id': entity.id }) }}">show</a>
                        </li>
                        <li>
                            <a href="{{ path('ens_job_edit', { 'id': entity.id }) }}">edit</a>
                        </li>
                    </ul>
                </td>
            </tr>
        {% endfor %}
        </tbody>
    </table>

    <ul>
        <li>
            <a href="{{ path('ens_job_new') }}">
                Create a new entry
            </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 content with the one below:

{% block content %}
    <div id="jobs">
      <table class="jobs">
        {% for entity in entities %}
          <tr class="{{ cycle(['even', 'odd'], loop.index) }}">
            <td class="location">{{ entity.location }}</td>
            <td class="position">
              <a href="{{ path('ens_job_show', { 'id': entity.id }) }}">
                {{ entity.position }}
              </a>
            </td>
            <td class="company">{{ entity.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:

<!-- src/Ens/JobeetBundle/Resources/views/Job/show.html.twig -->
{% extends 'EnsJobeetBundle::layout.html.twig' %}

{% block title %}
    {{ entity.company }} is looking for a {{ entity.position }}
{% endblock %}

{% block stylesheets %}
    {{ parent() }}
    <link rel="stylesheet" href="{{ asset('bundles/ensjobeet/css/job.css') }}" type="text/css" media="all" />
{% endblock %}

{% block content %}
    <div id="job">
      <h1>{{ entity.company }}</h1>
      <h2>{{ entity.location }}</h2>
      <h3>
        {{ entity.position }}
        <small> - {{ entity.type }}</small>
      </h3>

      {% if entity.logo %}
        <div class="logo">
          <a href="{{ entity.url }}">
            <img src="/uploads/jobs/{{ entity.logo }}"
              alt="{{ entity.company }} logo" />
          </a>
        </div>
      {% endif %}

      <div class="description">
        {{ entity.description|nl2br }}
      </div>

      <h4>How to apply?</h4>

      <p class="how_to_apply">{{ entity.howtoapply }}</p>

      <div class="meta">
        <small>posted on {{ entity.createdat|date('m/d/Y') }}</small>
      </div>

      <div style="padding: 20px 0">
        <a href="{{ path('ens_job_edit', { 'id': entity.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($id)
{
    $em = $this->getDoctrine()->getEntityManager();

    $entity = $em->getRepository('EnsJobeetBundle:Job')->find($id);

    if (!$entity) {
        throw $this->createNotFoundException('Unable to find Job entity.');
    }

    $deleteForm = $this->createDeleteForm($id);

    return $this->render('EnsJobeetBundle:Job:show.html.twig', array(
        'entity'      => $entity,
        'delete_form' => $deleteForm->createView(),

    ));
}

As in the index action, the EnsJobeetBundle:Job repository class is used to retrieve a job, this time by using the find() method. The parameter of this method is the unique identifier of a job, its primary key. The next section will explain why the $id parameter of the actionShow() function contains the job primary key.

If the job does not exist in the database, we want to forward the user to a 404 page, which is exactly what the throw $this->createNotFoundException() does.

As for exceptions, the page displayed to the user is different in the prod environment and in the dev environment:

Symfony2 Jobeet Day 5: The Routing

Symfony2 Jobeet Day 3: The Data Model

21 thoughts on “Symfony2 Jobeet Day 4: The Controller and the View”

  1. when I am running the above project then i am getting a problem :
    Class Ens\JobeetBundle\Entity\Job does not exist
    while i hve changed the name by ensl from everywhere

  2. once again you’ve got some typo (missing slashes in some place with “src/EnsJobeetBundle” -> “src/Ens/JobeetBundle”)

    anyway goodjob

  3. good job
    everything is good except that the images extreme-sensio.gif and sensio-labs.gif not displayed
    the images are in the directory web/uploads/jobs/

    1. exact very good job!
      For the images issue :
      replace line 25 : — <img src="/uploads/jobs/{{ entity.logo }}" —
      by : — <img src="{{ asset('uploads/jobs/') }}{{ entity.logo }}" —

      it worked for me 🙂

    2. in show.html.twig:
      src=”/uploads/jobs/{{ entity.logo }}”
      should be replaced by:
      src=”{{ asset(‘uploads/jobs/’ ~ entity.logo) }}”

  4. Hi there,
    I am new in symfony2.1. I am using wamp server (apache 2.2.22 and php 5.3.13). I have configured everything properly for symfony2.1. but problem happen when I am trying to retrieve data from database . Sometimes its work properly but sometime it doesn’t work. Than I have to restart the server.
    To solve that I have installed apache, php and mysql separately. But still I am facing the same problem.
    Please give me a solution.

  5. Hi.
    Im following the tutorial, until now was all ok.
    But im getting an error:

    Unable to find template “EnsJobeetBundle::layout.html.twig”.
    500 Internal Server Error – InvalidArgumentException
    3 linked Exceptions:

    Twig_Error_Loader »
    InvalidArgumentException »
    InvalidArgumentException »

    Anyone have an idea whats going on?
    Im using Symfony 2.2.0 and PHP 5.3.10.
    thanks

    1. In Symfony 2.2.3, I have got the same problem What i have done is just copy the layout.html.twig file into C:\wamp\www\Symfony_demo1\app\Resources\views. Now its working fine to me. Try it out.

      1. In Symfony 2.2.3, I have got the same problem What i have done is just copy the layout.html.twig file and paste it into C:\wamp\www\Symfony_demo1\app\Resources\views. Now its working fine to me. Try it out.

  6. In newer versions (just tested in 2.3), you have to remove the comments at the first line at all twig files (except the layout.html.twig) because they throw the following exception:

    “A template that extends another one cannot have a body in EnsJobeetBundle:Job:show.html.twig at line 1.”

    The exception is thrown because child twig pages are not permitted to have html code outside defined blocks, not even if it’s a comment. So, if you have to enter a comment, just use twig comments: {# … #}

  7. From Symfony 2.3 handling of flash messages is changed. Use this:

    {% for flashMessage in app.session.flashbag.get(‘notice’) %}
    {{ flashMessage }}
    {% endfor %}

    {% for flashMessage in app.session.flashbag.get(‘error’) %}
    {{ flashMessage }}
    {% endfor %}

    1. Just to clarify for novices using this to learn Symfony, if you’re using Symfony >=2.3, in the layout.html.twig file you want to replace these lines:

      {% if app.session.hasFlash(‘notice’) %}

      {{ app.session.flash(‘notice’) }}

      {% endif %}

      {% if app.session.hasFlash(‘error’) %}

      {{ app.session.flash(‘error’) }}

      {% endif %}

      with this:

      {% for flashMessage in app.session.flashbag.get(‘notice’) %}

      {{ flashMessage }}

      {% endfor %}

      {% for flashMessage in app.session.flashbag.get(‘error’) %}

      {{ flashMessage }}

      {% endfor %}

      (Also if you’re copy/pasting mind that you’re using the correct single-quote.)

  8. Hello,

    Its a great tutorial, I’m a novice user for Symfony, you explained very well. Please help I have an Issue, I couldn’t able to display list of jobs in the browser as I have properly uploaded CSS and its related images but I couldn’t complete list of jobs. For more information I have verified that the job is available in database table but it is not displayed as expected could you please help me to move further.

    Thanks,
    Cyril.

    1. Cyril, I just started following the tutorial.
      Make sure that :
      {% block body%}
      is
      {% block content %}
      ————————- i.e “content” NOT “body”
      Check for it in:
      index.html.twig
      edit.html.twig
      new.html.twig
      show.html.twig

      It’s under src/**Your folder**/Resources/views/Job/

  9. CRUD was showing/working in day3. I wish i can go further with the tutorials but Im stuck.

    An exception has been thrown during the rendering of a template (“Unable to generate a URL for the named route “ens_job” as such route does not exist.”) in EnsJobeetBundle::layout.html.twig at line 21.

    We got the following routes: job, job_show, job_new, job_create, job_edit, job_update, job_delete BUT no routes for ens_job. In fact i dont know why and how ens_job is present in your code at layout.html.twig.

    Any help?

  10. EDIT: I renamed all my routes to match the new ones. I find it wierd that you used a format in a lesson and another one in another.

Leave a Reply

Your email address will not be published. Required fields are marked *