Going dynamic with PHP
The introduction of new object-oriented programming (OOP) features in PHP V5 has significantly raised the level of functionality in this programming language. Not only can you have private, protected, and public member variables and functions -- just as you would in the Java™, C++, or C# programming languages -- but you can also create objects that bend at runtime, creating new methods and member variables on the fly. You can't do that with the Java, C++, or C# languages. This kind of functionality makes super-rapid application development systems, such as Ruby on Rails, possible.
Before I get into all that, however, here's a word of caution: This article is about the use of very advanced OOP features in PHP V5 -- the kind of features you won't necessarily need in every application. Also, the kind of features that will be difficult to understand if you don't have a solid grounding in OOP and at least a beginner's knowledge of PHP object syntax.
The importance of being dynamic
Objects are a double-edged sword. On the one hand, objects are a great way to encapsulate data and logic and create a more maintainable system. On the other hand, they can become verbose and require that you write a lot of redundant code where the best you can hope for is not to make any mistakes. One example of this problem comes with database access objects. Generally speaking, you want a single class for every database table that performs the following functions: The object reads a data row from the database, allows you to update the fields, then allows you to update the database given the new data or delete the row. There is also a way of creating a new empty object, setting its fields, and inserting that data into the database.
If you had a table named Customers in the database, you would have an object named Customer
that would have the fields from the table and represent a single customer. And that Customer
object would allow you to insert, update or delete the corresponding record in the database. Now, that's all well and good, and it makes a lot of sense. But it's a lot of code to write. If you have 20 tables in the database, you will need 20 classes.
Three solutions are available to you. The first solution is simply to sit down at the keyboard and type for a while. That's fine for small projects, but I'm lazy. The second solution is to use a code generator that reads the database schema and writes the code for you. That's a great idea and a topic for another article. The third solution, which I cover in this article, is to write a single class that dynamically molds itself at runtime to the fields of a given table. This class may perform a bit more slowly than its table-specific counterpart, but it saves me from having to write a lot of code. This solution is particularly beneficial at the start of a project, where tables and fields are changing constantly, and keeping up with rapid changes is crucial.
So, how do you write a class that bends?
Writing a bendy class
Objects have two aspects: member variables and methods. In a compiled language, such as the Java language, if you try to call a method that doesn't exist or reference a member variable that doesn't exist, you get a compile-time error. But what happens in a language that doesn't compile, such as PHP?
A method call in PHP works as follows. First, the PHP interpreter looks for the method on the class. If the method is there, PHP calls it. Otherwise, the magic method __call
is invoked on the class if that method is present. If __call
fails, the parent class method is invoked, and so on.
Magic methods
A magic method is a method with a specific name that the PHP interpreter looks for at certain points in the execution of the script. The most common magic method is the constructor that's called when an object is created.
The __call
method takes two parameters: the name of the method being requested and the arguments. If you create a __call
method that takes these two parameters, performs a function, then returns TRUE, the code calling that object will never know the difference between a method that has code and a method that the __call
mechanism handles. In this way, you can create objects that imitate having an infinite amount of methods on the fly.
In addition to the __call
method, other magic methods -- including __get
and __set
-- that are invoked with member instance variables are referenced that don't exist. With this in mind, you can start to write a dynamic database access class that bends to fit any table.
Classic database access
Let's start with a simple database schema. The schema shown in Listing 1 is for a single data-table database that holds a list of books.
Listing 1. The MySQL database schema
DROP TABLE IF EXISTS book;
CREATE TABLE book (
book_id INT NOT NULL AUTO_INCREMENT,
title TEXT,
publisher TEXT,
author TEXT,
PRIMARY KEY( book_id )
);
Load this schema into a database named bookdb.
Next, write a conventional database class that you will then modify to become dynamic. Listing 2 shows a simple database access class for the book table.
Listing 2. The basic database access client
<?php
require_once("DB.php");
$dsn = 'mysql://root:password@localhost/bookdb';
$db =& DB::Connect( $dsn, array() );
if (PEAR::isError($db)) { die($db->getMessage()); }
class Book
{
private $book_id;
private $title;
private $author;
private $publisher;
function __construct()
{
}
function set_title( $title ) { $this->title = $title; }
function get_title( ) { return $this->title; }
function set_author( $author ) { $this->author = $author; }
function get_author( ) { return $this->author; }
function set_publisher( $publisher ) {
$this->publisher = $publisher; }
function get_publisher( ) { return $this->publisher; }
function load( $id )
{
global $db;
$res = $db->query( "SELECT * FROM book WHERE book_id=?",
array( $id ) );
$res->fetchInto( $row, DB_FETCHMODE_ASSOC );
$this->book_id = $id;
$this->title = $row['title'];
$this->author = $row['author'];
$this->publisher = $row['publisher'];
}
function insert()
{
global $db;
$sth = $db->prepare(
'INSERT INTO book ( book_id, title, author, publisher )
VALUES ( 0, ?, ?, ? )'
);
$db->execute( $sth,
array( $this->title,
$this->author,
$this->publisher ) );
$res = $db->query( "SELECT last_insert_id()" );
$res->fetchInto( $row );
return $row[0];
}
function update()
{
global $db;
$sth = $db->prepare(
'UPDATE book SET title=?, author=?, publisher=?
WHERE book_id=?'
);
$db->execute( $sth,
array( $this->title,
$this->author,
$this->publisher,
$this->book_id ) );
}
function delete()
{
global $db;
$sth = $db->prepare(
'DELETE FROM book WHERE book_id=?'
);
$db->execute( $sth,
array( $this->book_id ) );
}
function delete_all()
{
global $db;
$sth = $db->prepare( 'DELETE FROM book' );
$db->execute( $sth );
}
}
$book = new Book();
$book->delete_all();
$book->set_title( "PHP Hacks" );
$book->set_author( "Jack Herrington" );
$book->set_publisher( "O'Reilly" );
$id = $book->insert();
echo ( "New book id = $id\n" );
$book2 = new Book();
$book2->load( $id );
echo( "Title = ".$book2->get_title()."\n" );
$book2->delete( );
?>
To keep the code simple, I put the class and the test code in one file. The file starts with getting the database handle, which it stores in a global variable. The Book
class is then defined, with private member variables for each field. A set of methods for loading, inserting, updating, and deleting rows from the database is also included.
The test code at the bottom starts by deleting all the entries from the database. Next, the code inserts a book, telling you the ID of the new record. Then, the code loads that book into another object and prints the title.
Listing 3 shows what happens when you run the code on the command line with the PHP interpreter.
Listing 3. Running the code on the command line
% php db1.php
New book id = 25
Title = PHP Hacks
%
Not much to look at, but it gets the point across. The Book
object represents a row in the book data table. By using the fields and the methods above, you can create new rows, update them, and delete them.
A little dab of dynamic
The next step is to make the class a bit dynamic by creating the get_
and set_
methods on the fly for the individual fields. Listing 4 shows the updated code.
Listing 4. Dynamic get_ and set_ methods
<?php
require_once("DB.php");
$dsn = 'mysql://root:password@localhost/bookdb';
$db =& DB::Connect( $dsn, array() );
if (PEAR::isError($db)) { die($db->getMessage()); }
class Book
{
private $book_id;
private $fields = array();
function __construct()
{
$this->fields[ 'title' ] = null;
$this->fields[ 'author' ] = null;
$this->fields[ 'publisher' ] = null;
}
function __call( $method, $args )
{
if ( preg_match( "/set_(.*)/", $method, $found ) )
{
if ( array_key_exists( $found[1], $this->fields ) )
{
$this->fields[ $found[1] ] = $args[0];
return true;
}
}
else if ( preg_match( "/get_(.*)/", $method, $found ) )
{
if ( array_key_exists( $found[1], $this->fields ) )
{
return $this->fields[ $found[1] ];
}
}
return false;
}
function load( $id )
{
global $db;
$res = $db->query( "SELECT * FROM book WHERE book_id=?",
array( $id ) );
$res->fetchInto( $row, DB_FETCHMODE_ASSOC );
$this->book_id = $id;
$this->set_title( $row['title'] );
$this->set_author( $row['author'] );
$this->set_publisher( $row['publisher'] );
}
function insert()
{
global $db;
$sth = $db->prepare(
'INSERT INTO book ( book_id, title, author, publisher )
VALUES ( 0, ?, ?, ? )'
);
$db->execute( $sth,
array( $this->get_title(),
$this->get_author(),
$this->get_publisher() ) );
$res = $db->query( "SELECT last_insert_id()" );
$res->fetchInto( $row );
return $row[0];
}
function update()
{
global $db;
$sth = $db->prepare(
'UPDATE book SET title=?, author=?, publisher=?
WHERE book_id=?'
);
$db->execute( $sth,
array( $this->get_title(),
$this->get_author(),
$this->get_publisher(),
$this->book_id ) );
}
function delete()
{
global $db;
$sth = $db->prepare(
'DELETE FROM book WHERE book_id=?'
);
$db->execute( $sth,
array( $this->book_id ) );
}
function delete_all()
{
global $db;
$sth = $db->prepare( 'DELETE FROM book' );
$db->execute( $sth );
}
}
..
To make this change, you have to do two things. First, you must change the fields from individual instance variables to a hash table of field and value pairs. Then you must add a __call
method that simply looks at the method name to see whether it was a set_
or a get_
method and set the appropriate field in the hash table.
Note that the load
method actually uses the __call
method by calling the set_title
, set_author
, and set_publisher
methods -- none of which actually exists.
Going completely dynamic
Removing the get_
and set_
methods is just a starting point. To create a completely dynamic database object, you have to give the class the name of the table and the fields, and have no hard-coded references. Listing 5 shows this change.
Listing 5. A completely dynamic database object class
<?php
require_once("DB.php");
$dsn = 'mysql://root:password@localhost/bookdb';
$db =& DB::Connect( $dsn, array() );
if (PEAR::isError($db)) { die($db->getMessage()); }
class DBObject
{
private $id = 0;
private $table;
private $fields = array();
function __construct( $table, $fields )
{
$this->table = $table;
foreach( $fields as $key )
$this->fields[ $key ] = null;
}
function __call( $method, $args )
{
if ( preg_match( "/set_(.*)/", $method, $found ) )
{
if ( array_key_exists( $found[1], $this->fields ) )
{
$this->fields[ $found[1] ] = $args[0];
return true;
}
}
else if ( preg_match( "/get_(.*)/", $method, $found ) )
{
if ( array_key_exists( $found[1], $this->fields ) )
{
return $this->fields[ $found[1] ];
}
}
return false;
}
function load( $id )
{
global $db;
$res = $db->query(
"SELECT * FROM ".$this->table." WHERE ".
$this->table."_id=?",
array( $id )
);
$res->fetchInto( $row, DB_FETCHMODE_ASSOC );
$this->id = $id;
foreach( array_keys( $row ) as $key )
$this->fields[ $key ] = $row[ $key ];
}
function insert()
{
global $db;
$fields = $this->table."_id, ";
$fields .= join( ", ", array_keys( $this->fields ) );
$inspoints = array( "0" );
foreach( array_keys( $this->fields ) as $field )
$inspoints []= "?";
$inspt = join( ", ", $inspoints );
$sql = "INSERT INTO ".$this->table." ( $fields )
VALUES ( $inspt )";
$values = array();
foreach( array_keys( $this->fields ) as $field )
$values []= $this->fields[ $field ];
$sth = $db->prepare( $sql );
$db->execute( $sth, $values );
$res = $db->query( "SELECT last_insert_id()" );
$res->fetchInto( $row );
$this->id = $row[0];
return $row[0];
}
function update()
{
global $db;
$sets = array();
$values = array();
foreach( array_keys( $this->fields ) as $field )
{
$sets []= $field.'=?';
$values []= $this->fields[ $field ];
}
$set = join( ", ", $sets );
$values []= $this->id;
$sql = 'UPDATE '.$this->table.' SET '.$set.
' WHERE '.$this->table.'_id=?';
$sth = $db->prepare( $sql );
$db->execute( $sth, $values );
}
function delete()
{
global $db;
$sth = $db->prepare(
'DELETE FROM '.$this->table.' WHERE '.
$this->table.'_id=?'
);
$db->execute( $sth,
array( $this->id ) );
}
function delete_all()
{
global $db;
$sth = $db->prepare( 'DELETE FROM '.$this->table );
$db->execute( $sth );
}
}
$book = new DBObject( 'book', array( 'author',
'title', 'publisher' ) );
$book->delete_all();
$book->set_title( "PHP Hacks" );
$book->set_author( "Jack Herrington" );
$book->set_publisher( "O'Reilly" );
$id = $book->insert();
echo ( "New book id = $id\n" );
$book->set_title( "Podcasting Hacks" );
$book->update();
$book2 = new DBObject( 'book', array( 'author',
'title', 'publisher' ) );
$book2->load( $id );
echo( "Title = ".$book2->get_title()."\n" );
$book2->delete( );
? >
Here, you change the name of the class from Book
to DBObject
. Then you change the constructor to take the name of the table, as well as the names of the fields in the table. After that, most of the changes happen in the methods of the class, which instead of using some hard-coded Structured Query Language (SQL) now must create the SQL strings on the fly using the table and the field names.
The only assumptions the code makes is that there is a single primary key field and that the name of that field is the name of the table plus _id
. So, in the case of the book
table, there is a primary key field called book_id
. The primary key naming standards you use may be different; if so, you will need to change the code to suit.
This class is much more complex than the original Book
class. However, from the perspective of the client of the class, this class is still simple to use. That said, I think the class could be even simpler. In particular, I don't like that I have to specify the name of the table and the fields each time I create a book. If I were to copy and paste this code all around, then change the field structure of the book table, I would be in a bad way. In Listing 6, I solved this problem by creating a simple Book
class that inherits from DBObject
.
Listing 6. The new Book
class
..
class Book extends DBObject
{
function __construct()
{
parent::__construct( 'book',
array( 'author', 'title', 'publisher' ) );
}
}
$book = new Book( );
$book->delete_all();
$book->{'title'} = "PHP Hacks";
$book->{'author'} = "Jack Herrington";
$book->{'publisher'} = "O'Reilly";
$id = $book->insert();
echo ( "New book id = $id\n" );
$book->{'title'} = "Podcasting Hacks";
$book->update();
$book2 = new Book( );
$book2->load( $id );
echo( "Title = ".$book2->{'title'}."\n" );
$book2->delete( );
?>
Now, the Book
class really is simple. And the client of the Book
class no longer needs to know the names of the table or the fields.
Room for improvement
One final improvement I want to make on this dynamic class is to use member variables to access the fields, instead of the clunky get_
and set_
operators. Listing 7 shows how to use the __get
and __set
magic methods instead of __call
.
Listing 7. Using the __get
and __set
methods
<?php
require_once("DB.php");
$dsn = 'mysql://root:password@localhost/bookdb';
$db =& DB::Connect( $dsn, array() );
if (PEAR::isError($db)) { die($db->getMessage()); }
class DBObject
{
private $id = 0;
private $table;
private $fields = array();
function __construct( $table, $fields )
{
$this->table = $table;
foreach( $fields as $key )
$this->fields[ $key ] = null;
}
function __get( $key )
{
return $this->fields[ $key ];
}
function __set( $key, $value )
{
if ( array_key_exists( $key, $this->fields ) )
{
$this->fields[ $key ] = $value;
return true;
}
return false;
}
function load( $id )
{
global $db;
$res = $db->query(
"SELECT * FROM ".$this->table." WHERE ".
$this->table."_id=?",
array( $id )
);
$res->fetchInto( $row, DB_FETCHMODE_ASSOC );
$this->id = $id;
foreach( array_keys( $row ) as $key )
$this->fields[ $key ] = $row[ $key ];
}
function insert()
{
global $db;
$fields = $this->table."_id, ";
$fields .= join( ", ", array_keys( $this->fields ) );
$inspoints = array( "0" );
foreach( array_keys( $this->fields ) as $field )
$inspoints []= "?";
$inspt = join( ", ", $inspoints );
$sql = "INSERT INTO ".$this->table.
" ( $fields ) VALUES ( $inspt )";
$values = array();
foreach( array_keys( $this->fields ) as $field )
$values []= $this->fields[ $field ];
$sth = $db->prepare( $sql );
$db->execute( $sth, $values );
$res = $db->query( "SELECT last_insert_id()" );
$res->fetchInto( $row );
$this->id = $row[0];
return $row[0];
}
function update()
{
global $db;
$sets = array();
$values = array();
foreach( array_keys( $this->fields ) as $field )
{
$sets []= $field.'=?';
$values []= $this->fields[ $field ];
}
$set = join( ", ", $sets );
$values []= $this->id;
$sql = 'UPDATE '.$this->table.' SET '.$set.
' WHERE '.$this->table.'_id=?';
$sth = $db->prepare( $sql );
$db->execute( $sth, $values );
}
function delete()
{
global $db;
$sth = $db->prepare(
'DELETE FROM '.$this->table.' WHERE '.
$this->table.'_id=?'
);
$db->execute( $sth,
array( $this->id ) );
}
function delete_all()
{
global $db;
$sth = $db->prepare( 'DELETE FROM '.$this->table );
$db->execute( $sth );
}
}
class Book extends DBObject
{
function __construct()
{
parent::__construct( 'book',
array( 'author', 'title', 'publisher' ) );
}
}
$book = new Book( );
$book->delete_all();
$book->{'title'} = "PHP Hacks";
$book->{'author'} = "Jack Herrington";
$book->{'publisher'} = "O'Reilly";
$id = $book->insert();
echo ( "New book id = $id\n" );
$book->{'title'} = "Podcasting Hacks";
$book->update();
$book2 = new Book( );
$book2->load( $id );
echo( "Title = ".$book2->{'title'}."\n" );
$book2->delete( );
?>
The test code at the bottom illustrates just how much cleaner this syntax is. To get the title of the book, simply get the title
member variable. That variable, in turn, calls the __get
method on the object that looks for the title
item in the hash table and returns it.
And there you have it: a single dynamic database access class that can bend itself to fit any table in your database.
More uses for dynamic classes
Writing dynamic classes doesn't end with database access. Take the case of the Customer
object in Listing 8.
Listing 8. A simple Customer
object
<?php
class Customer
{
private $name;
function set_name( $value )
{
$this->name = $value;
}
function get_name()
{
return $this->name;
}
}
$c1 = new Customer();
$c1->set_name( "Jack" );
$name = $c1->get_name();
echo( "name = $name\n" );
?>
This object is simple enough. But what happens if I want to log every time the name of the customer is retrieved or set? I can wrap the object in a dynamic logging object that looks like the Customer
object but sends notifications of a get
or set
operation to a log. Listing 9 shows this type of wrapper object.
Listing 9. A dynamic wrapper object
<?php
class Customer
{
private $name;
function set_name( $value )
{
$this->name = $value;
}
function get_name()
{
return $this->name;
}
}
class Logged
{
private $obj;
function __call( $method, $args )
{
echo( "$method( ".join( ",", $args )." )\n" );
return call_user_func_array(array(&$this->obj,
$method), $args );
}
function __construct( $obj )
{
$this->obj = $obj;
}
}
$c1 = new Logged( new Customer() );
$c1->set_name( "Jack" );
$name = $c1->get_name();
echo( "name = $name\n" );
?>
The code that calls the logged version of Customer
looks the same as before, but this time, any access to the Customer
object is logged. Listing 10 shows what the log prints when I run this logged code.
Listing 10. Running the logged object
% php log2.php
set_name( Jack )
get_name( )
name = Jack
%
Here, the log prints that the set_name
method was called with the parameter Jack
. Then, the get_name
method was called. Finally, the test code printed the result of the get_name
call.
Conclusion
If this dynamic object stuff was difficult for you to understand, I don't blame you. It took me a couple of times walking through it and playing with the code to understand it and see the benefits for myself.
Dynamic objects have a lot of power, but they also carry significant risk. First, the complexity of classes increases tremendously when you start writing magic methods. These classes are harder to understand, debug, and maintain. In addition, as integrated development environments (IDEs) become more intelligent, they may experience problems with dynamic classes such as these because when they look up methods on a class, they won't find them.
Now, that's not to say that you should avoid writing this type of code. Far from it. I love that the designers of PHP have been thoughtful enough to include these magic methods in the language so that we can write precisely this type of code. But it's important to understand the drawbacks, as well as the benefits.
Certainly, for applications such as database access, the technique I've shown here -- similar to one being used in the wildly popular Ruby on Rails system -- can dramatically reduce the time required to implement database applications with PHP. And there's nothing wrong with saving yourself some time.
Resources
Learn
PHP.net is the starting point for all things PHP.
Object Oriented PHP is a good place to learn about the object-oriented features of PHP.
Check out the Magic Methods documentation.
PHP 5 Objects, Patterns, and Practice by Matt Zandstra (Apress 2004) is a solid book on PHP V5 objects.
The WASP Framework uses dynamic classes to speed up Web development using PHP V5.
The classic book on objects and patterns is Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides (Addison-Wesley Professional 1995). It's a must-read book for any programmer learning about objects.
Get more information about the Ruby language.
The Ruby on Rails package demonstrates a lot of these ideas in practice.
Visit developerWorks' PHP project resources to learn more about PHP.
For a series of developerWorks tutorials on learning to program with PHP, see "Learning PHP, Part 1," Part 2, and Part 3.
Visit the developerWorks Open source zone for extensive how-to information, tools, and project updates to help you develop with open source technologies and use them with IBM's products.
Get products and technologies
Innovate your next open source development project with IBM trial software, available for download or on DVD.
Discuss
Get involved in the developerWorks community by participating in developerWorks blogs.
About the author
Jack D. Herrington is a senior software engineer with more than 20 years of experience. He's the author of three books: Code Generation in Action, Podcasting Hacks, and PHP Hacks. He has also written more than 30 articles.
Report abuse help
Report abuse
Thank you. This entry has been flagged for moderator attention.
Report abuse help
Report abuse
Report abuse submission failed. Please try again later.
developerWorks: Sign in
The first time you sign into developerWorks, a profile is created for you. Select information in your profile (name, country/region, and company) is displayed to the public and will accompany any content you post. You may update your IBM account at any time.
All information submitted is secure.
Choose your display name
The first time you sign in to developerWorks, a profile is created for you, so you need to choose a display name. Your display name accompanies the content you post on developerWorks.
Please choose a display name between 3-31 characters. Your display name must be unique in the developerWorks community and should not be your email address for privacy reasons.
All information submitted is secure.
Rate this article
Comments