Ïðèãëàøàåì ïîñåòèòü
ßçûêîâ (yazykov.lit-info.ru)

Code Walkthrough

Previous
Table of Contents
Next

Code Walkthrough

Instead of just printing all the code for this sample (which would be a waste of paper and quite tedious to pore over), we instead let you peruse the files that make up this sample on your own. Here we focus on some of the more interesting pieces of code that we have written for the sample; these pieces demonstrate some of the key design decisions we made for the appointment manage application.

The AppointmentManager Class

For this sample, we abstracted out nearly all the middle-tier functionality into the AppointmentManager class, which we placed in the appointmentmanager.inc file. The pages and scripts using this class do not need to worry about exactly how the system is implemented or what particular DBMS we use.

The class is a single-instance class, which means that you obtain access to an instance of it by calling the getInstance method, as follows:

$am = AppointmentManager::getInstance();

You add an appointment to the system by calling the addAppointment method on the class, which is implemented as follows:

  /**
   *=---------------------------------------------------------=
   * addAppointment
   *=---------------------------------------------------------=
   * Adds the given appointment to the database.  Please note
   * that we do the necessary checking to make sure that there
   * are no appointments already scheduled for this time.
   *
   * Parameters:
   *    $in_userid    - user ID of appt owner.
   *    $in_title     - title of meeting.
   *    $in_location  - where the meeting will take place.
   *    $in_start     - DateTime object for appt start.
   *    $in_end       - DateTime for appointment end time.
   *    $in_desc      - description of the meeting.
   *
   * Throws:
   *    AppointmentConflictException
   *    DatabaseErrorException
   */
  public function addAppointment
  (
    $in_userid,
    $in_title,
    $in_location,
    DateTime $in_start,
    DateTime $in_end,
    $in_desc
  )
  {
    /**
     * First, check to see if there are any appointments that
     * overlap this one.  Throw if there are.
     */
    if ($this->getAppointments($in_userid,
            $in_start, $in_end) != NULL)
      throw new AppointmentConflictException();

    /**
     * Get a connection.
     */
    $conn = DBManager::getConnection();

    /**
     * Get the data for insertion and make sure it's safe.
     * The mega_escape_string function helps us avoid both XSS
     * and SQL injection attacks (by replacing both HTML tags
     * and quotes)
     */
    $title = DBManager::mega_escape_string($in_title);
    $location = DBManager::mega_escape_string($in_location);
    $desc = DBManager::mega_escape_string($in_desc);
    $start = $in_start->dbString();
    $end = $in_end->dbString();

    /**
     * Build a query to insert the new appointment.
     */
    $query = <<<EOQUERY
INSERT INTO Appointments
  SET
    user_id = $in_userid,
    title = '$title',
    start_time = '$start',
    end_time = '$end',
    location = '$location',
    description = '$desc'
EOQUERY;

    /**
     * Execute the query!
     */
    $results = @$conn->query($query);
    if ($results === FALSE or $results === NULL)
      throw new DatabaseErrorException($conn->error);

    /**
     * We're done!
     */
  }

Before executing the INSERT INTO statement, we make all the input parameters safe for our application. Any time we take a user input string and display it on the screen, we have to worry about cross-site scripting attacks in addition to SQL injection. (Note that attackers would only be hurting themselves in this sample application because they are the only ones viewing their appointments, but we will assume that future improvements might have others viewing the same text.)

The addAppointment method calls the getAppointments method to see whether any appointments are already scheduled during the time for the proposed new appointment. This new method takes two date/time values and returns an array of appointments (represented using the Appointment class) that fall within those two times. NULL is returned if none does:

  /**
   *=---------------------------------------------------------=
   * getAppointments
   *=---------------------------------------------------------=
   * This method takes two parameters specifying a time
   * interval and returns a list of existing appointments
   * that occur during the given time interval.  If none
   * occurs during that time period, then NULL is returned.
   *
   * Parameters:
   *    $in_userid      - user ID of appointment manager
   *    $in_start       - start DateTime of interval
   *    $in_end         - end DateTime of interval
   *
   * Returns:
   *    Array of Appointment Objects.
   *
   * Throws:
   *    DatabaseErrorException
   */
  public function getAppointments
  (
    $in_userid,
    DateTime $in_start,
    DateTime $in_end
  )
  {
    /**
     * Get a database connection with which to work.
     */
    $conn = DBManager::getConnection();

    /**
     * Build a query to ask if there is any overlap.
     */
    $startstr = $in_start->dbString();
    $endstr = $in_end->dbString();
    $query = <<<EOQUERY
SELECT * FROM Appointments
  WHERE (start_time >= '$startstr' AND end_time <= '$endstr')
     OR ('$startstr' >= start_time AND '$startstr' <= end_time)
     OR ('$endstr' >= start_time AND '$endstr' <= end_time)
  ORDER BY start_time ASC
EOQUERY;
    /**
     * Execute the query and look at the results.
     */
    $results = @$conn->query($query);
    if ($results === FALSE or $results === NULL)
      throw new DatabaseErrorException($conn->error);

    $output = NULL;
    while (($row = @$results->fetch_assoc()) != NULL)
    {
      $output [] = new Appointment($row);
    }

    /**
     * Clean up and return the matching appointments.
     */
    $results->close();
    return $output;
  }

The Appointment class is as follows:

/**
 *=-----------------------------------------------------------=
 * Appointment
 *=-----------------------------------------------------------=
 * This is a simple class to hold the details for an
 * appointment. It saves us from the ugliness of simply
 * returning the fields from the database in an array.
 */
class Appointment
{
  public $AppointmentID;
  public $Title;
  public $StartTime;
  public $EndTime;
  public $Location;
  public $Description;

  public function __construct
  (
    $in_appt
  )
  {
    $this->AppointmentID = $in_appt['appt_id'];
    $this->Title = $in_appt['title'];
    $this->StartTime
        =  DateTime::fromDBString($in_appt['start_time']);
    $this->EndTime
        = DateTime::fromDBString($in_appt['end_time']);
    $this->Location = $in_appt['location'];
    $this->Description = $in_appt['description'];
  }
}

We use a few other methods on the AppointmentManager class in our scripts. One just fetches an appointment from the database given its appointment ID; another fetches the next n appointments (chronologically from the current date and time) for the user.

Handling Dates and Times

One of the things we have to do in this application is work with dates and times on a regular basis. To help with this, we have created a new class called DateTime, which we will use to represent a date and a time (although only hours and minutes, because we will not schedule appointments on a more granular basis). This class has a number of public members to represent these values and a constructor to initialize an instance of the class given these, as follows:

class DateTime
{
  /**
   * These are the public variables that people can query on
   * this object.  They should probably not be changed by
   * users.
   */
  public $Year;
  public $Month;
  public $Day;
  public $Hour;
  public $Minute;

  /**
   *=---------------------------------------------------------=
   * __construct
   *=---------------------------------------------------------=
   * Initializes a new instance of this class.  The parameters
   * are the values to actually be used for the date and time,
   * and none is optional.  All must be integers.
   *
   * Parameters:
   *    $in_year
   *    $in_month
   *    $in_day
   *    $in_hour
   *    $in_minute
   *
   * Throws:
   *    InvalidDateException
   *    InvalidTimeException
   */
  public function __construct
  (
    $in_year,
    $in_month,
    $in_day,
    $in_hour,
    $in_minute
  )
  {
    $this->Year = $in_year;
    $this->Month = $in_month;
    $this->Day = $in_day;
    $this->Hour = $in_hour;
    $this->Minute = $in_minute;

    /**
     * Make sure we are given a valid date and time!
     */
    if (!checkdate($in_month, $in_day, $in_year))
      throw new InvalidDateException();
    else if (!DateTime::validTime($in_hour, $in_minute))
      throw new InvalidTimeException();
  }

We make sure that the given date and time values are valid using the checkdate function and another little helper we wrote called validTime. This helps validate input values from the user. We can also create a DateTime object given a date/time string from the database engine as follows:

define('DB_DATETIMESEP', ' ');
define('DB_DATESEP', '-');
define('DB_TIMESEP', ':');

  /**
   *=---------------------------------------------------------=
   * fromDBString
   *=---------------------------------------------------------=
   * Given a string for a date and time that we received from
   * a database, return the equivalent DateTime object
   * for this value. This is a static method that is another
   * way of creating a DateTime object instead of simply using
   * the constructor.
   *
   * Parameters:
   *    $in_string          - string from the db field.
   *
   * Returns:
   *    DateTime            - DateTime object representing that
   *                          string.
   */
  public static function fromDBString($in_string)
  {
    /**
     * Datetimes from DB are "date time." split those apart
     * first.
     */
    $comps = explode(DB_DATETIMESEP, $in_string);

    /**
     * Now process date. YYYY-MM-DD in MySQL
     */
    $dparts = explode(DB_DATESEP, $comps[0]);

    /**
     * and Time: HH:MM[:SS] in MySQL
     */
    $tparts = explode(DB_TIMESEP, $comps[1]);

    /**
     * Create the DateTime object ...
     */
    return new DateTime($dparts[0], $dparts[1], $dparts[2],
                        $tparts[0], $tparts[1]);
  }

Note that this function uses some constants beginning with DB_ to determine how to split up input strings from the database engine, which permits us to switch to database software that uses slightly different values without having to change any codeonly constants.

One of the common tasks the DateTime class performs is to return other dates relative to the current valuefor example, the next day, the first day of the week in which the given object lies, or the beginning and end of the given month. Some of these calculations can be a bit complicated and tricky. (For instance, asking for the next day on December 31, 1999 involves "rolling over" the day, month, and year.) To help keep our code simple, we use the timestamp functionality built into PHP.

To find the next day, we just get a timestamp representing the current value and add the number of seconds in one day (86,400) and return that as a date/time value:

/**
 *=---------------------------------------------------------=
 * nextDay
 *=---------------------------------------------------------=
 * Returns a DateTime object for the day following this. Due
 * to all the possible nastiness that can occur with rolling
 * over into new months, years, leap years, etc., we use the
 * timestamp functions to do most of the hard work for us.
 *
 * Returns:
 *    DateTime with the next day.
 */
public function nextDay()
{
  $ts = mktime(0, 0, 0, $this->Month, $this->Day,
               $this->Year);
  $ts += DAY_IN_SECS;
  $str = date('Y m d', $ts);
  $parts = explode(' ', $str);
  return new DateTime(intval($parts[0]), intval($parts[1]),
                      intval($parts[2]), 23, 59);
}

The timestamp functionality is extremely helpful, but it does limit our appointment system to dates between 1970 and early 2038. We do not anticipate this to be a huge problem, but we can look at other solutions to the problem, such as in the later section "Suggestions/Exercises."

The last set of functions in the DateTime class is for getting other representations for the date, such as the month name, the day of the week, or printable versions of the time.

Processing Forms and Page Progression

You might have noticed from Figure 31-4 that when users enter the data for a new appointment (in the addnewappt.php page), they are sent to a submitnewappt.php page before being sent to the showday.php page with a header('Location: showday.php') call. In fact, in most of our web applications, we follow this pattern (see Figure 31-7). There are two good reasons for this.

Figure 31-7. The standard three-page sequence when submitting form data.

Code Walkthrough


First, it helps keep our processing cleaner. The showday.php page does not have to be burdened with code to validate and enter input values; and if there are incorrect values, we can just send the user right back to the addnewappt.php form. In short, the subminewappt.php page is the one place where we process and create new appointments.

Second, and perhaps more importantly, this three-step sequence helps us avoid duplicate submissions and annoying browser messages about resending POST data. If we were to go from the addnewappt.php directly to the showday.php, and the user were to click the Back button in the browser, he would go back to the appointment submission form. Clicking the Forward button in the browser would cause a resubmission and revalidation of the data (and subsequent error due to conflicting appointments) and an annoying web browser message similar to that shown in Figure 31-8.

Figure 31-8. A sample browser message when resubmitting POST data.

Code Walkthrough


By having the addnewappt.php send the user to the submitnewappt.php, which then redirects the user to showday.php via a redirect, we avoid both the resubmission of data and the confusing error message from the browser when the user moves around with browser buttons.

The submitnewappt.php script is as follows:

<?php
/**
 *=-----------------------------------------------------------=
 * submitnewappt.php
 *=-----------------------------------------------------------=
 * The user has entered the information for a new
 * appointment.  Verify this information now, and
 * then confirm that there are no appointments in this time
 * frame.
 */
ob_start();

require_once('appts/coreincs.inc');

/**
 *=-----------------------------------------------------------=
 * input_error_abort
 *=-----------------------------------------------------------=
 * If there is an input error, this function saves
 * whatever input we have thus far in the session (so we can
 * put it back in the form) and sends the user back to the
 * addnewappt.php page to correct the error.
 *
 * Parameters:
 *      $in_code        - the error code to send back to
 *                        the addnewappt.php page.
 */
function input_error_abort($in_code)
{
  global $title, $location, $syear, $smonth, $sday, $stime,
         $eyear, $emonth, $eday, $etime, $desc;

  $_SESSION['apptinfo'] = array();
  $ai = &$_SESSION['apptinfo'];
  $ai['title'] = $title;
  $ai['location'] = $location;
  $ai['syear'] = $syear;
  $ai['smonth'] = $smonth;
  $ai['sday'] = $sday;
  $ai['stime'] = $stime;
  $ai['eyear'] = $eyear;
  $ai['emonth'] = $emonth;
  $ai['eday'] = $eday;
  $ai['etime'] = $etime;
  $ai['desc'] = $desc;

  header("Location: addnewappt.php?err=$in_code");
  ob_end_clean();
  exit;
}
/**
 * Determine what information we have so far.
 */
$title = isset($_POST['title']) ? $_POST['title'] : '';
$location = isset($_POST['location']) ? $_POST['location'] :'';
$syear = isset($_POST['syear']) ? $_POST['syear'] : '';
$smonth = isset($_POST['smonth']) ? $_POST['smonth'] : '';
$sday = isset($_POST['sday']) ? $_POST['sday'] : '';
$stime = isset($_POST['stime']) ? $_POST['stime'] : '';
$eyear = isset($_POST['eyear']) ? $_POST['eyear'] : '';
$emonth = isset($_POST['emonth']) ? $_POST['emonth'] : '';
$eday = isset($_POST['eday']) ? $_POST['eday'] : '';
$etime = isset($_POST['etime']) ? $_POST['etime'] : '';
$desc = isset($_POST['desc']) ? $_POST['desc'] : '';

/**
 * Make sure we have valid parameters.  They must specify:
 *
 * - a title
 * - a valid start datetime
 * - a valid end datetime
 * - an end datetime that is greater than the start datetime.
 */
if ($title == '')
{
  input_error_abort('title');
}

/**
 * Start Date. Redirect back on error.
 */
try
{
  $time = explode('.', $stime);
  $start_date = new DateTime($syear, $smonth, $sday,
                             $time[0], $time[1]);
}
catch (InvalidDateException $ide)
{
  input_error_abort('sdate');
}
catch (InvalidTimeException $ite)
{
  input_error_abort('stime');
}

/**
 * End Date.  Redirect back on error.
 */
try
{
  $time = explode('.', $etime);
  $end_date = new DateTime($eyear, $emonth, $eday,
                           $time[0], $time[1]);
}
catch (InvalidDateException $ide)
{
  input_error_abort('edate');
}
catch (InvalidTimeException $ite)
{
  input_error_abort('etime');
}

/**
 * End DateTime > start DateTime
 */
if (!$end_date->greaterThan($start_date))
{
  input_error_abort('lesser');
}

/**
 * Okay, input values verified.   Get an AppointmentManager
 * and ask it to add the appointment.  If there are any
 * conflicts, it will throw an AppointmentConflictException,
 * which we can trap and use to redirect the user back to the
 * addnewappt.php page ...
 */
$am = AppointmentManager::getInstance();
try
{
  $am->addAppointment($g_userID, $title, $location,
                      $start_date, $end_date, $desc);
}
catch (AppointmentConflictException $ace)
{
  input_error_abort('conflict');
}

/**
 * Success!  Clean up and redirect the user to the appropriate
 * date to show his appointments on that date.
 */
if (isset($_SESSION['apptinfo']))
{
  unset($_SESSION['apptinfo']);
}
header("Location: showday.php?y=$syear&m=$smonth&d=$sday");
ob_end_clean();
?>

One other section of code is noteworthy in the appointment creation process: the code in the addnewappt.php page that handles input errors. When the submitnewappt.php page detects an error, it sends the user back to the new appointment form and includes a code in the GET parameter list with the name err. Therefore, the user would be sent back to the form with a URL such as the following:

http://phpsrvr/appointments/addnewappt.php?err=conflict

Before redirecting the user, the submitnewappt.php saves the form data in the $_SESSION superglobal. This allows the addnewappt.php form to put the data back in the appropriate boxes and prevents the user from getting agitated that we have destroyed all his data on an error. When the submitnewappt.php succeeds and the appointment is created, we then remove the user from the $_SESSION array for safety reasons.

Although much of the addnewappt.php script concerns itself with the generation of HTML, we show the portions relevant to error processing:

/**
 * First, see if we were sent back here because of an input
 * error.
 */
if (isset($_SESSION['apptinfo']))
{
  $ai = &$_SESSION['apptinfo'];
  $title = $ai['title'];
  $location = $ai['location'];
  $syear = $ai['syear'];
  $smonth = $ai['smonth'];
  $sday = $ai['sday'];
  $stime = $ai['stime'];
  $eyear = $ai['eyear'];
  $emonth = $ai['emonth'];
  $eday = $ai['eday'];
  $etime = $ai['etime'];
  $desc = $ai['desc'];
}
else
{
  $title = '';
  $location = '';
  $syear = '';
  $smonth = '';
  $sday = '';
  $stime = '';
  $eyear = '';
  $emonth = '';
  $eday = '';
  $etime = '';
  $desc = '';
}

/**
 * Get the error message
 */
$msg = '';
if (isset($_GET['err']))
{
  switch ($_GET['err'])
  {
    case 'title':
      $msg = 'You must specify a title for the appointment';
      break;
    case 'sdate':
      $msg = 'The starting date for the appointment is not valid';
      break;
    case 'stime':
      $msg = 'The starting time for the appointment is not valid';
      break;
    case 'edate':
      $msg = 'The end date for the appointment is not valid';
      break;
    case 'dtime':
      $msg = 'The finish time for the appointment is not valid';
      break;
    case 'lesser':
      $msg = 'The end date and time for the appointment are before the start time!';
      break;
    case 'conflict':
      $msg = <<<EOM
Sorry, but there are already one or more appointments scheduled
at this time.
EOM;
      break;
  }

  if ($msg != '')
  {
    $msg = '<p align=\'center\'><br/><font class=\'errSmall\'>'
           . $msg .'</font><br/></p>';
  }
}

echo <<<EOHEADER
<h2 align='center'>Create New Appointment</h2>
$msg
EOHEADER;
// etc...

Note at the beginning of this code segment that we set a bunch of variables corresponding to each of the fields in our form. This enables us to set the value of these when we generate the XHTML, as in the following example for the title field:

echo <<<EOFORM

<form action='submitnewappt.php' method='POST'>
  <table align='center' width='80%' border='0' cellspacing='0'
         cellpadding='5' class='apptFormTable'>
  <tr>
    <td align='right' width='30%'>Title:&nbsp;</td>
    <td>
      <input type='text' size='40' name='title'
             value='$title'/>
    </td>
  </tr>

Showing a Week and a Month

One other particular interesting thing we do in this application is show all the appointments in a given week or month. As discussed in the earlier section, "Handling Dates and Times," we have written a few routines to get the beginning and end of a week or month given a particular date. We use these in our pages to display all of the appointments for a given week or month.

The basic functioning of these two pages is as follows:

1.
Get the input date (or today's date) and determine the boundary dates for the appropriate week or month.

2.
Find all the appointments for the user that fall within those two boundary dates.

3.
Split up all of these appointments according to which day they fall into, creating one "bucket" of appointments per day. Any appointment that spans multiple days will appear in multiple buckets.

4.
Display Previous and Next links on the page.

5.
Generate the XHTML for the given week/month.

We now show the code that splits up the appointments, as in Step 3. This code snippet breaks up the appointments for an entire month:

/**
 * Figure out the start and end days for this month, and
 * how many days there are.
 */
$dt = new DateTime($year, $month, $day, 0, 0);
$first = $dt->topOfMonth();
$last = $dt->bottomOfMonth();
$days_in_month = $last->Day;
$first_day_of_week = $first->getDayOfWeekInt();
$month_name = $first->getMonthName();

/* we trimmed out some code here for brevity */

/**
 * With this information, we can get the appointments
 * for the given month.
 */
$am = AppointmentManager::getInstance();
$appts = $am->getAppointments($g_userID, $first, $last);
if ($appts === NULL)
  $appts = array();

/**
 * Now, split up these appointments into the individual days.
 * Appointments spanning multiple days are put in multiple
 * buckets.
 */
$day_appts = array();
$curday = $first;
for ($x = 0; $x < $days_in_month; $x++)
{
  $day_appts[$x] = array();
  foreach ($appts as $appt)
  {
    if ($curday->containedInDates($appt->StartTime, $appt->EndTime))
    {
      $day_appts[$x][] = $appt;
    }
  }

  $curday = $curday->nextDay();
}

After executing this code, the $day_appts array has one value for each day in the month. This value is an array with those appointments that fall on that day. The code to display a month, which hyperlinks those days that have an appointment, is as follows:

/**
 * Now start dumping the month.
 */
echo <<<EOTABLE
<table width='100%' border='1' cellspacing='0' cellpadding='0'
       class='apptTable'>
<tr>
  <td align='center' width='14%' class='apptMonthHeader'>
    Sunday
  </td>
  <td align='center' width='14%' class='apptMonthHeader'>
    Monday
  </td>
  <td align='center' width='14%' class='apptMonthHeader'>
    Tuesday
  </td>
  <td align='center' width='14%' class='apptMonthHeader'>
    Wednesday
  </td>
  <td align='center' width='14%' class='apptMonthHeader'>
    Thursday
  </td>
  <td align='center' width='14%' class='apptMonthHeader'>
    Friday
  </td>
  <td align='center' class='apptMonthHeader'>
    Saturday
  </td>
</tr>
<tr>

EOTABLE;

$current_day = 1;
$dumped = 0;

/**
 * First, fill in any spaces until the 1st of the month.
 */
for ($x = 0; $x < $first_day_of_week; $x++)
{
  echo "<td>&nbsp;</td>\n";
}
$dumped = $first_day_of_week;

/**
 * Now dump out all the days, making sure to wrap every
 * 7 days.
 */
while ($current_day <= $days_in_month)
{
  if (($dumped % 7) == 0)
  {
    echo "</tr>\n<tr/>\n";
  }

  /**
   * If there are any appts on a given day, make it a nice
   * link so the user can click on the day and see what
   * appointments there are ...  Otherwise, just print the
   * day.
   */
  if (count($day_appts[$current_day - 1]) == 0)
  {
    echo <<<EOTD
  <td align='center' valign='center'>
    <br/>$current_day<br/><br/>
  </td>

EOTD;
  }
  else
  {
    $href = <<<EOHREF
showday.php?y={$first->Year}&m={$first->Month}&d=$current_day
EOHREF;

    echo <<<EOLINK
  <td align='center' valign='center' class='dateWithAppts'>
    <br/><a class='apptDispLink'
            href='$href'>
      $current_day
    </a><br/><br/>
  </td>

EOLINK;
  }

  $current_day++;
  $dumped++;
}

/**
 * Now close it off with any trailing blank slots.
 */
while ($dumped % 7 != 0)
{
  echo "<td>&nbsp;</td>\n";
  $dumped++;
}

echo <<<EOTABLE
</tr>
</table>

The code for the showweek.php script behaves similarly.


Previous
Table of Contents
Next