Simplify Business Logic with PHP DataObjects
Pages: 1, 2, 3
Result Sets
Now we can find a row and perform an insert, delete, or update on our
DO_User DataObject. Remember how I mentioned that our DataObject
is a simple wrapper that represents a single row from our table? What if we
need to perform a query that returns more than one row? Our DataObject can't
handle this, nor should it. We need another wrapper for the result set.
The simplest approach to solving this problem is to create an array of DataObjects that contains all of the found rows. That may seem logical at first, but what if your query returns 10,000, 100,000, or more rows? That'd make a mighty big array of objects, turning your application into a RAM hog and convincing your web host to shut down your site.
We need a proper wrapper for the result set that holds a reference to the
native (or Pear::DB) result set. This wrapper won't return an array of each
row. Instead, it returns instances of our DO_User DataObject.
Pretty cool.
We'll have to write a new class for this. I always call it
ReadOnlyResultSet, as the result set itself is read-only (we can't
update or delete rows in the result set directly), and it happens to be the
name used in the J2EE design patterns (let's hear it for consistency). Before
we write this class, we'll add one more method to DO_User to find
a bunch of rows using data currently in the DataObject. When it finds the
rows, this new function will return a new instance of our new
ReadOnlyResultSet class. This is easier than it sounds. We'll
start by adding the find() method [file source]:
public function find()
{
$sql = "SELECT * FROM User";
// This array will hold the where clause
$where = array();
// Using PHP 5's handy new reflection API
$class = new ReflectionClass('DO_User');
// Get all of DO_User's variable (or property) names
$properties = $class->getProperties();
// Loop through the properties
for ($i = 0; $i < count($properties); $i++) {
$name = $properties[$i]->getName();
if ($this->$name != '') {
// Add this to the where clause
$where[] = "`" . $name . "`='"
. mysql_escape_string($this->$name) . "'";
}
}
// If we have a where clause, build it
if (count($where) > 0){
$sql .= " WHERE " . implode(' AND ', $where);
}
$rs = mysql_query($sql);
include_once('class-ReadOnlyResultSet.php');
return new ReadOnlyResultSet($rs);
}
This one is a doozy. PHP 5 has a built-in Reflection API that we can use to retrieve any and all information about a class and/or object. Consider this built-in reverse engineering. We use reflection here to pull the property names (member variable names) from the DataObject. Take a look at this new API some time; it's quite powerful.
Before calling find(), we have to to put data into those
properties we want to search. If we want to find people with a certain first
name, we set only that property and then call find. The
find() method builds the query and returns a
ReadOnlyResultSet. Before we explore how to use this method, we
need to code the ReadOnlyResultSet class.
Coding ReadOnlyResultSet
The ReadOnlyResultSet class is a wrapper for a result set that
returns DataObject instances. I use a generic
ReadOnlyResultSet for all projects. It works so well that I never
have to modify it. This new wrapper has three methods: getNext(),
rowCount(), and reset(). Here's the code for the
class [file source]:
File contents of: class-ReadOnlyResultSet.php
<?php
class ReadOnlyResultSet {
// This member variable will hold the native result set
private $rs;
// Assign the native result set to an instance variable
function __construct($rs)
{
$this->rs = $rs;
}
// Receives an instance of the DataObject we're working on
function getNext($dataobject)
{
$row = mysql_fetch_array($this->rs);
// Use reflection to fetch the DO's field names
$class = new ReflectionObject($dataobject);
$properties = $class->getProperties();
// Loop through the properties to set them from the current row
for ($i = 0; $i < count($properties); $i++) {
$prop_name = $properties[$i]->getName();
$dataobject->$prop_name = $row[$prop_name];
}
return $dataobject;
}
// Move the pointer back to the beginning of the result set
function reset()
{
mysql_data_seek($this->rs, 0);
}
// Return the number of rows in the result set
function rowCount()
{
return mysql_num_rows($this->rs);
}
}
?>
As a note for those used to using & to pass by variables
reference, PHP 5 always passes objects by reference now (like Java). That's
why I avoid the & in my examples.
Because this is a wrapper for a result set, the constructor for this class
requires that we pass a result set. For this example, it's a native MySQL
result set, for the reasons previously stated. The reset() method
moves the pointer of the native result set back to 0, the beginning.
rowCount() returns the number of rows in the native result set.
The getNext() method is more curious. Let's break it down a
little.
First, because this class needs to work for all DataObjects, we have to have
a way to tell this method which DataObject to work with. That's why we have to
pass the DataObject we want to fill with the next row's data. This will usually
be an empty DataObject. The first line in this method fetches the next row from
the native result set with mysql_fetch_array(). Next, we need to
loop through all of the passed-in object's properties using the Reflection API.
Notice the usage of the class ReflectionObject instead of
ReflectionClass. This is because we're trying to reverse engineer
an object, not a class.
As the loop executes, it sets the value of each property in our DataObject to the value of each field in our native row. When we're all done, we return the now-filled DataObject. This class works for all DataObjects and you should never need to modify it.
It's time to see ReadOnlyResultSet in action. Here's how to
look for all users who have the first name jane:
<?php
include_once('class-DO_User.php');
$user = new DO_User();
// We'll use a literal string here,
// but this could come from anywhere,
// such as $_POST or $_GET
$user->firstName = 'jane';
// Call find(), which returns an instance of ReadOnlyResultSet
$rs = $user->find();
?>
<html>
<head>
<title>Found Users</title>
</head>
<body>
<p>Here the found users:</p>
<table border="1">
<tr>
<td>User ID</td>
<td>First Name</td>
<td>Last Name</td>
<td>Email</td>
</tr>
?>
// Loop through the result set
for ($i=0; $i < $rs-rowCount(); $i++) {
// Pass on a new instance of DO_User, receiving it back filled in
$userRow = $rs->getNext(new DO_User());
?>
// Display the current row in an HTML table
<tr>
<td><?=$userRow->userId?></td>
<td><?=$userRow->firstName?></td>
<td><?=$userRow->lastName?></td>
<td><?=$userRow->email?></td>
</tr>
<?php
}
<?php
</table>
</body>
</html>
Notice that when we call $rs->getNext() we needed to pass a
new DO_User instance. This is how the
ReadOnlyResultSet knows which kind of DataObject it's working
with. The ReadOnlyResultSet populates this new DataObject with the
next row's data, returning the now-populated DataObject. Remember, PHP 5 always
passes objects by reference, so the new DataObject passed on to
getNext() is the exact same object that returned from
getNext(). Once again, compact, readable and clean code.
Further Ideas
Beyond these basic five methods, you can add more functionality as needed. I often add a method to authenticate a user upon login. This method expects the username and password as parameters and returns true or false. If it returns true, I fill the DataObject with the found record. If you need very specific or more complex finding, add a new method to accomplish that task. The fundamental methods shown in this article are just the starting point for your own DataObjects.
It's actually quite possible to generalize these five methods and drop them into a super class. The Reflection API makes this pretty easy to do.
Also note that there is one limitation to this simple DataObject
implementation -- you can't easily do joins. When I need a join, I create
view DataObjects that only return a ReadOnlyResultSet, since you
generally can't do joined INSERTs, DELETEs, and
UPDATEs. There are other ways to accomplish joins, though.
Finally, you may have noticed that there is a Pear::DB_DataObject package available. This is a solid package, but it can be a bit of a challenge to figure out if you're not already familiar with the concepts of DataObjects. This article makes a good introduction to Pear::DB_DataObject, a package I hope to write about in the future.
This brings us to the end of our first look at DataObjects. What we've seen here is a simple implementation of DataObjects using PHP 5. With some small modifications, you can adapt this implementation to work with PHP 4, as well as with any database abstraction layer you wish (such as Pear::DB or DBX). Many folks also write up a script that automatically generates the initial DataObjects by querying the database's table structure and creating the necessary PHP files. This can save a lot of time.
In future articles, I hope to cover Pear::DB_DataObject and PHP 5's new Reflection API.
Resources
- Source code: [
ReadOnlyResultSet] [DO_User] - PHP 5: [Zend's PHP5 InfoCenter] [Reflection API]
- Design Patterns: [Data Access Object] [Data Transfer Object]
Darryl Patterson is a senior instructor at Centennial College in Toronto, where he has developed and taught many programming courses including PHP, SQL, Java/J2EE, HTML, JavaScript and CGI/Perl.
Return to the PHP DevCenter
You must be logged in to the O'Reilly Network to post a talkback.
Showing messages 1 through 19 of 19.
-
DataObject per table
2005-06-21 13:17:50 raminoacid [Reply | View]
After reading the comments (I may be late though, it's 2005), I noticed that my implementation is sort of abstract/general and sort of NOT.
I'm using PHP4 so I had to 'carve' some OOP in my classes. My DataObject is a base-class for all other DataObject's. I only created subclasses for the relation tables to make them more flexible in showing their rows. Instead of showing PK values, they refer to the corresponding table and show the readable values.
I totally see Evil3's opinion but in my case, the subclasses are really for specific table-related stuff. Don't make DO's for each table unless you have to ... or have a better solution.
Cheers. -
DataObject per table
2005-07-01 14:18:49 y11o [Reply | View]
you might be interested in seeing the EZPDO project (http://www.ezpdo.net) and especially this post (http://www.ezpdo.net/forum/viewtopic.php?id=20) which talks about exactly the problem you try to solve.
[Excerpt from the post]
The class hierarchy looks like this.
Graphic -+- Point
|
+- Line
|
+- Text
This is a simple example of the Composite design pattern. Each Graphic object can have
a parent Graphic object and a set of child Graphic objects. EZPDO can persist all the
composition relationships and you will find it is fairly easy to do with EZPDO.
-
Shuffled tags
2005-06-01 13:39:41 PowerMacX [Reply | View]
The last code bit has:
?>
?>
Followed by:
<?php
<?php
</code>
Should be
<?php
?>
<?php
?>
:)
-
A great article
2005-05-06 14:51:25 mattneedles [Reply | View]
This article is well-written compared to vague/half-hearted articles by others.
Thank you very much Darryl.
-
Some Followup
2004-12-12 21:45:21 DarrylP [Reply | View]
A couple of good replies have been posted. Before I address them, let me start by saying that this article is meant to be an introduction to the concepts and design patterns related to DataObjects. I chose very simple examples of how to implement DataObjects in PHP. It'a also meant to help demonstrate how DataObjects can make working with a database more transparent. The hope is to, at the very least, get the novice PHP coder to think about how they interact with their database.
In response to Tony, I also use an abstract DataObject super class in my applications. It takes care of the basics: select, insert, update, delete. I didn't use this super class in my article, as I felt it would complicate the topic too much for the novice (although I did mention writing one in the last paragraphs). I spent some time reading over your site as well. It seems we hold many of the same views on many topics. It's nice to see someone else using XSLT for their presentation layer,
In response to EViL3, you're right, In your words: "...this is an old idea being re-hashed for a new generation." I've found that a large number of PHP coders out there (from the novice to some veterans) generally hard code queries, often repeating common DB tasks. The intention here is to get folks thinking about how best to represent data from a relational database as PHP code. Once the DataObjects are created, the coder can interact with the DB using only PHP code, not SQL. As I stated in the article, there are many ways to implement DataObjects. This article purposefully presents them in a very simplistic manner. The "how" is not as important as the "why" in this case. For a simple solution to joins, I tend to use read-only view DataObjects (I have an abstract ViewDataObject super class I regularly use).
I took some time to read about SQLMaps. And yeah, they do look interesting. Pear:DB_DataObject tries to do this (in a manner of speaking), but they chose not to use XML... an odd descision if you ask me.
Thanks for the comments. -
Some Followup
2004-12-19 19:28:29 alien3d [Reply | View]
I have created with some method like this before i visiting this site but the main problem with php when created a lot of sub class and one superclass sometimes i execute the code but sometimes are not.And when execute $object->sub_object(); php will confuse with other page same $object->sub_object() and it will return empty page with no error without execution
-
An old idea for a new generation
2004-12-12 18:42:29 EViL3 [Reply | View]
I find your general approach for identifying the common database functions interesting, but its both annoying and disappointing that respected developers in the PHP community still consider mapping an individual table to a class a suitable practice, and promote it almost like a panacea solution.
With this method we could end up with a massive no. classes (1 per table), with the headache of performing cross table joins - the obvious solution is to execute multiple queries from the various objects till we retrieve all the details required.
But consider, proper database design promotes the seperation of data using the NF (normal form) paradigm, in the "real world" databases consist of tens to hundreds of tables, and 99% of SELECT queries perform cross table joins - So apart the very simplest of scripts and databases, this approach is bad for several reasons
1) Potentially hundreds of classes
2) Very difficult to perform cross table joins
3) Any relationships need to be hard coded in various places (rather than just within the SQL).
4) Increased complexity and execution time.
These lend to increased development, maintainance and running costs.
Instead, classes should reflect the NF relationship to describe "real world" objects, and then pull the relevant fields to fill these attributes - Using this approach will reduce the number of required classes, centralise the SQL generation, reduce the number of queries and overall be more maintainable.
I do apologise if my opinion offends, its not meant as personal critism, but this is an old idea being re-hashed for a new generation.
One new solution that does intrigrue me is the idea of SQLMaps, which maps database values to objects by the use of XML configuration files.
http://www.developer.com/db/print.php/3346301
Unfortunately nothing similar is yet available for PHP :)
-
Why not a single DAO for ALL database tables?
2004-12-09 06:19:13 Tony Marston [Reply | View]
What's this? A separate DataObject for each database table? How soooo last century. Surely a better way would be to have a single Data Access Object for the whole application regardless of the number of tables.
Don't say that it cannot be done because I have already done it. Take a look at http://www.tonymarston.net/php-mysql/sample-application.html which gives you access to some source code.
-
Greetings from Perl!
2004-08-09 19:12:05 aristotle [Reply | View]
We've had a module for that called Class::DBI for nearly three years. :-)
The greatest part about it is that you can declare relationships between classes/tables by indicating that a certain column in your table either is a foreign key (has_a) or that your table's primary key is referenced from another one (has_many). All the joins will then happen behind the scenes and you can use simple accessors to navigate your relations.
There is also about half a million ways to specialize the behaviour of classes and instances in particular ways.
Kake Pugh wrote an article on O'Reilly's Perl.com about How to avoid writing code using that module and a powerful templating system.
-
How about Propel?
2004-08-09 02:23:19 Joe_Cai [Reply | View]
http://propel.phpdb.org/wiki/
Propel is a full-service object persistence and query toolkit for PHP5. What this means, in practical terms, is that Propel allows you to treat your database as a set of objects, providing a simple API for storing and querying data. Other names associated with this type of tool include Object Relational Mapping (ORM) and Data Access Objects (DAO). Propel provides a generator that creates SQL and PHP classes and also provides the runtime environment for managing object persistence and retrieval.
-
Pear::DB_DataObject note
2004-08-07 07:55:04 DarrylP [Reply | View]
DB_DataObject is mentioned in the 2nd last paragraph. This article is meant to be an introduction to DataObjects to help lessen the learning curve a little for a larger implementation of DataObjects, like Pear::DB_DataObject.
I'm sure there will be a future article specifically about DB_DataObject as a second part to this article. That Pear package is much more powerful than the simple implementation outlined here in the article... but keeping it simple was one of the main goals of this article.
-
Already present solution
2004-08-06 18:59:00 Oyku [Reply | View]
PEAR already holds a module named DB::DataObjects
Although for demonstration purposes this article serves very well, Pear DatabOject is worth mentioning in the article. Combined with smarty or any other templating kit it is easily possible to creat a homegrown MVC type framework without dealing with database complexities. -
Already present solution
2006-05-27 16:23:09 goa103 [Reply | View]
You're probably refering to the DB_DataObject (http://pear.php.net/package/DB_DataObject) PEAR package. About template engines check out the Introducing Smarty: A PHP Template Engine (http://www.onlamp.com/pub/a/php/2002/09/05/smarty.html) ONLamp.com article by Joao Prado Maia. But one of my all time favorite is the Beyond The Template Engine (http://www.sitepoint.com/article/1218) SitePoint article by Brian Lozier.
-
DataObject super class
2004-08-06 11:55:20 DarrylP [Reply | View]
Something I didn't get a chance to include in the article was some code for a simple generic DataObject super class. Click here to see the source of a handy DataObject super class I use in my real world code.
Note, it does use the Reflection API and some PHP 5 specific keywordds, but it can easily be modified to work with PHP 4. -
DataObject super class
2006-09-18 19:46:45 LeandroVieiraPinho [Reply | View]
Hi DarryIP,
I like so much your article, itīs pretty cool.
But, I`m studing your DataObject super class, and Iīm found a dificult, maybe you can help me
In this line: $this->dop_className = 'DO_' . $tableName;
I create the name of a class, and in that:
$class = new ReflectionClass($this->dop_className);
Iīm using the ReflectionClass, but it isnīt function, the PHP is return the error:
Fatal error: Uncaught exception 'ReflectionException' with message 'Class dop_teste does not exist' in ...
Of course, the class doesnīt exist, but how you work with this? Can you help-me?
Thanks. -
DataObject super class
2005-06-24 23:31:45 phylaxis [Reply | View]
So what are the chances of you getting to show us the DataObject super class converted (for PHP 4) to not use the Reflection API and PHP 5 keywords? You say it can be done easily, but I am kind of a newbie to this stuff and don't know where to start. Thanks.



$where[] = $name . " = '" . mysql_escape_string($this->$name) . "'";
Also, the Reflection class returns too much properties if you are inheriting from another class like I did. ;-)