Hands-on Project: Twitter Clone - Login and Signup
September 20, 2018
The wait is finally over! Welcome to your final project for the PHP course. In this three part project, you’ll create a clone of Twitter from scratch using PHP and some basic HTML and CSS. Completing this project will require the use of all the skills that you have learnt in this course.
Overview
Here is an overview of what you will be creating in each of the three parts:
- In the first part, you will learn how to structure your project. Then you will setup your database and create a login and signup page for your twitter clone website.
- In the second part, you will create the user feed and the user profile page.
- In the final part, you will add more features to the website such as follow / unfollow and search.
The guidance provided in these write-ups will also help you understand how to properly structure your code to incorporate all of the above features. Let’s get started!
Step 0: Resources
Before we begin, download all the project resources here. This includes HTML, CSS and image files that we will be using throughout the project.
We will focus mainly on PHP in this project and will use these resources to take care of the HTML and CSS.
Step 1: Directory Structure
Okay. Let’s begin by creating the directory structure for our project:
- Make sure your server running. We covered Running the Server in the installation tutorial.
- Open the
htdocs
folder located in{your_xampp_directory}
. - Create a new folder inside
{your_xampp_directory}/htdocs
and call ittwitter
.
Now create 3 new folders inside the twitter
folder:
assets
: for images, CSS files and javascript code. Create three folders inside the assets folder:css
,images
,js
.core
: for the core files of the project. Create two folders inside the core folder:classes
anddatabase
.includes
: for all files that will be included in other files.
Your directory structure will look like this:
Now open up the project-resources
folder that you downloaded and copy-paste the assets
folder into your project directory (inside twitter
folder).
Inside the css
folder, you will see style-complete.css
which contains all the CSS for our project. Inside the images
folder, you will find some background and profile images.
Step 2: Database Connection
Create a database in phpMyAdmin and call it twitter
.
Create the connection.php
file inside core/database/
folder.
Then, create a file index.php
inside the twitter
folder and include the connection.php
file in it.
Checkpoint: Open your browser and type in localhost/twitter
. If you see a blank page, the database connection is successful. The index.php
page will be the login and signup page.
Solution to this section can be found in section Solution: Database Connection below.
Step 3: Classes and init
Okay, now the next step is to answer the question: How many tables do you need? Think about it before seeing the answer below.
We will have a users
table to store the user data, a tweets
table to store the tweets and finally a follow
table to store the followers and following data. We can later add more tables depending on our requirements. But these are enough for now.
For each table, you need to create a class.
Remember the Base
class which has the generic insert, update and delete functions? Yes, you will be needing that as well.
Create these 4 classes in separate files inside core/classes/
folder.
Solution to this section can be found in section Solution: Classes and init below.
Now we will include all these classes (and also the connection.php
file) in file core/init.php
:
<!-- init.php -->
<?php
include 'database/connection.php';
include 'classes/base.php';
include 'classes/user.php';
include 'classes/tweet.php';
include 'classes/follow.php';
global $pdo;
session_start();
$getFromU = new User($pdo);
$getFromT = new Tweet($pdo);
$getFromF = new Follow($pdo);
define("BASE_URL", "http://localhost/twitter/");
?>
Notice that we are setting a global $pdo
variable to use the $pdo
database connection object everywhere in the code. We also add the session_start()
function and create instances of each class. We then create an instance of each of the three classes User
, Tweet
and Follow
. Finally, we use the define()
function which basically says we want to refer to http://localhost/twitter/
as BASE_URL
.
Now, instead of including the connection.php
file to index.php
, we will include init.php
:
<!-- index.php -->
<?php
include 'core/init.php';
?>
Step 4: User Table
Great! Now create a table users
inside the twitter
database with 8 columns in phpMyAdmin which should look like this:
Most of the rows are self explanatory. profileImage
will have the path to an image file that is stored on your machine. following
and followers
is the number of users the user is following, and the number of followers of a user, respectively.
Create this table using phpMyAdmin.
Solution to this section can be found in section Solution: User Table below.
Step 5: Index (login and sign-up) page HTML
Great! You created the table users
, have the User
class ready and also an instance of the User
class $getFromU
(we created this in init.php
).
Let’s now work on the front-end and write some PHP code to create the signup and login page (i.e. index.php
).
In this section, we will provide you the HTML for index.html (for the log-in and sign-up forms). You will write the
.php
code yourself in the next section.
Open the project-resources
folder and find index.html
. Copy all the HTML code from here and paste it below the PHP tags in index.php
:
<!-- index.php -->
<?php
include 'core/init.php';
?>
<html>
<head>
<title>Twitter Clone</title>
<meta charset="UTF-8" />
<link rel="stylesheet" href="assets/css/font/css/font-awesome.css"/>
<link rel="stylesheet" href="assets/css/style-complete.css"/>
</head>
<body>
<div class="bg">
<div class="wrapper">
<!---Inner wrapper-->
<div class="inner-wrapper-index">
<!-- main container -->
<div class="main-container">
<!-- content left-->
<div class="content-left">
<h1>Welcome to Twitter</h1>
<br/>
<p>See what's happening in the world right now.</p>
</div><!-- content left ends -->
<!-- content right ends -->
<div class="content-right">
<!-- Log In Section -->
<div class="login-wrapper">
<?php include 'includes/login.php' ?>
</div><!-- log in wrapper end -->
<!-- SignUp Section -->
<div class="signup-wrapper">
<?php include 'includes/signup-form.php' ?>
</div>
<!-- SIGN UP wrapper end -->
</div><!-- content right ends -->
</div><!-- main container end -->
</div><!-- inner wrapper ends-->
</div><!-- ends wrapper -->
</div>
</body>
</html>
Now we need to create the login.php
and signup-form.php
files that will have the login and signup forms which we will attach to the index.php
page. Go ahead and open the includes
folder and create two new files: login.php
and signup-form.php
with simple PHP open and close tags.
This is how includes/login.php
will look:
<!-- login.php -->
<?php ?>
And includes/signup-form.php
:
<!-- signup-form.php -->
<?php ?>
Checkpoint: Save the files and open http://localhost/twitter/
. You will see something like this:
Now we will add the HTML for the forms. Open up the project-resources
folder and copy paste the HTML from login.html
and signup-form.html
below the PHP tags inside login.php
and signup-form.php
respectively.
Checkpoint: Refresh the browser, and you should see this:
Looking good, right? Time to make the login and signup forms work.
Step 6: Login Form
Let’s create a record in the users table so that we can login with an email and password. Open phpMyAdmin and in the users table, browse to the SQL tab and execute this statement:
INSERT INTO `users`(`username`, `email`, `password`, `fullname`, `profileImage`) VALUES ("jamesmat","jamesmat@test.com",md5("password"),"James Mat","assets/images/defaultprofile1.png")
Now check the table for the record:
md5()
is a hashing function which encrypts the password. You should never store the password text directly. Always store a hash of the password. That way, even if someone maliciously gets access to the database, they will not know the users’ passwords.
OK, now this is your next task:
When the user enters email
and password
in the login form and clicks on the Login
button:
- Perform form validation
- Check that all the inputs are non-empty. If they are, set the
$error
variable to “Please enter email and password!“. - Remove special characters from the inputs using the
checkInput()
function. - Check if the email has a valid format. If not, set the
$error
variable to “Invalid Email format”. - Log the user in
- Check from the database if a user with the given email and password exists. If not, set the
$error
variable to “Invalid username or password”. - If the user exists, set the session
user_id
variable to theuser_id
of the logged in user and redirect the user tohome.php
. Createhome.php
, which includescore/init.php
andecho
s the sessionuser_id
variable.
Hint 1:
Do the form validation etc in login.php
, but create helper methods in User
class.
Hint 2:
Create methods checkInput()
and login()
inside the User
class and access them like $getFromU->checkInput()
and $getFromU->login()
in login.php
.
Your browser might re-submit a form when you refresh. This might cause you some confusion as you are debugging your code. For example, suppose you try to log-in, but there is some error in your code. After fixing the error, when you refresh your browser, your browser might automatically re-submit the form data as you had filled it last time. So you might see a another error even without filling the form, or you might get logged-in (if your code is working now).
Checkpoint: Enter the email and password that you added in the database and hit login.
If you are redirected to home.php
and you see the user_id
of the logged in user, everything is working fine. In this case the output would be:
1
Also, try different incorrect / empty inputs and see if the corresponding error messages are displayed or not.
Solution to this section can be found in section Solution: Login form below.
Step 7: Signup form
Awesome, the login form is working as expected. Let’s get the signup form working too. This is your task:
When the user enters the full-name, username, email and password in the signup form and clicks on the Signup
button,
- Perform form validation
- Check if all the inputs are not empty. If they are, set the
$signupError
variable to “Please enter email and password!“. - Remove special characters from the inputs using the
checkInput()
function. - Check if the email has a valid format. If not, set the
$signupError
variable to “Invalid Email format”. - The full-name and username fields should be below 20 characters. If this is not true, set the
$signupError
variable to “Name must be between 6-20 characters”. - If the length of the password is below 5 characters, set
$signupError
to “Password too short”. - Create user account
- Check from the database if a user with the given email or username already exists. If a row is returned, set the
$signupError
variable to “Email already registered” or “Username already exists” respectively. - If all the above checks are completed and all the inputs are correct, create a new record with the given details and set the session
user_id
variable to theuser_id
of the new user and redirect the user tohome.php
.
Hint 1:
Create new methods checkEmail()
and checkUsername()
inside the User
class which will check if a user with the given email or username already exists and access them as $getFromU->checkEmail()
and $getFromU->checkUsername()
in login.php
.
Hint 2:
Use the $getFromU->create()
method to create the new record in the users table.
Use assets/images/defaultprofile1.png
as the default value for profileImage
of the user. In this project, we will not implement a way for the user to edit his profile image. If you want, you can change this value in the users
table directly from phpMyAdmin.
Checkpoint: Go ahead and refresh your browser and enter some details into the signup form. Try incorrect / empty inputs and see if the appropriate error messages are being displayed or not.
Once you click on Signup
, you should be redirected to home.php
and the new user_id
should be displayed. In this case, the output would be:
2
Also verify that the row created in your database table looks as you would expect it to be.
Solution to this section can be found in section Solution: Signup form below.
Amazing! You just finished implementing the created the login and signup flow!
Step 8: Home page
Now, let’s add some HTML to home.php
and a method to fetch all the user details from the table to display them on the home page.
Add a method in the User
class which will fetch all the user data for a given user_id
. Let’s call it userData()
:
public function userData($user_id) {
$stmt = $this->pdo->prepare("SELECT * FROM users WHERE user_id = :user_id");
$stmt->bindParam(":user_id", $user_id, PDO::PARAM_INT);
$stmt->execute();
return $stmt->fetch(PDO::FETCH_OBJ);
}
Open the project-resources
folder and copy-paste the HTML from home_1.html
below the PHP tags in home.php
.
Replace the echo
in home.php
with the following lines to call the userData()
method:
<!-- home.php -->
<?php
include 'core/init.php';
$user_id = $_SESSION['user_id'];
$user = $getFromU->userData($user_id);
?>
Checkpoint: Now save the file and try to login with valid email and password. You will see:
This is how the home page looks like after logging-in!
Step 9: Log-out
So you have implemented everything required for someone to log-in, but what about log-out? Let’s do it!
Create a logout()
method in the User
class. What all does it need to do?
Answer:
All you have to do in the logout()
method is destroy the session and redirect the user to index.php
!
In home.php
, on the top right corner, you will see a profile icon. Click on it and you will see 2 options: the first will take you to your profile page (we haven’t done this yet!) and the second one says logout.
On clicking the logout button, you will be redirected to logout.php
and this file should call the logout method. Create logout.php
inside the includes
folder
Checkpoint: Now save and try to logout from home.php
. You should be redirected to the index page as expected.
Solution to this section can be found in section Solution: Log-out below.
Step 10: Restricting access to index.php and home.php
Final step for part 1! Phew!!
In this part, you will write the code restrict access to index.php and home.php pages. A user should be able to access index.php
only if he / she is logged-out, and a user should be able to access home.php
only if he / she is logged-in.
If a user tries to access index.php
from the URL (by typing in http://localhost/index.php
in the browser) and user has already logged in, we want to redirect the user to home.php
.
This can be done by adding the following lines in index.php
:
<!-- index.php -->
<?php
include 'core/init.php';
if (isset($_SESSION['user_id'])) {
header('Location: home.php');
}
?>
<!-- html goes here -->
And lastly, you need to restrict a user who tries to directly access the home.php
page (by typing in http://localhost/home.php
in the browser) without logging in! Currently, this will throw a lot of errors as the session user_id
won’t be set and we don’t want this to happen, do we?
To do this, create a method in the User
class called loggedIn()
which checks if the session variable user_id
is set or not.
public function loggedIn() {
if (isset($_SESSION['user_id'])) {
return true;
}
return false;
}
Then, call this function in home.php
. Like so:
<!-- home.php -->
<?php
include 'core/init.php';
$user_id = $_SESSION['user_id'];
$user = $getFromU->userData($user_id);
if ($getFromU->loggedIn() === false) {
header('Location: index.php');
}
?>
Checkpoint: Great! Now if you try to access home.php
from the URL without logging in, you will be redirected to the index page. Similarly, if you are logged-in and you try to access index.php
, you will be redirected to home.php
.
Summary
In this part of the project, you created the index page with login and signup forms and the home page which is the user feed. You also added some HTML and CSS and your website is starting to look better!
If you lost track, got stuck, or just need to double check, the solution for each of the above steps is included below. You can also find all the code we have written so far here (that is, what your current code should look like).
In the next part of the project, you will work on the user profile and the tweet feature in the user feed page. Aren’t you excited?!
Solution: Database Connection
connection.php
:
<!-- connection.php -->
<?php
$dsn = 'mysql:host=localhost; dbname=twitter'; // database name
$user = 'root';
$pass = '';
try {
$pdo = new PDO($dsn, $user, $pass);
} catch(PDOException $e) {
echo 'Connection error! '. $e->getMessage();
}
?>
index.php
:
<!-- index.php -->
<?php
include 'core/database/connection.php';
?>
Solution: Classes and init
base.php
:
<!-- base.php -->
<?php
class Base {
protected $pdo;
function __construct($pdo) {
$this->pdo = $pdo;
}
public function create($table, $fields = array()) {
$columns = implode(',', array_keys($fields));
$values = ':' . implode(', :', array_keys($fields));
$sql = "INSERT INTO {$table} ({$columns}) VALUES ({$values})";
if ($stmt = $this->pdo->prepare($sql)) {
foreach ($fields as $key => $data) {
$stmt->bindValue(':'.$key, $data);
}
$stmt->execute();
return $this->pdo->lastInsertId();
}
}
public function update($table, $user_id, $fields = array()) {
$columns = '';
$i = 1;
foreach ($fields as $name => $value) {
$columns .= "{$name} = :{$name}";
if ($i < count($fields)) {
$columns .= ", ";
}
$i++;
}
$sql = "UPDATE {$table} SET {$columns} WHERE user_id = {$user_id}";
if ($stmt = $this->pdo->prepare("$sql")) {
foreach ($fields as $key => $value) {
$stmt->bindValue(':' . $key, $value);
}
$stmt->execute();
}
}
public function delete($table, $array) {
$sql = "DELETE FROM {$table}";
$where = " WHERE";
foreach ($array as $name => $value) {
$sql .= "{$where} {$name} = :{$name}";
$where = " AND ";
}
if ($stmt = $this->pdo->prepare($sql)) {
foreach ($array as $name => $value) {
$stmt->bindValue(':'.$name, $value);
}
}
$stmt->execute();
}
}
?>
user.php
:
<!-- user.php -->
<?php
class User extends Base {
function __construct($pdo) {
$this->pdo = $pdo;
}
}
?>
tweet.php
:
<!-- tweet.php -->
<?php
class Tweet extends Base {
function __construct($pdo) {
$this->pdo = $pdo;
}
}
?>
follow.php
:
<!-- follow.php -->
<?php
class Follow extends Base {
function __construct($pdo) {
$this->pdo = $pdo;
}
}
?>
Solution: User Table
Here is how your rows should look like when creating the table:
Make sure A_I
for user_id
is checked and it is the PRIMARY
key in the table.
Solution: Login form
This is what user.php looks like after adding checkInput($var)
and login($email, $password)
:
<!-- user.php -->
<?php
class User extends Base {
function __construct($pdo) {
$this->pdo = $pdo;
}
public function checkInput($var) {
$var = htmlspecialchars($var);
$var = trim($var);
$var = stripslashes($var);
return $var;
}
public function login($email, $password) {
$stmt = $this->pdo->prepare("SELECT user_id FROM users WHERE email = :email AND password = :password");
$stmt->bindParam(":email", $email, PDO::PARAM_STR);
$hash = md5($password);
$stmt->bindParam(":password", $hash, PDO::PARAM_STR);
$stmt->execute();
$user = $stmt->fetch(PDO::FETCH_OBJ);
$count = $stmt->rowCount();
if ($count > 0) {
$_SESSION['user_id'] = $user->user_id;
header('Location: home.php');
} else {
return false;
}
}
}
?>
The login()
function queries the database for the email and password. If a row is returned, the session variable is set and the user is redirected to home.php
. Otherwise, login()
returns false
.
login.php
after adding form validation:
<!-- login.php -->
<?php
if (isset($_POST['login']) && !empty($_POST['login'])) {
$email = $_POST['email'];
$password = $_POST['password'];
if(!empty($email) || !empty($password)) {
$email = $getFromU->checkInput($email);
$password = $getFromU->checkInput($password);
if(!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$error = "Invalid email format";
} else {
if($getFromU->login($email, $password) === false) {
$error = "The email or password is incorrect";
}
}
} else {
$error = "Please enter email and password!";
}
}
?>
<!-- html goes here -->
Finally, we have twitter/home.php
file which echo
s the user_id
:
<!-- home.php -->
<?php
include 'core/init.php';
echo $_SESSION['user_id'];
?>
Solution: Sign-up form
New methods in the User
class:
public function checkEmail($email) {
$stmt = $this->pdo->prepare("SELECT email FROM users WHERE email = :email");
$stmt->bindParam(":email", $email, PDO::PARAM_STR);
$stmt->execute();
$count = $stmt->rowCount();
if ($count > 0) {
return true;
} else {
return false;
}
}
public function checkUsername($username) {
$stmt = $this->pdo->prepare("SELECT username FROM users WHERE username = :username");
$stmt->bindParam(":username", $username, PDO::PARAM_STR);
$stmt->execute();
$count = $stmt->rowCount();
if ($count > 0) {
return true;
} else {
return false;
}
}
Form validation in signup-form.php
:
<!-- signup-form.php -->
<?php
if (isset($_POST['signup'])) {
$fullname = $_POST['fullname'];
$username = $_POST['username'];
$password = $_POST['password'];
$email = $_POST['email'];
$signupError = "";
if(empty($fullname) || empty($username) || empty($password) || empty($email)) {
$signupError = 'All feilds are required';
} else {
$email = $getFromU->checkInput($email);
$fullname = $getFromU->checkInput($fullname);
$username = $getFromU->checkInput($username);
$password = $getFromU->checkInput($password);
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$signupError = "Invalid email";
} elseif (strlen($fullname) > 20) {
$signupError = "Name must be between 6-20 characters";
} elseif (strlen($username) > 20) {
$signupError = "Username must be between 4-20 characters";
} elseif (strlen($password) < 5) {
$signupError = "Password too short";
} else {
if ($getFromU->checkEmail($email) === true) {
$signupError = "Email already registered";
} elseif ($getFromU->checkUsername($username) === true) {
$signupError = "Username already exists";
}
else {
$user_id = $getFromU->create('users', array('email' => $email,'password' => md5($password), 'fullname' => $fullname, 'username' => $username, 'profileImage' =>'assets/images/defaultprofile1.png'));
$_SESSION['user_id'] = $user_id;
header('Location: home.php');
}
}
}
}
?>
<!-- html goes here -->
Solution: Log-out
logout()
method in User
class:
public function logout() {
session_destroy();
header('Location: '. BASE_URL .'index.php');
}
includes/logout.php
:
<!-- logout.php -->
<?php
include '../core/init.php';
$getFromU->logout();
?>