A Simple and Elegant PHP/MySQL Web Application Framework, Part 3: My God, it's Full of Objects!

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.