The eclectic musings of a bitter software engineer.

Objects are your friends. PHP’s object-oriented features don’t get nearly as much use as they should. This is probably because, in PHP 4, they weren’t worth writing home about. But PHP 5 has improved things greatly, and while PHP still isn’t anywhere near as object-oriented a language as Java or—be still my heart—Ruby, it’s come a long way. And we’re going to take advantage of it, because it’ll make our lives a whole lot easier.

First we’ll design a class which will allow us to manipulate users in the user table. Columns will be represented by public class variables. If we wanted to be anal about it, we could use __get and __set methods (which PHP confusingly calls overloading for some reason) to represent our class properties, but that would be a bit overkill for our needs. We’ll keep it simple by sticking with public variables. If you were designing this class to be used by third-party developers, then it’d be worth going to the trouble of using getters and setters to validate your property values, but we’re just designing it for us, and we trust us not to do something silly like assigning a string to a property that should be an integer.

The MonkeyUser class will have a mix of static functions (which can be called without instantiating the class) and instance functions (which will apply only to a single instance). The static functions will serve as object factories; for example, we’ll call MonkeyUser::getByUsername when we want to retrieve a MonkeyUser object representing a specific user. The static getByUsername function will retrieve the appropriate row from the user database table, then return a MonkeyUser object representing that user.

classes/MonkeyUser.php

class MonkeyUser
{
  public $id;
  public $username;
  public $password;
  public $email;
  public $fur_color;
  public $height;
  public $weight;

  // -- Public Static Methods -------------------------------------------------
  public static function getByUsername($username)
  {
    $result = Monkey::query('user.getByUsername', array(
      'username' => $username
    ));

    if ($result && $row = $result->fetch_assoc())
      return MonkeyUser::load($row);

    return false;
  }

  public static function load($row)
  {
    return new MonkeyUser($row['id'], $row['username'], $row['password'],
      $row['email'], $row['fur_color'], $row['height'], $row['weight']);
  }

  // -- Public Instance Methods -----------------------------------------------
  public function __construct($id = 0, $username = '', $password = '',
    $email = '', $fur_color = '', $height = 0, $weight = 0)
  {
    $this->id        = $id;
    $this->username  = $username;
    $this->password  = $password;
    $this->email     = $email;
    $this->fur_color = $fur_color;
    $this->height    = $height;
    $this->weight    = $weight;
  }
}

You’ll notice that the getByUsername function doesn’t create the MonkeyUser object itself; it calls the load method to do that. This way we can add any number of functions to retrieve MonkeyUser objects, and we won’t need to duplicate the instantiation code; each function will just pass an associative array representing a database row to the load function, and it’ll return an instantiated object.

Don’t forget to add a line to common.php to include our new class:

require_once 'classes/MonkeyUser.php';

Now whenever we want to retrieve a user object, all we have to do is call a single function, like so:

<?php
$user = MonkeyUser::getByUsername($_GET['username']);
?>

<h2>Hello, <?php echo htmlentities($user->username); ?></h2>

<p>
  Your fur is <?php echo htmlentities($user->fur_color); ?> and you claim
  to be <?php echo $user->height; ?> centimeters tall.
</p>

We don’t need to do anything special to sanitize the user-submitted value $_GET['username'] because our Monkey::query function will take care of that automatically. Whenever we display user-modifiable strings, we sanitize them with htmlentities to ensure that the user can’t include HTML or JavaScript that could be used to carry out a cross-site scripting attack. It’s not necessary to use htmlentities on numerical data, since we know that the database wouldn’t allow strings to exist in numerical columns.

The MonkeyUser class can be expanded with a new static function whenever you have a need for a new way of retrieving users. You could even add a getAll function that would return an array of MonkeyUser objects for every user in the database (just remember to add the associated user.getAll SQL query in db/queries.xml):

public static function getAll()
{
  $result = Monkey::query('user.getAll');

  $users = array();

  while($result && $row = $result->fetch_assoc())
    $users[] = MonkeyUser::load($row);

  return $users;
}

If we want to be able to modify the values of a MonkeyUser object (or create a brand new user) and save it to the database, we can add a save instance function. It might look something like this:

public function save()
{
  if ($this->id == 0)
  {
    // The id is 0, so this is a new user.
    $result = Monkey::query('user.insert', array(
      'username'  => $this->username,
      'password'  => $this->password,
      'email'     => $this->email,
      'fur_color' => $this->fur_color,
      'height'    => $this->height,
      'weight'    => $this->weight
    ));

    // If the new user was inserted successfully, grab the new id.
    if ($result)
      $this->id = Monkey::$db->insert_id;
  }
  else
  {
    // The id isn't 0, so we're updating an existing user.
    $result = Monkey::query('user.update', array(
      'id'        => $this->id,
      'username'  => $this->username,
      'password'  => $this->password,
      'email'     => $this->email,
      'fur_color' => $this->fur_color,
      'height'    => $this->height,
      'weight'    => $this->weight
    ));
  }

  return $result;
}

That’s all there is to it. Any values you pass to the database are automatically escaped, so you don’t need to worry about doing that manually a zillion times throughout your application. And any values that come out of the database only need to be run through htmlentities before being displayed. If you actually want to allow your users to use HTML in certain database fields, you should use the strip_tags function to limit the tags they can use. Or, for more advanced HTML filtering, check out Cal Henderson’s excellent lib_filter.

So. Now you have the beginnings of a fairly complete framework, except that you haven’t got a presentation layer. You have a multitude of options at this point. For smaller projects, I recommend sticking with HTML and minimal inline PHP, using CSS for formatting and layout. But if you want to do it right, an XML/XSLT-based template system is the way to go. Never, under any circumstances, use a template system like Smarty. If you do, I’ll point and laugh.

Someday I’ll write another article discussing how to use XML and XSLT for your presentation layer. It’s not as complicated as you might think. Right now I’m going to go have a sandwich. I hope you’ve found this series useful.

Comments

Thanks! Love reading your posts.

Saturday October 08, 2005 @ 04:22 PM (PDT) Posted by Rob

Ditto. Hopefully I'll learn enough to do something useful with Poseidon. ;)

Saturday October 08, 2005 @ 08:22 PM (PDT) Posted by Cena

This is a very inspiring series. I do hope you will enlighten us on the intricacies of XML and XSLT in a follow-up.

Sunday October 09, 2005 @ 03:03 PM (PDT) Posted by GreyStork
"This is a very inspiring series. I do hope you will enlighten us on the intricacies of XML and XSLT in a follow-up."

+1
Sunday October 09, 2005 @ 07:32 PM (PDT) Posted by Tabor
Don't you need an array element for id in your call to Monkey::query for user.update?

Great series.
Monday October 10, 2005 @ 02:30 PM (PDT) Posted by Caleb

Oops, you're right. Good catch!

Monday October 10, 2005 @ 03:02 PM (PDT) Posted by Ryan Grove

What's you naming convention for queries that use multiple tables (joins etc). And where would you put functions that retrieve all monkeys?

Monday October 17, 2005 @ 02:03 AM (PDT) Posted by Michiel
For multiple-table queries, I use the name of whatever table is most important. For example, if I'm doing a join between a comment table and a user table for the purpose of retrieving all comments by a specific user, I'd probably name the query comment.getByUserId. And for a query that retrieves all users, I'd just call it user.getAll.

The naming convention used for the queries isn't really important. You can use whatever convention you want; it's plenty flexible. This is just the one that works for me.
Monday October 17, 2005 @ 10:43 AM (PDT) Posted by Ryan Grove

A bit late, but thank you for writing this series. While it's quite easy to find PHP examples that work, it's absurdly difficult to find PHP examples that are, well, exemplory.

Tuesday July 25, 2006 @ 03:37 AM (PDT) Posted by Jaci

How can I use the save() function?? i.e. Insert new user

Monday March 19, 2007 @ 08:48 AM (PDT) Posted by eric

All you need to do is create a new MonkeyUser object with an id of 0, then call save(). The save() method knows that when the id is 0, it needs to insert a new row; once the row has been inserted, the id will be non-zero, so then it will know to update the existing row rather than inserting a new one:

$bob = new MonkeyUser(0, 'Bob');
$bob->save();  // inserts a new row

$bob->username = 'Susie';
$bob->save();  // updates the existing row
Monday March 19, 2007 @ 10:39 AM (PDT) Posted by Ryan Grove

hello wonko,
one last question (maybe). part of my problem is brain-fade.
Like trying to write the same data into a new record, where I have constraint on the table (like the name of image (or user) needs to be unique). It takes me about 3 to 5 minutes to figure out (while testing), I need to change the name of what I am entering into table. what would be a nice way to add error checking to this?
then when I violate a table constraint I will get a message of the mysql error. I guess some sort of error class??
no need to reply, but thanks for a nice intro!
ewholz

Monday March 19, 2007 @ 03:01 PM (PDT) Posted by eric

I can't thank you enough for this series of articles. I was just looking for a simple framework to make sql queries and this is probably the most elegant and easy to use system I've come across. I'll be using this in all projects I work on and I just can't believe the amount of time it has saved me. Thanks!

Monday May 14, 2007 @ 08:58 AM (PDT) Posted by Mike Gioia

I was thinking, if you run a query on two tables, and it returns results for all fields from each table, or say even an odd subset of fields, what kind of object do you store the results into? In your example, with a query to comments and users you couldn't store the results into either a comment or a user object. Any help on this???

Wednesday May 16, 2007 @ 01:33 PM (PDT) Posted by chris

As long as the query returns all the fields that are necessary to construct the objects, you can pass the same result rows to the load() methods for multiple classes and the different objects will be correctly instantiated, even when you do a select from multiple tables.

This works because load() only pays attention to the fields that are necessary to instantiate an object of a specific class, regardless of how many extra fields are also in the result set.

Wednesday May 16, 2007 @ 03:49 PM (PDT) Posted by Ryan Grove

Actually, I'm wrong. Brett just reminded me that since most tables are likely to have fields such as "id" and "created", there's a very high probability that the field names from two tables would clobber each other, so this wouldn't work very well.

I suppose that's a caveat to keep in mind about this framework: like many simple frameworks, it works great as long as your needs are also simple.

Wednesday May 16, 2007 @ 06:48 PM (PDT) Posted by Ryan Grove

I've really enjoyed this series and have used it in a pretty big project I've been working on. However, someone posted that there was a problem when you try to run queries on multiple tables. The way that I got around it was by not using the load() function when returning the array of objects. Instead I'll have one or more load_XYZ() functions that loads the query's special elements.

So, for example, a "SELECT users.username, addresses.address_id FROM users, addresses..." query would instead return an non-object array with keys username and address_id. It isn't as elegant as your classes, but for specialized queries it works okay.

Thanks again and hopefully I've explained my workaround ok.

Saturday November 03, 2007 @ 07:10 PM (PDT) Posted by Mike Gioia

I have own framework but your is simply amazing, I changed my way of thinking your framework is better. Thank you for sharing your idea

Thursday November 29, 2007 @ 06:40 AM (PST) Posted by Hugo Moran

Could give a few examples why Smarty is such a bad template system?

Monday June 30, 2008 @ 01:29 AM (PDT) Posted by dePaljas

Smarty isn’t really a bad templating system, it’s just an unnecessary one. PHP is its own templating language, and unless you have a very good reason for adding another layer of abstraction on top of it, all you’re doing is adding more complexity and reducing the performance of your application.

Note that this is a slightly different opinion than the one I held when I wrote this article almost three years ago. In the article, I said that XML/XSLT is the way to go. Now I would disagree with that; both XML/XSLT and Smarty have their uses, but in most cases I would recommend sticking to straight PHP. That’ll give you the best performance with the least complexity.

Monday June 30, 2008 @ 10:51 AM (PDT) Posted by Ryan Grove

Nice Framework. I will try it as soon as I have some spare time. I wonder if you have some nice/elegant idea about a login system (register,login,logout,personal pages,etc)

Tuesday July 22, 2008 @ 07:44 AM (PDT) Posted by Maurizio
Post a comment

Basic XHTML (including links) is allowed, just don't try anything fishy. Your comment will be auto-formatted unless you use your own <p> tags for formatting. You're also welcome to use Textile.

Don't type anything here unless you're an evil robot:


And especially don't type anything here:

Copyright © 2002-2008 Ryan Grove. All rights reserved.
Powered by Thoth.