Creating a Flat-File Blog Engine Using Slim

Creating a Flat-File Blog Engine Using Slim

Tutorial Details
  • Language: PHP
  • Version : 5.3
  • Difficulty: Medium

Final Product What You'll Be Creating

In this article, we will learn how to create a flat file blog engine in PHP using the Slim framework. If you don’t understand the jargon, don’t worry. We’re simply going to create a spartan blogging application, which utilizes text files instead of a database to store data.

If you’re a beginner, fret not! We will start with the basics of using Slim. Let’s get started after the jump!


Introduction

Slim is a light weight RESTful PHP framework for creating simple websites. It’s inspired from Sinatra, a framework written in Ruby. Slim ships with some minimal components, like Request, Response and View which are the only required components in our flat-file blog engine.


Step 1: Getting Started

To get started, let’s set up a hello world application. Before that, you need to download the Slim framework to your system. We are not going to discuss a lot about Slim since it’s already been covered here at Nettuts+. You should ideally have the following files in you folder:

  • Slim/ – Slim framework
  • Index.php – The index file
  • .htaccess – For URL rewriting

Now open up index.php, where you will see a bunch of HTML inside the following section. I have removed all of it and replaced it with “Hello world”. You index.php should hopefully look like this now.

require 'Slim/Slim.php';
$app = new Slim();
$app->get('/', function () {
    echo "<h1>Hello world</h1>";
});
$app->run();

I’ve created a Virtualhost in my machine which is the URL shown in the screenshot. Adjust the URL based on the location of Slim in your machine.

Navigate to the URL and you’ll see a home page with the text “Hello world” in it.

get() is a Slim method, which will take a route as the first argument and a corresponding callback function as the last. Other than get, we can have routes for the POST, PUT and DELETE verbs too. As Slim supports PHP 5.3, the callback function can be, and is, written as an anonymous function.


Step 2: Render a PHP Page

The next step is to render a PHP file. Before we continue, create a directory called templates to store all our template files. Any static HTML or template files we create will be placed in this folder. Slim allows us to specify the path to our template files in its configuration. We can add a configuration as shown below.

$app->config(array(
   'templates.path' => './templates'
));

Let’s create an about page for our blog. Create a PHP file called about.php and place it in the templates folder. Add the following code to it:

<html>
<head>
	<title>A Slim Blog engine</title>
</head>
<body>
	<h1>About page</h1>
	<p>
		This page is an example of static route, rendering a php file.
	</p>
</body>
</html>

For Slim to process a request, we need to define a corresponding Route that can map to that URL. In our first step, we have added a route to the index or ‘/” . Now let’s add another route for our about page.

$app->get('/about', function () use ($app) {
    $app->render('about.php');
});

If you load up http://slim.local/about in your browser, it should hopefully display something like this:

We have added a route to the about page, which will display about.php located at ./templates (remember the template.path config variable?). Did you notice that we used use ($app) inside the get method? Well, this is just a way to use a variable inside an anonymous function which is outside its scope.


Step 3: Assign Values to the Template

Now we can display a template page for a route. It’s time to think about displaying some dynamic values in the template, as we may not be rendering and serving static files all the time. We can assign values to be rendered in a template from the render() method. It should be passed as the second parameter as an associative array. Let’s change the above code to look like so:

$app->get('/about', function () use ($app) {
    $data = array(
	    'heading' => 'About page',
	    'message' => 'This page is an example of static route, rendering a php file.'
	);
    $app->render('about.php',$data);
});

And change the body of the template a little bit.

<body>
	<h1><?php echo $heading; ?></h1>
	<p>
		<?php echo $message; ?>
	</p>
</body>

You’ll notice that you can see the same page if you reload the previous URL. Variables used in the template file are the corresponding keys in the associative array.


Step 4: Add a Dynamic Route

So far, we’ve been playing with some static routes, ‘/’ and ‘/about’. Now we are going to create a dynamic route, i.e. a route that can respond to different URLs.

$app->get('/:param1/:param2', function ($param1,$param2) use ($app) {
    echo $param1 .' - ' . $param2;
});

Slim always invokes the first route that matches the current HTTP request. This means that all static routes must be defined before dynamic routes.

If you load up http://slim.local/first-param/second-param in your browser, it will display first-param - second-param.

A variable’s route must start with a ‘:‘. Slim will pass the value of this variable as an argument to our callback function, so that we can parse it and perform an appropriate operation. Slim expects exactly two parameters for the above route since the two route variables are mandatory. If it’s not present, Slim will display a 404 error. We can make a URL parameter optional as shown in the next step.


Step 5: Optional Route Parameters

To make a route parameter optional, rewrite the above code as shown below:

$app->get('/:param1(/:param2(/:param3))', function () use ($app) {
	$args = func_get_args();
	foreach($args as $arg){
		echo $arg . ' -- ';	
	}
});

With that, the second and third parameters are optional. We can use the func_get_args()method to obtain all arguments that are passed into the callback function.

Alright, it’s time to get into about some serious business. Now we have all the required information to create a flat-file blog engine. Let’s put the above discussed knowledge together to create it.


Step 6: Create a File System

We need to create a file-system before we put all these different components together. Here is a simple file-system for our application.

This is a minimalistic file-system with only the required files/folders. All articles will be kept in the articles folder. The assets folder will house our CSS and JavaScript files as well as images. Slim will contain the framework and template files.


Step 7: Add a Route for the Article View

If you’re feeling a little lost, here is an quick bird’s eye view of how our application is structured.

  • The article will be written in a text file with the url as its file name.
  • Our application will map URLs to corresponding articles.
  • For our convenience, we will keep the meta information (like the author name, date etc.) of an article inside the text file in JSON. It will help us retrieve them using PHP’s inbuilt json_decode() function. Content and metadata will be separated with a blank line.

Let’s add a route which will load an article from articles folder based on the url.

// add article location in configuration
$app->config(array(
   'templates.path' => 	'./templates',
   'article.path'	=>	'./articles'   // location of articles
));
// '/post-url' will load post-url.txt file.
$app->get('/:article',function($article) use($app){
	$path 	 = $app->config('article.path');
	//open text file and read it
	$handle  = fopen($path . '/' . $article . '.txt', 'r');
	$content = stream_get_contents($handle);
	// split the content to get metadata
	$content = explode("\n\n", $content);
	$rawMeta = array_shift($content);
	// metadata is json encoded. so decode it.
	$meta    = json_decode($rawMeta,true);
	$content = implode("\n\n", $content);
	$article = array('meta' => $meta , 'content' => $content);
	$app->render('article.php', $article);
});

We have added a dynamic route with a single route parameter. The callback function will receive the value for that parameter, which should be a file name without its extension. Next, we extract the value of the article.path configuration variable, where we keep our articles.

In the next two lines, we read that file and store its content in a variable, $content . As I mentioned in the previous paragraph, an article will have metadata and actual content which will be separated by a single line (“\n\n”). Often, there may be many other blank lines in an article’s content, which will probably break the current method. To avoid that, we will use the first element to get metadata and will join the rest of the array using the same blank line. Since the metadata is in the JSON format, we need to decode that from here and store it in the $meta array.

Create a template file to render an article and place it in the template folder.

//article.php
echo '<h2>' . $meta['title'] . '</h2>';
echo $content;

Let’s create our first blog post now. Create a new file named first-article.txt, place it in the articles folder and add the content as shown below. Make sure that you have the metadata and content separated by a blank line.

//first-article.txt
{
	"title" : "This is my first article",
	"date"  : "02/15/2012",
	"slug"  : "first-article",
	"author": "Author name"
}

Fruitcake jelly-o halvah marshmallow bonbon. Croissant candy canes chocolate cake muffin jelly beans liquorice.... (copy paste some lipsum)

Excellent! You can start posting articles now. But wait! We don’t have a listing page yet. We need to list all the articles available in our system, with its title and a small description. To get this done, we will need to parse the articles folder to find all articles and add them to an array reading them one after the other.


Step 8: Create a Listing Page

$app->get('/', function() use ($app){
	$path = $app->config('article.path');
	$dir = new DirectoryIterator($path);
	$articles = array();
	foreach($dir as $file){
		if($file->isFile()){
			$handle  = fopen($path . '/' . $file->getFilename(), 'r');
			$content = stream_get_contents($handle);
			$content = explode("\n\n", $content);
			$rawMeta = array_shift($content);
			$meta    = json_decode($rawMeta,true);
			$content = implode("\n\n", $content);
			$articles[$file->getFilename()] = array('meta' => $meta, 'content' => $content);
		}
	}
	$app->render('index.php',array('articles' => $articles));
});

Here, we have added a route to the home page. We are using PHP’s inbuilt Directoryiterator class to loop through each file in the directory. Each article is added to the $articles array. From the template file (index.php), we can loop through these articles as shown below.

foreach($articles as $article){
	echo "<h1> ". $article['meta']['title'] ." </h1> ";
	echo substr(strip_tags($article['content']), 0,200) 
			. '... <a href="/' . $article['meta']['slug'] 
			. '">Read more >> </a>';
}

Our home page is ready now. It will list all the articles in our blog, with its corresponding title and a portion of the content.


Step 9: Create an Archives Page

Next up, we will create an ‘archives’ page. Since we expect the archives page to have filters based on year, month and date, we will add a route with optional parameters. The archives page will the support following URLs.

  • /archives
  • /archives/yyyy
  • /archives/yyyy/mm
  • /archives/yyyy/mm/dd

To achieve this, we will load all the articles and filter them based on the arguments passed to the callback function. I have moved everything to a class so that we can start reusing our code. A route that will support the above URLs will look something like so:

	// assign $this to another variable as it is not supported inside closure
	$blog = new Blog();
	$slim->get('/archives(/:yyyy(/:mm(/:dd)))', function() use ($blog,$slim) {});

Note that in this route, the year, month and date are optional parameters. /archives is the only required part of URL. Next, we need to implement this route which will respond based on the optional parameters.

	$args  = func_get_args();
	//load all articles
	$articles = $blog->loadArticles();
	$archives = array();
	// check count($args) for optional route params
	if(count($args)>0) {
		switch(count($args)){
			case 1 :    //only year is present
				$format = 'Y';
				$date = $dateFormat($args,$format);
				break;
			case 2 :    //year and month are present
				$format = 'Y-m';
				$date = $dateFormat($args,$format);
				break;
			case 3 :	//year, month and date are present
				$format = 'Y-m-d';
				$date = $dateFormat($args,$format);
				break;
		}
		// filter articles
		foreach($articles as $article){
			if($dateFormat($article['meta']['date'], $format) == $date){
				$archives[] = $article;
			}
		}
	}
	else{
		$archives = $articles;
	}
	// render archives
	$slim->render('archives.php',array('archives' => $archives));

Inside the switch statement, we create the date to be filtered using the arguments passed. This date is compared against the date of each article and if they match, it is added to the $archives array. $dateFormat() is an anonymous function inside the route to format dates.

	$dateFormat = function($args,$format){
		$temp_date = is_array($args) ? implode('-', $args) : $args;
		$date   = new DateTime($temp_date);
		return $date->format($format);
	};

We can write conditions for a route that should be satisfied by the arguments, to validate the year, month and date passed to the route.

$slim->get('/archives(/:yyyy(/:mm(/:dd)))', function() use $blog {
})->conditions(
		array(
			 'yyyy' => '(19|20)\d\d'
			,'mm'=>'(0[1-9]|1[0-2])'
			,'dd'=>'(0[1-9]|[1-2][0-9]|3[0-1])'
		));

Here, the year should start with 19 or 20 followed by any two digits. The month should be between 1 and 12 and date should be in between 01 and 31.

Here is the archives page I have created with the above code. If you noticed that I’ve used Twitter Bootstrap for applying some basic styles, you get an extra cookie!


Step 9: Spice it Up

Well, we now possess a working model of a flat-file blog engine. The next thing we need to do is organize the application to avoid code duplication and add features like commenting and so on. Let’s move this code from index.php to a separate class for better organization.

So far, we have used the View class of the Slim framework. We can create a custom view class which will extend Slim_View for adding some additional features like setting a base layout, global settings, etc. If you prefer to write articles in Markdown, you can include a Markdown parser as well.

We should also look into improving the aesthetics of the application. I prefer using Twitter bootstrap since it’s really very easy to use and customize. I don’t think it’s a good idea to dig into these details here. I have compiled them into a simple application called TextPress which can be downloaded here.


Wrapping Up

Nearly all flat-file blog engines prefer living in the cloud. Further Git is most likely used for publishing articles. With our engine, you can create posts using a simple text file and publish it using the command line. As there is no admin panel to hack, it is much more secure than almost any other content management system. Above all, it’s easy to host, since platform services, like PHP Fog allow us to host applications freely in the cloud.

So that’s about it. Let me know if you have any questions in the comments below and thank you so much for reading!

Note: Want to add some source code? Type <pre><code> before it and </code></pre> after it. Find out more
  • Tony

    This looks like something I want to try!

  • http://kriix.com Kristijan

    Great article, only one question, which local server do you use and how did you manage to change the localhost address :)

    “I’ve created a Virtualhost in my machine which is the URL shown in the screenshot. Adjust the URL based on the location of Slim in your machine.”

    • George Robinson

      Once you have set a virtual host you then need to add this to the hosts file on your local machine. For example:

      127.0.0.1 slim.local

      The location for the hosts file is different between Windows and Mac, I’m not certain about Linux. You will most certainly need administrative privileges to be able to modify the hosts file.

    • PunKeel

      Hi, the .local URL are like localhost, they point to 127.0.0.1
      I think.

      Else, you can use hosts file to redirect you to Apache :)

      PunKeel’

    • http://danielwilhelmsen.com Dan

      In windows, you have to set up your hosts file with the url you want to use. It’s located in windows/system32/drivers/etc/ and called simply ‘hosts’. You would then add the following line to the file.

      127.0.01 slim.local

      From there, you navigate to your apache installation and set up a virtual host that points to the directory where your files are stored.

      • http://kriix.com Kristijan

        Great, thanks :)

  • Jesus Bejarano

    have some ever tried popcornphp?

  • Blake

    This is a really interesting way to create a blog engine..

    I’m going to venture into this and see what cool stuff I can come up with! :)

    Thanks for the article.

  • http://www.leachcreative.com Andrew

    Great tutorial, thanks for sharing.

  • http://willmorgan.co.uk/ Will

    If you wanted to extend this to have an admin panel, you could simply store the entire article as a JSON file and load it with PHP.

    Do you think there is any advantage to using a flat file blog system? Extending it for comments, searching, and tagging is going to be a lot of effort.

    • http://shameerc.com ShameerC
      Author

      Hi Will,
      Performance is the most obvious and important advantage. For comments you can use Disqus or livefyre.
      In TextPress you can have tags and categories, create tag cloud, etc. Searching is something that I’m working on right now and its not that complex :)

    • Tina Ng

      Flat file blog system is the most flexible way to host a blog on cheap shared hosts. Those environments usually limit how many MySQL databases you can have and the scripting technology that you can use. By stripping the technology of your blog engine down to the barebone PHP and flat files, you can host the blog anywhere.

  • http://channeleaton.com Aaron

    Thanks for this timely article. I’ve been working on a Markdown file & Git powered blog engine for a couple of weeks now and I had no idea about the DirectoryIterator class. I’ve been doing it the hard way all along.

  • http://www.sivocha.com David

    I saw this tutorial and thought, forget PHP this would be BADASS in Node and as a result I just spent a good few hours redoing this in Node!
    Thanks for the inspiration!

  • http://expiringlinks.co Joseph Szymborski

    Really interesting article, especially like how you format the articles in JSON. Only thing that makes me nervous is when you take the URL parameter to define a path to open up. Looks like a clever hacker can have access to your entire file-system with some crafty manipulation (../index.php). Even then it wouldn’t achieve much, but still seems like something that can be abused.

    I’d maybe make an article index (probs. using JSON) where the parameter would cross reference the article-id/name to a file, just to make the whole system that much more closed.

    Really nice article, I love the Slim framework because it’s really simple. Almost more of a toolkit.

    • http://shameerc.com Shameer C
      Author

      Hi Joseph,

      Good point about the file-system. Though I don’t really think someone can access the entire file-system by passing URL parameters, I will look at this in depth. After all we are opening up some text or JSON files. Other files and framework itself can be placed in a more secure location.

      Thanks

  • SidiRod

    I’ve never used Slim. Is it worth to learn this framework instead of yii, for example?
    Thanks

    • http://shameerc.com Shameer C
      Author

      I’ve been using Slim for building some simple applications and I liked it very much and there is no big learning curve. I haven’t used Yii framework in any real projects.

  • http://hizup.com Anton

    Whow! Very nice tutorial!
    File system like this is really faster than a database like MySQL?

  • Manu

    Anyone can explain differences between this micro-framework and Silex, for example ?
    There is no tuts about Silex (Sensio) here, what is your opinion about this one ?

  • Paul

    Thanks for this timely article. But I couldn’t load the metadata (Windows XP). Here some changes in the class Textpress:

    public function loadArticle($fileName)
    {
    ….

    $raw = stream_get_contents($handle);

    // separate the meta part and the contents
    $pos = strpos($raw, ‘}’) + 1;
    $meta = json_decode(rtrim(substr($raw, 0, $pos)),true);
    $contents = substr($raw, $pos);

    if($this->markdown)
    {

    • http://shameerc.com Shameer C
      Author

      I will definitely take a look at this.

      -Thanks

  • Abdallah

    Do you really use http://slim.local/?
    is it online host?

  • http://harrymahardhika.com Harry M.

    Is this similar to RoR’s jekyll? https://github.com/mojombo/jekyll/

  • Rick

    Shameer, I thought you did a really nice job walking someone through Slim. However, I think you’ve got some code and files missing towards the end(?). Where’s the Blog class you mentioned in Step 9? In your “second” Step 9 (“Spice it Up”), the files in your git-hub link do not seem to work as expected – I was hoping to find your previously mentioned Blog class and twitter bootstrap use that is depicted on your textPress site. Still, a great tutorial and you’ve got a nice, friendly intellect that is easy to follow. Thanks for the contribution and tutorial. (Please let me know if my points are wrong.)

    • http://shameerc.com/ Shameer C
      Author

      Hi Rick,
      Thanks for the comment. I’m sorry that Blog class and the code discussed in this article is not there in TextPress. Its a full fledged blogging application which you can easily get started with.

  • otravezm

    Este artículo me ha parecido de lo más interestante.

    Saludos.

  • http://sdsalyer.com S. D. Salyer

    Thanks for the tutorial. I had been working on a similar project and was able to eliminate a nasty nested IF structure for my archive page with your method of counting the number of function arguments. Thanks!

  • http://www.LikeTheMammal.com/ Chris Dolphin

    I had the hardest time around Step 7. I just couldn’t get it to explode() out the contents correctly. Being a complete PHP newbie, I eventually found out it was the “nn” separator that was giving me the hardest trouble. Decided to just switch it out with another symbol in the code and my .txt docs and it ended up working fine. Just a heads up to anyone who might be having the same trouble.

    • wnpers

      your systems is windows?? if it is, you can use “rn”,

  • http://www.facebook.com/Draconyster Roman Dracony Tsjupa

    Check out Hollow, it’s a flat file blogging script built with PHPixie and Bootstrap, so it’s quite easy to modify to your liking. https://github.com/dracony/Hollow
    And if you want to see the live preview go to http://hollow.phpixie.com