Note – I corrected this code in July 2021, because the original was unaware that some months span six Monday-to-Sunday weeks. The setDates() function is the one that needed to be corrected.
Further note – I decided to use PHP’s relative date calculations to simplify the code – once again, the setDates() function has been updated below.
I needed to build a monthly events calendar to display on one of my WP sites. I’d been using The Events Calendar plugin, which had served me well, but a recent new version had caused some issues, so I started looking at alternatives. To be fair, even with its problems, The Events Calendar was still better than the others for what I needed.
But the plugin was a heavyweight solution for my requirement, so I set about seeing if I could develop something of my own. One of the primary challenges was to generate a monthly calendar, ideally one with responsive markup.
There were many examples on the web, which were interesting enough, but most of them used HTML tables, so I tried a slightly different approach, and I’m happy with the solution I devised.
Here’s the PHP class for the calendar:
class myCalendar {
private $curr_date, $prev_date, $next_date;
private $first_cal_day, $last_cal_day, $num_weeks, $has_events;
public function __construct($year, $month) {
$this->setDates($year, $month);
}
public function setDates($year, $month) {
$this->curr_date = new DateTime();
$this->curr_date->setDate($year, $month, 1);
// Last day of previous month
$this->prev_date = new DateTime();
$this->prev_date->setDate($year, $month, 0);
// First day of next month
$this->next_date = new DateTime();
$this->next_date->setDate($year, $month + 1, 1);
// Get first Monday to appear on calendar
$this->first_cal_day = new DateTime();
$this->first_cal_day->setDate($year, $month, 2)->modify('previous Monday');
// Get last Sunday to appear on calendar
$this->last_cal_day = new DateTime();
$this->last_cal_day->setDate($year, $month + 1, -1)->modify('first Sunday');
// Calculate number of weeks
$diff = $this->first_cal_day->diff($this->last_cal_day);
$this->num_weeks = ceil ($diff->days / 7);
}
public function get_date() {
return $this->curr_date;
}
public function get_prevdate() {
return $this->prev_date;
}
public function get_nextdate() {
return $this->next_date;
}
public function day_names($format = 'abbr') {
switch ($format) {
case 'abbr':
return ['Mon','Tue','Wed','Thu','Fri','Sat','Sun'];
break;
case 'full':
return ['Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday'];
break;
default:
return ['M','T','W','T','F','S','S'];
break;
}
}
public function getCalendar($with_events = true) {
if ($with_events) {
$events = $this->getEvents();
} else {
$events = [];
}
$calendar = [];
$oneday = new DateInterval('P1D');
$calday = $this->first_cal_day;
for ($w = 1; $w <= $this->num_weeks; $w++) {
for ($d = 1; $d < 8; $d++) {
$day = new stdClass();
$event_date = $calday->format("Y-m-d");
$day->date = clone $calday;
$day->has_event = array_key_exists($event_date, $events);
if ($day->has_event) {
$day->events = $events[$event_date];
$this->has_events = true;
} else {
$day->events = [];
}
$calendar[] = $day;
$calday->add($oneday);
}
}
return $calendar;
}
public function getEvents() {
$events = [];
$events['2020-04-01'][] = ['text' => 'Event1', 'slug' => 'event-one'];
$events['2020-04-01'][] = ['text' => 'Event2', 'slug' => 'event-two'];
$events['2020-04-05'][] = ['text' => 'Event3', 'slug' => 'event-three'];
$events['2020-04-22'][] = ['text' => 'Event4', 'slug' => 'event-four'];
$events['2020-05-01'][] = ['text' => 'Event5', 'slug' => 'event-five'];
$events['2020-05-11'][] = ['text' => 'Event6', 'slug' => 'event-six'];
$events['2020-05-25'][] = ['text' => 'Event7', 'slug' => 'event-seven'];
$events['2020-05-27'][] = ['text' => 'Event8', 'slug' => 'event-eight'];
return $events;
}
public function has_events() {
return $this->has_events;
}
}
After instantiating the object with a year and month, I can ask it for an array of days, covering the full weeks that span the start and end of the month. For example, the calendar for April 2020 runs from Monday March 30 to Sunday May 3.
Each element in the array is an object, with properties:
date = PHP DateTime object for the day
has_event = true / false showing whether there are events on this day
events = array of events occurring on this day
In my site, the events are a custom post type that are queried in the getEvents method, so I’ve hard-coded some examples in the code above. The events are presented as an array of arrays so that each date can have more than one event. I use the text property to display in the calendar, and the slug property becomes a link to the page for that specific event.
There are a few helper methods such as get_prevdate and get_nextdate, which can be used to create Previous / Next month links above the calendar on the web page.
Now, for presentation, rather than using HTML tables, I’m using “display: grid” for the calendar’s container div. For a wide screen, the CSS is:
.calendar-grid {
display: grid;
grid-template-columns: 14% 14% 14% 14% 14% 14% 14%;
}
Then with a media query for smaller displays, it falls back to a simple vertical list. Neat, and with some accompanying use of display: none; I can easily hide any days with no events when it’s in this format.
@media screen and (max-width: 600px) {
.calendar-grid {
grid-template-columns: auto;
}
.no-event {
display: none;
}
...
Here’s an example of using the class on the front end:
$calendar = new myCalendar($year, $month);
echo '<div class="calendar-container">';
echo '<div class="calendar-headers">';
echo '<div class="calendar-nav-prev"><a class="prev-month" href="?caldate=',$calendar->get_prevdate()->format('Y-m'),'"><< ',$calendar->get_prevdate()->format('F'),'</a></div>';
echo '<div class="calendar-title">',$calendar->get_date()->format('F Y'),'</div>';
echo '<div class="calendar-nav-next"><a class="next-month" href="?caldate=',$calendar->get_nextdate()->format('Y-m'),'">',$calendar->get_nextdate()->format('F'),' >></a></div>';
echo '</div>';
$caldays = $calendar->getCalendar();
if (!$calendar->has_events()) {
echo '<div class="calendar-empty">No events scheduled for this month</div>';
}
echo '<div class="calendar-grid">';
foreach ($calendar->day_names() as $dayname) {
echo '<div class="day-name">',$dayname,'</div>';
}
foreach ($caldays as $calday) {
$calmonth = $calday->date->format("m");
$cellclass = ($calmonth == $month) ? 'current-month' : 'adjacent-month';
$cellclass .= $calday->has_event ? " has-event" : " no-event";
echo '<div class="',$cellclass,'">';
echo '<div class="dayinfull">',$calday->date->format("l j M"),'</div>';
echo '<div class="daynumber">',$calday->date->format("d"),'</div>';
if ($calday->has_event) {
foreach ($calday->events as $day_event) {
echo '<div class="calendar-event">';
echo '<a href="/events/',$day_event['slug'],'">',$day_event['text'],'</a>';
echo '</div>';
}
}
echo '</div>';
}
// End the grid
echo '</div>';
// End the container
echo '</div>';