[ Team LiB ] Previous Section Next Section

An Example

Let's bring most of these functions together into an example. We are going to build a calendar that can display the dates for any month between 1980 and 2010. The user will be able to select both month and year with pull-down menus, and the dates for that month will be organized according to the days of the week. If the input is invalid or absent, we will default to the first day of the current month. To develop our calendar, we will create three classes.

The DateIterator Class

The DateIterator class in Listing 16.4 is responsible for setting a pointer to the beginning of the given month and counting each of its days.

Listing 16.4 The DateIterator Class
 1: <?php
 2: class DateIterator {
 3:   public static $ADAY = 86400;
 4:   private $pointer;
 5:
 6:   function __construct( $month, $day, $year ) {
 7:     $this->pointer= mktime ( 0, 0, 0, $month, $day, $year );
 8:   }
 9:
10:   function incrementDay() {
11:     $this->pointer += (DateIterator::$ADAY);
12:   }
13:
14:   function getMonthStartWDay() {
15:     $date_array = $this->getPointerArray();
16:     $date = mktime ( 0, 0, 0,
17:       $date_array['mon'],
18:       1, $date_array['year']);
19:     $array = getdate( $date );
20:     return $array['wday'];
21:   }
22:
23:   function getPointer() {
24:     return $this->pointer;
25:   }
26:
27:   function getPointerArray() {
28:     return getDate( $this->pointer );
29:   }
30: }
31: ?>

The DateIterator class includes a useful static property on line 3. DateIterator::$ADAY contains the number of seconds in a day, and this value is used to move a DateIterator object's pointer forward day by day. The constructor accepts three integer arguments for month of year, day of month, and year, respectively. We construct a timestamp representing the required date on line 7 using the mktime() function.

The real business of the class takes place in the incrementDay() method (line 10). It simply advances the pointer forward by one day.

The getMonthStartWDay() method on line 14 returns the day of the week index for the first day of the current month. This is used later to work out whether a calendar cell should be filled.

The getPointer() method returns the current date timestamp. getPointerArray() uses the getDate() function to return an associative array for the same date.

This simple class enables us to tick through the days in a month and is used by a QuickCalendar object.

The QuickCalendar Class

The QuickCalendar class steps through a calendar grid. The grid is indexed on the x-axis by days of the week and on the y-axis by discrete weeks. You can see the class in Listing 16.5.

Listing 16.5 The QuickCalendar Class
 1: <?php
 2: include_once( "listing16.4.php" );
 3:
 4: class QuickCalendar {
 5:   private $cellno=0;
 6:   private $month;
 7:   private $year;
 8:   private $dateIterator;
 9:
10:   function __construct( $month, $year ) {
11:     if ( empty( $month ) || empty( $year ) ) {
12:       $nowArray = getdate();
13:       $year = $nowArray['year'];
14:       $month = $nowArray['mon'];
15:     }
16:
17:     $this->dateIterator = new DateIterator( $month, 1, $year );
18:     $this->month = $month;
19:     $this->year = $year;
20:   }
21:
22:   function getCurrentArray() {
23:     return $this->dateIterator->getPointerArray();
24:   }
25:
26:   function cellBeforeMonthStart() {
27:     return ( $this->cellno < $this->dateIterator->getMonthStartWDay() );
28:   }
29:
30:   function cellAfterMonthEnd() {
31:     $current = $this->getCurrentArray();
32:     if ($this->month == 12) {
33:       return ( $this->year < $current['year'] );
34:     }
35:     return ( $this->month < $current['mon'] );
36:   }
37:
38:   function endOfRow() {
39:     return ( ! ( $this->cellno % 7 ) );
40:   }
41:
42:   function endOfGrid() {
43:     return ( $this->cellAfterMonthEnd() && $this->endOfRow() );
44:   }
45:
46:   function nextCell() {
47:     if ( $this->endOfGrid() ) {
48:     $ret = null;
49:   } else if ( $this->cellBeforeMonthStart() ||
50:         $this->cellAfterMonthEnd() ) {
51:     $ret = array();
52:   } else {
53:     $ret = $this->getCurrentArray();
54:     $this->dateIterator->incrementDay();
55:   }
56:   $this->cellno++;
57:   return $ret;
58:  }
59: }
60: ?>

The constructor accepts month and year integers and instantiates a DateIterator object. If these arguments are empty, we use the current date and derive the month and year from that. We cache the starting month and year on lines 18 and 19.

The getCurrentArray() on line 22 returns the DateIterator object's pointer as an associative array (as derived from getDate()).

Each cell can have a number of statuses. Our grid starts on a Sunday, so if the first day of a month is a Wednesday, the cells between Sunday and Tuesday are empty. For these cells, the cellBeforeMonthStart() method (line 26) returns true. By the same token, the last day of a month might not fall on the final cell in the grid. The cellAfterMonthEnd() method returns true for each cell that is within the grid but after the end of the month. Every seventh cell is the last in a row (that is, the end of a week). For these, the endOfRow() method returns true. Finally, for the last cell in a grid, the endOfGrid() method returns true.

These tests are all used by the nextCell() method, which iterates a $cellno property and calls on the DateIterator object's iterateDay() method. nextCell() returns an empty array for cells that fall before the start of a month or after the end of a month. It returns a date array as returned by getCurrentArray() for cells within the month. Finally, when the grid is finished, it returns null. Client code can call this method repeatedly and call on the test method endOfRow() to determine when to break a row.

This class is not easy to grasp at a sitting. Let's look at the client code that uses it to get a sense of the interface:


<table border="1" cellpadding="5">
<tr>
<?php
$cal = new QuickCalendar( );
while ( ! is_null( $cell = $cal->nextCell() ) ) {
  if ( empty( $cell ) ) {
    print "<td> - </td>";
  } else {
    print "<td>".$cell['mday']." ".$cell['month']."</td>";
  }
  if ( $cal->endOfRow() && ! $cal->endOfGrid() ) {
    print "</tr><tr>";
  }
}
?>
</tr>
</table>

The client code instantiates a QuickCalendar object and calls nextCell() until it returns null. If getCell() returns an empty array, it displays an empty cell; otherwise, it displays a cell containing a date. At the end of each iteration, the code uses the QuickCalendar object's endOfRow() method to test whether it should end the table row and begin a new one. This is all it takes to convert the virtual grid managed by QuickCalendar into a real grid. Because the display is entirely divorced from the logic of the calendar, you could output calendar grids in different formats very easily.

Now that you can output a calendar, you need to build a mechanism with which the user can select a month and a year. You will need to generate two pull-downs and headings for the HTML grid. Rather than embed it directly in HTML markup, you'll tuck code to generate pull-downs into a helper class.

The DateViewHelper Class

This simple class consists of two static methods for generating pull-downs and two arrays that hold the days of the week and months of the year. You can see the DateViewHelper class in Listing 16.6.

Listing 16.6 The DateViewHelper Class
 1: <?php
 2: class DateViewHelper {
 3:   static $MONTHS = array(
 4:         "January", "February", "March", "April",
 5:         "May", "June", "July", "August", "September",
 6:         "October", "November", "December");
 7:   static $DAYS = Array(
 8:         "Sunday", "Monday", "Tuesday", "Wednesday",
 9:         "Thursday", "Friday", "Saturday");
10:
11:   static function yearPulldown( $from, $to, $selected ) {
12:     $ret = "";
13:     for ( $x = $from; $x <= $to; $x++ ) {
14:       $ret .= "<option";
15:     $ret .= ($x == $selected )?' selected="selected"':"";
16:     $ret .= ">$x</option>\n";
17:   }
18:   return $ret;
19:  }
20:
21:  static function monthPulldown( $selected ) {
22:    for ( $x=1; $x <= 12; $x++ ) {
23:      $ret .= "<option value=\"$x\"";
24:      $ret .= ($x == $selected )?' selected="selected"':"";
25:      $ret .= ">".dateViewHelper::$MONTHS[$x-1]."</option>\n";
26:    }
27:    return $ret;
28:  }
29: }
30: ?>

The yearPulldown() method on line 11 requires starting and ending years as well as a selected year. It uses a for loop to iterate through all the numbers between $from and $to, adding <option> elements to a return string as it does so. The monthPulldown() method on line 21 is similar: It loops through the static $MONTHS array property to generate a string of <option> elements.

These three classes are now ready to be used to output a calendar.

The Client Code

It is now really only a matter of working with the classes we have created and formatting the output. We do this in Listing 16.7.

Listing 16.7 The Calendar Client Code
 1: <?php
 2: include_once( "listing16.5.php" );
 3: include_once( "listing16.6.php" );
 4:
 5: $cal = new QuickCalendar( $_REQUEST['month'],
 6:           $_REQUEST['year] );
 7: $current = $cal->getCurrentArray( );
 8: ?>
 9:
10: <!DOCTYPE html PUBLIC
11:   "-//W3C//DTD XHTML 1.0 Strict//EN"
12:   "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
13: <html>
14: <head>
15: <title>Calendar: <?php print $current['month']." ".
16:                 $current['year'] ?></title>
17: </head>
18: <body>
19:
20: <h1>Calendar: <?php print $current['month']." ".
21:                 $current['year'] ?></h1>
22:
23: <form method="post" action="<?php echo $_SERVER['PHP_SELF'] ?>">
24: <div>
25: <select name="month">
26: <?php print DateViewHelper::monthPulldown(
27:              $current['mon'] ); ?>
28: </select>
29:
30: <select name="year">
31: <?php print DateViewHelper::yearPulldown( 1980, 2010,
32:              $current['year'] ); ?>
33: </select>
34:
35: <input type="submit" value="Go!" />
36: </div>
37: </form>
38:
39: <table border="1" cellpadding="5">
40: <tr><td><b>
41: <?php print implode( "</b></td><td><b>",
42:              DateViewHelper::$DAYS ); ?>
43: </b></td></tr>
44: <tr>
45: <?
46: while ( ! is_null( $cell = $cal->nextCell() ) ) {
47:   if ( empty( $cell ) ) {
48:     print "<td> - </td>";
49:   } else {
50:     print "<td>".$cell['mday']." ".$cell['month']."</td>";
51:   }
52:   if ( $cal->endOfRow() && ! $cal->endOfGrid() ) {
53:     print "</tr><tr>";
54:   }
55: }
56: ?>
57: </tr>
58: </table>
59: </body>
60: </html>

We first include the QuickCalendar and DateViewHelper classes on lines 2 and 3. We instantiate a QuickCalendar object, passing it the submitted parameters 'month' and 'year' on line 5. We then assign it to the variable $cal. If we are visiting this page for the first time, the 'month' and 'year' arguments have not yet been filled. Our QuickCalendar object deals with this for us, though, by working to the current date.

We call the QuickCalendar object's getCurrentArray() method to get a date array for the first day of the month in question. We use this to output a descriptive title detailing the month and year we will be displaying.

Between lines 25 and 28, we output a pull-down of months, calling the DateViewHelper class's static monthPulldown() method. Notice that we do not not need to instantiate a DateViewHelper object to call a static method on the class. Between lines 30 and 33, we do the same thing for a month's pull-down by calling the DateViewHelper class's yearPulldown() method.

We need our grid to be labelled with the days of the week. We could type them out, but we have opted to save typing by accessing the DateViewHelper class's static $DAYS array, calling the implode() function to output the day strings, separated by table cell element tags.

Finally, we work with our QuickCalendar object to output the cells in a loop starting on line 46. We have already looked at this fragment, which calls on the nextCell(), endOfRow(), and endOfGrid() methods to format the output.

You can see the output from a call to this script in Figure 16.2.

Figure 16.2. The calendar script.

graphics/16fig02.gif

    [ Team LiB ] Previous Section Next Section