Half-baked Objects and 10% ORM

(2011)

I've used Object Relational Mapping (ORM) libraries on a few projects in the past. Without getting into the many, many details, ORM bridges the gap between data storage in a relational database and Object-Oriented Programming. Simply, instead of writing SQL queries, you let the ORM library write them for you. It's great when it works out, but like all code generators, there are some potential downsides:

Whether it's worthwhile is simply a matter of getting more out of it than you put in. As an alternative, I've started using a technique to build Objects on-the-fly from multi-table joins. This doesn't handle every case (not even close!), but it does handle the cases I need.

So suppose you've got a webpage with Users and Posts and Comments. Each Post can have multiple Comments, and a User can "Like" a comment. A normalized version looks something like this:

CREATE TABLE users (
 id INT AUTO_INCREMENT PRIMARY KEY,
 name VARCHAR(50)
);
CREATE TABLE posts (
 id INT AUTO_INCREMENT PRIMARY KEY,
 name VARCHAR(50)
);
CREATE TABLE comments (
 id INT AUTO_INCREMENT PRIMARY KEY,
 post_id INT,
 content VARCHAR(50)
);
CREATE TABLE liked_comments (
 user_id INT,
 comment_id INT
);

Now on this webpage, you want to show all of a user's Liked Comments. So you probably have a view template that loops over the comments, showing the comment text and a link back to the Post, something like this:

<?php foreach($comments as $comment): ?>
 <div class="comment">
   <p><?php echo($comment->content); ?>
   <p>On <a href="<?php echo($comment->post->link()); ?>"><?php echo($comment->post->name); ?></a></p>
 </div>
<?php endforeach ?>

Now the question is, where should the post permalink come from? I can think of at least 3 reasonable answers:

// 1. from a method on the comment
<a href="<?php echo($comment->post_link()); ?>"><?php echo($comment->post_name()); ?></a>

// 2. from a method on the post
<a href="<?php echo($comment->post->link()); ?>"><?php echo($comment->post->name()); ?></a>

// 3. from the template, using properties of the comment
<a href="/post/<?php echo($comment->post_id); ?>"><?php echo($comment->post_name); ?></a>

I would argue that the 2nd option is the best. In the 1st option, the Comment class needs methods to handle displaying a post, which seems unnatural and leads to duplication. In the 3rd option, the View is building the URL, which is a pain if you ever want to change it later, since you'd need to update all your views. The best thing is to let the Post know how to build it's own permalink, the method might look like this:

// in Post class
public function link() {
 return "/post/" . $this->id;
}

So how to build a list of Comments, each with a nested Post object? Here's one possibility:

SELECT
 comments.content as content,
 posts.id as post_id,
 posts.name as post_name
FROM liked_comments
JOIN comments ON liked_comments.comment_id = comments.id
JOIN posts on comments.post_id = posts.id
WHERE user_id = 1

So that's fine, let's say you instantiate a Comment for each row. Something like this:

<?php
$comments = array();
$rs = mysql_query($sql);
while($row = mysql_fetch_assoc($rs)) {
 $comments[] = new Comment($row);
}
?>

So that creates a list of comments for our View. All that's missing is to instantiate a nested Post for each Comment. This can be done in the Comment constructor:

public function __construct($args=NULL) {
 if($args && is_array($args)) {
   if(array_key_exists('post_id', $args) && array_key_exists('post_name', $args)) {
     $this->post = new Post();
     $this->post->id = $args['post_id'];
     $this->post->name = $args['post_name'];
   }
 }
 // other stuff..
}

So when we instantiate a Comment and provide the appropriate keys (post_id and post_name), it will instantiate a Post for us. It's not really a proper Post, but more of a half-baked object. It doesn't have an author, content or other things you might expect in a Post; instead, it has just the two keys to know how to display its permalink.

Now this works fine, but having a bunch of hacked-up constructors isn't very nice and we're still requiring the Comment class to know something about the structure of Posts. A better alternative is to make a super class with a more generic constructor that can be used by any class to instantiate any other class (or classes) based only on the row names. Here is the more generic version I am currently using:

// in a base class
function __construct($row, $params=NULL)
{
   foreach($row as $k=>$v) {
     $this->$k = $v;
   }
   $klass_map = NULL;
   if( $params ) {
       if(array_key_exists('klass_map', $params)) {
           $klass_map = $params['klass_map'];
       } 
   }
   $vars = get_object_vars($this);
   foreach($vars as $k=>$v)
   {
       $split = strpos($k, '_');
       if( $split === FALSE ) {
           continue;
       } else {
           $prefix = substr($k, 0, $split);
           $postfix = substr($k, $split+1);
           if( ! isset($this->{$prefix}) ) {
               if( $klass_map && array_key_exists($prefix, $klass_map) ) {
                   $this->{$prefix} = new $klass_map[$prefix];
               } else {
                   $this->{$prefix} = new stdClass;
               }
           }
           $this->{$prefix}->{$postfix} = $v;
           unset($this->$k);
       }
   }
   //echo('<pre>');
   //exit(print_r($this));
}

Well that looks a little more complicated. Basically, it just looks to see if there is an underscore in each property name, and if there is, it tries to instantiate that property as a class. A mapping tells it which prefixes go with which classes. For example:

$this->post_id becomes $this->post->id
$this->post_name becomes $this->post->name
$this->user_id becomes $this->user->id
$this->content just stays the same (no underscore)

So how to use that constructor? Something like this:

<?php
$params = array(
 'klass_map' => array(
   'post' => 'Post', // post_ prefix maps to Post class
  ),
);
$comments = array();
$rs = mysql_query($sql);
while($row = mysql_fetch_assoc($rs)) {
 $comments[] = new Comment($row, $params);
}
?>

The key observation is that $this->post is not a generic stdClass, but an instance of Post that has been created with only the properties we know we're gonna need.

There are some obvious downfalls here:

First, using magic constructors can make things unnecessarily complicated and may cause conflicts with libraries that do their own magic. Adding/removing (unsetting) properties seems particularly hazardous.

Second, you have to write your SQL carefully so you get the row names and mappings you need. In particular, row names like "modified_on" would not behave as expected. It should be easy to tweak the generic constructor to be a bit more robust.

Also, this really only handles the case of these nested 1:1 mappings. I think you could extend the idea, which is fairly useful by itself, but I would bet it gets complicated quickly as you head towards real ORM territory.

Despite the shortcomings, I'm finding this to be a convenient way to construct objects on-the-fly at the early prototyping stages of a project when I'm constantly renaming things and moving code around.