PHP Advanced and Object-Oriented Programming (2013)

Visual Quickpro Guide

9. Example—CMS with OOP


In This Chapter

Identifying the Goals

Creating the Database

Making the Template

Writing a Utilities File

Creating the Error View File

Defining the Classes

Creating the Home Page

Viewing a Page

Using HTML_QuickForm2

Logging Out

Adding Pages

Review and Pursue


New in this edition of the book is this example chapter. A popular feature of my other PHP books, an example chapter walks through the entire implementation of a real-world site...well, as “entire” as is possible within the confines of a single chapter, that is.

For the example, I’ll create a content management system (CMS), with users who can create and view pages of content. In keeping with the material taught over the previous five chapters, the example will use OOP. Still, the most relevant information will come from Chapter 4, “Basic Object-Oriented Programming,” and Chapter 8, “Using Existing Classes.” While using those ideas to create this site, you will still learn a few new things and get plenty of suggestions for how you could extend this example.

Identifying the Goals

The goal for this chapter is to implement an example site using OOP. The specific example I came up with is a content management system, both because I haven’t used that example in previous books and because CMS represents a large percentage of existing Web sites, often created using WordPress, Drupal, or the like.

That being said, there’s a limit to how much of the site can be implemented and explained in a single chapter of a book (I could write a short book on the subject alone). This chapter focuses on the two most important aspects of a CMS:

• Pages (i.e., the content)

• Users (the people both creating and reading the content)

Just implementing the most critical features for these two aspects requires 16 PHP scripts and HTML files (and, consequently, about 50 book pages). But I’ve managed to define all the requisite functionality:

• Pages can be added.

• The most recent pages are previewed on the home page.

• An individual page can be seen in its entirety.

• A user can log in.

• A user can log out.

• Only administrative users, or the author of the currently viewed page of content, will be provided access to edit the page.

• Only administrative users or author users can create new pages.

Because it’s an object-based system, you can easily extend this initial functionality to fit your needs. I’ll provide many such recommendations along the way.

As an added bonus, the example will use a light, makeshift Model-View-Controller (MVC) approach. MVC is a common software architectural pattern (it’s not technically a design pattern), first introduced in the Smalltalk programming language. MVC is very popular, and for good reason. Using MVC, you separate the data (i.e., the Model), from the output (i.e., the View), using the Controller as the agent.

MVC can be used for application design just as it can Web design. In a Web environment, the Models are normally represented by database tables, although some Models can also represent form data that doesn’t get stored in the database (such as that used in a contact form). Naturally, on a Web page, the Views are the HTML pages—the final, dynamically generated output—that the user actually sees. The Controllers react to user actions, such as the request of a single page or the submission of a form. Controllers implement the logic: validate some data, insert it into a database, show the results, and so forth.

By using MVC, you’ll find that your project will be both easier to expand and easier to maintain. You can add and change any of the three components without necessarily touching the others. For example, you can change how a certain bit of logic is executed in a Controller without adjusting the associated Model or View. And since each component is its own file, edits are quicker, since you don’t have to scan through oodles of integrated PHP, SQL, and HTML.

In more formal MVC structures, you would use objects as Controllers. Here, individual PHP scripts will act as the Controllers. However, just as in formal MVC, the Controllers will not contain or generate any HTML, pushing that onto the Views, which are individual HTML files. As you’ll see, the View files are primarily HTML, with very little logic, which is to say only the bare minimum of PHP code. The Models will always be classes.

The Controllers are represented by top-level PHP scripts. All of the classes go into a separate directory, as do the View files image. Three other PHP scripts, which don’t fit neatly into one of these three categories, go in the includes directory.

image

image The organization of the site.

Through the rest of the chapter, you’ll walk through the implementation of this example in much the same order as I developed the site itself.


Making Improvements with Apache

In Chapter 2, “Developing Web Applications,” I discuss how to configure the Apache Web server to change how a site operates. Two primary concepts were introduced:

• Using mod_rewrite to create prettier, more SEO-friendly URLs

• Using .htacess to limit access to a directory

Although I don’t do so in this chapter, both ideas could be applied to this site.

For example, with mod_rewrite, the URL for a page of content could be changed from page.php?id=X to page/X/The+Content+Title. The X values would still be used to pull the content from the database (on page.php), the URL would be much prettier, informative, and SEO-friendly.

As for using .htaccess to protect a directory, that would be most appropriate for the includes directory. Also, adding that same restriction to the views directory would prevent someone from trying to load a page from that folder directly (assuming the malicious user knows it exists, of course).


Creating the Database

For me, when it comes to Web development, I almost always begin with the underlying database. In the MVC approach, the database tables tend to represent most of the Models, too. For this example, as I’ve implemented it in this chapter, there are two tables: users (Table 9.1) and pages(Table 9.2). The id column from the users table is a foreign key in the pages table, indicating who created the page.

Table 9.1. The users Table

image

Table 9.2. The pages Table

image

Some CMS systems distinguish between “pages,” which are intended as longstanding, time-insensitive, types of content, versus “posts,” which are time-sensitive. For example, an “about” page would be a page but news would be put into a post. This site does not do that, but you’d just need to create a posts table if you wanted that ability. It would be designed just like pages.

If you want the ability to associate categories or tags with pages or posts, you would need to create a categories or tags table. Then you would need to create a pages_categories (or pages_tags or posts_categories or posts_tags) table that would act as the intermediary for the many-to-many relationship between pages/ posts and categories/tags.

If you want users to be able to add comments to a page (or post), create a comments table. It would need to store the page ID, the comment content, the date submitted, and some reference to the user. What that reference would be would depend on whether or not only registered users could comment.

To create the database

1. Access MySQL (or other database application) via whatever interface you prefer.

I’ll be using the command-line mysql client, but you can use phpMyAdmin or whatever.

Also, because this site will use PDO (see Chapter 8) for all database interactions, changing database applications will only require that you edit a single line of code.

2. Create and use a new database:

CREATE DATABASE cms;
USE cms;

I’m creating a new database for this example. If that’s not possible for you (e.g., you’re using a hosted site), just select your existing database.

3. Create the users table image:

CREATE TABLE users (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
userType ENUM('public','author','admin'),
username VARCHAR(30) NOT NULL,
email VARCHAR(40) NOT NULL,
pass CHAR(40) NOT NULL,
dateAdded TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE (username),
UNIQUE (email),
INDEX login (email, pass)
);

image

image Creating the first table.

The users table uses the id column as its primary key. The userType column represents three possible types of users. Users can have usernames, which must be unique, as must the email address. The password will be encrypted using SHA1(), which outputs a string 40 characters long.

Note that because I’m using OOP on the programming side of things, I’m naming my database columns using conventional OOP lower camel-case syntax. For example, it’s userType, not user_type. This allows my class code to be $this->userType, which is more standard than $this->user_type.

4. Create the pages table image:

CREATE TABLE pages (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
creatorId INT UNSIGNED NOT NULL,
title VARCHAR(100) NOT NULL,
content TEXT NOT NULL,
dateUpdated TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
dateAdded TIMESTAMP NOT NULL,
PRIMARY KEY (id),
INDEX (creatorId),
INDEX (dateUpdated)
);

image

image Creating the second table.

The pages table is also pretty simple. It uses the id column as the primary key, and identifies the creator of the page via creatorId. A page itself is made up of a title and its content. Two dates are stored: when the page was first created and when it was last updated. Both could be meaningful on the public side of things.

5. Create a couple of users image:

INSERT INTO users VALUES
(NULL, 'public', 'publicUser', 'public@example.com', SHA1('publicPass'), NULL),
(NULL, 'author', 'authorUser', 'author@example.com', SHA1('authorPass'), NULL),
(NULL, 'admin', 'adminUser', 'admin@example.com', SHA1('adminPass'), NULL);

image

image Prepopulating the users table.

Given the limitations of a book, I haven’t implemented a registration process, so create a couple of usable users via this INSERT command. Later in the book you’ll see how easy it will be to create a registration process, using the site’s classes.

6. Create a couple of pages:

INSERT INTO pages VALUES
(NULL, 2, 'This is a post', '<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>', NULL, NOW());

The script for adding new pages of content won’t be written until the end of the chapter. In the interim, use an INSERT command to create at least three pages of content.

Use whatever combination of text and HTML you want for the content.


Tip

You can download a couple of sample SQL commands along with the site’s code from www.LarryUllman.com.



Tip

If you wanted to allow for a more flexible user type structure, create a userTypes table. Then relate that back into the users table.


Making the Template

Next in the process, I often turn from the most hidden component—the database—to the most obvious: the HTML template. Having no design skills myself, I was fortunate that Tjobbe Andrews (http://tawd.co.uk) volunteered to create an HTML5 template for me to use with this chapter of the book. Tjobbe had let me freely use one of his existing templates in the previous edition of this book, and it was so kind of him to take the time to create a new one for this chapter’s example.

With that template developed, the entire site can use a three-include approach image:

include('includes/header.inc.php');
include('views/specific_content.html');
include('includes/footer.inc.php');

image

image How a page of content is broken into three distinct files.

The header file has a little bit of PHP logic to set the page title based on the presence of a variable. Second, both it and the footer will toggle the login/logout link, based on whether or not the user is currently logged in.

The footer file also creates an “add page” link if the current user is either an author or an administrator.

All of the code can be downloaded from the corresponding Web site at www.LarryUllman.com. In fact, that’s clearly a better way of creating the two template files, in particular. In the following sequence, I’ll only highlight the dynamic PHP code found in both files. Also see the first image in the chapter for the site’s overall directory structure.

To create the header and footer

1. In header.inc.php, print the page title based on the existence of a variable (Script 9.1):

<title><?php echo (isset($pageTitle)) ? $pageTitle : 'Some Content Site'; ?></title>

This code checks if the $pageTitle variable is set (has a non-empty value). If so, its value is printed. Otherwise, a default page title is printed. This is simplified to a single line thanks to the ternary operator.

Normally, in procedural code, I would name the variable $page_title, but because I’m using OOP conventions here, I’ve chosen to use $pageTitle, despite the fact that it won’t be an object.

Script 9.1. The header file begins the HTML page, dynamically sets the title value, and toggles the login/logout link.


1    <!DOCTYPE HTML>
2    <html>
3    <head>
4       <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
5       <meta name="viewport" content="width=device-width, initial-scale=1.0">
6       <title><?php echo (isset($pageTitle)) ? $pageTitle : 'Some Content Site'; ?></title>
7       <!--[if IE]>
8       <script src="http://html5shiv.googlecode.com/svn/trunk/html5.js"></script>
9       <![endif]-->
10      <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
11      <script src="js/custom-jquery.js"></script>
12      <link rel="stylesheet" href="css/reset.css">
13      <link rel="stylesheet" href="css/fonts/fonts.css">
14      <link rel="stylesheet" href="css/main.css">
15      <!--[if lt IE 8]>
16      <link rel="stylesheet" href="css/ie6-7.css">
17      <![endif]-->
18   </head>
19   <!-- # header.inc.php - Script 9.1 -->
20   <body>
21      <header>
22         <h1>Content Site<span>Home to lots of great content!</span></h1>
23         <nav>
24            <ul>
25               <li><a href="index.php">Home</a></li><li><a href="#">Archives</a></li><li><a href="contact.php">Contact</a></li><li><?php if ($user) { echo '<a href="logout.php">Logout</a>'; } else { echo '<a href="login.php">Login</a>'; } ?></li><li><a href="#">Register</a></li>
26            </ul>
27         </nav>
28      </header>


2. Within the header navigation links, toggle the login/logout link:

<li><?php if ($user) { echo '<a href="logout.php">Logout</a>'; } else { echo '<a href="login.php">Login</a>'; } ?></li>

The $user variable will represent the currently logged-in user, or have a NULL value if the user is not logged in. Thus, if ($user) is enough of a conditional to determine whether the login or the logout link should be shown.

3. Save the file as header.inc.php in the includes directory.

I’m using .inc to indicate that this file is meant to be included by others.

4. Within footer.inc.php, again toggle the login/logout links (Script 9.2):

<li><?php if ($user) { echo '<a href="logout.php">Logout</a>'; } else { echo '<a href="login.php">Login</a>'; } ?></li>

This is the same code as in the header, since all the links are just repeated.

Script 9.2. The footer file also toggles the login/logout link, and creates a new link for certain users.


1    <!-- # footer.inc.php - Script 9.2 -->
2       <div class="footerBox">
3          <footer class="threeColumns">
4             <article>
5                <h1>Navigation</h1>
6                <nav>
7                   <ul>
8                      <li><a href="index.php">Home</a></li><li><a href="#">Archives</a></li><li><a href="#">Contact</a></li><li><?php if ($user) { echo '<a href="logout.php">Logout</a>'; } else { echo '<a href="login.php">Login</a>'; } ?></li><li><a href="#">Register</a></li>
9                   </ul>
10               </nav>
11            </article>
12            <article>
13               <h1>Advertisement:</h1>
14               <img src="images/book.png" class="alignright">
15               <p>Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.</p>
16               <p><a href="#">read more here..</a></p>
17            </article>
18            <article>
19               <h1>Other Stuff</h1>
20               <ul>
21                  <?php if ($user && $user->canCreatePage()) echo '<li><a href="add_page.php">Add a New Page</a></li>'; ?>
22                  <li><a href="#">This</a></li>
23                  <li><a href="#">That</a></li>
24                  <li><a href="#">Foo</a></li>
25                  <li><a href="#">Bar</a></li>
26               </ul>
27            </article>
28            <small>© <?php echo date('Y'); ?> Tjobbe Andrews - Website design by <a href="http://www.tawd.co.uk">tawd.co.uk</a></small>
29         </footer>
30      </div>
31   </body>
32   </html>


5. Create a link to the add_page.php script, should the user be logged in and have the power to add pages:

<?php if ($user && $user->can CreatePage()) echo '<li><a href= "add_page.php">Add a New Page</a></li>'; ?>

Two types of users, authors and administrators, can create new pages of content. You could write a conditional here that checks for either of those types:

if (($user->getUserType() == 'admin') || ($user->getUserType() == 'author')) {

But that ends up being a lot of logic and specificity (i.e., the particular values being watched for) buried among this other PHP code and HTML. Therefore, it makes more sense to write an object method that can return a Boolean value indicating the user’s ability to perform this task. That’s all the canCreatePage() method does. You’ll see it in a few pages.

6. Save the file as footer.inc.php, also in your includes directory.

Writing a Utilities File

The next step I took in developing this site was to create a utilities file. This file will be included by every primary PHP script (i.e., every Controller). On other sites, I might call this a configuration file, but this one does a bit more than just that.

As written for the relatively straightforward site, the utilities file fulfills three needs:

• It defines the class loading function (see Chapter 8).

• It starts the session and checks for the presence of a User object previously stored in the session.

• It opens the database connection, creating a PDO object in the process.

The only truly new idea here involves the serializing of objects, which I’ll explain.

It’s very easy to store simple data types, such as strings and numbers, in a session, file, or database, as you already know. But complex data types, such as arrays and objects, are not easily stored in their original state. Those flat storage mediums cannot directly support multifaceted formats like arrays and objects. The solution is to convert the complex data type into a simple data type. This is done in PHP using the serialize() function:

$data = array('Karen' => 'Toronto', 'Stephanie' => 'Boston', 'Jessica' => 'State College');
$sData = serialize($data);

The serialize() function outputs a string that represents the complex data. Now, $sData would have a value of

a:3:{s:5:"Karen";s:7:"Toronto";s:9: "Stephanie";s:6:"Boston";s:7: "Jessica";s:13:"State College";}

(That says that the data is an array, consisting of three elements. The first element uses a string for its key, containing five characters, with a value of Karen. That element has a string for its value, containing seven characters, with a value of Toronto. And so on.)

From that string, the data can be reconstituted into its complex format via the unserialize() function:

$data = unserialize($sData);

The process is the same with objects, with two exceptions. First, the serialized version of the object will only store the values of the object’s attributes, along with the name of the object’s class. The object’s methods will not be stored (this is fine; they’ll come back to the object when the object is unserialized).

The second difference is that PHP needs access to the object’s class definition in order to re-create the object. As long as PHP can do that, the original object will be properly reconstituted, retaining its attribute values and able to invoke its methods once again.

Now, given that long explanation, if you go to store an object in the session, PHP will automatically serialize and unserialize the data on the fly. This process will work as long as PHP can access the corresponding class definitions when the session is started again.

Script 9.3. This script provides some needed functionality to all the primary PHP scripts.


1    <?php # utilities.inc.php - Script 9.3
2    // This page needs to do the setup and configuration required by every other page.
3
4    // Autoload classes from "classes" directory:
5    function class_loader($class) {
6       require('classes/' . $class . '.php');
7    }
8    spl_autoload_register('class_loader');
9
10   // Start the session:
11   session_start();
12
13   // Check for a user in the session:
14   $user = (isset($_SESSION['user'])) ? $_SESSION['user'] : null;
15
16   // Create the database connection as a PDO object:
17   try {
18
19      // Create the object:
20      $pdo = new PDO('mysql:dbname=cms; host=localhost', 'username', 'password');
21
22   } catch (PDOException $e) { // Report the error!
23
24      $pageTitle = 'Error!';
25      include('includes/header.inc.php');
26      include('views/error.html');
27      include('includes/footer.inc.php');
28      exit();
29
30   }


To create the utilities file

1. Begin a new PHP script in your text editor or IDE, to be named utilities.inc.php (Script 9.3):

<?php # utilities.inc.php - Script 9.3

2. Define a function that will autoload the classes:

function class_loader($class) {
  require('classes/' . $class . '.php');
}
spl_autoload_register ('class_loader');

This code comes from Chapter 8. The only difference here is that the class definition files will be placed in the classes subdirectory.

3. Start the session:

session_start();

Again, the session is being started at this point so that it can access all the class definitions (thanks to the autoloader). This order is required should there be a stored, serialized object in the session.

4. Check for a user in the session:

$user = (isset($_SESSION['user'])) ? $_SESSION['user'] : null;

The $user variable, as already explained in the discussion of the header and footer files, will be a reference to the currently logged-in user. The user object will be stored in the session upon successfully logging in. On subsequent pages, the $user variable will be reconstituted from the session.

If the user is not logged in, then the $user variable is set to null. This will make uses of the variable, as in

if ($user) {

be false and not create an error (for referencing an undefined variable.

5. Create the database connection as a PDO object:

try {
$pdo = new PDO('mysql:dbname=cms;host=localhost', 'username', 'password');

This code also comes from Chapter 8. Change the particulars of the DSN to match your setup.

6. Catch any PDO exception:

} catch (PDOException $e) {
  $pageTitle = 'Error!';
    include('includes/header.inc.php');
  include('views/error.html');
    include('includes/footer.inc.php');
  exit();
}

The site will make frequent use of exception handling, starting with this try...catch block. If the database connection could not be made, then the page title is set to Error!, the header is included, as is the error.html view file, and the footer. As this script will be included by other scripts, theexit() function must then be called to stop the including script from continuing to execute, as almost every page does require a database connection.

As you’ll see in the next section, the $e exception will be used in the error view file image.

image

image How a database connection error is treated during development of the site.

7. Save the file as utilities.inc.php, in the includes directory.


Tip

To be clear, if you were storing an object in a database or a file, you would need to formally serialize the object prior to storage. Upon retrieval, you would unserialize the object.


Creating the Error View File

The utilities.inc.php (Script 9.3) page makes reference to views/error.html, so let’s go ahead and write that script now.

To create the error view file

1. Begin a new HTML document in your text editor or IDE, to be named error.html (Script 9.4):

<!-- # error.html - Script 9.4 -->

2. Begin the page-specific content:

<section class="fullWidth">
  <article>
    <h1>An Error Occurred!</h1>

As you’ll see in time, the site has been designed so that all page-specific content goes within its own section. The section element’s class attribute dictates how much of the page is taken up by that section. This section will use the full width.

Script 9.4. All significant problems will be handled by this single view file.


1    <!-- # error.html - Script 9.4 -->
2    <section class="fullWidth">
3       <article>
4          <h1>An Error Occurred!</h1>
5          <p>The content is not viewable because an error occurred. We apologize for any inconvenience. The system administrator has been notified and will correct the problem as soon as possible.</p>
6          <p>Details (not for public consumption): <span class="error"><?php echo $e->getMessage(); ?></span></p>
7       </article>
8    </section>


3. Add a generic message:

<p>The content is not viewable because an error occurred. We apologize for any inconvenience. The system administrator has been notified and will correct the problem as soon as possible.</p>

This is essentially what the public user would see, or a variation on this. Of course, you’d want to add code to make sure the system administrator is actually notified!

4. Add a detailed debugging message:

<p>Details (not for public consumption): <span class="error"><?php echo $e-> getMessage(); ?></span></p>

Here the exception is being used to report what the problem was (as in image in the previous section). You would never want to show this to the public, because it’s both unprofessional and not secure.

When the site goes live, you could just remove this section and replace it with the code that emails the administrator with the details of the problem.

5. Complete the HTML:

  </article>
</section>

6. Save the file as error.html in the views directory.

Defining the Classes

Surprisingly, I was able to pull off this entire site defining only two classes. That’s a misleading count, of course, since the site uses PDO (and the corresponding PDOStatement and PDOException classes), as you’ve already seen. And, as you’ll see soon enough, another series of existing classes are used to create and validate the forms. So, in full disclosure, aside from all the excellent existing classes used by the site, there are two new, user-defined classes:

• Page

• User

Let’s look at both in detail.

The Page class

The Page class will be used to represent a page of content. This will come up in several places:

• On the home page, which shows the three most recent additions

• On an individual page, which displays an entire page of content

• When new pages of content are added

The definition of the Page class starts with one attribute for each corresponding database column. This makes sense, because PDO will be used to fetch records from the pages table into new objects of type Page, assigning column values to class attributes (see Chapter 8).

From there, I created six methods to return the values of each protected attribute (i.e., “getters”). Finally, the Page class has a method that returns the first X number of characters of the page’s content. This method will initially be used on the home page.

To create the Page class

1. Begin a new PHP script in your text editor or IDE, to be named Page.php (Script 9.5):

<?php # Page.php - Script 9.5

Script 9.5. This class represents a page of content.


1    <?php # Page.php - Script 9.5
2    // This script defines the Page class.
3
4    /* Class Page.
5     * The class contains six attributes: id, creatorId, title, content, dateAdded, and dateUpdated.
6     * The attributes match the corresponding database columns.
7     * The class contains seven methods:
8     * - getId()
9     * - getCreatorId()
10    * - getTitle()
11    * - getContent()
12    * - getDateAdded()
13    * - getDateUpdated()
14    * - getIntro()
15    */
16   class Page {
17
18      // All attributes correspond to database columns.
19      // All attributes are protected.
20      protected $id = null;
21      protected $creatorId = null;
22      protected $title = null;
23      protected $content = null;
24      protected $dateAdded = null;
25      protected $dateUpdated = null;
26
27      // No need for a constructor!
28
29      // Six methods for returning attribute values:
30      function getId() {
31         return $this->id;
32      }
33      function getCreatorId() {
34         return $this->creatorId;
35      }
36      function getTitle() {
37         return $this->title;
38      }
39      function getContent() {
40         return $this->content;
41      }
42      function getDateAdded() {
43         return $this->dateAdded;
44      }
45      function getDateUpdated() {
46         return $this->dateUpdated;
47      }
48
49      // Method returns the first X characters from the content:
50      function getIntro($count = 200) {
51         return substr(strip_tags ($this->content), 0, $count) . '...';
52      }
53
54   } // End of Page class.


2. Begin defining the class:

class Page {
  protected $id = null;
  protected $creatorId = null;
  protected $title = null;
  protected $content = null;
  protected $dateAdded = null;
  protected $dateUpdated = null;

Here are the six attributes that correspond to the database columns. Each is initialized to null, just in case that column isn’t selected from the database when the time comes.

3. Define the six “getter” methods:

function getId() {
  return $this->id;
}
function getCreatorId() {
  return $this->creatorId;
}
function getTitle() {
  return $this->title;
}
function getContent() {
  return $this->content;
}
function getDateAdded() {
  return $this->dateAdded;
}
function getDateUpdated() {
  return $this->dateUpdated;
}

There’s nothing particularly notable about these methods.

4. Define the getIntro() method:

function getIntro($count = 200) {
  return substr(strip_tags ($this->content), 0, $count) . '...';
}

The purpose of this method is to return an abbreviated, initial part of the page’s content as a way of previewing the page. In theory, this is just a matter of returning the first X number of characters. To make the method more flexible, it takes an argument to indicate how many characters are desired (e.g., the home page may want 200 but a sidebar only 100).

However, if the content begins with HTML, that will throw off the count. For example, an early image in the content could require 50 characters—not to mention the fact that the HTML could throw off the layout of the page requesting the snippet. For that reason, strip_tags() is first applied to the content, and then the first X number of characters will be returned.

5. Complete the class:

} // End of Page class.

6. Save the file as Page.php in the classes directory.


Tip

A fancier version of the getIntro() method would break the returned text on a space, comma, period, semicolon, question mark, or exclamation point.


The User class

The other new class being defined is User, which represents registered and logged-in users only (i.e., users not logged in are not represented by this class). Like the Page class, User begins with attributes for the database columns already defined.

From there, to address the needs of the site as written for this chapter, there are four methods. One method returns the user’s ID, which will be necessary when a user adds a new page of content: the user’s ID will need to be added to the page record as the creatorId value.

The remaining three methods return Boolean values indicating qualities and capabilities of the user:

• Is the user an administrator?

• Can the user edit the current page?

• Can the user create new pages?

To create the User class

1. Begin a new PHP script in your text editor or IDE, to be named User.php (Script 9.6):

<?php # User.php - Script 9.6

2. Begin defining the class:

class User {
  protected $id = null;
  protected $userType = null;
  protected $username = null;
  protected $email = null;
  protected $pass = null;
  protected $dateAdded = null;

Script 9.6. This class represents the currently logged-in user.


1    <?php # User.php - Script 9.6
2    // This script defines the User class.
3
4    /* Class User.
5     * The class contains six attributes: id, userType, username, email, pass, and dateAdded.
6     * The attributes match the corresponding database columns.
7     * The class contains four methods:
8     * - getId()
9     * - isAdmin()
10    * - canEditPage()
11    * - canCreatePage()
12    */
13   class User {
14
15      // All attributes correspond to database columns.
16      // All attributes are protected.
17      protected $id = null;
18      protected $userType = null;
19      protected $username = null;
20      protected $email = null;
21      protected $pass = null;
22      protected $dateAdded = null;
23
24      // Method returns the user ID:
25      function getId() {
26         return $this->id;
27      }
28
29      // Method returns a Boolean if the user is an administrator:
30      function isAdmin() {
31         return ($this->userType == 'admin');
32      }
33
34      // Method returns a Boolean indicating if the user is an administrator
35      // or if the user is the original author of the provided page:
36      function canEditPage(Page $p) {
37         return ($this->isAdmin() || ($this->id == $page->getCreatorId()));
38      }
39
40      // Method returns a Boolean indicating if the user is an administrator or an author:
41      function canCreatePage() {
42         return ($this->isAdmin() || ($this->userType == 'author'));
43      }
44
45   } // End of User class.


3. Define the getId() method:

function getId() {
  return $this->id;
}

This method merely returns the attribute value.

4. Define the isAdmin() method:

function isAdmin() {
  return ($this->userType == 'admin');
}

Administrators in the site can create new pages or edit any page. Presumably, administrators would be able to do other things, such as edit user records. Therefore, it’s useful to have a method that can be determined if the user is an administrator.

One option would be to just return the userType value. However, then the underlying logic—$userType == 'admin'—would need to be written into multiple scripts. If for whatever reason you later change the type to administrator, or create other types that would be treated like an administrator, all of that embedded code would need to be found and edited.

By placing the logic in the class, only one line of code would ever need to be tweaked to enact changes like those.

5. Define the canEditPage() method:

function canEditPage(Page $p) {
  return ($this->isAdmin() || ($this->id == $page->getCreatorId()));
}

This method returns true if the user is an administrator—determined by invoking the isAdmin() method—or if the current user’s ID equals the page’s creatorID value.

This method uses class type hinting, as explained in Chapter 6, “More Advanced OOP.”

6. Define the canCreatePage() method:

function canCreatePage() {
  return ($this->isAdmin() || ($this->userType == 'author'));
}

This method returns true if the user is an administrator or an author.

7. Complete the class:

} // End of User class.

8. Save the file as User.php in the classes directory.

Creating the Home Page

With all of the core functionality—the database, the template, the utilities file, the main classes—in place, it’s time to start creating the PHP scripts that do the actual work. (Again, in the MVC-lite approach being implemented, I’m talking about the Controllers here.) To start, let’s create the home page, index.php. It will retrieve the three most recent pages of content, display previews of them, and link to the script where the viewer can see the entire content.

Along with this PHP script, you’ll need to create the View file that’s included by this page.

To create the home page

1. Begin a new PHP script in your text editor or IDE, to be named index.php (Script 9.7):

<?php # index.php - Script 9.7

2. Include the utilities file:

require('includes/utilities.inc.php');

Because the utilities file is so important, I’ve chosen to use require() for it, not include().

Script 9.7. The home page uses a try...catch block to pull three records from the table, and then includes the proper View file.


1    <?php # index.php - Script 9.7
2
3    // Need the utilities file:
4    require('includes/utilities.inc.php');
5
6    // Include the header:
7    $pageTitle = 'Welcome to the Site!';
8    include('includes/header.inc.php');
9
10   // Fetch the three most recent pages:
11   try {
12
13      $q = 'SELECT id, title, content, DATE_FORMAT(dateAdded, "%e %M %Y") AS dateAdded FROM pages ORDER BY dateAdded DESC LIMIT 3';
14      $r = $pdo->query($q);
15
16      // Check that rows were returned:
17      if ($r && $r->rowCount() > 0) {
18
19         // Set the fetch mode:
20         $r->setFetchMode(PDO::FETCH_CLASS, 'Page');
21
22         // Records will be fetched in the view:
23         include('views/index.html');
24
25      } else { // Problem!
26         throw new Exception('No content is available to be viewed at this time.');
27      }
28
29   } catch (Exception $e) { // Catch generic Exceptions.
30      include('views/error.html');
31   }
32
33   // Include the footer:
34   include('includes/footer.inc.php');
35   ?>


3. Define the page title and include the header:

$pageTitle = 'Welcome to the Site!';
include('includes/header.inc.php');

The $pageTitle variable will be used within the header, as already explained. The header file is then included. Because it’s not the worst thing in the world if the header and the footer aren’t included (quite ugly, yes, but still...), I will use include() for them. This way, the script will not be terminated if the file cannot be included.

4. Fetch the three most recent pages:

try {
  $q = 'SELECT id, title, content, DATE_FORMAT(dateAdded, "%e %M %Y") AS dateAdded FROM pages ORDER BY dateAdded DESC LIMIT 3';
  $r = $pdo->query($q);

The query fetches four columns from the pages table, formatting the date in the process image. Note that you want to create an alias of the formatted date—back to the original column name—or else the resulting records won’t have a dateAdded value image (and, therefore, the generated class won’t have that attribute).

image

image The results returned by the home page’s query.

image

image Without using an alias, the last value selected—the formatted date—will not have the name expected by the class.

5. Check that some rows were returned:

if ($r && $r->rowCount() > 0) {

This is just a nice precaution to take.

6. Set the fetch mode:

$r->setFetchMode(PDO::FETCH_CLASS, 'Page');

This line, as explained in Chapter 8, will fetch each record as a new Page object.

7. Include the View:

include('views/index.html');

As you saw in error.html, the View will do all the handling and displaying of the data.

8. Throw an exception if no rows were returned:

} else {
  throw new Exception('No content is available to be viewed at this time.');
}

If no rows were returned, then there was probably a problem with the query (or, less likely, you haven’t created any pages of content yet). In either case, the problem needs to be addressed, so a generic Exception is thrown, indicating the problem.

9. Catch any exceptions:

} catch (Exception $e) {
  include('views/error.html');
}

Exceptions could have been thrown by any of the PDO functionality within the try block, or simply because the query did not return any records. Any thrown exception will be caught here and then handled in the error view file image.

image

image If the query had an error in it, or there is no content, an exception is thrown.

Note that you must catch generic Exceptions here, which will cover both PDOExceptions (as that class extends Exception) and the possible exception thrown in Step 8. If you only caught PDOExceptions here, then the exception thrown in Step 8 would not be caught.

10. Complete the page:

include('includes/footer.inc.php');
?>

11. Save the file as index.php in the main web directory.

To create the home page view

1. Begin a new HTML script in your text editor or IDE, to be named index.html (Script 9.8):

<!-- # index.html - Script 9.8 -->

2. Begin a new section:

<section class="threeColumns">

Again, this comes from the wonderful template created by Tjobbe Andrews.

Script 9.8. The View file for the home page fetches and displays the preview of three items.


1    <!-- # index.html - Script 9.8 -->
2    <section class="threeColumns">
3    <?php // Fetch the results and display them:
4    while ($page = $r->fetch()) {
5       echo "<article>
6       <h1><span>{$page->getDateAdded()} </span>{$page->getTitle()}</h1>
7       <p>{$page->getIntro()}</p>
8       <p><a href=\"page.php?id={$page-> getId()}\">read more here...</a></p>
9       </article>
10      ";
11   }
12   ?>
13   </section>


3. Fetch the results and display them:

<?php
while ($page = $r->fetch()) {
  echo "<article>
  <h1><span>{$page->getDateAdded ()}</span>{$page->getTitle()}
  v</h1>
  <p>{$page->getIntro()}</p>
  <p><a href=\"page.php?id={$page->getId()}\">read more here...</a></p>
  </article>
  ";
}
?>

The loop fetches each record from the database and creates a new Page object in the process. That object can then invoke any of the Page class methods in order to create the proper output.

4. Complete the HTML:

</section>

5. Save the file as index.html in the views directory, and test by going to index.php in your Web browser image.

image

image The final home page.

Viewing a Page

Moving onward into the site, the home page shows previews of the three most recently added pages of content. Each is linked to page.php, passing along the page’s ID value in the URL. The purpose of page.php is to display the full content image. This is a simple process:

• Validate the page ID

• Retrieve the corresponding database record

• Include the View file

image

image A single page of content, in its entirety.

Again, let’s first create the PHP script and then the simple View file.

To create the page-viewing page

1. Begin a new PHP script in your text editor or IDE, to be named page.php (Script 9.9):

<?php # page.php - Script 9.9
require('includes/utilities.inc.php');

Script 9.9. This is the “Controller” file for displaying a single, full page of content.


1    <?php # page.php - Script 9.9
2    // This page displays a single page of content.
3
4    // Need the utilities file:
5    require('includes/utilities.inc.php');
6
7    try {
8
9       // Validate the page ID:
10      if (!isset($_GET['id']) || !filter_var($_GET['id'], FILTER_VALIDATE_INT, array ('min_range' => 1))) {
11         throw new Exception('An invalid page ID was provided to this page.');
12      }
13
14      // Fetch the page from the database:
15      $q = 'SELECT id, title, content, DATE_FORMAT(dateAdded, "%e %M %Y") AS dateAdded FROM pages WHERE id=:id';
16      $stmt = $pdo->prepare($q);
17      $r = $stmt->execute(array(':id' => $_GET['id']));
18
19      // If the query ran okay, fetch the record into an object:
20      if ($r) {
21         $stmt->setFetchMode(PDO::FETCH_CLASS, 'Page');
22         $page = $stmt->fetch();
23
24         // Confirm that it exists:
25         if ($page) {
26
27            // Set the browser title to the page title:
28            $pageTitle = $page->getTitle();
29
30            // Create the page:
31            include('includes/header.inc.php');
32            include('views/page.html');
33
34         } else {
35            throw new Exception('An invalid page ID was provided to this page.');
36         }
37
38      } else {
39         throw new Exception('An invalid page ID was provided to this page.');
40      }
41
42   } catch (Exception $e) { // Catch generic Exceptions.
43
44      $pageTitle = 'Error!';
45      include('includes/header.inc.php');
46      include('views/error.html');
47
48   }
49
50   // Include the footer:
51   include('includes/footer.inc.php');
52   ?>


2. Validate the page ID:

try {
  if (!isset($_GET['id']) || !filter_var($_GET['id'], FILTER_VALIDATE_INT, array('min_range' => 1))) {
    throw new Exception('An invalid page ID was provided to this page.');
}

The page ID should have been received in the URL. The conditional first verifies that a page ID was received, and then uses PHP’s Filter extension to validate that it’s an integer greater than or equal to 1.

If this conditional is false, an exception is thrown.

3. Query the database:

$q = 'SELECT id, title, content, DATE_FORMAT(dateAdded, "%e %M %Y") AS dateAdded FROM pages WHERE id=:id';
$stmt = $pdo->prepare($q);
$r = $stmt->execute(array(':id' => $_GET['id']));

The query itself is much like that on the index page.

4. Fetch the record into an object:

if ($r) {
  $stmt->setFetchMode (PDO::FETCH_CLASS, 'Page');
    $page = $stmt->fetch();

5. If an object was made, create the page:

if ($page) {
  $pageTitle = $page->getTitle();
    include('includes/header.inc.php');
  include('views/page.html');

The order of this page is a bit different than the others, because I want the content title to also be the browser window title.

6. Throw exceptions for the conditionals begun in Step 4 and Step 5:

  } else {
    throw new Exception('An invalid page ID was provided to this page.');
  }
} else {
  throw new Exception('An invalid page ID was provided to this page.');
}

7. Catch any exceptions:

} catch (Exception $e) {
  $pageTitle = 'Error!';
    include('includes/header.inc.php');
  include('views/error.html');
}

Again, generic Exception objects must be caught, not just PDOException objects.

8. Complete the page:

include('includes/footer.inc.php');
?>

9. Save the file as page.php.

To create the page-viewing View

1. Begin a new HTML script in your text editor or IDE, to be named page.html (Script 9.10):

<!-- # page.html - Script 9.10 -->

2. Begin a new section:

<section class="fullWidth">
  <article>

In the template, the fullWidth class sections use the entire browser window width, which is preferred here.

3. Display the page content:

<h1><span><?php echo $page->getDateAdded(); ?></span><?php echo $page->getTitle(); ?></h1>
<?php echo $page->getContent(); ?>
<?php if ($user && $user->canEditPage($page)) {
echo '<p><a href="edit_page.php?id=' . $page->getId() . '">EDIT</a></p>';
} ?>

This View will have access to the $page variable, which will be an object of type Page. From there, it’s just a matter of invoking the right methods to generate the desired output.

Along with the actual content, a link to an edit page is created if the user has the authority to edit this particular page. The conditional first checks that $user has a non-false value (users not logged in will have a $user value of null). Then the conditional invokes the canEditPage() method of the User object. That method takes the current page as its argument. The method will return true if the user is an administrator or the original author of this page.

4. Complete the HTML:

  </article>
</section>

5. Save the file as page.html in the views directory, and test by clicking one of the page links on the home page image.

Script 9.10. This View file outputs the page content within a few HTML tags.


1    <!-- # page.html - Script 9.10 -->
2    <section class="fullWidth">
3       <article>
4          <h1><span><?php echo $page->getDateAdded(); ?></span><?php echo $page->getTitle(); ?></h1>
5          <?php echo $page->getContent(); ?>
6          <?php if ($user && $user->canEditPage($page)) {
7          echo '<p><a href="edit_page.php?id=' . $page->getId() . '">EDIT</a></p>';
8          } ?>
9       </article>
10   </section>


Using HTML_QuickForm2

In creating the requisite classes for this example, I began designing a class for creating and validating the different forms. Such classes are very useful but a bit tricky: they must be designed to handle:

• Different types of form elements

• Different ways to validate data (numbers vs. emails address vs. anything else you can think of)

• Displaying of errors

• And more

Fortunately, before I got too far along, I remembered my old friend HTML_ QuickForm.HTML_QuickForm is a PEAR (http://pear.php.net) class that does all of the above. I wrote about it in the previous edition of the book. Now, there’s HTML_QuickForm2, rewritten to take advantage of the object features available in PHP5.

The site as written has two forms, one for logging in image, and another for creating new pages of content image. Both are created and validated using HTML_QuickForm2. I’ll briefly explain how to use the class, and then walk through the specific forms.

image

image The login form.

image

image The form for adding new pages of content.

Note that to use HTML_QuickForm2, you must install it. This is normally a matter of executing this line from a command-line interface:

pear install HTML_QuickForm2

Note that, on some operating systems, you might have to preface that with sudo, or provide a full path to PEAR. If you have any trouble installing PEAR, see the PEAR manual, search online, or ask in my support forums.

Creating a form

After you’ve installed the PEAR class, you must first include it in scripts that will use it:

require('HTML/QuickForm2.php');

Then create an object of type HTML_QuickForm2, providing the constructor with a unique ID value for the form:

$form = new HTML_QuickForm2 ('someForm');

When creating this object, you can pass other parameters to the constructor to change the form method (the default is POST) or add other attributes to the opening form tag.

From there you want to add elements to the form. There are many element types, from the standard HTML ones with which you are accustomed (Table 9.3) to useful ones defined by QuickForm2 (Table 9.4). The syntax for adding a form element is

$form->addElement('type', 'name', 'attributes', 'data');

Table 9.3. Standard HTML Element Types

image

Table 9.4. Custom QuickForm2 Element Types

image

You may want to create a reference to the element being created for future manipulations. If so, just assign the addElement() call to another variable:

$elem = $form->addElement(/* ... */);

To set the label for a form element, invoke the setLabel() method on the element:

$element->setLabel('Prompt: ');

Once you’ve added all the elements, you show the form by simply printing it:

echo $form;

The class will take care of the rest.

Filtering and validating form data

Commonly, you’ll want to apply some sort of filter to your form data: send it through a process that changes the data in some way. The addFilter() method does this, taking the name of the PHP function to apply as its arguments:

$elem->addFilter('function_name');

For example, this line will apply the trim() function to whatever value a user entered in the name field:

$name->addFilter('trim');

Or you might want to apply nl2br() to a text area field:

$comments->addFilter('nl2br');

Along with filters, you can add rules to form elements. Rules can apply validation techniques to the form data. The addRule() method is used here:

$elem->addRule('rule_type', 'error message');

Table 9.5 lists some of the available validation rules. For example, to make a field required, use

$elem->addRule('required', 'Enter this value.');

Table 9.5. QuickForm2 Validation Rules

image

If a rule, like length, takes another argument, that would come after the rule type:

$age->addRule('length', 'Please enter your age.', array('min' => 1, 'max' => 120));

The HTML_QuickForm2 class will automatically add error messages when rules aren’t passed image.

image

image HTML_QuickForm2 will add appropriate errors next to the form elements.


Tip

Better yet, you can have QuickForm2 perform not only server-side validation (using PHP) but also client-side (by generating the necessary JavaScript). See the QuickForm2 documentation for details.



Tip

You can create your own validation rules and then declare them for use by invoking registerRule(). This could be done, for example, to make sure that a username or email address has not already been registered. The function involved would check your database for that name or address’s presence. Again, see the QuickForm2 documentation for the specifics.


Script 9.11. The login process is made much easier thanks to the HTML_QuickForm2 PEAR class.


1    <?php # login.php - 9.11
2    // This page both displays and handles the login form.
3
4    // Need the utilities file:
5    require('includes/utilities.inc.php');
6
7    // Create a new form:
8    set_include_path(get_include_path() . PATH_SEPARATOR . '/usr/local/pear/share/pear/');
9    require('HTML/QuickForm2.php');
10   $form = new HTML_QuickForm2('loginForm');
11
12   // Add the email address:
13   $email = $form->addElement('text', 'email');
14   $email->setLabel('Email Address');
15   $email->addFilter('trim');
16   $email->addRule('required', 'Please enter your email address.');
17   $email->addRule('email', 'Please enter your email address.');
18
19   // Add the password field:
20   $password = $form->addElement ('password', 'pass');
21   $password->setLabel('Password');
22   $password->addFilter('trim');
23   $password->addRule('required', 'Please enter your password.');
24
25   // Add the submit button:
26   $form->addElement('submit', 'submit', array('value'=>'Login'));
27
28   // Check for a form submission:
29   if ($_SERVER['REQUEST_METHOD'] == 'POST') { // Handle the form submission
30
31      // Validate the form data:
32      if ($form->validate()) {
33
34         // Check against the database:
35         $q = 'SELECT id, userType, username, email FROM users WHERE email=:email AND pass=SHA1(:pass)';
36         $stmt = $pdo->prepare($q);
37         $r = $stmt->execute(array(':email' => $email->getValue(), ':pass' => $password->getValue()));
38
39         // Try to fetch the results:
40         if ($r) {
41            $stmt->setFetchMode(PDO::FETCH_CLASS, 'User');
42            $user = $stmt->fetch();
43         }
44
45         // Store the user in the session and redirect:
46         if ($user) {
47
48            // Store in a session:
49            $_SESSION['user'] = $user;
50
51            // Redirect:
52            header("Location:index.php");
53            exit;
54
55         }
56
57      } // End of form validation IF.
58
59   } // End of form submission IF.
60
61   // Show the login page:
62   $pageTitle = 'Login';
63   include('includes/header.inc.php');
64   include('views/login.html');
65   include('includes/footer.inc.php');
66   ?>


Processing form data

The final step in the whole form dance is to do something with the form data. Quick-Form2 provides a method that returns a Boolean value indicating if the form passes the server-side validation. This method can be used in a conditional:

if ($form->validate()) {
  // Good to go!
}

The form data will pass the validate() test if every form element passes all of the applicable rules you’ve established.

To then access the form values, refer to $elem->getValue(). This method returns the processed version of the submitted data after running the data through the filters.

With this quick introduction to HTML_QuickForm2 in place, let’s implement the login process.

To create the login.php script

1. Begin a new PHP script in your text editor or IDE, to be named login.php (Script 9.11):

<?php # login.php - 9.11
require('includes/utilities.inc.php');

2. Create a new form:

require('HTML/QuickForm2.php');
$form = new HTML_QuickForm2 ('loginForm');

As a reminder, you must have already installed the PEAR class for this to work. If you know you installed it and the PHP script cannot find the class, you can add the include path using this code, prior to the require() line:

set_include_path(get_include_path() . PATH_SEPARATOR . '/usr/local/pear/share/pear/');

The set_include_path() function changes where PHP can find files to include. It takes as its argument the new include path. So as not to overwrite the existing include path, you can set this value as the current include path, plus the path separator for the operating system, plus the location of where the PEAR files are, as in this line of code.

Again, if you have any problems with this, search online or ask in my support forums (www.LarryUllman.com/forums/).

3. Add the email address:

$email = $form->addElement('text', 'email');
$email->setLabel('Email Address');
$email->addFilter('trim');
$email->addRule('required', 'Please enter your email address.');
$email->addRule('email', 'Please enter your email address.');

First the email address element is added as a text input. Then its label is set. The trim filter is added next. Finally, two rules are applied. The first says that the field is required and the second says that it must be a syntactically valid email address.

4. Add the password field:

$password = $form->addElement ('password', 'pass');
$password->setLabel('Password');
$password->addFilter('trim');
$password->addRule('required', 'Please enter your password.');

5. Add the submit button:

$form->addElement('submit', 'submit', array('value'=>'Login'));

6. Check for a form submission:

if ($_SERVER['REQUEST_METHOD'] == 'POST') {

Next, the script will check for a form submission in order to process that login attempt.

7. Validate the form data:

if ($form->validate()) {

That’s all that’s required to validate the form data, given the rules already established!

8. Check the submitted values against the database:

$q = 'SELECT id, userType, username, email FROM users WHERE email=:email AND pass=SHA1(:pass)';
$stmt = $pdo->prepare($q);
$r = $stmt->execute(array(':email' => $email->getValue(), ':pass' => $password->getValue()));

The query uses prepared statements to select four columns from the users table. For the values, call the getValue() method of the corresponding element object to retrieve the filtered data.

9. Fetch the results:

if ($r) {
  $stmt->setFetchMode(PDO::FETCH_CLASS, 'User');
  $user = $stmt->fetch();
}

If the query was executed, the next step is to fetch the query results, assuming there were any.

10. If there was one record returned, store it in a session and redirect the user:

if ($user) {
  $_SESSION['user'] = $user;
  header("Location:index.php");
  exit;
}

There’s no great way to see how many records were returned by a SELECT prepared statement. The solution then is to fetch the record (or all of them, if necessary), and count how many were fetched. Or, in this case, just confirm that $user has a non-false value.

11. Complete the conditionals begun in Step 6 and Step 7:

  } // End of form validation IF.
} // End of form submission IF.

12. Create the login page:

$pageTitle = 'Login';
include('includes/header.inc.php');
include('views/login.html');
include('includes/footer.inc.php');

13. Complete the script:

?>

14. Save the file as login.php.

To create the login View

1. Begin a new HTML script in your text editor or IDE, to be named login.html (Script 9.12):

<!-- # login.html - Script 9.12 -->

2. Begin a new section:

<section class="threeColumns">
  <article>
    <p>Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt.</p>
  </article>

Presumably, some instructions might go in this area.

3. Add the form:

<article class="twoThirds">
  <h1>Login</h1>
  <?php if ($form->isSubmitted() && $form->validate()) {
    echo '<p class="error">The values submitted do not match those on file!</p>';
  }?>
  <?php echo $form; ?>
</article>

Showing the form is just a matter of printing the form object. Prior to this, a conditional checks if the form was submitted and passed validation. If so, the only reason the form is being shown again is because the provided values didn’t match those in the database image.

image

image The result if the form was completed but the values were incorrect.

4. Complete the HTML:

</section>

5. Save the file as login.html in the views directory, and test by attempting to log in.

Use the values from the earlier INSERT commands.

Script 9.12. The login View file only needs to print the form and possibly an error message.


1    <!-- # login.html - Script 9.12 -->
2    <section class="threeColumns">
3       <article>
4          <p>Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt.</p>
5       </article>
6       <article class="twoThirds">
7          <h1>Login</h1>
8          <?php if ($form->isSubmitted() && $form->validate()) {
9             echo '<p class="error">The values submitted do not match those on file!</p>';
10         }?>
11         <?php echo $form; ?>
12      </article>
13   </section>


Logging Out

Logging out is really simple. All the script has to do is:

• Clear the session data in the array

• Clear the session cookie

• Destroy the session data on the server (i.e., the session file)

This is fairly standard stuff and not impacted by the use of objects at all.

The View file is quite simple (Script 9.13) and doesn’t require any further instructions. It should be named logout.html, and stored in the views directory.

To create the logout script

1. Begin a new PHP script in your text editor or IDE, to be named logout.php (Script 9.14):

<?php # logout.php - Script 9.14
require('includes/utilities.inc.php');

2. Check that the user is logged in:

if ($user) {

Just in case this page is somehow accessed accidentally (such as the user clicking the back button to return to it after already logging out), the actual logging out will only take place if the user is, in fact, logged in.

Script 9.13. The logout View file only contains literal HTML.


1    <!-- # logout.html - Script 9.13 -->
2    <section class="fullWidth">
3       <article>
4          <h1>You are now logged out.</h1>
5          <p>Thank you for visiting!</p>
6    </article>
7    </section>


Script 9.14. The logout page clears out all the session data.


1    <?php # logout.php - Script 9.14
2    // This page logs the user out.
3
4    // Need the utilities file:
5    require('includes/utilities.inc.php');
6
7    // Check for a user before attempting to actually log them out:
8    if ($user) {
9
10      // Clear the variable:
11      $user = null;
12
13      // Clear the session data:
14      $_SESSION = array();
15
16      // Clear the cookie:
17      setcookie(session_name(), false, time()-3600);
18
19      // Destroy the session data:
20      session_destroy();
21
22   } // End of $user IF.
23
24   // Set the page title and include the header:
25   $pageTitle = 'Logout';
26   include('includes/header.inc.php');
27
28   // Need the view:
29   include('views/logout.html');
30
31   // Include the footer:
32   include('includes/footer.inc.php');
33   ?>


3. Clear the variable:

$user = null;

The variable should be set to null, not entirely unset, because other code (such as in the footer) expects this variable to exist.

4. Clear the session data:

$_SESSION = array();

5. Clear the cookie:

setcookie(session_name(), false, time()-3600);

6. Destroy the session data:

session_destroy();

7. Complete the conditional begun in Step 2:

} // End of $user IF.

8. Create the page:

$pageTitle = 'Logout';
include('includes/header.inc.php');
include('views/logout.html');
include('includes/footer.inc.php');

9. Complete the page:

?>

10. Save the file as logout.php and test by logging in and then clicking the logout link image.

image

image The user can now be logged out.

Adding Pages

Finally, there’s the most important feature of a content management system: the ability to dynamically add content. Thanks to HTML_QuickForm2, this isn’t that much harder than the login form. In fact, aside from checking to ensure that the current user has permission to create new pages, the process is very similar.

I have made one assumption here: that the content is just simple HTML that the user would hand-edit. Naturally, full-fledged CMS systems use plug-ins to aid in the creation of HTML, the adding of media, and so forth. That is something that could be added here, too, using TinyMCE (www.tinymce.com) or the like.

To create the add_page.php script

1. Begin a new PHP script in your text editor or IDE, to be named add_page.php (Script 9.15):

<?php # add_page.php - Script 9.15
require('includes/utilities.inc.php');

2. Create a new form:

require('HTML/QuickForm2.php');
$form = new HTML_QuickForm2 ('addPageForm');

3. Add the title field:

$title = $form->addElement('text', 'title');
$title->setLabel('Page Title');
$title->addFilter('strip_tags');
$title->addRule('required', 'Please enter a page title.');

The title field is a text input. The strip_tags() function will be applied to it, just in case.

4. Add the content field:

$content = $form->addElement ('textarea', 'content');
$content->setLabel('Page Content');
$content->addFilter('trim');
$content->addRule('required', 'Please enter the page content.');

Script 9.15. HTML_QuickForm2 is also used here to easily create and validate a form.


1    <?php # add_page.php - Script 9.15
2    // This page both displays and handles the "add a page" form.
3
4    // Need the utilities file:
5    require('includes/utilities.inc.php');
6
7    // Redirect if the user doesn't have permission:
8    if (!$user->canCreatePage()) {
9       header("Location:index.php");
10      exit;
11   }
12
13   // Create a new form:
14   set_include_path(get_include_path() . PATH_SEPARATOR . '/usr/local/pear/share/pear/');
15   require('HTML/QuickForm2.php');
16   $form = new HTML_QuickForm2('addPageForm');
17
18   // Add the title field:
19   $title = $form->addElement('text', 'title');
20   $title->setLabel('Page Title');
21   $title->addFilter('strip_tags');
22   $title->addRule('required', 'Please enter a page title.');
23
24   // Add the content field:
25   $content = $form->addElement('textarea', 'content');
26   $content->setLabel('Page Content');
27   $content->addFilter('trim');
28   $content->addRule('required', 'Please enter the page content.');
29
30   // Add the submit button:
31   $submit = $form->addElement('submit', 'submit', array('value'=>'Add This Page'));
32
33   // Check for a form submission:
34   if ($_SERVER['REQUEST_METHOD'] == 'POST') { // Handle the form submission
35
36      // Validate the form data:
37      if ($form->validate()) {
38
39         // Insert into the database:
40         $q = 'INSERT INTO pages (creatorId, title, content, dateAdded) VALUES (:creatorId, :title, :content, NOW())';
41         $stmt = $pdo->prepare($q);
42         $r = $stmt->execute(array(':creatorId' => $user->getId(), ':title' => $title->getValue(), ':content' => $content->getValue()));
43
44         // Freeze the form upon success:
45         if ($r) {
46            $form->toggleFrozen(true);
47            $form->removeChild($submit);
48         }
49
50      } // End of form validation IF.
51
52   } // End of form submission IF.
53
54   // Show the page:
55   $pageTitle = 'Add a Page';
56   include('includes/header.inc.php');
57   include('views/add_page.html');
58   include('includes/footer.inc.php');
59   ?>


5. Add the submit button:

$submit = $form->addElement ('submit', 'submit', array ('value'=>'Add This Page'));

For a reason to be explained shortly, the script will later make reference to the submit button, so that element must be created as a variable, too.

6. Check for a form submission and validate:

if ($_SERVER['REQUEST_METHOD'] == 'POST') {
  if ($form->validate()) {

7. Insert the record into the database:

$q = 'INSERT INTO pages (creatorId, title, content, dateAdded) VALUES (:creatorId, :title, :content, NOW())';
$stmt = $pdo->prepare($q);
$r = $stmt->execute(array (':creatorId' => $user->getId(), ':title' => $title->getValue(), ':content' => $content->getValue()));

Two of the values come from the processed form data, and the creatorId value comes from the $user object.

8. If the insert query worked, freeze the form to show the results:

if ($r) {
  $form->toggleFrozen(true);
  $form->removeChild($submit);
}

Here is a bit of new information about HTML_QuickForm2: the toggleFrozen() method, when passed a value of true, will “freeze” the form, which makes the form elements no longer editable. This is one way of showing the user what they just accomplished. In addition, the submit button will be removed so that the frozen form cannot be submitted again.

9. Complete the conditionals begun in Step 6:

  } // End of form validation IF.
} // End of form submission IF.

10. Create the page:

$pageTitle = 'Add a Page';
include('includes/header.inc.php');
include('views/add_page.html');
include('includes/footer.inc.php');

11. Complete the script:

?>

12. Save the file as add_page.php.

To create the “add a page” view file

1. Begin a new HTML script in your text editor or IDE, to be named add_page.html (Script 9.16):

<!-- # add_page.html - Script 9.16 -->

2. Begin a new section:

<section class="threeColumns">
  <article>
    <p>Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt.</p>
  </article>

Presumably, this first section would provide instructions.

3. Add the section for the form:

<article class="twoThirds">
<h1>Add a New Page of Content</h1>

4. Print a message if the previous submission worked:

<?php if ($form->isSubmitted() && $form->validate()) {
echo '<p>The page has been added!</p>';
}?>

The page should display something to the user to indicate success (errors will automatically be indicated by HTML_QuickForm2). One way of testing for success is confirming that the form has been submitted and that it did validate.

5. Display the form:

<?php echo $form; ?>

6. Complete the HTML:

  </article>
</section>

7. Save the file as add_page.html in the views directory, and test by logging in as the proper user type and clicking the link in the footer image.

image

image This page of content has successfully been added.

Script 9.16. Like the logout page View, the “add a page” View simply displays the form. This file does present a message upon success, though.


1    <!-- # add_page.html - Script 9.16 -->
2    <section class="threeColumns">
3       <article>
4          <p>Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt.</p>
5       </article>
6       <article class="twoThirds">
7          <h1>Add a New Page of Content</h1>
8          <?php if ($form->isSubmitted() && $form->validate()) {
9          echo '<p>The page has been added!</p>';
10         }?>
11         <?php echo $form; ?>
12      </article>
13   </section>


Review and Pursue

If you have any problems with these sections, either in answering the questions or pursuing your own endeavors, turn to the book’s supporting forum (www.LarryUllman.com/forums/).

Review

• What is MVC? How is MVC implemented in this chapter? What are the benefits to using MVC? (See pages 284 and 285.)

• What does an autoloading function do? Why must it be defined before the session is started? (See pages 279 and 294.)

• What does it mean to serialize and unserialize data? (See page 294.)

• Why must some of the catch blocks catch generic Exception objects, not PDOException objects? (See page 251.)

Pursue

• Use the information about Apache covered in Chapter 2 to make improvements to how this project runs.

• If you want a site that supports both pages and posts, create a posts table and then all the requisite code. For example, the home page would select the most recent posts and link to post.php, passing along the post ID in the URL. The “about” page would be linked to page.php, passing along the page ID in the URL.

• If you want to support the ability to add categories or tags, do so using the suggestions on page 286.

• If you want the ability to support comments, do so using the suggestions on page 286.

• Create a user-registration process using HTML_QuickForm2.

• Change error.html so that it works as it should on a live site. Have the script email you when a problem occurs, rather than showing the error to the end user.

• Implement the fancier version of Page::getIntro(), as suggested in the tip on page 301.

• If you like HTML_QuickForm2, read more about what it can do and how to make the most of its capabilities.

• Implement the ability to edit an existing page.

• Create an “archives” page that lists the pages in reverse chronological order, linking to the full version of each.

• Create a “contact” page that uses HTML_QuickForm2 to create and validate the form.