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
01
|
<!DOCTYPE
html>
|
|
02
|
<html>
|
03
|
<head>
|
|
04
|
<meta http-equiv="Content-Type" content="text/html;
charset=utf-8" />
|
05
|
<title></title>
|
|
06
|
|
07
|
<!-- Loading the
jScrollPane CSS, along with the styling of the
|
|
08
|
chat
in chat.css and the rest of the page in page.css -->
|
09
|
|
|
10
|
<link rel="stylesheet" type="text/css" href="js/jScrollPane/jScrollPane.css" />
|
11
|
<link rel="stylesheet" type="text/css" href="css/page.css" />
|
12
|
<link rel="stylesheet" type="text/css" href="css/chat.css" />
|
13
|
|
|
14
|
</head>
|
15
|
|
|
16
|
<body>
|
17
|
|
|
18
|
<div id="chatContainer">
|
19
|
|
|
20
|
<div id="chatTopBar" class="rounded"></div>
|
21
|
<div id="chatLineHolder"></div>
|
|
22
|
|
23
|
<div id="chatUsers" class="rounded"></div>
|
|
24
|
<div id="chatBottomBar" class="rounded">
|
25
|
<div class="tip"></div>
|
|
26
|
|
27
|
<form id="loginForm" method="post" action="">
|
|
28
|
<input id="name" name="name" class="rounded" maxlength="16" />
|
29
|
<input id="email" name="email" class="rounded" />
|
|
30
|
<input type="submit" class="blueButton" value="Login" />
|
31
|
</form>
|
|
32
|
|
33
|
<form id="submitForm" method="post" action="">
|
|
34
|
<input id="chatText" name="chatText" class="rounded" maxlength="255" />
|
35
|
<input type="submit" class="blueButton" value="Submit" />
|
|
36
|
</form>
|
37
|
|
|
38
|
</div>
|
39
|
|
|
40
|
</div>
|
41
|
|
|
42
|
<!-- Loading
jQuery, the mousewheel plugin and jScrollPane, along with our script.js
-->
|
43
|
|
||
44
|
|||
45
|
<script src="js/jScrollPane/jquery.mousewheel.js"></script>
|
|
46
|
<script src="js/jScrollPane/jScrollPane.min.js"></script>
|
47
|
<script src="js/script.js"></script>
|
|
48
|
</body>
|
49
|
</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.
An AJAX Web Chat with PHP, MySQL and
jQuery
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.
Webchat Users Table Structure
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).
Webchat Lines Table Structure
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
01
|
require "classes/DB.class.php";
|
|
02
|
require "classes/Chat.class.php";
|
03
|
require "classes/ChatBase.class.php";
|
04
|
require "classes/ChatLine.class.php";
|
05
|
require "classes/ChatUser.class.php";
|
|
06
|
|
07
|
session_name('webchat');
|
|
08
|
session_start();
|
09
|
|
|
10
|
if(get_magic_quotes_gpc()){
|
11
|
|
|
12
|
//
If magic quotes is enabled, strip the extra slashes
|
13
|
array_walk_recursive($_GET,create_function('&$v,$k','$v
= stripslashes($v);'));
|
14
|
array_walk_recursive($_POST,create_function('&$v,$k','$v
= stripslashes($v);'));
|
15
|
}
|
|
16
|
|
17
|
try{
|
|
18
|
|
19
|
//
Connecting to the database
|
|
20
|
DB::init($dbOptions);
|
21
|
|
|
22
|
$response =
array();
|
23
|
|
|
24
|
//
Handling the supported actions:
|
25
|
|
|
26
|
switch($_GET['action']){
|
27
|
|
|
28
|
case 'login':
|
29
|
$response =
Chat::login($_POST['name'],$_POST['email']);
|
|
30
|
break;
|
31
|
|
|
32
|
case 'checkLogged':
|
33
|
$response =
Chat::checkLogged();
|
|
34
|
break;
|
35
|
|
|
36
|
case 'logout':
|
37
|
$response =
Chat::logout();
|
|
38
|
break;
|
39
|
|
|
40
|
case 'submitChat':
|
41
|
$response =
Chat::submitChat($_POST['chatText']);
|
|
42
|
break;
|
43
|
|
|
44
|
case 'getUsers':
|
45
|
$response =
Chat::getUsers();
|
|
46
|
break;
|
47
|
|
|
48
|
case 'getChats':
|
49
|
$response =
Chat::getChats($_GET['lastID']);
|
|
50
|
break;
|
51
|
|
|
52
|
default:
|
53
|
throw new Exception('Wrong
action');
|
|
54
|
}
|
55
|
|
|
56
|
echo json_encode($response);
|
57
|
}
|
|
58
|
catch(Exception $e){
|
59
|
die(json_encode(array('error' =>
$e->getMessage())));
|
|
60
|
}
|
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
01
|
class DB
{
|
|
02
|
private static $instance;
|
03
|
private $MySQLi;
|
|
04
|
|
05
|
private function __construct(array $dbOptions){
|
|
06
|
|
07
|
$this->MySQLi
= @ new mysqli( $dbOptions['db_host'],
|
|
08
|
$dbOptions['db_user'],
|
09
|
$dbOptions['db_pass'],
|
|
10
|
$dbOptions['db_name']
);
|
11
|
|
|
12
|
if (mysqli_connect_errno())
{
|
13
|
throw new Exception('Database
error.');
|
|
14
|
}
|
15
|
|
|
16
|
$this->MySQLi->set_charset("utf8");
|
17
|
}
|
|
18
|
|
19
|
public static function init(array $dbOptions){
|
|
20
|
if(self::$instance instanceof
self){
|
21
|
return false;
|
|
22
|
}
|
23
|
|
|
24
|
self::$instance =
new self($dbOptions);
|
25
|
}
|
|
26
|
|
27
|
public static function getMySQLiObject(){
|
|
28
|
return self::$instance->MySQLi;
|
29
|
}
|
|
30
|
|
31
|
public static function query($q){
|
|
32
|
return self::$instance->MySQLi->query($q);
|
33
|
}
|
|
34
|
|
35
|
public static function esc($str){
|
|
36
|
return self::$instance->MySQLi->real_escape_string(htmlspecialchars($str));
|
37
|
}
|
|
38
|
}
|
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
01
|
/* This is the base
class, used by both ChatLine and ChatUser */
|
|
02
|
|
03
|
class ChatBase{
|
|
04
|
|
05
|
//
This constructor is used by all the chat classes:
|
|
06
|
|
07
|
public function __construct(array $options){
|
|
08
|
|
09
|
foreach($options as $k=>$v){
|
|
10
|
if(isset($this->$k)){
|
11
|
$this->$k =
$v;
|
|
12
|
}
|
13
|
}
|
|
14
|
}
|
15
|
}
|
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
01
|
/* Chat line is used
for the chat entries */
|
|
02
|
|
03
|
class ChatLine
extends ChatBase{
|
|
04
|
|
05
|
protected $text =
'', $author = '', $gravatar
= '';
|
|
06
|
|
07
|
public function save(){
|
|
08
|
DB::query("
|
09
|
INSERT
INTO webchat_lines (author, gravatar, text)
|
|
10
|
VALUES
(
|
11
|
'".DB::esc($this->author)."',
|
|
12
|
'".DB::esc($this->gravatar)."',
|
13
|
'".DB::esc($this->text)."'
|
|
14
|
)");
|
15
|
|
|
16
|
//
Returns the MySQLi object of the DB class
|
17
|
|
|
18
|
return DB::getMySQLiObject();
|
19
|
}
|
|
20
|
}
|
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
01
|
class ChatUser
extends ChatBase{
|
|
02
|
|
03
|
protected $name =
'', $gravatar = '';
|
|
04
|
|
05
|
public function save(){
|
|
06
|
|
07
|
DB::query("
|
|
08
|
INSERT
INTO webchat_users (name, gravatar)
|
09
|
VALUES
(
|
|
10
|
'".DB::esc($this->name)."',
|
11
|
'".DB::esc($this->gravatar)."'
|
|
12
|
)");
|
13
|
|
|
14
|
return DB::getMySQLiObject();
|
15
|
}
|
|
16
|
|
17
|
public function update(){
|
|
18
|
DB::query("
|
19
|
INSERT
INTO webchat_users (name, gravatar)
|
|
20
|
VALUES
(
|
21
|
'".DB::esc($this->name)."',
|
|
22
|
'".DB::esc($this->gravatar)."'
|
23
|
)
ON DUPLICATE KEY UPDATE last_activity = NOW()");
|
|
24
|
}
|
25
|
}
|
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
01
|
/* The Chat class
exploses public static methods, used by ajax.php */
|
|
02
|
|
03
|
class Chat{
|
|
04
|
|
05
|
public static function login($name,$email){
|
|
06
|
if(!$name ||
!$email){
|
07
|
throw new Exception('Fill
in all the required fields.');
|
|
08
|
}
|
09
|
|
|
10
|
if(!filter_input(INPUT_POST,'email',FILTER_VALIDATE_EMAIL)){
|
11
|
throw new Exception('Your
email is invalid.');
|
|
12
|
}
|
13
|
|
|
14
|
//
Preparing the gravatar hash:
|
15
|
$gravatar =
md5(strtolower(trim($email)));
|
|
16
|
|
17
|
$user =
new ChatUser(array(
|
|
18
|
'name' => $name,
|
19
|
'gravatar' => $gravatar
|
|
20
|
));
|
21
|
|
|
22
|
//
The save method returns a MySQLi object
|
23
|
if($user->save()->affected_rows
!= 1){
|
|
24
|
throw new Exception('This
nick is in use.');
|
25
|
}
|
|
26
|
|
27
|
$_SESSION['user']
= array(
|
|
28
|
'name' => $name,
|
29
|
'gravatar' => $gravatar
|
|
30
|
);
|
31
|
|
|
32
|
return array(
|
33
|
'status' => 1,
|
|
34
|
'name' => $name,
|
35
|
'gravatar' => Chat::gravatarFromHash($gravatar)
|
|
36
|
);
|
37
|
}
|
|
38
|
|
39
|
public static function checkLogged(){
|
|
40
|
$response =
array('logged' => false);
|
41
|
|
|
42
|
if($_SESSION['user']['name']){
|
43
|
$response['logged']
= true;
|
|
44
|
$response['loggedAs']
= array(
|
45
|
'name' => $_SESSION['user']['name'],
|
|
46
|
'gravatar' =>
Chat::gravatarFromHash($_SESSION['user']['gravatar'])
|
47
|
);
|
|
48
|
}
|
49
|
|
|
50
|
return $response;
|
51
|
}
|
|
52
|
|
53
|
public static function logout(){
|
|
54
|
DB::query("DELETE
FROM webchat_users WHERE name =
'".DB::esc($_SESSION['user']['name'])."'");
|
55
|
|
|
56
|
$_SESSION =
array();
|
57
|
unset($_SESSION);
|
|
58
|
|
59
|
return array('status' =>
1);
|
|
60
|
}
|
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
01
|
public static function submitChat($chatText){
|
|
02
|
if(!$_SESSION['user']){
|
03
|
throw new Exception('You
are not logged in');
|
|
04
|
}
|
05
|
|
|
06
|
if(!$chatText){
|
07
|
throw new Exception('You
haven\' entered a chat message.');
|
|
08
|
}
|
09
|
|
|
10
|
$chat =
new ChatLine(array(
|
11
|
'author' => $_SESSION['user']['name'],
|
|
12
|
'gravatar' => $_SESSION['user']['gravatar'],
|
13
|
'text' => $chatText
|
|
14
|
));
|
15
|
|
|
16
|
//
The save method returns a MySQLi object
|
17
|
$insertID =
$chat->save()->insert_id;
|
|
18
|
|
19
|
return array(
|
|
20
|
'status' => 1,
|
21
|
'insertID' => $insertID
|
|
22
|
);
|
23
|
}
|
|
24
|
|
25
|
public static function getUsers(){
|
|
26
|
if($_SESSION['user']['name']){
|
27
|
$user =
new ChatUser(array('name'
=> $_SESSION['user']['name']));
|
|
28
|
$user->update();
|
29
|
}
|
|
30
|
|
31
|
//
Deleting chats older than 5 minutes and users inactive for 30 seconds
|
|
32
|
|
33
|
DB::query("DELETE
FROM webchat_lines WHERE ts < SUBTIME(NOW(),'0:5:0')");
|
34
|
DB::query("DELETE
FROM webchat_users WHERE last_activity < SUBTIME(NOW(),'0:0:30')");
|
35
|
|
|
36
|
$result =
DB::query('SELECT * FROM webchat_users ORDER BY name ASC LIMIT 18');
|
37
|
|
|
38
|
$users =
array();
|
39
|
while($user =
$result->fetch_object()){
|
|
40
|
$user->gravatar
= Chat::gravatarFromHash($user->gravatar,30);
|
41
|
$users[]
= $user;
|
|
42
|
}
|
43
|
|
|
44
|
return array(
|
45
|
'users' =>
$users,
|
|
46
|
'total' =>
DB::query('SELECT COUNT(*) as cnt FROM
webchat_users')->fetch_object()->cnt
|
47
|
);
|
|
48
|
}
|
49
|
|
|
50
|
public static function getChats($lastID){
|
51
|
$lastID =
(int)$lastID;
|
|
52
|
|
53
|
$result =
DB::query('SELECT * FROM webchat_lines WHERE id > '.$lastID.' ORDER BY id
ASC');
|
|
54
|
|
55
|
$chats =
array();
|
|
56
|
while($chat =
$result->fetch_object()){
|
57
|
|
|
58
|
//
Returning the GMT (UTC) time of the chat creation:
|
59
|
|
|
60
|
$chat->time
= array(
|
61
|
'hours' =>
gmdate('H',strtotime($chat->ts)),
|
|
62
|
'minutes' => gmdate('i',strtotime($chat->ts))
|
63
|
);
|
|
64
|
|
65
|
$chat->gravatar
= Chat::gravatarFromHash($chat->gravatar);
|
|
66
|
|
67
|
$chats[]
= $chat;
|
|
68
|
}
|
69
|
|
|
70
|
return array('chats' =>
$chats);
|
71
|
}
|
|
72
|
|
73
|
public static function gravatarFromHash($hash,
$size=23){
|
|
74
|
75
|
|||
76
|
}
|
||
77
|
}
|
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.