Jobeet - Day 15: Feeds

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

For the 21st day of Jobeet, we will organize a "design day" contest and the community will have to choose the default design that will be bundled with Jobeet.

If you want to submit an original design, the rules are simple: you must design the website with only stylesheets and images.

To participate, download the archive that contains the main pages we have developed during the tutorial (static HTML files, stylesheets, and images), and submit your design to fabien.potencier [.at.] symfony-project.com before the 21st.

Previously on Jobeet

Yesterday, you started developing your first very own symfony application. Don't stop now. As you learn more on symfony, try to add new features to your application, host it somewhere, and share it with the community.

Let's move on to something completely different today.

If you are looking for a job, you will probably want to be informed as soon as a new job is posted. And it is not very convenient to check the website every other hour. To keep our Jobeet users up-to-date, we will add several job feeds today.

Formats

The symfony framework has native support for formats and mime-types. This means that the same Model and Controller can have different templates based on the requested format. The default format is HTML but symfony supports several other formats out of the box like txt, js, css, json, xml, rdf, or atom.

The format can be set by using the setRequestFormat() method of the request object:

$request->setRequestFormat('xml');
 

But most of the time, the format is embedded in the URL. In this case, symfony will set it for you if the special sf_format variable is used in the corresponding route. For the job list, the list URL is:

http://jobeet.localhost/frontend_dev.php/job

This URL is equivalent to:

http://jobeet.localhost/frontend_dev.php/job.html

Both URLs are equivalent because the routes generated by the sfPropelRouteCollection class have the sf_format as the extension. You can check it for yourself by running the app:routes task:

Cli

Feeds

Latest Jobs Feed

Supporting different formats is as easy as creating different templates. To create an Atom feed for the latest jobs, create an indexSuccess.atom.php template:

<!-- apps/frontend/modules/job/templates/indexSuccess.atom.php -->
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Jobeet</title>
  <subtitle>Latest Jobs</subtitle>
  <link href="" rel="self"/>
  <link href=""/>
  <updated></updated>
  <author><name>Jobeet</name></author>
  <id>Unique Id</id>
 
  <entry>
    <title>Job title</title>
    <link href="" />
    <id>Unique id</id>
    <updated></updated>
    <summary>Job description</summary>
    <author><name>Company</name></author>
  </entry>
</feed>
 

By default, symfony will change the response Content-Type according to the format, and for all non-HTML formats, the layout is disabled. For an Atom feed, symfony changes the Content-Type to application/atom+xml; charset=utf-8.

In the Jobeet footer, update the link to the feed:

<!-- apps/frontend/templates/layout.php -->
<li class="feed">
  <a href="<?php echo url_for('@job?sf_format=atom') ?>">Full feed</a>
</li>
 

The internal URI is the same as for the job list with the sf_format added as a variable.

Add a <link> tag in the head section of the layout:

<!-- apps/frontend/templates/layout.php -->
<link rel="alternate" type="application/atom+xml" title="Latest Jobs"
  href="<?php echo url_for('@job?sf_format=atom', true) ?>" />
 

For the link href attribute, an absolute URL is used thanks to the second argument of the url_for() helper.

Let's update the Atom template header:

<!-- apps/frontend/modules/job/templates/indexSuccess.atom.php -->
<title>Jobeet</title>
<subtitle>Latest Jobs</subtitle>
<link href="<?php echo url_for('@job?sf_format=atom', true) ?>" rel="self"/>
<link href="<?php echo url_for('@homepage', true) ?>"/>
<updated><?php echo gmstrftime('%Y-%m-%dT%H:%M:%SZ', JobeetJobPeer::getLatestPost()->getCreatedAt('U')) ?></updated>
<author>
  <name>Jobeet</name>
</author>
<id><?php echo sha1(url_for('@job?sf_format=atom', true)) ?></id>
 

Notice the usage of U as an argument to getCreatedAt() to get the date as a timestamp. To get the date of the latest post, create the getLatestPost() method:

// lib/model/JobeetJobPeer.php
class JobeetJobPeer extends BaseJobeetJobPeer
{
  static public function getLatestPost()
  {
    $criteria = new Criteria();
    self::addActiveJobsCriteria($criteria);
 
    return JobeetJobPeer::doSelectOne($criteria);
  }
 
  // ...
}
 

The feed entries can be generated with the following code:

<!-- apps/frontend/modules/job/templates/indexSuccess.atom.php -->
<?php foreach ($categories as $category): ?>
  <?php foreach ($category->getActiveJobs(sfConfig::get('app_max_jobs_on_homepage')) as $job): ?>
    <entry>
      <title>
        <?php echo $job->getPosition() ?> (<?php echo $job->getLocation() ?>)
      </title>
      <link href="<?php echo url_for('job_show_user', $job, true) ?>" />
      <id><?php echo sha1($job->getId()) ?></id>
      <updated><?php echo gmstrftime('%Y-%m-%dT%H:%M:%SZ', $job->getCreatedAt('U')) ?></updated>
      <summary type="xhtml">
       <div xmlns="http://www.w3.org/1999/xhtml">
         <?php if ($job->getLogo()): ?>
           <div>
             <a href="<?php echo $job->getUrl() ?>">
               <img src="http://<?php echo $sf_request->getHost().'/uploads/jobs/'.$job->getLogo() ?>"
                 alt="<?php echo $job->getCompany() ?> logo" />
             </a>
           </div>
         <?php endif; ?>
 
         <div>
           <?php echo simple_format_text($job->getDescription()) ?>
         </div>
 
         <h4>How to apply?</h4>
 
         <p><?php echo $job->getHowToApply() ?></p>
       </div>
      </summary>
      <author>
        <name><?php echo $job->getCompany() ?></name>
      </author>
    </entry>
  <?php endforeach; ?>
<?php endforeach; ?>
 

The getHost() method of the request object ($sf_request) returns the current host, which comes in handy for creating an absolute link for the company logo.

Feed

When creating a feed, debugging is easier if you use command line tools like curl or wget, as you see the actual content of the feed.

Latest Jobs in a Category Feed

One of the goals of Jobeet is to help people find more targeted jobs. So, we need to provide a feed for each category.

First, let's update the category route to add support for different formats:

// apps/frontend/config/routing.yml
category:
  url:     /category/:slug.:sf_format
  class:   sfPropelRoute
  param:   { module: category, action: show, sf_format: html }
  options: { model: JobeetCategory, type: object }
  requirements:
    sf_format: (?:html|atom)
 

Now, the category route will understand both the html and atom formats. Update the links to category feeds in the templates:

<!-- apps/frontend/modules/job/templates/indexSuccess.php -->
<div class="feed">
  <a href="<?php echo url_for('category', array('sf_subject' => $category, 'sf_format' => 'atom')) ?>">Feed</a>
</div>
 
[php]
<!-- apps/frontend/modules/category/templates/showSuccess.php -->
<div class="feed">
  <a href="<?php echo url_for('category', array('sf_subject' => $category, 'sf_format' => 'atom')) ?>">Feed</a>
</div>
 

The last step is to create the showSuccess.atom.php template. But as this feed will also list jobs, we can refactor the code that generates the feed entries by creating a _list.atom.php partial. As for the html format, partials are format specific:

<!-- apps/frontend/job/templates/_list.atom.php -->
<?php use_helper('Text') ?>
 
<?php foreach ($jobs as $job): ?>
  <entry>
    <title><?php echo $job->getPosition() ?> (<?php echo $job->getLocation() ?>)</title>
    <link href="<?php echo url_for('job_show_user', $job, true) ?>" />
    <id><?php echo sha1($job->getId()) ?></id>
    <updated><?php echo gmstrftime('%Y-%m-%dT%H:%M:%SZ', $job->getCreatedAt('U')) ?></updated>
    <summary type="xhtml">
     <div xmlns="http://www.w3.org/1999/xhtml">
       <?php if ($job->getLogo()): ?>
         <div>
           <a href="<?php echo $job->getUrl() ?>">
             <img src="http://<?php echo $sf_request->getHost().$job->getLogo() ?>" alt="<?php echo $job->getCompany() ?> logo" />
           </a>
         </div>
       <?php endif; ?>
 
       <div>
         <?php echo simple_format_text($job->getDescription()) ?>
       </div>
 
       <h4>How to apply?</h4>
 
       <p><?php echo $job->getHowToApply() ?></p>
     </div>
    </summary>
    <author>
      <name><?php echo $job->getCompany() ?></name>
    </author>
  </entry>
<?php endforeach; ?>
 

You can use the _list.atom.php partial to simplify the job feed template:

<!-- apps/frontend/modules/job/templates/indexSuccess.atom.php -->
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Jobeet</title>
  <subtitle>Latest Jobs</subtitle>
  <link href="<?php echo url_for('@job?sf_format=atom', true) ?>" rel="self"/>
  <link href="<?php echo url_for('@homepage', true) ?>"/>
  <updated><?php echo gmstrftime('%Y-%m-%dT%H:%M:%SZ', JobeetJobPeer::getLatestPost()->getCreatedAt('U')) ?></updated>
  <author>
    <name>Jobeet</name>
  </author>
  <id><?php echo sha1(url_for('@job?sf_format=atom', true)) ?></id>
 
<?php foreach ($categories as $category): ?>
  <?php include_partial('job/list', array('jobs' => $category->getActiveJobs(sfConfig::get('app_max_jobs_on_homepage')))) ?>
<?php endforeach; ?>
</feed>
 

Eventually, create the showSuccess.atom.php template:

<!-- apps/frontend/modules/category/templates/showSuccess.atom.php -->
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Jobeet (<?php echo $category ?>)</title>
  <subtitle>Latest Jobs</subtitle>
  <link href="<?php echo url_for('category', array('sf_subject' => $category, 'sf_format' => 'atom'), true) ?>" rel="self" />
  <link href="<?php echo url_for('category', array('sf_subject' => $category), true) ?>" />
  <updated><?php echo gmstrftime('%Y-%m-%dT%H:%M:%SZ', $category->getLatestPost()->getCreatedAt('U')) ?></updated>
  <author>
    <name>Jobeet</name>
  </author>
  <id><?php echo sha1(url_for('category', array('sf_subject' => $category), true)) ?></id>
 
  <?php include_partial('job/list', array('jobs' => $pager->getResults())) ?>
</feed>
 

As for the main job feed, we need the date of the latest job for a category:

// lib/model/JobeetCategory.php
class JobeetCategory extends BaseJobeetCategory
{
  public function getLatestPost()
  {
    $jobs = $this->getActiveJobs(1);
 
    return $jobs[0];
  }
 
  // ...
}
 

Category Feed

See you Tomorrow

As with many symfony features, the native format support allows you to add feeds to your websites without effort.

Today, we have enhanced the job seeker experience. Tomorrow, we will see how to provide greater exposure to the job posters by providing a Web Service.

Comments

had to insert:


In the indexSuccess.atom.php to use the simple_format_text function
That was only needed with the first version, with the introduction of the partial this is actually fixed (the use_helper is added there)
What about Doctrine version? Still stuck at day 9...
er... I thought symfony was about not reinventing the wheel? Today's tutorial reinvents the sfFeedPlugin, which has been one of the first plugin for symfony. I understand the purpose of demonstrating the multiformat feature, but doing it on an atom feed is very misleading.
I got a Parse error: syntax error, unexpected T_STRING

why?


i fixed it with a php echo on the first line of the indexSuccess.atom.php


@Gianko: Try to set "short_tags = Off" in your php.ini. It works for me.
Thanks for making the latest Doctrine versions available !

Comments are closed.

To ensure that comments stay relevant, they are closed for old posts.