Quantcast
Viewing latest article 4
Browse Latest Browse All 10

An experiment: Django Framework-like querying/model interface in PHP

I have continued with this experiment and created a new querying interface compatible with PHP 5.3 and has no dependent PECL extensions: Experiment update: revised Django-like PHP (5.3 compatible) querying interface.

So this is an experiment I’ve had in the making for quite some time. I’ve been working for years trying to build a reusable “generic object” querying framework that I made my PHP applications easier to maintain and faster to develop. I originally started with a small library of classes, including a ‘GenericObject’ class and a ‘Collection’, which allowed for proper iteration using the Iterator interface — all wrapped around a very messy Factory design pattern.

Up until about a year, I used hacked and modified versions of these classes for all of my projects and was getting tired of the mess. After doing some casual research I finally came across the Django Framework and found the object creation and database querying quite phenomenal as a programming interface. I didn’t want to spend years trying to get my Python up to par with my abilities with PHP, so I decided to try and replicate it. For the last year, again, I’ve been using a pretty hacked up version of what seemed to me like a “Django-like” interface, combined together with my old GenericObject and Collection classes from long before. With this, I was previously able to perform really simple database queries with an interface like so:

$users = User::get(array("fullname__contains" => "Andrew"));

foreach($users as $user) {
  // ...
}

Extending this interface, I could further create more complex queries with AND and OR’s by how I passed a series of function arguments and array items. This worked pretty great actually, definitely increased my productivity when adding to an existing application, but made maintenance somewhat difficult due to hard-to-read queries that took a lot of pausing and mental analysis.

My research went deeper into the Django documentation and I decided: what’s stopping me from making an almost (this being the key word here) identical interface/framework for PHP, complete with complex querying using Q() and F() objects? Well, ultimately: a lot. But I’ve started, and I’m proud of my work that I want to share.

The Problems

First and foremost, the biggest hurdle in my goal was the lack of operator overloading in PHP. This is what I would need in order to successfully achieve complex querying with Q() and F() objects, such as listed in the Django examples:

Q(question__startswith='Who') | Q(question__startswith='What')

I also discovered this topic has been brought up many times before, with the final word being an unquestionable “never.” This was obviously disappointing for two reason: 1) it meant my goal was out of reach; and 2) PHP is a mature language which I have based my career around up to this point, and now I’ve come across what I believe to be a major limitation for the future.

Some more research turned up the PECL operator Package, which does override PHP’s functionality and allow for some basic operator overloading. Perfect! But another roadblock: it took me long enough to dig up a precompiled version of the package (since PECL doesn’t compile on Windows systems) and I only had a version that was compatible with PHP 5.2.x. Tough news. This meant I didn’t have access to beautiful things in PHP 5.3 such as late static bindings and other new additions to the language. I was able to overcome this by writing my own hacked-up, squirrely version of the get_called_class function and gratuitous cringe-worthy use of the eval function.

My final problem was Python’s beautiful ability to pass arguments to functions/methods in a format such as:

Entry.objects.filter(headline__startswith="What")

As much research as I could possibly stand to do, there really is no way around this in PHP. there is no way of passing a variable name a value as a function argument. My work around, despite being what I originally wanted to get away from, was to do something as follows:

Entry::get(array("headline__startswith" => "What"));

Obviously this creates some unfortunate coding overhead and reduces fast readability, it’s my only solution thus far.

Despite these trade-offs, I now had just about everything I needed to create a respectable PHP equivalent of Django querying.

The Solution

I went through the Django document on making queries up and down, left and right, and tried to get the best grasp the concepts of the interface and how they could be applied in PHP. Before even getting to code, I spent a couple nights just thinking about how this could be done in the back-end code of my framework/interface, and finally began coding backwards: first I started writing out some of the Django examples in PHP, then writing backwards through all the classes and objects that I would require in order for these examples to output what I wanted.

Before I get into the nitty gritty of how all the code works together, I’ll provide a few examples of my interface looks like so far and the queries it’s able to generate. I’m not yet at the point where I have QuerySets and usable objects, but I’ve done the hard part by creating the MySQL statement query building.

First I start by creating my initial database Model:

require_once("Model.class.php");

class Product extends Model {
// optional: table name is automatically generated
// using the class name and pluralizing it
//const table_name = "products";
}

From here, I can use my static get() method to create queries:

$r = Product::get();
print $r->create_statement();
// output: SELECT id FROM products

$r = Product::get(array("make__contains" => "Sony"));
print $r->create_statement();
// output: SELECT id FROM products WHERE make LIKE '%Sony%'

$r = Product::get(array("make__contains" => "Sony"), array("category__exact" => "TV"));
print $r->create_statement();
// output: SELECT id FROM products WHERE (make LIKE '%Sony%' AND category = 'TV')

I can then filter() and exclude() these Query objects like with Django:

$r = Product::get(array("make__contains" => "Sony"));
$r = $r->exclude(array("model__contains" => "RD3"));
print $r->create_statement();
// output: SELECT id FROM products WHERE make LIKE '%Sony%' AND NOT model LIKE '%RD3%'

$r = Product::get(array("category__contains" => "Stereo"));
$r = $r->filter(array("make__exact" => "Panasonic"));
print $r->create_statement();
// output: SELECT id FROM products WHERE category LIKE '%Stereo%' AND make = 'Panasonic'

And I can also use the critical Q() and F() functions, along with operator overloading to create complex queries:

$r = Product::get(Q(array("make__contains" => "Sony")) | ~Q(array("category__exact" => "TV")));
print $r->create_statement();
// output: SELECT id FROM products WHERE (make LIKE '%Sony%' OR  NOT category = 'TV')

$r = Product::get(array("selling_price__gte" => F("cost") * 2));
print $r->create_statement();
// output: SELECT id FROM products WHERE selling_price >= cost * 2

Of course, this isn’t as perfectly pretty as the Django interface, but it is quite readable and more maintainable than any other existing PHP solution I am aware of.

Additionally, I also added basic functionality for delete() and update() methods:

$r = Product::get(array("selling_price__gte" => F("cost") * 2));

print $r->delete();
// output: DELETE FROM products WHERE selling_price >= cost * 2

print $r->update(array("make" => "Sony", "category" => "TV"));
// output: UPDATE products SET make = 'Sony', category = 'TV' WHERE selling_price >= cost * 2

Use of limiting (through the ArrayAccess interface) and order_by() methods is also available:

$r = Product::get(array("make__exact" => "Sony"))->order_by("-model");
print $r->create_statement();
// output: SELECT id FROM products WHERE make = 'Sony' ORDER BY model DESC

$r = Product::get(array("make__exact" => "Sony"));
print $r["5:10"]->create_statement();
// output: SELECT id FROM products WHERE make = 'Sony' LIMIT 5 OFFSET 5

print $r[":10"]->create_statement();
// output: SELECT id FROM products WHERE make = 'Sony' LIMIT 10

At the moment, as mentioned previously, this interface is only good for creating MySQL queries. But now that my base functionality exists, applying this to full iterable query sets and database objects should be easy in comparison.

I still have a lot of work to go to round this all together, but I’m quite happy with my progress so far with a couple days of on-and-off investment of time.

I’m more than happy to offer up my code so far, but before there is any severe judgment in the quality of it, I should point out that I made little to no reference to the core Django code (except for one small piece that originally lead me to object overloading). If any readers show interest in my code, then I’ll do a small explanatory write-up of the classes and design patterns in another blog at a later date.

View/download the Model abstract class.

View/download my hacked up get_called_class function.

View/download the Query class.

View/download the S class (used for Q and F objects, shows operator overloading).

View/download the examples used in this blog entry.

Of course, please let me know if any of this code piques any interest in you or if you use any of the code in your applications.

Update – August 11, 2010 – So I caved and took a good hard look at the Django source. It’s not nearly as complicated as I imagined and my suspicions about how the back-end works just from looking at the docs is pretty close. I’m now either considering a full line-for-line port of Django to PHP or continuing with what I currently have and making it my own.


Viewing latest article 4
Browse Latest Browse All 10

Trending Articles