Приглашаем посетить
Культурология (cult-lib.ru)

Code Walkthrough

Previous
Table of Contents
Next

Code Walkthrough

Because printing all the scripts for this sample would require about 60 pages, we just cover some of the key pieces of code that make this sample work. To look at all the code, you can download the samples (as mentioned in the preceding section, "Installing and Running the Sample").

User Interface Generation

Most of the output in this web application is done using the HtmlGenerator class. As discussed in Chapter 14, "Implementing a User Interface," you can generate XHTML in your scripts in a number of different ways. For this sample, we demonstrate the use of a class or library to do much of the work. We still sometimes generate markup by hand, but for much of the complicated or repetitive work, we use the class we have written.

When writing the HtmlGenerator class, we logically split up our page into a number of key areas, as shown in Figure 32-5. By breaking down our page this way, we can structure the page into a number of increasingly more granular portions:

  • The entire page encapsulated by the <html> element

  • The page body encapsulated by the <body> element

  • The page content, held inside of a table element (<td>)

Figure 32-5. How we structure pages in the SimpleBlog application.

Code Walkthrough


The class is single instance (see the section titled "Singleton Objects" in Chapter 30, "Strategies for Successful Web Applications") and is created by using the getInstance method:

$hg = HtmlGenerator::getInstance();

Most of the methods in the HtmlGenerator class just display some XTHML code. For example, the startPage and closePage methods are as follows:

  /**
   *=---------------------------------------------------------=
   * startPage
   *=---------------------------------------------------------=
   * This routine starts a page by emitting the XHTML headers
   * and the <head> portion of the HTML.
   *
   * Parameters:
   *    $in_pageTitle       - the title for this page.
   */
  public function startPage($in_pageTitle)
  {
    if ($this->m_pageOpened)
    {
      throw new IncorrectUsageException('The startPage()
                          method has already been called!');
    }

    $this->m_pageOpened = TRUE;
    echo <<<EOHEAD
<!DOCTYPE html PUBLIC "~//W3C//DTD XHTML 1.0 Transitional//EN"
  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="en-US"
      xml:lang="en-US">
<head>
  <title>$in_pageTitle</title>
  <meta http-equiv="content-type"
        content="text/html; charset=utf-8"/>
  <link rel="stylesheet" href="basestyle.css" type="text/css"/>
</head>

EOHEAD;
  }
  /**
   *=---------------------------------------------------------=
   * closePage
   *=---------------------------------------------------------=
   * We are finished generating the page, so emit the final
   * closing </html> tag.
   */
  public function closePage()
  {
    if ($this->m_bodyOpened)
    {
      throw new IncorrectUsageException('The closeBody() method
                      has not been called to close the body.');
    }
    else if ($this->m_contentOpened)
    {
      throw new IncorrectUsageException('The closeContent()
                method has not been called to close the page
                contents');
    }
    else if (!$this->m_pageOpened)
    {
      throw new IncorrectUsageException('The startPage()
                method was never called to open the page
                0body');
    }

    $this->m_pageOpened = FALSE;
    echo "</html>\n";
  }

Much of the rest of the class functions like this for the core portions of the pagea start or open method with a corresponding close method. The XHTML for the content of the pages is often generated by hand, because this tends to differ wildly from page to page. For those cases where there is some frequently repeated code, we factor this into a routine in the HtmlGenerator class. The code that emits XHTML for entries and comments is such a case.

The function to print a blog entry is as follows:

  /**
   *=---------------------------------------------------------=
   * emitJournalEntry
   *=---------------------------------------------------------=
   * This function takes the various information related to a
   * journal entry and emits it in some pleasant UI form.
   * The default/current action is to put it in a box (table)
   * with a nice little border around it.  We use various
   * pieces of CSS to help with colors, etc...
   *
   * Parameters:
   *    $in_entry               - Entry object to display.
   */
  public function emitJournalEntry
  (
    Entry $in_entry
  )
  {
    /**
     * First, one little thing:  Replace newlines in the entry
     * body with <br/> tags so that the formatting appears the
     * same as when the user wrote it.  Note that we do not
     * actually replace these newlines in the data stored in
     * the database because we might have other (non-XHTML)
     * uses for this data ...
     */
    $body = ereg_replace('\n', '<br/>', $in_entry->Body);
    echo <<<EOENTRY

<table width='90%' border='2' cellspacing='0' cellpadding='0'>
<tr>
  <td width='100%'>
    <table width='100%' border='0' cellspacing='0'
           cellpadding='4' class='entryTable'>
    <tr>
      <td width='70%' align='left'>
        <a class='entryTitleLink'
           href='showentry.php?eid={$in_entry->EntryID}'>
        {$in_entry->Title}
        </a>
      </td>
      <td align='right' class='entryDate'>
        {$in_entry->Posted}
      </td>
    </tr>
    <tr>
      <td colspan='2'><hr size='1'/></td>
    </tr>
    <tr>
      <td colspan='2' class='entryBody'>
        {$body}
      </td>
    </tr>
    <tr>
      <td colspan='2' width='100%' align='right'>
        <a class='entryWriteCommentLink'
           href='newcomment.php?eid={$in_entry->EntryID}'>
          Add a Comment
        </a>
      </td>
    </tr>
    </table>
  </td>
</tr>
</table>
<br/>

EOENTRY;
  }

Note that we use type hinting to indicate that the parameter must be an Entry object. An entry (or comment, which uses a very similar function) looks similar to that shown in Figure 32-6.

Figure 32-6. Displaying a journal entry in XHTML.

Code Walkthrough


User Management

The SimpleBlog web application is a multiuser system that supports arbitrary numbers of users being logged in and working with the application simultaneously. To manage the creation and listing of users, we wrote the UserManager class, which is, as usual, a single-instance class in the middle tier.

New accounts are created in our system by calling the createNewUser method on this class:

  /**
   *=---------------------------------------------------------=
   * createNewUser
   *=---------------------------------------------------------=
   * Given the full set of information for a user, 
   * create an account for him in our database.
   *
   * Parameters:
   *    $in_uname       - username
   *    $in_passwd      - password
   *    $in_fullname    - user's full name
   *    $in_email       - user's e-mail address
   *    $in_byear       - birth year
   *    $in_bmonth      - birth month
   *    $in_bday        - birth day of month
   *    $in_bio         - user written bio
   *
   * Returns:
   *    The user's user ID (INTEGER)
   */
  public function createNewUser
  (
    $in_uname,
    $in_passwd,
    $in_fullname,
    $in_email,
    $in_byear,
    $in_bmonth,
    $in_bday,
    $in_bio
  )
  {
    /**
     * Validate the input quickly.
     */
    if (!UserManager::validUserName($in_uname))
      throw new IllegalUserNameException($in_uname);
    if ($in_passwd == NULL)
      throw new InvalidArgumentException('$in_passwd');
    $in_byear = intval($in_byear);
    $in_bmonth = intval($in_bmonth);
    $in_bday = intval($in_bday);
    if (($in_byear != 0
         and ($in_byear < 1900
              or $in_byear > intval(date('Y'))))
        or ($in_bmonth != 0
            and ($in_bmonth < 0 or $in_bmonth > 12))
        or ($in_bday != 0
            and ($in_bday <= 0 or $in_bday > 31)))
    {
      throw new InvalidArgumentException(
            '$in_byear-$in_bmonth-$in_bday');
    }

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

    /**
     * Make the parameters safe.  We don't have to do the
     * username because validUserName will fail if it
     * contains unsafe characters.
     *
     * Note that for this application we store only
     * encrypted password hashes, which makes it harder for
     * people to steal passwords but means we can never
     * tell a user what his forgotten password is -- we can
     * only reset it for him.
     *
     * The htmlspecialchars() function helps us avoid both XSS
     * and SQL injection attacks (by replacing both HTML tags
     * and quotes)
     */
    $passwd = md5($in_passwd);
    $fullname = DBMananager::mega_escape_string($in_fullname);
    $email = DBManager::mega_escape_string($in_email);
    $bio = DBManager::mega_escape_string($in_bio);

    /**
     * Build a query.
     */
    $query = <<<EOQUERY
INSERT INTO Users
  (username,password,fullname,email,birthdate,userbio)
  VALUES ('$in_uname', '$passwd', '$fullname',
          '$email', '{$in_byear}-{$in_bmonth}-{$in_bday}',
          '$bio')
EOQUERY;

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

    /**
     * Now just return the new user ID!!
     */
    return $conn->insert_id;
  }

This method and others use some static methods we have written on the UserManager class to perform data validation. All these methods begin with valid and ensure in various different ways that the data they are given conforms to what we expect in our application:

  /**
   *=---------------------------------------------------------=
   * validUserName
   *=---------------------------------------------------------=
   * Checks to see if the given username is valid.
   * We define valid usernames to be those containing
   * alphanumeric characters (Latin alphabet only, sorry),
   * spaces, underscores, or -.  Leading and trailing
   * whitespace are trimmed. Usernames must be at least 8
   * characters and at most 50.
   *
   * Parameters:
   *    $in_uname           - test me.
   *
   * Returns:
   *    TRUE if value, FALSE if not.
   */
  public static function validUserName($in_uname)
  {
    $in_uname = trim($in_uname);
    $min = UserManager::LEN_UNAME_MIN;
    $max = UserManager::LEN_UNAME_MAX;

    /**
     * Note that we have to escape the { because otherwise PHP
     * would interpret it as part of a variable expansion.
     */
    $pattern = "[[:alnum:] _-]\{$min,$max}";
    return ereg($pattern, $in_uname);
  }
  /**
   *=---------------------------------------------------------=
   * validEmail
   *=---------------------------------------------------------=
   * Asks whether the given email address is valid.
   *
   * Parameters:
   *    $in_email       - am i valid please?
   *
   * Returns:
   *    TRUE = "AYE LADDIE!", FALSE = "NAY, LADDIE"
   */
  public static function validEmail($in_email)
  {
    $pattern = <<<EOP
[[:alnum:]._-]+@[[:alnum:]-]+\.([[:alnum:]-]+\.)*[[:alnum:]]+
EOP;
    return ereg($pattern, $in_email);
  }

The createnewacct.php form is a straightforward HTML form that submits its data to the submitnewacct.php script. This script performs the following tasks:

  • Validates the user data

  • Makes sure the chosen username does not already exist

  • Submits the account creation data to the middle-tier UserManager

The second step is done by the UserManager::userNameExists method, as follows:

  /**
   *=---------------------------------------------------------=
   * userNameExists
   *=---------------------------------------------------------=
   * This function asks the database whether there is
   * a user with the existing username.
   *
   * Parameters:
   *    $in_uname           - name to check
   *
   * Returns:
   *    The user's ID (integer) if it exists, or -1 if it
   *    does not.
   *
   * Throws:
   *    DatabaseErrorException
   */
  public function userNameExists($in_uname)
  {
    if ($in_uname === NULL or trim($in_uname) == '')
      throw new InvalidArgumentException('$in_uname');

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

    /**
     * we've likely already checked the name for illegal chars,
     * but we'll escape it here just to be safe ...
     */
    $uname = DBManager::mega_escape_string($in_uname);

    /**
     * Build the query.
     */
    $query = <<<EOQUERY
SELECT username, user_id FROM Users
  WHERE username = '$uname'
EOQUERY;
    /**
     * Execute!
     */
    $results = @$conn->query($query);
    if ($results === FALSE or $results === NULL)
      throw new DatabaseErrorException($conn->error);

    /**
     * See how many results we got. (better be 0 or 1!!!)
     */
    if ($results->num_rows == 0)
    {
      $results->close();
      return -1;
    }
    else if ($results->num_rows == 1)
    {
      $row = @$results->fetch_assoc();
      $results->close();
      return $row['user_id'];
    }
    else
    {
      $results->close();
      throw new InternalErrorException('Multiple users with same name');
    }
  }

One problem with this system is that we are exposed to a concurrency problem (in this case, often referred to as a race condition) if two users are creating an account at the same time. Consider the following sequence of events, occurring in the given order over the span of less than a second:

  • Process 1 User submits account creation form for user SuperAwesomeDude.

  • Process 2 User submits account creation form for user SuperAwesomeDude. (It is a very popular name, you see.)

  • Process 1 submitnewacct.php calls UserManager::userNameExists and sees that it does not. The other data is validated and approved.

  • Process 2 submitnewacct.php also calls the userNameExists function and sees that the username is still available. The other data is validated and approved.

  • Process 1 The UserManager::createNewUser method is called, and the user is inserted into the database.

  • Process 2 This script also calls createNewUser, and we now have two accounts with the same namea true disaster!

Clearly, we need some sort of additional help here to ensure that we do not end up creating two accounts with the same name. At the most basic level in the web application sample, we have used a feature in the database server to ensure that a field contains only unique values. The username field in the Users table has been declared as follows (MySQL syntax):

username VARCHAR(50) UNIQUE NOT NULL

With this setup, the database server does not permit the race condition to result in two records with the same name. When Process 2 attempts to create the entry with the same username, the database server generates an error and refuses to insert the data.

The only downside to this is that it results in a DatabaseErrorException and sends the user to the error.php script. Far more ideal would be to detect this earlier and send the user back to the createnewacct.php page with an error code. We look at a solution to this in the later section titled "Suggestions/Exercises."

Tracking Logged In Users

In addition to creating and managing user accounts, we must be able to log these users in and out of our system. The UserManager class takes care of most of this functionality, working with the LoggedInUsers table in the database. As we saw previously, this table stores three values:

  • The session ID of a logged in session

  • The user ID of the user logged in to that session

  • A timestamp representing the last time this login was accessed

Before any page in our system executes, the coreincs.inc file includes the loginproc.inc script, which calls the UserManager::isUserLoggedIn method. This method returns the user ID of the logged in user associated with this session, or -1 if there is none. The loginproc.inc file then sets the $g_loggedInUID variable with this value. Thus, any script executing in our system knows, right from the beginning, whether there is a logged in user.

The loginproc.inc file executes the following code:

$usrmgr = UserManager::getInstance();
$g_loggedInUID = $usrmgr->isUserLoggedIn();

The UserManager::isUserLoggedInMethod is as follows:

  /**
   *=---------------------------------------------------------=
   * isUserLoggedIn
   *=---------------------------------------------------------=
   * Asks to see if the given username is logged in 
   * for the current session ID !!  Stale logins or those
   * logins from other session IDs are not counted.
   *
   * Parameters:
   *    $in_uname       - user to check
   *
   * Returns:
   *    - user ID of logged in user (integer) or -1 if
   *      not logged in.
   *
   * Throws:
   *    DatabaseErrorException
   */
  public function isUserLoggedIn()
  {
    /**
     * get a db connection and make parameter safe.
     */
    $conn = DBManager::getConnection();

    /**
     * To start, clear out any stale logins.
     */
    $this->clearStaleLoginEntries();

    /**
     * Build and execute the query.
     */
    $sid = session_id();
    $interval = STALE_LOGIN_INTERVAL;

    $query = <<<EOQ
SELECT user_id FROM LoggedInUsers
 WHERE session_id = '$sid'
   AND last_access >= DATE_SUB(NOW(), INTERVAL $interval)
EOQ;
    $results = @$conn->query($query);
    if ($results === FALSE or $results === NULL)
      throw new DatabaseErrorException($conn->error);

    /**
     * See if we have an entry for this session ID ...
     */
    if ($results->num_rows == 0)
    {
      $results->close();
      return -1;
    }
    else if ($results->num_rows == 1)
    {
      $row = @$results->fetch_assoc();
      $results->close();
      if ($row !== NULL)
      {
        /**
         * update the last access time for the user so that
         * we don't think he's been inactive for too long.
         */
        $uid = intval($row['user_id']);
        $this->updateLastAccessTime($uid);
        return $uid;
      }
      else
      {
        return -1;
      }
    }
    else
    {
      $results->close();
      throw new InternalErrorException(
            'Multiple login entries with same session ID !!!');
    }
  }

One important task performed by this method is to only consider those logins that have been "active" in the past two hours or so. (This exact value is stored in the STALE_LOGIN_INTERVAL constant.) Before the function checks to see whether a user login is still valid, it instructs the database to delete all those entries that are older than the STALE_LOGIN_INTERVAL period by calling the clearStaleLoginEntries method. After a login has been determined to still be valid, it performs the second part of this job, which is to update the last_access field in the LoggedInUsers table by calling the updateLastAccessTime method on the UserManager class. This enforcement of "stale" logins is a minor security feature that helps reduce abuse of the system from users on public computers who have left without fully closing all of their browsers (leaving session cookies still in memory). The updateLastAccessTime method is trivial. It merely executes an UPDATE SQL query.

The actual methods to log users in to the system and log them out are as follows:

  /**
   *=---------------------------------------------------------=
   * loginUser
   *=---------------------------------------------------------=
   * Logs the given user into our system. We take the session
   * ID, the user ID, and put that along with a time into the
   * LoggedInUsers table.  We also perform some cleanup
   * of this table whenever we get the chance!
   *
   * Parameters:
   *    $in_uname       - user to log in
   *    $in_passwd      - password attempting to log in with.
   *
   * Throws:
   *    LoginFailureException   - credentials bad.
   *    NoSuchUserException     - no such user.
   *    DatabaseErrorException  - db error.
   */
  public function loginUser($in_uname, $in_passwd)
  {
    /**
     * get a db connection.
     */
    $conn = DBManager::getConnection();

    /**
     * We need the user's user ID.  This throws if the user
     * does not exist.
     */
    $user_id = $this->userIDFromUserName($in_uname);

    /**
     * Make sure the passwords match up.  Throws on failure.
     */
    $this->confirmPasswordForLogin($in_uname, $in_passwd);

    /**
     * If there are any existing login entries for this
     * user, clear them out now!! (This occurs if a user
     * closes his web browser without first explicitly
     * logging out.)
     *
     * Note that it is critical that this be performed
     * after the confirmPasswordForLogin function is called.
     * Otherwise, anybody could come along and enter the username
     * and bogus password to cause the user to be logged
     * out!!!
     */
    $this->clearLoginEntriesForUser($user_id);

    /**
     * Build a query.  We'll make sure the provided name is
     * safe against SQL injection.
     */
    $sid = session_id();
    $query = <<<EOQ
INSERT INTO LoggedInUsers (user_id, session_id, last_access)
  VALUES($user_id, '$sid', NOW())
EOQ;

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

    /**
     * That's it!
     */
    return $user_id;
  }

  /**
   *=---------------------------------------------------------=
   * logoutUser
   *=---------------------------------------------------------=
   * Logs the user associated with the current session ID
   * out of the system.
   *
   * Throws:
   *    DatabaseErrorException
   */
  public function logoutUser()
  {
    /**
     * get a db connection.
     */
    $conn = DBManager::getConnection();

    /**
     * Create and execute the query to do the cleanup!
     */
    $sid = session_id();
    $query = <<<EOQ
DELETE FROM LoggedInUsers WHERE session_id ='$sid'
EOQ;

    $results = @$conn->query($query);
    if ($results === FALSE or $results === NULL)
      throw new DatabaseErrorException($conn->error);

    /**
     * That's it!
     */
  }

The loginUser function executes one last little piece of magicit makes sure that any other login entries with the same user ID are deleted from the LoggedInUsers table by calling the clearLoginEntriesForUser function. This would occur when a user logs in to the application, closes her web browser without logging out, and then starts up a new browser to come back. The entry for the user with the old session ID would still be there. This last new method merely deletes any entries from the table that have the same user ID as that of the user trying to log in.

The clearLoginEntriesForUser method looks like this:

  /**
   *=---------------------------------------------------------=
   * clearLoginEntriesForUser
   *=---------------------------------------------------------=
   * This deletes from the LoggedInUsers table any entries
   * related to the specified user ID.
   *
   * Parameters:
   *    $in_userid          - ID of user
   */
  private function clearLoginEntriesForUser($in_userid)
  {
    if (!is_int($in_userid))
      throw new InvalidArgumentException('$in_userid');

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

    /**
     * Build a query.
     */
    $query = <<<EOQ
DELETE FROM LoggedInUsers
  WHERE user_id = $in_userid;
EOQ;

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

   /**
    * That's it!
    */
  }

Managing Entries and Comments

The final major area of this project is the code to manage comments and entries. The class to manage Entry objects is the EnTRyManager, whereas Comments are managed by the CommentManager. Both are single-instance classes that are created using the getInstance method. The EnTRy and Comment classes are extremely simple in this sample and serve merely to avoid sending database data directly back to users of the ConnectionManager and EnTRyManager objects:

/**
 *=-----------------------------------------------------------=
 * Entry
 *=-----------------------------------------------------------=
 * This class holds the data that we use for a entry.  It
 * is quite simple. It serves basically so we don't have to
 * send back data directly from the database, letting us
 * provide a layer of abstraction.
 */
class Entry
{
  public $EntryID;
  public $AuthorID;
  public $AuthorName;
  public $Title;
  public $Posted;
  public $Body;

  /**
   *=---------------------------------------------------------=
   * __construct
   *=---------------------------------------------------------=
   * Initializes a new instance of the Entry class.
   *
   * Parameters:
   *    $in_rowdata     - row data from the database.
   */
  public function __construct($in_rowdata)
  {
    $this->EntryID = $in_rowdata['entry_id'];
    $this->AuthorID = $in_rowdata['author_id'];
    $this->AuthorName = $in_rowdata['username'];
    $this->Title = $in_rowdata['title'];
    $this->Posted = $in_rowdata['posted'];
    $this->Body = $in_rowdata['body'];
  }
}
/**
 *=-----------------------------------------------------------=
 * Comment
 *=-----------------------------------------------------------=
 * This class holds the data that we use for a comment.  It
 * is quite simple. It serves basically so we don't have to
 * send back data directly from the database.
 */
class Comment
{
  public $CommentID;
  public $EntryID;
  public $AuthorID;
  public $AuthorName;
  public $Posted;
  public $Title;
  public $Body;

  /**
   *=---------------------------------------------------------=
   * __construct
   *=---------------------------------------------------------=
   * Initializes a new instance of the Comment class.
   *
   * Parameters:
   *    $in_comment     - row data from the database.
   */
  public function __construct($in_comment)
  {
    $this->CommentID = $in_comment['comment_id'];
    $this->EntryID = $in_comment['entry_id'];
    $this->AuthorID = $in_comment['author_id'];
    $this->AuthorName = $in_comment['username'];
    $this->Posted = $in_comment['posted'];
    $this->Title = $in_comment['title'];
    $this->Body = $in_comment['body'];
  }
}

Note that both actually require more than the data in the JournalEntries or JournalComments tables in their respective constructorsboth also require a username field. We always obtain this by using an INNER JOIN when we fetch entries or comments, as follows:

SELECT JournalEntries.*,Users.username FROM JournalEntries
  INNER JOIN Users
  WHERE JournalEntries.author_id = Users.user_id
        AND entry_id = $in_entryid

Adding a new entry to the database is done by the submitnewentry.php script, called from newentry.php. This script makes sure that at least a title or a body was specified, and then calls the addEntry method on the EntryManager:

  /**
   *=---------------------------------------------------------=
   * addEntry
   *=---------------------------------------------------------=
   * The currently logged in user has written a new entry,
   * and we need to add it to the database.  This method
   * does just that.
   *
   * Parameters:
   *    $in_uid             - user ID of author.
   *    $in_title           - entry title.
   *    $in_body            - text of entry.
   *
   * Returns:
   *    integer with entry_id of the created entry.
   *
   * Throws:
   *    DatabaseErrorException
   */
  public function addEntry($in_uid, $in_title, $in_body)
  {
    if (!is_int($in_uid))
    {
      throw new InvalidArgumentException('$in_uid');
    }

    /**
     * Get a database connection with which to work.
     */
    $conn = DBManager::getConnection();

    /**
     * Build up the query to insert the entry into the
     * database.  We have to perform two security checks on
     * the strings we are given.
     *
     * 1. Make sure that there are no HTML tags in them.
     * 2. Make sure that they are safe against SQL injection.
     *
     * For the first, we will actually allow <img> and <a>
     * tags in the message body, but otherwise allow no other
     * tags.
     *
     * We are using the strip_tags() function provided by PHP,
     * which has a few serious limitations:
     *
     * - it is not really mbcs/UTF-8 enabled.
     * - it translates 'Many<b>Moons</b>' into 'ManyMoons'
     * - it has some trouble with complete XHTML documents.
     *
     * As an exercise, you could try to write a more robust version 
     * that addresses some of these problems.
     */
    $title = DBManager::mega_escape_string($in_title);
    $body = strip_tags($in_body, '<a><img>');
    $body = DBManager::mega_escape_string($body, FALSE);

    $query = <<<EOQUERY
INSERT INTO JournalEntries (title,posted,author_id,body)
  VALUES('$title', NOW(), $in_uid, '$body')
EOQUERY;

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

    /**
     * Otherwise, just return the entry ID of the created
     * item.
     */
    return $conn->insert_id;
  }

In the preceding code, we use the strip_tags function in PHP, which removes all tags, except those specified in the optional second parameter. As we also note, however, it has some problems that make it less than optimal for our use. As an exercise in "Suggestions/ Exercises," we suggest writing a new version of this.

We fetch the entries for a user by calling the conveniently named getEntriesForUser method:

  /**
   *=---------------------------------------------------------=
   * getEntriesForUser
   *=---------------------------------------------------------=
   * Retrieves all of the entries for the given user.  An
   * optional two parameters specify which items to fetch
   * in case you don't want to get all of them. (These are used
   * with the LIMIT keyword in SQL. See Chapter 10.) 
   * Note that entries are always returned last to first (i.e.,
   * most recent are the first in the output).
   *
   * Parameters:
   *    $in_uid             - user whose entries we want
   *    $in_start           - [optional] starting index
   *    $in_citems          - [optional] number to fetch
   *
   * Output:
   *    Array of Entry objects.
   */
  public function getEntriesForUser
  (
    $in_uid,
    $in_start = -1,
    $in_citems = -1
  )
  {
    if (!is_int($in_uid) or $in_uid < 0)
    {
      throw new InvalidArgumentException('$in_uid');
    }
    if ($in_start < -1 or $in_citems < -1)
    {
      throw new InvalidArgumentExceptoin('$in_start/$in_citems');
    }

    /**
     * Get a database connection with which to work!!
     */
    $conn = DBManager::getConnection();
    /**
     * Build the query, taking into consideration the
     * possible LIMIT clause...
     */
    $query = <<<EOQUERY
SELECT * FROM JournalEntries
  WHERE author_id = $in_uid
  ORDER BY posted DESC
EOQUERY;
    if ($in_start != -1)
    {
      $limit2 = ($in_citems != -1) ? ",$in_citems" : '';
      $limit = " LIMIT $in_start$limit2";
      $query .= $limit;
    }

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

    /**
     * Build the output array.
     */
    $output = array();
    while (($row = @$results->fetch_assoc()) != NULL)
    {
      $output[] = new Entry($row);
    }

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

Finally, we also provide a getEntry method, which merely returns a single entry (given its entry ID) rather than all the entries for a particular user.

Our CommentManager class has a similar set of methods. The addComment method adds a comment associated with a given entry ID, whereas the getCommentsForEntry returns all those comments entered against a particular entry, as follows:

  /**
   *=---------------------------------------------------------=
   * getCommentsForEntry
   *=---------------------------------------------------------=
   * Given the entry ID of some entry in our system, retrieve
   * all the comments associated with this entry.
   *
   * Parameters:
   *    $in_entryid         - entry ID of entry whose comments
   *                          we wish to fetch.
   *
   * Returns:
   *    Comment
   *
   * Throws:
   *    InvalidArgumentException
   *    NoSuchEntryException
   *    DatabaseErrorException
   */
  public function getCommentsForEntry($in_entryid)
  {
    if (!is_int($in_entryid) or $in_entryid < 0)
      throw new InvalidArgumentException('$in_entryid');

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

    /**
     * One thing of which we must absolutely be sure is that
     * the specified entry ID is actually valid.  To do this,
     * we'll just call getEntry on the entry manager.  This is
     * slightly less efficient than simply calling a query
     * and not fetching the results, but it's not significantly
     * so, and it saves us from having to manage another
     * routine on the EntryManager class.
     *
     * This will throw if the specified entry ID is invalid.
     *
     * NOTE:  we could probably avoid this outright if we
     *        used the FOREIGN KEY in our database.
     */
    $em = EntryManager::getInstance();
    $entry = $em->getEntry($in_entryid);

    /**
     * Build a query and execute that puppy!
     */
    $query = <<<EOQUERY
SELECT JournalComments.*, Users.username FROM JournalComments
  INNER JOIN Users
  WHERE JournalComments.author_id = Users.user_id
        AND JournalComments.entry_id = $in_entryid
  ORDER BY posted ASC
EOQUERY;

    $results = @$conn->query($query);
    if ($results === FALSE or $results === NULL)
      throw new DatabaseErrorException($conn->error);

    /**
     * now whip through this list and build the output array.
     */
    $output = array();
    while (($row = @$results->fetch_assoc()) != NULL)
    {
      $output[] = new Comment($row);
    }

    /**
     * Clean up and exit!
     */
    $results->close();
    return $output;
  }


Previous
Table of Contents
Next