10.3 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.

10.3.1 Constructor

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

Example 10-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 in Section 10.1. The result handle is stored in the $db property.

10.3.2 Adding a Person to an addressBook

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

Example 10-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 10-9 inserts a new Person into a clean addressBook.

Example 10-9. Adding a Person into an addressBook
$rasmus = new Person;

$rasmus->firstname = 'Rasmus';

$rasmus->lastname  = 'Lerdorf';

$rasmus->email     = 'rasmus@php.net';

try {

    $ab = new addressBook;


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

} catch (Exception $e) {

    // Error!


The results look like:

<?xml version="1.0" encoding="UTF-8"?>







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 $rasmus->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.

10.3.3 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-10.

Example 10-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 Rasmus 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 10-11 shows one search that finds records and another that fails.

Example 10-11. Searching an addressBook
$ab = new addressBook;

$rasmus = new Person;

$rasmus->firstname = 'Rasmus';

print 'Rasmus: ' . $ab->search($rasmus) . "\n";

$zeev = new Person;

$zeev->firstname = 'Zeev';

print 'Zeev: ' . $ab->search($zeev) . "\n";

Rasmus: 1

Zeev: 0

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

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

10.3.4 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 10-12.

Example 10-12. addressBook::getIterator( )
    public function getIterator( ) {

        return new ArrayObject($this->data);


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

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

Example 10-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);



        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 10-14.

Example 10-14. Converting search results to XML
$zeev = new Person;

$zeev->firstname = 'Zeev';

$zeev->lastname = 'Suraski';

$zeev->email = 'zeev@php.net';

$ab = new addressBook;


$ab->search(new Person);

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

<?xml version="1.0" encoding="UTF-8"?>















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.