Creating Web Calendars With The PEAR Calendar Class

Need a quick-and-dirty Web calendar? Look no further!

Hot Date

One of the nicest things about PHP is the huge support the language enjoys in the developer community. A prime example of this is PEAR, the PHP Extension and Application Repository, which offers a robust, feature-rich and well-supported collection of PHP widgets that can save developers a fair amount of time and effort. One such widget is PEAR's Calendar class, a little-known module that allows you to create and customize calendar data structures. These raw structures can then be used in date-heavy user interfaces, like an appointment calendar or event tracker.

I don't blame you if you're not turning cartwheels just yet - it sounded pretty boring to me the first time I heard about it too. But then I tried using Calendar in a project, and it turned me into a believer...

Why, you ask? You see, if you attempt to roll your own calendar in PHP, there are a hundred tiny temporal adjustments you have to code for - leap years, 30/31-day months, year rollovers and so on. The Calendar class handles all these adjustments internally, allowing you to focus on the big picture instead of worrying about the little details. If you're a lazy programmer (and you know you are!), using a ready-made Calendar significantly reduces the amount of code you have to write and test...and that's always a Good Thing!

Over the next few pages, I'm going to give you a quick run-down on the PEAR Calendar class, together with a few examples of how you can use it to rapidly develop Web-based calendar applications while still getting ten hours of sleep every night. Keep reading!

Building Blocks

The Calendar class is a PEAR package which provides an API for PHP developers to build date-based applications. It's currently maintained by Harry Fuecks, Lorenzo Alberton and Greg Beaver, and is freely available from http://pear.php.net/package/Calendar.

The Calendar class works by building object representations of various temporal units. For example, a year is represented by a Year object, which may internally create Month objects. Month objects may themselves spawn Day objects or Week objects. Each of these objects publishes certain methods and properties, which come in handy when developing the user interface - for example, a Month may publish the number of Days it has, while a Day may publish its day number in a Month or position in a Week.

To better understand how this works, consider the following example, which creates a Year and populates it with Months:

<?php
// include class
include("Calendar/Year.php");

// create and build year data structure
// for 2006
$year = new Calendar_Year(2006);
$year->build();

// display months
while ($month = $year->fetch()) {
    echo $month->thisMonth() .  " ";
}
?>

This script first creates a Year() object for 2006. It then populates the Year with 12 Month() objects, by calling the Year() object's build() method. Each individual Month can then be retrieved with a call to the Year() object's fetch() method.

The procedure described above holds good for other temporal data structures as well. To create Hours, for example, initialize a Day and then build Hours from it:

<?php
// include class
include("Calendar/Day.php");

// create and build day data structure
// for 01-Jan-2006
$day = new Calendar_Day(2006, 1, 1);
$day->build();

// display hours of day
while ($hour = $day->fetch()) {
    echo $hour->thisHour() .  ":00 ";
}
?>

To create Seconds, initialize a Minute and then build Seconds from it:

<?php
// include class
include("Calendar/Minute.php");

// create and build minute data structure
// for 01-Jan-2006 12:41
$minute = new Calendar_Minute(2006, 1, 1, 12, 41);
$minute->build();

// display seconds of minute
while ($sec = $minute->fetch()) {
    echo $sec->thisSecond() .  " ";
}
?>

If you've understood how this works, flip the page, and let's start doing more complicated stuff.

A Month And A Year

Before using Calendar in a project, it's important to visualize the type of calendar you want to display, and then use the appropriate object from Calendar's collection. Most of the time, you'll want to display the traditional monthly calendar, which looks like this:

Here's the code to accomplish this:

<html>
<head></head>
<body>
<pre>
<?php
// include class
include("Calendar/Month/Weekdays.php");

// create and build month data structure
// containing day objects
// classified by week
$month = & new Calendar_Month_Weekdays(2005, 6);
$month->build();

// print day names
echo " M  T  W  T  F  S  S";

// iterate over days
// display as weekly calendar
while ($day = $month->fetch()) {
    // if first day of week
    // start a new line
    if ($day->isFirst()) {
        echo "\r\n";
    }

    // if this day does not belong to this month
    // print empty spaces
    if ($day->isEmpty()) {
        echo "   ";
    // else print the day number
    } else {
        echo sprintf("%2d", $day->thisDay()) . " ";
    }
}
?>
</pre>
</body>
</html>

To support creation of the traditional monthly calendar, the Calendar package includes a special object, the Month_Weekdays object. This object, when initialized with a specific month and year, spawns Day objects for that month. Each of these Days comes with additional attributes indicating its week and month position. It's then possible to iterate over the object collection with fetch() and display the days in calendar format. The isFirst() method indicates whether the day is the first day of the week, while the isEmpty() method indicates whether the day belongs to the previous or next month.

Wanna do a calendar for the whole year? Just wrap the whole thing in a loop that runs twelve times:

<html>
<head></head>
<body>
<pre>
<?php
// include class
include("Calendar/Month/Weekdays.php");

// loop 12 times
// once for each month
for ($x=1; $x<13; $x++) {
    // create and build month data structure
    // containing day objects
    // classified by week
    $month = & new Calendar_Month_Weekdays(2005, $x);
    $month->build();

    // print month
    echo "       $x/2005 \r\n";

    // print day names
    echo " M  T  W  T  F  S  S";

    // iterate over days
    // display as weekly calendar
    while ($day = $month->fetch()) {
        // if first day of week
        // start a new line
        if ($day->isFirst()) {
            echo "\r\n";
        }

        // if this day does not belong to this month
        // print empty spaces
        if ($day->isEmpty()) {
            echo "   ";
        // else print the day number
        } else {
            echo sprintf("%2d", $day->thisDay()) . " ";
        }
    }
    echo "\r\n\r\n";
}
?>
</pre>
</body>
</html>

Here's what it looks like:

Changing The Decor

The Calendar package also includes what it calls "decorators". No, these aren't the guys who attempt to re-arrange your living room, but a set of tools to help you create more user-friendly and usable calendars. The one you're likely to use fairly often is called the Textual decorator, which lets you print names instead of numbers for months and weekdays. Here's an example:

<html>
<head></head>
<body>
<table>
<?php
// include classes
include("Calendar/Month/Weekdays.php");
include("Calendar/Decorator/Textual.php");

// create and build month data structure
// containing day objects
// classified by week
$month =& new Calendar_Month_Weekdays(2005, 6);
$month->build();

// create decorator
$deco =& new Calendar_Decorator_Textual($month);

// print month and year
echo "<tr><td colspan=7 align=center>" . $deco->thisMonthName('short') . " " . $deco->thisYear() . "</td></tr>";

// print day names
$dayNames = $deco->orderedWeekdays('two');
echo "<tr>";
foreach ($dayNames as $name) {
    echo "<td>$name</td>";
}
echo "</tr>";

// iterate over days
// display as weekly calendar
while ($day = $month->fetch()) {
    // if first day of week
    // start a new row
    if ($day->isFirst()) {
        echo "<tr>";
    }

    // if this day does not belong to this month
    // print empty spaces
    if ($day->isEmpty()) {
        echo "<td>&nbsp;</td>";
    // else print the day number
    } else {
        echo "<td align=right>" . $day->thisDay() . "</td>";
    }

    // if last day
    // end row
    if ($day->isLast()) {
        echo "</tr>";
    }
}
?>
</table>
</body>
</html>

In this case, an instance of the Calendar_Decorator_Textual class is initialized with a Month() object. This decorator object is then used to print month names and weekday names, via its thisMonthName() and orderedWeekdays() methods respectively. Here's what the result looks like:

Notice that this script differs from previous ones, in that it generates the calendar using HTML table cells. I've done this mostly because it provides me with the opportunity to showcase another method of the Day() object, the isLast() method. This method does the opposite of the isFirst() method discussed earlier, indicating whether the particular day falls at the end of the week. If isLast() returns true, it means it's time to end the current table row with a .

Time Travel

Now, you'll have noticed that most Web-based calendars include the ability to scroll forward and backward between months. If you've ever tried coding this manually, you already know just how difficult this seemingly-innocuous feature can be: you've got to worry about invalid month and year values, as well as rolling over to the following or previous year when year boundaries are breached.

With the Calendar package's Uri decorator, all this worry is a thing of the past. This decorator takes care of building navigation links to move from one month to another. Here's how:

<html>
<head></head>
<body>
<table>
<?php
// include classes
include("Calendar/Month/Weekdays.php");
include("Calendar/Decorator/Textual.php");
include("Calendar/Decorator/Uri.php");

// get month and year
// defaults to current month and year
// if no values available
$yy = (isset($_GET['yy']) && is_numeric($_GET['yy'])) ? $_GET['yy'] : date("Y");
$mm = (isset($_GET['mm']) && is_numeric($_GET['mm'])) ? $_GET['mm'] : date("m");

// create and build month data structure
// containing day objects
// classified by week
$month =& new Calendar_Month_Weekdays($yy, $mm);
$month->build();

// create decorator
$text =& new Calendar_Decorator_Textual($month);

// create URI helper for navigation
// define GET variables to use
$uri =& new Calendar_Decorator_Uri($month);
$uri->setFragments("yy", "mm");

// print prev page link
echo "<tr><td><a href=?" . $uri->prev('month') . "><<</a></td>";

// print month and year
echo "<td colspan=5 align=center>" . $text->thisMonthName('short') . " " . $text->thisYear() . "</td>";

// print next page link
echo "<td><a href=?" . $uri->next('month') . ">>></a></td></tr>";

// print day names
$dayNames = $text->orderedWeekdays('two');
echo "<tr>";
foreach ($dayNames as $name) {
    echo "<td>$name</td>";
}
echo "</tr>";

// iterate over days
// display as weekly calendar
while ($day = $month->fetch()) {
    // if first day of week
    // start a new row
    if ($day->isFirst()) {
        echo "<tr>";
    }

    // if this day does not belong to this month
    // print empty spaces
    if ($day->isEmpty()) {
        echo "<td>&nbsp;</td>";
    // else print the day number
    } else {
        echo "<td align=right>" . $day->thisDay() . "</td>";
    }

    // if last day
    // end row
    if ($day->isLast()) {
        echo "</tr>";
    }
}
?>
</table>
</body>
</html>

Here's what it looks like:

Here, each time a user clicks the next or previous hyperlink, the PHP script calls itself with a new set of month and year parameters. These parameters are then used to generate an appropriate calendar. The URL code for the hyperlinks is generated by calling the Calendar_Decorator_Uri object's next() and prev() methods.

If you'd like to hyperlink every single day in the rendered calendar - useful, for example, if you're building an online appointment calendar - you can use the Calendar_Decorator_Uri to do this too. Simply wrap each day number in a hyperlink created by calling the decorator's this() method, as in the next example:

<html>
<head></head>
<body>
<table>
<?php
// include classes
include("Calendar/Month/Weekdays.php");
include("Calendar/Decorator/Textual.php");
include("Calendar/Decorator/Uri.php");

// get month and year
// defaults to current month and year
// if no values available
$yy = (isset($_GET['yy']) && is_numeric($_GET['yy'])) ? $_GET['yy'] : date("Y");
$mm = (isset($_GET['mm']) && is_numeric($_GET['mm'])) ? $_GET['mm'] : date("m");

// create and build month data structure
// containing day objects
// classified by week
$month =& new Calendar_Month_Weekdays($yy, $mm);
$month->build();

// create text decorator to get string names
$text =& new Calendar_Decorator_Textual($month);

// create URI helper for navigation
// define GET variables to use
$uri =& new Calendar_Decorator_Uri($month);
$uri->setFragments("yy", "mm");

// print prev page link
echo "<tr><td><a href=?" . $uri->prev('month') . "><<</a></td>";

// print month and year
echo "<td colspan=5 align=center>" . $text->thisMonthName('short') . " " . $text->thisYear() . "</td>";

// print next page link
echo "<td><a href=?" . $uri->next('month') . ">>></a></td></tr>";

// print day names
$dayNames = $text->orderedWeekdays('two');
echo "<tr>";
foreach ($dayNames as $name) {
    echo "<td>$name</td>";
}
echo "</tr>";

// reset URI helper for day links
// attach to dummy $day object
// set GET variables
$day =& new Calendar_Day(null, null, null);
$uri =& new Calendar_Decorator_Uri($day);
$uri->setFragments("yy", "mm", "dd");

// iterate over days
// display as weekly calendar
while ($day = $month->fetch()) {
    // if first day of week
    // start a new row
    if ($day->isFirst()) {
        echo "<tr>";
    }

    // if this day does not belong to this month
    // print empty spaces
    if ($day->isEmpty()) {
        echo "<td>&nbsp;</td>";
    // else print the day number
    // with a hyperlink
    } else {
        echo "<td align=right><a href=processor.php?" . $uri->this('day') . ">" . $day->thisDay() . "</a></td>";
    }

    // if last day
    // end row
    if ($day->isLast()) {
        echo "</tr>";
    }
}
?>
</table>
</body>
</html>

And here's what this looks like:

Notice that each day number is linked to the example script processor.php, and passes it the current month, day and year as GET variables. The hard work of creating the hyperlink is done by the decorator's this() method.

Holiday Hi-Jinks

Calendar also comes with built-in capabilities to flag certain days within the month as "special". These special days can be treated differently when displaying the calendar. A good example of this is an appointment calendar, wherein days which have active appointments can be highlighted in a different colour.

Consider the following example, which demonstrates how this works:

<html>
<head></head>
<body>
<table>
<?php
// include classes
include("Calendar/Month/Weekdays.php");
include("Calendar/Day.php");
include("Calendar/Decorator/Textual.php");

// create array of "special" days
// 3 June 2005 and 17 June 2005
$specialDays = array(
    new Calendar_Day(2005, 6, 3),
    new Calendar_Day(2005, 6, 17)
    );

// create and build month data structure for June 2005
// containing day objects
// classified by week
$month =& new Calendar_Month_Weekdays(2005, 6);
$month->build($specialDays);

// create decorator
$text =& new Calendar_Decorator_Textual($month);

// print month and year
echo "<td colspan=7 align=center>" . $text->thisMonthName('short') . " " . $text->thisYear() . "</td>";

// print day names
$dayNames = $text->orderedWeekdays('two');
echo "<tr>";
foreach ($dayNames as $name) {
    echo "<td>$name</td>";
}
echo "</tr>";

// iterate over days
// display as weekly calendar
while ($day = $month->fetch()) {
    // if first day of week
    // start a new row
    if ($day->isFirst()) {
        echo "<tr>";
    }

    // if this day does not belong to this month
    // print empty spaces
    if ($day->isEmpty()) {
        echo "<td>&nbsp;</td>";
    // if this day is "special"
    // highlight with yellow background
    } elseif ($day->isSelected()) {
        echo "<td align=right bgcolor='yellow'>" . $day->thisDay() . "</td>";
    // else print day number
    } else {
        echo "<td align=right>" . $day->thisDay() . "</td>";
    }

    // if last day
    // end row
    if ($day->isLast()) {
        echo "</tr>";
    }
}
?>
</table>
</body>
</html>

Here's what it might look like:

Here, the build() method is passed an array of Day() objects, which represent "special" days that are to be flagged. When iterating over the object collection, it now becomes possible to test each day with the isSelected() method to see if it is "special", and highlight it if so.

One application of this capability might be to visually mark public holidays in a calendar. To do this, first create a file containing the list of holidays, like this:

01-01-2005, New Year's Day
14-02-2005, Valentine's Day
17-09-2005, The Boss's Birthday

Then, write some code to read and parse this file, generate Day objects from it, and pass these Day objects to the build() method when rendering the calendar:

<html> <head> <body> <table> <?php // include classes include("Calendar/Month/Weekdays.php"); include("Calendar/Day.php"); include("Calendar/Decorator/Textual.php"); include("Calendar/Decorator/Uri.php");

// get month and year // defaults to current month and year // if no values available $yy = (isset($_GET['yy']) && is_numeric($_GET['yy'])) ? $_GET['yy'] : date("Y"); $mm = (isset($_GET['mm']) && is_numeric($_GET['mm'])) ? $_GET['mm'] : date("m");

// retrieve holidays list // get day/month/year components // for each holiday $lines = file("holidays.txt") or die("ERROR:Cannot find holiday database"); foreach ($lines as $line) { $fields = explode(",", $line); $date = explode("-", $fields[0]); $holidaysData[] = new Calendar_Day($date[2], $date[1], $date[0]); }

// create and build month data structure // containing day objects // classified by week // take account of selected dates $month =& new Calendar_Month_Weekdays($yy, $mm); $month->build($holidaysData);

// create text decorator to get string names $text =& new Calendar_Decorator_Textual($month);

// create URI helper for navigation // define GET variables to use $uri =& new Calendar_Decorator_Uri($month); $uri->setFragments("yy", "mm");

// print prev page link echo "<tr>``<td>``<a href=?" . $uri->prev('month') . "><<";

// print month and year echo "<td colspan=5 align=center>" . $text->thisMonthName('short') . " " . $text->thisYear() . "";

// print next page link echo "<td>``<a href=?" . $uri->next('month') . ">>>";

// print day names $dayNames = $text->orderedWeekdays('two'); echo "<tr>"; foreach ($dayNames as $name) { echo "<td>$name"; } echo "";

// reset URI helper for day links // attach to dummy $day object // set GET variables $day =& new Calendar_Day(null, null, null); $uri =& new Calendar_Decorator_Uri($day); $uri->setFragments("yy", "mm", "dd");

// iterate over days // display as weekly calendar while ($day = $month->fetch()) { // if first day of week // start a new row if ($day->isFirst()) { echo "<tr>"; }

// if this day does not belong to this month
// print empty spaces
if ($day->isEmpty()) {
    echo "`<td>`&nbsp;</td>";
// if this day is a holiday or a Sunday
// print it without a hyperlink
} elseif ($day->isSelected() || $day->isLast()) {
    echo "`<td align=right>`" . $day->thisDay() . "</td>";
// else print it
// with a hyperlink
} else {
    echo "`<td align=right>``<a href=processor.php?" . $uri->`this('day') . ">" . $day->thisDay() . "</a></td>";
}

// if last day
// end row
if ($day->isLast()) {
    echo "</tr>";
}

} ?>


Here, the calendar display code includes a test for special days (read: holidays) and Sundays. No hyperlinks are produced for these days in the calendar display. Here's what it might look like:

<img src="../../../data/archives/trog/collateral/00277/image7.gif" />

## Testing Times

The Calendar package also exposes some utility methods for date validation. The most important of these is the isValid() method, which checks dates to see if they're valid. To use this method, simply initialize a Day() object for the date you wish to test and then call the object's isValid() method, as in the example below:

<?php // include Calendar class include("Calendar/Day.php");

// initialize Day object to 31-Jun-2004 $day = & new Calendar_Day(2004, 6, 31);

// check date // returns "Invalid date" echo $day->isValid() ? "Valid date" : "Invalid date"; ?>


You can also use the Month() object's size() method to find out how many days exist in a month. To do this, initialize a Month object with a month and year number, and then call the object's size() method. Here's an example:

<?php // include class include("Calendar/Month.php");

// create and build month data structure // for June 2005 $month = new Calendar_Month(2005, 6); $month->build();

// display days in month echo "This month has " . $month->size() . " days"; ?>



And that's about it for this tutorial. Over the last few pages, I took you on a whirlwind tour of the Calendar class, one of the more useful items in PEAR's collection of date and time tools. I showed you the basics of creating a calendar, adding month navigation to it, and attaching actions to day links. I also showed you how to flag particular days in a month as special, and created a small Web calendar that automatically treated holidays and Sundays differently from other days. Finally, I described how you could use ancillary methods exposed by the various Calendar objects to validate dates and calculate the number of days in a month.

I hope you enjoyed this tutorial, and that it will save you some time the next time you sit down to code a Web calendar. Happy coding!
This article was first published on28 Nov 2006.