In the initial entry in this series, we took care of the UI-aspect of our Twitter-clone, called Ribbit. Now, we’ll begin coding the application in a number of languages. This lesson will leverage standard PHP (with homegrown MVC), but, in future articles, we’ll review other implementations, such as with Rails or Laravel.
There is a lot to cover, so let’s get started.
Where We Last Left Off
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 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
To start, open up a terminal window, and enter the following command:
mysql -u username -h hostAddress -P portNumber -pIf 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:CREATE DATABASE Ribbit; USE Ribbit;Let’s begin by creating the
Users
table:CREATE TABLE Users ( id INT NOT NULL AUTO_INCREMENT, username VARCHAR(18) NOT NULL, name VARCHAR(36), password VARCHAR(64), created_at DATETIME, email TEXT, gravatar_hash VARCHAR(32), PRIMARY KEY(id, username) );This gives us the following table:
Users Table
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 ( id INT NOT NULL AUTO_INCREMENT, user_id INT NOT NULL, ribbit VARCHAR(140), created_at DATETIME, PRIMARY KEY(id, user_id) );
Ribbits Table
Next, the
Follows
table. This simply holds the id
s of both the follower and followee:CREATE Table Follows ( id INT NOT NULL AUTO_INCREMENT, user_id INT NOT NULL, followee_id INT, PRIMARY KEY(id, user_id) );
Follows Table
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 ( id INT NOT NULL AUTO_INCREMENT, hash VARCHAR(52) NOT NULL, username VARCHAR(18), PRIMARY KEY(id, hash) );And this final table looks like the following image:
UserAuth Table
The Model
Create a file, calledmodel.php
and enter the following class declaration:class Model{ private $db; // Holds mysqli Variable function __construct(){ $this->db = new mysqli('localhost', 'user', 'pass', 'Ribbit'); } }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 likeThe first function I want to implement isSELECT
andINSERT
.
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 SELECTs private function select($table, $arr){ $query = "SELECT * FROM " . $table; $pref = " WHERE "; foreach($arr as $key => $value) { $query .= $pref . $key . "='" . $value . "'"; $pref = " AND "; } $query .= ";"; 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) { $query = "INSERT INTO " . $table . " ("; $pref = ""; foreach($arr as $key => $value) { $query .= $pref . $key; $pref = ", "; } $query .= ") VALUES ("; $pref = ""; foreach($arr as $key => $value) { $query .= $pref . "'" . $value . "'"; $pref = ", "; } $query .= ");"; return $this->db->query($query); } //--- private function for performing standard DELETEs private function delete($table, $arr){ $query = "DELETE FROM " . $table; $pref = " WHERE "; foreach($arr as $key => $value) { $query .= $pref . $key . "='" . $value . "'"; $pref = " AND "; } $query .= ";"; 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://--- private function for checking if a row exists private function exists($table, $arr){ $res = $this->select($table, $arr); return ($res->num_rows > 0) ? true : false; }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 calledRouter
. 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{ private $routes; function __construct(){ $this->routes = array(); } public function lookup($query) { if(array_key_exists($query, $this->routes)) { return $this->routes[$query]; } else { return false; } } }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:function __construct(){ $this->routes = array( "home" => "indexPage", "signup" => "signUp", "login" => "login", "buddies" => "buddies", "ribbit" => "newRibbit", "logout" => "logout", "public" => "publicPage", "profiles" => "profiles", "unfollow" => "unfollow", "follow" => "follow" ); }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:RewriteEngine On RewriteRule ^/?Resource/(.*)$ /$1 [L] RewriteRule ^$ /home [redirect] RewriteRule ^([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 calledcontroller.php
and enter in the following:require("model.php"); require("router.php"); class Controller{ private $model; private $router; //Constructor function __construct(){ //initialize private variables $this->model = new Model(); $this->router = new Router(); //Proccess Query String $queryParams = false; if(strlen($_GET['query']) > 0) { $queryParams = explode("/", $_GET['query']); } $page = $_GET['page']; //Handle Page Load $endpoint = $this->router->lookup($page); if($endpoint === false) { header("HTTP/1.0 404 Not Found"); } else { $this->$endpoint($queryParams); } }
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:
private function redirect($url){ header("Location: /" . $url); }The next two functions load a view and a page, respectively:
private function loadView($view, $data = null){ if (is_array($data)) { extract($data); } require("Views/" . $view . ".php"); } private function loadPage($user, $view, $data = null, $flash = false){ $this->loadView("header", array('User' => $user)); if ($flash !== false) { $flash->display(); } $this->loadView($view, $data); $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:private function checkAuth(){ if(isset($_COOKIE['Auth'])) { return $this->model->userForAuth($_COOKIE['Auth']); } else { return false; } }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 theModel
class, right after the exists()
function, add the following function:public function userForAuth($hash){ $query = "SELECT Users.* FROM Users JOIN (SELECT username FROM UserAuth WHERE hash = '"; $query .= $hash . "' LIMIT 1) AS UA WHERE Users.username = UA.username LIMIT 1"; $res = $this->db->query($query); if($res->num_rows > 0) { return $res->fetch_object(); } else { return false; } }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 theFor 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 aloadPage()
function, there was an option to pass aflash
object, a message that appears above all the content.
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{ public $msg; public $type; function __construct($msg, $type) { $this->msg = $msg; $this->type = $type; } public function display(){ echo "<div class=\"flash " . $this->type . "\">" . $this->msg . "</div>"; } }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:<?php require("controller.php"); $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 calledViews
. 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> </div> <footer> <div class="wrapper"> Ribbit - A Twitter Clone Tutorial<img src="http://cdn.tutsplus.com/net.tutsplus.com/authors/jeremymcpeak//Resource/gfx/logo-nettuts.png"> </div> </footer> </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 addedResources/
to the beginning of the path (for the mod_rewrite rule).
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:<!DOCTYPE html> <html> <head> <link rel="stylesheet/less" href="/Resource/style.less"> <script src="/Resource/less.js"></script> </head> <body> <header> <div class="wrapper"> <img src="http://cdn.tutsplus.com/net.tutsplus.com/authors/jeremymcpeak//Resource/gfx/logo.png"> <span>Twitter Clone</span> <?php if($User !== false){ ?> <nav> <a href="/buddies">Your Buddies</a> <a href="/public">Public Ribbits</a> <a href="/profiles">Profiles</a> </nav> <form action="/logout" method="get"> <input type="submit" id="btnLogOut" value="Log Out"> </form> <?php }else{ ?> <form method="post" action="/login"> <input name="username" type="text" placeholder="username"> <input name="password" type="password" placeholder="password"> <input type="submit" id="btnLogIn" value="Log In"> </form> <?php } ?> </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"> <div class="panel right"> <h1>New to Ribbit?</h1> <p> <form action="/signup" method="post"> <input name="email" type="text" placeholder="Email"> <input name="username" type="text" placeholder="Username"> <input name="name" type="text" placeholder="Full Name"> <input name="password" type="password" placeholder="Password"> <input name="password2" type="password" placeholder="Confirm Password"> <input type="submit" value="Create Account"> </form> </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 theController
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(); if($user !== false) { $this->redirect("buddies"); } else { $flash = false; if($params !== false) { $flashArr = array( "0" => new Flash("Your Username and/or Password was incorrect.", "error"), "1" => new Flash("There's already a user with that email address.", "error"), "2" => new Flash("That username has already been taken.", "error"), "3" => new Flash("Passwords don't match.", "error"), "4" => new Flash("Your Password must be at least 6 characters long.", "error"), "5" => new Flash("You must enter a valid Email address.", "error"), "6" => new Flash("You must enter a username.", "error"), "7" => new Flash("You have to be signed in to acces that page.", "warning") ); $flash = $flashArr[$params[0]]; } $this->loadPage($user, "home", array(), $flash); } }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 theindexPage()
:private function signUp(){ if($_POST['email'] == "" || strpos($_POST['email'], "@") === false){ $this->redirect("home/5"); } else if($_POST['username'] == ""){ $this->redirect("home/6"); } else if(strlen($_POST['password']) < 6) { $this->redirect("home/4"); } else if($_POST['password'] != $_POST['password2']) { $this->redirect("home/3"); } else{ $pass = hash('sha256', $_POST['password']); $signupInfo = array( 'username' => $_POST['username'], 'email' => $_POST['email'], 'password' => $pass, 'name' => $_POST['name'] ); $resp = $this->model->signupUser($signupInfo); if($resp === true) { $this->redirect("buddies/1"); } else { $this->redirect("home/" . $resp); } } }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'])); if($emailCheck){ return 1; } else { $userCheck = $this->exists("Users", array("username" => $user['username'])); if($userCheck){ return 2; } else{ $user['created_at'] = date( 'Y-m-d H:i:s'); $user['gravatar_hash'] = md5(strtolower(trim($user['email']))); $this->insert("Users", $user); $this->authorizeUser($user); return true; } } }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:public function authorizeUser($user){ $chars = "qazwsxedcrfvtgbyhnujmikolp1234567890QAZWSXEDCRFVTGBYHNUJMIKOLP"; $hash = sha1($user['username']); for($i = 0; $i<12; $i++) { $hash .= $chars[rand(0, 61)]; } $this->insert("UserAuth", array("hash" => $hash, "username" => $user['username'])); setcookie("Auth", $hash); }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 thelogin()
and logout()
functions. Add the following to the Controller
class after the login()
function:private function login(){ $pass = hash('sha256', $_POST['password']); $loginInfo = array( 'username' => $_POST['username'], 'password' => $pass ); if($this->model->attemptLogin($loginInfo)) { $this->redirect("buddies/0"); } else { $this->redirect("home/0"); } }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:private function logout() { $this->model->logoutUser($_COOKIE['Auth']); $this->redirect("home"); }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){ if($this->exists("Users", $userInfo)){ $this->authorizeUser($userInfo); return true; } else{ return false; } } public function logoutUser($hash){ $this->delete("UserAuth", array("hash" => $hash)); 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 calledbuddies.php
in the Views
folder and insert the following:<div id="createRibbit" class="panel right"> <h1>Create a Ribbit</h1> <p> <form action="/ribbit" method="post"> <textarea name="text" class="ribbitText"></textarea> <input type="submit" value="Ribbit!"> </form> </p> </div> <div id="ribbits" class="panel left"> <h1>Your Ribbit Profile</h1> <div class="ribbitWrapper"> <img class="avatar" src="http://www.gravatar.com/avatar/<?php echo $User->gravatar_hash; ?>"> <span class="name"><?php echo $User->name; ?></span> @<?php echo $User->username; ?> <p> <?php echo $userData->ribbit_count . " "; echo ($userData->ribbit_count != 1) ? "Ribbits" : "Ribbit"; ?> <span class="spacing"><?php echo $userData->followers . " "; echo ($userData->followers != 1) ? "Followers" : "Follower"; ?></span> <span class="spacing"><?php echo $userData->following . " Following"; ?></span><br> <?php echo $userData->ribbit; ?> </p> </div> </div> <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 echo $ribbit->username; ?> <span class="time"> <?php $timeSince = time() - strtotime($ribbit->created_at); if($timeSince < 60) { echo $timeSince . "s"; } else if($timeSince < 3600) { echo floor($timeSince / 60) . "m"; } else if($timeSince < 86400) { echo floor($timeSince / 3600) . "h"; } else{ echo floor($timeSince / 86400) . "d"; } ?> </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){ $user = $this->checkAuth(); if($user === false){ $this->redirect("home/7"); } else { $userData = $this->model->getUserInfo($user); $fribbits = $this->model->getFollowersRibbits($user); $flash = false; if(isset($params[0])) { $flashArr = array( "0" => new Flash("Welcome Back, " . $user->name, "notice"), "1" => new Flash("Welcome to Ribbit, Thanks for signing up.", "notice"), "2" => new Flash("You have exceeded the 140 character limit for Ribbits", "error") ); $flash = $flashArr[$params[0]]; } $this->loadPage($user, "buddies", array('User' => $user, "userData" => $userData, "fribbits" => $fribbits), $flash); } }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, followers, following "; $query .= "FROM (SELECT COUNT(*) AS ribbit_count FROM Ribbits WHERE user_id = " . $user->id . ") AS RC "; $query .= "LEFT JOIN (SELECT user_id, ribbit FROM Ribbits WHERE user_id = " . $user->id . " ORDER BY created_at DESC LIMIT 1) AS R "; $query .= "ON R.user_id = " . $user->id . " JOIN ( SELECT COUNT(*) AS followers FROM Follows WHERE followee_id = " . $user->id; $query .= ") AS FE JOIN (SELECT COUNT(*) AS following FROM Follows WHERE user_id = " . $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) { $query = "SELECT name, username, gravatar_hash, ribbit, Ribbits.created_at FROM Ribbits JOIN ("; $query .= "SELECT Users.* FROM Users LEFT JOIN (SELECT followee_id FROM Follows WHERE user_id = "; $query .= $user->id . " ) AS Follows ON followee_id = id WHERE followee_id = id OR id = " . $user->id; $query .= ") AS Users on user_id = Users.id ORDER BY Ribbits.created_at DESC LIMIT 10;"; $res = $this->db->query($query); $fribbits = array(); while($row = $res->fetch_object()) { array_push($fribbits, $row); } 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 theController
and Model
classes. In Controller
, add the following function:private function newRibbit($params){ $user = $this->checkAuth(); if($user === false){ $this->redirect("home/7"); } else{ $text = mysql_real_escape_string($_POST['text']); if(strlen($text) > 140) { $this->redirect("buddies/2"); } else { $this->model->postRibbit($user, $text); $this->redirect("buddies"); } } }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){ $r = array( "ribbit" => $text, "created_at" => date( 'Y-m-d H:i:s'), "user_id" => $user->id ); $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:private function publicPage($params){ $user = $this->checkAuth(); if($user === false){ $this->redirect("home/7"); } else { $q = false; if(isset($_POST['query'])) { $q = $_POST['query']; } $ribbits = $this->model->getPublicRibbits($q); $this->loadPage($user, "public", array('ribbits' => $ribbits)); } } private function profiles($params){ $user = $this->checkAuth(); if($user === false){ $this->redirect("home/7"); } else{ $q = false; if(isset($_POST['query'])) { $q = $_POST['query']; } $profiles = $this->model->getPublicProfiles($user, $q); $this->loadPage($user, "profiles", array('profiles' => $profiles)); } }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:<div class="panel right"> <h1>Search Ribbits</h1> <p> </p><form action="/public" method="post"> <input name="query" type="text"> <input type="submit" value="Search!"> </form> <p></p> </div> <div id="ribbits" class="panel left"> <h1>Public Ribbits</h1> <?php foreach($ribbits 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 echo $ribbit->username; ?> <span class="time"> <?php $timeSince = time() - strtotime($ribbit->created_at); if($timeSince < 60) { echo $timeSince . "s"; } else if($timeSince < 3600) { echo floor($timeSince / 60) . "m"; } else if($timeSince < 86400) { echo floor($timeSince / 3600) . "h"; } else{ echo floor($timeSince / 86400) . "d"; } ?> </span> <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> </p><form action="/profiles" method="post"> <input name="query" type="text"> <input type="submit" value="Search!"> </form> <p></p> </div> <div id="ribbits" class="panel left"> <h1>Public Profiles</h1> <?php foreach($profiles as $user){ ?> <div class="ribbitWrapper"> <img class="avatar" src="http://www.gravatar.com/avatar/<?php echo $user->gravatar_hash; ?>"> <span class="name"><?php echo $user->name; ?></span> @<?php echo $user->username; ?> <span class="time"><?php echo $user->followers; echo ($user->followers > 1) ? " followers " : " follower "; ?> <a href="<?php echo ($user->followed) ? "unfollow" : "follow"; ?>/<?php echo $user->id; ?>"><?php echo ($user->followed) ? "unfollow" : "follow"; ?></a></span> <p> <?php echo $user->ribbit; ?> </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) { $query = "SELECT name, username, gravatar_hash, ribbit, Ribbits.created_at FROM Ribbits JOIN Users "; $query .= "ON user_id = Users.id ORDER BY Ribbits.created_at DESC LIMIT 10;"; } else{ $query = "SELECT name, username, gravatar_hash, ribbit, Ribbits.created_at FROM Ribbits JOIN Users "; $query .= "ON user_id = Users.id WHERE ribbit LIKE \"%" . $q ."%\" ORDER BY Ribbits.created_at DESC LIMIT 10;"; } $res = $this->db->query($query); $ribbits = array(); while($row = $res->fetch_object()) { array_push($ribbits, $row); } 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:
public function getPublicProfiles($user, $q){ if($q === false) { $query = "SELECT id, name, username, gravatar_hash FROM Users WHERE id != " . $user->id; $query .= " ORDER BY created_at DESC LIMIT 10"; } else{ $query = "SELECT id, name, username, gravatar_hash FROM Users WHERE id != " . $user->id; $query .= " AND (name LIKE \"%" . $q . "%\" OR username LIKE \"%" . $q . "%\") ORDER BY created_at DESC LIMIT 10"; } $userRes = $this->db->query($query); if($userRes->num_rows > 0){ $userArr = array(); $query = ""; while($row = $userRes->fetch_assoc()){ $i = $row['id']; $query .= "SELECT " . $i . " AS id, followers, IF(ribbit IS NULL, 'This user has no ribbits.', ribbit) "; $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(*) "; $query .= "AS followed FROM Follows WHERE followee_id = " . $i . " AND user_id = " . $user->id . ") AS F2 LIMIT 1;"; $userArr[$i] = $row; } $this->db->multi_query($query); $profiles = array(); do{ $row = $this->db->store_result()->fetch_object(); $i = $row->id; $userArr[$i]['followers'] = $row->followers; $userArr[$i]['followed'] = $row->followed; $userArr[$i]['ribbit'] = $row->ribbit; array_push($profiles, (object)$userArr[$i]); }while($this->db->next_result()); return $profiles; } 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 theController
class:private function follow($params){ $user = $this->checkAuth(); if($user === false){ $this->redirect("home/7"); } else{ $this->model->follow($user, $params[0]); $this->redirect("profiles"); } } private function unfollow($params){ $user = $this->checkAuth(); if($user === false){ $this->redirect("home/7"); } else{ $this->model->unfollow($user, $params[0]); $this->redirect("profiles"); } }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){ $this->insert("Follows", array("user_id" => $user->id, "followee_id" => $fId)); } public function unfollow($user, $fId){ $this->delete("Follows", array("user_id" => $user->id, "followee_id" => $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:Order allow,deny Deny from allThis 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!
No comments:
Post a Comment