We all know how to fetch a row from MySQL, read the data, and save some data back. It's fairly simple and straightforward, with nothing tricky is going on behind the scenes. However, there is much to be gained by using an OOP approach to managing data in your database. This article is a sketch of how one might design an Object-Oriented approach to managing records in a database. All the logic inherent in your data can be encapsulated into discreet Record objects, which provide a single codebase for validation, translation, and manipulation. With the release of Zend Engine 2 and PHP5, PHP developers will have some powerful new object oriented tools to work with, which will make this approach even more attractive.
Some of the advantages of representing your database data with objects over raw associative arrays:

Accessor Methods

Accessors are methods that wrap (or give access to) instance variables in a class. For example, if I have a class User with an instance variable $username, I would write the accessor methods User->username() and User->setUsername() that return and set the instance variable.

<?php
class User {
    var 
$username;

    function 
username() {
        return 
$this->username;
    }

    function 
setUsername($newUsername) {
        
$this->username $newUsername;
    }
}
?>
There is a good reason for all this "extra code". It gives the developer (you) more flexibility to change how the guts of your class work, without breaking other php code which uses the class. Consider the following evolutionary version of our trusty User class.

<?php
class User {
    var 
$_data = array(); // associative array containing all the attributes for the User

    
function username() {
        return !empty(
$this->_data['username']) ? $this->_data['username'] : '(no name!)';
    }

    function 
setUsername($newUsername) {
        if (
$this->validateUsername($newUsername)) {
            
$this->_data['username'] = $newUsername;
        }
    }

    function 
validateUsername(&$someName) {
        if (
strlen($someName) > 12) {
            throw new 
Exception('Your username is too long'); // PHP5 only
        
}
        return 
true;
    }
}
?>
Obviously, this gives tremendous control over how to access the properties of an object. If a programmer had accessed the username property directly, the changes above would have broken her code. By using the accessor methods, however, the code referencing our User class gets the additional validation functionality without changing anything.
Note that the username validation code is in a separate method from the setUsername() method. This will come in handy when validating the object before saving to the database. Also, it's a good rule of thumb that the less a method or class needs to do, the more reusable it will be. This is even more apparent when you start subclassing, and want to override a specific part of a class's parent behavior. If the methods are small and specific, this is a snap. If the methods are bloated and multipurpose, you will probably end up duplicating a lot of the super class code in your subclasses.
For instance, suppose Admin is a subclass of User. There may be a different, less lenient method for validating passwords for admin users. Better to only override the validation method then the entire setUsername() method.

More on Accessors

Below are some other examples of how accessors can be used to good effect. Calculated results are possible, instead of simply returning the static data from the array. One other useful function accessors can play is updating cached values. Since all changes must go through the setX() method, that is a perfect place to reset some cached value which is dependent on X.
Our class hierarchy is evolving as well:
The Record class handles the details of accessing the $_data array, calling validation methods before properties are modified, and posting change notifications to Records, as well as to a central ObjectStore instance.

<?php
class User extends Record {

    
// --- OMITTED CODE --- //

    /**
    * Do not show the actual password for the user, 
    * only some asterixes with the same strlen as the password value.
    */
    
function password() {
        
$passLength strlen($this->_getData('password'));
        return 
str_repeat('*'$passLength);
    }
    
/**
    * Setting the user password is not affected.
    */
    
function setPassword($newPassword) {
        
$this->_setData('password'$newPassword);
    }

    
/**
    * fullName is a derived attribute from firstName and lastName
    * and does not need to be stored as a variable.
    * It is therefore read-only, and has no 'setFullname()' accessor method.
    */
    
function fullName() {
        return 
$this->firstName() . " " $this->lastName();
    }

    
/**
    * Spending limit returns the currency value of the user's spending limit.
    * This value is stored as an INT in the database, eliminating the need
    * for more expensive DECIMAL or DOUBLE column types.
    */
    
function spendingLimit() {
        return 
$this->_getData('spendingLimit') / 100;
    }

    
/**
    * The set accessor multiplies the currency value by 100, so it can be stored in the database again
    * as an INT value.
    */
    
function setSpendingLimit($newSpendLimit) {
        
$this->_setData('spendingLimit'$newSpendLimit 100);
    }

    
/**
    * The validateSpendingLimit is not called in this class, but is called automatically by the _setData() method
    * in the Record superclass, which in turn is called by the setSpendingLimit() method.
    */
    
function validateSpendingLimit(&$someLimit) {
        if (
is_numeric($someLimit) AND $someLimit >= 0) {
            return 
true;
        } else {
            throw new 
Exception("Spending limit must be a non-negative integer"); // PHP5 only
        
}
    }
}

/**
* Record is the superclass for all database objects.
*/
abstract class Record {
    var 
$_data = array();
    var 
$_modifiedKeys = array(); // keeps track of which fields have changed since record was created/fetched

    /**
    * Returns an element from the $_data associative array.
    */
    
function _getData($attributeName) {
        return 
$this->_data[$attributeName];
    }

    
/**
    * If the supplied value passes validation, this
    * sets the value in the $_data associative array.
    */
    
function _setData($attributeName$value) {
        if (
$this->validateAttribute($attributeName$value)) {
            if (
$value != $this->_data[$attributeName]) {
                
$this->_data[$attributeName] = $value;
                
$this->_modifiedKeys[] = $attributeName;
                
$this->didChange();
            } else {
                
// the new value is identical to the current one
                // no change necessary
            
}
        }
    }

    
/**
    * For an attribute named "foo", this looks for a method named "validateFoo()"
    * and calls it if it exists.  Otherwise this returns true (meaning validation passed).
    */
    
function validateAttribute($attributeName, &$value) {
        
$methodName 'validate' $attributeName;
        if (
method_exists($this$methodName)) {
            return 
$this->$methodName($value);
        } else {
            return 
true;
        }
    }

    function 
didChange() {
        
// notify the objectStore that this record changed
    
}
}
?>
Now that we have an abstract superclass (Record), we can move a lot of code out of the User class, and let the User subclass focus on User-specific items like accessors and validation methods. You may note that there is no SQL code in our Record classes. This is not an omission! The ObjectStore class (to be covered in part II) is responsible for all interaction with the database, and for the actual instantiation of our Record subclasses. This lets us keep our Record classes lean and efficient, which can be a big factor when you are dealing with many objects.
If you're interested in seeing the complete source code this article is based on (without all the syntax errors which are probably in this article), send me an email at sam@360works.com