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

All posts page

The next step is to add an All Posts screen for administrative users. We'll start with a static mock-up with hard-wired post values:

Expand/contract code area Select previous tab
Select next tab
98
99
100
101
/* Some browsers make labels too tall, and as a result they incorrectly stack horizontally.
Let's reset each to the left-hand side to be sure. */
clear: left;
}
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
/* Some browsers make labels too tall, and as a result they incorrectly stack horizontally.
Let's reset each to the left-hand side to be sure. */
clear: left;
}
#post-list {
border-collapse: collapse;
border: 1px solid silver;
}
#post-list td {
padding: 8px;
}
#post-list tbody tr:nth-child(odd) {
background-color: #f4f4f4;
}
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
<?php
require_once 'lib/common.php';
session_start();
?>
<!DOCTYPE html>
<html>
<head>
<title>A blog application | Blog posts</title>
<?php require 'templates/head.php' ?>
</head>
<body>
<?php require 'templates/top-menu.php' ?>
<h1>Post list</h1>
<form method="post">
<table id="post-list">
<tbody>
<tr>
<td>Title of the first post</td>
<td>
<a href="edit-post.php?post_id=1">Edit</a>
</td>
<td>
<input
type="submit"
name="post[1]"
value="Delete"
/>
</td>
</tr>
<tr>
<td>Title of the second post</td>
<td>
<a href="edit-post.php?post_id=2">Edit</a>
</td>
<td>
<input
type="submit"
name="post[2]"
value="Delete"
/>
</td>
</tr>
<tr>
<td>Title of the third post</td>
<td>
<a href="edit-post.php?post_id=3">Edit</a>
</td>
<td>
<input
type="submit"
name="post[3]"
value="Delete"
/>
</td>
</tr>
</tbody>
</table>
</form>
</body>
</html>
1
2
3
 
 
4
5
6
<div class="top-menu">
<div class="menu-options">
<?php if (isLoggedIn()): ?>
<a href="edit-post.php">New post</a>
|
Hello <?php echo htmlEscape(getAuthUser()) ?>.
1
2
3
4
5
6
7
8
<div class="top-menu">
<div class="menu-options">
<?php if (isLoggedIn()): ?>
<a href="list-posts.php">All posts</a>
|
<a href="edit-post.php">New post</a>
|
Hello <?php echo htmlEscape(getAuthUser()) ?>.

Of course, since this is a restricted screen, we must only allow authorised users to see it. To do so, we redirect back to the home page, and exit as usual:

Expand/contract code area Select previous tab
Select next tab
3
4
5
 
 
 
 
 
 
6
7
8
session_start();
?>
<!DOCTYPE html>
<html>
3
4
5
6
7
8
9
10
11
12
13
14
session_start();
// Don't let non-auth users see this screen
if (!isLoggedIn())
{
redirectAndExit('index.php');
}
?>
<!DOCTYPE html>
<html>

Great stuff. Don't forget to test it by logging out, and then trying to visit the page manually!

Now, the logic we use to read the posts for the front page will be useful also for this new screen. Since we don't want to write this twice, let's move it to a function so we can reuse it later on.

Expand/contract code area Select previous tab
Select next tab
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
26
27
28
29
30
31
32
51
52
53
54
55
56
57
// Connect to the database, run a query, handle errors
$pdo = getPDO();
$stmt = $pdo->query(
'SELECT
id, title, created_at, body
FROM
post
ORDER BY
created_at DESC'
);
if ($stmt === false)
{
throw new Exception('There was a problem running this query');
}
$notFound = isset($_GET['not-found']);
<?php endif ?>
<div class="post-list">
<?php while ($row = $stmt->fetch(PDO::FETCH_ASSOC)): ?>
<div class="post-synopsis">
<h2>
<?php echo htmlEscape($row['title']) ?>
<?php endif ?>
</div>
</div>
<?php endwhile ?>
</div>
</body>
5
6
7
8
 
 
 
 
 
 
 
 
 
 
 
9
10
11
26
27
28
29
30
31
32
51
52
53
54
55
56
57
// Connect to the database, run a query, handle errors
$pdo = getPDO();
$posts = getAllPosts($pdo);
$notFound = isset($_GET['not-found']);
<?php endif ?>
<div class="post-list">
<?php foreach ($posts as $row): ?>
<div class="post-synopsis">
<h2>
<?php echo htmlEscape($row['title']) ?>
<?php endif ?>
</div>
</div>
<?php endforeach ?>
</div>
</body>
73
74
75
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
77
78
return date('Y-m-d H:i:s');
}
/**
* Converts unsafe text to safe, paragraphed, HTML
*
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
return date('Y-m-d H:i:s');
}
/**
* Gets a list of posts in reverse order
*
* @param PDO $pdo
* @return array
*/
function getAllPosts(PDO $pdo)
{
$stmt = $pdo->query(
'SELECT
id, title, created_at, body
FROM
post
ORDER BY
created_at DESC'
);
if ($stmt === false)
{
throw new Exception('There was a problem running this query');
}
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
/**
* Converts unsafe text to safe, paragraphed, HTML
*

While working on the home page, I noticed I'd used the variable name $row. This is rather generic — most things read from a database are rows — so I swapped to a better name for it. This is more readable, and fits in with the common naming convention of using a plural name for an array and the corresponding singular name for each item.

Expand/contract code area Select previous tab
Select next tab
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
<?php endif ?>
<div class="post-list">
<?php foreach ($posts as $row): ?>
<div class="post-synopsis">
<h2>
<?php echo htmlEscape($row['title']) ?>
</h2>
<div class="meta">
<?php echo convertSqlDate($row['created_at']) ?>
(<?php echo countCommentsForPost($pdo, $row['id']) ?> comments)
</div>
<p>
<?php echo htmlEscape($row['body']) ?>
</p>
<div class="post-controls">
<a
href="view-post.php?post_id=<?php echo $row['id'] ?>"
>Read more...</a>
<?php if (isLoggedIn()): ?>
|
<a
href="edit-post.php?post_id=<?php echo $row['id'] ?>"
>Edit</a>
<?php endif ?>
</div>
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
<?php endif ?>
<div class="post-list">
<?php foreach ($posts as $post): ?>
<div class="post-synopsis">
<h2>
<?php echo htmlEscape($post['title']) ?>
</h2>
<div class="meta">
<?php echo convertSqlDate($post['created_at']) ?>
(<?php echo countCommentsForPost($pdo, $post['id']) ?> comments)
</div>
<p>
<?php echo htmlEscape($post['body']) ?>
</p>
<div class="post-controls">
<a
href="view-post.php?post_id=<?php echo $post['id'] ?>"
>Read more...</a>
<?php if (isLoggedIn()): ?>
|
<a
href="edit-post.php?post_id=<?php echo $post['id'] ?>"
>Edit</a>
<?php endif ?>
</div>

Next, we'll modify the mock-up by adding in a creation time for posts:

Expand/contract code area Select previous tab
Select next tab
26
27
28
 
 
 
29
30
31
42
43
44
 
 
 
45
46
47
58
59
60
 
 
 
61
62
63
<tbody>
<tr>
<td>Title of the first post</td>
<td>
<a href="edit-post.php?post_id=1">Edit</a>
</td>
</tr>
<tr>
<td>Title of the second post</td>
<td>
<a href="edit-post.php?post_id=2">Edit</a>
</td>
</tr>
<tr>
<td>Title of the third post</td>
<td>
<a href="edit-post.php?post_id=3">Edit</a>
</td>
26
27
28
29
30
31
32
33
34
42
43
44
45
46
47
48
49
50
58
59
60
61
62
63
64
65
66
<tbody>
<tr>
<td>Title of the first post</td>
<td>
dd MM YYYY h:mi
</td>
<td>
<a href="edit-post.php?post_id=1">Edit</a>
</td>
</tr>
<tr>
<td>Title of the second post</td>
<td>
dd MM YYYY h:mi
</td>
<td>
<a href="edit-post.php?post_id=2">Edit</a>
</td>
</tr>
<tr>
<td>Title of the third post</td>
<td>
dd MM YYYY h:mi
</td>
<td>
<a href="edit-post.php?post_id=3">Edit</a>
</td>

As with our mock-up approach before, once a layout contains everything required, it is time to convert to a working version. So, let's do that now: we'll add a loop and render table data in the HTML.

Expand/contract code area Select previous tab
Select next tab
9
10
11
 
 
 
 
12
13
14
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
redirectAndExit('index.php');
}
?>
<!DOCTYPE html>
<html>
<form method="post">
<table id="post-list">
<tbody>
<tr>
<td>Title of the first post</td>
<td>
dd MM YYYY h:mi
</td>
<td>
<a href="edit-post.php?post_id=1">Edit</a>
</td>
<td>
<input
type="submit"
name="post[1]"
value="Delete"
/>
</td>
</tr>
<tr>
<td>Title of the second post</td>
<td>
dd MM YYYY h:mi
</td>
<td>
<a href="edit-post.php?post_id=2">Edit</a>
</td>
<td>
<input
type="submit"
name="post[2]"
value="Delete"
/>
</td>
</tr>
<tr>
<td>Title of the third post</td>
<td>
dd MM YYYY h:mi
</td>
<td>
<a href="edit-post.php?post_id=3">Edit</a>
</td>
<td>
<input
type="submit"
name="post[3]"
value="Delete"
/>
</td>
</tr>
</tbody>
</table>
</form>
9
10
11
12
13
14
15
16
17
18
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
redirectAndExit('index.php');
}
// Connect to the database, run a query
$pdo = getPDO();
$posts = getAllPosts($pdo);
?>
<!DOCTYPE html>
<html>
<form method="post">
<table id="post-list">
<tbody>
<?php foreach ($posts as $post): ?>
<tr>
<td>
<?php echo htmlEscape($post['title']) ?>
</td>
<td>
<?php echo convertSqlDate($post['created_at']) ?>
</td>
<td>
<a href="edit-post.php?post_id=<?php echo $post['id']?>">Edit</a>
</td>
<td>
<input
type="submit"
name="delete-post[<?php echo $post['id']?>]"
value="Delete"
/>
</td>
</tr>
<?php endforeach ?>
</tbody>
</table>
</form>

As it stands, the user may click on a delete button to remove a post, but this is not presently handled. So, let's do that now:

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
<?php
/**
* Tries to delete the specified post
*
* @param PDO $pdo
* @param integer $postId
* @return boolean Returns true on successful deletion
* @throws Exception
*/
function deletePost(PDO $pdo, $postId)
{
$sql = "
DELETE FROM
post
WHERE
id = :id
";
$stmt = $pdo->prepare($sql);
if ($stmt === false)
{
throw new Exception('There was a problem preparing this query');
}
$result = $stmt->execute(
array('id' => $postId, )
);
return $result !== false;
}
1
2
 
3
4
5
10
11
12
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
14
15
<?php
require_once 'lib/common.php';
session_start();
redirectAndExit('index.php');
}
// Connect to the database, run a query
$pdo = getPDO();
$posts = getAllPosts($pdo);
1
2
3
4
5
6
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?php
require_once 'lib/common.php';
require_once 'lib/list-posts.php';
session_start();
redirectAndExit('index.php');
}
if ($_POST)
{
$deleteResponse = $_POST['delete-post'];
if ($deleteResponse)
{
$keys = array_keys($deleteResponse);
$deletePostId = $keys[0];
if ($deletePostId)
{
deletePost(getPDO(), $deletePostId);
redirectAndExit('list-posts.php');
}
}
}
// Connect to the database, run a query
$pdo = getPDO();
$posts = getAllPosts($pdo);

Finally let's add in a post count on this page:

Expand/contract code area Select previous tab
Select next tab
41
42
43
 
 
44
45
46
<h1>Post list</h1>
<form method="post">
<table id="post-list">
<tbody>
41
42
43
44
45
46
47
48
<h1>Post list</h1>
<p>You have <?php echo count($posts) ?> posts.
<form method="post">
<table id="post-list">
<tbody>