Making an AJAX Web Chat (Part 1) - PHP and MySQL
When discussing real time communication, there aren't many solutions that can rival the power of a simple webchat. What is even better, is that you already have all the tools you need to create one - your web browser. This, coupled with the fact that this is also one of the most requested tutorials by Tutorialzine's readers, means that it is about time to start coding.
In this two-part tutorial, we will be creating an AJAX Web Chat using PHP, MySQL and jQuery. In this first part, we will be discussing the PHP & MySQL side, and next week we will continue with the jQuery and CSS front-end. Go to part two.
HTML
As usual, the first step is to lay down the HTML markup. Our document is structured as HTML5 for convenience, as this allows us to use the new, shorter (and more memorable) doctype, and skip the type attribute on the script tags.
ajax-chat.html
<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title></title> <!-- Loading the jScrollPane CSS, along with the styling of the chat in chat.css and the rest of the page in page.css --> <link rel="stylesheet" type="text/css" href="js/jScrollPane/jScrollPane.css" /> <link rel="stylesheet" type="text/css" href="css/page.css" /> <link rel="stylesheet" type="text/css" href="css/chat.css" /> </head> <body> <div id="chatContainer"> <div id="chatTopBar" class="rounded"></div> <div id="chatLineHolder"></div> <div id="chatUsers" class="rounded"></div> <div id="chatBottomBar" class="rounded"> <div class="tip"></div> <form id="loginForm" method="post" action=""> <input id="name" name="name" class="rounded" maxlength="16" /> <input id="email" name="email" class="rounded" /> <input type="submit" class="blueButton" value="Login" /> </form> <form id="submitForm" method="post" action=""> <input id="chatText" name="chatText" class="rounded" maxlength="255" /> <input type="submit" class="blueButton" value="Submit" /> </form> </div> </div> <!-- Loading jQuery, the mousewheel plugin and jScrollPane, along with our script.js --> <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script> <script src="js/jScrollPane/jquery.mousewheel.js"></script> <script src="js/jScrollPane/jScrollPane.min.js"></script> <script src="js/script.js"></script> </body> </html>
To optimize the load time, the stylesheets are included in the head section, and the JavaScript files in the footer, just before the closing body tag.
We are using the jScrollPane plugin to create the scrollable area with the chats entries. This plugin comes with its own stylesheet, which is the first thing we've included into the page.
The markup of the chat consists of four main divs - the top bar, the chat container, the user container and the bottom bar. The latter holds the login and submit forms. The submit form is hidden by default and only shown if the user has successfully logged in the chat system.
Lastly we include the JavaScript files. Starting with the jQuery library, we add the mousewheel plugin (used by jScrollPane), the jScrollPane plugin itself and our script.js file.
Database Schema
Before we move on with the PHP part, we first have to take a closer look at how the chat data is organized in the MySQL database.
For the purposes of this script we use two tables. In webchat_users we are storing the chat participants. This table has na id, name, gravatar and a last_activity field. The name field is defined as unique, so that no users have duplicate nick names in the chatroom.
Another useful feature of the unique index fields, is that insert queries will fail and the inserted_rows property of the MySQLi object will be set to zero if we attempt to insert a duplicate row. This finds its place in the Chat PHP class you will see in the next step.
The last_activity column holds a timestamp, which is updated every 15 seconds for every user. It is also defined as an index, so it is faster to delete inactive users (having a last_activity column with a greater value than 15, would mean that the user is no longer viewing the chat window).
The webchat_lines table holds the individual chat entries. Notice that we are storing the author name and gravatar here as well. This duplication is worthwhile as it frees us from using an expensive join when requesting the latest chats - the most frequently accessed feature of the application.
The definitions of these tables are available in tables.sql in the download archive. You can execute the code in phpMyAdmin to create them. Also, when setting up the chat on your own host, remember to modify ajax.php with your MySQL database login details.
PHP
Now that we have the database in place, lets start discussing the PHP scripts that drive the chat.
The first file we are going to take a closer look at, is ajax.php. It handles the AJAX requests sent from the jQuery front end and outputs JSON formatted data.
ajax.php
require "classes/DB.class.php"; require "classes/Chat.class.php"; require "classes/ChatBase.class.php"; require "classes/ChatLine.class.php"; require "classes/ChatUser.class.php"; session_name('webchat'); session_start(); if(get_magic_quotes_gpc()){ // If magic quotes is enabled, strip the extra slashes array_walk_recursive($_GET,create_function('&$v,$k','$v = stripslashes($v);')); array_walk_recursive($_POST,create_function('&$v,$k','$v = stripslashes($v);')); } try{ // Connecting to the database DB::init($dbOptions); $response = array(); // Handling the supported actions: switch($_GET['action']){ case 'login': $response = Chat::login($_POST['name'],$_POST['email']); break; case 'checkLogged': $response = Chat::checkLogged(); break; case 'logout': $response = Chat::logout(); break; case 'submitChat': $response = Chat::submitChat($_POST['chatText']); break; case 'getUsers': $response = Chat::getUsers(); break; case 'getChats': $response = Chat::getChats($_GET['lastID']); break; default: throw new Exception('Wrong action'); } echo json_encode($response); } catch(Exception $e){ die(json_encode(array('error' => $e->getMessage()))); }
For convenience, I've used a simple switch statement to define the actions, supported by the script. These include chat submission, login/logout functionality, and actions for requesting a list of chats and online users.
All output is in the form of JSON messages (conveniently handled by jQuery), and errors are raised in the form of exceptions. The switch statement routes all requests to the appropriate static method of the Chat class, which we will discuss later in this section.
DB.class.php
class DB { private static $instance; private $MySQLi; private function __construct(array $dbOptions){ $this->MySQLi = @ new mysqli( $dbOptions['db_host'], $dbOptions['db_user'], $dbOptions['db_pass'], $dbOptions['db_name'] ); if (mysqli_connect_errno()) { throw new Exception('Database error.'); } $this->MySQLi->set_charset("utf8"); } public static function init(array $dbOptions){ if(self::$instance instanceof self){ return false; } self::$instance = new self($dbOptions); } public static function getMySQLiObject(){ return self::$instance->MySQLi; } public static function query($q){ return self::$instance->MySQLi->query($q); } public static function esc($str){ return self::$instance->MySQLi->real_escape_string(htmlspecialchars($str)); } }
The DB class is our database manager. The constructor is private, which means that no objects can be created from the outside, and the initialization is only possible from the init() static method. It takes an array with MySQL login details, and creates an instance of the class, held in the self::$instance static variable. This way we can be sure that only one connection to the database can exists in the same time.
The rest of the classes take advantage of the static query() method to communicate with the database.
ChatBase.class.php
/* This is the base class, used by both ChatLine and ChatUser */ class ChatBase{ // This constructor is used by all the chat classes: public function __construct(array $options){ foreach($options as $k=>$v){ if(isset($this->$k)){ $this->$k = $v; } } } }
This is a simple base class. It's main purpose is to define the constructor, which takes an array with parameters, and saves only the ones that are defined in the class.
ChatLine.class.php
/* Chat line is used for the chat entries */ class ChatLine extends ChatBase{ protected $text = '', $author = '', $gravatar = ''; public function save(){ DB::query(" INSERT INTO webchat_lines (author, gravatar, text) VALUES ( '".DB::esc($this->author)."', '".DB::esc($this->gravatar)."', '".DB::esc($this->text)."' )"); // Returns the MySQLi object of the DB class return DB::getMySQLiObject(); } }
Here is the ChatLine class. It extends ChatBase, so you can easily create an object of this class by providing an array with a text, author, and gravatar elements. The gravatar property contains a md5 hash of the person's email address. This is required so we can fetch the user's gravatar from gravatar.com.
This class also defines a save method, which the object to our database. As it returns the MySQLi object, contained in the DB class, you can check whether the save was successful by checking the affected_rows property (we will come back to this in the Chat class).
ChatUser.class.php
class ChatUser extends ChatBase{ protected $name = '', $gravatar = ''; public function save(){ DB::query(" INSERT INTO webchat_users (name, gravatar) VALUES ( '".DB::esc($this->name)."', '".DB::esc($this->gravatar)."' )"); return DB::getMySQLiObject(); } public function update(){ DB::query(" INSERT INTO webchat_users (name, gravatar) VALUES ( '".DB::esc($this->name)."', '".DB::esc($this->gravatar)."' ) ON DUPLICATE KEY UPDATE last_activity = NOW()"); } }
The same is also valid here. We have the name and gravatar properties (notice the protected access modifier - this means that they will be accessible in the ChatBase class, so we can set them in the constructor).
The difference is that we also have an update() method, which updates the last_activity timestamp to the current time. This shows that this person keeps a chat window open and is displayed as online in the users section.
Chat.class.php - Part 1
/* The Chat class exploses public static methods, used by ajax.php */ class Chat{ public static function login($name,$email){ if(!$name || !$email){ throw new Exception('Fill in all the required fields.'); } if(!filter_input(INPUT_POST,'email',FILTER_VALIDATE_EMAIL)){ throw new Exception('Your email is invalid.'); } // Preparing the gravatar hash: $gravatar = md5(strtolower(trim($email))); $user = new ChatUser(array( 'name' => $name, 'gravatar' => $gravatar )); // The save method returns a MySQLi object if($user->save()->affected_rows != 1){ throw new Exception('This nick is in use.'); } $_SESSION['user'] = array( 'name' => $name, 'gravatar' => $gravatar ); return array( 'status' => 1, 'name' => $name, 'gravatar' => Chat::gravatarFromHash($gravatar) ); } public static function checkLogged(){ $response = array('logged' => false); if($_SESSION['user']['name']){ $response['logged'] = true; $response['loggedAs'] = array( 'name' => $_SESSION['user']['name'], 'gravatar' => Chat::gravatarFromHash($_SESSION['user']['gravatar']) ); } return $response; } public static function logout(){ DB::query("DELETE FROM webchat_users WHERE name = '".DB::esc($_SESSION['user']['name'])."'"); $_SESSION = array(); unset($_SESSION); return array('status' => 1); }
This is where all the work gets done. Remember the switch statement in ajax.php above? It maps the supported actions with the corresponding methods from this class. Each of these methods returns an array, as it is later converted to a JSON object with the internal json_encode() function (this happens at the bottom of ajax.php).
When the user logs in, their name and gravatar get saved as elements of the $_SESSION array, and become available on consecutive requests. We will be using this to validate that the user is allowed to add chats later on.
You can also see how we are preparing the gravatar hash. This is done according to their best practices guide and ensures that if the person has configured a Gravatar, it will be properly displayed.
Chat.class.php - Part 2
public static function submitChat($chatText){ if(!$_SESSION['user']){ throw new Exception('You are not logged in'); } if(!$chatText){ throw new Exception('You haven\' entered a chat message.'); } $chat = new ChatLine(array( 'author' => $_SESSION['user']['name'], 'gravatar' => $_SESSION['user']['gravatar'], 'text' => $chatText )); // The save method returns a MySQLi object $insertID = $chat->save()->insert_id; return array( 'status' => 1, 'insertID' => $insertID ); } public static function getUsers(){ if($_SESSION['user']['name']){ $user = new ChatUser(array('name' => $_SESSION['user']['name'])); $user->update(); } // Deleting chats older than 5 minutes and users inactive for 30 seconds DB::query("DELETE FROM webchat_lines WHERE ts < SUBTIME(NOW(),'0:5:0')"); DB::query("DELETE FROM webchat_users WHERE last_activity < SUBTIME(NOW(),'0:0:30')"); $result = DB::query('SELECT * FROM webchat_users ORDER BY name ASC LIMIT 18'); $users = array(); while($user = $result->fetch_object()){ $user->gravatar = Chat::gravatarFromHash($user->gravatar,30); $users[] = $user; } return array( 'users' => $users, 'total' => DB::query('SELECT COUNT(*) as cnt FROM webchat_users')->fetch_object()->cnt ); } public static function getChats($lastID){ $lastID = (int)$lastID; $result = DB::query('SELECT * FROM webchat_lines WHERE id > '.$lastID.' ORDER BY id ASC'); $chats = array(); while($chat = $result->fetch_object()){ // Returning the GMT (UTC) time of the chat creation: $chat->time = array( 'hours' => gmdate('H',strtotime($chat->ts)), 'minutes' => gmdate('i',strtotime($chat->ts)) ); $chat->gravatar = Chat::gravatarFromHash($chat->gravatar); $chats[] = $chat; } return array('chats' => $chats); } public static function gravatarFromHash($hash, $size=23){ return 'http://www.gravatar.com/avatar/'.$hash.'?size='.$size.'&default='. urlencode('http://www.gravatar.com/avatar/ad516503a11cd5ca435acc9bb6523536?size='.$size); } }
As you will see in the next part of this tutorial, jQuery sends a getUsers() request every 15 seconds. We are using this to delete chats older than 5 minutes and inactive users from the database. We could potentially delete those records in getChats, but that is requested once every second and the extra processing time could severely impact the performance of our app.
Another thing worth noting, is that, in the getChats() method, we are using the gmdate function to output a GMT time. In the frontend, we use the hour and minute values to feed the JavaScript date object, and as a result all the times are displayed in the user's local time.
With this the first part of this tutorial is complete!
To be continued
Go to the second part of this tutorial, when we are building the slick jQuery & CSS3 interface and making it all work together.
Bootstrap Studio
The revolutionary web design tool for creating responsive websites and apps.
Learn more
This is awesome. Thank you for posting this, Martin! I can't tell you how much this has helped me understand PHP... You've got yourself a new reader! ;)
You are welcome!
I assume you are going to use a JavaScript time-interval to check for new lines ? Maybe it's interesting to use Comet ( long-polling or APE ). New lines will be pushed directly from the server to the chat client. That would be damn interesting (and saves lots op bandwidth)!
Nice tutorial so far!
In this tutorial I am using regular AJAX requests on an interval, but I have wanted to experiment with these techniques for some time.
Something I am looking forward to are the new WebSockets, which, unfortunately, only work in webkit based browsers for now.
There's actually some new js lib that allows cross browser websocket support:
Socket.IO
http://socket.io/
I heard about on the Dev Show at 5by5:
http://5by5.tv/devshow/29
Perhaps you could give Node.js a try.
Of all I tested this one works best in terms of server resources.
If tweaked a bit (some minor fixes for less loops) it could perform very well on a large scale user driven webapp.
Cheers, Martin ;)
Have you sucessfully implemented node.js? i'm halfway there.
Wow, really amazing tutorial.
Thumbs up
I love it, can be used for instant support on websites or just for fun.
Thank you again you guys are the best.
great tutorial, well done.
Awesome..thanks for sharing..
looks pretty cool. I will mess with it when the full tutorial is out. Should be well worth the read.
Great tutorial so far! Thanks for sharing!
really great tutorial..
God, I absofreakinglutely love this! I hope to make some kind of customer support thingy out of it for my web hosting business. Is the complete code included in the ZIP?
Yes, everything is included.
Amazing. Very well executed, now let us modify/add some things and share.
Excellente tutorial
This time you've outdone yourself - BEST TUT EVER!!!
Hi Martin, i have set up everything on my wamp local server but when add a user it give me the error This Nick in use.
After taking a look at all the files the class ChatUser is failing to insert data into the table webchat_users.
Any idea why this is failing. I have configured ajax.php as follows:
The ChatUser class uses a simple insert query, so I guess there is something wrong with your database. I'd suggest that you make sure that the two tables are properly created and are located in the right database.
Also you might want to set up a password for your mysql account.
make sure you create a database called dbOptions on ur server and on ajax.php placle or replace this code
$dbOptions = array(
'db_host' => 'localhost',
'db_user' => 'your username',
'db_pass' => ' your password',
'db_name' => 'dbOptions'
);
i think you are change the DB.class.php and you should not change the DB.class.php file and just enter your database information into ajax.php file
Nice looking chat but one thing struck me: "Notice that we are storing the author name and gravatar here [in the webchat_lines table] as well. This duplication is worthwhile as it frees us from using an expensive join"
I really don't think that MySQL is your bottleneck here, but the fact that you're doing one HTTP request per second per user.
There is also one more reason to choose this approach - users are purged every thirty seconds, but chats are kept for five minutes. This would normally break the join.
As for the 1 request per second, in the next part of the tutorial, you will see that the frequency of the requests is gradually decreased and falls down to one every 15 seconds depending on the activity of the chat.
Nice.
I've used one or two of your tuts in personal projects at work in the development of a companywide intranet. i've obviously changed them a lot from your code, but its all in codeigniter so can be an ass to convert.
I've been looking for a chat system for users as they want something other than personal messaging.
This would do the trick, but the conversion to codeigniter is gonna be fun!!
And possibly creating password accessible rooms to keep chats between users and not everyone!
Its just a great work, really amazing with some fancy look. good work !
cool and clean !!!
10+ !!
WOW! THANKS!!!!!!!!
nice tut...
Hey Martin,
Thanks for the awesome code. I would suggest to give the user the ability to choose the background color for his chat text. For instance, you could add a field at the login for the user to enter the color code, or choose from a drop down menu, which will reflect as a background for his own text.
Nice one though :-)
Cheers
Been looking for a simpler replacement to phpchat for ev-ver! Thanks!
Hello,
Some future features could be:
Thanks and great script =D
Yes i want it too! And its unique script!! wow you are god!
Nice tutorial.. Waiting for part 2
Wery Good Work!!!
Super-tutorialzine.
Is there any way to moderate the chat?
Thanks you are awsame!
Had a 5 minute chat session with some other users on your demo page. Just one word "AMAZING"
You are awsome!
Can the admin moderate the chat?
works and looks great
Wow, now that is a brilliant chat script. I'll definitely be having a play around with that when I get a chance.
Congrats and keep the excellent scripts coming =)
Really wonderful! Would it be possible to linkfy URLs typed in the chat?
I want to do this as well... I thought about using eregi_replace but then I just got confused...
please see this link : http://woktec.net84.net/ajax-chat.html
not work all thing but why it is?
i enter the correct :
'db_host' => 'mysql3.000webhost.com',
'db_user' => 'a3289575_chat1',
'db_pass' => 'chat123',
'db_name' => 'a3289575_chatv'
I inspected your chat installation with firebug, and it shows a parse error. It is probably because you are running it on a free host and it is possible that they modify the files behind the scenes.
just one word, awesome....
how can you get this idea?
you really wicked, man!
That's a very nice tutorial & very helpful indeed.
I think this is the most attractive looking web based chat I have ever see.
However I have some problems installing this chat program.
in my pc(localhost) that chat program is working fine after changing values in db.class.php and inserting the database.
But in my webserver(www.khantony.com) the chat program isn't working. I have done the same thing that I did with my localhost.
I think my web server do not support mysqli. Because couple of monts ago I tried make a simple database connection with mysqli but it did not work. When I changed it with mysql_connet It Worked!
previously I could see my web server information by putting phpinfo() but now they have turned it off for security reason.
I think I did not see myqli driver installed on my webserver.
I told them to install mysali, PHP:PDO etc driver but they simply denied for security reason.
If you can give me a suggestion what to do then it would be helpful. You can simply give some hints where to change from myqli to mysql_connet
Thanks
i hope someone can integrate this with facebook connect
so that instead of using temporary names..
why not use facebook names and pics as avatars?
Really wonderful! ... Thanks!!!!
How can I integrate this chat in my login system? It should work like the following. Once you've logged in you don't have to relogin for the chat, because your name is stored in the db. I'm using this login: link.
Hello I am getting database error after using the following in ajax.php
$dboptions = array(
'db_host' => 'localhost',
'db_user' => 'root',
'db_pass' => ' ',
'db_name' => 'dboptions'
);
I am testing the script on my local system and I will like to know if I need to insert values into the tables (through php MyAdmin) or leave it blank and just login.
Also, what is supposed to be in the gravatar field? (Blank or Email?)
Thanks :)
How can you insert a new line character??. like in the regular chat windows (microsoft communicator) you press Shift+Enter to get a new line character... hence you can print multiple lines at a time. How do you do this.?
Hello dear Martin, I want to save all the history chat. How can I do it ?
God Bless To You.
You can delete this lines:
DB::query("DELETE FROM webchat_lines WHERE ts < SUBTIME(NOW(),'0:5:0')");
DB::query("DELETE FROM webchat_users WHERE last_activity < SUBTIME(NOW(),'0:0:30')");
Hi,
I wanna start by thanking to Martin, this is an amazing script.
I put it online on my domain, you can see it here: Danny3 chat
I just added some code from one of your previous tutorials to align it vertically.
I have just one small problem, the comment are deleted after ~3 hours. On localhost everything is okay.
Hi ! Thanks for this tutorial but i have 1 problem if i want to log in first time the chat wont work cos "Nickname in use" what the hell ? why is this ? :/
did you manage to clear that error?
The DB structure figure is just beautiful. What tool do you use to draw it ?
Thanks a lot Martin
Is there a limit to how many users can be logged in at once? is this line what limits that:
$result = DB::query('SELECT * FROM webchat_users ORDER BY name ASC LIMIT 18');
Nice. But you must think to add a destruct method to your class DB to close connction to the database ;)
good work
hi martin nice code, It is very useful for me but how to chat with particular person only.
No need to display another person window. Quickly send me the code
hi i want to build the same application how i can add sound, when receiving new messege to apply sound.any idea.thank you a lot
Nice application!
Although i am hesitating to use the gravatar function, since in this application somebody could easily type in your name and email and pretend to be you...
Cheers!
Hello. This functions fine. Deep thanks.
On question: can I embed this function into a website where user have logged in already. Login results are save in a session variable. Is there a way to have them logged in automatically in the chat session, avoiding the login form? I have tried a lot, but did not succeed.
Thanks in advance.
Hey nice chat
I have a online mafia game whit users in it (id name etc ) and is ther a way to use the in the chat and skip that login part ???
Thx in advance
I have been searching for tutorials on the internet but i havnt come across such a PERFECTLY DONE tutorial as this one .... i am sure a lot of hard work went into the preparation of this tutorial.... you deserve more than a payment. thanks a lot for helping those who want to learn great things. only God will provide an appropriate reward for you. 1000000000 thanks to you
The chat is loading but I cant receive any messages. I can see those messages in the DB but Cant understand why isnt being displayed.
I am getting this error in the logs.
It is not safe to rely on the system's timezone settings. You are required to use the date.timezone setting or the date_default_timezone_set() function. In case you used any of those methods and you are still getting this warning, you most likely misspelled the timezone identifier. We selected 'Asia/Calcutta' for 'IST/5.0/no DST' instead in /home/earnfame/public_html/php/classes/Chat.class.php on line 125
I don't Understand why you have used three classes 'ChatUser', 'ChatLine' and 'ChatBase'. So that you could simply add the methods of 'ChatUser' and 'ChatLine' in the class Chat as static methods and this will works finely.
Please Martin, what's the magic behind separating theme in this way ?
Thank you in advance.
Thanks!
This post really helped me. I do not currently programme in PHP, so decided to re-implement this project in Java. If anyone is interested, I did a ruff implementation for deployment to Apache TomEE 1.5.1 that can be found at http://coderforever.blogspot.com/2013/03/web-chat-example-javaserver-faces-on.html?view=sidebar
Thanks again.
This is the first time i actually bother commenting on a tutorial but i feel that i must thank you so thank you very much, this tutorial was amazing.
Wow, it's awesome.
thanks you.
Dude this tutorial is awesome, and this is the first time I've ever commented on a tutorial. EVER. Thank you for this great/amazing tutorial man, helped me out alot :D
It's great! Thank you for this tutorial!
How would I set the time to read a chatline every 10 seconds? for example if 2 or more chatline get submitted at the same time for it to shown in 10 seconds each of them by order that they went in.
lines submitted at the same time:
LINE 1: HELLO
//10 seconds after show LINE 2
LINE 2: HI
//10 seconds after show LINE 3
LINE 3: GREAT
This is my dilema. Please help
Hello there,
I want to ask , how can I add play() sound or beep notification when users post their messages, and where is the proper place to add it?
Also want to know, is there possibility to play this sound on mobile (android or Iphone)?
Thank you in advance!
Every time I reload the page it deletes one comment.