The GenericObject Class

The GenericObject Class

The GenericObject class is what is known as an abstract superclass. It is so called because it will never be instantiated directly from within your code. Rather, it will always be instantiated through an extension or subclass that inherits its properties and methods.

If object inheritance doesn't ring a bell, you may want to refer to Chapter 3, "Putting Objects to Work,'' where they were examined in more detail.

Important 

The purpose of GenericObject is to allow you to neatly and concisely represent as a class a single row (or tuple) of a single table in your database, where that table describes a single entity, and therefore manage that row in an object-oriented manner.

When to Use GenericObject

Prime examples of tables you may have in your application that are just perfect for class implementation using GenericObject are user, customer, product, order, and so forth.

The tables you should avoid attempting to represent are those that simply link two entities in some way. For example, you may have a table called order_product with columns order_id, product_id, and quantity. You would be very much applauded for doing so. This is, after all, an excellent example of the third normal form of database normalization at work (see http://databases.about.com/library/glossary/bldef-3nf.htm for more information).

However, such tables are not representative of entities in their own right because they have no properties that can be read or set, even though they may have qualifiers, such as the quantity field in the previous example.

What GenericObject Allows You to Do

The functionality of GenericObject is not purely symbolic, nor is it merely a component of some toolkit for ensuring fastidious and unerring OOP-compliance. It is a genuinely useful tool.

For any given row (say, the 314th row) in any given table (say, the order table), you can represent that row as an object that extends the GenericObject class. This allows you to quickly and effectively:

  • Read any given property (column) of that object (row)

  • Set any given property (column) of that object (row)

  • Save your changes to that object (row)

Crucially, you can also create brand-new instances of your particular entity, set its properties, and save your changes in exactly the same way.

Consider the example of a customer object. You would probably have a table in your database schema called customer that has columns id, first_name, last_name, addr_line_1, and so forth.

Say that you are interested in the first name of the customer, which is represented by an ID of 3139. The following logic would apply:

  • Instantiate a customer object (the class of which extends GenericObject) with ID 3139.

  • Get the first_name property of that object.

And that's it.

What about if you wanted to then change the value of that first name from John to Jane. How would you go about doing that?

  • Instantiate a customer object (the class of which extends GenericObject) with ID 3139.

  • Set the first_name property of that object to be Jane.

  • Save your changes.

Say that you wanted to create a brand-new customer. The approach would be much the same:

  • Instantiate a customer object (the class of which extends GenericObject) with no parameters.

  • Set the first_name property of that object to be Jane.

  • Save your changes.

  • Get the ID just allocated to your customer for future reference.

In this last example, you do not pass any identifying parameter when you instantiate the class. This tells GenericObject that you are creating a brand new instance of the entity in question.

After you have saved, the ID that has been allocated to this new row by the database will then be a property that you may retrieve for your own use.

It must be admitted that all the above examples could easily be implemented using simple, procedural code with embedded SQL statements. Perhaps that variety of implementation might even be slightly quicker than using GenericObject. But this is often true of object-oriented programming, of course, and the value in adopting this approach is the additional human clarity introduced by it. The extra few hours it might take to implement the preceding approach may well save you hundreds of hours in weeks to come, when you may need to maintain or evolve your code.

Assessing Suitability for Implementation

There is just one big prerequisite for using GenericObject. Your entities in your database tables must all have a unique ID that is their primary key and that increments automatically.

This is almost always certainly the case. It is good database design to have a primary key, and it almost always makes sense to have a numeric primary key for reasons of speed, even when another column lends itself naturally to being a primary key, too.

A good example (in PostgreSQL) of a database that lends itself well to implementation in your application using GenericObject is as follows:

   CREATE TABLE "user" (     "id" SERIAL PRIMARY KEY NOT NULL,     "username" character varying(32),     "first_name" character varying(64),     "last_name" character varying(64)    ); 

As you can see, we have an id column uniquely representing each user, and properties username, first_name, and last_name, which we can read or write.

Bad examples for GenericObject implementation include:

   CREATE TABLE "airport" (     "iata_airport_code" character(3) PRIMARY KEY NOT NULL,     "airport_name" character varying(128),     "airport_city" character varying(128)    ); 

The preceding is perfectly valid and quite plausible. Airports are often uniquely identified by their three-letter IATA code (LAX for Los Angeles International, JFK for Kennedy Airport, and so forth). This is not, however, suitable for GenericObject implementation without the addition of a numeric ID column, which must be the primary key. The reason for this is that GenericObject expects new instances to have their unique identifiers produced by the database and does not allow the user to set them explicitly. A three-letter airport code is not something the database knows to generate automatically; a numeric identifier is.

Another bad example is something like the following:

   CREATE TABLE "user_group" (     "user_id" int2,     "group_id" int2,     PRIMARY KEY (user_id, group_id)    ); 

This is a classic example of an associative table used in the practice of good normalization techniques. It does not, however, readily represent an entity; also, it sports a composite primary key. Using GenericObject here is definitely a no-no.

As a rule, if you can happily prefix the name of the table with an article (that is, the, a, or an) and it still makes grammatical sense, you have an entity and, as long as a numeric primary key is in place, using GenericObject is feasible.

Typical GenericObject Implementation

Consider the example of the user entity from the previous section, doubtless used to contain user logins to some closed or semiclosed Web application, such as a corporate intranet.

Without the benefit of GenericObject, we would probably implement a corresponding class as follows:

   class User {     private $user_id;     public function __construct($id) {             $this->user_id = $id;     }     public function GetField($strFieldName) {             // ...     }     public function SetField($strFieldName,$strValue) {             // ...     }     public function Save() {             // ...    }     public function Destroy() {             // ...     }     // etc ...    }; 

This approach means, of course, that a huge copy-and-paste job is necessary to implement the basic get and set methodology for each entity. Not only that, but the resulting core functionality for each entity class (such as AddToGroup for user) is obfuscated from view by the noncore entity management functionality. Your class will be littered with the methods illustrated in the preceding code. All these are important, but they are off-the-shelf routines common to every class, and of little interest to you as a programmer. The important routines will be those that are specific to and tailored for that class. The off-the-shelf and unimportant routines' very existence will make it harder to work with those bespoke, and therefore important, routines.

Using GenericObject, we implement our class simply as follows:

   class User extends GenericObject {     public function __construct($id) {             $this->initialize("user", $id);     }    } 

We'll introduce the code for GenericObject itself shortly. Don't panic.

In practice, after you have defined your basic extension of GenericObject as a new class called User, you can then put it to use. Before you do that, you might want to populate your user table with some data, so here's some test data for you to use. You can paste the following SQL statements directly into a console session with PostgreSQL:

   COPY "user" (id, username, first_name, last_name) FROM stdin;    1       ed              Ed      Lecky-Thompson    2       steve           Steve   Nowicki    3       alec            Alec    Cove    4       heow            Heow    Eide-Goodman    5       john            John    Doe    6       jane            Jane    Doe    \.    SELECT pg_catalog.setval('user_id_seq', 6, true); 

With some test data in the database, and using the new version of your User class that makes use of GenericObject, the following simple code will produce useful output:

   $objUser = new User(1);    $strUsername = $objUser->GetField("username");    print $strUsername; 

Using the preceding data set as an example, this would produce ed as its output because that is the value for the username column in the row where the id is 1.

Using the instantiation parameter 2 would yield steve, 3 would yield alec, and so on.

Updating properties of an entity using GenericObject is just as easy.

   $objUser = new User(1);    $strUsername = $objUser->GetField("username");    print $strUsername . "<br />\n";    $objUser->SetField("username", "edward");    $strUsername = $objUser->GetField("username");    $objUser->Save();    print $strUsername . "<br />\n"; 

In the previous example, the username is changed from ed to edward. Calling the Save() method generates and executes the necessary SQL statements to make your changes to the object permanent.

Creating brand-new users is also child's play:

   $objUser = new User();    $objUser->SetField("username", "clive");    $objUser->SetField("first_name", "Clive");    $objUser->SetField("last_name", "Gardner");    $objUser->Save();    $id = $objUser->GetID();    print $id; 

GenericObject is neat in that upon saving changes to a new entity, it takes the time to figure out what ID has been allocated by the database. The database will have looked at the most recent ID used to identify a row in the database, incremented it by one, and given it to your new row. In order to be able to make good use of your new row, GenericObject determines the actual value of id that has just been allocated. This means that any subsequent changes you make to that object will be recorded with an appropriate UPDATE statement completely transparently, by making use of that new id.

That's the instruction manual over and done with, but as a PHP professional, you want to know how it works, don't you?

Meet the Parent

Take a look at the source code for the GenericObject class. We list the complete code to start with, and then we look at the methods and properties in more detail. Save this class as genericobject.phpm.

   <?    class GenericObject {      # Member Variables      private $id;      private $table_name;      private $database_fields;      private $loaded;      private $modified_fields      # Methods      public function Reload() {        $sql = new sql(0);        $id = $this->id;        $table_name = $this->table_name;        $sql->query("SELECT * FROM \"$table_name\" WHERE id='$id'");        $result_fields = $sql->get_row_hash(0);        $this->database_fields = $result_fields;        $this->loaded = 1;        if (sizeof($this->modified_fields) > 0) {          foreach ($this->modified_fields as $key => $value) {            $this->modified_fields[$key] = false;          };        };      }      private function Load() {        $this->Reload();        $this->loaded = 1;      }      public function ForceLoaded() {        $this->loaded = 1;      }      public function GetField($field) {        if ($this->loaded == 0) {          $this->Load();        };        return $this->database_fields[$field];      }      public function GetAllFields() {        if ($this->loaded == 0) {          $this->Load();        };        return($this->database_fields);      }      public function GetID() {        return $this->id;      }      public function Initialize($table_name, $tuple_id = "") {        $this->table_name = $table_name;        $this->id = $tuple_id;      }      public function SetField($field, $value) {        if ($this->loaded == 0) {          if ($this->id) {            $this->Load();          };        };        $this->database_fields[$field] = $value;        $this->modified = 1;        $this->modified_fields[$field] = true;      }      public function Destroy() {        $id = $this->id;        $table_name = $this->table_name;        if ($id) {          $sql = new sql(0);          $stmt = "DELETE FROM \"" . $table_name . "\" WHERE id='" . $id . "'";          $sql->query($stmt);        };      }      public function Save() {        $id = $this->id;        $table_name = $this->table_name;        $sql = new sql(0);        if (!$id) {          $this->loaded = 0;        };        if ($this->loaded == 0) {          # assume this is a new entity          $stmt = "INSERT INTO \"" . $table_name ."\"(";          foreach ($this->database_fields as $key => $value) {            if (!is_numeric($key)) {              $key = str_replace("'", "\'", $key);              if ($value != "") {                $stmt .= "\"$key\",";              };            };          };          # Chop last comma          $stmt = substr($stmt,0,strlen($stmt)-1);          $stmt .= ") VALUES (";          foreach ($this->database_fields as $key => $value) {            if (!is_numeric($key)) {              if ($value != "") {                $value = str_replace("'", "\'", $value);                $stmt .= "'$value',";              };            };          };          # Chop last comma          $stmt = substr($stmt,0,strlen($stmt)-1);          $stmt .= ")";        } else {          $stmt = "UPDATE \"" . $table_name ."\" SET ";          foreach ($this->database_fields as                           $key => $value) {            if (!is_numeric($key)) {              if ($this->modified_fields[$key] == true) {                $value = str_replace("'", "\'", $value);                if ($value == "") {                  $stmt .= "\"$key\" = NULL, ";                } else {                  $stmt .= "\"$key\" = '$value', ";                };              };            };          };          # Chop last comma and space          $stmt = substr($stmt,0,strlen($stmt)-2);          $stmt .= " WHERE id='$id'";       };       $return_code = $sql->query($stmt, 1);       if ($this->loaded == 0) {         # Try to get the ID of the new tuple.         $stmt = "SELECT MAX(id) AS id FROM \ "$table_name\" WHERE ";         foreach ($this->database_fields as $key => $value) {           if (!is_numeric($key)) {             if ($value) {               if ($this->modified_fields[$key] == true) {                 $value = str_replace("'", "\'", $value);                 $stmt .= "\"$key\" = '$value' AND ";               };             };           };         };         # Chop last " AND " (superfluous)         $stmt = substr($stmt,0,strlen($stmt)-5);         $sql->query($stmt);         $result_rows = $sql->get_table_hash();         $proposed_id = $result_rows[0]["id"];         if ($proposed_id > 0) {           $this->loaded = 1;           $this->id = $proposed_id;           return true;         } else {           return false;         };         };        return($return_code);      }    }; 

GenericObject Database Connectivity

The first thing to notice is that this class makes very heavy use of a class called sql an interface to a PostgreSQL database.

In fact, this class can be made to work with virtually any database: Microsoft SQL Server, MySQL, Oracle, anything. Only small amendments are required. For now, however, you can use the following to provide an interface to PostgreSQL. Save this class as sql.phpm.

   class sql {      private $result_rows; # Result rows hash      private $query_handle; # db: the query handle      private $link_ident; # db: the link identifier      public function __construct() {        $db_username = "gobjtest";        $db_password = "";        $db_host = "db";        $db_name = "gobjtest";        $this->link_ident = pg_Connect("user='$db_username'password=    '$db_password' dbname='$db_name' host='$db_host'");      }      public function query($sql, $code_return_mode = 0) {        $q_handle = pg_exec($this->link_ident, $sql);        for ($i=0; $i<=pg_numrows($q_handle)-1; $i++) {          $result = pg_fetch_array($q_handle,$i);          $return_array[$i] = $result;        };        if (!$q_handle) {          error_log("QUERY FAILED: $sql\n");        };        $this->result_rows = $return_array;        if (!$q_handle) {          return(1);         } else {          return(0); # return 0 if it fails        };      }      public function get_result($row_num, $column_name) {        return ($this->result_rows[$row_num][$column_name]);      }      public function get_row_hash($row_num) {        return ($this->result_rows[$row_num]);      }      public function get_table_hash() {        return $this->result_rows;      }      public function done($close_connection = 0) {        if ($close_connection) {          pg_Close($this->link_ident);        };      }    }; 

Note that we've hard coded the database username, password (none in this case), and name into the class. This is considered bad form in production projects and is the kind of data that should be exported into constants files or, even better, human-readable configuration files. For the purposes of this chapter, however, we keep things simple.

There's nothing particularly clever about the sql class in the preceding code. An example of simple usage is as follows:

   $sql = new sql();    $sql->query("SELECT id FROM \"user\");    $result_rows = $sql->get_table_hash();    for ($i=0; $i<=sizeof($result_rows)-1; $i++) {      print ($result_rows[$i] . "\n");    };    $sql->done(1);    }; 

Calling the done() method with a nonfalse parameter causes the database connection to be closed. If this is not desired, you can omit it.

As you can see, the class as a whole is not exactly rocket science. In the next chapter, we discuss the concept of database abstraction layers, which you can use to truly genericize and insulate your source code from the subtle differences between databases.

GenericObject Methods and Properties

Back now to the GenericObject class. This section discusses how it works in more detail.

Properties

If you examine the code listing in the previous section, you can see that GenericObject has the following member properties:

Property

Details

id

The identifier for the row in question, for example, 6.

table_name

The name of the table in question, for example, user.

database_fields

An associative array with keys representing the names of the columns of the table, and their values being the value of that column for the given row (for example, username => ed).

loaded

Indicates whether this object has been populated with data from the database. It is not necessary to load data unless it will be retrieved. This is set to 0 if no data is loaded, 1 if data is loaded.

modified_fields

A hash identical in key-values to database_fields, but with values true or false to represent whether that particular database field has been modified since the contents from the database was last loaded (for example, username => true, first_name => false).

modified

Has anything at all been modified since the load? 1 if yes, 0 if no.

Methods

If you examine the code listing for GenericObject, you'll also see that it has the following member methods available to you:

Method

Partameters

Details

Initialize

table_name: name of table in database; tuple_id: identifier of row in question

Called by the subclass to set the table name and id of the row in question.

Load

None

An alias for Reload.

Reload

None

Populates the database_fields member variable with current values from the database.

ForceLoaded

None

Makes this instance of the subclass think that it is loaded even if it isn't; useful if values have been set manually by some third-party process because this precludes any automatic loading taking place when GetField is called.

GetID

None

Gets the current ID of the loaded row. This will either have been set upon instantiation or will have been determined when Save() was called if originally a new entity.

GetField

field name of the field to retrieve

Gets the value of the field in question. If not yet loaded, it will automatically call Reload first to load values from the database.

GetAllFields

None

Same as preceding, but returns a hash of all fields and values rather than just a single field. Again, automatically calls Reload if so required.

SetField

field: the name of the field to set; value: the value to set

Updates the internal hash (database_fields) to reflect new value of given field; it then sets modified to 1 and the appropriate modified_fields key to 1.

Destroy

None

Permanently deletes the entity from the database. The object should not be used after this method has been called!

Save

None

Saves the contents of the object to the database.

It's worth examining how the Save method works in a bit more detail because it is among the more sophisticated methods in the class.

The Save Method

The method first determines whether this is a new entity in the database or an existing entity. It does this quite simply by looking at the id property. If it is null, this is a new entity; otherwise, this is an existing entity. This decision is then used to determine whether an UPDATE or INSERT statement is required of the database. An UPDATE is used to update an existing tuple; an INSERT is used to insert a new one.

In the event of an INSERT, a SQL statement is built up and inserted into the table in question with every field and value pair that has been specified prior to the method's call. The class is not aware of which columns in your table you may have specified as NOT NULL and that are optional, so in your code you must ensure that a value has been set for every column upon which the database insists; otherwise, your insertion will fail.

For example, if you specify the fields first_name and last_name of your user object to have meaningful values (say, John and Doe respectively), then GenericObject will in turn produce the following statement:

   INSERT INTO "user"("first_name", "last_name") VALUES ('John','Doe') 

Note that the username was never specified, hence it never made it into the insert. It will be left null by the database or, if in a NOT NULL column, an error will be returned.

Having successfully made the insert, our extended instance of GenericObject suddenly changes shape. We are no longer dealing with a new entity; we are dealing with an existing entity. With this in mind, it must have what all existing entities have an id value. How is this determined?

Both MySQL and PostgreSQL have functionality to determine the last id generated automatically as a result of an immediately preceding insertion. Such functionality is not available in all database platforms, however, and its implementation differs massively.

Accordingly, the easiest route is to use a SELECT to determine the value. The following approach might work 90 percent of the time:

   SELECT MAX(id) FROM "user" 

However, in the split second between INSERT and SELECT, it's entirely possible that somebody else has made an insertion, which would incorrectly skew your result.

A safer route is to add a condition to this query. Use the parameters originally specified to find the highest matching record with those results. The chances of another identical record's having been inserted in that split second gap are infinitesimal.

   SELECT MAX(id) FROM "user" WHERE "first_name"='John' AND "last_name"='Doe' 

The id value retrieved is then applied to the object, and it is set as loaded.

In the event of updating an existing record, there is no need to try to determine the tuple's id value it is already well known. A simple UPDATE statement built in much the same way as the INSERT statement shown previously is all that's required. However, rather than simply updating all columns, only those columns that have been modified are changed.

This is important not just as a time saver but also for the purposes of enforcing the integrity of your database. Any NULL values from the database will be stored as empty strings in GenericObject. The difference may not matter much to you, but if your UPDATE statement blithely turned all those NULL values into empty strings during an update, and subsequent filter queries that your application performed used a NOT NULL clause to filter results, you could get some quite unexpected behavior.

As you can see, the Save() method is really very clever and truly at the heart of GenericObject and its inner workings.

Benefits of GenericObject

By now, you've met GenericObject and seen how it works. We hope that you've had a chance to run through the code and match up each line against the preceding description above. The power of this class is such that it isn't something you should use lightly.

You'll meet a small demo application later in the chapter, but in the meantime, it's worth summarizing what some of the benefits of using GenericObject in your application might be.

Dealing with entities in isolation is all very well, but many times you'll find that you want to deal with them as part of a collection of entities of the same ilk.

It's time to meet the GenericObjectCollection class, which allows you to do just that.



Professional PHP5 (Programmer to Programmer Series)
Professional PHP5 (Programmer to Programmer Series)
ISBN: N/A
EAN: N/A
Year: 2003
Pages: 182
BUY ON AMAZON

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net