PHP 5 in Action

Kaushik Datta

In this tutorial author combines multiple features of PHP 5 into a unified program with an example.

Specifically, we present a simple address book application that allows you to add new people and search for existing people. This application is designed to:

  • Explains a wide range of new PHP 5 features
  • Work completely, even though it's limited in scope
  • Be accessible both over the web and from the command line
  • Separate application and presentation logic

Records are limited to first name, last name, and email address, and are stored in an SQLite database. A record can be turned into a Person object, and multiple Persons are stored in an addressBook object.

Since the application needs to work from both the Web and from the command line, you need an easy way to convert records into both HTML and plain text. To facilitate this task, every object is capable of using DOM to create an XML representation of its contents. You then use SimpleXML to manipulate these XML documents into formatted output.

The first section in this tutorial covers the database schema, so you know how the records are stored. Parts two and three cover the Person and addressBook classes. These sections use many of PHP 5's new object-oriented features, including property overloading, visibility, class type hints, and custom object iterators using the IteratorAggregate interface. The classes also create, append, and import DOM documents and elements.

Now that you've defined how the data is represented, both in the underlying storage mechanism (the database) and within your program (the Person and addressBook classes), the next step is to translate the objects into user-visible content.

The fourth section shows how you create a set of templates, one for HTML and another for plain text. To ensure consistency, you use an abstract class to define the template interface for the page header, body, and footer. Within the templates, process the XML with SimpleXML.

Finally, you're ready to put together the actual application. However, since you've already created all the component parts, the assembly process is short. The biggest difficulty is converting input from both the Web and the command line into a similar format. One piece that is considerably simplified, however, is error handling through the use of exceptions.

Defining Your Database Schema

The first version of the address book contains only a person's first name, last name, and email address. However, you want to leave open the option of adding additional fields at a later time.

You also know that the data is accessed for reading, as you look up existing records. Writes will be relatively infrequent, but will definitely occur.

Based on those criteria, SQLite is a good choice as a database. Your dataset isn't gigantic, nor do you need advanced features such as replication. SQLite also retrieves data very quickly, so it's perfect for pulling records out of the database.

The next step is writing a database schema and creating a new database, as shown in Example -1.

Example-1. Database schema for address book

$sql = <<< _SQL_

CREATE TABLE people (
id INTEGER PRIMARY KEY,
firstname TEXT,
lastname TEXT,
email TEXT);

CREATE INDEX people_firstname_index ON people(firstname); 
CREATE INDEX people_lastname_index  ON people(lastname);
CREATE INDEX people_email_index     ON people(email);

_SQL_;

$db = new SQLiteDatabase('addressBook.db');
$db->query($sql);

Example-1 creates a new database file called addressBook.db and adds a people table with four fields: id, firstname, lastname, and email.

The id field is an integer and the primary key. The other three fields are simply defined as TEXT. Since you need to search against the text fields, you create indexes for all firstname, lastname, and email fields using the CREATE INDEX command.

The Person Class

The next step is creating Person, an object representation of the data. This class encapsulates the address book entry's information into an object you can manipulate. You need to be able to create a new object, get and set information, cycle through all the fields, and convert the object to XML.

Constructor

The class begins with its constructor, as shown in Example-2.

Example-2. Person::_ _construct( )

class Person implements IteratorAggregate {

 

    private $data;     public function _ _construct($person = NULL) {         $this->data = array('firstname' => '',                             'lastname'  => '',                             'email'     => '',                             'id'        =>  0);         if (is_array($person)) {
foreach ($person as $field => $value) {
$this->$field = $value;
}
}
}

Example-2 initializes the $data property as an array of four elements: firstname, lastname, email, and id. The first three hold information about a person, and the last one is the record's unique database key.

When nothing's passed into the constructor, you're left with an empty Person object, which can then be defined. However, you can optionally pass an array of data that contains information about a person.

When this happens, the constructor loops through the array and converts each element into an object property. As you'll see, this class actually overloads property access with _ _get( ) and _ _set( ), so the information is really going inside $data.

You've probably noticed that none of the properties are set by name. For example, there's no call to $this->firstname. Instead, the code references $this->$field.

It's somewhat unusual to refer to a property name with a variable; however, this allows you to avoid hardcoding property names inside the foreach. In general, you want to write generic code like this, as it reduces headaches when you modify your code.

The foreach converts an array with any combination of keys into properties. Assume, for instance, that $person is array('firstname' => 'Rajesh'). Then inside the loop, $field is firstname and $value is Rajesh. Therefore, $this->$field = $value translates to $this->firstname = 'Rajesh'.

When $person holds additional elements, the foreach automatically takes care of every field in turn without any action on your part. Don't worry if someone tries to pass in additional array elements, such as address. These are filtered out inside of _ _set( ).

Encapsulating Property Access with _ _get( ) and _ _set( )

Public properties break encapsulation, so Person implements the property overload methods of _ _get( ) and _ _set( ). The methods in Example-3 actually retrieve and store data from the protected $data property.

 Example-3. Person::_ _set( ) and Person::_ _get( )
public function _ _set($property, $value) {

if (isset($this->data[$property])) {
$this->data[$property] = $value;
}     
}
    public function _ _get($property) {
if (isset($this->data[$property])) {
return $this->data[$property];     
} else {
return false;
}
}


The _ _set( ) method doesn't add all properties to $data. It first uses isset( ) to check that $property already exists within the array. This will return true only for the elements assigned in the constructor. If $property fails this test, the method discards $value. This check prevents you from adding arbitrary data to the object, so you can be sure of the class's properties.

The _ _get( ) method behaves in a similar fashion. If the property isset( ), the method returns the property's value. If it's not set, the method returns false.

Enabling Custom Object Iteration with getIterator( )

Since you're overloading property access, the object is no longer iterable using PHP's default iteration behavior. There's only one class property, $data, and its visibility is restricted from public view.

The fix is to implement the IteratorAggregate interface and to write a getIterator( ) method, as shown in Example-4.

Example-4. Person::getIterator( )
public function getIterator( ) {

 

  return new ArrayObject($this->data); }

This method must return an iterator. Instead of creating a custom iterator, it's better to use SPL's ArrayObject. This object takes an array, such as $this->data, and converts it into an iterator object that acts just like an array.

Converting a Person Object to an XML Document Using DOM

Your final method is toDOM( ), shown in Example-5. This method converts Person into a DOM object for use with the XML functions.

 Example-5. Person::toDOM( )
public function toDOM( ) {
$xml = new DOMDocument('1.0', 'UTF-8'); $xml->formatOutput = true; // indent elements $person = $xml->appendChild(new DOMElement('person'));
foreach ($this as $key => $value) { $person->appendChild(new DOMElement($key, $value)); }    return $xml; }

The goal of Example-5 is to create a DOM object and populate it with child elements for each piece of data stored in the object.

Our first step is instantiating a new DOMDocument. Set the XML version to 1.0 and the encoding to UTF-8. The second line indents the output with spaces. This makes the XML easier to read for humans.

Now you create the DOM elements. The root element is a new person. Next, a foreach loop creates elements for each of the current object's properties.

It's important to loop through $this instead of $this->data. Right now, you happen to store your data inside the $data property, but that could change. If you do loop through $this->data and then change this, you would have to update all your methods. Looping through $this invokes getIterator( ), so as long as you update getIterator( ), it's a seamless change every place else.

The code uses a feature of the DOMElement constructor. When a second parameter is passed, the object creates a text node containing $value and places it as a child of the new DOM element.

Creating and Manipulating a Person Object

Example-6 shows the Person class in action.

Example-6. Creating a new Person

$Rajesh = new Person; $Rajesh->firstname = 'Rajesh';

 

$Rajesh->lastname = 'Kumar';

$Rajesh->email = 'Rajesh@xyz.com';

foreach($Rajesh as $property => $value) {

print "$value\n";

}

print $Rajesh->toDOM( )->saveXML( );

Example-6 creates a new Person, assigns values to the object, and then iterates through it. It also prints the object's DOM representation. This is the result:

firstname: Rajesh
lastname: Kumar
email: Rajesh@xyz.com

id: 0

<?xml version="1.0" encoding="UTF-8"?>
<person>
  <firstname>Rajesh</firstname>
  <lastname>Kumar</lastname>
  <email>Rajesh@xyz.com</email>
  <id>0</id>
</person>

The properties all hold their assigned values; however, since id wasn't changed, it's still at the default value of 0.

Alternatively, you can pass an array to the constructor:

$info = array('firstname' => 'Prem',
'lastname'  => 'Kumari',
'email'     => 'Prem@xyz.com');

$Prem = new Person($info);
print $Prem->toDOM( )->saveXML( );

<?xml version="1.0" encoding="UTF-8"?>
<person>
<firstname>Prem</firstname>
<lastname>Kumari</lastname>
<email>Prem@xyz.com</email>

  <id>0</id>
</person>

Passing an array is functionally equivalent to setting properties one by one, but it's more concise.

The addressBook Class

Now that you have a Person, it's time to turn to the address book. Just like a physical address book, an addressBook object holds a collection of information about people.

The addressBook class gives you two actions: you can either add a new record or search existing records. That's it. Updating and deleting records is left for Version 2.0. However, as always, you want to design the class so that it's easy to add these methods.

Like Person, addressBook has a toDOM( ) method, which exports an entire collection of people to XML as a group.

Constructor

Besides instantiating the object, addressBook's constructor connects to the database, as shown in Example-7.

Example-7. addressBook::_ _construct( )

class addressBook implements IteratorAggregate {
protected $data;
protected $db;

public function _ _construct( ) {
$this->data = array( );
$this->db = new SQLiteDatabase('addressBook.db');
}


The constructor opens the SQLite database addressBook.db that was created earlier section. The result handle is stored in the $db property.

Adding a Person to an addressBook

An empty address book isn't very interesting, so Example-8 implements the addPerson( ) method. This method takes a Person object, converts it into an SQL query, and inserts it into the database.

Example-8. addressBook::addPerson( )
public function addPerson(Person $person) {

 

        $data = array( ); foreach ($person as $fields => $value) { $data[$fields] = "'" . sqlite_escape_string($value) . "'"; }

$data['id'] = 'NULL'; $sql = 'INSERT INTO people '. '(' . join(',', array_keys(  $data)) . ')' . 'VALUES (' . join(',', array_values($data)) . ');';
if ($this->db->query($sql)) { $rowid = $this->db->lastInsertRowid( ); $person->id = $rowid; return $rowid; } else { throw new SQLiteException( sqlite_error_string($this->db->lastError( )), $this->db->lastError( )); }

Since addPerson( ) will work only on a Person object, the argument is type hinted to require a Person.

This method's main job is converting the Person object into an SQL statement. Like Person::_ _construct( ), the goal is to have little or no Person-specific details inside of addPerson( ).

You want to be able to update Person to include, for example, a cell phone number field, without modifying addressBook. This reduces the coupling between the classes, which is a major design goal of object-oriented programming. A class should be able to modified without affecting the behavior of any of the other classes.

The top half of the method iterates through the fields returned by $person's iterator. It creates an array whose keys are the object's properties. The array's values are escaped SQLite strings, wrapped inside single quotation marks.

To ensure that SQLite generates the correct row ID, you explicitly set the id element to NULL. This prevents someone from assigning their own id and disturbing the auto-increment sequence.

The general syntax for an SQL INSERT statement is INSERT INTO table (field1, field2, ..., fieldN) VALUES (value1, value2, ..., valueN). So, you need a way to put all the field names into one comma-separated list and all the values into another.

Fortunately, the $data array's keys are the field names and the array's values are the escaped SQL values. Therefore, you can extract the desired portion of the array with array_keys( ) or array_values( ).

Those arrays are then join( )ed together with commas (,) to create the proper SQL statement. This algorithm works regardless of the name or number of fields in the database.

If the query succeeds, the new primary key is assigned to $rowid from SQLite's lastInsertRowid( ) method. The method also updates $person to contain the correct value instead of 0. This code takes advantage of PHP 5's pass-by-reference feature for objects. Without it, the change in $person would exist only in the local copy within the method. The method returns $rowid on a successful query or throws an SQLiteException on an error.

Because DOM already throws exceptions, it's cleaner for you to manually throw SQLiteExceptions of your own. This allows you to process all errors, from DOM and from SQLite, in a consistent manner.

Since the SQLite extension isn't throwing the error, you need to populate the exception's message and code fields yourself. SQLite's lastError( ) method returns an integer error code that describes of the problem. You can convert that number into an English description with sqlite_error_string( ). These are the two pieces of data you need to pass when you create the SQLiteException.

Example-9. Adding a Person into an addressBook
      
$Rajesh = new Person; $Rajesh->firstname = 'Rajesh'; $Rajesh->lastname  = 'Kumar'; $Rajesh->email     = 'Rajesh@xyz.com';
try { $ab = new addressBook; $ab->addPerson($Rajesh); print $Rajesh->toDOM( )->saveXML( ); } catch (Exception $e) { // Error! }
The results look like: <?xml version="1.0" encoding="UTF-8"?> <person> <firstname>Rajesh</firstname> <lastname>Kumar</lastname> <email>Rajesh@xyz.com</email>

 

  <id>1</id> </person>

As you can see, the id element is 1 instead of the default value of 0.

When there's an SQLite error, such as when the people table does not exist, the addPerson( ) method throws an exception and print $Rajesh->toDOM( )->saveXML( ); is never called. Instead, control immediately jumps to the catch block for error processing. For now, the examples are only trapping the error, not processing it. Later on, once you assemble the full application, you'll add in more complete error handling.

Searching for People Within an addressBook

It's boring just to enter people into an address book. The real fun comes when you retrieve them using search( ), as shown in Example-10.

Example-10. addressBook::search( )
public function search(Person $person) { $where = array( ); foreach ($person as $field => $value) {
if (!empty($value)) { $where[  ] = "$field = '" . sqlite_escape_string($value) . "'"; } }
$sql = 'SELECT * FROM people'
if (count($where)) $sql .= ' WHERE ' . join(' AND ', $where); }

if ($people = $this->db->query($sql)) { foreach ($people as $person) { $this->data[  ] = new Person($person); }

 

            return $people->numRows( ); } else { throw new SQLiteException( sqlite_error_string($this->db->lastError( )), $this->db->lastError( )); }
}

The method works similarly to addPerson( ), using a foreach loop to build up an SQL statement. However, unlike an INSERT, a SELECT doesn't require a parallel set of records joined by commas. Instead, fields and values are separated with an equals sign (=).

To keep searches loose, the WHERE clause doesn't include any empty( ) valued fields. This allows you to find all the people with a firstname of Rajesh by keeping the other fields as blanks.

Since an empty WHERE clause is illegal, WHERE is appended to $sql only if $where has at least one element. These elements are then ANDed together using join( ).

The query results are retrieved using the SQLite iterator that fetches rows as associative arrays. Inside the loop, pass the result array to Person. This creates a new Person object with all the correct details that are stored in the $data property.

Finally, the method returns the total number of found rows, using the numRows( ) method of the SQLite result object. When no rows are found, this is equal to 0.

Example-11 shows one search that finds records and another that fails.

Example-11. Searching an addressBook

$ab = new addressBook;
$Rajesh = new Person;
$Rajesh->firstname = 'Rajesh';
print 'Rajesh: ' . $ab->search($Rajesh) . "\n";
$Prem = new Person;
$Prem->firstname = 'Prem';
print 'Prem: ' . $ab->search($Prem) . "\n";

Rajesh: 1
Prem: 0

Since you've already inserted Rajesh into the address book back in Example-9, the first search returns 1. However, Prem is not to be found.

The search( ) method is quite basic. It doesn't allow you to find all people named Rajesh or Prem in a single query, for instance. However, you can run two search( )es against the same address book to create a composite search result.

Converting an addressBook Object to an XML Document Using DOM

It's not very interesting merely to see the number of matches for your search. What you really want is access to the information about each person. Like Person, this is accomplished using a combination of iterators and XML, as shown in Example-12.

Example-12. addressBook::getIterator( )
public function getIterator( ) {
return new ArrayObject($this->data);
}

As in Example-4, getIterator( ) returns the object's $data property.

Example-13 contains the code for the addressBook::toDOM( ) method.

Example-13. addressBook::toDOM( )
public function toDOM( ) { $xml = new DOMDocument('1.0', 'UTF-8'); $xml->formatOutput = true; // indent elements $ab = $xml->appendChild(new DOMElement('addressBook')); foreach ($this as $person) { $p = $person->toDOM( ); $p = $xml->importNode($p->documentElement, true); $ab->appendChild($p); }   
return $xml; }

The toDOM( ) method here acts similarly, but not identically, to the toDOM( ) method in Person. Its first half is the same, but the second is different.

This method also creates a new DOMDocument and uses the same set of XML versions and document encodings. It creates a root element, too, but this time it's called addressBook instead of person.

Inside the foreach, there's no need to iterate through $person like you did inside Person::toDOM( ). Instead, you can just ask $person to convert itself into a DOM object using its own toDOM( ) method.

However, it's illegal to directly append parts of one DOMDocument to another. You must first convert the object using DOM's importNode( ) method. The first parameter is the part of the document you want, and the second indicates whether you want to make a deep or shallow copy. The call in this example grabs everything from the root node down and does a deep copy. The imported nodes are then appended to the address book to create a master XML document that contains all the matching People.

With toDOM( ), you can view the results of your searches in Example -14.

Example-14. Converting search results to XML
$Prem = new Person; $Prem->firstname = 'Prem'; $Prem->lastname = 'Kumari'; $Prem->email = 'Prem@xyz.com'; $ab = new addressBook; $ab->addPerson($Prem); $ab->search(new Person); print $ab->toDOM( )->saveXML( );
<?xml version="1.0" encoding="UTF-8"?> <addressBook> <person> <firstname>Rajesh</firstname> <lastname>Kumar</lastname> <email>Rajesh@xyz.com</email> <id>1</id> </person> <person>

 

    <firstname>Prem</firstname> <lastname>Kumari</lastname> <email>Prem@xyz.com</email> <id>2</id>
</person> </addressBook>

Perfect! Here's an XML document containing all the people in the address book. Additionally, since neither search( ), getIterator( ), nor toDOM( ) hardcode any details about Person, they're not affected when you modify the Person class.

Since the application needs to produce multiple output formats, you need a way to control the display. For example, the HTML output starts with <html>, but you certainly don't want that to appear on the command line.

Good programming style says that it's bad form to mix programming and display logic. This leads to messy code because everything becomes intertwined. Additionally, since you already know you need a minimum of two different types of output, doing everything inline not only makes it harder to add additional output styles, but it's also more difficult to maintain your existing styles.

Therefore, you should create template objects. Each object should have the same set of display methods, such as getHeader( ) and getFooter( ). However, they'll return different content—for example, HTML or plain text.

This is the perfect place to use an abstract base class. The abstract class specifies the exact names and prototypes for all your methods. Then, you create one class for each format and make sure that each of those classes extends the base.

The Template class in Example-15 has four methods.

Example15. Template abstract class

        abstract class Template {
        abstract public  function getHeader( );
        abstract public function  getBody(DOMDocument $dom);
abstract public function getFooter( );  
public function printAll(addressBook $ab) {
print $this->getHeader( ); print $this->getBody($ab->toDOM( )); print $this->getFooter( ); } }

Two methods—getHeader( ) and getFooter( )—take no parameters. They display the page header and footer, respectively.

In between them is getBody( ). It requires a DOMDocument, which should contain an XML address book full of the information that you want to print.

The last and only nonabstract method, printAll( ), is a convenience method that calls the other three methods and prints them out.

Creating an HTML Template

The HTML class is mostly static HTML, but the getBody( ) method is generated dynamically, as shown in Example-16.

Example-16. htmlTemplate class

class htmlTemplate extends Template {
      public function getHeader( ) {
      $action = $_SERVER['PHP_SELF'];
      $header = <<< _HEADER_

 

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> <html> <head> <title>Address Book</title> </head> <body>
<form action="$action" method="POST"> <select type="select" name="mode"> <option value="add">Add</option> <option value="search" selected="selected">Search for</option>
</select>
<label for="firstname">First Name</label> <input type="text" name="firstname" id="firstname" size="8">
<label for="lastname">Last Name</label> <input type="text" name="lastname"  id="lastname" size="14">
<label for="email">Email</label>
<input type="text" name="email" id="email" size="14">
<input type="submit" value="Do it!">
</form> _HEADER_;
return $header;
}
public function getBody(DOMDocument $dom) {
$body = "<table>\n";
$people = simplexml_import_dom($dom);
foreach ($people as $person) {
$body .= "<tr>\n<td>" . $person->firstname . ' ' .
$person->lastname . "</td>\n<td>" .
$person->email . "</td></tr>\n";
}
$body .= "</table>\n";
return $body;   
}

public function getFooter( ) { $footer = "</body>\n</html>\n"; return $footer; }
}

While getHeader( ) is mostly static HTML, it contains an HTML form that allows you to both insert new people into and search the address book. The form's action is set to $_SERVER['PHP_SELF'], so your script also processes the form. The input elements are named the same as the properties of your Person class. This allows you to instantiate a Person object from the form without needing to munge the input data.

The getBody( ) method in Example-16 takes a DOM object, but that doesn't mean it needs to parse the XML with DOM. SimpleXML is the perfect way to handle this basic type of XML document.

The simplexml_import_dom( ) function converts an object from DOM to SimpleXML. Now it's no problem to create an HTML table: you iterate though the SimpleXML object using foreach, turning every $person into another HTML table row.

Creating a Plain-Text Template

The text template, shown in Example-17, is less complex than the HTML version because its header and footer are empty.

Example-17. textTemplate class

class textTemplate extends Template {
      private $nameLength;
      private $emailLength;
  
      public function _  _construct($nameLength = 30, $emailLength = 30) {
$this->nameLength  = (int) $nameLength;
$this->emailLength = (int) $emailLength;
}
public function getHeader( ) { return; }
public function getBody(DOMDocument $dom) { // 7 is the size of the column divider // spacers: "| ", " | ", and " |" $lineLength = $this->nameLength + $this->emailLength + 7; $text = str_repeat('-', $lineLength) . "\n"; $people = simplexml_import_dom($dom); foreach ($people as $person) { $name = $person->firstname . ' ' . $person->lastname; $text .= sprintf("| %{$this->nameLength}s "| %-{$this->emailLength}s |\n", $name, $person->email);
}

 

        $text .= str_repeat('-', $lineLength) . "\n";
return $text;   
}
public function getFooter( ) { return; }
}

This class has a constructor with two optional arguments, $nameLength and $emailLength. These parameters control the length of the printed name and email columns, respectively. They both default to 30, but you can adjust them if your data is particularly long or short.

Again, the getBody( ) method is the most interesting of the three methods. Since there's no way to automatically place data into a table from the command line, the method uses str_repeat( ) and sprintf( ) to recreate a table. The calls to str_repeat( ) create the top and bottom table borders.

Inside the foreach, sprintf( ) formats the individual lines. The string that reads | %{$this->nameLength}s | %-{$this->emailLength}s |\n has two special codes. The first, %{$this->nameLength}s, turns the input into a $this->nameLength character-wide, right-aligned, space-padded string. The second, %-{$this->emailLength}s, does something similar, but aligns the data on the left side instead of the right.

Displaying Multiple Output Formats with a Template

Now that you've defined your templates, the next step is printing out address books. This is done in Example -18.

Example-18. Printing addressBook results using templates

try {
// Create address book and find all people
$ab = new addressBook; $ab->search(new Person);
// Create HTML template $template = new htmlTemplate( );

 

    // Convert address book XML and print results
$template->printAll($ab); } catch (Exception $e) { // Error
}

Alternatively, setting $template to a new textTemplate produces:

-------------------------------------------------------------------

|                 Rajesh Kumar | Rajesh@xyz.com                 |
|                   Prem Kumari | Prem@xyz.com                   |

-------------------------------------------------------------------

Assembling the Application

Finally, you can integrate the classes and templates into a complete application. This section creates a dual web and command-line program that shares the same set of code but abstracts out the details, such as processing the different sources of input data.

Creating the Web Version

The web version gets its input from a form embedded in the HTML template's header, as shown in Example-19.

Example-19. Web-enabled address book application

// Configure format-specific details
$input = $_POST; $template = new htmlTemplate; // Set mode if (isset($input['mode'])) { $mode = $input['mode']; } else { $mode = false; }
try { // Create address book $ab = new addressBook; // Load data into Person $person = new Person($input);

 

    // Add person, if necessary if ($mode =  = 'add') { $ab->addPerson($person); }

    // Return results $ab->search($person); // Print page

    $template->printAll($ab); } catch (Exception $e) { $ob = ob_start( ); print $e; error_log(ob_get_clean( )); }

At the top of Example-19, you set a few configuration variables. Since this is the web version, the $input comes from $_POST and the $template is an htmlTemplate. Later, you will set $input and $template to different values in the command-line version.

The $mode variable, which controls whether you should add a new person, is assigned using the mode element of the input source.

Once everything is set up, create the addressBook and Person objects. Use the $input array to configure Person.

Now, if the $mode is set to add, call addPerson( ) to insert the person into the database. This converts Person into SQL and makes a new entry.

The next step is populating the address book with results. If you're doing a search, $ab contains all the matching records. If you're inserting a record, $ab contains the new record.

The last action is the call to print out your template methods using the handy printAll( ) method instead of making individual calls.

All that assumes, of course, that everything goes smoothly and you encounter no exceptions. Many of the underlying methods throw exceptions, some because the extension does so automatically and some because you're actually throwing them yourself.

However, here's where the benefit of exceptions shines through. You don't need to worry which methods do or don't throw exceptions, nor do you need to care if the exception is actually thrown by a second method called by a first one. Everything propagates up to this location and is caught by your catch block. You've effectively replaced the necessity to check the return value of every function and method in your application with five lines:

} catch (Exception $e) {
$ob = ob_start( );
print $e;
error_log(ob_get_clean( ));
}

You can print an exception to get a full set of debugging messages. By wrapping the print call within a output buffer, you can redirect these lines from the screen to the error log.

For example, when the SQLite can't find the person database table, you get:

[Thu Mar 9 11:20:18 2010] [error] exception 'SQLiteException' with message

  'SQL logic error or missing database' in

  /www/www.example.com/addressBook/index.php:48

Stack trace:

#0 /www/www.example.com/addressBook/index.php(48): addressBook->search(Object(Person))

#1 {main}

Sure, you still need to check occasional return values at the component level, i.e., inside individual methods. However, those checks are all encapsulated within your objects, so they occur at a lower level. When you assemble a program by combining multiple objects together, which is a higher level of programming, you only need to handle exceptions.

One thing your code doesn't do is print out any error message to a person using the address book.

Creating the Command-Line Version


The largest difference between the command-line version and the web version is that you need to parse command-line options yourself. The easiest way to do this is with PEAR's Console_Getopt class, as shown in Example-20.

Example-20. Command-line-enabled address book application

require_once('Console/Getopt.php');
$opts = new Console_Getopt; // Accept these four optional command line flags. $longopts  = array('mode=', 'firstname=', 'lastname=', 'email=');
$parsed = $opts->getopt($argv, NULL, $longopts);
// Strip leading "--" from flag name
// Convert from getopt( ) array return format to one like $_POST
foreach ($parsed[0] as $opt) {
$key = substr($opt[0], 2); $input[$key] = $opt[1]; }

$template = new textTemplate;

Console_Getopt parses $argv and breaks it apart into option pairs. You specify any or all of four possible options: --mode, --firstname, --lastname, and --email.

Since getopt( ) doesn't return an array that's formatted like $_REQUEST, you need to rework the array it returns. This requires you to strip the leading -- from the option name and reorganize how the information is stored.

After fixing $input, set the $template to new textTemplate.

And now you're done. There's nothing left to modify in the program. This command runs the command-line version of the address book program, ab.php, and searches for people with a first name of Rajesh:

$ php ab.php --firstname=Rajesh

-------------------------------------------------------------------
|                 Rajesh Kumar | Rajesh@xyz.com                 |
-------------------------------------------------------------------

Passing --firstname=Rajesh on the command line is equivalent to setting the form element firstname to Rajesh.

Creating a Unified Program

The final step is unifying the two versions into a single program. This requires you to differentiate between the command-line and web versions of PHP.

The function php_sapi_name( ) checks which version of PHP you're running and returns its name. When you're running under the command-line version, it returns cli. The unified application code shown in Example-21.

Example-21. Unified address book application

// Configure format-specific details
if (php_sapi_name( ) =  = 'cli') { require_once('Console/Getopt.php'); $opts = new Console_Getopt; $longopts  = array('mode=', 'firstname=', 'lastname=', 'email=');
$parsed = $opts->getopt($argv, NULL, $longopts);
foreach($parsed[0] as $opt) {
$key = substr($opt[0], 2); $input[$key] = $opt[1]; }
$template = new textTemplate; } else { $input = $_REQUEST; $template = new htmlTemplate; }
// Set mode
if (isset($input['mode'])) { $mode = $input['mode']; } else { $mode = false; }

try {
// Create address book
$ab = new addressBook; // Load data into Person $person = new Person($input);

 

    if ($mode =  = 'add') { $ab->addPerson($person); } 

    // Return results
$ab->search($person); $template->printAll($ab); } catch (Exception $e) { $ob = ob_start( ); print $e; error_log(ob_get_clean( )); }

Now the program will parse $argv and print text when it's run from the command line, but also produce HTML when executed under a web server.

Wrap-Up and Future Directions

Although you can write a similar address book application in PHP 4, this version is better encapsulated, doesn't need to worry about passing objects as references, has simplified error handling, and makes it easy to use XML.

In PHP 4, there's no concept of visibility, so all object properties and methods are public. However, in PHP 5, you're able to effectively wall off public properties as protected data, yet still allow people easy access using _ _get( ) and _ _set( ).

By making _ _set( ) accept only the properties defined in the object constructor, you're able to limit exactly which properties are and aren't valid. This allows you to iterate through properties, creating SQL statements and DOM objects, without worrying that you'll encounter some unexpected data.

Additionally, the application takes advantage of PHP 5's automatic pass-by-reference behavior for objects. After you insert a new Person into the database, you can update its id property within the addPerson( ) method and have this change affect the original object.

The program also uses exceptions to simplify error handling. DOM automatically throws exceptions for any errors mentioned in the DOM specification. SQLite also throws exceptions from its constructor. By manually throwing a few additional SQLiteExceptions, you consolidate all the error handling into a single try/catch block.

Finally, the interoperability of the new PHP XML extensions allows you to easily create documents with DOM, yet access them for printing using SimpleXML. Furthermore, it's no problem to substitute XSLT for SimpleXML within your templates, or to search an addressBook object using XPath.

However, the address book application created in this article provides a solid foundation for future enhancements. For instance, it would be nice to allow people to edit and delete entries from the address book, or to enlarge the number of fields from beyond first name, last name, and email address.

Also, as mentioned earlier, this application doesn't print a friendly error message when an exception is thrown. One solution is to modify Template to provide a getErrorMessage( ) method, which displays format-appropriate text.

Another nifty feature would be enabling this application as a web service, using either SOAP or REST. REST is a different approach to web services, where you make requests using HTTP methods such as GET and POST, and the method type tells the server what action it should take. For example, GET tells the server you want to retrieve existing data, whereas POST means you want to update existing data. The server then replies with the results in an XML document that you can process.

Since the application already uses XML internally, it should be a simple extension to create an xmlTemplate. For more on REST, see the REST Wiki.

Finally, you can add the ability to read and write other address book formats.

For more information about PHP5 you can reach the author from the following url.
KDatta23@gmail.com








}