CommonLounge Archive

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:

  1. 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.
  2. In the second part, you will create the user feed and the user profile page.
  3. 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 it twitter.

Now create 3 new folders inside the twitter folder:

  1. assets: for images, CSS files and javascript code. Create three folders inside the assets folder: css, images, js.
  2. core: for the core files of the project. Create two folders inside the core folder: classes and database.
  3. 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 the user_id of the logged in user and redirect the user to home.php. Create home.php, which includes core/init.php and echos the session user_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 the user_id of the new user and redirect the user to home.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 echos 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();
?>

© 2016-2022. All rights reserved.