Learn PHP in One Day and Learn It Well

PHP for Beginners with Hands-on Project. The only book you need to start coding in PHP immediately. By Jamie Chan

Chapter 13: Project

Congratulations on making it to the end of the book! We’ve covered a lot in the preceding chapters.

The best way to learn programming is to work on an actual project. In this chapter, we’re going to work through a project together. This project covers numerous concepts you’ve learned in the previous chapters and allows you to see how everything works together. We’ll also be covering some new miscellaneous concepts in this project. Excited? Let’s do it!

13.1 About the Project

This project involves creating a website that works like a mini blog, where users with an admin account can log in to post. When posting to the blog, the admin user can choose whether to make the post available to non-members or members.

Admin and members need to be logged in while non-members do not. Admin can read and write posts, members can read “Members only” posts after they log in, and non-members can only read “Public” posts.

When a member or admin logs in to the site, the website retrieves the last post he/she has read and notifies him/her which posts are new.

To see a demonstration of how the site works, go tohttps://learncodingfast.com/php.

13.2 Acknowledgements and Requirements

This project uses HTML, CSS, Javascript, PHP and MySQL. An understanding of HTML (especially HTML forms) and SQL is essential for the project. This chapter only covers PHP and will guide you through all the PHP code used.

All HTML, CSS, Javascript and SQL code will be provided for you. You can download them athttps://learncodingfast.com/php.

However, as the project uses Bootstrap (https://getbootstrap.com/) for the user interface and CKEditor (https://ckeditor.com/ckeditor-4/) for the text editor, their code will not be provided. Instead, links will be provided in the <head> element to include these files from their respective content delivery networks (CDN). This means that you need an internet connection when running the code in the project.

Certain instructions (such as creating a database and user account) provided here are specifically for XAMPP. If you are not using XAMPP, you’ll have to refer to the software’s documentation for specific instructions.

Last but not least, this project uses PHP 5.5 and above. If you have just downloaded XAMPP, the PHP interpreter bundled with it is at least PHP 7.

13.3 Structure of the Project

The main folder of the project is phpproject.

Inside this main folder, we have eight files: admin.php, error.html, index.php, logout.php, read.php, signup.php, UI_include.php and write.php. These files are responsible for the user interface of the blog.

Besides the eight files, we have a sub-folder called includes.

Inside includes, we have three files: loadclasses.php, header.html and debugging.php.

We also have three sub-folders: process (contains files used for processing HTML forms), classes (contains files where we define our classes) and css (contains files with CSS code).

13.4 Creating Database, User Account and Tables

Before we begin working on the PHP code, we need to create the database, tables and user account.

First, ensure that you have started XAMPP and the Apache and MySQL servers. Next, proceed tohttp://localhost/phpmyadmin/index.phpand follow the instructions in Chapter 11.5 to create a database called “project”.

Next, create a user with the following information for the “project” database:

User Name: project_admin
Host Name: localhost

Choose your own desired password for the user and click “Go” to create the user.

Once that is done, click on the SQL tab at the top of the page and copy the following SQL code into the editor. This code can be downloaded athttps://learncodingfast.com/php.

USE project;
CREATE TABLE IF NOT EXISTS members (
	username VARCHAR(100) PRIMARY KEY,
 	password VARCHAR(255) NOT NULL,
 	is_admin BOOLEAN DEFAULT false,
	last_viewed int DEFAULT 0
);
CREATE TABLE IF NOT EXISTS posts(
	id INT AUTO_INCREMENT PRIMARY KEY,
	post_date TIMESTAMP DEFAULT NOW() NOT NULL,
	username VARCHAR(100) NOT NULL,
	title VARCHAR(255) NOT NULL,
	post TEXT NOT NULL,
	audience INT NOT NULL,
 	CONSTRAINT FOREIGN KEY (username) REFERENCES members(username) ON DELETE CASCADE
);

The code above creates two tables, “members” and “posts”, for the “project” database. Click on “Go” to execute the code. Once that is done, we are ready to work on the PHP code.

13.5 Editing The classes Folder

First, navigate to your htdocs folder and paste the unzipped phpproject folder into it. Next, launchhttp://localhost/phpproject/index.phpin your browser. If you see the page below, all is good.

Close your browser and navigate to the htdocs\phpproject\includes\classes folder on your computer. You should see five PHP files inside.

13.5.1 Helper.php

We’ll start with Helper.php. Open this file in Brackets; you’ll see that we’ve created a class called Helper .

This class has no properties and constructor. Inside the class, our job is to implement five public methods – passwordsMatch()isValidLength()isEmpty()isSecure() and keepValues() .

Let’s start with the passwordsMatch() method. This method has two parameters, $pw1 and $pw2 . In this project, to keep our code compatible with older versions of PHP, we will not be using type declaration for functions and methods. With this in mind, try declaring the passwordsMatch() method yourself.

Next, within the method, we need to check if the values of the two parameters are equal. If they are, we return true . Else, we return false . Try doing this yourself. Hint: You need to use an if-else statement. Once you are done with the if-else statement, the passwordsMatch() method is complete.

Next, let’s move on to the isValidLength() method. This method has three parameters – $str$min and $max .

 $min has a default value of 8 , while $max has a default value of 20 . You can refer to Chapter 7.1 if you are not familiar with default values for functions (and methods).

Within the method, we need to check if the length of $str is smaller than $min or greater than $max . If it is, we return false . Else, we return true .

Try coding the method yourself. Hint: You can use the built-in function strlen() to get the length of $str .

After the isValidLength() method, we have the isEmpty() method. This method has one parameter, $postValues , which stores an array. The method checks if any of the elements in the array is an empty string.

Inside the method, we need to use a foreach loop to loop through each element in $postValues and use an if statement to check if the element equals an empty string. If it equals, we return true .

After looping through all the elements, if no empty string is found, we return false . In other words, we return false outside the foreach loop.

Try coding this method yourself. You can refer to Chapter 6.3.5 for help on using foreach loops.

Next, we have the isSecure() method. This method has one parameter – $pw – and checks if $pw contains at least one lowercase character, one uppercase character and one digit. To do so, we need to use regular expressions. A regular expression allows us to translate the requirements above (written in English) into an expression that PHP can understand.

All regular expressions must start and end with a delimiter. This delimiter can be any non-alphanumeric, non-backslash and non-whitespace character. Often used delimiters include forward slashes ( / ), hash signs ( # ) and tildes ( ~ ). We’ll use tildes in our code.

The regular expression for “at least one lowercase character” is ~[a-z]+~ , where ~ is the delimiter, [a-z] represents the set of lowercase characters and + represents “at least one”.

The regular expression for “at least one uppercase character” is ~[A-Z]+~ and that for “at least one digit” is ~[0-9]+~ .

To check if $pw satisfies the regular expressions above, we need to use a built-in function called preg_match() . This function accepts two arguments – the regular expression and the string that we want to check. The regular expression is passed as a string to the function.

To check if $pw contains at least one lowercase character, we write

preg_match("~[a-z]+~", $pw)

This returns true if $pw contains at least one lowercase character. Else, it returns false .

Our isSecure() method needs to apply the preg_match() function three times to check if all three requirements are met. If all three are met, it returns true . Else, it returns false . Try doing this yourself.

Done?

Last but not least, let’s move on to the most complicated method in the Helper class. This method is used to preserve user input in a form when the form is not processed successfully (you can refer to Chapter 8.1.4 for more details). To do that, we need to prefill the form with the user’s previous input when the form reloads.

To prefill textboxes, we use the value attribute. For instance,

<input type="text" value = "Hello">

prefills a textbox with “Hello”.

To prefill textareas, we enclose the text between the <textarea> opening and closing tags. For instance,

<textarea>Hello</textarea>

prefills a textarea with “Hello”.

To preselect drop-down lists, we add the word “selected” to the selected option. For instance,

<select>
	<option value = 'P'>Public</option>
	<option value = 'M' selected>Members Only</option>
 </select>

preselects the “Members Only” option.

Our job now is to write a method to prefill/preselect user input for us. This method is called keepValues() and has three parameters – $val$type and $attr .

 $val represents the value submitted by the user. For textboxes and textareas, the value submitted is the text entered into the respective form elements. For drop-down lists, the value submitted is the string assigned to the value attribute of the selected option. For instance, in the drop-down list above, if the second option is selected, the value submitted is 'M' (not “Members Only”).

 $type represents the type of form element.

 $attr is only applicable for drop-down lists and has a default value of '' (an empty string). It represents the string assigned to the value attribute of a drop-down list’s option. For instance, for the drop-down list above, $attr is 'P' for the first option and 'M' for the second.

Try declaring this method yourself.

Next, inside the method, we have the following switch statement:

switch ($type){
	case 'textbox':
		echo "value = '$val'";
		break;
	case 'textarea':
		//Add code here
	case 'select':
		//Add code here
	default:
		echo '';
}

This statement uses the value of $type to determine what string to echo. The first case has been completed for you. Based on the description above, try completing the other two cases yourself by echoing a suitable string for the respective form elements.

Hint: Refer to the underlined text for each HTML form element in the description above.

For the 'select' case, you need to use an if statement to compare the value of $val (which is the value submitted by the user) with $attr (which is the string assigned to the value attribute of an option) to decide whether to echo anything for a particular option. Got it?

You may need to read through this section more than once to complete this switch statement.

Once you are done with the switch statement, the keepValues() method is complete and so is the Helper class. Remember to close the braces for the switch statement, the keepValues() method and the class itself.

13.5.2 Database.php

Next, let’s move on to the Database.php file. Inside this file, we’ve created a class called Database with three constants ( SELECTSINGLESELECTALL and EXECUTE ) and one private property ( $pdo ). In addition, we have a constructor that is used to create a new PDO object.

You need to modify two things in the constructor. First, on line 13, you need to change “Your Password” to your actual password.

Next, notice that $pdo is a property of the Database class? We learned in Chapter 9.2 that to access any property of a class, you need to use the $this keyword inside the class.

Hence, on line 13, you need to change

$pdo = …

to

$this->pdo = …

The same applies to line 14. Try doing this yourself. Once that is done, the constructor is complete.

Next, we need to add a public method called queryDB() to the Database class. To code this method, you need to be familiar with using prepared statements in PHP. You can refer to Chapter 11.4 for reference if you are not familiar.

The queryDB() method has three parameters, $sql$mode and $values .

 $sql represents the SQL statement to be executed, $mode indicates whether the method needs to fetch any row(s) from the database and $values , which has a default value of array() (i.e., an empty array), is used for binding variables to the placeholders in $sql .

Try declaring the method yourself.

Inside the method, we need to use the $pdo property (reminder: you access it using $this->pdo ) to prepare the SQL statement ( $sql ) and assign the result to a variable called $stmt . Try doing this yourself.

Next, we need to bind values to the placeholders in the SQL statement. The placeholders and values for binding are passed as a two-dimensional array ( $values ) to the method. Refer to Chapter 5.3.1 if you are not familiar with multidimensional arrays.

Suppose the placeholders are :username and :password and the variables to bind are $uname and $pwd respectively, users need to pass the following array to the queryDB() method:

$values = array(
	array(':username', $uname),
	array(':password', $pwd)
);

To process this array, we use the foreach loop below:

foreach($values as $valueToBind){
    $stmt->bindValue($valueToBind[0], $valueToBind[1]);
}

When this loop runs for the first time, the array (':username', $uname) gets assigned to $valueToBind . Inside the loop, we use the bindValue() method to bind $uname$valueToBind[1] ) to ':username'$valueToBind[0] ).

When the loop runs for the second time, we use the bindValue() method to bind $pwd to ':password' . Got it?

Copy the foreach loop above into the queryDB() method and make sure you understand it before proceeding.

After binding values to placeholders, we need to use $stmt to call the execute() method. Try doing this yourself.

Finally, we need to determine if there are any values to be fetched. We do that using the second parameter – $mode .

 $mode can take one of three constants – SELECTSINGLESELECTALL or EXECUTE . These three constants were defined in the class previously. We use an if statement to determine whether we should fetch any results. The if statement works similar to the pseudocode below:

if ($mode is not equal to SELECTSINGLE, SELECTALL and EXECUTE){
	 throw an Exception	using 'Invalid Mode' as the error message
}
else if ($mode equals SELECTSINGLE){
	 use $stmt to call the fetch(PDO::FETCH_ASSOC) method and return the result using the return keyword
}
else if ($mode equals SELECTALL){
	 use $stmt to call the fetchAll(PDO::FETCH_ASSOC) method and return the result using the return keyword
}

First, we check if the value of $mode is valid (i.e., it must be either SELECTSINGLESELECTALL or EXECUTE ).

Next, we check if $mode is SELECTSINGLE or SELECTALL and use the $stmt variable to call the fetch(PDO::FETCH_ASSOC) or fetchAll(PDO::FETCH_ASSOC) method respectively. We then use the return keyword to return the results fetched.

Try converting the pseudocode to PHP code yourself.

Hint:

To access the constants defined in the Database class, you need to use the self keyword or the class name followed by the :: operator. For instance, to access EXECUTE , you can write Database::EXECUTE . Refer to Chapter 9.7 for reference on using constants in classes.

To throw an exception, you create a new Exception object and pass the error message to the constructor. Refer to Chapter 12.1.3 for reference on throwing exceptions.

Once you are done with the if statement, the queryDB() method is complete and so is the Database class.

13.5.3 BlogReader.php

Now, let’s proceed to the BlogReader class. This class has two constants, READER and MEMBER , with values 1 and 2 respectively. In addition, it has two protected properties $db and $type . The constructor of the class has been coded for you.

public function __construct(){
	$this->db = new Database();
	$this->type = BlogReader::READER;
}

This constructor initializes the values of $db and $type

Now, we need to add a method called getPostsFromDB() to the class. This method is public and has no parameter; try declaring it yourself.

A blog reader refers to readers of the blog who are not logged in. Readers who are not logged in can only read posts from the “posts” table where the “audience” column has a value smaller than or equal to 1. Hence, inside the getPostsFromDB() method, we need to use the following statement to query the “posts” table:

$sql = "SELECT id, unix_timestamp(post_date) as `post_date`, username, title, post, audience FROM posts WHERE audience <= :audience ORDER BY id DESC";

Add the statement above to the getPostsFromDB() method; we’ll use the queryDB() method to execute it later.

This statement has one placeholder :audience .

Based on what we mentioned when we coded the Database class, we need to declare a two-dimensional array and use it to bind a value to the placeholder. This is done with the following statement:

$values = array(
	array(':audience', $this->type)
);

Here, we declare a two dimensional array called $values with one inner array –  (':audience', $this->type) .

We use this inner array to bind the $type property to the :audience placeholder. As we’ve previously assigned BlogReader::READER (which equals 1 ) to the $type property in the constructor, we are essentially binding the value 1 to the :audience placeholder. Got it?

Add the $values array above to the getPostsFromDB() method. Next, we need to use the Database object ( $db ) to call the queryDB() method in the Database class. To do that, we use the statement below:

$result = $this->db->queryDB($sql, Database::SELECTALL, $values);

Here, we use $this->db to access $db as it is a class property. After accessing $db , we use it to call the queryDB() method, passing the SQL statement ( $sql ), the mode ( Database::SELECTALL ) and the $values array to the method.

The mode is Database::SELECTALL as we are retrieving more than one row from the “posts” table. Next, we assign the result returned to a variable called $result .

Add the statement above to the getPostsFromDB() method.

Finally, we need to check if there are any rows returned by the queryDB() method. To do that, we check if there are any elements in $result . If there aren’t, we return false . Else, we return the $result array. Try writing this if statement yourself.

Hint: You can use the count() function to get the number of elements in $result .

Once that is done, the getPostsFromDB() method is complete and so is the BlogReader class. Read through the getPostsFromDB() method carefully and make sure you understand it before proceeding; we’ll be coding many methods similar to it later.

13.5.4 BlogMember.php

Now, let’s write a class that extends the BlogReader class.

Open BlogMember.php and create a new class called BlogMember . This class extends the BlogReader class and has a private property called $username .

Try declaring the class yourself.

Inside the class, we have a constructor that has one parameter – $pUsername .

Inside the constructor, we need to call the parent class constructor to initialize the inherited property $db . Next, we need to assign $pUsername to the $username property and BlogMember::MEMBER (this constant is inherited from the BlogReader class) to the inherited property $type .

Try implementing the constructor yourself. Hint: You need to use the parent keyword, followed by the :: operator, to call the parent class constructor.

Got it? Good! After you have implemented the constructor, you need to implement six more methods. The methods are:

public function isDuplicateID()
public function insertIntoMemberDB($pPassword)
public function isValidLogin($pPassword)
private function getLatestPostID()
public function updateLastViewedPost()
public function getLastViewedPost()

Let’s start with the isDuplicateID() method. This method is public and has no parameter. It is called whenever a new user signs up and returns true if the username selected by the new user already exists in the “members” table. Try declaring the method yourself.

Inside the method, we need to execute the following SQL statement:

SELECT count(username) AS num FROM members WHERE username = :username

This statement returns 0 if the username bound to :username is not found.

Try using the queryDB() method in the Database class to execute this SQL statement. You need to bind the $username property to :username and use the Database::SELECTSINGLE mode (as we are only selecting one row from the database) to call the queryDB() method.

Try doing this yourself. You can refer to the getPostsFromDB() method in the BlogReader class for reference on using the queryDB() method. Got it?

Once that is done, assign the result returned by queryDB() to a variable called $result .

Next, use an if statement to check if $result['num'] is equal to zero. If it is, return false . Else, return true .

Try coding this method yourself.

Once you have completed the isDuplicateID() method, we can move on to the insertIntoMemberDB() method.

This method is public and has one parameter – $pPassword . It is used to insert a new row into the “members” table when a new user signs up.

 $pPassword stores the password entered by the user when he/she submits the sign-up form.

Inside the method, we need to use the queryDB() method with the Database::EXECUTE mode (as we are not fetching any data from the database) to execute the following SQL statement:

INSERT INTO members (username, password) VALUES (:username, :password)

To execute this statement, we need to bind $username (the class property) to :username and $pPassword (the parameter) to :password .

However, as $pPassword stores the password selected by our user, we should not store it in the database in its original form. Instead, we should hash it first. Hashing is similar to encrypting and can be done using a built-in function in PHP called password_hash() . This function is available from PHP 5.5 onwards and accepts two arguments – the string to hash and the algorithm to use.

The algorithm to use can be any of the predefined constants found athttps://www.php.net/manual/en/password.constants.php.

In our project, we’ll use PASSWORD_DEFAULT as the constant. This constant indicates that we’ll use the default algorithm in PHP. To hash $pPassword , we use the code below:

password_hash($pPassword, PASSWORD_DEFAULT)

Hence, to bind values to the :username and :password placeholders in our SQL statement, we use the $values array below:

$values = array(
    array(':username', $this->username),
array(':password', password_hash($pPassword, PASSWORD_DEFAULT))
);

Try using this array, the Database::EXECUTE constant and the SQL INSERT statement above to call the queryDB() method. Once that is done, the insertIntoMemberDB() method is complete.

There is no need to return any result for this method as the queryDB() method does not return any result when the mode is Database::EXECUTE .

Done? Great!

Let’s move on to the isValidLogin() method. This method is public and has one parameter – $pPassword . It checks if the username and password entered by the user are valid when he/she tries to log in.

Inside the method, we need to use the queryDB() method to execute the following SQL statement:

SELECT password FROM members WHERE username = :username

To do that, you need to bind the $username property to :username and use the Database::SELECTSINGLE mode (as we are only selecting one row from the database) to call the queryDB() method.

Once that is done, assign the result to a variable called $result .

Try doing this yourself.

Next, we need to check if $result['password'] is set.

 $result['password'] is set only if the fetch() method in queryDB() manages to fetch the password. If there is no user with the username stated in the SQL query, $result['password'] will not be set.

Besides checking if $result['password'] is set, we also need to check if the password entered by the user matches the password fetched from the database.

Recall that the password stored in the database is hashed and no longer in its original form?

To check if the hashed password matches the password entered by the user, we need to use another built-in function called password_verify() . This function accepts two arguments and returns true if the first argument matches the second. The second argument has to be a password hashed using the password_hash() function.

In our isValidLogin() method, to determine if the two passwords match, we use the following if statement:

if (isset($result['password']) && password_verify($pPassword, $result['password']))
    return true;
else
    return false;

Add the statement above to the isValidLogin() method and the method is complete.

Great! Let’s proceed to the getLatestPostID() method. This is a private method with no parameters and will be used in the updateLastViewedPost() method later. Try declaring it yourself.

Inside the method, we need to use the queryDB() method to execute the following SQL statement:

SELECT max(id) AS max FROM posts

Decide on the appropriate mode to use for this SQL statement and try calling the queryDB() method yourself. Note that for this SQL statement, there is no need to pass any array to the queryDB() method as the statement does not contain any placeholder.

Once you have executed the queryDB() method, assign the result to a variable called $result .

The SQL statement above returns NULL if there is no post in the “posts” table.

Hence, we need to first check if $result['max'] is set. If it is, we return its value. Else, we return 0 . Try doing this yourself. Once this is done, the getLatestPostID() method is complete and we can move on to the updateLastViewedPost() method.

The updateLastViewedPost() method is public and has no parameters. Try declaring it yourself.

Inside the method, we need to update the “last_viewed” column of the “members” table to reflect the latest post viewed by a member. Whenever a member logs into our website, the “last_viewed” value of that member will be updated to the id of the latest post in the “posts” table.

This id is given by the getLatestPostID() method we coded earlier. To use this method, add the following line to the updateLastViewedPost() method:

$max = $this->getLatestPostID();

Here, we use the $this keyword to call the getLastestPostID() method and assign its result to $max . Next, we need to use the queryDB() method to execute the following SQL statement:

UPDATE members SET last_viewed = :max WHERE username = :username

To execute this statement, we need to bind the $username property to :username and the variable $max to :max . In addition, we need to decide on the appropriate mode to use when calling queryDB() .

Try doing this yourself. Once this is done, the method is complete.

Finally, we move on to the getLastViewedPost() method. This method is public and has no parameters. It uses the queryDB() method to execute the following SQL statement:

SELECT last_viewed FROM members WHERE username = :username

To execute this statement, we need to bind the $username property to :username and decide on the appropriate mode to use when calling the queryDB() method. Try doing this yourself and assign the result to a variable called $result .

Next, we need to check if $result['last_viewed'] is set. If there is no user with the username stated in the SQL query, $result['last_viewed'] will not be set.

If $result['last_viewed'] is set, we return its value. Else, we return 0 .

Try doing this yourself. Once this is done, the getLastViewedPost() method is complete and so is the BlogMember class.

13.5.5 Admin.php

We are left with one more class to code – the Admin class.

The Admin class has two private properties – $db and $username .

Inside the class, we have a constructor with one parameter – $pUsername . This constructor initializes $username with $pUsername and $db with a new Database object. Try declaring the class and properties and implement the constructor yourself.

Next, we have a public method called isValidLogin() that has one parameter – $pPassword . This method is very similar to the isValidLogin() method in the BlogMember class except that it executes the following SQL statement:

SELECT password FROM members WHERE username = :username AND is_admin = true

Try coding this method yourself.

Once that is done, we can move on to the final method – insertIntoPostDB() .

This method is public and has three parameters – $title$post and $audience .

Inside the method, we use the queryDB() method to execute the following SQL statement:

INSERT INTO posts (username, title, post, audience) VALUES (:username, :title, :post, :audience)

You need to bind the $username property to :username and the parameters $title$post and $audience to :title:post and :audience respectively. In addition, you need to choose the correct mode for queryDB() .

Try coding this method yourself.

Done? Great! All our classes are now complete. We are ready to move on to the process folder.

13.6 Editing The process Folder

The files in the process folder are for processing HTML forms.

13.6.1 p-index.php

We’ll start with p-index.php. This file is for processing index.php, which contains a form for members to log in to read “Members Only” posts.

index.php has two input boxes named “username” and “password” and one button named “submit”. If an error occurs when processing index.php, the page echoes an error message stored in a variable called $msg .

If you open p-index.php (inside the process folder), you’ll see that the code has already been completed for you. This is done so that you can refer to this file when working on other processing files later. Let’s run through the code together.

First, we declare a variable called $h and assign a new Helper object to it. Next, we declare two variables $msg and $username and assign an empty string to each of them.

After declaring and initializing the variables, we are ready to process the form in index.php. We use the following if statement to check if the “submit” button has been clicked.

if (isset($_POST['submit'])){
}

Inside the if block, we first assign $_POST['username'] to $username . We need to do this as we’ll be using $username in index.php later.

Next, we use an if-else statement to check if users have entered data into both the “username” and “password” input boxes.

To do that, we use $h to call the isEmpty() method in the Helper class. This method accepts an array that contains all the variables we want to check. We pass the following array to the method:

array($username, $_POST['password'])

If any of the elements in the array is an empty string, the isEmpty() method returns true . When that happens, the if block is executed and the string 'All fields are required' is assigned to $msg .

On the other hand, if none of the elements are empty, the else block is executed.

Inside the else block, we create a BlogMember object called $member . Next, we have another if-else statement. This inner if-else statement uses the $member object to call the isValidLogin() method in the BlogMember class.

If the method returns false , the condition

 !$member->isValidLogin($_POST['password'])

evaluates to true (as !false equals true ) and the if block is executed. The string 'Invalid Username or Password' will then be assigned to $msg .

On the other hand, if the method returns true , the else block is executed. Within this else block, we assign $username to a session variable called $_SESSION['username'] and use the header() function to redirect users to read.php. Once that is done, the p-index.php page is complete.

Go through p-index.php carefully and make sure you understand it before proceeding. Got it? Great!

13.6.2 p-admin.php

Let’s move on to the p-admin.php file now. This file is for processing admin.php, which contains a form for admin to log in.

admin.php has two input boxes named “username” and “password” and one button named “submit”.  If an error occurs when processing admin.php, the page echoes an error message stored in a variable called $msg .

As you may have guessed, p-admin.php is very similar to p-index.php. Here’s what we need to do in p-admin.php:

First, declare and initialize $h$msg and $username (similar to what was done in p-index.php).

Next, check if the “submit” button has been clicked. If it has, assign $_POST['username'] to $username .

Next, ensure that all input boxes are not empty.

If any of the boxes are empty, assign an appropriate error message to $msg . Else, create an Admin object and use it to call the isValidLogin() method. (Refer to the Admin class to find out what needs to be passed to the constructor and the isValidLogin() method when calling them.)

If isValidLogin() returns false , assign an appropriate error message to $msg to inform users that the login credentials are invalid. Else, assign $username to $_SESSION['username'] . In addition, declare a session variable called $_SESSION['is_admin'] and assign true to it. Finally, redirect users to write.php using the header() function.

Got it? Try coding p-admin.php yourself. You can refer to p-index.php for reference.

Once you are done, we are ready to move on to p-signup.php.

13.6.3 p-signup.php

p-signup.php is for processing signup.php, which contains a form for members to sign up.

To keep this project short, we’ll only create a sign-up form for blog members, not for admin. We’ll learn to convert a blog member to an admin later using phpMyAdmin.

The sign-up form for blog members (signup.php) has three input boxes named “username”, “password” and “confirm_password” and one button named “submit”. If an error occurs when processing signup.php, the page echoes an error message stored in a variable called $msg .

Here’s what we need to do in p-signup.php:

First, declare and initialize three variables $h$msg and $username (similar to what was done in p-index.php).

Next, check if the “submit” button has been clicked. If it has, we first assign $_POST['username'] to $username . Next, we need to ensure the following:

  1. All input boxes in signup.php have been filled out
  2.  $username has a length of between 6 and 100 characters (inclusive)
  3.  $_POST['password'] has a length of between 8 and 20 characters (inclusive)
  4.  $_POST['password'] contains at least one lowercase character, one uppercase character and one digit.
  5.  $_POST['password'] and $_POST['confirm_password'] match

If any of the cases above are not met, we assign an appropriate error message to $msg . Else, we do the following:

Create a BlogMember object and use it to call the isDuplicateID() method.

If the method returns true , assign an appropriate error message to $msg , informing users that the username is already in use. Else, use the BlogMember object to call the insertIntoMemberDB() method and use the header() function to redirect users to index.php. Append the query string new=1 to index.php.

Got it? Try coding p-signup.php yourself.

Hint: Refer to p-index.php for help on processing forms. If you are not familiar with query strings, you can refer to Chapter 8.1.2.

To check the five cases listed above, you can use the isEmpty()isValidLength()isSecure() and passwordsMatch() methods in the Helper class.

Refer to Helper.php and BlogMember.php to figure out what needs to be passed to the various methods in the Helper and BlogMember classes when calling them.

Once you are done with p-signup.php, we can move on to p-write.php. This file is for processing write.php, which contains a form for admin to write their posts.

13.6.4 p-write.php

write.php has an input box named “title”, a textarea named “post”, a drop-down list named “audience” and a button named “submit”. If an error occurs when processing write.php, the page echoes an error message stored in a variable called $msg .

If you analyze the code in write.php, you’ll notice that we added a <script> element after the textarea. This is for replacing the textarea with a more advanced text editor known as the CKEDITOR (available for free athttps://ckeditor.com/ckeditor-4/). We won’t go into details on how to use CKEDITOR as it only affects the user interface. Using CKEDITOR does not affect our PHP code in any way.

Here’s what we need to do in p-write.php:

First, we need to retrieve two session variables – $_SESSION['username'] and $_SESSION['is_admin'] – and check if they are set. If either of the session variables is not set, we know that the user is trying to access write.php without logging in with an admin account.

If that’s the case, we use the header() function to redirect them to admin.php. Else, we do the following:

First, declare and initialize a Helper object called $h . Next, declare four variables $title$post$audience and $msg and assign an empty string to each of them.

After that, check if the “submit” button has been clicked.

If it has, assign $_POST['title']$_POST['post'] and $_POST['audience'] to $title$post and $audience respectively.

Next, check that all fields in write.php have been filled out. If there is an empty field, assign an appropriate error message to $msg .

Else, create an Admin object using $_SESSION['username'] as the argument. Use this object to call the insertIntoPostDB() method. (Refer to the Admin class to decide what arguments to pass to the method.) Once that is done, assign the string 'Message saved successfully' to $msg .

Clear? Try coding p-write.php yourself. Once you are done, we are ready to move on to the most complex processing file – p-read.php.

13.6.5 p-read.php

This file is for processing read.php, which is used for displaying posts to readers. We need to implement two essential features in p-read.php:

First, depending on whether the user is logged in, we need to display different posts. If the user is logged in, we display all posts. Else, we only display “Public” posts. Next, if the user is logged in, we need to display a “New” icon for posts posted to the database after the user’s last login.

To achieve the above, open p-read.php and do the following:

First, declare a variable called $h and assign a Helper object to it. Next, declare a variable called $update and assign false to it.

Last but not least, declare a variable called $is_member and assign isset($_SESSION['username']) to it.

Here, we use the isset() function to check if the session variable $_SESSION['username'] is set. If it is (i.e., isset() returns true ), we know that the reader accessing read.php is logged in.

Try doing the above yourself.

Next, we need to use a couple of if-else statements in p-read.php. The structure is shown below:

if ($is_member){
	//Create a BlogMember object and use it to call the getLastViewedPost() method
}
else{
	//Create a BlogReader object
}
// Call getPostsFromDB() method
if ($posts == false){
 	//include the blankcard.html file
}
else{
	//use a foreach loop to process the elements in $posts
	//include the messagecard.php file
}
if ($is_member){
 	//include the logout.html file
	if ($update)
		 //use the BlogMember object to call the updateLastViewedPost() method
}

In the first if-else statement, we check if the user is logged in. If the user is logged in (i.e., $is_member is true ), we initialize a BlogMember object and assign it to a variable called $reader . We then use $reader to call the getLastViewedPost() method in the BlogMember class and assign the result to a variable called $lastPost .

On the other hand, if the user is not logged in, we initialize a BlogReader object and assign it to $reader .

Try coding this if-else statement yourself. Refer to the respective classes to find out what needs to be passed to the various methods when calling them.

Once the if-else statement is complete, we need to use the $reader object to call the getPostsFromDB() method; this method is defined in the BlogReader class.

Recall that BlogMember is a subclass of BlogReader ? Hence, regardless of whether $reader is a BlogMember or BlogReader object, it can access the getPostsFromDB() method. Try calling this method yourself and assign the result to a variable called $posts .

Next, we need to use a second if-else statement to check if getPostsFromDB() returned any results.

If there are no results (i.e., $post is false ), we want to display the HTML code in blankcard.html (located in the output_code folder). To include this file, we use the following statement:

include "output_code/blankcard.html";

Next, inside the else block (if there are results), we use a foreach loop to loop through $posts .

 $posts is a two-dimensional array fetched using the getPostsFromDB() method in the BlogReader class. This method uses the queryDB() method in the Database class, which in turn uses the built-in fetchAll() method in the PDO class. The fetchAll() method fetches data from a table as a two-dimensional array, where each element in the array is an array representing a row from the table.

In our p-read.php file, we assign the data fetched from the “posts” table to $posts . To process $posts , we can loop through it using the following foreach loop.

foreach($posts as $result){
	$msgid = $result['id'];
	$title = htmlspecialchars($result['title']);
	$post = strip_tags($result['post'], "<strong><em><p><ol><ul><li><a>");
  	$username = htmlspecialchars($result['username']);
	$postdate = htmlspecialchars($result['post_date']);
 	include "output_code/messagecard.php";
}

For each iteration, we assign the array in $posts to $result . Next, inside the loop, we assign the elements in $result to various variables.

For instance, we assign $result['title'] to $title . However, before we do that, we apply the htmlspecialchars() function to $result['title'] first. The same applies to $result['username'] and $result['post_date'] .

The reason for this is we’ll be displaying the values of these elements in read.php later. Hence, we have to convert any special characters in these elements to HTML entities to prevent cross-site scripting. Refer to Chapter 8.1.6 if you have forgotten what cross-site scripting is.

However, notice that we did not apply the htmlspecialchars() function to $result['post'] ?

This is because we want to allow certain HTML tags in $result['post'] . Specifically, we want to allow the <strong><em><p><ol><ul><li> and <a> tags.

To do that, we need to use another built-in function called strip_tags() . This function strips a string of all HTML tags except those passed as a second argument to the function. Hence,

strip_tags($result['post'], "<strong><em><p><ol><ul><li><a>");

strips $result['post'] of all HTML tags except the ones we want. Got it?

Good! After assigning all the elements in $result to their respective variables, we use an include statement to include the messagecard.php file. messagecard.php is stored in the output_code folder and contains code for displaying the values of those variables above.

Once that is done, the foreach loop is complete. Based on the code and description given above, try completing the second if-else statement yourself.

Once you are done, we can move on to the last if statement. This statement checks if $is_member is true . If it is, we use an include statement to include the logout.html file. This file is stored in the output_code folder and contains HTML code with a logout link. In addition, we use an inner if statement to check if $update is true . If it is, we use the $reader object to call the updateLastViewedPost() method in the BlogMember class.

Try completing this if statement yourself. Once that is done, the p-read.php file is complete.

Great! You have completed the hardest file in this project; we just need to tie up some loose ends now.

13.6.6 messagecard.php

First, we need to add some code to the messagecard.php file. This file is found inside the output_code folder. As mentioned above, this file contains code for displaying posts retrieved from the “posts” table.

When a user is logged in to our blog, we need to display a “New” icon in read.php for posts that were posted after the user’s last login.

As posts are displayed using the messagecard.php file, we need to make some modifications to this file.

To do that, replace the comment ( //add PHP code here ) in messagecard.php with the following if statement:

<?php
	if ($is_member and $lastPost < $msgid){
 	  echo '<span class = "new-post">NEW</span>';
 	$update = true;
  	}
?>

Here, we use an if statement to check if the user is logged in. In addition, we check if $lastPost is smaller than $msgid .

 $lastPost stores the id of the last post viewed by the user. We got that by calling the getLastViewedPost() method in the first if-else statement in p-read.php.

 $msgid stores the id of the current post in the foreach loop.

If $lastPost is smaller than the id of the current post, we know that this is a new post. Hence, we use an echo statement to echo a <span> tag with the word “NEW”. In addition, we set the value of $update to true .

 $update is used to indicate whether we need to call the updateLastViewedPost() method (in the last if statement in p-read.php). If $update is true , we call the method to update the value of the “last_viewed” column in the “members” table to the latest post id in the “posts” table.

Got it? Refer to the BlogMember class (BlogMember.php) if you are not sure how the updateLastViewedPost() method works.

After inserting the if statement, we need to replace some text in messagecard.php with PHP code. Specifically, we need to replace “post_title”, “user_name”, “post_date” and “post_text” with the values of $title$username$postdate and $post respectively.

We do that using echo statements. For instance, to replace “post_title”, we write

<?php echo $title; ?>

Try replacing “user_name”, “post_date” and “post_text” yourself. However, before replacing “post_date”, you need to convert $postdate to a string. This is because $postdate currently stores a UNIX timestamp.

To display $postdate in a more human-readable format, you need to use the date() function to convert the timestamp to a datetime string. Try doing this yourself, using 'd-M-Y g:i a' as the first argument to the function. Refer to Chapter 5.2.2 if you need help with the date() function.

Once you have updated messagecard.php, the process folder is complete.

13.7 The includes Folder

We are now ready to discuss the three remaining files in the includes folder. These three files are header.html, debugging.php and loadclasses.php.

header.html contains HTML code for the <head> element and has already been completed for you.

debugging.php contains code for handling errors and exceptions. This file was explained in detail in Chapter 12.3. Hence, we won’t be going through it here.

However, in this project, note that we did not try to catch any exceptions. Instead, we use debugging.php to handle all exceptions. This is because it is pointless for the site to proceed when an exception occurs. For instance, if we fail to connect to the database, the rest of the site will not work. Therefore, it makes sense to simply use debugging.php to handle the exception.

If an exception or error occurs, debugging.php displays a detailed message on the browser when display_errors is set to '1' (i.e. when we are developing the site). When display_errors is set to '0' (i.e. on a live site), it logs the message and redirects users to error.html (which is stored in the main phpproject folder).

Last but not least, we have the loadclasses.php file. As the name suggests, this file is for autoloading classes. Did you notice that in all the previous files we coded, I asked you to create objects without asking you to include the relevant class files? For instance, in p-admin.php, I asked you to create a Helper object without asking you to include Helper.php.

This will typically lead to a fatal error as each PHP script is unaware of code written in another PHP script. Hence, p-admin.php is unaware of the class defined in Helper.php. To prevent that error, we need to include Helper.php before we create a Helper object.

However, instead of including this file ourselves, PHP provides us with a convenient alternative known as an autoloader. If we define an autoloader in our PHP script, whenever we create an object in that script, the autoloader includes the file for us automatically.

To use an autoloader, we need to ensure that the file name matches the class name. For instance, if the file name is MyClass.php, the class defined inside has to be called MyClass .

Besides that, using an autoloader is straightforward. If you open loadclasses.php, you’ll see that we’ve defined a function called myAutoloader() that has one parameter – $class . Inside this function, we use an include_once statement to include the relevant class file.

 INC_DIR is a constant that gives us a direct path to the includes folder in our project; we’ll talk more about this constant in the next section.

Inside our myAutoloader() function, we concatenate INC_DIR with the string 'classes/' , the variable $class and the string '.php' to get a direct path to the respective class files.

For instance, if $class equals 'Helper' , the concatenation gives us the string

INC_DIR.'classes/Helper.php';

which is a direct path to the Helper.php file. We then use the include_once statement to help us include this file. Got it?

After we code myAutoloader() , we need to use a built-in function called  spl_autoload_register() to register it as the autoloader.

Once this is done, we simply need to include loadclasses.php in all our PHP scripts and PHP will autoload classes for us whenever we create an object.

That’s it! We are now ready to go back to the main phpproject folder. 

13.8 Editing The phpproject Folder

Inside this folder, we have eight files, seven of which are user interface files. The seven files are signup.php, admin.php, write.php, index.php, read.php, logout.php and error.html.

Besides these user interface files, we have a file called UI_include.php.

13.8.1 UI_include.php

We’ll start with the UI_include.php file. If you open this file, you’ll see that it has already been completed for you. Let’s go through the code.

Inside the file, we first define a constant called INC_DIR and assign the string

$_SERVER["DOCUMENT_ROOT"]. "/phpproject/includes/"

to it. $_SERVER['DOCUMENT_ROOT'] is a predefined variable that stores the document root directory under which the current script is executing. If you are using XAMPP, this root directory refers to the htdocs folder.

When we concatenate $_SERVER['DOCUMENT_ROOT'] and "/phpproject/includes/" , we get a direct path to the includes folder.

If you rename the phpproject folder, you have to edit the path assigned to INC_DIR accordingly. For instance, if you rename the folder to myproject, you have to assign $_SERVER["DOCUMENT_ROOT"]. "/myproject/includes/" to INC_DIR instead.

Next, we have two include statements for including loadclasses.php and debugging.php. As mentioned previously, we use loadclasses.php to autoload our classes and debugging.php to handle all errors and exceptions. These two files have to be included in most of our user interface files later.

With that, the UI_include.php file is complete and we are ready to move on to the user interface files.

13.8.2 User Interface Files

The first is the error.html file. This file contains HTML code for displaying a custom message to users when an error or exception occurs on our site. It has already been completed for you.

Next, we have five very similar files – admin.php, write.php, index.php, signup.php and read.php.

Inside each file, we need to add PHP code to do the following: start a new session and include the UI_include.php and header.html files.

In addition, each of the files contains a HTML form that is processed by the file itself. To process the form, we need to use an include statement to add the relevant processing file to the user interface file. For instance, to process the form in admin.php, we need to include the p-admin.php file.

The above has already been done for you in admin.php.

With reference to the code in admin.php, try doing the same for write.php, index.php, signup.php and read.php. In each case, note that the UI_include.php file must be included before the other two files (as we need INC_DIR to be defined before using it). In addition, you need to change the processing file accordingly. Got it?

Great! Once you are done with the above, we need to make some modifications.

For signup.php, there is no need to start a new session as p-signup.php does not use any session variable. Hence, we should remove the session_start(); statement from signup.php.

Next, for read.php, as p-read.php includes code for displaying output, we should not include it at the start of the file. Instead, we should include it between the <body>...</body> tags.

Last but not least, for signup.php, admin.php, index.php and write.php, we need to replace the text “Error Message Here” (without quotes) in the HTML code with the actual error message stored in a variable called $msg . To do that, we use the PHP code below:

<?php echo $msg; ?>

Try doing all the modifications above yourself.

Now, we need to make one more modification to index.php. If you refer to p-signup.php, you’ll notice that we added the query string new=1 when directing users to index.php after a successful sign-up, we want to make use of this query string inside index.php.

To do that, add the following code to index.php after the line <div class="form"> :

<div class = "new">
	<?php
 		if (isset($_GET['new']))
			echo 'ACCOUNT CREATED SUCCESSFULLY';
	?>
</div>

Here, we use the query string to check if users are directed to index.php after a successful sign-up. If yes, we echo the string 'ACCOUNT CREATED SUCCESSFULLY' .

Got it? Great. Let’s move on to the last user interface file – logout.php. As the code for logging out is very straightforward, we did not create a separate file for processing logout.php. Instead, we’ll add the processing code to logout.php directly.

To do that, we need to do a few things in logout.php. First, we need to resume the existing session (using session_start() ) and destroy all variables in that session (using session_destroy() ). Next, we need to include the UI_include.php and header.html files.

Try doing the above yourself. Once that is done, load the pagehttp://localhost/phpproject/logout.phpin your browser; you’ll notice two identical links that say, “Click here to log in again”.

This is not a mistake. The first link points to admin.php while the second points to index.php.

If you refer back to write.php and study the code carefully, you’ll notice that we added a query string ( admin=1 ) to the logout URL near the end of the page. This query string tells us that the person logging out is an admin user.

When that happens, we want to display the first logout link in logout.php. On the other hand, if the person is not an admin user (i.e., there is no query string), we want to display the second logout link. Try using an if-else statement in logout.php to achieve the above. You can refer to index.php for help on using query strings.

Once that is done, the logout.php file is complete.

The project is almost complete at this point. In fact, if you have done everything correctly, you can loadhttp://localhost/phpproject/signup.phpand everything will work.

Try entering your desired username into the first input box and click “Submit”. What do you notice? You should get an error message that says “All fields are required”. Notice that the username you entered into the first input box is gone?

The last part of our project involves adding PHP code to your user interface files to ensure that values entered into forms are maintained if there’s an error processing the form.

Recall that we wrote a method called keepValues() inside the Helper class for this purpose? Suppose we have a Helper object called $h , here’s how we use the method:

If we have a form with a textbox named “tb” and we store the value submitted for “tb” as $tb , we write

 <input type="text" name="tb" <?php $h->keepValues($tb, 'textbox');?> >

If we have a textarea named “ta” and we store the value submitted for “ta” as $ta , we write

 <textarea name = "ta"><?php $h->keepValues($ta, 'textarea'); ?></textarea>

Finally, if we have a dropdrop list named “sl” with two value attributes “1” and “2”, and we store the value submitted for “sl” as $sl , we write

<select name = "sl">
	<option value = '1' <?php $h->keepValues($sl, 'select', '1'); ?>>1</option>
 	<option value = '2' <?php $h->keepValues($sl, 'select', '2'); ?>>2</option>
</select>

In our project, the names of the variables used to store each input correspond to the names of the input fields. For instance, $username stores the input for the textbox named “username”.

Based on the description above, modify index.php, signup.php, admin.php and write.php so that all values entered, except passwords, are maintained if there’s an error processing the form. Got it?

Once the above is done, we need to make some additional changes to write.php. For most pages, if there’s no error processing the form, the website automatically loads another page based on the URL we passed to the header() function.

However, this does not happen for write.php. For this page, after the admin clicks on the “submit” button, he/she will stay on the same page regardless of whether data is submitted to the database successfully or not.

We want to modify write.php so that if data is inserted into the database successfully, we’ll clear the form so that the admin can write a new post. In other words, if data is inserted successfully, we do not want to call the keepValues() method.

To do that, we need to use the variable $msg declared in p-write.php. Recall that after data is inserted into the database, we assign the string 'Message saved successfully' to $msg ? We can use this string to decide whether we need to call the keepValues() method.

The example below shows how it can be done for the first input field (“title”). The underlined code shows the if condition to use.

<input id = "txttitle" type="text" name="title" placeholder="Enter Title" autofocus <?php if ($msg != 'Message saved successfully') $h->keepValues($title, 'textbox'); ?>>

Try doing this for the other input fields.

Once that is done, we’ve completed the project. Congratulations! You are now ready to test your code to see if everything works as expected.

13.9 Running the Code

Before running the code, we need to make some changes to php.ini. Follow the instructions in Chapter 2.1 to locate php.ini and open it in Brackets. Scroll to the bottom of the page and add the following lines to it (if you have yet to do so in previous chapters):

error_reporting=E_ALL
display_errors=On
date.timezone=America/New_York

Next, we want to add one more line to php.ini. Specifically, we want to add a line to prepend debugging.php. Prepending a file means specifying that we want PHP to process this file before it processes any other PHP script.

Previously, we used the include statement in UI_include.php to add debugging.php to our PHP scripts. This works for most errors. However, it will not work if the file that debugging.php is included in has syntax errors. If you want debugging.php to work even when there are syntax errors, you need to prepend the file. To do that, add the line

auto_prepend_file="<DOCUMENT_ROOT>/phpproject/includes/debugging.php"

to php.ini, where <DOCUMENT_ROOT> refers to the actual path of your htdocs folder.

To find this path, loadhttp://localhost/dashboard/phpinfo.phpin your browser and search for “DOCUMENT_ROOT” (without quotes). You’ll see the path listed beside. You need to replace <DOCUMENT_ROOT> with the path listed. For instance, if the path is /opt/lampp/htdocs , the line should be

auto_prepend_file="/opt/lampp/htdocs/phpproject/includes/debugging.php"

The code above may appear as two lines in this book due to the limited width of the book. Do not break it into two lines, it should be written as a single line in php.ini. After prepending debugging.php, if you are using PHP 7.2 and above, you should also turn track_errors off.

 track_errors is a built-in feature in PHP that has been deprecated since PHP 7.2. However, this feature is set to “On” by default. Leaving it on may lead to a warning that says “Directive ‘track_errors’ is deprecated found on line 0 in file Unknown” on some servers. To turn it off, simply add track_errors=Off to the bottom of php.ini. That’s it. You can now save the file and restart Apache.

Next, go to UI_include.php and comment out the include statement for debugging.php. As we have already prepended the file, we should not include it again.

Next, launchhttp://localhost/phpproject/signup.phpin your browser and sign up for two new accounts.

Everything works? If yes, congratulations! Give yourself a pat on the back.

If no, it’s all right. Figuring out what went wrong is a large part of programming. This, to me, is where the most learning takes place. If something fails to work, you will likely get an error message. Try to use the line number and file name given in the error message to figure out what is wrong.

Alternatively, you can use echo statements to determine which part of your code works. For instance, if you add an echo statement to line 10 of your code and you don’t see the output, you know that something has likely gone wrong before line 10. Similarly, if you add an echo statement to an if block and don’t see the output, you know that the if block is not executed.

Try finding the error yourself. If you really can’t figure out the issue, you can check the suggested solution in the phpproject-complete folder and compare the code with your code. Copy and paste the functions that you suspect may be causing the error from the suggested solution to your solution and rerun your code to see if it works. Study the suggested solution carefully to really understand how it works. Got it?

Once you manage to find the bug and are able to sign up for two accounts successfully, you can proceed to convert one of them to an admin account. To do that, go tohttp://localhost/phpmyadmin/and click on the “project” database. Click on the SQL tab and run the following SQL statement, replacing YOUR_ADMIN_USERNAME with the username you want to convert to an admin user:

UPDATE members SET is_admin = 1 WHERE username = 'YOUR_ADMIN_USERNAME';

Once that is done, you can use the admin account to post to the blog. To do that, go tohttp://localhost/phpproject/admin.phpto log in as an admin. Try adding some posts to the blog. Does everything work?

After posting, you can go tohttp://localhost/phpproject/index.phpto log in as a member (using either account) to read the posts.

Alternatively, you can go tohttp://localhost/phpproject/read.phpdirectly to read “Public” posts without logging in.

Play around with the site to see if everything works as expected. If something does not work, try debugging it. With perseverance, you can definitely find the error and learn a lot in the process. Have fun!