This post was published as part of the symfony 2008 advent calendar. As this tutorial might have been updated since then, you are advised to read the last version from the symfony 1.2 documentation (for Propel or Doctrine).

Before we start

If you already have a jobeet_category_affiliate table in your database schema, you can skip this section and go straight to the Previously on Jobeet section.

If you have a jobeet_job_affiliate table, keep reading.

The Jobeet tutorial is made on a day-to-day basis. We really appreciate all the feedback you give us in real-time. Based on some comments from yesterday tutorial, we decided to change the database schema. Instead of having a many-to-many relationship between affiliates and jobs, some of you suggested that it would make more sense to instead have one between affiliates and categories. And we cannot agree more. So, before starting today's tutorial, let's make the change. If won't take too long as most of the things will be automated by the framework.

First replace the jobeet_job_affiliate table definition with the jobeet_category_affiliate one:

// config/schema.yml
jobeet_category_affiliate:
  category_id:  { type: integer, foreignTable: jobeet_category, foreignReference: id, required: true, primaryKey: true, onDelete: cascade }
  affiliate_id: { type: integer, foreignTable: jobeet_affiliate, foreignReference: id, required: true, primaryKey: true, onDelete: cascade }
 

Rebuild the model classes, re-generate the table creation SQL statements, and insert the data fixtures:

$ php symfony propel:build-all-load

Remove files we don't need anymore:

  • lib/filter/base/BaseJobeetJobAffiliateFormFilter.class.php
  • lib/filter/JobeetJobAffiliateFormFilter.class.php
  • lib/form/base/BaseJobeetJobAffiliateForm.class.php
  • lib/form/JobeetJobAffiliateForm.class.php
  • lib/model/JobeetJobAffiliate.php
  • lib/model/JobeetJobAffiliatePeer.php
  • lib/model/map/JobeetJobAffiliateMapBuilder.php
  • lib/model/om/BaseJobeetJobAffiliate.php
  • lib/model/om/BaseJobeetJobAffiliatePeer.php

Clear the symfony cache to take the changes into account:

$ php symfony cc

We are done.

The SVN tag for day 4 has also been updated to reflect the change.

Previously on Jobeet

Yesterday, we explored how symfony simplifies database management by abstracting the differences between database engines, and by converting the relational elements to nice object oriented classes. We have also played with Propel to describe the database schema, create the tables, and populate the database with some initial data.

Today, we are going to customize the basic job module we created yesterday. The job module already has all 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

If you are used to developing PHP websites without a framework, you probably use the one PHP file per HTML page paradigm. These PHP files probably contain the same kind of structure: initialization and global configuration, business logic related to the requested page, database records fetching, and finally HTML code that builds the page.

You may use a templating engine to separate the logic from the HTML. Perhaps you use a database abstraction layer to separate model interaction from business logic. But most of the time, you end up with a lot of code that is a nightmare to maintain. It was fast to build but over time, it's more and more difficult to make changes, especially because nobody except you understands how it is built and how it works.

As with every problem, there are nice solutions. 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 lib/model directory.

  • 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 PHP templates. They are stored in various templates directories as we will see later on today.

  • 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 the first day, we saw that all requests are managed by front controllers (index.php and frontend_dev.php). These front controllers delegate the real work to actions. As we saw yesterday, these actions are logically grouped into modules.

MVC

Today, we will use the mockup defined in day 2 to customize the homepage and the job page and make them dynamic. Along the way, we will tweak a lot of things in many different files to demonstrate the symfony directory structure and the way to separate code between layers.

The Layout

First, 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:

Header and footer

But here the header and the footer files do not contain valid HTML. There must be a better way. Instead of reinventing the wheel, we will 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 in symfony:

Layout

The default layout of an application is called layout.php and can be found in the apps/frontend/templates/ directory. This directory contains all the global templates for an application.

Replace the default symfony layout with the following code:

<!-- apps/frontend/templates/layout.php -->
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
  <head>
    <title>Jobeet - Your best job board</title>
    <link rel="shortcut icon" href="/favicon.ico" />
    <?php include_javascripts() ?>
    <?php include_stylesheets() ?>
  </head>
  <body>
    <div id="container">
      <div id="header">
        <div class="content">
          <h1><a href="/job">
            <img src="/images/jobeet.gif" alt="Jobeet Job Board" />
          </a></h1>
 
          <div id="sub_header">
            <div class="post">
              <h2>Ask for people</h2>
              <div>
                <a href="/job/new">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">
        <?php if ($sf_user->hasFlash('notice')): ?>
          <div class="flash_notice"><?php echo $sf_user->getFlash('notice') ?></div>
        <?php endif; ?>
 
        <?php if ($sf_user->hasFlash('error')): ?>
          <div class="flash_error"><?php echo $sf_user->getFlash('error') ?></div>
        <?php endif; ?>
 
        <div class="content">
          <?php echo $sf_content ?>
        </div>
      </div>
 
      <div id="footer">
        <div class="content">
          <span class="symfony">
            <img src="/images/jobeet-mini.png" />
            powered by <a href="http://www.symfony-project.org/">
            <img src="/images/symfony.gif" alt="symfony framework" /></a>
          </span>
          <ul>
            <li><a href="">About Jobeet</a></li>
            <li class="feed"><a href="">Full RSS feed</a></li>
            <li><a href="">Jobeet API</a></li>
            <li class="last"><a href="">Affiliates</a></li>
          </ul>
        </div>
      </div>
    </div>
  </body>
</html>
 

A symfony template is just a plain PHP file. In the layout template, you will see calls to PHP functions and references to PHP variables. $sf_content is the most interesting variable: it is defined by the framework itself and contains the HTML generated by the action.

If you browse the job module (http://jobeet.localhost/frontend_dev.php/job), you will see that all actions are now decorated by the layout.

In the layout, we have included a favicon. You can download the Jobeet one and put it under the web/ directory.

The Stylesheets, Images, and JavaScripts

As we will organize a "best design" contest on the 21st day, we have prepared a very basic design to use in the meantime: download the image files archive and put them into the web/images/ directory; download the stylesheet files archive and put them into the web/css/ directory.

The job module with a layout and assets

By default, the generate:project task has created three directories for the project assets: web/images/ for images, web/css/ for stylesheets, and web/js/ for JavaScripts. This is one of the many conventions defined by symfony, but you can of course store them elsewhere under the web/ directory.

The astute reader will have noticed that even if the main.css file is not mentioned anywhere in the default layout, it is definitely present in the generated HTML. But not the other ones. How is this possible?

The stylesheet file has been included by the include_stylesheets() function call found the layout <head> tag. The include_stylesheets() function is called a helper. A helper is a function, defined by symfony, that can take parameters and returns HTML code. Most of the time, helpers are time-savers, they package code snippets frequently used in templates. The include_stylesheets() helper generates <link> tags for stylesheets.

But how does the helper know which stylesheets to include?

The View layer can be configured by editing the view.yml configuration file of the application. Here is the default one generated by the generate:app task:

# apps/frontend/config/view.yml
default:
  http_metas:
    content-type: text/html
 
  metas:
    #title:        symfony project
    #description:  symfony project
    #keywords:     symfony, project
    #language:     en
    #robots:       index, follow
 
  stylesheets:    [main.css]
 
  javascripts:    []
 
  has_layout:     on
  layout:         layout
 

The view.yml file configures the default settings for all the templates of the application. For instance, the stylesheets entry defines an array of stylesheet files to include for every page of the application (the inclusion is done by the include_stylesheets() helper).

In the default view.yml configuration file, the referenced file is main.css, and not /css/main.css. As a matter of fact, both definitions are equivalent as symfony prefixes relative paths with /css/.

If many files are defined, symfony will include them in the same order as the definition:

stylesheets:    [main.css, jobs.css, job.css]
 

You can also change the media attribute and omit the .css suffix:

stylesheets:    [main.css, jobs.css, job.css, print: { media: print }]
 

This configuration will be rendered as:

<link rel="stylesheet" type="text/css" media="screen" href="/css/main.css" />
<link rel="stylesheet" type="text/css" media="screen" href="/css/jobs.css" />
<link rel="stylesheet" type="text/css" media="screen" href="/css/job.css" />
<link rel="stylesheet" type="text/css" media="print" href="/css/print.css" />
 

The view.yml configuration file also defines the default layout used by the application. By default, the name is layout, and so symfony decorates every page with the layout.php file. You can also disable the decoration process altogether by switching the has_layout entry to false.

It works as is but the jobs.css file is only needed for the homepage and the job.css file is only needed for the job page. The view.yml configuration file can be customized on a per-module basis. Change the application view.yml file to only contain the main.css file:

# apps/frontend/config/view.yml
stylesheets:    [main.css]
 

To customize the view for the job module, create a view.yml file in the apps/frontend/modules/job/config/ directory:

# apps/frontend/modules/job/config/view.yml
indexSuccess:
  stylesheets: [jobs.css]
 
showSuccess:
  stylesheets: [job.css]
 

Under the indexSuccess and showSuccess sections (they are the template names associated with the index and show actions, as we will see later on), you can customize any entry found under the default section of the application view.yml. All specific entries are merged with the application configuration. You can also define some configuration for all actions of a module with the special all section.

As a rule of thumb, when something is configurable via a configuration file, the same can be accomplished with PHP code. Instead of creating a view.yml file for the job module for instance, you can also use the use_stylesheet() helper to include a stylesheet from a template:

<?php use_stylesheet('main.css') ?>
 

You can also use this helper in the layout to include a stylesheet globally.

Choosing between one method or the other is really a matter of taste. The view.yml file provides a way to define things for all actions of a module, which is not possible in a template, but the configuration is quite static. On the other hand, using the use_stylesheet() helper is more flexible and moreover, everything is in the same place: the stylesheet definition and the HTML code. For Jobeet, we will use the use_stylesheet() helper, so you can remove the view.yml we have just created and update the job templates with the use_stylesheet() calls.

Symmetrically, the JavaScript configuration is done via the javascripts entry of the view.yml configuration file and the use_javascript() helper defines JavaScript files to include for a template.

The Job Homepage

As seen in day 3, the job homepage is generated by the index action of the job module. The index action is the Controller part of the page and the associated template, indexSuccess.php, is the View part:

apps/
  frontend/
    modules/
      job/
        actions/
          actions.class.php
        templates/
          indexSuccess.php

The Action

Each action is represented by a method of a class. For the job homepage, the class is jobActions (the name of the module suffixed by Actions) and the method is executeIndex() (execute suffixed by the name of the action). It retrieves all the jobs from the database:

// apps/frontend/modules/job/actions/actions.class.php
class jobActions extends sfActions
{
  public function executeIndex(sfWebRequest $request)
  {
    $this->jobeet_job_list = JobeetJobPeer::doSelect(new Criteria());
  }
 
  // ...
}
 

Let's have a closer look at the code: the executeIndex() method (the Controller) calls the Model JobeetJobPeer to retrieve all the jobs (new Criteria()). It returns an array of Job objects that are assigned to the jobeet_job_list object property.

All such object properties are then automatically passed to the template (the View). To pass data from the Controller to the View, just create a new property:

public function executeIndex(sfWebRequest $request)
{
  $this->foo = 'bar';
  $this->bar = array('bar', 'baz');
}
 

This code will make $foo and $bar variables accessible in the template.

The Template

By default, the template name associated with an action is deduced by symfony thanks to a convention (the action named suffix by Success).

The indexSuccess.php template generates an HTML table for all the jobs:

<!-- apps/frontend/modules/job/templates/indexSuccess.php -->
<h1>Job List</h1>
 
<table>
  <thead>
    <tr>
      <th>Id</th>
      <th>Category</th>
      <th>Type</th>
<!-- more columns here -->
      <th>Created at</th>
      <th>Updated at</th>
    </tr>
  </thead>
  <tbody>
    <?php foreach ($jobeet_job_list as $jobeet_job): ?>
    <tr>
      <td>
        <a href="<?php echo url_for('job/show?id='.$jobeet_job->getId()) ?>">
          <?php echo $jobeet_job->getId() ?>
        </a>
      </td>
      <td><?php echo $jobeet_job->getCategoryId() ?></td>
      <td><?php echo $jobeet_job->getType() ?></td>
<!-- more columns here -->
      <td><?php echo $jobeet_job->getCreatedAt() ?></td>
      <td><?php echo $jobeet_job->getUpdatedAt() ?></td>
    </tr>
    <?php endforeach; ?>
  </tbody>
</table>
 
<a href="<?php echo url_for('job/new') ?>">New</a>
 

In the template code, the foreach iterates through the list of Job objects ($jobeet_job_list), and for each job, each column value is output. Remember, accessing a column value is a simple as calling an accessor method which name begins with get and the camelCased column name (for instance the getCreatedAt() method for the created_at column).

Let's clean this up a bit to only display a sub-set of the available columns:

<!-- apps/frontend/modules/job/templates/indexSuccess.php -->
<?php use_stylesheet('jobs.css') ?>
 
<div id="jobs">
  <table class="jobs">
    <?php foreach ($jobeet_job_list as $i => $job): ?>
      <tr class="<?php echo fmod($i, 2) ? 'even' : 'odd' ?>">
        <td><?php echo $job->getLocation() ?></td>
        <td>
          <a href="<?php echo url_for('job/show?id='.$job->getId()) ?>">
            <?php echo $job->getPosition() ?>
          </a>
        </td>
        <td><?php echo $job->getCompany() ?></td>
      </tr>
    <?php endforeach; ?>
  </table>
</div>
 

Homepage

The url_for() function call in this template is a symfony helper that we will discuss tomorrow.

The Job Page Template

Now let's customize the template of the job page. Open the showSuccess.php file and replace its content with the following code:

<?php use_stylesheet('job.css') ?>
<?php use_helper('Text') ?>
 
<div id="job">
  <h1><?php echo $job->getCompany() ?></h1>
  <h2><?php echo $job->getLocation() ?></h2>
  <h3>
    <?php echo $job->getPosition() ?>
    <small> - <?php echo $job->getType() ?></small>
  </h3>
 
  <?php if ($job->getLogo()): ?>
    <div class="logo">
      <a href="<?php echo $job->getUrl() ?>">
        <img src="<?php echo $job->getLogo() ?>"
          alt="<?php echo $job->getCompany() ?> logo" />
      </a>
    </div>
  <?php endif; ?>
 
  <div class="description">
    <?php echo simple_format_text($job->getDescription()) ?>
  </div>
 
  <h4>How to apply?</h4>
 
  <p class="how_to_apply"><?php echo $job->getHowToApply() ?></p>
 
  <div class="meta">
    <small>posted on <?php echo $job->getCreatedAt('m/d/Y') ?></small>
  </div>
 
  <div style="padding: 20px 0">
    <a href="<?php echo url_for('job/edit?id='.$job->getId()) ?>">Edit</a>
  </div>
</div>
 

This template uses the $job variable passed by the action to display the job information. As we have renamed the variable passed to the template from $jobeet_job to $job, you need to also make this change in the show action (be careful, there are two occurrences of the variable):

// apps/frontend/modules/job/actions/actions.class.php
public function executeShow(sfWebRequest $request)
{
  $this->job = JobeetJobPeer::retrieveByPk($request->getParameter('id'));
  $this->forward404Unless($this->job);
}
 

Notice that some Propel accessors take arguments. As we have defined the created_at column as a timestamp, the getCreatedAt() accessor takes a date formatting pattern as its first argument:

$job->getCreatedAt('m/d/Y');
 

The job description uses the simple_format_text() helper to format it as HTML, by replacing carriage returns with <br /> for instance. As this helper belongs to the Text helper group, which is not loaded by default, we have loaded it manually by using the use_helper() helper.

Slots

Right now, the title of all pages is defined in the <title> tag of the layout:

<title>Jobeet - Your best job board</title>
 

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 slot:

Slots

Add a slot to the layout to allow the title to be dynamic:

// apps/frontend/templates/layout.php
<title><?php include_slot('title') ?></title>
 

Each slot is defined by a name (title) and can be displayed by using the include_slot() helper. Now, at the beginning of the showSuccess.php template, use the slot() helper to define the content of the slot for the job page:

// apps/frontend/modules/job/templates/showSuccess.php
<?php slot('title', sprintf('%s is looking for a %s', $job->getCompany(), $job->getPosition())) ?>
 

If the title is complex to generate, the slot() helper can also be used with a block of code:

// apps/frontend/modules/job/templates/showSuccess.php
<?php slot('title') ?>
  <?php echo sprintf('%s is looking for a %s', $job->getCompany(), $job->getPosition()) ?>
<?php end_slot(); ?>
 

For some pages, like the homepage, we just need a generic title. Instead of repeating the same title over and over again in templates, we can define a default title in the layout:

// apps/frontend/templates/layout.php
<title>
  <?php if (!include_slot('title')): ?>
    Jobeet - Your best job board
  <?php endif; ?>
</title>
 

The include_slot() helper returns true if the slot has been defined. So, when you define the title slot content in a template, it is used; if not, the default title is used.

We have already seen quite a few helpers beginning with include_. These helpers output the HTML and in most cases have a get_ helper counterpart to just return the content:

<?php include_slot('title') ?>
<?php echo get_slot('title') ?>
 
<?php include_stylesheets() ?>
<?php echo get_stylesheets() ?>
 

The Job Page Action

The job page is generated by the show action, defined in the executeShow() method of the job module:

class jobActions extends sfActions
{
  public function executeShow(sfWebRequest $request)
  {
    $this->job = JobeetJobPeer::retrieveByPk($request->getParameter('id'));
    $this->forward404Unless($this->job);
  }
 
  // ...
}
 

As in the index action, the JobeetJobPeer class is used to retrieve a job, this time by using the retrieveByPk() method. The parameter of this method is the unique identifier of a job, its primary key. The next section will explain why the $request->getParameter('id') statement returns the job primary key.

The generated model classes contain a lot of useful methods to interact with the project objects. Take some time to browse the code located in the lib/om/ directory and discover all the power embedded in these classes.

If the job does not exist in the database, we want to forward the user to a 404 page, which is exactly what the forward404Unless() method does. It takes a Boolean as its first argument and, unless it is true, stops the current flow of execution. As the forward methods stops the execution of the action right away by throwing a sfError404Exception, you don't need to return afterwards.

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

404 error in the dev environment

404 error in the prod environment

Before we deploy the Jobeet website to the production server, you will learn how to customize the default 404 page.

The Request and the Response

When you browse to the /job or /job/show/id/1 pages in your browser, your are initiating a round trip with the web server. The browser is sending a request and the server sends back a response.

We have already seen that symfony encapsulates the request in a sfWebRequest object (see the executeShow() method signature). And as symfony is an Object-Oriented framework, the response is also an object, of class sfWebResponse. You can access the response object in an action by calling $this->getResponse().

These objects provide a lot of convenient methods to access information from PHP functions and PHP global variables.

Why does symfony wrap existing PHP functionalities? First, because the symfony methods are more powerful than their PHP counterpart. Then, because when you test an application, it is much more easier to simulate a request or a response object than trying to fiddle around with global variables or work with PHP functions like header() which do too much magic behind the scene.

The Request

The sfWebRequest class wraps the $_SERVER, $_COOKIE, $_GET, $_POST, and $_FILES PHP global arrays:

Method name PHP equivalent
getMethod() $_SERVER['REQUEST_METHOD']
getUri() $_SERVER['REQUEST_URI']
getReferer() $_SERVER['HTTP_REFERER']
getHost() $_SERVER['HTTP_HOST']
getLanguages() $_SERVER['HTTP_ACCEPT_LANGUAGE']
getCharsets() $_SERVER['HTTP_ACCEPT_CHARSET']
isXmlHttpRequest() $_SERVER['X_REQUESTED_WITH'] == 'XMLHttpRequest'
getHttpHeader() $_SERVER
getCookie() $_COOKIE
isSecure() $_SERVER['HTTPS']
getFiles() $_FILES
getGetParameter() $_GET
getPostParameter() $_POST
getUrlParameter() $_SERVER['PATH_INFO']
getRemoteAddress() $_SERVER['REMOTE_ADDR']

We have already accessed request parameters by using the getParameter() method. It returns a value from the $_GET or $_POST global variable, or from the PATH_INFO variable.

If you want to ensure that a request parameter comes from a particular one of these variables, you need use the getGetParameter(), getPostParameter(), and getUrlParameter() methods respectively.

When you want to restrict an action for a specific method, for instance when you want to ensure that a form is submitted as a POST, you can use the isMethod() method: $this->forwardUnless($request->isMethod('POST'));.

The Response

The sfWebResponse class wraps the header() and setrawcookie() PHP methods:

Method name PHP equivalent
setCookie() setrawcookie()
setStatusCode() header()
setHttpHeader() header()
setContentType() header()
addVaryHttpHeader() header()
addCacheControlHttpHeader() header()

Of course, the sfWebResponse class also provides a way to set the content of the response (setContent()) and send the response to the browser (send()).

Earlier today we saw how to manage stylesheets and JavaScripts in both the view.yml file and in templates. In the end, both techniques use the response object addStylesheet() and addJavascript() methods.

The sfAction, sfRequest, and sfResponse classes provide a lot of other useful methods. Don't hesitate to browse the API documentation to learn more about all symfony internal classes.

See you Tomorrow

Today, we have described some design patterns used by symfony. Hopefully the project directory structure now makes more sense. We have played with the templates by manipulating the layout and the template files. We have also made them a bit more dynamic thanks to slots and actions.

Tomorrow, we will learn more about the url_for() helper we have used today, and the routing sub-framework associated with it.

Until then, feel free to browse the source of today's tutorial (tag release_day_04) at:

http://svn.jobeet.org/tags/release_day_04/
Published in #Tutorials