Make your own blog
Adding some polish
Excellent, we're now on the home straight! Our functionality is finished, so we'll just do a last round of sprucing up. To start, let's add in a home link for authorised users. This is useful on the restricted pages, since they don't have a home link themselves:
 
		 
	- templates/top-menu.php templates/top-menu.php
 
				1
					2
					3
					 
					 
					4
					5
					6
					<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>
					1
					2
					3
					4
					5
					6
					7
					8
					<div class="top-menu">
						<div class="menu-options">
							<?php if (isLoggedIn()): ?>
								<a href="index.php">Home</a>
								|
								<a href="list-posts.php">All posts</a>
								|
								<a href="edit-post.php">New post</a>
					Next, I thought it would be nice to have a comment count for each article on the All Posts page. Add the following changes:
 
		 
	- lib/common.php lib/common.php
- list-posts.php list-posts.php
 
				83
					84
					85
					86
					 
					87
					88
					89
					{
						$stmt = $pdo->query(
							'SELECT
								id, title, created_at, body
							FROM
								post
							ORDER BY
					83
					84
					85
					86
					87
					88
					89
					90
					{
						$stmt = $pdo->query(
							'SELECT
								id, title, created_at, body,
								(SELECT COUNT(*) FROM comment WHERE comment.post_id = post.id) comment_count
							FROM
								post
							ORDER BY
					54
					55
					56
					 
					 
					 
					57
					58
					59
												<td>
													<?php echo convertSqlDate($post['created_at']) ?>
												</td>
												<td>
													<a href="edit-post.php?post_id=<?php echo $post['id']?>">Edit</a>
												</td>
					54
					55
					56
					57
					58
					59
					60
					61
					62
												<td>
													<?php echo convertSqlDate($post['created_at']) ?>
												</td>
												<td>
													<?php echo $post['comment_count'] ?>
												</td>
												<td>
													<a href="edit-post.php?post_id=<?php echo $post['id']?>">Edit</a>
												</td>
					Let's take a closer look at how the new query works. The query now looks like this:
SELECT id, title, created_at, body, (SELECT COUNT(*) FROM comment WHERE comment.post_id = post.id) comment_count FROM post ORDER BY created_at DESC
	The new part of this is the bracketed expression on the third line. This
	sort of SQL is known as a sub-query since it is a query contained inside another
	one. The result of it can be read like a real column, which we explicitly name by adding
	comment_count at the end of it.
	This sub-query will count rows in the comment table, based on which post they
	belong to. Since the job of the outer query is to list posts, we can filter the comment count
	for each row by making a comparison to the outer table, post.
The next change is another easy one: we'll add a header row to the All Posts table:
 
		 
	- assets/main.css assets/main.css
- list-posts.php list-posts.php
 
				110
					111
					112
					113
					114
					 
					115
					116
					117
						border: 1px solid silver;
					}
					#post-list td {
						padding: 8px;
					}
					#post-list tbody tr:nth-child(odd) {
					110
					111
					112
					113
					114
					115
					116
					117
					118
						border: 1px solid silver;
					}
					#post-list td, #post-list th {
						padding: 8px;
						text-align: left;
					}
					#post-list tbody tr:nth-child(odd) {
					45
					46
					47
					 
					 
					 
					 
					 
					 
					 
					 
					 
					48
					49
					50
							<form method="post">
								<table id="post-list">
									<tbody>
										<?php foreach ($posts as $post): ?>
											<tr>
					45
					46
					47
					48
					49
					50
					51
					52
					53
					54
					55
					56
					57
					58
					59
							<form method="post">
								<table id="post-list">
									<thead>
										<tr>
											<th>Title</th>
											<th>Creation date</th>
											<th>Comments</th>
											<th />
											<th />
										</tr>
									</thead>
									<tbody>
										<?php foreach ($posts as $post): ?>
											<tr>
					While we are on the All Posts page, it would make sense to add in a link to the view page for each article. So let's do that now:
 
		 
	- list-posts.php list-posts.php
 
				58
					59
					60
					61
					 
					 
					62
					63
					64
										<?php foreach ($posts as $post): ?>
											<tr>
												<td>
													<?php echo htmlEscape($post['title']) ?>
												</td>
												<td>
													<?php echo convertSqlDate($post['created_at']) ?>
					58
					59
					60
					61
					62
					63
					64
					65
					66
										<?php foreach ($posts as $post): ?>
											<tr>
												<td>
													<a
														href="view-post.php?post_id=<?php echo $post['id']?>"
													><?php echo htmlEscape($post['title']) ?></a>
												</td>
												<td>
													<?php echo convertSqlDate($post['created_at']) ?>
					Next up is a refactoring that simplifies the code (quite a bit is removed) and reduces the number of calls made to the database. The essence of the modification is in lib/view-post.php — we merge two SQL calls together via another sub-query.
There's quite a few files to modify here, so make sure you get them all.
 
		 
	- index.php index.php
- lib/common.php lib/common.php
- lib/view-post.php lib/view-post.php
- templates/list-comments.php templates/list-comments.php
- view-post.php view-post.php
 
				34
					35
					36
					37
					38
					39
					40
										<div class="meta">
											<?php echo convertSqlDate($post['created_at']) ?>
											(<?php echo countCommentsForPost($pdo, $post['id']) ?> comments)
										</div>
										<p>
											<?php echo htmlEscape($post['body']) ?>
					34
					35
					36
					37
					38
					39
					40
										<div class="meta">
											<?php echo convertSqlDate($post['created_at']) ?>
											(<?php echo $post['comment_count'] ?> comments)
										</div>
										<p>
											<?php echo htmlEscape($post['body']) ?>
					125
					126
					127
					128
					129
					130
					131
					132
					133
					134
					135
					136
					137
					138
					139
					140
					141
					142
					143
					144
					145
					146
					147
					148
					149
					150
					151
					152
					153
					154
					155
						exit();
					}
					/**
					 * Returns the number of comments for the specified post
					 *
					 * @param PDO $pdo
					 * @param integer $postId
					 * @return integer
					 */
					function countCommentsForPost(PDO $pdo, $postId)
					{
						$sql = "
							SELECT
								COUNT(*) c
							FROM
								comment
							WHERE
								post_id = :post_id
						";
						$stmt = $pdo->prepare($sql);
						$stmt->execute(
							array('post_id' => $postId, )
						);
						return (int) $stmt->fetchColumn();
					}
					/**
					 * Returns all the comments for the specified post
					 *
					125
					126
					127
					 
					 
					 
					 
					 
					 
					 
					 
					 
					 
					 
					 
					 
					 
					 
					 
					 
					 
					 
					 
					 
					 
					 
					 
					 
					128
					129
					130
						exit();
					}
					/**
					 * Returns all the comments for the specified post
					 *
					100
					101
					102
					103
					 
					104
					105
					106
					{
						$stmt = $pdo->prepare(
							'SELECT
								title, created_at, body
							FROM
								post
							WHERE
					100
					101
					102
					103
					104
					105
					106
					107
					{
						$stmt = $pdo->prepare(
							'SELECT
								title, created_at, body,
								(SELECT COUNT(*) FROM comment WHERE comment.post_id = post.id) comment_count
							FROM
								post
							WHERE
					2
					3
					4
					 
					5
					6
					7
					10
					11
					12
					13
					14
					15
					16
					/**
					 * @var $pdo PDO
					 * @var $postId integer
					 */
					?>
					<form
						method="post"
						class="comment-list"
					>
						<h3><?php echo countCommentsForPost($pdo, $postId) ?> comments</h3>
						<?php foreach (getCommentsForPost($pdo, $postId) as $comment): ?>
							<div class="comment">
					2
					3
					4
					5
					6
					7
					8
					10
					11
					12
					13
					14
					15
					16
					/**
					 * @var $pdo PDO
					 * @var $postId integer
					 * @var $commentCount integer
					 */
					?>
					<form
						method="post"
						class="comment-list"
					>
						<h3><?php echo $commentCount ?> comments</h3>
						<?php foreach (getCommentsForPost($pdo, $postId) as $comment): ?>
							<div class="comment">
					18
					19
					20
					 
					21
					22
					23
					// Connect to the database, run a query, handle errors
					$pdo = getPDO();
					$row = getPostRow($pdo, $postId);
					// If the post does not exist, let's deal with that here
					if (!$row)
					18
					19
					20
					21
					22
					23
					24
					// Connect to the database, run a query, handle errors
					$pdo = getPDO();
					$row = getPostRow($pdo, $postId);
					$commentCount = $row['comment_count'];
					// If the post does not exist, let's deal with that here
					if (!$row)
					I noticed when doing some testing at this point that posts which have comments cannot be deleted - try that now. You'll get an error known as a foreign key constraint violation; this is what happens when a row is deleted that a foreign key depends on (in our case we are trying to delete a post to which comments are still attached). To fix this, comments should be deleted first, prior to the post being deleted:
 
		 
	- lib/list-posts.php lib/list-posts.php
 
				3
					4
					5
					 
					 
					6
					7
					8
					12
					13
					14
					15
					16
					 
					 
					 
					 
					 
					 
					17
					18
					19
					20
					21
					22
					23
					24
					25
					 
					 
					 
					26
					27
					28
					29
					 
					 
					 
					 
					 
					 
					 
					30
					31
					32
					/**
					 * Tries to delete the specified post
					 *
					 * @param PDO $pdo
					 * @param integer $postId
					 * @return boolean Returns true on successful deletion
					 */
					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;
					}
					3
					4
					5
					6
					7
					8
					9
					10
					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
					/**
					 * Tries to delete the specified post
					 *
					 * We first delete the comments attached to the post, and then delete the post itself
					 *
					 * @param PDO $pdo
					 * @param integer $postId
					 * @return boolean Returns true on successful deletion
					 */
					function deletePost(PDO $pdo, $postId)
					{
						$sqls = array(
							// Delete comments first, to remove the foreign key objection
							"DELETE FROM
								comment
							WHERE
								post_id = :id",
							// Now we can delete the post
							"DELETE FROM
								post
							WHERE
								id = :id",
						);
						foreach ($sqls as $sql)
						{
							$stmt = $pdo->prepare($sql);
							if ($stmt === false)
							{
								throw new Exception('There was a problem preparing this query');
							}
							$result = $stmt->execute(
								array('id' => $postId, )
							);
							// Don't continue if something went wrong
							if ($result === false)
							{
								break;
							}
						}
						return $result !== false;
					}
					
	Whilst we are looking at the database side of things, I noticed that the
	user.is_enabled field hasn't yet been used. This was intended mainly for future
	enhancements (in particular for a user administration page), but the application can make an
	initial use of it immediately, by disallowing authenticated features to non-enabled users.
	The installer sets the test user to is_enabled=1 already, so all we need
	to do is to adjust the SQL statements that fetch data from the user table.
 
		 
	- lib/common.php lib/common.php
 
				159
					160
					161
					 
					162
					163
					164
					225
					226
					227
					 
					228
					229
					230
								user
							WHERE
								username = :username
						";
						$stmt = $pdo->prepare($sql);
						$stmt->execute(
								user
							WHERE
								username = :username
						";
						$stmt = $pdo->prepare($sql);
						$stmt->execute(
					159
					160
					161
					162
					163
					164
					165
					225
					226
					227
					228
					229
					230
					231
								user
							WHERE
								username = :username
								AND is_enabled = 1
						";
						$stmt = $pdo->prepare($sql);
						$stmt->execute(
								user
							WHERE
								username = :username
								AND is_enabled = 1
						";
						$stmt = $pdo->prepare($sql);
						$stmt->execute(
					Our final improvement is to add some labels and make use of existing form styles:
 
		 
	- login.php login.php
 
				57
					58
					59
					 
					60
					61
					62
					 
					 
					63
					64
					 
					65
					66
					67
					68
					69
					70
					71
					72
					 
					 
					 
					 
					 
					 
					73
					74
					75
							<form
								method="post"
							>
								<p>
									Username:
									<input
										type="text"
										name="username"
										value="<?php echo htmlEscape($username) ?>"
									/>
								</p>
								<p>
									Password:
									<input type="password" name="password" />
								</p>
								<input type="submit" name="submit" value="Login" />
							</form>
						</body>
					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
							<form
								method="post"
								class="user-form"
							>
								<div>
									<label for="username">
										Username:
									</label>
									<input
										type="text"
										id="username"
										name="username"
										value="<?php echo htmlEscape($username) ?>"
									/>
								</div>
								<div>
									<label for="password">
										Password:
									</label>
									<input
										type="password"
										id="password"
										name="password"
									/>
								</div>
								<input type="submit" name="submit" value="Login" />
							</form>
						</body>
					Well done, you have finished the tutorial! It is a good idea to regenerate the database one last time, and then give the whole application a thorough testing. Add some posts and comments, both while logged in and while logged out, and delete comments in the admin interface.
 Download
						Download