0% found this document useful (0 votes)
146 views

Ribbit Instagram Clone em PHP

Uploaded by

Guilherme Ayin
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
146 views

Ribbit Instagram Clone em PHP

Uploaded by

Guilherme Ayin
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 33

For the unfamiliar, MVC stands for Model-View-Controller.

You can thing of MVC as


Database-HTML-Logic Code. Separating your code into these distinct parts makes it
easier to replace one or more of the components without interfering with the rest of
your app. As you will see below, this level of abstraction also encourages you to write
small, concise functions that rely on lower-level functions.

I like to start with the Model when building this type of application--everything tends
Object 2

to connect to it (I.E. signup, posts, etc). Let's setup the database.

The Database
We require four tables for this application. They are:

• Users - holds the user's info.

• Ribbits - contains the actual ribbits (posts).

• Follows - the list of who follows who.

• UserAuth - the table for holding the login authentications

I'll show you how to create these tables from the terminal. If you use an admin
program (such as phpMyAdmin), then you can either click the SQL button to directly
enter the commands or add the tables through the GUI.

To start, open up a terminal window, and enter the following command:

mysql -u username -h hostAddress -P portNumber


1
-p
If you are running this command on a MySQL machine, and the port number was not
modified, you may omit the -h
and -P arguments. The command defaults to localhost and port 3306, respectively.
Once you login, you can create the database using the following SQL:

1CREATE DATABASE Ribbit;


2USE Ribbit;
Let's begin by creating the Users table:

CREATE TABLE Users (


01 id INT NOT NULL
02AUTO_INCREMENT,
03 username VARCHAR(18) NOT NULL,
04 name VARCHAR(36),
05 password VARCHAR(64),
06
created_at DATETIME,
07
08 email TEXT,
09 gravatar_hash VARCHAR(32),
10 PRIMARY KEY(id, username)
);
This gives us the following table:

Users Table

The next table I want to create is the Ribbits table. This table should have four
fields: id , user_id , ribbit and created_at . The SQL code for this table is:

CREATE TABLE Ribbits (


1 id INT NOT NULL
2AUTO_INCREMENT,
3 user_id INT NOT NULL,
4
ribbit VARCHAR(140),
5
created_at DATETIME,
6
7 PRIMARY KEY(id, user_id)
);
Ribbits Table

This is fairly simple stuff, so I won't elaborate too much.

Next, the Follows table. This simply holds the id s of both the follower and followee:

CREATE Table Follows (


1 id INT NOT NULL
2AUTO_INCREMENT,
3 user_id INT NOT NULL,
4
followee_id INT,
5
6 PRIMARY KEY(id, user_id)
);

Follows Table

Finally, we have a table, called UserAuth . This holds the user's username and
password hash. I opted not to use the user's ID, because the program already stores
the username, when logging in and signing up (the two times when entries are added
to this table), but the program would need to make an extra call to get the user's ID
number. Extra calls mean more latency, so I chose not to use the user's ID.

In a real world project, you may want to add another field like 'hash2' or 'secret'. If all
you need to authenticate a user is one hash, then an attacker only has to guess that
one hash. For example: I randomly enter characters into the hash field in the cookie. If
there are enough users, it might just match someone. But if you have to guess and
match two hashes, then the chance of someone guessing the correct pair drops
exponentially (the same applies to adding three, etc). But to keep things simple, I will
only have one hash.
Here's the SQL code:

CREATE TABLE UserAuth (


1 id INT NOT NULL
2AUTO_INCREMENT,
3 hash VARCHAR(52) NOT NULL,
4
username VARCHAR(18),
5
6 PRIMARY KEY(id, hash)
);
And this final table looks like the following image:

UserAuth Table

Now that we have all the tables setup, you should have a pretty good idea of how the
overall site will work. We can start writing the Model class in our MVC framework.

The Model
Create a file, called model.php and enter the following class declaration:

1class Model{
2
3 private $db; // Holds mysqli Variable
4
5 function __construct(){
6 $this->db = new mysqli('localhost', 'user', 'pass', 'Ribbit');
7 }
8}

This looks familiar to you if you have written PHP classes in the past. This code
basically creates a class called Model . It has one private property named $db which
holds a mysqli object. Inside the constructor, I initialized the $db property using the
connection info to my database. The parameter order is: address, username,
password and database name.
Before we get into any page-specific code, I want
to create a few low-level commands that abstract
the common mySQL functions
like SELECT and INSERT .

The first function I want to implement is select() . It accepts a string for the table's
name and an array of properties for building the WHERE clause. Here is the entire
function, and it should go right after the constructor:

//--- private function for performing standard


01SELECTs
02private function select($table, $arr){
03 $query = "SELECT * FROM " . $table;
04 $pref = " WHERE ";
05 foreach($arr as $key => $value)
06 {
07
$query .= $pref . $key . "='" . $value . "'";
08
$pref = " AND ";
09
10 }
11 $query .= ";";
12 return $this->db->query($query);
}
The function builds a query string using the table's name and the array of properties.
It then returns a result object which we get by passing the query string
through mysqli 's query() function. The next two functions are very similar; they are
the insert() function and the delete() function:
//--- private function for performing standard INSERTs
private function insert($table, $arr)
01{
02 $query = "INSERT INTO " . $table . " (";
03 $pref = "";
04
foreach($arr as $key => $value)
05
06 {
07 $query .= $pref . $key;
08 $pref = ", ";
09 }
10 $query .= ") VALUES (";
11 $pref = "";
12 foreach($arr as $key => $value)
13 {
14 $query .= $pref . "'" . $value . "'";
15 $pref = ", ";
16
}
17
18 $query .= ");";
19 return $this->db->query($query);
20}
21
22//--- private function for performing standard
23DELETEs
24private function delete($table, $arr){
25 $query = "DELETE FROM " . $table;
26 $pref = " WHERE ";
27 foreach($arr as $key => $value)
28 {
29 $query .= $pref . $key . "='" . $value . "'";
30 $pref = " AND ";
31
}
32
$query .= ";";
33
return $this->db->query($query);
}
As you may have guessed, both functions generate a SQL query and return a result. I
want to add one more helper function: the exists() function. This will simply check if a
row exists in a specified table. Here is the function:

1//--- private function for checking if a row exists


2private function exists($table, $arr){
3 $res = $this->select($table, $arr);
4 return ($res->num_rows > 0) ? true : false;
5}
Before we make the more page-specific functions, we should probably make the
actual pages. Save this file and we'll start on URL routing.
The Router
In a MVC framework, all HTTP requests usually go to a single controller, and the
controller determines which function to execute based on the requested URL. We are
going to do this with a class called Router . It will accept a string (the requested page)
and will return the name of the function that the controller should execute. You can
think of it as a phone book for function names instead of numbers.

Here is the completed class's structure; just save this to a file called router.php :

class Router{
01 private $routes;
02
03 function __construct(){
04
$this->routes = array();
05
06 }
07
08 public function lookup($query)
09 {
10 if(array_key_exists($query, $this->routes))
11 {
12 return $this->routes[$query];
13 }
14 else
15
{
16
17 return false;
18 }
19 }
}
This class has one private property called routes , which is the "phone book" for our
controllers. There's also a simple function called lookup() , which returns a string if the
path exists in the routes property. To save time, I will list the ten functions that our
controller will have:

01function __construct(){
02 $this->routes = array(
03 "home" => "indexPage",
04 "signup" => "signUp",
05 "login" => "login",
06 "buddies" => "buddies",
07
"ribbit" => "newRibbit",
08
"logout" => "logout",
09
10 "public" => "publicPage",
11 "profiles" => "profiles",
12 "unfollow" => "unfollow",
13 "follow" => "follow"
14 );
}
The list goes by the format of 'url' => 'function name' . For example, if someone goes
to ribbit.com/home , then the router tells the controller to execute
the indexPage() function.

The router is only half the solution; we need to tell Apache to redirect all traffic to the
controller. We'll achieve this by creating a file called .htaccess in the root directory of
the site and adding the following to the file:

1RewriteEngine On
2RewriteRule ^/?Resource/(.*)$ /$1 [L]
3RewriteRule ^$ /home [redirect]
4RewriteRule ^([a-zA-Z]+)/?([a-zA-Z0-9/]*)$ /app.php?page=$1&query=$2 [L]
This may seem a little intimidating if you've never used apache's mod_rewrite. But
don't worry; I'll walk you through it line by line.

In a MVC framework, all HTTP requests usually go


to a single controller.

The first line tells Apache to enable mod_rewrite; the remaining lines are the rewrite
rules. With mod_rewrite, you can take an incoming request with a certain URL and
pass the request onto a different file. In our case, we want all requests to be handled
by a single file so that we can process them with the controller. The mod_rewrite
module also lets us have URLs like ribbit.com/profile/username instead
of ribbit.com/profile.php?username=username --making the overall feel of your app more
professional.

I said, we want all requests to go to a single file, but that's really not accurate. We
want Apache to normally handle requests for resources like images, CSS files, etc.
The first rewrite rule tells Apache to handle requests that start with Resource/ in a
regular fashion. It's a regular expression that takes everything after the
word Resource/ (notice the grouping brackets) and uses it as the real URL to the file.
So for example: the link ribbit.com/Resource/css/main.css loads the file located
at ribbit.com/css/main.css .

The next rule tells Apache to redirect blank


requests (i.e. a request to the websites root)
to /home .
The word "redirect" in the square brackets at the end of the line tells Apache to
actually redirect the browser, as opposed rewriting on URL to another (like in the
previous rule).

There are different kinds of flashes: error,


warning and notice.

The last rule is the one we came for; it takes all requests (other than those that start
with Resource/ ) and sends them to a PHP file called app.php . That is the file that
loads the controller and runs the whole application.

The " ^ " symbol represents the beginning of the string and the " $ " represents the
end. So the regular expression can be translated into English as: "Take everything
from the beginning of the URL until the first slash, and put it in group 1. Then take
everything after the slash, and put it in group 2. Finally, pass the link to Apache as if it
said app.php?page=group1&query=group2 ." The " [L] " that is in the first and third line
tells Apache to stop after that line. So if the request is a resource URL, it shouldn't
continue to the next rule; it should break after the first one.

I hope all that made sense; the following picture better illustrates what's going on.

If you are still unclear on the actual regular expression, then we have a very nice
article that you can read.

Now that we have everything setup URL-wise, let's create the controller.
The Controller
The controller is where most of the magic happens; all the other pieces of the app,
including the model and router, connect through here. Let's begin by creating a file
called controller.php and enter in the following:

require("model.php");
require("router.php");
01
02class Controller{
03
04
private $model;
05
06 private $router;
07
08 //Constructor
09 function __construct(){
10 //initialize private variables
11 $this->model = new Model();
12 $this->router = new Router();
13
14
//Proccess Query String
15
16 $queryParams = false;
17 if(strlen($_GET['query']) > 0)
18 {
19 $queryParams = explode("/", $_GET['query']);
20 }
21
22 $page = $_GET['page'];
23
24
25 //Handle Page Load
26 $endpoint = $this->router->lookup($page);
27 if($endpoint === false)
28 {
29 header("HTTP/1.0 404 Not Found");
30 }
31 else
32 {
33 $this->$endpoint($queryParams);
34
35
}
}
With mod_rewrite, you can take an incoming request with a
certain URL and pass the request onto a different file.

We first load our model and router files, and we then create a class called Controller .

It has two private variables: one for the model and one for the router. Inside the
constructor, we initialize these variables and process the query string.
If you remember, the query can contain multiple values (we wrote in the .htaccess file
that everything after the first slash gets put in the query--this includes all slashes that
may follow). So we split the query string by slashes, allowing us to pass multiple
query parameters if needed.

Next, we pass whatever was in the $page variable to the router to determine the
function to execute. If the router returns a string, then we will call the specified
function and pass it the query parameters. If the router returns false , the controller
sends the 404 status code. You can redirect the page to a custom 404 view if you so
desire, but I'll keep things simple.

The framework is starting to take shape; you can now call a specific function based
on a URL. The next step is to add a few functions to the controller class to take care
of the lower-level tasks, such as loading a view and redirecting the page.

The first function simply redirects the browser to a different page. We do this a lot, so
it's a good idea to make a function for it:

1private function redirect($url){


2 header("Location: /" . $url);
3}
The next two functions load a view and a page, respectively:

private function loadView($view, $data = null){


01 if (is_array($data))
02 {
03 extract($data);
04 }
05
06 require("Views/" . $view . ".php");
07}
08
09
private function loadPage($user, $view, $data = null, $flash = false)
10
{
11
$this->loadView("header", array('User' => $user));
12
if ($flash !== false)
13
14 {
15 $flash->display();
16 }
17
18 $this->loadView($view, $data);
19 $this->loadView("footer");
}
The first function loads a single view from the "Views" folder, optionally extracting the
variables from the attached array. The second function is the one we will reference,
and it loads the header and footer (they are the same on all pages around the
specified view for that page) and any other messages (flash i.e. an error message,
greetings, etc).

There is one last function that we need to implement which is required on all pages:
the checkAuth() function. This function will check if a user is signed in, and if so, pass
the user's data to the page. Otherwise, it returns false. Here is the function:

01private function checkAuth(){


02 if(isset($_COOKIE['Auth']))
03 {
04 return $this->model->userForAuth($_COOKIE['Auth']);
05 }
06 else
07 {
08 return false;
09 }
10}

We first check whether or not the Auth cookie is set. This is where the hash we talked
about earlier will be placed. If the cookie exists, then the function tries to verify it with
the database, returning either the user on a successful match or false if it's not in the
table.

Now let's implement that function in the model class.

A Few Odds and Ends


In the Model class, right after the exists() function, add the following function:
public function userForAuth($hash){
01 $query = "SELECT Users.* FROM Users JOIN (SELECT username FROM UserAuth WHERE
02hash = '";
03 $query .= $hash . "' LIMIT 1) AS UA WHERE Users.username = UA.username LIMIT 1";
04 $res = $this->db->query($query);
05 if($res->num_rows > 0)
06 {
07
return $res->fetch_object();
08
}
09
10 else
11 {
12 return false;
13 }
}
If you remember our tables, we have a UserAuth table that contains the hash along
with a username. This SQL query retrieves the row that contains the hash from the
cookie and returns the user with the matching username.

That's all we have to do in this class for now. Let's go back into the controller.php file
and implement the Flash class.

In the loadPage() function, there was an option to


pass a flash object, a message that appears
above all the content.

For example: if an unauthenticated user tries to post something, the app displays a
message similar to, "You have to be signed in to perform that action." There are
different kinds of flashes: error, warning and notice, and I decided it is easier to create
a Flash class instead of passing multiple variables (like msg and type . Additionally,
the class will have the ability to output a flash's HTML.

Here is the complete Flash class, you can add this to controller.php before
the Controller class definition:
class Flash{
01
02 public $msg;
03 public $type;
04
05 function __construct($msg, $type)
06 {
07
$this->msg = $msg;
08
09 $this->type = $type;
10 }
11
12 public function display(){
13 echo "<div class=\"flash " . $this->type . "\">" . $this->msg .
14"</div>";
15 }
}
This class is straight-forward. It has two properties and a function to output the
flash's HTML.

We now have all the pieces needed to start displaying pages, so let's create
the app.php file. Create the file and insert the following code:

1<?php
2 require("controller.php");
3 $app = new Controller();
And that's it! The controller reads the request from the GET variable, passes it to the
router, and calls the appropriate function. Let's create some of the views to finally get
something displayed in the browser.

The Views
Create a folder in the root of your site called Views . As you may have already
guessed, this directory will contains all the actual views. If you are unfamiliar with the
concept of a view, you can think of them as files that generate pieces of HTML that
build the page. Basically, we'll have a view for the header, footer and one for each
page. These pieces combine into the final result (i.e. header + page_view + footer =
final_page).

Let's start with the footer; it is just standard HTML. Create a file
called footer.php inside the Views folder and add the following HTML:
</div>
1 </div>
2 <footer>
3 <div class="wrapper">
4 Ribbit - A Twitter Clone Tutorial<img
5src="http://cdn.tutsplus.com/net.tutsplus.com/authors/jeremymcpeak//Resource/gfx/logo-
6nettuts.png">
7 </div>
8 </footer>
9</body>
</html>
I think this demonstrates two things very well:

• These are simply pieces of an actual page.

• To access the images that are in the gfx folder, I added Resources/ to the
beginning of the path (for the mod_rewrite rule).

Next, let's create the header.php file. The header is a bit more complicated because it
must determine if the user is signed in. If the user is logged in, it displays the menu
bar; otherwise, it displays a login form. Here is the complete header.php file:

01<!DOCTYPE html>
02<html>
03 <head>
04 <link rel="stylesheet/less" href="/Resource/style.less">
05 <script src="/Resource/less.js"></script>
06 </head>
07 <body>
08
<header>
09
<div class="wrapper">
10
11 <img
12src="http://cdn.tutsplus.com/net.tutsplus.com/authors/jeremymcpeak//Resource/gfx/logo.png"
13>
14 <span>Twitter Clone</span>
15 <?php if($User !== false){ ?>
16 <nav>
17 <a href="/buddies">Your Buddies</a>
18 <a href="/public">Public Ribbits</a>
19 <a href="/profiles">Profiles</a>
20 </nav>
21 <form action="/logout" method="get">
22
<input type="submit" id="btnLogOut" value="Log Out">
23
</form>
24
25 <?php }else{ ?>
26 <form method="post" action="/login">
27 <input name="username" type="text"
28placeholder="username">
29 <input name="password" type="password"
placeholder="password">
<input type="submit" id="btnLogIn" value="Log In">
</form>
30 <?php } ?>
31 </div>
</header>
<div id="content">
<div class="wrapper">
I'm not going to explain much of the HTML. Overall, this view loads in the CSS style
sheet and builds the correct header based on the user's authentication status. This is
accomplished with a simple if statement and the variable passed from the controller.

The last view for the homepage is the actual home.php view. This view contains the
greeting picture and signup form. Here is the code for home.php :

<img
src="http://cdn.tutsplus.com/net.tutsplus.com/authors/jeremymcpeak//Resource/gfx/frog.jpg"
01
>
02
<div class="panel right">
03
<h1>New to Ribbit?</h1>
04
05 <p>
06 <form action="/signup" method="post">
07 <input name="email" type="text" placeholder="Email">
08 <input name="username" type="text" placeholder="Username">
09 <input name="name" type="text" placeholder="Full Name">
10 <input name="password" type="password" placeholder="Password">
11 <input name="password2" type="password" placeholder="Confirm Password">
12 <input type="submit" value="Create Account">
13 </form>
14 </p>
</div>
Together, these three views complete the homepage. Now let's go write the function
for the home page.

The Home Page


We need to write a function in the Controller class called indexPage() to load the home
page (this is what we set up in the router class). The following complete function
should go in the Controller class after the checkAuth() function:
private function indexPage($params){
$user = $this->checkAuth();
01
02 if($user !== false) { $this->redirect("buddies"); }
03 else
04 {
05 $flash = false;
06 if($params !== false)
07 {
08 $flashArr = array(
09 "0" => new Flash("Your Username and/or Password was incorrect.", "error"),
10 "1" => new Flash("There's already a user with that email address.", "error"),
11
"2" => new Flash("That username has already been taken.", "error"),
12
"3" => new Flash("Passwords don't match.", "error"),
13
14 "4" => new Flash("Your Password must be at least 6 characters long.",
15"error"),
16 "5" => new Flash("You must enter a valid Email address.", "error"),
17 "6" => new Flash("You must enter a username.", "error"),
18 "7" => new Flash("You have to be signed in to acces that page.", "warning")
19 );
20 $flash = $flashArr[$params[0]];
21 }
22 $this->loadPage($user, "home", array(), $flash);
23 }
}
The first two lines check if the user is already signed in. If so, the function redirects
the user to the "buddies" page where they can read their friends' posts and view their
profile. If the user is not signed in, then it continues to load the home page, checking
if there are any flashes to display. So for instance, if the user goes
to ribbit.com/home/0 , then it this function shows the first error and so on for the next
seven flashes. Afterwards, we call the loadPage() function to display everything on the
screen.

At this point if you have everything setup correctly (i.e. Apache and our code so far),
then you should be able to go to the root of your site (e.g. localhost) and see the
home page.
Congratulations!! It's smooth sailing from here on out... well at least smoother sailing.
It's just a matter of repeating the previous steps for the other nine functions that we
defined in the router.

Rinse and Repeat


The next logical step is to create the signup function, you can add this right after
the indexPage() :
private function signUp(){
if($_POST['email'] == "" || strpos($_POST['email'], "@") === false)
01{
02 $this->redirect("home/5");
03 }
04 else if($_POST['username'] == ""){
05 $this->redirect("home/6");
06 }
07 else if(strlen($_POST['password']) < 6)
08 {
09
$this->redirect("home/4");
10
11 }
12 else if($_POST['password'] != $_POST['password2'])
13 {
14 $this->redirect("home/3");
15 }
16 else{
17 $pass = hash('sha256', $_POST['password']);
18
19 $signupInfo = array(
20
'username' => $_POST['username'],
21
22 'email' => $_POST['email'],
23 'password' => $pass,
24 'name' => $_POST['name']
25 );
26
27 $resp = $this->model->signupUser($signupInfo);
28
29
if($resp === true)
30
{
31
32 $this->redirect("buddies/1");
33 }
34 else
35 {
36 $this->redirect("home/" . $resp);
37 }
}
}
This function goes through a standard signup process by making sure everything
checks out. If any of the user's info doesn't pass, the function redirects the user back
to the home page with the appropriate error code for the indexPage() function to
display.

The checks for existing usernames and


passwords cannot be performed here.

Those checks need to happen in the Model class because we need a connection to
the database. Let's go back to the Model class and implement
the signupUser() function. You should put this right after the userForAuth() function:
public function signupUser($user){
$emailCheck = $this->exists("Users", array("email" => $user['email']));
01
02
03 if($emailCheck){
04 return 1;
05 }
06 else {
07 $userCheck = $this->exists("Users", array("username" =>
08$user['username']));
09
10
if($userCheck){
11
return 2;
12
13 }
14 else{
15 $user['created_at'] = date( 'Y-m-d H:i:s');
16 $user['gravatar_hash'] = md5(strtolower(trim($user['email'])));
17 $this->insert("Users", $user);
18 $this->authorizeUser($user);
19 return true;
20 }
21 }
}
We use our exists() function to check the provided email or username, returning an
error code either already exists. If everything passes, then we add the final few
fields, created_at and gravatar_hash , and insert them into the database.

Before returning true , we authorize the user. This function adds the Auth cookie and
inserts the credentials into the UserAuth database. Let's add
the authorizeUser() function now:

01public function authorizeUser($user){


02 $chars = "qazwsxedcrfvtgbyhnujmikolp1234567890QAZWSXEDCRFVTGBYHNUJMIKOLP";
03 $hash = sha1($user['username']);
04 for($i = 0; $i<12; $i++)
05 {
06 $hash .= $chars[rand(0, 61)];
07 }
08 $this->insert("UserAuth", array("hash" => $hash, "username" => $user['username']));
09 setcookie("Auth", $hash);
10}

This function builds the unique hash for a user on sign up and login. This isn't a very
secure method of generating hashes, but I combine the sha1 hash of the username
along with twelve random alphanumeric characters to keep things simple.

It's good to attach some of the user's info to the


hash because it helps make the hashes unique to
that user.
There is a finite set of unique character combinations, and you'll eventually have two
users with the same hash. But if you add the user's ID to the hash, then you are
guaranteed a unique hash for every user.

Login and Logout


To finish the functions for the home page, let's implement
the login() and logout() functions. Add the following to the Controller class after
the login() function:

private function login(){


01 $pass = hash('sha256', $_POST['password']);
02
$loginInfo = array(
03
04 'username' => $_POST['username'],
05 'password' => $pass
06 );
07 if($this->model->attemptLogin($loginInfo))
08 {
09 $this->redirect("buddies/0");
10 }
11 else
12 {
13 $this->redirect("home/0");
14
}
15
}
This simply takes the POST fields from the login form and attempts to login. On a
successful login, it takes the user to the "buddies" page. Otherwise, it redirects back
to the homepage to display the appropriate error. Next, I'll show you
the logout() function:

1private function logout() {


2 $this->model->logoutUser($_COOKIE['Auth']);
3 $this->redirect("home");
4}
The logout() function is even simpler than login() . It executes one of Model 's

functions to erase the cookie and remove the entry from the database.

Let's jump over to the Model class and add the necessary functions for these to
updates. The first is attemptLogin() which tries to login and returns true or false . Then

we have logoutUser() :
public function attemptLogin($userInfo){
01 if($this->exists("Users", $userInfo)){
02 $this->authorizeUser($userInfo);
03 return true;
04 }
05 else{
06
return false;
07
08 }
09}
10
11public function logoutUser($hash){
12 $this->delete("UserAuth", array("hash" =>
13$hash));
14 setcookie ("Auth", "", time() - 3600);
}

The Buddies Page

Hang with me; we are getting close to the end! Let's build the "Buddies" page. This
page contains your profile information and a list of posts from you and the people
you follow. Let's start with the actual view, so create a file called buddies.php in
the Views folder and insert the following:

01<div id="createRibbit" class="panel right">


02 <h1>Create a Ribbit</h1>
03 <p>
04 <form action="/ribbit" method="post">
05 <textarea name="text" class="ribbitText"></textarea>
06 <input type="submit" value="Ribbit!">
07
</form>
08
</p>
09
10</div>
11<div id="ribbits" class="panel left">
12 <h1>Your Ribbit Profile</h1>
13 <div class="ribbitWrapper">
14 <img class="avatar" src="http://www.gravatar.com/avatar/<?php echo $User-
15>gravatar_hash; ?>">
16 <span class="name"><?php echo $User->name; ?></span> @<?php echo
17$User->username; ?>
18 <p>
19 <?php echo $userData->ribbit_count . " "; echo ($userData->ribbit_count !=
201) ? "Ribbits" : "Ribbit"; ?>
21 <span class="spacing"><?php echo $userData->followers . " "; echo
22($userData->followers != 1) ? "Followers" : "Follower"; ?></span>
23 <span class="spacing"><?php echo $userData->following . " Following"; ?
24></span><br>
25 <?php echo $userData->ribbit; ?>
26
</p>
27
28 </div>
29</div>
30<div class="panel left">
<h1>Your Ribbit Buddies</h1>
<?php foreach($fribbits as $ribbit){ ?>
<div class="ribbitWrapper">
<img class="avatar" src="http://www.gravatar.com/avatar/<?php echo
$ribbit->gravatar_hash; ?>">
<span class="name"><?php echo $ribbit->name; ?></span> @<?php
31echo $ribbit->username; ?>
32 <span class="time">
33 <?php
34 $timeSince = time() - strtotime($ribbit->created_at);
35 if($timeSince < 60)
36
{
37
38 echo $timeSince . "s";
39 }
40 else if($timeSince < 3600)
41 {
42 echo floor($timeSince / 60) . "m";
43 }
44 else if($timeSince < 86400)
45 {
46 echo floor($timeSince / 3600) . "h";
47 }
48
else{
49
50 echo floor($timeSince / 86400) . "d";
51 }
52 ?>
</span>
<p><?php echo $ribbit->ribbit; ?></p>
</div>
<?php } ?>
</div>
The first div is the form for creating new "ribbits". The next div displays the user's
profile information, and the last section is the for loop that displays each "ribbit".
Again, I'm not going to go into to much detail for the sake of time, but everything here
is pretty straight forward.

Now, in the Controller class, we have to add the buddies() function:


private function buddies($params){
01 $user = $this->checkAuth();
02 if($user === false){ $this->redirect("home/7"); }
03 else
04 {
05 $userData = $this->model->getUserInfo($user);
06 $fribbits = $this->model->getFollowersRibbits($user);
07 $flash = false;
08 if(isset($params[0]))
09 {
10
$flashArr = array(
11
12 "0" => new Flash("Welcome Back, " . $user->name, "notice"),
13 "1" => new Flash("Welcome to Ribbit, Thanks for signing up.", "notice"),
14 "2" => new Flash("You have exceeded the 140 character limit for Ribbits", "error")
15 );
16 $flash = $flashArr[$params[0]];
17 }
18 $this->loadPage($user, "buddies", array('User' => $user, "userData" => $userData,
19"fribbits" => $fribbits), $flash);
20 }
}
This function follows the same structure as the indexPage() function: we first check if
the user is logged in and redirect them to the home page if not.

We then call two functions from the Model class:


one to get the user's profile information and one
to get the posts from the user's followers.

We have three possible flashes here: one for signup, one for login and one for if the
user exceeds the 140 character limit on a new ribbit. Finally, we call
the loadPage() function to display everything.

Now in the Model class we have to enter the two functions we called above. First we
have the 'getUserInfo' function:

public function getUserInfo($user)


{
$query = "SELECT ribbit_count, IF(ribbit IS NULL, 'You have no Ribbits', ribbit) as ribbit,
01followers, following ";
02 $query .= "FROM (SELECT COUNT(*) AS ribbit_count FROM Ribbits WHERE user_id = " .
03$user->id . ") AS RC ";
04 $query .= "LEFT JOIN (SELECT user_id, ribbit FROM Ribbits WHERE user_id = " . $user->id .
05
" ORDER BY created_at DESC LIMIT 1) AS R ";
06
$query .= "ON R.user_id = " . $user->id . " JOIN ( SELECT COUNT(*) AS followers FROM
07
Follows WHERE followee_id = " . $user->id;
08
$query .= ") AS FE JOIN (SELECT COUNT(*) AS following FROM Follows WHERE user_id = " .
09
10$user->id . ") AS FR;";
$res = $this->db->query($query);
return $res->fetch_object();
}
The function itself is simple. We execute a SQL query and return the result. The query,
on the other hand, may seem a bit complex. It combines the necessary information
for the profile section into a single row. The information returned by this query
includes the amount of ribbits you made, your latest ribbit, how many followers you
have and how many people you are following. This query basically combines one
normal SELECT query for each of these properties and then joins everything together.

Next we had the getFollowersRibbits() function which looks like this:

public function getFollowersRibbits($user)


{
01 $query = "SELECT name, username, gravatar_hash, ribbit, Ribbits.created_at FROM Ribbits
02JOIN (";
03 $query .= "SELECT Users.* FROM Users LEFT JOIN (SELECT followee_id FROM Follows
04WHERE user_id = ";
05 $query .= $user->id . " ) AS Follows ON followee_id = id WHERE followee_id = id OR id = " .
06$user->id;
07 $query .= ") AS Users on user_id = Users.id ORDER BY Ribbits.created_at DESC LIMIT 10;";
08 $res = $this->db->query($query);
09
$fribbits = array();
10
while($row = $res->fetch_object())
11
12 {
13 array_push($fribbits, $row);
14 }
return $fribbits;
}
Similar to the previous function, the only complicated part here is the query. We need
the following information to display for each post: name, username, gravatar image,
the actual ribbit, and the date when the ribbit was created. This query sorts through
your posts and the posts from the people you follow, and returns the latest ten ribbits
to display on the buddies page.

You should now be able to signup, login and view the buddies page. We are still not
able to create ribbits so let's get on that next.

Posting Your First Ribbit


This step is pretty easy. We don't have a view to work with; we just need a function in
the Controller and Model classes. In Controller , add the following function:
private function newRibbit($params){
01 $user = $this->checkAuth();
02
if($user === false){ $this->redirect("home/7"); }
03
else{
04
05 $text = mysql_real_escape_string($_POST['text']);
06 if(strlen($text) > 140)
07 {
08 $this->redirect("buddies/2");
09 }
10 else
11 {
12 $this->model->postRibbit($user, $text);
13 $this->redirect("buddies");
14
}
15
}
16
}
Again we start by checking if the user is logged in, and if so, we ensure the post is not
over the 140 character limit. We'll then call postRibbit() from the model and redirect
back to the buddies page.

Now in the Model class, add the postRibbit() function:

public function postRibbit($user, $text)


1{
2 $r = array(
3 "ribbit" => $text,
4 "created_at" => date( 'Y-m-d H:i:s'),
5
"user_id" => $user->id
6
7 );
8 $this->insert("Ribbits", $r);
}
We are back to standard queries with this one; just combine the data into an array
and insert it with our insert function. You should now be able to post Ribbits, so go try
to post a few. We still have a little more work to do, so come back after you post a
few ribbits.

The Last Two Pages


The next two pages have almost identical functions in the controller so I'm going to
post them together:

01private function publicPage($params){


02 $user = $this->checkAuth();
03 if($user === false){ $this->redirect("home/7"); }
else
04 {
05 $q = false;
06 if(isset($_POST['query']))
07 {
08 $q = $_POST['query'];
09 }
10 $ribbits = $this->model->getPublicRibbits($q);
11 $this->loadPage($user, "public", array('ribbits' => $ribbits));
12 }
13}
14
15
private function profiles($params){
16
$user = $this->checkAuth();
17
18 if($user === false){ $this->redirect("home/7"); }
19 else{
20 $q = false;
21 if(isset($_POST['query']))
22 {
23 $q = $_POST['query'];
24 }
25 $profiles = $this->model->getPublicProfiles($user, $q);
26 $this->loadPage($user, "profiles", array('profiles' =>
27
$profiles));
28
}
}
These functions both get an array of data; one gets ribbits and the other profiles. They
both allow you to search by a POST string option, and they both get the info from
the Model . Now let's go put their corresponding views in the Views folder.

For the ribbits just create a file called public.php and put the following inside:

01<div class="panel right">


02 <h1>Search Ribbits</h1>
03 <p>
04
</p><form action="/public" method="post">
05
<input name="query" type="text">
06
07 <input type="submit" value="Search!">
08 </form>
09 <p></p>
10</div>
11<div id="ribbits" class="panel left">
12 <h1>Public Ribbits</h1>
13 <?php foreach($ribbits as $ribbit){ ?>
14 <div class="ribbitWrapper">
15 <img class="avatar" src="http://www.gravatar.com/avatar/<?php echo
16$ribbit->gravatar_hash; ?>">
17 <span class="name"><?php echo $ribbit->name; ?></span> @<?php
18echo $ribbit->username; ?>
19 <span class="time">
20
<?php
21
$timeSince = time() - strtotime($ribbit->created_at);
22
23 if($timeSince < 60)
{
echo $timeSince . "s";
}
24 else if($timeSince < 3600)
25
{
26
27 echo floor($timeSince / 60) . "m";
28 }
29 else if($timeSince < 86400)
30 {
31 echo floor($timeSince / 3600) . "h";
32 }
33 else{
34 echo floor($timeSince / 86400) . "d";
35 }
36 ?>
37
</span>
38
39 <p><?php echo $ribbit->ribbit; ?></p>
</div>
<?php } ?>
</div>
The first div is the ribbit search form, and the second div displays the public ribbits.

And here is the last view which is the profiles.php view:

<div class="panel right">


<h1>Search for Profiles</h1>
<p>
01 </p><form action="/profiles" method="post">
02 <input name="query" type="text">
03
<input type="submit" value="Search!">
04
05 </form>
06 <p></p>
07</div>
08<div id="ribbits" class="panel left">
09 <h1>Public Profiles</h1>
10 <?php foreach($profiles as $user){ ?>
11 <div class="ribbitWrapper">
12 <img class="avatar" src="http://www.gravatar.com/avatar/<?php echo $user-
13>gravatar_hash; ?>">
14 <span class="name"><?php echo $user->name; ?></span> @<?php echo
15$user->username; ?>
16 <span class="time"><?php echo $user->followers; echo ($user->followers > 1) ?
17" followers " : " follower "; ?>
18 <a href="<?php echo ($user->followed) ? "unfollow" : "follow"; ?>/<?php
19echo $user->id; ?>"><?php echo ($user->followed) ? "unfollow" : "follow"; ?></a></span>
20 <p>
21
<?php echo $user->ribbit; ?>
22
23 </p>
</div>
<?php } ?>
</div>
This is very similar to the public.php view.
The last step needed to get these two pages working is to add their dependency
functions to the Model class. Let's start with the function to get the public ribbits. Add
the following to the Model class:

public function getPublicRibbits($q){


if($q === false)
01 {
02 $query = "SELECT name, username, gravatar_hash, ribbit, Ribbits.created_at FROM
03Ribbits JOIN Users ";
04 $query .= "ON user_id = Users.id ORDER BY Ribbits.created_at DESC LIMIT 10;";
05 }
06 else{
07 $query = "SELECT name, username, gravatar_hash, ribbit, Ribbits.created_at FROM
08Ribbits JOIN Users ";
09 $query .= "ON user_id = Users.id WHERE ribbit LIKE \"%" . $q ."%\" ORDER BY
10
Ribbits.created_at DESC LIMIT 10;";
11
}
12
13 $res = $this->db->query($query);
14 $ribbits = array();
15 while($row = $res->fetch_object())
16 {
17 array_push($ribbits, $row);
18 }
return $ribbits;
}
If a search query was passed, then we only select ribbits that match the provided
search. Otherwise, it just takes the ten newest ribbits. The next function is a bit more
complicated as we need to make multiple SQL queries. Enter this function to get the
public profiles:

01public function getPublicProfiles($user, $q){


02 if($q === false)
03 {
04 $query = "SELECT id, name, username, gravatar_hash FROM Users WHERE id != " .
05$user->id;
06 $query .= " ORDER BY created_at DESC LIMIT 10";
07 }
08 else{
09 $query = "SELECT id, name, username, gravatar_hash FROM Users WHERE id != " .
10$user->id;
11 $query .= " AND (name LIKE \"%" . $q . "%\" OR username LIKE \"%" . $q . "%\") ORDER
12
BY created_at DESC LIMIT 10";
13
}
14
$userRes = $this->db->query($query);
15
16 if($userRes->num_rows > 0){
17 $userArr = array();
18 $query = "";
19 while($row = $userRes->fetch_assoc()){
20 $i = $row['id'];
21 $query .= "SELECT " . $i . " AS id, followers, IF(ribbit IS NULL, 'This user has no
22ribbits.', ribbit) ";
23 $query .= "AS ribbit, followed FROM (SELECT COUNT(*) as followers FROM Follows
WHERE followee_id = " . $i . ") ";
$query .= "AS F LEFT JOIN (SELECT user_id, ribbit FROM Ribbits WHERE user_id = " .
$i;
$query .= " ORDER BY created_at DESC LIMIT 1) AS R ON R.user_id = " . $i . " JOIN
(SELECT COUNT(*) ";
24 $query .= "AS followed FROM Follows WHERE followee_id = " . $i . " AND user_id = " .
25$user->id . ") AS F2 LIMIT 1;";
26 $userArr[$i] = $row;
27 }
28 $this->db->multi_query($query);
29 $profiles = array();
30 do{
31 $row = $this->db->store_result()->fetch_object();
32 $i = $row->id;
33
$userArr[$i]['followers'] = $row->followers;
34
$userArr[$i]['followed'] = $row->followed;
35
36 $userArr[$i]['ribbit'] = $row->ribbit;
37 array_push($profiles, (object)$userArr[$i]);
38 }while($this->db->next_result());
39 return $profiles;
40 }
else
{
return null;
}
}
It's a lot to take in, so I'll go over it slowly. The first if...else statement checks whether
or not the user passed a search query and generates the appropriate SQL to retrieve
ten users. Then we make sure that the query returned some users, and if so, it moves
on to generate a second query for each user, retrieving there latest ribbit and info.

After that, we send all the queries to the


database with the multi_query command to
minimize unnecessary trips to the database.

Then, we take the results and combine them with the user's information from the first
query. All this data is returned to display in the profiles view.

If you have done everything correctly, you should be able to traverse through all the
pages and post ribbits. The only thing we have left to do is add the functions to follow
and unfollow other people.
Tying up the Loose Ends
There is no view associated with these functions, so these will be quick. Let's start
with the functions in the Controller class:
private function follow($params){
01 $user = $this->checkAuth();
02
if($user === false){ $this->redirect("home/7"); }
03
04 else{
05 $this->model->follow($user, $params[0]);
06 $this->redirect("profiles");
07 }
08}
09
10private function unfollow($params){
11 $user = $this->checkAuth();
12 if($user === false){ $this->redirect("home/7"); }
13 else{
14 $this->model->unfollow($user, $params[0]);
15
$this->redirect("profiles");
16
}
17
}
These functions, as you can probably see, are almost identical. The only difference is
that one adds a record to the Follows table and one removes a record. Now let's finish
it up with the functions in the Model class:

public function follow($user, $fId){


1 $this->insert("Follows", array("user_id" => $user->id, "followee_id" => $fId));
2
}
3
4
public function unfollow($user, $fId){
5
$this->delete("Follows", array("user_id" => $user->id, "followee_id" =>
6
7$fId));
}
These functions are basically the same; they only differ by the methods they call.

The site is now fully operational!!! The last thing which I want to add is
another .htaccess file inside the Views folder. Here are its contents:

1Order allow,deny
2Deny from all
This is not strictly necessary, but it is good to restrict access to private files.

Conclusion
We definitely built a Twitter clone from scratch!
This has been a very long article, but we covered a lot! We setup a database and
created our very own MVC framework. We definitely built a Twitter clone from
scratch!

Please note that, due to length restraints, I had to omit a lot of the features that you
might find in a real production application, such as Ajax, protection against SQL
injection, and a character counter for the Ribbit box (probably a lot of other things as
well). That said, overall, I think we accomplished a great deal!

I hope you enjoyed this article, feel free to leave me a comment if you have any
thoughts or questions. Thank you for reading!

Advertisement

Gabriel Manricks

I'm a freelance web developer with experience spanning the full stack of application
development and a senior writer here at NetTuts+. Besides for that I spend my time writing
books for Packt or working on open source projects I find intriguing . You can find me on Twitter
@gabrielmanricks or visit my site to see all the things I'm working on gabrielmanricks.com.

You might also like