Choose a version here. If you've not already started the tutorial, just go with the latest one.

30 Aug 2014 Preview version Choose
5 Oct 2014 Explicitly adds a specific charset for htmlspecialchars(), and wraps it with a custom function Choose
16 Oct 2014 Updated redirect function to work with vhost subfolders Choose
4 Nov 2014 Minor improvements: added missing docblock, fixed security issue, CSS tweak. Switched hashing method to DEFAULT instead of BCRYPT, this is best practice. Choose
25 Nov 2014 Improve the notes on getting started, in particular choosing a programmer's editor. Added introduction to mod_rewrite rules. Choose
16 Aug 2018 Some bug fixes, remove compatibility library for earlier version of PHP Choose
OK
NB: There are several versions of this tutorial, each successive one containing additional improvements. If you're in the middle of working through it, please check the versions panel above, to ensure you're not mixing code from different versions.

Make your own blog

Introduction

In this tutorial, I'll present how to write a simple blog system, using the PHP language and the Apache web server. For the database, we'll use SQLite, to keep things simple. Don't worry if these terms don't mean anything yet, that's okay. I'm assuming readers are familiar with how to use their computer, but perhaps have not programmed before. That said, even a beginner's tutorial can get non-trivial quickly, so if you are brand new to programming, you may wish to research unfamiliar keywords (usually on the PHP website).

The application we will build together was built prior to writing the tutorial. The text is therefore written around the way I think development happens in real life; some steps may introduce bugs, or be written in ways the purist might consider non-ideal. However, for the most part, these problems will be ironed out as we go. At the end of this project, we'll have a functional, maintainable and secure application, but there'll be much that can still be improved on (which will likely make for a subsequent tutorial).

If you are new to programming, it is possible that you might feel somewhat lost as you work through the material. Whilst any prior practice will always help, the before-and-after code samples should see you right through to the end, even if you don't initially understand every step. You should find that some of this will sink in, and will contribute to your future "aha" moment, even if it is daunting at the start. The elation of getting it working will, I hope, encourage you to dive even deeper into programming. Enjoy the ride!

I'll assume that your development environment is set up. Here's what you will need:

For Windows and Mac users, you can get all of this with XAMPP. If you use Linux, you may have all of this already — although XAMPP works on Linux too, and adds a user-friendly control panel. If you get stuck, turn to your favourite search engine, and ask "How do I install [whatever] on [Windows]". In 99% of cases, your question has already been answered, so be persistent.

You'll also need an editor to modify files. Most computers have a free text editor already installed, but these are rarely good enough. Use whatever you like, but if you have no preference, download and install NetBeans.

~

In designing this tutorial, I wrote out a list of things I thought would be great to have, and then developed until I had a basic complete product. In retrospect, it would have been better to decide this prior to development; regardless of whether you are setting up a business or programming for fun, having a simple and achievable plan is miles better than an all-singing, all-dancing one that ends up abandoned half-way through (either through boredom, bankruptcy, or both).

Here's the initial list of features/technologies I put on paper:

Login, Logout, Add comment, Create user, Amend user, Delete user, Email validation for new user accounts, Display post, Accept markdown format, Create post, Edit post, Unpublish post, Delete post, Unit tests, List posts, AJAX posting/commenting, Database profiler, Internationalisation, Pretty URLs, Use template engine, Per-environment configuration files, Post pagination, Comment pagination, Unpublish comment, Delete comment, Foreign key constraints, Unique constraints, User access levels.

To illustrate how much an "initial working product" should trim the initial wishlist, here's what made it into the first version:

Login, Logout, Add comment, Display post, Create post, Edit post, Delete post, List posts, Delete comment, Foreign key constraints.

It's worth pointing out that, with any software project, rejecting a feature now does not mean you can never add that feature. It just means that you are prioritising getting important things done, so that the product is in a releasable state quickly. Once it is out the door, you can then find a few more items on your list to add into the next iteration. In a commercial situation, this approach is helpful because if your users don't like a change you've made, you've wasted two months rather than two years — a saving that could be the difference between steering things back on track, and going bust.

One of the other features of this tutorial is that it uses an absolute minimum of additional software to get it working. Software projects nearly always make use of libraries, each of which offer some particular pre-written functionality. These are well worth using in proper development, since they are better-tested by more people than an individual can usually achieve on their own. However, for a tutorial I think they can add integration complexity, and explaining the advanced-level code within is usually quite a distraction. Thus, I have eschewed libraries more than I would normally, and suspect this will be corrected in a second (more advanced) tutorial.

I have made one exception for this rule, however, for reasons of password security. The login system requires some cryptographic code, and this sort of thing is quite easy to get wrong. Whilst this is just a tutorial, if it teaches readers to leave security to the experts (the library-writers), it has served a valuable purpose.

~

So, let us get started. I'll assume you know how to create, edit, save and delete files in your editor, and that you can create folders as required. This might be in the root of your "htdocs" or "www" folder, depending on what you used to install Apache. You can use a subfolder of "blog" if you wish, or for more advanced users, feel free to set up a new vhost. So, when I use http://localhost/, you can assume I am referring to the root of your project, even if your URL is somewhat different.

Let us first check that your server installation is working okay. Create a file called info.php in the root of your project folder:

<?php phpinfo();

Then, run this in your browser, by accessing the URL http://localhost/info.php. You should see a PHP configuration page. If you do, then check you are running a version later than 5.4; if it is earlier than this then you will need to upgrade. If you don't see this screen at all, then check that you have created the file in the right folder.

Once you have this working, and showing an acceptable version number, you can delete this file.

Here's the first bit of code to add:

Expand/contract code area Select previous tab
Select next tab
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<!DOCTYPE html>
<html>
<head>
<title>A blog application</title>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
</head>
<body>
<h1>Blog title</h1>
<p>This paragraph summarises what the blog is about.</p>
<h2>Article 1 title</h2>
<div>dd Mon YYYY</div>
<p>A paragraph summarising article 1.</p>
<p>
<a href="#">Read more...</a>
</p>
<h2>Article 2 title</h2>
<div>dd Mon YYYY</div>
<p>A paragraph summarising article 2.</p>
<p>
<a href="#">Read more...</a>
</p>
</body>
</html>

This is the initial file, so all we have to do is copy and paste it in. To do so, create index.php in the root of your project folder, open it in your editor, and ensure it is empty before pasting in the code. Then visit http://localhost/index.php in your browser, to ensure that you can see the initial mock-up of our blog's home page.

If you can, then your first piece of coding is done! The language is HTML, which is understood by your web browser as a way to describe documents on the web. HTML is not actually a programming language: it is properly referred to as a markup language. The markup itself is made up of tags such as <body> and <h1>, each of which has an opening tag (without the slash) and a closing tag (with the slash). These are nested to form a hierarchy in the same way as an ordinary letter is hierarchical: instead of an envelope containing a document containing headings and paragraphs, we have an <html> containing a <body> containing <h1> and <p>. Easy peasy!

Let us start improving our skeleton home page. One of the observations we can make is that there is a fair bit of repetition: there are two articles that both have a heading, date, summary and hyperlink. Since this is just test data, let us add a loop around the first article block and delete the second one.

Here is how I am presenting the changes in this tutorial. This will be good practice for you, since all changes will be shown in this way. The approach of showing code changes like this is known as a diff, for obvious reasons (though the diffs I use are more colourful than the usual monochrome versions).

Expand/contract code area Select previous tab
Select next tab
8
9
10
11
12
13
14
15
16
 
 
17
18
19
20
21
22
23
24
25
<h1>Blog title</h1>
<p>This paragraph summarises what the blog is about.</p>
<h2>Article 1 title</h2>
<div>dd Mon YYYY</div>
<p>A paragraph summarising article 1.</p>
<p>
<a href="#">Read more...</a>
</p>
<h2>Article 2 title</h2>
<div>dd Mon YYYY</div>
<p>A paragraph summarising article 2.</p>
<p>
<a href="#">Read more...</a>
</p>
</body>
</html>
8
9
10
11
12
13
14
15
16
17
18
19
 
 
 
 
 
 
20
21
<h1>Blog title</h1>
<p>This paragraph summarises what the blog is about.</p>
<?php for ($postId = 1; $postId <= 3; $postId++): ?>
<h2>Article <?php echo $postId ?> title</h2>
<div>dd Mon YYYY</div>
<p>A paragraph summarising article <?php echo $postId ?>.</p>
<p>
<a href="#">Read more...</a>
</p>
<?php endfor ?>
</body>
</html>

The rules for making these changes are simple: the code on the left shows the old file, and the code on the right shows the new one. Also, red lines are deleted, green lines are added. So, the two red blocks on the left are in the existing file, and can be removed. The green block on the right should be added. Where you see a solid horizontal bar across the whole diff, that's just a sign that some identical lines have been missed out for brevity.

Thus, if you wish, you can delete both article blocks and replace them with the new loop block (containing the for() through to the endfor). However, the way this was actually written was as follows:

In summary, it doesn't matter too much how you apply these changes (or, indeed, any changes in this tutorial). That said, understanding the way in which changes are written in real life may help towards a greater understanding of the code. Don't worry though if you think you've made a mistake; the full file for each change is available above each one.

So, finally, refresh your browser screen, and make sure your changes work. You should now see three articles, each with a different heading and paragraph.

~

The next step we will take is to set up the database. We'll use a database system called SQLite (pronounced "see-kwel light") as it is really easy to connect to, and does not require a database server. Create the following two files in your project, paste in the contents, and then we'll discuss what they do.

You'll notice that some changes in this tutorial — including one of these files, data/init.sql — has a name that contains a forward slash. This means that the file is stored in a sub-directory, and that you'll need to create that manually if it does not already exist.

Expand/contract code area Select previous tab
Select next tab
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
/**
* Database creation script
*/
DROP TABLE IF EXISTS post;
CREATE TABLE post (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
title VARCHAR NOT NULL,
body VARCHAR NOT NULL,
user_id INTEGER NOT NULL,
created_at VARCHAR NOT NULL,
updated_at VARCHAR
);
INSERT INTO
post
(
title, body, user_id, created_at
)
VALUES(
"Here's our first post",
"This is the body of the first post.
It is split into paragraphs.",
1,
date('now', '-2 months')
)
;
INSERT INTO
post
(
title, body, user_id, created_at
)
VALUES(
"Now for a second article",
"This is the body of the second post.
This is another paragraph.",
1,
date('now', '-40 days')
)
;
INSERT INTO
post
(
title, body, user_id, created_at
)
VALUES(
"Here's a third post",
"This is the body of the third post.
This is split into paragraphs.",
1,
date('now', '-13 days')
)
;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
<?php
// Get the PDO DSN string
$root = realpath(__DIR__);
$database = $root . '/data/data.sqlite';
$dsn = 'sqlite:' . $database;
$error = '';
// A security measure, to avoid anyone resetting the database if it already exists
if (is_readable($database) && filesize($database) > 0)
{
$error = 'Please delete the existing database manually before installing it afresh';
}
// Create an empty file for the database
if (!$error)
{
$createdOk = @touch($database);
if (!$createdOk)
{
$error = sprintf(
'Could not create the database, please allow the server to create new files in \'%s\'',
dirname($database)
);
}
}
// Grab the SQL commands we want to run on the database
if (!$error)
{
$sql = file_get_contents($root . '/data/init.sql');
if ($sql === false)
{
$error = 'Cannot find SQL file';
}
}
// Connect to the new database and try to run the SQL commands
if (!$error)
{
$pdo = new PDO($dsn);
$result = $pdo->exec($sql);
if ($result === false)
{
$error = 'Could not run SQL: ' . print_r($pdo->errorInfo(), true);
}
}
// See how many rows we created, if any
$count = null;
if (!$error)
{
$sql = "SELECT COUNT(*) AS c FROM post";
$stmt = $pdo->query($sql);
if ($stmt)
{
$count = $stmt->fetchColumn();
}
}
?>
<!DOCTYPE html>
<html>
<head>
<title>Blog installer</title>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
<style type="text/css">
.box {
border: 1px dotted silver;
border-radius: 5px;
padding: 4px;
}
.error {
background-color: #ff6666;
}
.success {
background-color: #88ff88;
}
</style>
</head>
<body>
<?php if ($error): ?>
<div class="error box">
<?php echo $error ?>
</div>
<?php else: ?>
<div class="success box">
The database and demo data was created OK.
<?php if ($count): ?>
<?php echo $count ?> new rows were created.
<?php endif ?>
</div>
<?php endif ?>
</body>
</html>

This change comprises of two new files, a SQL file and a PHP file. I'll discuss the SQL file first.

Broadly, databases are stored in terms of tables, columns and rows. Each different kind of data we wish to store needs its own table, but to start with, we only have one: a post. The CREATE TABLE statement specifies which properties we want a post to have, namely:

The column id has two particular features:

A blog post is therefore just a row added to the posts table, since each row has space to store all of these values. Once the table is created, we also create some dummy article data, using INSERT. The format of this command, as used in the SQL script, is simply thus:

INSERT INTO table_name (column_1, column_2, ...) VALUES (value_1, value_2, ...);

Now, the SQL file won't do anything on its own: we need a program to send the commands to the database. This is where the install.php file comes in; we'll run that whenever we want to set up the blog (or to wipe it and start again). I'll describe its broad features here, but do also read the comments in the code.

The first half of the code is written in PHP. Here, we use a series of if() statements to ensure everything goes smoothly. The variable $error is set to an error message if something goes wrong. Here are the steps we take:

The second half of the file (from <!DOCTYPE html>) presents the results of the script, in HTML.

So, if you have not already tried it, visit http://localhost/install.php in your browser, and make sure some posts are created. If you want to re-run it, delete the data.sqlite file manually and refresh your browser page.