How I made a social network in a bunch of hours with PHP + Redis

Salvatore Zappalà
7 min readApr 12, 2015

--

This is a repost of a two years old post from my old blog.
You can see a working demo
here

The project

This is a school project for my Database course. I built a simple Facebook clone called Fakebook (yes, Fakebook, I know it’s an awful name and even my professor laughed about it!).

It was inspired by the offical case-study about building *Retwis*, a twitter clone. You can see it here.

You can see, fork or follow the code from the github repo here.

This is a poorly translated and cropped version of my original project documentation in Italian.
As you can see I’m not a native English speaker and I apologize for any errors in advance.

Features

  • User signup
  • Post wall status
  • Request/Accept friendship
  • Send private messages

Technologies and languages used

  • Php as a server side language
  • Redis
  • HTML, CSS, Javascript

Connecting PHP to Redis: Predis

To connect with Redis we need a Client. You can see a list of redis clients here. For this project i used Predis.

To install Predis you should use Composer, but for simplicity I’m going to use an unique generated Php file.

More info on installing Predis with Composer [here].

In our case the Predis file will be located on `php/Predis.php`, so we just need to import this file. (I use the default Redis address and port).

The Fakebook class

For the sake of simplicity, all the main logic of our social network will be located in an unique class, located on `php/fakebook.php`.

I’m aware that this is an antipattern, but I wanted to keep this as simple as possible because this is just a project for educational purposes.

In this class we will have a reference to the Predis instance for the database connection, that is made every time that the class is istantiated.

Data Layout

Here we define the structure of the database.

No tables, no foreign keys! Just a bunch of key and values.

Values are string, lists or sets, depending of the data organization that we needed.

Remember, this is a key-value store, so we need to manually keep a reference to all data that we need. There is no table or autoincrement like all popular SQL DBMS.

This key will contain the id for the next new user:

global:nextUserId

And this one the id of the next status:

global:nextStatusId

Users

We are going to reference every user with an unique id, like we would do in a normal relational database.

This is what will happen every time a new user is created, at database level:

INCR global:nextUserId => 1000SET uid:1000:email mario@rossi.itSET uid:1000:password r0ss1

We atomically increment the nextUserID and we assign it to the new user. Every data related to the user will be accessed by this key.

In this example the password is stored in plain text, but obviously we would use hash & salt in a real production enviroment.

How do we get the ID from the email address? There is no external key here, so we need to set a key that associates the email address to the relative user id. This is because when a user will logon, he will use the email address to be identified, he doesn’t even know his unique user id.

SET email:mario@rossi.it:uid 1000

This way we can retrieve the id from the email.

As you can imagine, we must be more careful at the application level now, because the data integrity is not handled at the database level like it would be in a relational database.

uid:x:email -> E-mail address
uid:x:password -> Password
uid:x:name -> Name
uid:x:surname -> Surname
uid:x:token -> Authentication token
uid:x:friendrequests -> Set with all incoming friend requests
uid:x:friends -> Set with all friends
uid:x:statuses -> List with every user status (post
uid:x:updates -> List with all friends statuses
uid:x:messages -> List with every incoming messages
uid:x:notifications -> List with notifications
email:y:uid -> User id corresponding to the email y
fullname:w:uid -> User id corresponding to the full name w
token:z -> User id corresponding to the auth token z

status:x:likes -> Set with all the ids of the people that like this status
status:x:comments -> List with all comments relative to the status

Authentication

Users can signup directly from the first page.

We will not use php sessions, because they would compromise the scalability. We will instead make a random token for every user, and associate the token with the relative user id.

So we need two pair of key-values:

SET uid:1000:token fea5e81ac8ca77622bed1c2132a021f9SET token:fea5e81ac8ca77622bed1c2132a021f9 1000

Note that every token is generated when the user is made, it will be rigenerated at every logout and handed over at the next succesful login.

Signup

This is our signup process in pseudo-code:

  • Retrieve email and password from the login form
  • Check if the email is associated to a user
  • If yes, we’ve got the user id
  • Check if the password maches
  • If yes, set a new cookie with the authentication token

Login

The login function is quite straightforward:

  • Check if the email exists
  • If yes, we’ve got the userid
  • Check if the password matches
  • If so, set a new cookie with the authtoken

Every time that we want to check if the user is logged, we just need to>

  • Check if the cookie with the authentication token exists
  • If yes, use the key `token:x` to get the user-id
  • Check if the tokens match
  • If thes, return the user-id of the logged user

Logout

To logout, we just create a new token and delete the reference to the old one:

/**
* Logout the user and set a new token string
*/
function logout() {
if ($userId = $this->getLoggedUserId()) {
$newToken = Fakebook::generateToken();
$oldToken = $this->r->get(“uid:$userId:token”);
$this->r->set(“uid:$userId:token”, $newToken);
$this->r->set(“token:$newToken”, $userId);
$this->r->del(“token:$oldToken”);
}
}

Friendship

For every user we use two SETs to handle friends requests and friends relationship (already accepted).

The SET containing all incoming friend requests will be reachable with the key `uid:x:friendrequests`.

The SET containing all already accepted friends is `uid:x:friends`.

Friendship request

To make a new friend request, we need to add the *requesting* user id into the SET of the incoming requests of the *receiving* user.

Let’s suppose that Alfio, with user id 1000, is requesting a friendship to Nella, with user id 2000:

SADD uid:2000:friendrequests 1000

`SADD `is the command to add a new element to a SET.

Approve a friendship request

To approve Alfio’s request, we need to add Alfio into Nella set of friends, Nella into Alfio’s one and remove the friendship request:

SADD uid:1000:friends 2000SADD uid:2000:friends 1000SREM uid:2000:friendrequests 1000

In a real world enviroment, we would use a transaction to avoid that concurrent operation from different clients make a mess with our database.

This is not complicated in Redis but I’m going to skip it.

For more infos on Redis transactions take a look here.

Remove a friendship

To remove a friendship we just delete the references from both SETs:

SREM uid:1000:friends 2000SREM uid:2000:friends 1000

Status

For every user we’ve got two status lists, one relative to the user statuses, and one were we push friend statuses for the news feed (updates). They are called `uid:x:statuses` and `uid:x:updates`.

To add a new status we will:

  • Add the status to the statuses list
  • For every friend, push the status to his updates list
 /**
* Publish a new user status
* @param string $message status message
*/
function pushStatus($message) {
// get the logged user id
$uid = $this->getLoggedUserId();
// get the post id
$pid = $this->r->incr(‘global:nextPostId’);
// build the status string
$status = $pid . ‘|’ . $uid . ‘|’ . $message . ‘|’ . time();
// prepend the status in the user statuses list
$this->r->lpush(“uid:$uid:statuses”, $status);
// prepend the status in every friend’s updates list
$friends = $this->r->smembers(“uid:$uid:friends”);
foreach ($friends as $fid) {
$this->r->lpush(“uid:$fid:updates”, $status);
}
}

Note that all data relative to a status is stored in a unique string, separated with the ‘|’ character.

We put the post id, author id, message and timestamp.

News feed

To print the news feed, we fetch the updates from the list and print it in the page. To do this we use the `LRANGE` command.

To take 100 updates:

`LRANGE uid:1000:updates 0 100`

in php:

 function getUpdates() {
$uid = $this->getLoggedUserId();
return $this->r->lrange(“uid:$uid:updates”, 0, 100);
}

User profiles

To see a user profile we redirect to the page `user.php`.

In this page we do the following:

  • Check if the user is a friend, looking for it on the `uid:x:friends` list
  • If he is not, show the link to request the friendship
  • Show all the user statuses, stored in `uid:x:statuses`

Messages

Messages are stored in a queue, implemented with a list.
Every message is formed by a string containing:

1. Sending user id

2. Text

3. Timestamp

As we already did, we will concatenate them with the ‘|’ character.

If Nella sends a message to Alfio containing “Ciao!”, this is what will happen in a database level:

LPUSH uid:1000:messages 2000|Ciao!|1384252919 /**
* Send message to a user
* @param int $to User id of the receiving user
* @param string $text Text of the message
*/
function sendMessage($to, $text) {
// build the message string
$myid = $this->getLoggedUserId();
$message = $myid . ‘|’ . $text . ‘|’ . time();
// prepend the message in the receiving user message list
$this->r->lpush(“uid:$to:messages”, $message);
}

Comments and likes

To implements comments and likes I used the same technique as above, using a queue and a set, respectively.

Conclusion

Even if this project lacks some important features like password hashing and atomic transactions, I wanted to show that is possible to build a simple high performance, scalable social network from scratch in just a bunch of hours.

If you liked this post follow me on twitter: @salvozappa

If you want more info about the project feel free to leave a comment or to contact me. Comments are highly appreciated and incouraged.

Ciao!

--

--

Salvatore Zappalà

Software Engineer based in London. Opinions expressed here are mine.