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
No title
Thanks! Love reading your posts.
No title
Ditto. Hopefully I'll learn enough to do something useful with Poseidon. ;)
Well done
This is a very inspiring series. I do hope you will enlighten us on the intricacies of XML and XSLT in a follow-up.
No title
+1
Small correction
Great series.
Re: small correction
Oops, you're right. Good catch!
Nice, but what about...
What's you naming convention for queries that use multiple tables (joins etc). And where would you put functions that retrieve all monkeys?
Re: Nice, but what about...
commenttable and ausertable for the purpose of retrieving all comments by a specific user, I'd probably name the querycomment.getByUserId. And for a query that retrieves all users, I'd just call ituser.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.
Wonderful
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.
how to use the save() funct.
How can I use the save() function?? i.e. Insert new user
how to use the save() funct.
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:
Thanks, the save is fine
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
Exactly what I needed
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!
What do multiple table queries return???
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???
Re: What do multiple table queries return???
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.Re: What do multiple table queries return???
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.
Figured out multiple table queries issue
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.
Amazing post
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
Smarty
Could give a few examples why Smarty is such a bad template system?
Re: Smarty
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.
Nice framework
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)
Integrate ajax in this framework
Hi Ryan,
I’d like to hear from you on how would you integrate Ajax in this structure you developed.
Thank you.
Re: Integrate ajax in this framework
Sure. Here you go:
Framework limitation (?)
Hi Ryan,
I’m having trouble with one aspect of the framework and I’d like to ask your help. I’ll try to explain clearly.
On my framework every action has a request.php and a reply.php files which are loaded in the middle of the index.
Page request (?action=site/actions/listSome thing) → Index (1st loads the layout.request.php, 2nd load the layout.reply.php).
layout.reply.php is responsible for checking if the action files (listSomething.request.php, listSomething.reply.php) exist and load them.
The problem is that this way, all pages have the same header and appearance. Only the middle content changes. If, for example, I want to open a lightbox with a form (?action=site/actions/lightBox Form), It will show up with the same header, appearance, etc, of the rest of the website.
How do I escape this?
Thank you,
Pedro
Set page title
Hi Ryan,
How would one define page titles and meta tags, etc, for each different action?
Thank you,
Pedro