Private Messaging System
Introduction
After much discussion and many requests, I have decided to write up a basic PM (Private Messaging) system tutorial. For those of you who have ever been a member of a forum that supported writing little messages (almost like mini-emails) to other users, this is the same type of system we will develop in this tutorial.
Before we jump right in to application design and structure, there are a couple things you need to be aware of and have in place. Obviously, if you are going to allow members to send messages to other members, you must have some sort of membership system in place. How you deal with your membership system is entirely up to you at this point, but there are some good tutorials right here on PHPFreaks. As far as I am concerned, for this tutorial, the only requirement of your membership system is that you have access to the current user's unique ID through a session variable. That being said, let's get right down to business.
This tutorial borders on the beginning to intermediate level of PHP understanding. There are a few functions and theories we will be using that may be unfamiliar to some of you beginners out there, but for learning, there's no time like the present! I will attempt to describe and spell out all the nuances of such code so that even beginning users will be able to not only understand what is happening, but they will also be able to modify and use this script on their own. Everything I've written for this example is very linear code, and should not be used as a pattern for coding practice. There are much better ways of implementing good coding practices and writing a module that can then be integrated into a membership system that will not be covered in this tutorial. Instead, I simply am attempting to walk through the principles and logic needed to have a successful PM system. Perhaps, if I can find the time, I will modify this script and write up a slightly more advanced module using full PHP 5 OOP for those more advanced users. In the meantime, let's begin.
Overview and Structure
The first thing we need to consider when planning a project such as this is what features we want included. We have limitless options at our disposal, and as with all other things PHP related, you are really only limited by your imagination. However, to keep everyone sane, we are simply going to include very basic, yet helpful features in this system. Here is a list of some of the features we will build into our PM system:
* Send new messages to other users
* Distinguish between read and unread messages in my inbox
* Delete selected messages
* View my sent messages
* Record time message was sent as well as time message was opened for logging purposes
Some of these features, along with others, we have become so accustomed to having in email systems and other places that we may not even think about the logic that is needed to implement them. That is my primary goal in this tutorial: to get you to start thinking about the nuances behind every little addition and aspect of web application programming. The more you can think of and plan out in this, the planning stage, the more headache you will save yourself down the road.
Now that we have the basic overview out of the way, let's begin to develop our database to support the features mentioned above. For the sake of time, I will not be creating an entire membership system here, however, I will be "faking" it with some session variable to simulate what you may have in your own membership system. To help simulate this system, we will generate a small users table and populate it with some generic usernames:
CODE
CREATE TABLE users (
id INT(11) AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(20) NOT NULL UNIQUE
);
INSERT INTO users (username) VALUES ('grant', 'ralph', 'carrie', 'stacy', 'gertrude', 'sam', 'steph');
Once this simple table has been created and populated, we have a base set of users off of which we can build our application. The next thing we need is our PM table. All of the features we listed above can very easily be included in one fairly simple table. Here is the table I came up with to use. As a side note, the included INSERT statements will simply give us some random entries between our primary user and others so that we have some data to work our magic on.
CODE
CREATE TABLE myPMs (
id INT(11) AUTO_INCREMENT PRIMARY KEY,
to_id INT(11) REFERENCES users (id),
from_id INT(11) REFERENCES users (id),
time_sent DATETIME NOT NULL,
subject VARCHAR(50) NOT NULL DEFAULT '',
message TEXT NOT NULL DEFAULT '',
opened CHAR(1) NOT NULL DEFAULT 'n',
time_opened DATETIME DEFAULT NULL
);
INSERT INTO myPMs (to_id, from_id, time_sent, subject, message) VALUES
(1, 2, '2006-02-14 02:34:22', 'Happy Valentines Day!', 'Just wanted to wish you a happy heart day!'),
(1, 3, '2006-04-01 08:59:45', 'April Fools!', 'You better keep an eye on your back all day long!!!'),
(2, 1, '2006-02-14 10:14:52', 'Back at ya', 'Thanks for the note... happy valentines yourself;-)'),
(1, 6, '2005-12-25 22:01:19', 'Merry Christmas!', '...and a Happy New Year, too!'),
(1, 4, '2006-09-18 16:48:02', 'Happy B-Day', 'It is your birthday, right???');
As you can see by the structure of our PM table, we only have to provide five pieces of information any time we are creating a new PM: 1) user id of the recipient, 2) user id of the sender (our session user ID), 3) the date and time it was sent (will always be NOW() from our script), 4) a subject, and finally 5) the actual body of the message. All the other columns will be handled initially by the database, and we will modify a couple of the other fields as we start marking messages as "read" and so forth.
Code Structure
Sending a Message
Since we are discussing a private messaging system, I thought it only fair to begin the coding section of the tutorial by dealing with how to send messages. If we consider what all is involved with the messaging, every aspect of reading and modifying depends upon the understanding that we have successfully generated a message in the first place. Since everything else stems from this central principle, let's delve into the details of it so we will understand how easily we can add features later. The following is a simple page that will allow a user to create a new PM. Please keep in mind that any time you see the $_SESSION['userID'] variable used, you need to replace it with the current user's unique ID from your membership system.
CODE
<?php
// Process the message once it has been sent
if (isset($_POST['newMessage'])) {
// Escape and prepare our variables for insertion into the database
// This is also where you would run any sort of editing, such as BBCode parsing
$to = mysql_real_escape_string($_POST['to']);
$from = $_SESSION['userID'];
$sub = mysql_real_escape_string($_POST['subject']);
$msg = mysql_real_escape_string($_POST['message']);
// Handle all your specific error checking here
if (empty($to) || empty($sub) || empty($msg)) {
$error = "<p>You must select a recipient and provide a subject and message.</p>\n";
} else {
// Notice carefully how we only have to provide the five values we previously discussed
$sql = "INSERT INTO myPMs (to_id, from_id, time_sent, subject, message) VALUES ('$to', '$from', NOW(), '$sub', '$msg')";
if (!mysql_query($sql)) {
$error = "<p>Could not send message!</p>\n";
} else {
$message = "<p>Message sent successfully!</p>\n";
}
}
}
echo isset($error) ? $error : '';
echo isset($message) ? $message : '';
echo "<form name=\"newMessage\" action=\"\" method=\"post\">\n";
echo "<table border=\"0\" cellpadding=\"0\" cellspacing=\"0\">\n";
echo "<tr>\n";
echo "<td>To:</td>\n";
echo "<td><select name=\"to\">\n";
echo "<option value=\"\"></option>\n";
// Collect and loop through all usernames that are not the current user
$sql = mysql_query("SELECT * FROM users WHERE id != '$_SESSION[userID]' ORDER BY username");
if (mysql_num_rows($sql) > 0) {
while ($x = mysql_fetch_assoc($sql)) echo "<option value=\"$x[id]\">$x[username]</option>\n";
}
echo "</select></td>\n";
echo "</tr>\n";
echo "<tr>\n";
echo "<td>Subject:</td>\n";
echo "<td><input type=\"text\" name=\"subject\" value=\"" . (isset($error) ? $_POST['subject'] : '') . "\" maxlength=\"50\" /></td>\n";
echo "</tr>\n";
echo "<tr>\n";
echo "<td>Message:</td>\n";
echo "<td>\n";
echo "<textarea name=\"message\" cols=\"\" rows=\"\">" . (isset($error) ? $_POST['message'] : '') . "</textarea>\n";
echo "</td>\n";
echo "</tr>\n";
echo "<tr>\n";
echo "<td></td>\n";
echo "<td><input type=\"submit\" name=\"newMessage\" value=\"Send\" /></td>\n";
echo "</tr>\n";
echo "</table>\n";
echo "</form>\n";
?>
Now, let's take just a moment and review this code, although, anyone who has done basic form processing can most likely figure out what we're doing here. First off, we're checking to see if the "newMessage" button (submit) has been pressed, and if so, we are attempting to clean the entries and submit them into the database. If this is successfull, we display a message to the user stating that fact, otherwise, we print out an error message of some sort. Although the echo statements may look a bit jumbled at first glance, the perceptive user will see that we are simply printing out the form in such a way as to remember what we have typed in our text fields if we have an error in our processing.
As I have mentioned, this is simply a study on technique and logic, not on coding practices. At this point, I highly recommend you do some research and come up with some validation techniques to protect yourself against XSS and SQL injection with this form. I have done some of that for you with my use of mysql_real_escape_string() on the variables, but you should take this a step further before ever using the script provided on a production server.
That's really all there is to sending a message! The "read" column of the database automatically defaults to 'n' showing us that the message has not been read. The "id" column is populated by the AUTO_INCREMENT attribute we added, and the "time_read" is left at NULL until we do some modification a little later in our tutorial. We now have a way for each user to send messages to any other user. Notice that since we are using our membership user ID to tell us who the message is from, we only need to worry about the logic from one direction. We never have to really think too much about things from the side of the recipient for sending anything. Now that we are able to successfully send, though, we need to come up with a way to read our incoming messages. That brings us to the next section of our tutorial.
Generating an Inbox
By default, I like to have a user see their inbox when they navigate to a PM system. In my mind, it's sort of like logging into an email application. Therefore, I want to give them any information about new messages they have received at their fingertips. Obviously, there are many different ways to go about presenting messages and data to the user, and this is where your creativity comes into play. We could list all unread messages first, followed by others, we could sort them alphabetically according to sender, or we could even let the user define how they prefer to have them displayed. For the sake of this example, I have simply chosen to list messages by time they were sent: newest first. You may recognize this as a typical default for many mail applications as well. So, with that in mind, we will first generate an inbox for our users, and then we will look at some code modifications that will allow us to see our sent messages with the same script as well.
Let's begin now. First of all, I want to explain the major differences between my sample script and your final one. I am creating a session variable defaulting to the first user in my database table. That way, I am simulating what may be seen if I were to be logged in as that user. To do this, I'm simply creating the $_SESSION['userID'] variable at the top of my page. So, here is how I have started out:
CODE
<?php
session_start();
$_SESSION['userID'] = 1;
?>
Easy enough, right? Now, keep in mind, this is done to assign the user ID variable I am using throughout the rest of the script. I can't stress enough how important it is that you replace all references to this variable with your own membership system ID variable for your final product. Now, we obviously can't do much with our table until we make a connection to the database, so let's handle that now as well:
CODE
?php
$HOST = 'localhost'; // Set to your database server
$USER = 'myDBUser'; // Set to your database user
$PASS = 'myDBPass'; // Set to your database password
$NAME = 'myDBName'; // Set to the appropriate database
$conn = mysql_connect($HOST, $USER, $PASS);
if (!$conn) die('Error connecting to server!');
mysql_select_db($NAME, $conn) or die('Error selecting database');
?>
Notice that I am using some error checking to assure that I have successfully connected to my database server and selected the database I need. One of the most frustrating things when working with scripts like this is to get everything set up and spend hours debugging because it doesn't display anything, only to find out that your initial database connection is not working properly! So, hopefully, this little error checking will help you get your connection set up accurately.
Now that we have our database connection, there are a couple things we need to do to set up for displaying our messages. First, I have written up a couple functions to help obfuscate some of the clutter when collecting the messages to be displayed. Let's look at those functions, and then we'll include them into our script. I saved the functions into a file called simply functions.php for this script:
CODE
<?php
// Returns an array of all my messages
// Defaults to current user and INBOX messages
// Make the $sent parameter "true" to retrieve sent messages
function getMyMessageList($id = '', $sent = false) {
$id = empty($id) ? $_SESSION['userID'] : $id;
$where = $sent ? "from_id = '$id'" : "to_id = '$id'";
$join = $sent ? "p.to_id = u.id" : "p.from_id = u.id";
$messages = array();
// Construct query
$sql = "SELECT p.id, to_id, from_id, time_sent, subject, message, opened,
time_opened, username FROM myPMs p LEFT JOIN users u
ON $join WHERE $where order by time_sent DESC";
$res = mysql_query($sql) or die(mysql_error());
// If there are records, populate the array to return
if (mysql_num_rows($res) > 0) {
while ($row = mysql_fetch_assoc($res)) {
$messages[] = $row;
}
}
// Return the array of messages to the caller
return $messages;
}
// Gets a specific message, but only if it corresponds to the
// provided user ID and type of message provided. Once again, we
// default to the current user and INCOMING messages.
function getMyMessage($id, $user = '', $sent = false) {
$user = empty($user) ? $_SESSION['userID'] : $user;
$where = $sent ? "from_id = '$user'" : "to_id = '$user'";
$join = $sent ? "p.to_id = u.id" : "p.from_id = u.id";
// Construct query
$sql = "SELECT p.id, to_id, from_id, time_sent, subject, message, opened,
time_opened, username FROM myPMs p LEFT JOIN users u
ON $join WHERE $where AND p.id = '$id'";
$res = mysql_query($sql);
// If there is a row found, return it as an associative array,
// otherwise, return false to show we didn't find anything.
if (mysql_num_rows($res) == 1) {
return mysql_fetch_assoc($res);
} else return false;
}
?>
As you can tell by the comments, each of these functions simply assists us in more efficiently being able to grab the proper list or single message when the time comes. Now that we have the functions written, though, we must include them in our script. In addition, there is one constant variable that we will define since it could be used in multiple locations, and we want our output to be uniform. That variable is the Date Format that we want to use to display the message times. To do both these things, we just need to add two lines of code to our main script:
CODE
<?php
require_once("functions.php"); // include the functions we just wrote
define('DATE_FORMAT', 'Y-m-d g:ia'); // define our date format variable
?>
Once these are set, we are ready to start the actual guts of our script. Preparation is wrapping up, and we are ready to start outputting some markup to display our actual messaging system. First off, as I mentioned before, we are going to attempt to use one single setup to display both our inbox and our sent messages. As such, we must do a little data gathering before we can know what we are to display. The first thing I did was to check and see if we were trying to send a new message. The following code is all we need to do that:
CODE
<?php
if (isset($_GET['action']) && $_GET['action'] == 'send') {
$title = "Send Message";
require('new.php');
exit();
}
?>
This snippet assumes that new.php is a file that contains your form and form handler we first discussed in this tutorial. That way, any time we pass the action "send" through the URL, we can expect to be able to create and send a new PM. Now that we have sending completely out of the way, we need to decide whether or not to show the inbox or our sent messages. I chose to do that, once again, by use of passing variables through the query string. In this case, I'm looking for a folder variable. If it is absent, I simply default to showing the inbox. Also, based on the folder I have chosen, I set a $title variable that will be used as the title of my HTML page.
CODE
<?php
$folder = isset($_GET['folder']) ? $_GET['folder'] : 'inbox';
switch($folder) {
case 'sent':
// Show my sent messages
$title = "Sent Messages";
// Notice we set the second parameter to "true" to pull sent messages
$myMessages = getMyMessageList('', true);
// Set the columns we will be using for our display
$cols = array('To', 'Subject', 'Time');
break;
default:
// Show our inbox
// Notice we are setting the same variables as above
$title = "Inbox";
$folder = "inbox"; // This is in case we have something errant entered
$myMessages = getMyMessageList();
$cols = array('From', 'Subject', 'Time', 'Del');
}
// This is so we know how many columns we actually have
$span = count($cols);
?>
Now that we have our display prepped, we can begin our layout. I will not go through the entire setup of DOCTYPE and all the header information here, as I will leave that up to the individual user, but if you have set your $title variable properly, you can simply echo it out now in your header like this:
CODE
<title><?php echo $title; ?> ~ PM System</title>
Here is where we start to have a few options we need to consider. The first thing I want to check is to see whether or not the user has selected a message to view. If so, we want to display the message, otherwise, we want to show the inbox or sent messages accordingly. Since we've been using the query string to pass information to this point, let's continue. Based on whether or not we have an ID passed, we will display the message
CODE
if (isset($_GET['id'])) {
$id = $_GET['id'];
switch($folder) {
case 'sent':
$msg = getMyMessage($id, '', true);
$back = "?folder=sent"; // Set my link back to Sent Messages
$from = "To";
break;
case 'inbox':
$msg = getMyMessage($id);
$back = "?folder=inbox";
$from = "From";
break;
// Obviously, if you choose, you can easily add more boxes without
// too much difficulty.
}
// Output a "back" link
echo "<p><a href=\"$back\">« Back</a></p>\n";
// If there is no message returned, we have an error
if (!$msg) {
echo "<p>Invalid message requested</p>\n";
} else {
This code really does one major thing: retrieve the message that has been selected from the database and verify its existence. The other thing that happens is simply printing out a "Back" link for them to follow to get back to the box they were previously viewing. Notice how the code uses the functions we have previously defined to grab our message. This is much better than having to query each time, is it not? Now that we have our message, we can parse out the data and display it as you see fit. Keep in mind that this is a continuation from above, so we are inside of an "if" statement.
CODE
<?php
// Define our variables (removing slashes)
// Add any other formating you like here (including BBCode, etc)
$user = stripslashes($msg['username']);
$date = date(DATE_FORMAT, strtotime($msg['time_sent'])); // Notice our defined constant
$subject = stripslashes($msg['subject']);
$message = nl2br(stripslashes($msg['message']));
$opened = $msg['opened'];
// Mark a received message "read" when it's opened
if ($msg['to_id'] == $_SESSION['userID'] && $opened == 'n') {
$sql = "UPDATE myPMs SET opened = 'y', time_opened = NOW() WHERE id = '$id'";
mysql_query($sql);
}
// Output our message
echo "<h3>$subject</h3>\n";
echo "<p>$from <b>$user</b><br />on $date</p>\n";
echo "<p>$message</p>\n";
}
} else { // They haven't chosen a message, so show the box!
?>
We now are able to display any messages we have listed in our boxes. Now that we have that part of our script under our belt, we're ready to tackle the trickiest part of the application: the boxes themselves. We need to make sure we're aware of a few things up front. First, we don't want anyone to be able to edit messages in their sent box (unless, of course, you choose to let people retract unopened messages, but that's up to you to write that mod). In the inbox, on the other hand, people need to be able to read their messages, delete messages, and do any other sort of editing/modification of messages you see fit. One other note about the message display we just finished: you may want to consider coming up with some code that would allow a user to reply to the message he is currently viewing as well.
To start out our box display, I like to show a very simplistic navigation. I always want the user to be able to switch between the Inbox and the Sent Messages, but only from the Inbox do I want them to be able to send a new message. So, to get this sort of effect, I tap into the $folder variable we set a while back.
CODE
<?php
echo "<p><a href=\"?folder=inbox\">Inbox</a> |\n";
echo "<a href=\"?folder=sent\">Sent Messages</a></p>\n";
// If we're in the inbox, show a Create link
if ($folder == 'inbox') {
echo "<p><a href=\"?action=send\">Create New Message</a></p>\n";
}
?>
Now we're ready to actually output the message list we initially gathered for the currently selected box in a readable fashion. Continue on the next page, and we'll look at one way to do this.
When displaying a message box, we need to keep in mind that we want to provide enough markup variation that the webmaster (probably you, the reader, in this case) can use basic style sheets to display messages as he sees fit. For instance, we want to specify between read and unread messages, but I am of the opinion that I want to do as little as possible with markup since each person will have different preferences on how those distinctions should be made. So, I simply create some classes to attach to the rows of my messages. For the sake of this tutorial, I'm simply using "read" and "unread" as my classes.
CODE
// You'll notice throughout this snippet that we're only displaying
// the delete messages form if we are in the inbox.
if ($folder == 'inbox') {
echo "<form name=\"deleteMessages\" action=\"\" method=\"post\">\n";
}
echo "<table border=\"0\" cellpadding=\"0\" cellspacing=\"0\">\n";
echo "<tr>\n";
// Create our headings with the column names we defined previously
echo "<th>" . implode("</th>\n<th>", $cols) . "</th>\n";
echo "</tr>\n";
// Make sure we have some messages to display
if (count($myMessages) > 0) {
// Loop through each message and display it on a row
foreach ($myMessages as $msg) {
// Determine to show the message as read or unread
$class = $msg['opened'] == 'y' ? 'read' : 'unread';
$date = date(DATE_FORMAT, strtotime($msg['time_sent']));
echo "<tr class=\"$class\">\n";
echo "<td>$msg[username]</td>\n";
// Hyperlink subject to display message
echo "<td><a href=\"?folder=$folder&id=$msg[id]\">$msg[subject]</a></td>\n";
echo "<td>$date<?td>\n";
// Checkbox to select which messages to delete
if ($folder == 'inbox') {
echo "<td><input type=\"checkbox\" name=\"del[]\" value=\"$msg[id]\" /></td>\n";
}
echo "</tr>\n";
}
// More of our delete form
// This will be our submit button to delete selected entries
if ($folder == 'inbox') {
echo "<tr class=\"deleteRow\">\n";
echo "<td colspan=\"$span\"><input type=\"submit\" name=\"delete\" value=\"Delete Selected\" /></td>\n";
echo "</tr>\n";
}
} else {
// We have no messages in this box.
echo "<tr>\n";
echo "<td colspan=\"$span\"></p>You have no messages</p></td>\n";
echo "</tr>\n";
}
echo "</table>\n";
if ($folder == 'inbox') {
echo "</form>\n";
}
} // End Script
You may have noticed that I didn't account for the delete button actually being pressed. That's one of the nuances of the code I chose to leave up to the discretion of the reader. However, if you are strapped for ideas, here's one method of handling your message deletion. Keep in mind that for best results, you need to either redirect the user back to the inbox upon successful deletion, or at the very least, make sure you do this processing before you run your getMyMessageList() function to make sure your display reflects the newly deleted messages.
CODE
if (isset($_POST['delete']) && count($_POST['del']) > 0) {
// Make sure they are only attempting to delete their own messages!!!
$sql = "DELETE FROM myPMs WHERE id IN ('" . imlode("','", $_POST['del']) . "') AND to_id = '$_SESSION[userID]'";
if (!mysql_query($sql)) {
// Could not delete selected messages
} else {
// Successfully deleted messages
}
}
Notice the usage, yet again, of the $_SESSION['userID'] variable. This access to the identity of the person viewing the page is by far the single most important element of a PM system. Without accurately identifying the user and limiting access based on this identity, your PM system will not be worth having for your users.
Conclusion
If you've made it to this point by reading through the tutorial, you hopefully have a good understanding of some of the logic and principles resting behind a solid PM system. With those principles in place, you should be able to continue on the path of this very basic system and set up additional features and/or restrictions for your users. Additionally, if you get a good grasp on the techniques and logic for this system, you could revise and clean things up an incredible amount by writing up an Object Oriented solution.
If, on the other hand, you jumped right to the end of this tutorial to see how things end, I would recommend you take this tutorial a page at a time, since much of what we discussed built on previously written code. As with all my tutorials, please feel free to contact me with questions or suggestions for improvement! Thanks for sticking this one out with me, and I hope it is of some help.
enjoy