Building a Complete CodeIgniter Application: Part 2
Jim O'Halloran • September 23, 2007
php codeigniter feedignitionThis is the second installment in a series called "Building a Complete CodeIgniter application". In this series I'll walk readers through the construction of a complete AJAX application using the CodeIgniter framework. I've chosen to build a multi-user Feed Reader, which I'll call "Feedignition". Feed Readers seem to be the new "hello world", and there's good feed parsing libraries available which allow us to concentrate on the application itself without having to worry about the myriad of details involved in actually parsing of a feed. That leaves us free to explore a number of topics which will be of interest to anyone building applications with CodeIgniter.
In the last part of this series we created the foundations on which we'll build the FeedIgnition aggregator. We installed the basic CI framework, and set up our database connections. When we finished up we have an app that did absolutely nothing, every possible URL resulted in a 404 error. However, this was necessary to give us a base on which we can build our feed reader, now we'll get down to the nuts and bolts of actually building an app in CodeIgniter. Before we get started, you'll need to make sure you've worked through part 1 and have CI + a database ready to go.
In this part we'll build an extremely basic feed reader. We'll show you how to integrate third party classes into the CodeIgniter framework, and construct Models, Views and Controllers to work with our feeds and their items. It may be helpful to review my earlier post describing "How I use CodeIgniter MVC" before you go too much further, as this will help to explain why some things are where they are. In a nutshell though, here's how I use each component:
- Views - Generate everything the user might see, including HTML, email text, and PDFs.
- Models - Handle access to data sources, such as database tables and external API's.
- Controlers - Glue models and views together, handling form input and view selection.
- Libraries - Generic classes which need to be shared between one or more models, views or controllers.
- Helpers - Generic functions (not class based) which are shared between models views and controllers.
Feed Parsing? Simple as Pie!
The SimplePie class is a nice object oriented library which handles the download, and parsing of feeds. By using SimplePie we can get our feed reader off the ground without having to understand the nuts and bolts of parsing feeds, we don't need to learn the nuances involved in parsing different versions of RSS and Aton, we just throw a URL at SimplePie and let it do the heavy lifting for us. Much easier!
We'll need to download SimplePie from http://simplepie.org/ and unpack it into a temporary directory.
cd /tmp wget http://simplepie.org/downloads/?download
unzip simplepie_1.0.1.zip
External class libraries are quite easy to integrate into CodeIgniter, SimplePie especially. We need to ensure that:
- The main class file conforms to the naming conventions use by CodeIgniter.
- Any additional files required by the libraries are included from the main class file.
In the case of SimplePie, we only have to worry about one file (simplepie.inc). The main simplepie class is called "SimplePie", so to conform to CI standards we should move simplepie.inc into the system/application/libraries directory and rename it to simplepie.php:
cp /tmp/SimplePie 1.0.1/simplepie.inc /var/www/system/application/libraries/simplepie.php
That's all there is to it! SimplePie can now be loaded and accessed like any other CI library:
$this->load->library('simplepie');
$this->simplepie->do_stuff();
Simple Test Application
Now that we've got a feed parsing library which can do the heavy lifting, lets take it for a spin. We'll create a simple controller/view combination which can download the CodeIgniter news feed and display a simple bulleted list of news stories. We'll then use this as a starting point, and evolve it into our application.
First create a file called "feed.php" in the system/application/controllers directory. This will be our "feed controller". It should look like this: ```
<?php
if (!defined('BASEPATH')) {exit('No direct script access allowed');}
class Feed extends Controller {
function __construct() {
parent::Controller();
}
function view() {
$this->load->library('simplepie');
$this->simplepie->cache_location = BASEPATH .'cache';
$this->simplepie->set_feed_url('http://codeigniter.com/feeds/atom/full/');
$this->simplepie->init();
$this->load->view('feed/view', array('items' => $this->simplepie->get_items()));
}
}
This controller class will respond to URLs beginning with http://localhost/feed/ , and the view function will be called when the browser requests http://localhost/feed/view. The view method first loads the SimplePie class we created earlier, and sets it's cache path. Doing this causes SimplePie to use the standard CI cache folder rather than trying to create it's own cache. We set the URL to the CI feed, then call init() to get SimplePie to download and parse that feed. Finally, we load up a view and give the view access to all of our feed's items. We use a view here because we want to send some output to the browser. We could simply "echo" the output from the controller, but one of the goals of MVC frameworks is the separation of presentation from business logic. We use views to acheive that separation, and keep our html out of our controllers and models.
To actually get some output, we need to create our view. First create a folder called "feed" under system/application/views, then create a file called " view.php" in the new folder. As a matter of personal preference I like to store my views in a folder with the same name as the controller which will utilise them. Wherever practical, I like to name the view file after the method from which it will be used. Views don't need to be tied directly to controller methods, it may be possible in sone instances to create generic views. However, when the views and the controller are linked, I like to use a consistent naming convention.
The view file should contain the following:
<html>
<head>
<title>Feed Parser Test</title>
</head>
<body>
<b>CodeIgniter Feed</b>
<ul>
<?php
foreach($items as $item) {
echo "<li><a href='" .$item->get_link() . "'>" . $item->get_title() . "</a></li>";
}
?>
</li></ul>
</body>
</html>
If you open your browser and go to http://localhost/feed/view you should see a simple list of CI News items and a link to each. CI will use SimplePie to download and parse that feed everytime you request the page, so this won't scale to lots of users or feeds very well, but as a simple test it's pretty good. Now lets dig into some of CI's database functionality and really start to get this thing rolling!
Storing Feeds and Feed Items in the DB
What we really want to do with our feed reader is store our feeds URLs and their items in a database. That way when a user logs into the feed reader we've already got their news ready for tem on our server and we don't need to continually fetch the user's feeds. If we store the feed items in the database, then if a user doesn't read the their news for a while we can show them their unread items, even if they disappear from the feeds.
We'll need to create two database tables initially. One to store the Feed information, and a second for the items (e.g. news articles, blog posts, etc) we get from those feeds. Use phpMyAdmin, or the mysql command line tools to create the two database tables we need using the following SQL statements:
CREATE TABLE `feeds`
(
`id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
`feed_url` VARCHAR(255) NOT NULL,
`site_url` VARCHAR(255) NOT NULL,
`feed_name` VARCHAR(100) NOT NULL
) ENGINE = MYISAM;
CREATE TABLE `items`
(
`id` int(11) NOT NULL auto_increment,
`feed_id` int(11) NOT NULL,
`remote_id` varchar(32) NOT NULL,
`title` varchar(255) NOT NULL,
`link` varchar(255) NOT NULL,
`text` text NOT NULL,
`updated_time` datetime NOT NULL,
`created_time` datetime NOT NULL,
PRIMARY KEY (`id`)
) ENGINE = MyISAM;
The feeds table we'll use for basic feed information, and we'll expand that later with some additional data. The items table will be used for the news items, etc we get from those feeds. Again we'll add more fields to the items table later, but this will get us started. I don't want to build a subscription interface at the moment, so lets take a moment to insert a couple of feeds into our "feeds" table so we've got something to work with later:
INSERT INTO `feeds` (`id`, `feed_url`, `site_url`, `feed_name`)
VALUES (NULL, 'http://codeigniter.com/feeds/atom/full/',
'http://codeigniter.com/', 'CodeIgniter News Feed'),
(NULL, 'http://www.jimohalloran.com/feed/atom/',
'http://www.jimohalloran.com/', 'Jim O''Halloran''s Blog');
What we want to do now is to set up a controller method which gets a list of feeds to update, fetches the feeds and inserts the items in the feed into our new items table. We've got a database which we'll use for storing and retreiving information, so it's time to introduce models. I like to use one model for each of my database tables, so we'll use two models here, a Feeds model which we'll use to get a list of feed URLs and a FeedItems model which we'll use to insert data into our items table. The FeedItems model will also be used to retreive our feed items later on and display them to the end user.
Models live in the system/application/models directory. Their file name needs to be the same as the lowercase name of the class, and end in .php. To to create FeedModel, create a file called feedmodel.php in system/application/models with the following contents:
class FeedModel extends Model {
function __construct() {
parent::Model();
}
function get_feed_update_urls() {
$rs = $this->db->query('SELECT id, feed_url FROM feeds');
$feeds = array();
if ($rs->num_rows() > 0) {
foreach ($rs->result_array() as $row) {
$feed[$row['id']] = $row['feed_url'];
}
}
return $feed;
}
}
This class contains a single method which will return an array of feed URL's which we'll use in our update routine. Later on we'll flash this out and add some query function, subscription methods and so forth, but it will do for now.
Next we need to create the FeedItemModel class, create a file in the system/application/models directory called feeditemmodel.php with the following contents.
< ?php if (!defined('BASEPATH')) {exit('No direct script access allowed');}
class FeedItemModel extends Model {
private $_id = false;
public $feed_id;
public $remote_id;
public $link;
public $title;
public $text;
public $created_time;
public $updated_time;
function __construct() {
parent::Model();
}
public function reset() {
$this->_id = false;
$this->feed_id = 0;
$this->remote_id = '';
$this->link = '';
$this->title = '';
$this->text = '';
$this->created_time = localtime();
$this->updated_time = localtime();
}
public function load($feed_id, $remote_id) {
$rs = $this->db->query('SELECT * FROM items WHERE feed_id=? AND remote_id=?', array($feed_id, $remote_id));
if ($rs->num_rows() > 0) {
$row = $rs->row_array();
$this->_id = $row['id'];
$this->feed_id = $feed_id;
$this->remote_id = $remote_id;
$this->link = $row['link'];
$this->title = $row['title'];
$this->text = $row['text'];
$this->created_time = strtotime($row['created_time']);
$this->updated_time = strtotime($row['updated_time']);
return true;
} else {
$this->reset();
$this->feed_id = $feed_id;
$this->remote_id = $remote_id;
return false;
}
}
function save() {
if ($this->_id !== false) {
$this->db->query('UPDATE items SET link=?, title=?, text=?, updated_time=NOW() WHERE id=?', array($this->link, $this->title, $this->text, $this->_id));
} else {
$this->db->query('INSERT INTO items(feed_id, remote_id, link, title, text, created_time, updated_time) VALUES(?, ?, ?, ?, ?, NOW(), NOW())', array($this->feed_id, $this->remote_id, $this->link, $this->title, $this->text));
$this->_id = $this->db->insert_id();
}
}
function get_items($offset, $num_per_page) {
$rs = $this->db->query('SELECT count(*) as total FROM items', array($num_per_page, $offset));
if ($rs->num_rows() > 0) {
$row = $rs->row_array();
$total = $row['total'];
$rs = $this->db->query('SELECT * FROM items ORDER BY updated_time DESC LIMIT ? OFFSET ?', array($num_per_page, $offset));
return array('total' => $total, 'items' => $rs->result_array());
} else {
return array('total' => 0, 'items' => array());
}
}
}
?>
This is the FeedItemModel. This one is more complex than the FeedModel. See my How I use CodeIgniter MVC" piece for some background. We have a series of public member variables which will store the feed details we're going to collect at this point. We have a load method which given a feed id and unique id will retrieve any existing data from the database. If the load method does not find an existing record, the model state is emptied in preparation for a new record to be inserted. As it's name suggests, the save method saves that data back to the database. Save() will automatically handle inserts/updates. The reset() method can be used to reset the model ready to create another record. It's used internally by load(), but it can also be called directly.
Finally the controller method which binds it all together: ```
function update_all() {
$this->load->library('simplepie');
$this->simplepie->cache_location = BASEPATH .'cache';
$this->load->model('FeedModel');
$this->load->model('FeedItemModel');
$feeds = $this->FeedModel->get_feed_update_urls();
foreach ($feeds as $feed_id => $feed_url) {
$this->simplepie->set_feed_url($feed_url);
$this->simplepie->init();
$items = $this->simplepie->get_items();
foreach ($items as $item) {
$this->FeedItemModel->load($feed_id, md5($item->get_id()));
$this->FeedItemModel->link = $item->get_permalink();
$this->FeedItemModel->title = $item->get_title();
$this->FeedItemModel->text = $item->get_content();
$this->FeedItemModel->save();
}
}
}
This method loads the SimplePie library we created earlier, then used the FeedModel object to get a list of feeds. Next we retrieve each of these individually and use the FeedItemModel class to insert each item into the database. Because the load method resets the object for us if the feed item isn't found in the database we can safely ignore load()'s return value and just assign our values and save(). This makes our controller code much simpler!
Test out the update all method by going to http://localhost/feed/update_all. If you see a blank page it works fine! We'll avoid sending any output for the update_all method because we'll call it later from a cron job rather than a web browser. If everything worked you should now have a series of records in the items table of your database!
Now lets get those records out and display them to the end user, they're not doing us much good just sitting there in the database! Let's return to our FeedItemModel class and add in a new get_items method. Eventually, there might be a large number of feeds with a larger number of items, so we'll want to use pagination. This means we'll need to tell our get_items method to retrieve a range of records, but we'll also want to know how many records there are in total so we can generate the appropriate pagination links. Our get_items() method returns an array containing a total and an array of items on success. If no records were found we return the total as 0 and an ampty array. This means that unless we specifically want to display a "no items found" method later on in our view, we can virtually ignore the empty database case.
function get_items($offset, $num_per_page) {
$rs = $this->db->query('SELECT count(*) as total FROM items', array($num_per_page, $offset));
if ($rs->num_rows() > 0) {
$row = $rs->row_array();
$total = $row['total'];
$rs = $this->db->query('SELECT * FROM items ORDER BY updated_time DESC LIMIT ? OFFSET ?', array($num_per_page, $offset));
return array('total' => $total, 'items' => $rs->result_array());
} else {
return array('total' => 0, 'items' => array());
}
}
Now that we can use our model to get feed items from the database, lets tweak our controller and view to turn this into a "River of News" style display. First the controller. We want to define the number of items per page, rather than hard code that wherever we need it, lets define it as a constant at the top of the controller:
class Feed extends Controller {
const ITEMS_PER_PAGE = 20;
Now, lets go back and change the simple view method we created earlier:
function view($offset = 0) {
if (is_numeric($offset)) {
$offset = floor($offset);
} else {
$offset = 0;
}
$this->load->model('FeedItemModel');
$data = $this->FeedItemModel->get_items($offset, Feed::ITEMS_PER_PAGE);
$data['per_page'] = Feed::ITEMS_PER_PAGE;
$this->load->view('feed/view', $data);
}
The first part of the controller method turns the $offset parameter into a proper integer value. This prevents it from being incorrectly quoted later in the get_items method, and prevents the injection of a string (via the URL) into our SQL where an integer was expected. The later parts should be fairly self explanetory. We load our FeedItemModel class and gell it's get_items method, and feed the results into our view.
We'll need a new view to go with this, so lets take a look at that now:
<?php
// Initialise pagination first, then we can just call create_links() later wherever we want the pagination to appear.
$CI = &get_instance();
$CI->load->library('pagination');
$config['base_url'] = site_url('feed/view');
$config['total_rows'] = $total;
$config['per_page'] = $per_page;
$CI->pagination->initialize($config);
?>
<?php $this->load->view('header', array('title' => "Feed View")); ?>
<div class="pagination">
<?php echo $CI->pagination->create_links(); ?>
</div>
<?php foreach ($items as $item) { ?>
<div class="item">
<h1><a href="<?=$item['link'];?>">< ?=$item['title'];?></a></h1>
<p>< ?=$item['text'];?></p>
</div>
<?php } ?>
<div class="pagination">
< ?php echo $CI->pagination->create_links(); ?>
</div>
<?php $this->load->view('footer'); ?>
I've split our original view up into three parts. The feed/view.php file is shown above. This handles the job of displaying the feed content, and creating the pagination. Notice how we're loading the pagination library in the view and generating our pagination using the total records value and the number of records per page which were passed into the controller. I've wrapped up the pagination and each feed item in div's and assigned classes to them. The div's are invisible at the moment, but we'll use them later on in the tutorial series to add some style to our feed viewer.
We're also loading two other views, header.php and footer.php. These will contain the generic header and footer html which we'll use on all of the pages. They look pretty empty now, but we'll flesh them out with external CSS and Javascript, navigation and footer details as this series progresses. The header view is very basic right now:
<html>
<head>
<title>FeedIgnition<?php
if (isset($title)) {
echo " :: ".$title;
} ?></title>
</head>
<body></body></html>
The only feature of note here is the use of the $title variable to allow our main view to alter the page title if it wishes to. The footer.php file is even simpler:
Opening a browser now and going to http://localhost/feed/view should return the 20 most recent posts from the CI web site and my blog in a (pretty ugly) "River of News" style display. The pagination top and bottom of page should also work properly, passing an offset via the URL back to our controller.
One thing you'll find is that at the moment the URL http://localhost/ displays an error:
An Error Was Encountered
Unable to load your default controller. Please make sure the controller specified in your Routes.php file is valid.
Lets fix that up now so that the Feed viewer is our site's default home page. Open up system/application/config/routes.php and look for the line:
$route['default_controller'] = "welcome";
... and change it to ...
$route['default_controller'] = "feed";
This change will load the feed controller and execute the index function of controller/method is specified on the URL, so now we need to get our index function to act the same as the view() method, the easiest way to do this is to siplay call "$this->view()" like so:
function index() {
$this->view();
}
So now we have a simple feed reader. If we visit the http://localhost/feed/update_all URL periodically the app will update our feeds, then we can read them (with pagination) using the feed view we've built. There's still a long way to go! Things we will address in coming parts:
- I want to make the feed updating process transparent to the end user by hooking it up to our system's scheduler (e.g. cron).
- We've got a major security vulnerability at the moment, so we need to escape the data we're displaying to the end user to prevent someone from using cross site scripting (XSS) to interfere with our feed reading experience.
- We should harvest more information from the feeds, including feed level metadata, and the item fields we're not collecting at the moment.
The three items above will complete the basic feature set we already have, but there's a whole lot more I want to cover as part of this tutorial, including:
- Multi-user login/subscription facilities.
- Tagging of feeds.
- Reading feeds a tag at a time or individually as well as River of News style.
- Read/Unread tracking.
If you want to download all of the code created to date, click here to download a zip file containing a SQL dump of the database, the CI framework and all of the code from this part of the series. The version of the code in the zip file also includes phpDocumentor style comments for each of the functions which may help to explain the code. I've cut the comments out of the code on this page to save space.