Simple Events Calendar With PHP MySQL – Free Code Download

Welcome to a quick tutorial on how to build an AJAX-driven events calendar with PHP and MySQL. Looking to add some organizational features to your project? Too many calendars out there that are too complicated?

A simple PHP MySQL calendar only consists of a few key components:

  1. A database table to store the events.
  2. A PHP class library to manage the events.
  3. Lastly, an HTML page to show the calendar of events.

But just how is this done exactly? Let us walk through an example in this guide – Read on!

ⓘ I have included a zip file with all the source code at the start of this tutorial, so you don’t have to copy-paste everything… Or if you just want to dive straight in.

 

 

TABLE OF CONTENTS

Download & Notes Events Database PHP Events Library
Calendar Page Useful Bits & Links The End

 

DOWNLOAD & NOTES

Firstly, here is the download link to the example code as promised.

 

EXAMPLE CODE DOWNLOAD

Click here to download the source code, I have released it under the MIT license, so feel free to build on top of it or use it in your own project.

 

QUICK NOTES

  • Create a database and import the 1-database.sql file.
  • Change the database settings in 2-cal-core.php to your own.
  • Want to start the week on Monday instead? Set $sunFirst = false in 3d-ajax.php.
  • That’s all – Launch 3a-calendar.php in your browser.

If you spot a bug, please feel free to comment below. I try to answer questions too, but it is one person versus the entire world… If you need answers urgently, please check out my list of websites to get help with programming.

 

 

EVENTS DATABASE

Let us start by dealing with the foundation of the system – The database. Not to worry, this is nothing but a very simple table with only 5 fields.

 

EVENTS TABLE

1-database.sql
CREATE TABLE `events` (
  `evt_id` bigint(11) NOT NULL,
  `evt_start` date NOT NULL,
  `evt_end` date NOT NULL,
  `evt_text` text NOT NULL,
  `evt_color` varchar(7) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 
ALTER TABLE `events`
  ADD PRIMARY KEY (`evt_id`),
  ADD KEY `evt_start` (`evt_start`),
  ADD KEY `evt_end` (`evt_end`);
 
ALTER TABLE `events`
  MODIFY `evt_id` bigint(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=1;
Field Description
evt_id Primary key, the event ID.
evt_start Event start date.
evt_end Event end date.
evt_text Details of the event.
evt_color Color to show on the calendar.

 

 

PHP EVENTS LIBRARY

Now that we have established the database, let us move on to the next part of the foundation – A PHP library to manage the events.

 

PHP EVENTS CLASS

2-cal-core.php
<?php
class Calendar {
  // (A) CONSTRUCTOR - CONNECT TO DATABASE
  private $pdo = null;
  private $stmt = null;
  public $error = "";
  function __construct(){
    try {
      $this->pdo = new PDO(
        "mysql:host=".DB_HOST.";dbname=".DB_NAME.";charset=".DB_CHARSET, 
        DB_USER, DB_PASSWORD, [
          PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
          PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
        ]
      );
    } catch (Exception $ex) { die($ex->getMessage()); }
  }

  // (B) DESTRUCTOR - CLOSE DATABASE CONNECTION
  function __destruct(){
    if ($this->stmt!==null) { $this->stmt = null; }
    if ($this->pdo!==null) { $this->pdo = null; }
  }

  // (C) SAVE EVENT
  function save ($start, $end, $txt, $color, $id=null) {
    // (C1) START & END DATE QUICK CHECK 
    $uStart = strtotime($start);
    $uEnd = strtotime($end);
    if ($uEnd < $uStart) { $this->error = "End date cannot be earlier than start date";
      return false;
    }

    // (C2) SQL - INSERT OR UPDATE
    if ($id==null) {
      $sql = "INSERT INTO `events` (`evt_start`, `evt_end`, `evt_text`, `evt_color`) VALUES (?,?,?,?)";
      $data = [$start, $end, $txt, $color];
    } else {
      $sql = "UPDATE `events` SET `evt_start`=?, `evt_end`=?, `evt_text`=?, `evt_color`=? WHERE `evt_id`=?";
      $data = [$start, $end, $txt, $color, $id];
    }

    // (C3) EXECUTE
    try {
      $this->stmt = $this->pdo->prepare($sql);
      $this->stmt->execute($data);
    } catch (Exception $ex) {
      $this->error = $ex->getMessage();
      return false;
    }
    return true;
  }

  // (D) DELETE EVENT
  function del ($id) {
    try {
      $this->stmt = $this->pdo->prepare("DELETE FROM `events` WHERE `evt_id`=?");
      $this->stmt->execute([$id]);
    } catch (Exception $ex) {
      $this->error = $ex->getMessage();
      return false;
    }
    return true;
  }

  // (E) GET EVENTS FOR SELECTED MONTH
  function get ($month, $year) {
    // (E1) FIST & LAST DAY OF MONTH
    $daysInMonth = cal_days_in_month(CAL_GREGORIAN, $month, $year);
    $dayFirst = "{$year}-{$month}-01";
    $dayLast = "{$year}-{$month}-{$daysInMonth}";

    // (E2) GET EVENTS
    $this->stmt = $this->pdo->prepare("SELECT * FROM `events`
    WHERE `evt_start` BETWEEN ? AND ?
    OR `evt_end` BETWEEN ? AND ?");
    $this->stmt->execute([$dayFirst, $dayLast, $dayFirst, $dayLast]);
    
    /* $events = [
     *  "e" => [
     *    EVENT ID => [DATA],
     *    EVENT ID => [DATA],
     *    ...
     *  ],
     *  "d" => [
     *    DAY => [EVENT IDS],
     *    DAY => [EVENT IDS],
     *    ...
     *  ]
     * ]
     */
    $events = ["e" => [], "d" => []];
    while ($row = $this->stmt->fetch()) {
      $eStartMonth = substr($row['evt_start'], 5, 2);
      $eEndMonth = substr($row['evt_end'], 5, 2);
      $eStartDay = $eStartMonth==$month 
                 ? (int)substr($row['evt_start'], 8, 2) 
                 : 1 ;
      $eEndDay = $eEndMonth==$month 
               ? (int)substr($row['evt_end'], 8, 2) 
               : $daysInMonth ;
      for ($d=$eStartDay; $d<=$eEndDay; $d++) {
        if (!isset($events['d'][$d])) { $events['d'][$d] = []; }
        $events['d'][$d][] = $row['evt_id'];
      }
      $events['e'][$row['evt_id']] = $row;
      $events['e'][$row['evt_id']]['first'] = $eStartDay;
    }
    return $events;
  }
}

// (F) DATABASE SETTINGS - CHANGE TO YOUR OWN!
define('DB_HOST', 'localhost');
define('DB_NAME', 'test');
define('DB_CHARSET', 'utf8');
define('DB_USER', 'root');
define('DB_PASSWORD', '');

// (G) NEW CALENDAR OBJECT
$CAL = new Calendar();

This library looks massive at first, but keep calm and look carefully.

  • When the $CAL = new Calendar() object is created, the constructor will automatically connect to the database; The destructor will close the connection.
  • There are only 3 key functions here, and all they do are literally SQL queries!
    • function save() Saves an event (add new or update existing).
    • function del() Deletes an event.
    • function get() Gets all events for a selected month. This is probably the most complicated, just spend some time to familiarize with how the events data is formatted – This will be used to draw the calendar in 3d-ajax.php.

 

 

CALENDAR PAGE

We have now established all the required foundations, and all that’s left is to build the calendar page itself.

 

CALENDAR HTML PAGE

3a-calendar.php
<!-- (A) JS + CSS -->
<link rel="stylesheet" href="3b-calendar.css">
<script src="3c-calendar.js"></script>
 
<!-- (B) PERIOD SELECTOR -->
<div id="calPeriod"><?php
  // (B1) MONTH SELECTOR
  // NOTE: DEFAULT TO CURRENT SERVER MONTH YEAR
  $months = [
    1 => "January", 2 => "Febuary", 3 => "March", 4 => "April",
    5 => "May", 6 => "June", 7 => "July", 8 => "August",
    9 => "September", 10 => "October", 11 => "November", 12 => "December"
  ];
  $monthNow = date("m");
  echo "<select id='calmonth'>";
  foreach ($months as $m=>$mth) {
    printf("<option value='%s'%s>%s</option>", 
      $m, $m==$monthNow?" selected":"", $mth
    );
  }
  echo "</select>";

  // (B2) YEAR SELECTOR
  echo "<input type='number' id='calyear' value='".date("Y")."'/>";
?></div>
 
<!-- (C) CALENDAR WRAPPER -->
<div id="calwrap"></div>
 
<!-- (D) EVENT FORM -->
<div id="calblock"><form id="calform">
  <input type="hidden" id="evtid"/> 
  <label for="start">Date Start</label>
  <input type="date" id="evtstart" required/>
  <label for="end">Date End</label>
  <input type="date" id="evtend" required/>
  <label for="txt">Event</label>
  <textarea id="evttxt" required></textarea>
  <label for="color">Color</label>
  <input type="color" id="evtcolor" value="#e4edff" required/>
  <input type="submit" id="calformsave" value="Save"/>
  <input type="button" id="calformdel" value="Delete"/>
  <input type="button" id="calformcx" value="Cancel"/>
</form></div>

There is quite a bit of code to the landing page, but this should be pretty straightforward with 4 parts:

  1. CSS and Javascript. Doh.
  2. The month and year selector.
  3. The calendar itself – That will be loaded via AJAX.
  4. A hidden popup form to add/update an event.

 

 

THE CSS

3b-calendar.css
/* (A) FONT */
html, body { font-family: arial, sans-serif; }

/* (B) PERIOD SELECTOR */
#calPeriod input, #calPeriod select {
  padding: 10px;
  font-size: 1.5em;
  border: 0;
  outline: none;
  cursor: pointer;
}
#calmonth { width: 180px; }
#calyear { width: 100px; text-align: center; }

/* (C) CALENDAR */
#calwrap {
  display: flex;
  flex-wrap: wrap;
}
.calsq {
  box-sizing: border-box;
  width: 14.28%;
  padding: 5px;
}
.calsq.head {
  color: #fff;
  background: #5250da;
  font-weight: bold;
  text-align: center;
}
.calsq.blank, .calsq.day {
  height: 120px;
  overflow-y: auto;
  border: 1px solid #eee;
}
.calsq.blank { background: #f2f2f2; }
.calsq.today { background: #fbffde; }
.calnum { color: #888; }
.calevt {
  height: 20px;
  font-size: 0.8em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.calninja { display: none; }

/* (D) EVENT FORM */
#calblock {
  position: fixed;
  top: 0; left: 0;
  z-index: 998;
  width: 100vw;
  height: 100vh;
  background: rgba(0, 0, 0, 0.5);
  transition: opacity 0.2s;
  opacity: 0;
  visibility: hidden;
}
#calblock.show {
  opacity: 1;
  visibility: visible;
}
#calform {
  z-index: 999;
  position: absolute;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  min-width: 320px;
  max-width: 400px;
  background: #fafafa;
  border: 1px solid #ccc;
  padding: 10px;
  width: 320px;
}
#calform label, #calform input, #calform textarea {
  display: block;
  box-sizing: border-box;
  width: 100%;
  padding: 5px;
  resize: none;
}
#calform label { margin-top: 10px; }
#calformsave, #calformdel, #calformcx {
  border: 0;
  color: #fff;
  cursor: pointer;
  padding: 10px 0 !important;
  margin-top: 10px;
}
#calformsave { background: #5250da; }
#calformdel, #calformcx { background: #a22a2a; }

Just some cosmetics for the calendar. Feel free to change the theme to fit your own project.

 

 

THE JAVASCRIPT

3c-calendar.js
var cal = {
  // (A) SUPPORT FUNCTION - AJAX CALL
  ajax : function (data, load) {
    let xhr = new XMLHttpRequest();
    xhr.open("POST", "3d-ajax.php");
    if (load) { xhr.onload = load; }
    xhr.send(data);
  },

  // (B) ON PAGE LOAD - ATTACH LISTENERS + DRAW
  init : function () {
    document.getElementById("calmonth").addEventListener("change", cal.draw);
    document.getElementById("calyear").addEventListener("change", cal.draw);
    document.getElementById("calformdel").addEventListener("click", cal.del);
    document.getElementById("calform").addEventListener("submit", cal.save);
    document.getElementById("calformcx").addEventListener("click", cal.hide);
    cal.draw();
  },

  // (C) DRAW CALENDAR
  draw : function () {
    // (C1) FORM DATA
    let data = new FormData();
    data.append("req", "draw");
    data.append("month", document.getElementById("calmonth").value);
    data.append("year", document.getElementById("calyear").value);

    // (C2) ATTACH CLICK TO UPDATE EVENT ON AJAX LOAD
    cal.ajax(data, function(){
      let wrapper = document.getElementById("calwrap");
      wrapper.innerHTML = this.response;
      let all = wrapper.getElementsByClassName('day');
      for (let day of all) {
        day.addEventListener("click", cal.show);
      }
      all = wrapper.getElementsByClassName('calevt');
      if (all.length != 0) { for (let evt of all) {
        evt.addEventListener("click", cal.show);
      }}
    });
  },
  
  // (D) SHOW EVENT DOCKET
  show : function (evt) {
    let eid = this.getAttribute("data-eid");
    // (D1) ADD NEW EVENT
    if (eid === null) {
      let year = document.getElementById("calyear").value,
          month = document.getElementById("calmonth").value,
          day = this.dataset.day;
      if (month.length==1) { month = "0" + month; }
      if (day.length==1) { day = "0" + day; }
      document.getElementById("calform").reset();
      document.getElementById("evtid").value = "";
      document.getElementById("evtstart").value = `${year}-${month}-${day}`;
      document.getElementById("evtend").value = `${year}-${month}-${day}`;
      document.getElementById("calformdel").style.display = "none";
    }

    // (D2) EDIT EVENT
    else {
      let edata = JSON.parse(document.getElementById("evt"+eid).innerHTML);
      document.getElementById("evtid").value = eid;
      document.getElementById("evtstart").value = edata['evt_start'];
      document.getElementById("evtend").value = edata['evt_end'];
      document.getElementById("evttxt").value = edata['evt_text'];
      document.getElementById("evtcolor").value = edata['evt_color'];
      document.getElementById("calformdel").style.display = "block";
    }

    // (D3) SHOW DOCKET
    document.getElementById("calblock").classList.add("show");
    evt.stopPropagation();
  },
  
  // (E) HIDE EVENT DOCKET
  hide : function () {
    document.getElementById("calblock").classList.remove("show");
  },
  
  // (F) SAVE EVENT
  save : function (evt) {
    // (F1) FORM DATA
    let data = new FormData(),
        eid = document.getElementById("evtid").value;
    data.append("req", "save");
    data.append("start", document.getElementById("evtstart").value);
    data.append("end", document.getElementById("evtend").value);
    data.append("txt", document.getElementById("evttxt").value);
    data.append("color", document.getElementById("evtcolor").value);
    if (eid!="") { data.append("eid", eid); }

    // (F2) AJAX SAVE
    cal.ajax(data, function(){
      if (this.response=="OK") { cal.hide(); cal.draw(); }
      else { alert(this.response); }
    });
    evt.preventDefault();
  },

  // (G) DELETE EVENT
  del : function () { if (confirm("Delete Event?")) {
    // (G1) FORM DATA
    let data = new FormData();
    data.append("req", "del");
    data.append("eid", document.getElementById("evtid").value);
    
    // (G2) AJAX DELETE
    cal.ajax(data, function(){
      if (this.response=="OK") { cal.hide(); cal.draw(); }
      else { alert(this.response); }
    });
  }}
};
window.addEventListener("DOMContentLoaded", cal.init);

All right, the Javascript is actually pretty complicated. But in essence, drives the interface with the use of AJAX.

Function Description
ajax() A helper function. Does an AJAX call to 3d-ajax.php.
init() Called when the page is fully loaded, attaches various onclick and onchange listeners.
draw() Draws the calendar for the selected month (do an AJAX call to the server).
show() When the user clicks on an empty day or existing event – Show the add/edit event form.
hide() Hide the add/edit event form.
save() Self-explanatory… Save the event.
del() Self-explanatory again. Delete an event.

 

CALENDAR AJAX HANDLER

3d-ajax.php
<?php
// (A) INVALID AJAX REQUEST
if (!isset($_POST['req'])) { die("INVALID REQUEST"); }
require "2-cal-core.php";
switch ($_POST['req']) {
  // (B) DRAW CALENDAR FOR MONTH
  case "draw":
    // (B1) DATE RANGE CALCULATIONS
    // NUMBER OF DAYS IN MONTH
    $daysInMonth = cal_days_in_month(CAL_GREGORIAN, $_POST['month'], $_POST['year']);
    // FIRST & LAST DAY OF MONTH
    $dateFirst = "{$_POST['year']}-{$_POST['month']}-01";
    $dateLast = "{$_POST['year']}-{$_POST['month']}-{$daysInMonth}";
    // DAY OF WEEK - NOTE 0 IS SUNDAY
    $dayFirst = (new DateTime($dateFirst))->format("w");
    $dayLast = (new DateTime($dateLast))->format("w");

    // (B2) DAY NAMES
    $sunFirst = true; // CHANGE THIS IF YOU WANT MON FIRST
    $days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
    if ($sunFirst) { array_unshift($days, "Sun"); }
    else { $days[] = "Sun"; }
    foreach ($days as $d) { echo "<div class='calsq head'>$d</div>"; }
    unset($days);
 
    // (B3) PAD EMPTY SQUARES BEFORE FIRST DAY OF MONTH
    if ($sunFirst) { $pad = $dayFirst; }
    else { $pad = $dayFirst==0 ? 6 : $dayFirst-1 ; }
    for ($i=0; $i<$pad; $i++) { echo "<div class='calsq blank'></div>"; }
 
    // (B4) DRAW DAYS IN MONTH
    $events = $CAL->get($_POST['month'], $_POST['year']);
    $nowMonth = date("n");
    $nowYear = date("Y");
    $nowDay = ($nowMonth==$_POST['month'] && $nowYear==$_POST['year']) ? date("j") : 0 ;
    for ($day=1; $day<=$daysInMonth; $day++) { ?>
    <div class="calsq day<?=$day==$nowDay?" today":""?>" data-day="<?=$day?>">
      <div class="calnum"><?=$day?></div>
        <?php if (isset($events['d'][$day])) { foreach ($events['d'][$day] as $eid) { ?>
        <div class="calevt" data-eid="<?=$eid?>"
             style="background:<?=$events['e'][$eid]['evt_color']?>">
          <?=$events['e'][$eid]['evt_text']?>
        </div>
        <?php if ($day == $events['e'][$eid]['first']) {
          echo "<div id='evt$eid' class='calninja'>".json_encode($events['e'][$eid])."</div>";
        }}} ?>
      </div>
    <?php }
 
    // (B5) PAD EMPTY SQUARES AFTER LAST DAY OF MONTH
    if ($sunFirst) { $pad = $dayLast==0 ? 6 : 6-$dayLast ; }
    else { $pad = $dayLast==0 ? 0 : 7-$dayLast ; }
    for ($i=0; $i<$pad; $i++) { echo "<div class='calsq blank'></div>"; }
    break;
 
  // (C) SAVE EVENT
  case "save":
    echo $CAL->save(
      $_POST['start'], $_POST['end'], $_POST['txt'], $_POST['color'],
      isset($_POST['eid']) ? $_POST['eid'] : null
    ) ? "OK" : $CAL->error ;
    break;
 
  // (D) DELETE EVENT
  case "del":
    echo $CAL->del($_POST['eid']) ? "OK" : $CAL->error ;
    break;
}

The last piece of the puzzle, the AJAX handler. Not going to explain line-by-line, but a quick summary:

  • A – Only respond to valid requests. Doh.
  • B – Generate the calendar for the specified month… Just take your time to trace through.
  • C & D – Should be easy enough to understand. Just send $_POST['req'] = "save" or $_POST['del'], followed by all the rest of the required parameters to save or delete an event.

 

USEFUL BITS & LINKS

That’s it for all the project, and here are some small extras that you may find useful.

 

LIMITATIONS

Yes, this is a fully functioning simple events calendar system. But there are plenty of improvements that you can make, plus some limitations to take note of:

  • Events do not go into hours and minutes.
  • The system is open to all users – You will want to integrate some form of user login.
  • There is no security, some encryption may be good if sensitive information is involved.

 

LINKS & REFERENCES

 

THE END

Thank you for reading, and we have come to the end of this guide. I hope that it has helped you to build a better project, and if you have anything to share with this guide, please feel free to comment below. Good luck and happy coding!

20 thoughts on “Simple Events Calendar With PHP MySQL – Free Code Download”

  1. Anette Bergman

    Hello!
    I realy like your tutorial. And I have a question.
    If I want a second calendar on the same side.
    I try to put B2,B3,B4,B5 in an css box and then make a second css box and put B2,B3,B4,B5 in there too? But it doesent seem too work.
    How shuld I think?

  2. thanks this is a great job but there is a problem in your code . assuming i have an event 1 on a given date then i make a modification in the text of event 1 and after that i try to create a new event 2 in a DIFFERENT date. the result is that event 1 will be deleted and event 2 will be created .. how to fix this problem

      1. Super ! thanks you are quick to respond.
        I want the user to just put the event date and all the events last only one day (like making an appointment) what are the modifications to be made in this code ?
        In the database the table will contain eventid, date and text that’s all.
        i think the biggest modification will be in the get()

      2. 1) 3a-calendar.php D change the end date to a hidden field.
        2) Add onchange to the start date, set end date = start date.
        3) 2-cal-core.php C, also set end date = start date.

        That’s the laziest way I can think of. The not lazy way – Remove the end date in all the scripts and database.

    1. 1) Change database evt_start and evt_end to DATETIME.
      2) Add time to the HTML interface.
      3) 2-cal-core.php – function save() needs to be updated to include time.
      4) Calculations in 2-cal-core.php – function get() needs to be totally changed to include time.

      Good luck.

  3. This is incredible, thanks! I’m wondering how to go about limiting the output to only Monday through Friday. Additionally, how to highlight the current day. I will continue to try to do it on my own but if you can illuminate me with an easy way to do both requests, that would be much appreciated!

    1. Both can be done with some updates in 3d-ajax.php, section B.

      Mon to Fri – A painful change to the current calculations. The easy way out, B4 – if (SAT OR SUN) { DRAW BLANK SQUARE } else { AS USUAL }

      Highlight today – Section B4 again.

      $monthNow = date("m");
      $yearNow = date("Y");
      $highlight = 0;
      if ($_POST['month']==$monthNow && $_POST['year']==$yearNow) { $highlight = date("d"); }
      for ($day=1; $day<=$daysInMonth; $day++) {
        if ($day==$highlight) { IS TODAY - HIGHLIGHT SQUARE }
      }
      
      1. Thank you for your response! I have not tried the highlight function yet but you are correct…a painful change to current calc. The if/else statement works just fine for the cal header…not so much for the following days. I haven’t given up yet though.

      2. I finally got around to trying to highlight “today”, but went a different (CSS) route with it. I changed the B4 section of “3d-ajax.php” = CODE SNIPPET TRUNCATED =
        I hope this helps anyone out there with the same question as mine.

      3. Thanks for sharing… But an aggressive spam filter removed most of the code. Anyway, a small addition nonetheless, script updated.

  4. Hi, thank you very much. It is awesome, but how can i change the first day in week to monday? (I want Monday on the first column) Is this possible?

  5. What changes are needed to list the table starting from Monday and not Sunday?
    I want it to be MON-SUN instead of SUN-SAT

    Thank you

Leave a Comment

Your email address will not be published. Required fields are marked *