Building Ribbit in PHP

Building Ribbit in PHP

Tutorial Details
    • Difficulty: Advanced
    • Completion Time: 1.5 Hours

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


Ribbit

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

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 -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:

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

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 (
    id            INT NOT NULL AUTO_INCREMENT,
    user_id       INT NOT NULL,
    ribbit        VARCHAR(140),
    created_at    DATETIME,
    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 ids 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

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 (
    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

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:

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 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 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 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{
	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 called controller.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 the Model 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 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{
    
    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 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>
	</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 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:

<!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 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();
  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 the indexPage():

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 the login() 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 called buddies.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 the Controller 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 the Controller 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 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!

Tags: mvcribbit
Note: Want to add some source code? Type <pre><code> before it and </code></pre> after it. Find out more
  • http://twitter.com/jeff_pz_cr Jeffrey Briceno

    It’s a advanced php tutorial, but all lowers levels can understand the concepts, nice tut and very useful, are working with msqli class, I’m learning the PDO class this is a challenge for me do this tut but using the PDO class.

    Thank for all and excuse me my bad Englis. (not more bad than google translate)

  • http://xangelo.ca Angelo

    If you’re having a hard time understanding regex, check out: http://www.regexper.com/ to easily visualize what the regex is doing.

  • http://josephscott.org/ Joseph Scott

    Please don’t use sha256 for hashing passwords. Just use the phpass library – http://www.openwall.com/phpass/ – instead. It takes care of using bcrypt hashing for you, which is a much better option for hashing passwords.

  • http://twitter.com/ericcourville Eric Courville

    Really great tut. I am currently building a single page app and will use a lot of your concepts here for ideas. As @Jeffrey Briceno states I will go the PDO route, but look to your model abstraction concepts.

  • Brendam

    There are many things that can be better done, questionable choices maybe, but at least, stop propagating the use of hashing for storing password. It’s irresponsible. At least, use bcrypt!

    • MPinteractiv

      Bcrypt is an algorithm to hash password , your sentence makes no sense. You should say you are using the wrong algorithm to hash , not to stop using hashing for storing password.

    • http://twitter.com/junkjet Dave

      bcrypt might be designed for passwords, but SHA256 is fine too (ideally salted, but the rainbow tables are large enough that it’s rarely cracked). Definitely wouldn’t go as far as to say it was “irresponsible”, not like he’s using MD5 or something

      • http://twitter.com/minitechme Ryan O’Hara

        SHA*, salted or unsalted, is just as bad as MD5 if you only use one round.

  • Alex

    Wow great!, this one is the backend! nice..

  • Barryvdh

    I also agree, PDO with Prepared statements would have been a better example (or at least prepared statements), and bcrypt should have been used (or perhaps the php5.5 API / fallback or indeed phpass). And I think it is probably easier to redirect to the phpfile (RewriteRule ^ app.php [L]) and parse the request path, instead of using regexes.

  • Pete

    I was really enthusiastic until I read:

    “I had to omit a lot of the features that you might find in a real production application, such…. protection against SQL injection,”

    What’s the pointing in teaching something that can’t be used in the real world? I want to use these tutorials to lean something useful, not something theoretical… yes they are a building blocks for learning but I want to learn to code properly….

    • Tomasz Struczyński

      Exactly. Even worse – not escaping (AT ALL) user input in the controller or model (better) is nowadays considered a BAD programming practice – tutorials may not learn all good practices, but should at least not introduce bad ones.

      Why not use PDO instead of mysqli (it’s pretty much standard now) and prepared statements with parameter binding? It’d take same amount of space and at least secure app a little bit.

    • hates-lazies

      you mean, you want to copy and paste this persons code and not bother doing any on your own. Take your lazy ass to the documentation of php and other sites and add some security.

      • Cosmin

        no, you should let a junior just use and learn from code that does not respect good practices

  • cp

    If you’re going to build queries by just creating a huge strings, i think using heredoc is neater and more elegant.

    • MPinteractiv

      the way heredoc is implemented in php is tricky and error prone , i would not use it. imho

  • Pete

    OK, this tutorial has gone from Beginner to Advanced from part 1 to part 2. I’ve downloaded all the files, set up the database, now how do I view the site – there is no index file in the root directory of the downloads? Do I need the files from last week? if so the forms don’t post anywhere? The download has no .htaccess file in the root directory, OK I can add that… but still nothing.

    Also I’ve been learning the ‘PDO’ route and been learning it exclusively as it seems the most recommended on Nettuts. Now I have to go back and lean ‘mysqli’, please choose one and stick with it for all tutorials, I’m here to learn, but can’t learn everything to cove all bases…

    • http://gabrielmanricks.com/ Gabriel
      Author

      It does get a little complicated in the middle area, but it’s an unavoidable issue. Due to the nature of how this project works, you cant view a page until you write all of it’s components.

      As per the missing .htaccess file, that is my fault, it’s a hidden file and when I zipped the source I must have forgotten it.

      Here it is for your convenience – https://gist.github.com/4446339

      Thank you for your feedback

      • Pete

        thanks, but if I can’t view a page until I write all of its components, was that not covered by the tutorial and included in the download? You showed a screen-shot of the finished project with some posts you added: “If you have done everything correctly, you should be able to traverse through all the pages and post ribbits. ” I still can’t work out how to even view the page where I would post a ‘ribbit’ – I have this set up on localhost/ribbit/, did I have to add the files from the first part of the tutorial..?

      • http://gabrielmanricks.com/ Gabriel
        Author

        I replicated your setup and it seems like the problem is with how the links are being handled, they are being called on the localhost directly instead of “localhost/ribbit”.

        To fix this you have to add a .htaccess file to the root folder “localhost” with the contents of this link: https://gist.github.com/4446973

        it checks to see if the file exists and if not it will reroute the request to the ribbit folder, so your other project’s should not be affected.

        I hope this helps, if you have a different problem feel free to post the details here and I will try and get back to you as soon as possible.

        P.S. The files from the first part are not necessary for this project.

  • Joe Blow

    I think this is great introduction to MVC; however I always have had a problem having database related stuff encapsulated in models. Shouldn’t Models be nothing but dumb objects with properties and occasional methods that mutate/access those properties? When I say dumb objects I’m thinking of the way you work with database in ZF via db/table/row gateways. You keep your models separate from your database. Am I overthinking this? Anyone care to enlighten/debate me?

    • http://gabrielmanricks.com/ Gabriel
      Author

      I would also like to hear other opinions on this matter, but as far as I know there are no such restrictions for the model. It’s definitely not fit to go into a controller or view.

      There can be benefits to abstracting data by means of some class outside of the framework (a helper class) or even a REST service, in terms of flexibility later on, and it can make it a bit easier to scale in the case of building a RESTful data API.

      But as far as an MVC framework is concerned I think it’s perfectly good practice. (although if you are releasing a framework for other people to use, then you may want to take out thing like the connection info and put them in a config.php file so people don’t have to start opening class files unnecessarily).

    • http://twitter.com/junkjet Dave

      From what I’ve seen with MVC frameworks, generally the model is the part which loads data from the database and returns it in an appropriate format. It might be that there’s a separate class that deals with the connection and running the queries, but the queries themselves are still defined in the model

  • maxsurguy

    Cool tutorial! I made a similar one about half a year ago, including Laravel integration!

  • Fvg

    Would love to see this on Laravel 4 :)

  • Sean

    Is there any way I can get a link to see a demo?

    • Sean

      Nevermind I missed the demo link at the top.

  • Darragh Flynn

    This is definitely a great for beginner people wanting to work on the bones of an application from scratch. It is missing a lot of the advanced stuff that would be commonplace on social sites, such as Ajax. It’s a great resource and shouldn’t be bashed by those lucky enough to have a better knowledge of the technology used, myself included.

    Good job IMO!

  • Aris_B

    Hello Gabriel, first of all I have to say it is a great article and I enjoyed it pretty much so far.

    My question is, what if you want to create the application in a subdomain ? subdomain.domain.com or in a folder of another domain ? http://www.domain.com/application/ ? Second question, why didn’t you use a database like mongodb to store data ? I think its so much easier

    • Jose Gomez

      I think there is no problem with subdomains, but with folders maybe u have to prefix the rewrite rule with the directory.

      and perhaps mysql was used because its almost the most popular DB Engine on web hosts.

      • http://gabrielmanricks.com/ Gabriel
        Author

        That’s right subdomains should work fine and if you are using a subfolder then you should put the following .htaccess file in the root. (change the word ribbit in the last line with the actual folder name where you put this). https://gist.github.com/4446973

  • Sean

    Its been hacked

  • http://www.jsxtech.com Jaspal Singh aka jsxtech

    Great Article for beginners, lack security features,
    waiting for next part to complete the ribbit.

  • Marjun Tacder

    I like this site except for articles that are not implementing good practices. I know you all are capable of doing (at least I hope so) but no matter how trivial your examples/tutorials are, good practices should be there. Beginners will follow it and experienced programmers are gonna praise it (or at least doesn’t mind it).

  • Liam

    Are we going to get a ASP,net example?

  • Gabriele

    A twitter clone? Please, if you aren’t going to do it in Node Js I’ll probably never read another net tut+ tutorial!

    • Jose Gomez

      think its a nice tutorial for medium php level, the web development doesn’t mean just node.js…

      • Gabriele

        Oh I know, I even use PHP on daily basis, but for this type of application Node Js fits perfect!

  • http://www.i54design.com/ icank

    Awesome “mengagumkan”…. tapi itu sulit dipelajari bagi pemula seperti saya… :)

  • Aleksander

    Great tutorial – many thanks to the Author. Nevertheless, I completely disagree with the quoted sentence “In a MVC framework, all HTTP requests usually go to a single controller” and I have a slight feeling, that this controversial statement was used on purpose :). This is true when thinking of an architecture using the Front Controller pattern – and that goes well when implemented in smaller projects. The majority of mature PHP frameworks use a number of controllers, each designed for specific needs (Article, News, User, Page etc.). We can say that it all comes to one routing mechanism – FC routing to action classes, or some router class pointing to specific controllers… but I just had to point it out to feel a lil’ bit smug with myself ;-). Keep up the good work!

  • Alex Fraundorf

    Very nice article Gabriel and a ton of good information in there. No, its not perfect as some others have mentioned (I agree that PDO would have been a better choice), but I think there is a lot more good than bad, and plenty that people can learn from. I’m looking forward to reading the next articles in the series. Thank you for sharing!

  • Henrik Fröjdendal

    Hello

    I got problems whit my site http://www.iflip.se i only get internet sever error

    my .htaccess looks like this:

    RewriteEngine on
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteRule ^(.*)$ /$1

    The database connnection is in models.php and the database is created.

    Do anyone n

  • Vladislav Veselinov

    I’m scared how insecure this is! Authors of such things need to learn that most people are eager to get stuff done and omitting things like SQL INJECTION from a tutorial that creates a whole app. At least put in red the places that are insecure and are there just as a proof of concept.

    • Vladislav Veselinov

      edit: *from a tutorial that creates a whole app is plain wrong and irresponsible!

  • http://twitter.com/minitechme Ryan O’Hara

    Ever heard of CSRF? It stands for Cross-Site Request Forgery. It’s a bad thing and it’s going to hurt someone who makes something using this tutorial.

    • MPinteractiv

      So you may give an insight , even provide code to make the tutorial better. Ever heard of constructive comments ?

      • http://twitter.com/minitechme Ryan O’Hara

        Maybe you don’t get the meaning behind this insight, so I will phrase it as a typical insight: “This doesn’t protect against cross-site request forgery; maybe you should include some code for that?”

        As for code, I’m not going to waste my own time writing code for this tutorial when I could just write another tutorial. It’d be akin to doing that.

  • Chidiebere Nnadi

    Great tutorial…lots of bits to learn from. For me most especially the MVC part.

  • http://www.facebook.com/mcrorey Michael McRorey

    Just curious, if it would be better to use implode over string concatenation with your foreach. I am not taking into account escaping any variables, just going over the implode/foreach difference.


    function insert($table, $arr)
    {
    $query = "INSERT INTO " . $table . " (";
    $query .= implode(",", array_keys($arr));
    $query .= ") VALUES (";
    $query .= implode(",", array_values($arr));
    $query .= ");";

    return $query;
    }

    $my_array = array("username"=>"my_username", "name"=>"my_name", "password"=>"my_password", "email"=>"my_email");

    echo insert("my_table", $my_array);

    • http://twitter.com/junkjet Dave

      Implode and foreach would pretty much do the same job, so you’re probably best going with implode since it’s built specifically for this

  • Elze Kool

    Great that this is a free tutorial but it scares me. Every part contains bad practices.

    You can’t expect newcommers to do it right when there learning bad stuff from tutorials like this.

    Some remarks:

    Database: ribbit in the Ribbits and followee_id in the Follows table should not allow NULL

    Models: Never ever concat queries like this. Use prepared statements with PDO or at least some proper escaping.

    signUp function, your main flow in an else block? Are you assuming that it fails? Don’t make large blocks of else and if.

    signupUser,attemptLogin, newRibbit, publicPage functions, stick to one coding style, with the { on the next line or on the same line but not mix them up.

    buddies function, don’t concat strings like this. but use sprintf, so you can easly translate and search for strings

    getUserInfo,getFollowersRibbits function, my eyes hurt. Who tought of writing this SQL?

    newRibbit, this is the baddest mixup of writing styles there is. And add mysql_real_escape_string where it belongs, in the postRibbit function. Imagine that this public function should be used from somewhere else..

    getPublicProfiles function , imagine someone looking at this function and thinking whatt!? What does q means.. Split functions up when the get complicated.

    Sorry for the rand.. But I have to deal with this mistakes from others every day. It’s great to have a tutorial on something but start at the important basics, it’s like telling a DIY’er to build a house and forget to mention that a house should have a foundation..

  • http://www.facebook.com/joseph.freemind Joseph Freemind

    Almost everyone here is complaining about security instead of giving the author some credit. We all handle security in different ways so just take the idea and apply your own security the way you see it. And for the author using mysqli, that is not a bad idea because almost every PHP programmer understands and started out with that old library so use it or PDO, you can even plugin an ORM library if you want. What i`m interested in is the concept, this could be done using any of the frameworks out there like Laravel or Codeigniter with enhanced security and good database libraries.

    • Peter

      For those of us learning, we learn from tutorials like these, so can the author point us in the right direction on how to improve the security, I love to see what is possible with PHP, I learn by looking at the project as a whole then studying the code to see how it was done, but if nobody can point out exactly the bits that are coded badly/insecurely and give guidance on how to improve it or where to find those answers I’m not going to learn…yes I’m a slow learner but I get there quicker if I am pointed in the right direction it make learning more fun than having to do all the research then not actually know if I’m doing it properly when I find a solution.

      Yes, this tutorial is free, but it’s only free because people like me visit the site to learn; which makes sponsors want to put their hands in the wallets and pay to get their services on the site and in front of loads of potential clients.

      I’ve yet to see a real work login system, that I could implement on a real world project. Soon I’m paying a freelancer to create one for me (I would sign up for nettuts premium, but there arent any tutorials – for me this is the backbone to creating a freelance career and custom projects), and when I have forked out my hard earnt cash (maybe signed up for hosting that is sponsoring this site) I still dont know that the freelancer has coded it properly because I can’t find the step by step tutorials to help me learn to do it myself…

      • http://www.facebook.com/joseph.freemind Joseph Freemind

        You are right, if you are really interested in implementing the concept here then you may want to pick a framework like Codeigniter or Laravel.

  • http://www.jvsoftware.com/ Javier Villanueva

    Relax people come on, I agree this tutorial is full of security vulnerabilities and a few bad practices but 99.9% of tutorials on the internet aren’t meant to use “as is” or just copy/paste and that’s a good thing, if the author covers ALL the possible scenarios it’d make the article too long and lose its actual point.

    Also just complaining about its flaws doesn’t help either, maybe leave a link where new users can find more information about the issue or contribute with a fix can make wonders for the community in general (you can’t believe how many times I’ve found solutions to a problem on comments rather than the article itself). In the end you’re supposed to use it as a tool or a basic foundation to your project not like rules set in stone.

  • Dino

    Why not use a framework like you did in “Building Ribbit in Rails”? You do lots of Laravel so that seems like a perfect choice, or another one like zf2, cake, symfony, or lithium?

    Also, the syntax standards you implemented made this PHP code very easy to read.

    This kinda sounds mean, but that’s not my intention, just some friendly advice! [:

    • http://twitter.com/junkjet Dave

      I think the idea was to show it being built without a framework then recreate it in different ones (Rails and Laravel are the examples) so you can compare the development process

  • Dave

    I totally appreciate time taken to create this tutorial. When you search your own username on Ribbit it throws an error : Warning: Invalid argument supplied for foreach() in /var/www/Views/profiles.php on line 12 Kindly check on that.

  • Patkos Csaba

    Interesting article. I would have done it differently, I mean the coding, but I like the idea in this one. I do not particularly care about the security stuff everyone is whining about here, that has little to do with the tutorial.

    What I found a little strange is that you actually implemented a MVC framework and a way to communicate to a database. I am curious, wouldn’t it have been simpler to explain the concept by just taking some very simple/basic MVC framework, or just using 2-3 php files instead of MVC, and instead of using a database just serialize the objects into files? For a twitter size project my idea is obviously not practical, but for a tutorial it may take away some of the noise.

    What do you think?

    • http://gabrielmanricks.com/ Gabriel
      Author

      That’s definitely a valid option. I wouldn’t of thought to do it that way at first, but with frameworks out there like Statamic, you can really see the advantages of using static files for storage.

  • Samir

    Its very cool. but plz plz plz if you can make a video for this tutorial.

  • MPinteractiv

    silex version with doctrine orm : https://github.com/Mparaiso/silex-ribbit

  • Vanja Djurdjevic

    Actually, this is a great tutorial. The point of it is to introduce PHP MVC principles to the beginners, not to learn them how to create a full-functioning Twitter clone. Please stop complaining and get your hands dirty. Maybe the author overlooked the fact that he should’ve put a ‘PHP MVC’ or something similar in the tutorial title. Cheers

  • http://twitter.com/mezigh mezigh

    the title of this tuto is really cool , but …
    Are you sure of your way in terms of best practice. This stuff is trully untestable … But this stuff is working so…
    What about the same topics using php-aciverecords as ORM for the model .It would give the choice with databases…

  • ajay

    Hi,

    I just downloaded the demo folder from this tutorial and made all the setup with the database. After when I just browsed the app page I got this error Notice: Undefined index: query in controller.php on line 35 and
    also Notice: Undefined index: page in controller.php on line 43 and the total page is showing blank. Can you tell me why this error? I am using LAMP…