In this tutorial, we’re going to build a PHP/MySQL powered forum from scratch. This tutorial is perfect for getting used to basic PHP and database usage. Let’s dive right in!
It’s always a good idea to start with creating a good data model when building an application. Let’s describe our application in one sentence: We are going to make a forum which has users who create topics in various categories. Other users can post replies. As you can see, I highlighted a couple of nouns which represent our table names.
These three objects are related to each other, so we’ll process that in our table design. Take a look at the scheme below.

Looks pretty neat, huh? Every square is a database table. All the columns are listed in it and the lines between them represent the relationships. I’ll explain them further, so it’s okay if it doesn’t make a lot of sense to you right now.
I’ll discuss each table by explaining the SQL, which I created using the scheme above. For your own scripts you can create a similar scheme and SQL too. Some editors like MySQL Workbench (the one I used) can generate .sql files too, but I would recommend learning SQL because it’s more fun to do it yourself. A SQL introduction can be found at W3Schools.
1 2 3 4 5 6 7 8 9 10 | CREATE TABLE users ( user_id INT(8) NOT NULL AUTO_INCREMENT, user_name VARCHAR(30) NOT NULL, user_pass VARCHAR(255) NOT NULL, user_email VARCHAR(255) NOT NULL, user_date DATETIME NOT NULL, user_level INT(8) NOT NULL, UNIQUE INDEX user_name_unique (user_name), PRIMARY KEY (user_id) ) TYPE=INNODB; |
The CREATE TABLE statement is used to indicate we want to create a new table, of course. The statement is followed by the name of the table and all the columns are listed between the brackets. The names of all the fields are self-explanatory, so we’ll only discuss the data types below.
“A primary key is used to uniquely identify each row in a table.”
The type of this field is INT, which means this field holds an integer. The field cannot be empty (NOT NULL) and increments which each record inserted. At the bottom of the table you can see the user_id field is declared as a primary key. A primary key is used to uniquely identify each row in a table. No two distinct rows in a table can have the same value (or combination of values) in all columns. That might be a bit unclear, so here’s a little example.
There is a user called John Doe. If another users registers with the same name, there’s a problem, because: which user is which? You can’t tell and the database can’t tell either. By using a primary key this problem is solved, because both topics are unique.
All the other tables have got primary keys too and they work the same way.
This is a text field, called a VARCHAR field in MySQL. The number between brackets is the maximum length. A user can choose a username up to 30 characters long. This field cannot be NULL. At the bottom of the table you can see this field is declared UNIQUE, which means the same username cannot be registered twice. The UNIQUE INDEX part tells the database we want to add a unique key. Then we define the name of the unique key, user_name_unique in this case. Between brackets is the field the unique key applies to, which is user_name.
This field is equal to the user_name field, except the maximum length. Since the user password, no matter what length, is hashed with sha1(), the password will always be 40 characters long.
This field is equal to the user_pass field.
This is a field in which we’ll store the date the user registered. It’s type is DATETIME and the field cannot be NULL.
This field contains the level of the user, for example: ’0′ for a regular user and ’1′ for an admin. More about this later.
1 2 3 4 5 6 7 | CREATE TABLE categories ( cat_id INT(8) NOT NULL AUTO_INCREMENT, cat_name VARCHAR(255) NOT NULL, cat_description VARCHAR(255) NOT NULL, UNIQUE INDEX cat_name_unique (cat_name), PRIMARY KEY (cat_id) ) TYPE=INNODB; |
These data types basically work the same way as the ones in the users table. This table also has a primary key and the name of the category must be an unique one.
1 2 3 4 5 6 7 8 | CREATE TABLE topics ( topic_id INT(8) NOT NULL AUTO_INCREMENT, topic_subject VARCHAR(255) NOT NULL, topic_date DATETIME NOT NULL, topic_cat INT(8) NOT NULL, topic_by INT(8) NOT NULL, PRIMARY KEY (topic_id) ) TYPE=INNODB; |
This table is almost the same as the other tables, except for the topic_by field. That field refers to the user who created the topic. The topic_cat refers to the category the topic belongs to. We cannot force these relationships by just declaring the field. We have to let the database know this field must contain an existing user_id from the users table, or a valid cat_id from the categories table. We’ll add some relationships after I’ve discussed the posts table.
1 2 3 4 5 6 7 8 | CREATE TABLE posts ( post_id INT(8) NOT NULL AUTO_INCREMENT, post_content TEXT NOT NULL, post_date DATETIME NOT NULL, post_topic INT(8) NOT NULL, post_by INT(8) NOT NULL, PRIMARY KEY (post_id) ) TYPE=INNODB; |
This is the same as the rest of the tables; there’s also a field which refers to a user_id here: the post_by field. The post_topic field refers to the topic the post belongs to.
“A foreign key is a referential constraint between two tables. The foreign key identifies a column or a set of columns in one (referencing) table that refers to a column or set of columns in another (referenced) table.”
Now that we’ve executed these queries, we have a pretty decent data model, but the relations are still missing. Let’s start with the definition of a relationship. We’re going to use something called a foreign key. A foreign key is a referential constraint between two tables. The foreign key identifies a column or a set of columns in one (referencing) table that refers to a column or set of columns in another (referenced) table. Some conditions:
By adding foreign keys the information is linked together which is very important for database normalization. Now you know what a foreign key is and why we’re using them. It’s time to add them to the tables we’ve already made by using the ALTER statement, which can be used to change an already existing table.
We’ll link the topics to the categories first:
1 | ALTER TABLE topics ADD FOREIGN KEY(topic_cat) REFERENCES categories(cat_id) ON DELETE CASCADE ON UPDATE CASCADE; |
The last part of the query already says what happens. When a category gets deleted from the database, all the topics will be deleted too. If the cat_id of a category changes, every topic will be updated too. That’s what the ON UPDATE CASCADE part is for. Of course, you can reverse this to protect your data, so that you can’t delete a category as long as it still has topics linked to it. If you would want to do that, you could replace the ‘ON DELETE CASCADE’ part with ‘ON DELETE RESTRICT’. There is also SET NULL and NO ACTION, which speak for themselves.
Every topic is linked to a category now. Let’s link the topics to the user who creates one.
1 | ALTER TABLE topics ADD FOREIGN KEY(topic_by) REFERENCES users(user_id) ON DELETE RESTRICT ON UPDATE CASCADE; |
This foreign key is the same as the previous one, but there is one difference: the user can’t be deleted as long as there are still topics with the user id of the user. We don’t use CASCADE here because there might be valuable information in our topics. We wouldn’t want that information to get deleted if someone decides to delete their account. To still give users the opportunity to delete their account, you could build some feature that anonymizes all their topics and then delete their account. Unfortunately, that is beyond the scope of this tutorial.
Link the posts to the topics:
1 | ALTER TABLE posts ADD FOREIGN KEY(post_topic) REFERENCES topics(topic_id) ON DELETE CASCADE ON UPDATE CASCADE; |
And finally, link each post to the user who made it:
1 | ALTER TABLE posts ADD FOREIGN KEY(post_by) REFERENCES users(user_id) ON DELETE RESTRICT ON UPDATE CASCADE; |
That’s the database part! It was quite a lot of work, but the result, a great data model, is definitely worth it.
Each page of our forum needs a few basic things, like a doctype and some markup. That’s why we’ll include a header.php file at the top of each page, and a footer.php at the bottom. The header.php contains a doctype, a link to the stylesheet and some important information about the forum, such as the title tag and metatags.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="nl" lang="nl"> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <meta name="description" content="A short description." /> <meta name="keywords" content="put, keywords, here" /> <title>PHP-MySQL forum</title> <link rel="stylesheet" href="style.css" type="text/css"> </head> <body> <h1>My forum</h1> <div id="wrapper"> <div id="menu"> <a class="item" href="/forum/index.php">Home</a> - <a class="item" href="/forum/create_topic.php">Create a topic</a> - <a class="item" href="/forum/create_cat.php">Create a category</a> <div id="userbar"> <div id="userbar">Hello Example. Not you? Log out.</div> </div> <div id="content"> |
The wrapper div will be used to make it easier to style the entire page. The menu div obviously contains a menu with links to pages we still have to create, but it helps to see where we’re going a little bit. The userbar div is going to be used for a small top bar which contains some information like the username and a link to the logout page. The content page holds the actual content of the page, obviously.
The attentive reader might have already noticed we’re missing some things. There is no
or