Make your own blog
Improving the installer
At this juncture, I decided to tidy up the installer a bit more. Firstly, it would be nice to see some data about what it has created; in the course of development you can expect to wipe and recreate your test data hundreds of times, so it's worth making the output useful.
Secondly, as it stands it demonstrates non-optimal techniques, and fixing that gives me an opportunity to explain how to improve upon it. Broadly, the problem is that visiting the URL changes the database, but it does not take into account that web addresses can receive visits from automated software (e.g. search engines looking for new websites). To be sure that it is a human who has requested an install, I've used a form with a "post" method (this is explained in more detail later).
The changes here are quite substantial, so you may find it easier to download the file, and copy the whole thing over the top of the old version. So, here's the diff:
 
		 
	- install.php install.php
 
				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
					120
					121
					122
					123
					124
					125
					126
					 
					 
					 
					 
					 
					 
					 
					 
					 
					 
					 
					 
					 
					 
					 
					 
					 
					 
					127
					128
					129
					130
					131
					132
					133
					134
					135
					136
					137
					138
					139
					140
					141
					142
					143
					<?php
					require_once 'lib/common.php';
					// Get the PDO DSN string
					$root = getRootPath();
					$database = getDatabasePath();
					$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 = getPDO();
						$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 = array();
					foreach(array('post', 'comment') as $tableName)
					{
						if (!$error)
						{
							$sql = "SELECT COUNT(*) AS c FROM " . $tableName;
							$stmt = $pdo->query($sql);
							if ($stmt)
							{
								// We store each count in an associative array
								$count[$tableName] = $stmt->fetchColumn();
							}
						}
					}
					?>
							</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 foreach (array('post', 'comment') as $tableName): ?>
										<?php if (isset($count[$tableName])): ?>
											<?php // Prints the count ?>
											<?php echo $count[$tableName] ?> new
											<?php // Prints the name of the thing ?>
											<?php echo $tableName ?>s
											were created.
										<?php endif ?>
									<?php endforeach ?>
								</div>
							<?php endif ?>
						</body>
					</html>
					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
					97
					98
					99
					100
					101
					102
					120
					121
					122
					123
					124
					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
					156
					 
					 
					157
					158
					159
					<?php
					require_once 'lib/common.php';
					function installBlog()
					{
						// Get the PDO DSN string
						$root = getRootPath();
						$database = getDatabasePath();
						$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 = getPDO();
							$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 = array();
						foreach(array('post', 'comment') as $tableName)
						{
							if (!$error)
							{
								$sql = "SELECT COUNT(*) AS c FROM " . $tableName;
								$stmt = $pdo->query($sql);
								if ($stmt)
								{
									// We store each count in an associative array
									$count[$tableName] = $stmt->fetchColumn();
								}
							}
						}
						return array($count, $error);
					}
					// We store stuff in the session, to survive the redirect to self
					session_start();
					// Only run the installer when we're responding to the form
					if ($_POST)
					{
						// Here's the install
						list($_SESSION['count'], $_SESSION['error']) = installBlog();
						// ... and here we redirect from POST to GET
						$host = $_SERVER['HTTP_HOST'];
						$script = $_SERVER['REQUEST_URI'];
						header('Location: http://' . $host . $script);
						exit();
					}
					// Let's see if we've just installed
					$attempted = false;
					if ($_SESSION)
					{
						$attempted = true;
						$count = $_SESSION['count'];
						$error = $_SESSION['error'];
						// Unset session variables, so we only report the install/failure once
						unset($_SESSION['count']);
						unset($_SESSION['error']);
					}
					?>
							</style>
						</head>
						<body>
							<?php if ($attempted): ?>
								<?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 foreach (array('post', 'comment') as $tableName): ?>
											<?php if (isset($count[$tableName])): ?>
												<?php // Prints the count ?>
												<?php echo $count[$tableName] ?> new
												<?php // Prints the name of the thing ?>
												<?php echo $tableName ?>s
												were created.
											<?php endif ?>
										<?php endforeach ?>
									</div>
								<?php endif ?>
							<?php else: ?>
								<p>Click the install button to reset the database.</p>
								<form method="post">
									<input
										name="install"
										type="submit"
										value="Install"
									/>
								</form>
							<?php endif ?>
						</body>
					</html>
					Finally, let's add some links so we can easily move to our next task after re-installing:
 
		 
	- install.php install.php
 
				140
					141
					142
					 
					 
					 
					 
					 
					143
					144
					145
											<?php endif ?>
										<?php endforeach ?>
									</div>
								<?php endif ?>
							<?php else: ?>
					140
					141
					142
					143
					144
					145
					146
					147
					148
					149
					150
											<?php endif ?>
										<?php endforeach ?>
									</div>
									<p>
										<a href="index.php">View the blog</a>,
										or <a href="install.php">install again</a>.
									</p>
								<?php endif ?>
							<?php else: ?>
					As is our custom, delete your database file and re-run the installer. This time you should have an "Install" button. All being well, it will do the same installation when you click it, and then you can opt to go straight to the blog's home page.
Since our changes to the installer have been brief, let's do some refactoring and a couple of minor functionality tweaks. Now, it could be said that this process of tidying seems rather erratic, and that similar items of work should be neatly collected together. However, real-life development rarely works that way: instead, a rough but usable solution is developed, and then improvements and refactorings are added depending on user feedback, and often also on time available.
So, here's our first small improvement task. One of the things you may have noticed is that posts and comments are being marked as written on a particular day, but with no time information. It is usual to record and show this sort of data, so let's do that now. Don't forget to re-run the installer to test it!
 
		 
	- data/init.sql data/init.sql
- lib/common.php lib/common.php
 
				24
					25
					26
					27
					28
					29
					30
					38
					39
					40
					41
					42
					43
					44
					52
					53
					54
					55
					56
					57
					58
					74
					75
					76
					77
					78
					79
					80
					88
					89
					90
					91
					92
					93
					94
					It is split into paragraphs.",
							1,
							date('now', '-2 months')
						)
					;
							"This is the body of the second post.
					This is another paragraph.",
							1,
							date('now', '-40 days')
						)
					;
							"This is the body of the third post.
					This is split into paragraphs.",
							1,
							date('now', '-13 days')
						)
					;
						)
						VALUES(
							1,
							date('now', '-10 days'),
							'Jimmy',
							'http://example.com/',
							"This is Jimmy's contribution"
						)
						VALUES(
							1,
							date('now', '-8 days'),
							'Jonny',
							'http://anotherexample.com/',
							"This is a comment from Jonny"
					24
					25
					26
					27
					28
					29
					30
					38
					39
					40
					41
					42
					43
					44
					52
					53
					54
					55
					56
					57
					58
					74
					75
					76
					77
					78
					79
					80
					88
					89
					90
					91
					92
					93
					94
					It is split into paragraphs.",
							1,
							datetime('now', '-2 months', '-45 minutes', '+10 seconds')
						)
					;
							"This is the body of the second post.
					This is another paragraph.",
							1,
							datetime('now', '-40 days', '+815 minutes', '+37 seconds')
						)
					;
							"This is the body of the third post.
					This is split into paragraphs.",
							1,
							datetime('now', '-13 days', '+198 minutes', '+51 seconds')
						)
					;
						)
						VALUES(
							1,
							datetime('now', '-10 days', '+231 minutes', '+7 seconds'),
							'Jimmy',
							'http://example.com/',
							"This is Jimmy's contribution"
						)
						VALUES(
							1,
							datetime('now', '-8 days', '+549 minutes', '+32 seconds'),
							'Jonny',
							'http://anotherexample.com/',
							"This is a comment from Jonny"
					54
					55
					56
					57
					58
					59
					60
					61
					62
					function convertSqlDate($sqlDate)
					{
						/* @var $date DateTime */
						$date = DateTime::createFromFormat('Y-m-d', $sqlDate);
						return $date->format('d M Y');
					}
					/**
					54
					55
					56
					57
					58
					59
					60
					61
					62
					function convertSqlDate($sqlDate)
					{
						/* @var $date DateTime */
						$date = DateTime::createFromFormat('Y-m-d H:i:s', $sqlDate);
						return $date->format('d M Y, H:i');
					}
					/**
					Following on from our last bit of refactoring, here's another opportunity to tidy code. Arguably, the view post page has too much business logic in it, and it would be more maintainable to move this to a separate file. So, create lib/view-post.php and paste in the new content.
 
		 
	- lib/view-post.php lib/view-post.php
- view-post.php view-post.php
 
				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
					<?php
					/**
					 * Retrieves a single post
					 *
					 * @param PDO $pdo
					 * @param integer $postId
					 * @throws Exception
					 */
					function getPostRow(PDO $pdo, $postId)
					{
						$stmt = $pdo->prepare(
							'SELECT
								title, created_at, body
							FROM
								post
							WHERE
								id = :id'
						);
						if ($stmt === false)
						{
							throw new Exception('There was a problem preparing this query');
						}
						$result = $stmt->execute(
							array('id' => $postId, )
						);
						if ($result === false)
						{
							throw new Exception('There was a problem running this query');
						}
						// Let's get a row
						$row = $stmt->fetch(PDO::FETCH_ASSOC);
						return $row;
					}
					1
					2
					 
					3
					4
					5
					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
					<?php
					require_once 'lib/common.php';
					// Get the post ID
					if (isset($_GET['post_id']))
					// Connect to the database, run a query, handle errors
					$pdo = getPDO();
					$stmt = $pdo->prepare(
						'SELECT
							title, created_at, body
						FROM
							post
						WHERE
							id = :id'
					);
					if ($stmt === false)
					{
						throw new Exception('There was a problem preparing this query');
					}
					$result = $stmt->execute(
						array('id' => $postId, )
					);
					if ($result === false)
					{
						throw new Exception('There was a problem running this query');
					}
					// Let's get a row
					$row = $stmt->fetch(PDO::FETCH_ASSOC);
					// Swap carriage returns for paragraph breaks
					$bodyText = htmlEscape($row['body']);
					1
					2
					3
					4
					5
					6
					15
					16
					17
					18
					 
					 
					 
					 
					 
					 
					 
					 
					 
					 
					 
					 
					 
					 
					 
					 
					 
					 
					 
					 
					 
					19
					20
					21
					<?php
					require_once 'lib/common.php';
					require_once 'lib/view-post.php';
					// Get the post ID
					if (isset($_GET['post_id']))
					// Connect to the database, run a query, handle errors
					$pdo = getPDO();
					$row = getPostRow($pdo, $postId);
					// Swap carriage returns for paragraph breaks
					$bodyText = htmlEscape($row['body']);
					One of the situations we have not yet catered for is the user requesting a blog article that does not exist. We need to handle that based on the maxim that "if something can go wrong, it will". There are a good few small changes here, but in essence our approach is that if we cannot find a database row, we issue a browser redirect and show an error.
 
		 
	- index.php index.php
- install.php install.php
- lib/common.php lib/common.php
- view-post.php view-post.php
 
				16
					17
					18
					 
					 
					19
					20
					21
					28
					29
					30
					 
					 
					 
					 
					 
					 
					31
					32
					33
						throw new Exception('There was a problem running this query');
					}
					?>
					<!DOCTYPE html>
					<html>
						<body>
							<?php require 'templates/title.php' ?>
							<?php while ($row = $stmt->fetch(PDO::FETCH_ASSOC)): ?>
								<h2>
									<?php echo htmlEscape($row['title']) ?>
					16
					17
					18
					19
					20
					21
					22
					23
					28
					29
					30
					31
					32
					33
					34
					35
					36
					37
					38
					39
						throw new Exception('There was a problem running this query');
					}
					$notFound = isset($_GET['not-found']);
					?>
					<!DOCTYPE html>
					<html>
						<body>
							<?php require 'templates/title.php' ?>
							<?php if ($notFound): ?>
								<div style="border: 1px solid #ff6666; padding: 6px;">
									Error: cannot find the requested blog post
								</div>
							<?php endif ?>
							<?php while ($row = $stmt->fetch(PDO::FETCH_ASSOC)): ?>
								<h2>
									<?php echo htmlEscape($row['title']) ?>
					80
					81
					82
					83
					84
					85
					86
					87
					88
					89
						list($_SESSION['count'], $_SESSION['error']) = installBlog();
						// ... and here we redirect from POST to GET
						$host = $_SERVER['HTTP_HOST'];
						$script = $_SERVER['REQUEST_URI'];
						header('Location: http://' . $host . $script);
						exit();
					}
					// Let's see if we've just installed
					80
					81
					82
					83
					 
					 
					 
					84
					85
					86
						list($_SESSION['count'], $_SESSION['error']) = installBlog();
						// ... and here we redirect from POST to GET
						redirectAndExit('install.php');
					}
					// Let's see if we've just installed
					59
					60
					61
					 
					 
					 
					 
					 
					 
					 
					 
					 
					 
					 
					 
					 
					 
					62
					63
					64
						return $date->format('d M Y, H:i');
					}
					/**
					 * Returns the number of comments for the specified post
					 *
					59
					60
					61
					62
					63
					64
					65
					66
					67
					68
					69
					70
					71
					72
					73
					74
					75
					76
					77
					78
						return $date->format('d M Y, H:i');
					}
					function redirectAndExit($script)
					{
						// Get the domain-relative URL (e.g. /blog/whatever.php or /whatever.php) and work
						// out the folder (e.g. /blog/ or /).
						$relativeUrl = $_SERVER['PHP_SELF'];
						$urlFolder = substr($relativeUrl, 0, strrpos($relativeUrl, '/') + 1);
						// Redirect to the full URL (http://myhost/blog/script.php)
						$host = $_SERVER['HTTP_HOST'];
						$fullUrl = 'http://' . $host . $urlFolder . $script;
						header('Location: ' . $fullUrl);
						exit();
					}
					/**
					 * Returns the number of comments for the specified post
					 *
					17
					18
					19
					 
					 
					 
					 
					 
					 
					20
					21
					22
					$pdo = getPDO();
					$row = getPostRow($pdo, $postId);
					// Swap carriage returns for paragraph breaks
					$bodyText = htmlEscape($row['body']);
					$paraText = str_replace("\n", "</p><p>", $bodyText);
					17
					18
					19
					20
					21
					22
					23
					24
					25
					26
					27
					28
					$pdo = getPDO();
					$row = getPostRow($pdo, $postId);
					// If the post does not exist, let's deal with that here
					if (!$row)
					{
						redirectAndExit('index.php?not-found=1');
					}
					// Swap carriage returns for paragraph breaks
					$bodyText = htmlEscape($row['body']);
					$paraText = str_replace("\n", "</p><p>", $bodyText);
					Since the browser redirect is useful, it has been written as a function, and re-used by the installer. Although there is no new data to install, it is a good idea to delete your database file and re-install at this point, just to check it still works.
 Download
						Download