Create a jQuery calendar with AJAX, PHP, and MySQL

March 14, 2014
Development

Our Goal

This tutorial demonstrates how to build an interactive calendar that displays event data fetched from a JSON API. First, we will setup a MySQL database to store event data. Next, we will build a small PHP application with the Slim Framework to provide event data with a JSON API. Finally, we will create the front-end calendar with HTML and Javascript using the popular FullCalendar jQuery plugin.

The Database

We need a place to store event data. We can store data in a relational database like MySQL or PostgreSQL, in a document database like MongoDB, in a flat XML file, or anywhere else accessible to the PHP programming language. We will use a MySQL database for this tutorial.

Create the Database

Create a new database named calendar. You can do this with a web application like PHPMyAdmin. If you use the command line, log into the mysql server as a user with CREATE privileges and run this command:

CREATE DATABASE calendar;

Load the Schema

We need to define the schema for our calendar database. For the sake of time, I've included the complete schema here. You can execute this SQL with a web application like PHPMyAdmin, or pipe this SQL into the mysql command line program.

CREATE TABLE `calendar`.`events` (
    `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
    `title` VARCHAR(255),
    `description` TEXT,
    `location` TEXT,
    `contact` TEXT,
    `url` VARCHAR(255),
    `start` DATETIME,
    `end` DATETIME,
    PRIMARY KEY (`id`),
    INDEX idx_start (`start`),
    INDEX idx_end (`end`)
) ENGINE=INNODB DEFAULT CHARSET=UTF8 COLLATE=UTF8_UNICODE_CI;

Load Sample Data

We want sample data to play with. Load this SQL into your database.

INSERT INTO `calendar`.`events` (
    `title`,
    `description`,
    `location`,
    `contact`,
    `url`,
    `start`,
    `end`
) VALUES (
    'Test Event 1',
    'This is the first test event',
    'Acme Hall, Room 101',
    'John Smith',
    'http://www.example.com',
    CONCAT(DATE_FORMAT(NOW(),'%Y-%m'), '-03 13:00:00'),
    CONCAT(DATE_FORMAT(NOW(),'%Y-%m'), '-03 14:00:00')
), (
    'Test Event 2',
    'This is the second test event',
    'Venable Hall, Room 101',
    'Jane Smith',
    'http://www.example.com',
    CONCAT(DATE_FORMAT(NOW(),'%Y-%m'), '-08 09:00:00'),
    CONCAT(DATE_FORMAT(NOW(),'%Y-%m'), '-08 10:45:00')
), (
    'Test Event 3',
    'This is the third test event',
    'Sitterson Hall, Room 200',
    'Jane Smith',
    'http://www.example.com',
    CONCAT(DATE_FORMAT(NOW(),'%Y-%m'), '-17 15:00:00'),
    CONCAT(DATE_FORMAT(NOW(),'%Y-%m'), '-17 16:30:00') 
); 

The API

Now that we have our MySQL database, schema, and test data, let's build a PHP application with the Slim Framework. Our application will provide event data with a JSON API.

Setup the Slim Framework Project

First, let's create a new Slim Framework project. We'll use Composer to install the Slim Framework library, so make sure you have Composer installed on your machine. Create a new directory to contain our calendar API application; I'll refer to this as the project root directory. This directory should contain the following subdirectories and files:

composer.json
templates/
public/
    .htaccess
    index.php

What do these files mean? The composer.json file is a JSON manifest file used by Composer that lists the PHP components and dependencies used to build our application. The templates/ directory will contain our HTML template. The public/.htaccess file is used by the Apache web server to rewrite all HTTP requests to the index.php file. The index.php file will contain the Slim Framework application code.

I will leave the web server configuration up to you. Just be sure your virtual host's document root points to the project's public/ directory. You'll also want to use URL rewriting to send all HTTP requests to the public/index.php file. If you are using Apache, make sure the public/.htaccess file looks like this:

RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.php [QSA,L]

If you are using nginx, update your server configuration's location block with this code:

try_files $uri $uri/ /index.php$is_args$args;

Install Composer Dependencies

First, let's install our project dependencies with Composer. For this tutorial, we only need the Slim Framework. Open composer.json in your text editor and ensure its content is:

{
    "require": {
        "slim/slim": "2.*"
    }
}

Next, run this command from your project root directory:

composer install

You'll see some output as Composer downloads the Slim Framework library into a new vendors/ subdirectory beneath your project root directory.

Create the Slim Framework Application

Now that our project dependencies are installed, let's build the Slim Framework application. Open public/index.php in your text editor and add this PHP code:

<?php
require '../vendor/autoload.php';

$app = new \Slim\Slim(array(
    'templates.path' => '../templates'
));

$app->get('/api', function () use ($app) {
    // Fetch and display events as JSON
}); 

$app->run();

What does this code do? Let's walk through the code line by line.

require '../vendor/autoload.php';

This code instructs Composer to autoload our project dependencies. Remember the dependencies we listed above in our composer.json file? Those dependencies will be autoloaded by Composer on-demand as we use them in our application.

$app = new \Slim\Slim(array(
    'templates.path' => '../templates'
));

This code instantiates a new Slim Framework application. It also defines where our application templates are stored relative to the public/index.php file.

$app->get('/api', function () use ($app) {
     // Fetch and display events
});

This code defines a new application route and an associated callback to be invoked when the application receives a GET /api HTTP/1.1 request. We'll build out this route in the next section.

$app->run();

Finally, this code runs the Slim Framework application and routes the current HTTP request to the appropriate application route.

Query for Events

Now that we have our initial Slim Framework application, we need to build out the /api route so that it fetches the correct calendar events and returns them as JSON.

First, let's step back and think about how our front-end calendar will work. Our front-end calendar will use the FullCalendar jQuery plugin to display an interactive calendar that paginates by month. On each pagination (when the calendar month changes), the front-end calendar will send an AJAX request to our Slim Framework application with two GET query parameters: start and end. The query parameters' values will be UNIX timestamps that define the range of events to return (send me all events that start on or after start and end before end). With this in mind, let's update our Slim Framework application's /api route with this code:

$app->get('/api', function () use ($app) {
    // Get the start and end timestamps from request query parameters
    $startTimestamp = $app->request->get('start');
    $endTimestamp = $app->request->get('end');

    try {
        // Open database connection
        $conn = new \PDO('mysql:host=127.0.0.1;dbname=calendar', 'YOUR_USERNAME', 'YOUR_PASSWORD');

        // Query database for events in range
        $stmt = $conn->prepare('SELECT * FROM events WHERE start >= FROM_UNIXTIME(:start) AND end < FROM_UNIXTIME(:end) ORDER BY start ASC');
        $stmt->bindParam(':start', $startTimestamp, \PDO::PARAM_INT);
        $stmt->bindParam(':end', $endTimestamp, \PDO::PARAM_INT);
        $stmt->execute();

        // Fetch query results 
        $results = $stmt->fetchAll(\PDO::FETCH_ASSOC);

        // Return query results as JSON
        echo json_encode($results);
    } catch (\PDOException $e) {
        $app->halt(500, $e->getMessage());
   }
});

Be sure you change the database username and password shown above with your own username and password when instantiating the PDO database connection.

The Calendar

Now we'll build out the front-end calendar and bring together everything we've built so far. First, let's add a new route to our Slim Framework application. Add this code to public/index.php immediately before the last $app->run(); line of code.

$app->get('/', function () use ($app) {
    $app->render('calendar.html');
});

This code defines a new route that will draw the calendar HTML page when the Slim Framework application receives a GET / HTTP/1.1 request. Notice that we render a template named "calendar.html" in the route callback. Create this template at templates/calendar.html with this HTML content:

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8"/>
        <title>My Calendar</title>
        <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/fullcalendar/1.6.4/fullcalendar.css"/>
    </head>
    <body>
        <!-- We will attach the calendar to this element -->
        <div id="calendar"></div>

        <script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
        <script src="//cdnjs.cloudflare.com/ajax/libs/fullcalendar/1.6.4/fullcalendar.min.js"></script>
        <script>
            (function ($) { 
                $('#calendar').fullCalendar({
                    events: '/api',
                    eventDataTransform: function (rawEventData) {
                        return {
                            id: rawEventData.id,
                            title: rawEventData.title,
                            start: rawEventData.start,
                            end: rawEventData.end,
                            url: rawEventData.url
                        };
                    }
                });
            })(jQuery);
        </script>
    </body>
</html>

In the last script element, we apply the jQuery FullCalendar plugin to the div#calendar element. The fullCalendar() method accepts an object argument with an events property and an eventDataTransform property (and many other properties). The events property defines the absolute URI path (without query parameters) that will return JSON event data (this is the API we built earlier). The eventDataTransform property transforms each raw event returned from the API into the proper event schema expected by FullCalendar. The eventDataTransform property is unnecessary in this tutorial because our MySQL database schema's column names match the FullCalendar event schema. Should your database schema NOT match FullCalendar's event schema, the eventDataTransform property is how you can transform your raw event data into the FullCalendar format. For more information, read the jQuery FullCalendar documentation.

That's it. View your application in a web browser and you should have an interactive calendar with event data fetched from your Slim Framework API.

Comments

12Three Digital Agency's avatar
12Three Digital Agency
Blog names, as with any other websites, should have these characteristics so that you can drive traffic to your website:
Must contain relevant keywords, includes a maximum of 30 characters,
simple to spell, all to easy to remember, plus
it must communicate just what the whole blog is all about.
Some highly successful ones are PPC, article promotion and social networking.
This is where you want to do somewhat role use your target audience.
Joe's avatar
Joe NMC team member
@Karen
That piece of code should go in your: public/index.php. So the final public/index.php should look something like this:

<?php<br />
require '../vendor/autoload.php';

$app = new \Slim\Slim(array(
'templates.path' => '../templates'
));


$app->get('/api', function () use ($app) {
// Get the start and end timestamps from request query parameters
$startTimestamp = $app->request->get('start');
$endTimestamp = $app->request->get('end');

try {
// Open database connection
$conn = new \PDO('mysql:host=127.0.0.1;dbname=calendar', 'YOUR_USERNAME, 'YOUR_PASSWORD');

// Query database for events in range
$stmt = $conn->prepare('SELECT * FROM events WHERE start >= FROM_UNIXTIME(:start) AND end < FROM_UNIXTIME(:end) ORDER BY start ASC');
$stmt->bindParam(':start', $startTimestamp, \PDO::PARAM_INT);
$stmt->bindParam(':end', $endTimestamp, \PDO::PARAM_INT);
$stmt->execute();

// Fetch query results
$results = $stmt->fetchAll(\PDO::FETCH_ASSOC);

// Return query results as JSON
echo json_encode($results);
} catch (\PDOException $e) {
$app->halt(500, $e->getMessage());
}
});

$app->get('/', function () use ($app) {
$app->render('calendar.html');
});

$app->run();
?>
Christy's avatar
Christy
Ignore the '
' s in my previous comment, I think the text editor added those in after submission.
Christy's avatar
Christy
To maybe answer Karan's question, I think you replace:

// GET route
$app->get(
'/',
function () {
$template = <<<EOT<br />
In the vendor/slim/index.php file. So this section of code would look like:

// GET route
$app->get('/api', function () use ($app) {
// Get the start and end timestamps from request query parameters
$startTimestamp = $app->request->get('start');
$endTimestamp = $app->request->get('end');

try {
// Open database connection
$conn = new \PDO('mysql:host=127.0.0.1;dbname=calendar', 'YOUR_USERNAME', 'YOUR_PASSWORD');

// Query database for events in range
$stmt = $conn->prepare('SELECT * FROM events WHERE start >= FROM_UNIXTIME(:start) AND end < FROM_UNIXTIME(:end) ORDER BY start ASC');
$stmt->bindParam(':start', $startTimestamp, \PDO::PARAM_INT);
$stmt->bindParam(':end', $endTimestamp, \PDO::PARAM_INT);
$stmt->execute();

// Fetch query results
$results = $stmt->fetchAll(\PDO::FETCH_ASSOC);

// Return query results as JSON
echo json_encode($results);
} catch (\PDOException $e) {
$app->halt(500, $e->getMessage());
}
},
function () {
$template = <<<EOT<br />
I'm not 100% sure because I still don't have this working. I just wanted to suggest this to maybe help someone else. I know this article is a few years old at this point, but I'd still like to implement this into the project I'm currently working on as this solution seems simple and doesn't require a lot.
Jeff Riley's avatar
Jeff Riley
Has anyone done this using Web2py?
Regi's avatar
Regi
So well explained, for everybody learn how to do it. Thank you!
Hank Rausch's avatar
Hank Rausch
I am also new to this, did anyone answer Karan's question of where to put the code when you are directed to "With this in mind, let's update our Slim Framework application's /api route with this code:" I cannot find any fodler or file that corresponds to this statement.

Many thanks!
Fabrizio's avatar
Fabrizio
Very helpful tutorial mate!
I'm going to use Symfony framework and Fullcalendar plugin but the "key" is exactly the JSON interaction.
Shafiq ur Rehman's avatar
Shafiq ur Rehman
how to show description or location column on events ???
ukhan's avatar
ukhan
Sorry I didn't read through this thoroughly enough but when adding an appt/event to the calendar, can you just click an hour/day to create the appt/event?

Thanks
Camella's avatar
Camella
Time stamps are being logged into the database but all events are displayed in the calendar as "all-day." Can you go into a little more depth about how the api works?
Ting's avatar
Ting
Thanks a lot for this nice tutorial!
Could you explain more about how JSON data are used by the fullcalendar call?
Clinton's avatar
Clinton
Same issue as Tzvika here. Added 'index.php' to '/api' and it worked.
Tzvika's avatar
Tzvika
Thank you very much for this example.
One thing, at first when I tried it, it didn't work. the events didn't show up in the calendar.
I changed:
events: '/api',
to:
events: 'index.php/api',
in the calendar.html file and it solved it.

Thanks
Karan's avatar
Karan
I am new to Slim
Which file do I put this following piece of code.


$app->get('/api', function () use ($app) {
// Get the start and end timestamps from request query parameters
$startTimestamp = $app->request->get('start');
$endTimestamp = $app->request->get('end');

try {
// Open database connection
$conn = new \PDO('mysql:host=127.0.0.1;dbname=calendar', 'YOUR_USERNAME', 'YOUR_PASSWORD');

// Query database for events in range
$stmt = $conn->prepare('SELECT * FROM events WHERE start >= FROM_UNIXTIME(:start) AND end < FROM_UNIXTIME(:end) ORDER BY start ASC');
$stmt->bindParam(':start', $startTimestamp, \PDO::PARAM_INT);
$stmt->bindParam(':end', $endTimestamp, \PDO::PARAM_INT);
$stmt->execute();

// Fetch query results
$results = $stmt->fetchAll(\PDO::FETCH_ASSOC);

// Return query results as JSON
echo json_encode($results);
} catch (\PDOException $e) {
$app->halt(500, $e->getMessage());
}
});

Leave a comment