Going dynamic with PHP

2013 年 7 月 3 日3740

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

0 0