Simple php calendar with events

Welcome to a 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:

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

But just how is this done exactly? Let us walk through a step-by-step 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

Simple php calendar with events

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

QUICK NOTES

  • Create a database and import 1-database.sql.
  • Change the database settings in 2-cal-lib.php to your own.
  • That’s all – Launch 4a-cal-page.php in your browser.

If you spot a bug, feel free to comment below. I try to answer short 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.

SCREENSHOT

Simple php calendar with events

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.

All right, let us now get into the details of building a PHP MYSQL calendar.

PART 1) EVENTS DATABASE TABLE

1-database.sql

CREATE TABLE `events` (
  `evt_id` bigint(20) NOT NULL,
  `evt_start` datetime NOT NULL,
  `evt_end` datetime NOT NULL,
  `evt_text` text NOT NULL,
  `evt_color` varchar(7) NOT NULL,
  `evt_bg` varchar(7) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
 
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(20) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=1;

Let us start by dealing with the foundation of the system – A database table to save all the event entries.

Field Description
evt_id Primary key, auto-increment.
evt_start Event start date.
evt_end Event end date.
evt_text Details of the event.
evt_color Color of the text.
evt_color Background color.

PART 2) PHP CALENDAR LIBRARY

2A) INITIALIZE

2-cal-lib.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) { exit($ex->getMessage()); }}

  // (B) DESTRUCTOR - CLOSE DATABASE CONNECTION
  function __destruct () {
    if ($this->stmt!==null) { $this->stmt = null; }
    if ($this->pdo!==null) { $this->pdo = null; }
  }
 
  // (C) HELPER - RUN SQL QUERY
  function query ($sql, $data=null) {
    $this->stmt = $this->pdo->prepare($sql);
    $this->stmt->execute($data);
  }
  // ...
}

// (G) 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", "");

// (H) SUNDAY FIRST
define("SUN_FIRST", true);
 
// (I) NEW CALENDAR OBJECT
$_CAL = new Calendar();

This library looks massive at first, but keep calm and look carefully. The top and bottom sections are actually straightforward.

  • (A, B, I) When $_CAL = new Calendar() is created, the constructor connects to the database. The destructor closes the connection.
  • (C) query() is a helper function to run an SQL query.
  • (G) Remember to change the database settings to your own.
  • (H) Change SUN_FIRST to false if you want the week to start on Monday.

2B) EVENT FUNCTIONS

2-cal-lib.php

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

    // (D2) RUN SQL
    if ($id==null) {
      $sql = "INSERT INTO `events` (`evt_start`, `evt_end`, `evt_text`, `evt_color`, `evt_bg`) VALUES (?,?,?,?,?)";
      $data = [$start, $end, $txt, $color, $bg];
    } else {
      $sql = "UPDATE `events` SET `evt_start`=?, `evt_end`=?, `evt_text`=?, `evt_color`=?, `evt_bg`=? WHERE `evt_id`=?";
      $data = [$start, $end, $txt, $color, $bg, $id];
    }
    $this->query($sql, $data);
    return true;
  }

  // (E) DELETE EVENT
  function del ($id) {
    $this->query("DELETE FROM `events` WHERE `evt_id`=?", [$id]);
    return true;
  }
 
  // (F) GET DATES & EVENTS FOR SELECTED PERIOD
  function get ($month, $year) {
    // (F1) DATA STRUCTURE
    $cells = []; // "b" blank cell
                 // "d" day number
                 // "t" today
                 // "e" => [event ids]
    $events = []; // event id => data
    $map = []; // "yyyy-mm-dd" => $cells[n]

    // (F2) DATE RANGE CALCULATIONS
    $month = $month<10 ? "0$month" : $month ; $daysInMonth = cal_days_in_month(CAL_GREGORIAN, $month, $year); $dateYM = "{$year}-{$month}-"; $dateFirst = $dateYM . "01"; $dateLast = $dateYM . $daysInMonth; $dayFirst = (new DateTime($dateFirst))->format("w");
    $dayLast = (new DateTime($dateLast))->format("w");
    $nowDay = (date("n")==$month && date("Y")==$year) ? date("j") : 0 ;

    // (F3) PAD EMPTY CELLS BEFORE FIRST DAY OF MONTH
    if (SUN_FIRST) { $pad = $dayFirst; }
    else { $pad = $dayFirst==0 ? 6 : $dayFirst-1 ; }
    for ($i=0; $i<$pad; $i++) { $cells[] = ["b" => 1]; }

    // (F4) DAYS IN MONTH
    for ($day=1; $day<=$daysInMonth; $day++) {
      $i = count($cells);
      $map["$year-$month-".($day<10?"0$day":$day)] = $i; $cells[] = ["d" => $day];
      if ($day == $nowDay) { $cells[$i]["t"] = 1; }
    }

    // (F5) PAD EMPTY CELLS AFTER LAST DAY OF MONTH
    if (SUN_FIRST) { $pad = $dayLast==0 ? 6 : 6-$dayLast ; }
    else { $pad = $dayLast==0 ? 0 : 7-$dayLast ; }
    for ($i=0; $i<$pad; $i++) { $cells[] = ["b" => 1]; }

    // (F6) GET EVENTS
    $start = "$dateFirst 00:00:00";
    $end = "$dateLast 23:59:59";
    $this->query("SELECT * FROM `events` WHERE (
      (`evt_start` BETWEEN ? AND ?)
      OR (`evt_end` BETWEEN ? AND ?)
      OR (`evt_start` <= ? AND `evt_end` >= ?)
    )", [$start, $end, $start, $end, $start, $end]);

    while ($r = $this->stmt->fetch()) {
      // (F6-1) "SAVE" $EVENTS DETAILS
      $events[$r["evt_id"]] = [
        "s" => $r["evt_start"], "e" => $r["evt_end"],
        "c" => $r["evt_color"], "b" => $r["evt_bg"],
        "t" => $r["evt_text"]
      ];

      // (F6-2) "MAP" EVENTS TO $CELLS
      $start = substr($r["evt_start"], 5, 2)==$month ? (int)substr($r["evt_start"], 8, 2) : 1 ;
      $end = substr($r["evt_end"], 5, 2)==$month ? (int)substr($r["evt_end"], 8, 2) : 1 ;
      for ($d=$start; $d<=$end; $d++) {
        $eday = $dateYM . ($d<10?"0$d":$d);
        if (!isset($cells[$map[$eday]]["e"])) { $cells[$map[$eday]]["e"] = []; }
        $cells[$map[$eday]]["e"][] = $r["evt_id"];
      }
    } 
 
    // (F7) RESULTS 
    return ["s"=>SUN_FIRST, "e" => $events, "c" => $cells];
  }
}

There are only 3 “main event functions” here:

  • (D) save() Saves an event (insert or update).
  • (E) del() Deletes an event.
  • (F) get() Gets the date range and events for a selected month/year. Not going to explain line-by-line, so a quick spotlight on the returned array.
    • "s" Sunday or Monday first.
    • "e" Events in the format of [EVENT ID => EVENT DETAILS]. See (F6-1).
    • "c" The calendar cells in a “raw array form”. With flags to indicate the blank cells, day number, today, and if there are events.

PART 3) CALENDAR AJAX HANDLER

3-cal-ajax.php

<?php
if (isset($_POST["req"])) {
  // (A) LOAD LIBRARY
  require "2-cal-lib.php";
  switch ($_POST["req"]) {
    // (B) INVALID REQUEST
    default : echo "Invalid request"; break;

    // (C) GET DATES & EVENTS FOR SELECTED PERIOD
    case "get":
      echo json_encode($_CAL->get($_POST["month"], $_POST["year"]));
      break;

    // (D) SAVE EVENT
    case "save":
      echo $_CAL->save(
        $_POST["start"], $_POST["end"], $_POST["txt"], $_POST["color"], $_POST["bg"],
        isset($_POST["id"]) ? $_POST["id"] : null
      ) ? "OK" : $_CAL->error ;
      break;

    // (E) DELETE EVENT
    case "del":
      echo $_CAL->del($_POST["id"])  ? "OK" : $_CAL->error ;
      break;
  }
}

This AJAX handler pretty much “maps $_POST requests to library functions”. For example, we send $_POST["req"] = "del" and $_POST["id"] = 123 to delete an event.

PART 4) CALENDAR PAGE

4A) HTML PAGE

4a-cal-page.php

<?php
// (A) DAYS MONTHS 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");
$yearNow = date("Y"); ?>
 
<!-- (B) PERIOD SELECTOR -->
<div id="calPeriod">
  <select id="calMonth"><?php foreach ($months as $m=>$mth) {
    printf("<option value='%u'%s>%s</option>",
      $m, $m==$monthNow?" selected":"", $mth
    );
  } ?></select>
  <input id="calYear" type="number" value="<?=$yearNow?>">
  <input id="calAdd" type="button" value="+">
</div>
 
<!-- (C) CALENDAR WRAPPER -->
<div id="calWrap"></div>
 
<!-- (D) EVENT FORM -->
<div id="calForm"><form>
  <div id="evtCX">X</div>
  <input type="hidden" id="evtID">
  <label>Date Start</label>
  <input id="evtStart" type="datetime-local" required>
  <label>Date End</label>
  <input id="evtEnd" type="datetime-local" required>
  <label>Event</label>
  <textarea id="evtTxt" required></textarea>
  <label>Text Color</label>
  <input id="evtColor" type="color" value="#000000" required>
  <label>Background Color</label>
  <input id="evtBG" type="color" value="#ffdbdb" required>
  <input type="submit" id="evtSave" value="Save">
  <input type="button" id="evtDel" value="Delete">
</form></div>

Simple php calendar with events

  1. Month and year “stuff”.
  2. <div id="calPeriod"> The month and year selector.
  3. <div id="calWrap"> The calendar itself – This will be generated with Javascript.
  4. <form id="calForm"> A hidden popup form to add/update an event.

P.S. I am using native <input type="datetime-local"> and <input type="color">. Feel free to implement and use your own library if you want – React, Angular, Vue, jQuery, etc…

4B) JAVASCRIPT INITIALIZE

4b-calendar.js

var cal = {
  // (A) PROPERTIES
  events : null, // events data for current month/year
  hMth : null, hYear : null, // html month & year
  hWrap : null, // html calendar wrapper
  // html form & fields
  hFormWrap : null, hForm : null, hfID : null, 
  hfStart : null, hfEnd : null, hfTxt : null,
  hfColor : null, hfBG : null, hfDel : null,

  // (B) SUPPORT FUNCTION - AJAX FETCH
  ajax : (data, onload) => {
    // (B1) FORM DATA
    let form = new FormData();
    for (let [k,v] of Object.entries(data)) { form.append(k,v); }

    // (B2) FETCH
    fetch("3-cal-ajax.php", { method:"POST", body:form })
    .then(res=>res.text()).then(onload);
  },

  // (C) INIT CALENDAR
  init : () => {
    // (C1) GET HTML ELEMENTS
    cal.hMth = document.getElementById("calMonth");
    cal.hYear = document.getElementById("calYear");
    cal.hWrap = document.getElementById("calWrap");
    cal.hFormWrap = document.getElementById("calForm");
    cal.hForm = cal.hFormWrap.querySelector("form");
    cal.hfID = document.getElementById("evtID");
    cal.hfStart = document.getElementById("evtStart");
    cal.hfEnd = document.getElementById("evtEnd");
    cal.hfTxt = document.getElementById("evtTxt");
    cal.hfColor = document.getElementById("evtColor");
    cal.hfBG = document.getElementById("evtBG");
    cal.hfDel = document.getElementById("evtDel");

    // (C2) ATTACH CONTROLS
    cal.hMth.onchange = cal.draw;
    cal.hYear.onchange = cal.draw;
    document.getElementById("calAdd").onclick = () => { cal.show(); };
    cal.hForm.onsubmit = () => { return cal.save(); };
    document.getElementById("evtCX").onclick = () => { cal.hFormWrap.classList.remove("show"); };
    cal.hfDel.onclick = cal.del;

    // (C3) DRAW CALENDAR
    cal.draw();
  },
  // ...
};
window.onload = cal.init;

Once again, we have some “long stinky Javascript”. But look carefully, the first few sections should be self-explanatory.

  • var cal is an object that contains all the “calendar mechanics”.
  • (A & C) On window load, cal.init() will run. All it does is to get the HTML elements and “enable” them.
  • (B) cal.ajax() is a helper function to do a fetch() call to 3-cal-ajax.php.

4C) JAVASCRIPT DRAW CALENDAR

4b-calendar.js

// (D) DRAW CALENDAR
draw : () => {
  // (D1) FETCH DATA
  cal.ajax({
    req : "get", month : cal.hMth.value, year : cal.hYear.value
  }, data => {
    // (D2) "UNPACK DATA"
    data = JSON.parse(data);
    let cells = data.c, sunFirst = data.s,
       wrap, row, cell, evt, i = 0;
    cal.events = data.e;
    data = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
    if (sunFirst) { data.unshift("Sun"); }
    else { data.push("Sun"); }
 
    // (D3) "RESET" CALENDAR
    cal.hWrap.innerHTML = `<div class="calHead"></div>
    <div class="calBody">
      <div class="calRow"></div>
    </div>`;
 
    // (D4) CALENDAR HEADER - DAY NAMES
    wrap = cal.hWrap.querySelector(".calHead");
    for (let d of data) {
      cell = document.createElement("div");
      cell.className = "calCell";
      cell.innerHTML = d;
      wrap.appendChild(cell);
    }
 
    // (D5) CALENDAR BODY - INDIVIDUAL DAYS & EVENTS
    wrap = cal.hWrap.querySelector(".calBody");
    row = cal.hWrap.querySelector(".calRow");
    for (let c of cells) {
      // (D5-1) GENERATE CELL
      cell = document.createElement("div");
      cell.className = "calCell";
      if (c.t) { cell.classList.add("calToday"); }
      if (c.b) { cell.classList.add("calBlank"); }
      else {
        cell.innerHTML = `<div class="cellDate">${c.d}</div>
        <div class="cellEvt"></div>`;
      }
      row.appendChild(cell);
 
      if (c.e) {
        cell = cell.querySelector(".cellEvt");
        for (let id of c.e) {
          evt = document.createElement("div");
          evt.className = "evt";
          evt.style.color = cal.events[id]["c"];
          evt.style.background = cal.events[id]["b"];
          evt.innerHTML = cal.events[id]["t"];
          evt.onclick = () => { cal.show(id); };
          cell.appendChild(evt);
        }
      }
 
      // (D5-2) NEW ROW
      i++;
      if (i%7==0 && i!=cells.length) {
        row = document.createElement("div");
        row.className = "calRow";
        wrap.appendChild(row);
      }
    }
  });
},

The next piece of the puzzle – Where did the calendar come from? Right here. All cal.draw() does is get the calendar data from 3-cal-ajax.php and generate the HTML.

4D) JAVASCRIPT CALENDAR EVENTS

4b-calendar.js

// (E) SHOW EVENT FORM
show : id => {
  if (id) {
    cal.hfID.value = id;
    cal.hfStart.value = cal.events[id]["s"].replace(" ", "T").substring(0, 16);
    cal.hfEnd.value = cal.events[id]["e"].replace(" ", "T").substring(0, 16);
    cal.hfTxt.value = cal.events[id]["t"];
    cal.hfColor.value = cal.events[id]["c"];
    cal.hfBG.value = cal.events[id]["b"];
    cal.hfDel.style.display = "block";
  } else {
    cal.hForm.reset();
    cal.hfID.value = "";
    cal.hfDel.style.display = "none";
  }
  cal.hFormWrap.classList.add("show");
},
 
// (F) SAVE EVENT
save : () => {
  // (F1) COLLECT DATA
  var data = {
    req : "save",
    start : cal.hfStart.value,
    end : cal.hfEnd.value,
    txt : cal.hfTxt.value,
    color : cal.hfColor.value,
    bg : cal.hfBG.value
  };
  if (cal.hfID.value != "") { data.id = cal.hfID.value; }
 
  // (F2) DATE CHECK
  if (new Date(data.start) > new Date(data.end)) {
    alert("Start date cannot be later than end date!");
    return false;
  }
 
  // (F3) AJAX SAVE
  cal.ajax(data, res => { if (res == "OK") {
    cal.hFormWrap.classList.remove("show");
    cal.draw();
  } else { alert(res); }});
  return false;
},
 
// (G) DELETE EVENT
del : () => { if (confirm("Delete event?")) {
  cal.ajax({
    req : "del", id : cal.hfID.value
  }, res => { if (res == "OK") {
    cal.hFormWrap.classList.remove("show");
    cal.draw();
  } else { alert(res); }});
}}

The last few sections of the Javascript deal with calendar events.

  • cal.show() Show the add/edit event form.
  • cal.save() Save the event.
  • cal.del() Delete the event.

PART 5) PROGRESSIVE WEB APP

Simple php calendar with events

5A) HTML META HEADERS

4a-cal-page.php

<!-- ANDROID + CHROME + APPLE + WINDOWS APP -->
<meta name="mobile-web-app-capable" content="yes">
<meta name="theme-color" content="white">
<link rel="apple-touch-icon" href="icon-512.png">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="Calendar">
<meta name="msapplication-TileImage" content="icon-512.png">
<meta name="msapplication-TileColor" content="#ffffff">
 
<!-- WEB APP MANIFEST -->
<!-- https://web.dev/add-manifest/ -->
<link rel="manifest" href="5-manifest.json">
 
<!-- SERVICE WORKER -->
<script>
if ("serviceWorker" in navigator) {
  navigator.serviceWorker.register("5-worker.js");
}
</script>

The system is already complete and this is completely optional. But to turn this into a progressive web app, all it requires are 3 things:

  • Add some “web app” meta tags in the HTML head section. I am too lazy to resize the icons and set the colors properly… Change these in your own free time if you want.
  • Add a web app manifest.
  • Register a service worker.

5B) WEB MANIFEST

5-manifest.json

{
  "short_name": "Calendar",
  "name": "Calendar",
  "icons": [{
     "src": "favicon.png",
     "sizes": "64x64",
     "type": "image/png"
  }, {
    "src": "icon-512.png",
    "sizes": "512x512",
    "type": "image/png"
  }],
  "start_url": "/4a-calendar.php",
  "scope": "/",
  "background_color": "white",
  "theme_color": "white",
  "display": "standalone"
}

The manifest file is what it is, a file that contains information about your web app – App name, icons, theme, settings, etc…

5C) SERVICE WORKER

5-worker.js

// (A) FILES TO CACHE
const cName = "phpcal",
cFiles = [
  "4b-calendar.js",
  "4c-calendar.css",
  "favicon.png",
  "icon-512.png"
];

// (B) CREATE/INSTALL CACHE
self.addEventListener("install", (evt) => {
  evt.waitUntil(
    caches.open(cName)
    .then((cache) => { return cache.addAll(cFiles); })
    .catch((err) => { console.error(err) })
  );
});

// (C) LOAD FROM CACHE FIRST, FALLBACK TO NETWORK IF NOT FOUND
self.addEventListener("fetch", (evt) => {
  evt.respondWith(
    caches.match(evt.request)
    .then((res) => { return res || fetch(evt.request); })
  );
});

For those who are new – A service worker is simply Javascript that runs in the background. For this service worker:

  • (A & B) We create a browser cache and save the CSS/JS.
  • (C) Hijack the fetch requests. If the requested file is found in the cache, use it. If not, fall back to load from the network.

In other words, this service worker speeds up loading and lessens the server load.

COMPATIBILITY CHECKS

  • Arrow Function – CanIUse
  • Template Literals – CanIUse
  • Viewport Units – CanIUse
  • Add To Home Screen – CanIUse
  • Service Worker – CanIUse

This example will work on most modern “Grade A” browsers.

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:

  • 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.
  • Only one calendar instance per page.
  • Simple PHP Login Without Database – Code Boxx
  • Calendar Progress Web App – Code Boxx
  • For the people who want to share events, do mobile integration, set reminders, import/export, and all the “professional features” – That is not a “simple calendar”. Check out Google Calendar and the Calendar API instead.

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!

How to create event calendar using php?

Welcome to a tutorial on how to build an AJAX-driven events calendar with PHP and MySQL..
Create a database and import 1-database. sql ..
Change the database settings in 2-cal-lib. php to your own..
That's all – Launch 4a-cal-page. php in your browser..

How to use calendar in php?

PHP Calendar Introduction The calendar extension contains functions that simplifies converting between different calendar formats. Note: To convert between calendar formats, you must first convert to Julian Day Count, then to the calendar of your choice.

How to create a dynamic event calendar in php?

Create Database Table. To display the dynamic events in the calendar, the event data need to be stored in the database. ... .
Database Configuration (dbConfig.php) ... .
Helper Functions to Build Event Calendar (functions. ... .
Display Event Calendar (index. ... .
Testing. ... .
Conclusion..

How do I create a calendar event in HTML?

Read more in the resource calendar tutorials for JavaScript, Angular, React and Vue..
Step 1: Event Calendar JavaScript Library. Include daypilot-all. ... .
Step 2: Event Calendar Placeholder. ... .
Step 3: Initialize the Scheduler. ... .
Step 4: Load Data. ... .
Step 5: Event Moving. ... .
Step 6: Event Editing. ... .
Step 7: Apply the CSS Theme..