PHP Tutorial

Step 13

PHP Tutorial

In this tutorial we will create a simple twitter copycat. In step 13 we will do some dynamic loading of tweets using Ajax and JSON.

Step 13

1. In this step we want a message to pop up whenever new tweets are available. The user can then click a link and load those tweets without reloading the entire page.

For this effect we will have to use Ajax.

With Ajax, web applications can send data to, and retrieve data from, a server asynchronously (in the background) without interfering with the display and behavior of the existing page. – Wikipedia

Data will be transmitted using JSON.

JSON, or JavaScript Object Notation, is an open standard format that uses human-readable text to transmit data objects consisting of attribute–value pairs. It is used primarily to transmit data between a server and web application, as an alternative to XML. – Wikipedia


2. We will start by creating a new page that, unlike the other pages we created this far, will output JSON instead of HTML. This pages will be stored in a folder called api. This first page will be called tweets/count_after.php and will output the number of new tweets after a certain tweet id.

First we need to create the database function that performs the query in the database/tweets.php file:

function getTweetCountAfter($id) {
  global $conn;
  $stmt = $conn->prepare("SELECT COUNT(*) AS count
                          FROM tweets
                          WHERE id > ?");
  $stmt->execute(array($id));
  $result = $stmt->fetch();    
  return $result['count'];
}

The page will then look like this:

<?php
  include_once('../../config/init.php');
  include_once($BASE_DIR .'database/tweets.php');

  $count = getTweetCountAfter($_GET['id']);  

  echo json_encode($count);
?>

The resulting JSON response will look something like this:

"12"

3. We then create a placeholder where the message regarding new tweets will appear in the templates/tweets/list.tpl template. Right before the loop that outputs the tweets:

<section id="new_tweets"></section>

4. The jQuery code for this particular function only needs to be run in pages containing tweets. We can check if our page has tweets by looking for a section with the id tweets:

$(document).ready(function() {
  initMessageClosers();
  if ($('section#tweets').length)
    initTweetChecker();
});

5. In the initTweetChecker function we need to run some code from time to time. For this, we can use the javascript setInterval function that receives a function and a interval in miliseconds. The function will then run every time the interval elapses.

function initTweetChecker() {
  setInterval(checkForNewTweets, 5000);
};

The checkForNewTweets function will make an Ajax request to the api/tweets/count_after.php page to check if there are any new tweets. For now, lets just echo the response from the server (let’s use the console.log function this time):

function checkForNewTweets() {
  $.getJSON(BASE_URL + "api/tweets/count_after.php", function(data) {
    console.log(data);
  });
}

The BASE_URL variable is not known in our javascript context. We will add it in our main.js file:

let BASE_URL = '...'; //Replace with your base URL

6. We still have to add the id parameter to our Ajax request. For this lets add the tweet id to each tweet in templates/list.tpl:

{foreach $tweets as $tweet}
<article class="tweet">
  <header>
    <span class="id">{$tweet.id}</span>
    <span class="realname">{$tweet.realname}</span>
    <a href="{$BASE_URL}pages/tweets/list_user.php?username={$tweet.username}" class="username">@{$tweet.username}</a>
    <span class="time">{$tweet.time}</span>
  </header>

  <p>{$tweet.text}</p>
</article>
{/foreach}

To calculate the id of the last tweet we just need to get the first tweet in the page and extract its id.

function checkForNewTweets() {
  let last_id = $('article.tweet:first .id').text();

  $.getJSON(BASE_URL + "api/tweets/count_after.php", {id: last_id}, function(data) {
    console.log(data);
  });
}

We can now test if adding a new tweet in another browser window changes the console output as expected.


7. When the response from the Ajax request is larger than 0, we we will add a link to the new_tweets div with information about the new tweets.

function checkForNewTweets() {
  $.getJSON(BASE_URL + "api/tweets/count_after.php", {id: last_id}, function(data) {
    if (data > 0)
      $('#new_tweets').html('New tweets (' + data + '). <a href="#">Reload</a>?');
  });
}

Now we add some styling to our div without forgetting to hide it initially.

.tweet .id{
  display: none;
}

#new_tweets {
  display: none;
  background-color: #B8ECEF;
  text-align: center;
  padding: 10px;
  color: #333;
}

#new_tweets a{
  color: blue;
  text-decoration: none;
}

This of course means we have to unhide it when new tweets arrive.

if (data > 0) {
  $('#new_tweets').html('New tweets (' + data + '). <a href="#">Reload</a>?');
  $('#new_tweets').fadeIn();
}

8. Now we have to capture the click event on the Reload link. The problem here, is that the HTML code containing the link doesn’t exist when the document.ready event is first executed. To solve this we will use the special on event. This event receives an extra selector that specifies to which child the event handling should be delegated. This way, the original event is attached to the parent element and, everytime it is triggered, it is passed to the correct child (if it exists):

In our tweets.js file lets add a new function:

function initTweetReloader() {
  $('#new_tweets').on('click', 'a', function() {
    console.log('Reload clicked');
  });
}

and call it from the document.ready event:

$(document).ready(function() {
  setInterval(checkForNewTweets, 5000);
  initTweetReloader();
});

9. Finally we have to load the new tweets using Ajax. We will start by creating a new page that retrieves the new tweets and outputs them in JSON format. This new page, api/tweets/get_after.php, will receive an id parameter containing the last tweet id known to the browser:

<?php
  include_once('../../config/init.php');
  include_once($BASE_DIR .'database/tweets.php');

  $tweets = getTweetsAfter($_GET['id']);  

  echo json_encode($tweets);
?>

The getTweetsAfter after function will be defined in the database/tweets.php file as:

function getTweetsAfter($id) {
  global $conn;
  $stmt = $conn->prepare("SELECT *
                          FROM tweets JOIN
                               users USING(username)
                          WHERE id > ?
                          ORDER BY time");
  $stmt->execute(array($id));
  return $stmt->fetchAll();
}

To loop over the returned tweets we use the jQuery $.each method. This method receives an array and a function that will be executed for each element:

function initTweetReloader() {
  $('#new_tweets').on('click', 'a', function() {
    let last_id = $('article.tweet:first .id').text();
    $.getJSON(BASE_URL + "api/tweets/get_after.php", {id: last_id}, function(data) {
      console.log(data);
      $.each(data, function(i, tweet) {
        console.log(tweet);
      })
    });
  });
}

We now just have to add the tweets to the web page. For this we will use the before manipulation method (manipulation reference). We must also hide the new_tweets div:

function initTweetReloader() {
  $('#new_tweets').on('click', 'a', function() {
    let last_id = $('article.tweet:first .id').text();
    $.getJSON(BASE_URL + "api/tweets/get_after.php", {id: last_id}, function(data) {
      $.each(data, function(i, tweet) {
        $('#tweets .tweet:first').before('<article class="tweet">' +
            '<header>' +
              '<span class="id">' + tweet.id + '</span>' +
              '<span class="realname">' + tweet.realname + '</span>' +
              '<a href="{$BASE_URL}pages/tweets/list_user.php?username=' + tweet.username +
               '" class="username">@' + tweet.username +
              '</a>' +
              '<span class="time">' + tweet.time + '</span>' +
            '</header>' +
            '<p>' + tweet.text + '</p>' +
          '</article>');
        $('#new_tweets').fadeOut();
      });
    });
  });
}

You can find the complete code after step 13 here. In step 14 we will create field specific error messages.