MyActiveRecord - New Release
I've finally got it together to release an update to the MyActiveRecord class.
There's nothing like open-sourcing some code to teach you a few lessons:
- Not everyone is using the same version of PHP, MySQL or OS as you
- Not everyone is using English as their first language
- Whilst you might not be dealing with the kind of databases that trigger out-of-memory errors in your script, some of your users most definitely are...
So after releasing MyActiveRecord via phpclasses last March, I was overwhelmed with the response in both good and bad ways. Almost everyone who wrote said that the class had really helped them speed up a major project they were working on (good!). Also most people really got my motivations for developing the class; Many were quite familiar with Ruby on Rails but for whatever reason were sticking with PHP. Most were after a class they could use to cut down on basic SQL hoop-jumping, but they didn't want to learn an entire framework, and they wanted the flexibility to do things their own way. Many were developing in a PHP4 environment and deploying on PHP5 or vice-versa.
In my update I tried to incorporate bug-fixes, some of the nicer ideas, and stuff that I'd implemented half-heartedly and wanted to do properly. The major new features are:
- SQL Logging (handy for debugging)
- Query Caching (improves performance)
- Single Table Inheritance (allowing you to store a class hierarchy in a single table)
There are also some new, useful methods:
The Prepare() Method allows you to escape SQL parameters in a hassle-free way, using sprintf syntax. Eg:
$condition = MyActiveRecord::Prepare("first_name LIKE '%s' AND age > '%d'", $_GET['first_name'], $_GET['age']);
$people = MyActiveRecord::FindAll('Person', $condition);
The FreqDist() Method will provide you with an array containing the distribution of unique values for a given table/field. Eg:
foreach( MyActiveRecord::FreqDist('Person', 'name') as $name=>$total ) { print "There are $total people with the name $name";
}
The TableExists() is a utility method to tell you if a table exists in the database or not
if(MyActiveRecord::TableExists('person')) print '`person` table exists!';
The add_child() method provides a convenient way of creating new child objects:
$comment = $post->add_child('Comment', $_POST); $comment->save();
The validadate_uniqueness_of() method allows you to check whether a field value is unique before attempting to save it in the database:
$person->validate_uniqueness_of('username', 'username is already in use');
Most of the changes are under the hood. One you do need to know about is the way MyActiveRecord maps classes to tables. Originally classes were supposed to be named identically to the table in your database. So a Person class would map to a Person table. Unfortunately a bug in PHP4's get_class method meant that this could not be sustained. The get_class function always returns a lowercase string in PHP4. The only reason this didn't cause problems for most users is that most combinations of OS & MySQL effectively give you case-insensitive table-names. However this wasn't true for everyone. The only way to get 100% compatibility for all cases is to mandate that table-names are specified in lowercase. So a Person class will map to a person database table. It's one of those classic PHP frustrations, and it annoyed me no end, but you could say that this whole project was born out of a desire to circumvent PHP's foibles
What didn't make it.
A lot of people asked for stuff that seems perfectly reasonable, but that I decided to leave out anyway. Here's what got left out:
- Changes to mappingPeople wanted to be able to append stuff to the front of their table names i.e. myapp_person mapping to Person. People wanted to be able to change the name of the field that acted as primary key etc. I've resisted this because whilst on the surface these things seem simple enough to implement, in practice they add complexity and ambiguities at every level of the class. MyActiveRecord wasn't my first attempt at a mapping class, and defining a few simple, solid rules about naming and IDs was what freed me up to finally build a class that I felt had some elegance and integrity. It also allowed me to document and explain the class without constant caveats and exceptions. So I'm not bending on this yet. (Actually, working around the
get_class()bug required me to add aClass2Tablemethod, so in theory, adding a prefix to your table names would be easy enough and I might consider it.) - View logic in the mapper classA few people supplied me with some helpful methods for getting HTML out of the class. Pre-formed error lists and the like. Obviously this is evil, I even feel a bit icky about the
h()method, but it's just so damn handy and I stole the idea from Ning. Actually, I do have a class that takes a MyActiveRecord object and then uses methods to spit out helpful HTML fragments, but it's not quite ready for production yet... - Event hook methodsSome people supplied rails-esque event-hook methods like before_save(); before_destroy() etc.. After some deliberation I decided not to add these. Why? Because I think you can achieve exactly the same thing by extending the class. That is one of the key advantages of OO after all!
For example:
If someone can make a good argument as to the advantage of putting blank event hook methods into the class I'll look into it again, but for now I'd rather prevent the base class from getting any broader!Class Person extends MyActiveRecord { function save() { return $this->some_cleanup_or_test_method() ? parent::save() : false; } } - Shorter NameSo some people are developing RSI typing the same 14 characters over and over. I admit, the name is ugly and too long, but it has several marketing advantages with regards to describing what the class does, picking up search engines and so on. This one is easily solved anyway:
Actually I really recommend this approach whenever you build a serious application, as this gives you an opportunity to extend all sorts of basic functionality like saving etc.. at your application level. For example, you might decide to extend the save method so that it stores a serialised copy of every object that's ever updated, giving you a complete transactional history of every object in your application. It's easy to do. Plus when you develop some great ideas in your extended classes you can send them to me and I'll have nice cleanly separated code to incorporate into the next version of the base class.Class AR extends MyActiveRecord{} Class Person extends AR{} $people = AR::FindAll('Person');
What's next?
Apart from the fact that I'm extremely busy anyway I don't really intend to develop this class a whole lot further. It does a pretty nice job, and it's already a lot bigger than I would really like. There will be bugs in this release, and I will fix any that are reported. Other than that, I intend to improve the general integrity of the class and add some magic field names (datetimes called modified_on and created_on will get time-stamped automatically, stuff like that).
I want to deal with the caching and encapsulation of related objects so that - for example - saving a post object will automatically save changes to any child comment objects.
Once those basic features and bug fixes are implemented I'll declare the class at version 1(beta) and after a reasonable time to fix any bugs I'll declare it 1.0.
I'd like to thank Walter Lee Davis, Mark Kaye, Mike Stupak and Mihael Konjevic in particular for their help and feedback to date.

Leave a Reply