Day 14: Feeds
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. If you are looking for a job, you will probably want to be informed as soon as a new job is posted. Because it is not very convenient to check the website every other hour, we will add several job feeds here to keep our Jobeet users up-to-date.
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://www.jobeet.com.localhost/frontend_dev.php/job
This URL is equivalent to:
http://www.jobeet.com.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 and
because html
is the default format. You can check it for yourself by running
the app:routes
task:
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>
sidebar
Template Names
As html
is the most common format used for web applications, it can be
omitted from the template name. Both indexSuccess.php
and
indexSuccess.html.php
templates are equivalent and symfony uses the first one
it finds.
Why are default templates suffixed with Success
? An action can return a value
to indicate which template to render. If the action returns nothing, it is
equivalent to the following code:
return sfView::SUCCESS; // == 'Success'
If you want to change the suffix, just return something else:
return sfView::ERROR; // == 'Error' return 'Foo';
As seen yesterday, the name of the template can also be changed by using the
setTemplate()
method:
$this->setTemplate('foo');
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', array('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 to allow automatic discover
by the browser of our feed:
<!-- apps/frontend/templates/layout.php --> <link rel="alternate" type="application/atom+xml" title="Latest Jobs" href="<?php echo url_for('job', array('sf_format' => 'atom'), true) ?>" />
For the link href
attribute, an URL (Absolute) is used thanks to the second
argument of the url_for()
helper.
Replace the Atom template header with the following code:
<!-- apps/frontend/modules/job/templates/indexSuccess.atom.php --> <title>Jobeet</title> <subtitle>Latest Jobs</subtitle> <link href="<?php echo url_for('job', array('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', array('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 use_helper('Text') ?> <?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.
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> <!-- 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/modules/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().'/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 ?>
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', array('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', array('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() { return $this->getActiveJobs(1)->getFirst(); } // ... }
Final Thoughts
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.
This work is licensed under the Creative Commons Attribution-Share Alike 3.0 Unported License license.