DEV Community

Mateusz Jasiński
Mateusz Jasiński

Posted on • Updated on

Coding DIY pt.2 - making private message system with PHP

Hello again - welcome in another Coding DIY in PHP

Today we will be making simple messaging system - like on Instagram I'll call it DM's

So, let's start our new fantastic project

Requirements

In this tutorial I assume that you are familiar with basic HTML, CSS, JavaScript, PHP and SQL syntax

You also should know how to execute PHP and SQL code with XAMPP - I won't be covering those in this tutorial

So, when we have this said, Let's go

Coding

First thing we need to do is prepare our template

I was thinking about it for a bit, but I've decided I'll use the same database and login script as in first part of this "series"

But don't worry, you can download it's code from Github repository I'll add at the end (This code will also be there)

But let's go back to our lovely DM's

Template

I'll divide this project into following files -

Now secret.php will contain textarea and input for username. Apart from that, we will have a big <section> tag with listed usernames of people with whom user chatted before. Using buttons there, we will be able to fetch data from server to iframe on the website

We will also create new files called dm_send.php and dm_receive.php

They will be responsible for (something no one expected) sending and receiving DM's

There will also be a bit of updates regarding database - but about it you will learn in specific section

So, to sum up - What will we learn?

  • How to create DM system in PHP and SQL
  • How to handle exceptions in PHP
  • How to use ternary operator in PHP
  • How to write SQL queries with RIGHT JOIN and UNION
  • How to use DISTINCT operator in SQL

And also we will sharpen our skills in PHP, SQL, HTML and JS

So, let's start - we've covered every theoretical thing needed for now

Creating Front-end

So, our first task will be to create HTML structure - place it inside <body> tag at secret.php

    <h1>Welcome on secret page - Private DM's</h1>

    <main>
        <section class = "sendMsg">
            <article class = "conversation">

            </article>
            <form method="POST" action="dm_send.php">

            </form>
        </section>
        <section class = "friends">
            <ul class = "friends-list">

            </ul>
        </section>
    </main>
Enter fullscreen mode Exit fullscreen mode

Next thing is creating actual form fields - new content of <form> tag

                <label> To:
                <input type="text" name="to" placeholder="To:"  class = "form-field"/></label>
                <br/><br/>
                <label> Content:<br/>
                <textarea id="" cols="30" rows="5" maxlength ="249" name="message"></textarea><br/></label>
                <br/>
                <button type="submit" class = "btn-submit">Send</button>   
Enter fullscreen mode Exit fullscreen mode

Then, I want to add some, let's say, 'placeholder' for friends-list ul

<ul class = "friends-list">
    <li><button class="friend">Friend 1</button></li>
    <li><button class="friend">Friend 2</button></li>
    <li><button class="friend">Friend 3</button></li>
</ul>
Enter fullscreen mode Exit fullscreen mode

Now, we need a last part of creating template - styles

I won't be styling it a lot - Just positioning. It's not a CSS course - but it doesn't mean we don't need it

Create new file called dm.css - link it inside secret's <head> tag

 <link rel="stylesheet" href="dm.css" />
Enter fullscreen mode Exit fullscreen mode

Then, let's align that HTML skeleton :)

main{
    display: flex;
    width: 94%;
    justify-content: center;
    padding: 3%;
    height: 100%;
}

.sendMsg{
    width: 50%;
    text-align: center;
}

.friends{
    width: 50%;
}

.conversation{
    height: 50%;
}

.friend{
    background-color: #fff;
    border: none;    
}

.friend:hover{
    color: #333;
}
Enter fullscreen mode Exit fullscreen mode

I've also edited buttons for friends - so they look like normal styled links, but their semantic role stays the same

I think that's it for now - second part is updating our database

Database update

Get to http://localhost/phpmyadmin

We need a new table - I'll call it messages and it will contain 5 columns

  1. id - It's our primary key in int format
  2. content - As it says, type text or varchar
  3. destination - id of a user to whom is message directed as int
  4. sender - id of a sender
  5. timestamp - when was message sent. I'll use datetime format here

After all our table looks like this

Database structure

Here is the query for creating this table

CREATE TABLE `login-form`.`messages` ( `id` INT NOT NULL AUTO_INCREMENT , `content` TEXT NOT NULL , `to` INT NOT NULL , `from` INT NOT NULL , `timestamp` DATETIME NOT NULL , PRIMARY KEY (`id`)) ENGINE = InnoDB; 
Enter fullscreen mode Exit fullscreen mode

whole database dump will also be available in mentioned Github repository

This table will stay empty - We will fill it in the future

Let's leave phpmyadmin and focus on the most important part for us

Creating sending script

We should start our work on creating sending script - create and open dm_send.php

To begin with, we need to check if user is even logged in

<?php

session_start();

if(!isset($_SESSION['isLoged'])){
    header("Location: index.php");
    exit();
}
Enter fullscreen mode Exit fullscreen mode

Then, we need to escape some dangerous characters from content and save it to a variable - never trust data coming from users

$to = htmlentities($_POST['to'], ENT_QUOTES, "UTF-8");
$content = htmlentities($_POST['message'], ENT_HTML5, "UTF-8");
Enter fullscreen mode Exit fullscreen mode

I've used ENT_QUOTES in $to variable to avoid eventual conflicts when comparing usernames

I think you know, that sending message to non-existing user has no sense - let's check it

We need to make some changes in login.php - I want to extract database credentials and dsn to separate file, and require it every time I need to use it - in my code it's named creds.php

Then, in both login.php and dm_send.php import that file

require_once "creds.php";
Enter fullscreen mode Exit fullscreen mode

Also for login.php I'd like to get our uid and login - right before isLoged session variable add

$_SESSION['uid'] = $result['id'];
$_SESSION['login'] = $result['login'];
Enter fullscreen mode Exit fullscreen mode

Okay, when it's ready we can run a query, and write that condition

$db = new PDO($db_dsn, $db_user, $db_pass);
$sql = "SELECT * FROM `users` WHERE `login` = ?";

$stmt = $db->prepare($sql);
$stmt->execute([$to]);

if(!$stmt->rowCount()){
    $_SESSION['err'] = "That user does not exist";
    header("Location: secret.php");
    exit();
}
Enter fullscreen mode Exit fullscreen mode

That's actually only condition we need to check - now let's instead of login, put user's id inside that database

We should start with writing a query - that's simple SELECT

$sql = "SELECT `id` FROM `users` WHERE `login` = ?";
Enter fullscreen mode Exit fullscreen mode

Next - execute it

$stmt = $db->prepare($sql);
$stmt->execute([$to]);
Enter fullscreen mode Exit fullscreen mode

At this point we just need to fetc that friend's id

$fid = $stmt->fetch(PDO::FETCH_ASSOC)['id'];
Enter fullscreen mode Exit fullscreen mode

The most important part is left - sending actual message

First - prepare a insert query

$sql = "INSERT INTO `messages` VALUES (?????)";
Enter fullscreen mode Exit fullscreen mode

Then - secure yourself from unwanted errors with exceptions

try{

}catch(PDOException $e){
    $_SESSION['err'] = "Server error - please try again later";
    header("Location: index.php");
    exit();
}
Enter fullscreen mode Exit fullscreen mode

That's common implementation of exceptions in PHP

PDO is really clever - If something goes wrong, it will tell us. We just need to catch it

We don't have to share with user technical aspects of that bug - just tell something wrong happened and if we need to, we can log it. But here I'll skip this part

Now - prepare and execute written query inside try block

    $stmt = $db->prepare($sql);
    $stmt->execute([NULL, $content, $fid, $_SESSION['uid'], date("Y-m-d H:i:s")]);
    $_SESSION['err'] = "Message sent";

    header('Location: secret.php');
Enter fullscreen mode Exit fullscreen mode

Last thing we need to do is to add error handling in secret.php

Under submit button I'll add

  1. Check if err variable is set
  2. If true, show it's content to the user
  3. Unset that variable
<br/>
<?php 
if(isset($_SESSION['err'])){
    echo $_SESSION['err'];
    unset($_SESSION['err']);
}
?>
Enter fullscreen mode Exit fullscreen mode

Our sending is ready - now we need to code script for receiving messages

Receiving messages

When we can send some texts, we should also be able to receive them - create new file called dm_receive.php

First thing we need it uid - check if it's even present

<?php 
session_start();
if(!isset($_SESSION['uid'])){
    echo "Server error - no user id available";
    exit();
}
Enter fullscreen mode Exit fullscreen mode

Then, when we have it, I can explain how it will work

  1. JavaScript callback put in setInterval function will request from server refresh of the iframe element on secret.php. As GET parameter we will add login of another account.
  2. Server acknowledges request, and executes query and displays new content of the message.
  3. Messages window is refreshed

And in case we want to change the person, we will just update src in iframe with new login

This may sound complicated but don't worry - writing it's code is easier

First - assign friend's login and user's uid to variables.

$uid = $_SESSION['uid'];
$friend = htmlentities($_GET['friend'], ENT_QUOTES, "UTF-8");
$isValid = true;
Enter fullscreen mode Exit fullscreen mode

And let's start executing queries
In first one - check if friend even exist (this sounds a bit schizophrenic actually). If yes, then get their userid

try{
    require_once "creds.php";
    $db = new PDO($db_dsn, $db_user, $db_pass);

    $sql = "SELECT `id` FROM `users` WHERE `login` = ? OR `login` = ?";
    $stmt = $db->prepare($sql);
    $stmt->execute([$friend, $uid]);

    if(!$stmt->rowCount()){
        echo "This user does not exist";
       $isValid = false;
    }

    $fid = $stmt->fetch(PDO::FETCH_ASSOC)['id'];
}catch(PDOException $e){
    echo "Internal server error - try again";
}
Enter fullscreen mode Exit fullscreen mode

An if we are sure that we are not talking to any imaginary friend - let's fetch the messages

Our most important step is to write a query - without it we won't do anything - It'll be a bit harder

 $sql = "SELECT * FROM `messages` WHERE (`destination` = ? AND `sender` = ?) OR (`destination`= ? AND `sender` = ?)  LIMIT 15";

Enter fullscreen mode Exit fullscreen mode

At the and I've added LIMIT 15 - This indicates that I want only 15 newest results

Now, execute it

    $stmt = $db->prepare($sql);
    $stmt->execute([$fid, $uid, $uid, $fid]);
Enter fullscreen mode Exit fullscreen mode

And fetch the results into array

$messages =  $stmt->fetchAll(PDO::FETCH_ASSOC);
Enter fullscreen mode Exit fullscreen mode

I've used fetchAll to be perfectly sure, that this will give me all of the results and not only the first one

Lastly we need to show it in some clever form - I've got a idea

foreach($messages as $message){
        $datetime = $message['timestamp'];
        $content = $message['content'];
        $sender = $message['sender'] === $fid ? $friend : $_SESSION['login'];


        echo<<<END
            $content 
            <br/>
            Sent at $datetime
            <br/><br/>
        END;
    }
Enter fullscreen mode Exit fullscreen mode

I'll focus on this one line

$sender = $message['sender'] === $fid ? $friend : $_SESSION['login'];
Enter fullscreen mode Exit fullscreen mode

Here, I've used ternary operator - that's short form of if-else statement. That line basically means

" If value under sender key in $message array is equal to $fid then assign $friend variables value to $sender. Otherwise assign value from $_SESSION['login'] there "

Finally after I entered dm_receive.php I've seen this

Final result

Note: For tests I've sent a bit of messages before, using our script. You can do this too if you want. I've also added one new user for realism - as I said whole dump will be available on Github

Now, let's go back to secret.php - that's the last part

Merging everything together

When we go back to secret.php we need to add 2 things

  1. iframe element, but as we will be adding it with JavaScript, some info for people with it disabled needs to be present. I'll use noscript tag for it

  2. List of friends.

We can start with creating that list - you can get rid of the placehoder inside ul

Creating list of friends

Most here is to run the query but select only unique items - that's how DISTINCT in SQL work

So, let's write the query, this one will be a bit complicated - of course we need to put it in php tags. Don't forget closing one too

<?php
$sql = "SELECT DISTINCT `users`.`login` 
        FROM `messages` 
        RIGHT JOIN `users` ON `users`.`id` = `messages`.`sender` 
        WHERE `destination` = ?
        UNION
        SELECT DISTINCT `users`.`login`
        FROM `messages`
        RIGHT JOIN `users` ON `users`.`id`=`messages`.`destination`
        WHERE `sender` = ?
";
Enter fullscreen mode Exit fullscreen mode

Let's explain it a bit, as we used 3 new things here, just to send less queries to database - we are sending only one

  1. UNION - This operator is used to combine results of 2 SELECT statements. So here we will have combined results from records where logged user is either sender or receiver of the message - instead of 2 queries we have 1. How cool is this

  2. RIGHT JOIN - It's one of SQL JOIN types. RIGHT means that second table (Here users) is more important than the first one.

What does it mean?

It means, that if we have a record from first table (messages) that doesn't match anything from the second table (users) then it will be skipped - that's exactly what we need

  1. DISTINCT - that's an operator used to select only unique items in the set - so if we would have 2 messages from the same user to us this operator will choose only the first one, skipping second.

Okay, this is covered - let's execute the query

require_once "creds.php";
try{
    $db = new PDO($db_dsn, $db_user, $db_pass);
    $stmt = $db->prepare($sql);
    $stmt->execute([$_SESSION['uid'], $_SESSION['uid']]);

    $result = $stmt->fetchAll(PDO::FETCH_ASSOC);
}catch(PDOException $e){ 
    echo "Server error - Can't fetch friends </br>".$e;
}
?>
Enter fullscreen mode Exit fullscreen mode

Last part is to show it - set buttons name attribute as login from database

Like this

foreach($result as $user){
    echo '<li><button class="friend" name = '.$user['login'].'>'.$user['login'].'</button></li>';
}
Enter fullscreen mode Exit fullscreen mode

And we have it ready - now let's display it on secret.php

Creating iframe with messages

First put noscript inside conversation article tag
We need to be sure that user knows why there are no messages displayed

<noscript>Can't load iframe with messages - enable your JavaScript and refresh the page</noscript>
Enter fullscreen mode Exit fullscreen mode

Now, when we have this secured. Let's write JavaScript

Create a file called conversation.js and link it at the bottom of secret.php right before </body>

<script src ="conversation.js"></script>
Enter fullscreen mode Exit fullscreen mode

Now open that file and start writing

First, we need to get every button previously made into array of objects

const friendListButtons = document.querySelectorAll('.friend');
Enter fullscreen mode Exit fullscreen mode

Then let's determine which conversation is active

let activeConversation = friendListButtons[0].name
Enter fullscreen mode Exit fullscreen mode

We also need a function to generate our iframe

const createIframe = (conversation) =>{
    document.querySelector('.conversation')
    .innerHTML = 
    `<iframe src="dm_receive.php?friend=${conversation}" frameborder="0"></iframe>`

}

Enter fullscreen mode Exit fullscreen mode

Now, the most important part - to each button add a event listener that will change activeConversation variable and regenerate iframe

friendListButtons.forEach((el)=>{
    el.addEventListener("click", ()=>{
        activeConversation = el.name
        createIframe(activeConversation);
    })
})
Enter fullscreen mode Exit fullscreen mode

Last part - let's refresh this iframe each 10 seconds

createIframe(activeConversation)
setInterval(()=>{createIframe(activeConversation)}, "10000")
Enter fullscreen mode Exit fullscreen mode

I've also added one call for that function - so we don't have to wait at the beginning just to get the first conversation

And that's it - Congratulations, you've successfully created DM system in PHP and learned a lot of new things

Conclusion and some comments from the author

This was (I think) the longest article I've ever written - and I really enjoyed it

I've had a bit of false start with this article (Instead of save draft I've clicked `Publish) but here is the finished version

Let me know what do you think - If there are any improvements I could make, feel free to suggest them in comments here or on Github

Speaking of Github here is the link for the repository (I won't be mad if you give me a star or a follow too ;))

Every account here has the same password - MyCoolPassword

What should I code next? Share your ideas and feedback here on dev.to and check out my other articles

Bye and see you soon

Top comments (0)