Further work on a django-like PHP ORM framework: “phrames”

It’s been a long time since I have last posted and since I have had the time to develop my django-like PHP framework/ORM. However in last few days I was able to crack down and get quite a lot done from where I left off.

I last left off considering what my ultimate plan was: do I want to sit and study the entire django codebase and create a line-for-line duplicate? Not really, but I was able to use the source for a lot of reference points. Please keep in mind that, more than a serious project, this iss more about an experiment in exploring some of the deeper functionality in PHP. I haven’t had a chance to refactor any of my code, so some of it is pure garbage, but I’ve commented as much as I could that didn’t seem immediately obvious. If this inspires one person or if one person uses even one class or one method of my code, then mission accomplished!

I’m now calling my ORM “phrames” (as in PHP Framework)… not original, but among the millions of PHP frameworks it wasn’t already taken.

Models and Field Types

My first goal of this project was simply to create a lightweight and rapid-development framework similar to Python’s django. I didn’t want to create a set of “rails” scripts, I didn’t want to sit and redefine all of my MySQL tables as models. I wanted to be able to whip together some quick ORM-style models and only define what needed to be defined. It doesn’t have (or need) all of the functionality that django has in its giant feature set.

Defining models is quite similar to django. You don’t need to define each database table column, you simply specify what your model represents (and, optionally, what the database table name is). Given the following table definitions:

CREATE TABLE `manufacturers` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(250) DEFAULT NULL,
PRIMARY KEY (`id`)
);

CREATE TABLE `products` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `manufacturer` int(11) DEFAULT NULL,
  `name` varchar(250) DEFAULT NULL,
  `weight` int(11) DEFAULT NULL,
  `cost` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
);

You could quickly define the following “phrames” models:

require_once("phrames/Model.class.php");

class Manufacturer extends Model {}

class Product extends Model {}

and right away you could begin querying these tables (read further below). The Model class will automatically determine what the table name should be (Product class looks for products table). Alternatively, the table name could easily be manually defined:

class Product extends Model {
  const table_name = "my_products_table";
}

Further, it is fully possible to quickly create foreign key and one-to-many relationships. These are defined by adding class constants of the table column or pseudo-field names:

class Manufacturer extends Model {
  const products = "Product:"; // One-to-many field, returns all of the products of this manufacturer
}

class Product extends Model {
  const manufacturer = "Manufacturer"; // Uses the manufacturer table column to reference a Manufacturer object
}

So now if you had a Product object (again, see section on querying below for how to obtain one), you could easily obtain it’s manufacturer as an object as well:

print $some_product->manufacturer->name;

Likewise, if you have a given manufacturer, you could easily obtain all of it’s products (however, it is required that the ‘manufacturer’ foreign key definition of Product is defined, so it knows where to look):

foreach($some_manufacturer->products as $product) {
  print $product->name . "<br />";
}

I have also added ability to support many-to-many relations between tables. Here is a new example:

class Topping extends Model { }

class PizzaTopping extends Model {
  const the_topping = "Topping";
  const the_pizza = "Pizza";
}

class Pizza extends Model {
  const toppings = "Topping:PizzaTopping";
}

In this example, a many-to-many relationship is created between pizzas and any combination of toppings, which could exist on any topping. The ‘toppings’ constant of the Pizza model is not a column in the database table, however it uses the PizzaTopping model/table to find its toppings. The format is

const some_virtual_field = "Find_Class:Through_Class";

Obviously this is very similar to the one-to-many relationship example above.

Querying

I will use an example above to demonstrate some examples of how queries are performed, how they are retrieved and store, and how my intelligent table joining works.

First and foremost, I’ve done by best to copy most of django’s query types. A few examples of queries using the models above and what SQL statements they would generate (a small note: these aren’t the exact statements generated. I use a number of table aliases to better support intelligent table joining, to only join foreign key relationships when absolutely necessary):

$m = Manufacturer::objects()->get(1);
// SELECT ... FROM manufacturers WHERE id = 1
// (returns a single object)

$ms = Manufacturer::objects()->all();
// SELECT ... FROM manufacturers

$ms = Manufacturer::objects()->filter(Field::name__contains('S'));
// SELECT ... FROM manufacturers WHERE name LIKE '%S%'

$ms = Manufacturer::objects()->exclude(Field::name__startswith('S'));
// SELECT ... FROM manufacturers WHERE NOT (name LIKE 'S%')

All of the examples above (except the first), return a QuerySet object which can be iterated over:

foreach($ms as $m)
  print $m->name;

In cases where foreign keys are used, you are able to query the table joins by the foreign key. SQL joins are only created when necessary

$ps = Product::objects()->filter(Field::name__contains('A'));
// SELECT ... FROM products WHERE name LIKE '%A%'

$ps = Product::objects()->filter(Field::manufacturer__name__contains('S'));
// SELECT ... FROM products LEFT JOIN manufacturers ON products.manufacturer = manufacturers.id
// WHERE manufacturers.name LIKE '%S%'

Additionally, with many-to-many relationships, SQL sub-queries are only performed when needed:

$pizza = Pizza::objects()->get(101);
// SELECT ... FROM pizzas WHERE id=101
$toppings = $pizza->toppings;
foreach($toppings as $topping)
  print $topping->name;
// SELECT ... FROM toppings WHERE id IN (SELECT the_topping FROM pizzatoppings WHERE the_pizza=101)

Alternative/Experimental Syntax

I’ve also been playing around with a bit of an alternative syntax for query expressions (i.e. Field::somefield__contains($testval)) where using the Field class static method call isn’t necessary. This helps to create a more native django-like feel (with reduced PHP verbage):

Products::objects()->filter(name__contains('A'));

Products::objects()->filter(manufacturer__name__contains('S'));

Basically, how this works is when the objects() method is called on a class (returning a QuerySet manager), a list of all possible combinations of fields/table columns (obtained by performing a INFORMATION_SCHEMA query on the table) and query types (__in, __contains, __exact, __gte, __startswith, etc) are created as functions (using eval()).

This is still a bit experimental, but in my limited number of tests it has worked well. For small table sets (such as the manufacturer/product table definitions) I was able to create all required query functions in less than 0.05 seconds.

More Query Features

I’ve also since added the ability to create more complex expressions with ANDs and ORs using predefined functions. Last time I was experimenting with the idea of operator overloading using the PECL operator package. It worked, and it worked really well, but it simply wasn’t stable or reliable for production or long-term use. Instead I’ve implemented _AND_() and _OR_() functions, as borrowed from Python’s Storm ORM, brought up by a insightful reader.

Here is a quick example:

Manufacturer::objects()->filter(_OR_(name__contains('Sony'),
    name__contains('Toshiba'));
// SELECT ... FROM manufacturers WHERE (name LIKE '%Sony%' OR name LIKE '%Toshiba%'))

The _AND_() functions works the same way, and the two can be nested if necessary:

Product::objects()->filter(
    _AND_(cost__gte(10),
        _OR_(weight__lte(5), name__contains('P'))
    )
);
// SELECT ... FROM products WHERE (cost >= 10 AND (weight <= 5 OR name LIKE '%P%'))

Query Arithmetic

Lastly, I have added basic functionality for performing basic field arithmetic in queries:

Product::objects()->filter(cost__lt(selling__divided_by(2));
// SELECT ... FROM products WHERE cost <= (cost / 2)

Supported arithmetic functions: times, divided_by, plus, minus. I haven’t had much time to run tests on this feature, so it is still a bit experimental.

Ordering & Limiting

My favourite functionality that I’ve implemented thus far is the ability to order queries and extract sections of the results (using SQL LIMIT operators). QuerySet splicing was implemented using PHP’s ArrayAccess interface to create a much cleaner and easier to read coding style:

Product::objects()->all()->order_by("-name");
// SELECT ... FROM products ORDER BY name DESC

Product::objects()->all()->order_by("weight", "-cost");
// SELECT ... FROM products ORDER BY weight ASC, cost DESC

$products = Product::objects()->all();
$p = $products[1];
// returns the second item in the result set

$some_subset_of_products = $products[":5"];
// SELECT ... FROM products LIMIT 5

$another_subset = $products["2:5"];
// SELECT ... FROM products LIMIT 4 OFFSET 1

$last_subset = $products["5:"];
// SELECT ... FROM products LIMIT 18446744073709551610 OFFSET 4
// this is a bit of a dirty trick <a href="http://stackoverflow.com/questions/255517/mysql-offset-infinite-rows" target="_blank">explained over at Stack Overflow</a>

Conclusions

I still have quite a ways to go before I would ever be able to completely replicate all of django’s ORM features, but I’m fairly happy with my progress and it’s been a great learning experience.

I am offering my source for this little project, but please understand a lot of this was hacked together on a whim without much code clean-up. It desperately needs a refactoring and better documentation (read: any documentation). All of the internals could be better abstracted for code clarity and portability. I want to stress these statements for two reasons: 1) I don’t come off as a terrible programmer in regards to something I piece together in very small bits at a time, more as a proof-of-concept than anything; and 2) people know what they’re getting into when they look at my source.

If there’s interest, I wouldn’t mind tossing it up on github and seeing what other sorts of things people can contribute. As much as I would love to start a large open source project (if it could ever turn into that), my fear is that it would encompass too much functionality/bloat and become just like every other PHP framework out there. As I mentioned in my introduction, my original goal was simply to develop a flexible and lightweight ORM (possibly later on with Smarty template integration and forms) for someone that needs to develop, for example, a quick and dirty CRUD interface without hacking together a lot of unmaintainable spaghetti code strewn with MySQL statements and (sometimes at best) a poorly-implemented database abstraction/wrapper class.

Download the Source

Download my latest source as a ZIP file: phrames_b20110617.zip

9 thoughts on “Further work on a django-like PHP ORM framework: “phrames”

  1. zhangxiao

    Hey, Thanks for awesome work on this! I am also considering implementing a django-style model layer in PHP. Your work is definitely helpful for me 😉

    I briefly read your codes (not finish yet…). Regarding the Model::objects() method, have you thought about using PHP Overloading mechanism (http://www.php.net/manual/en/language.oop5.overloading.php#language.oop5.overloading.methods) ? So you don’t need call eval() to define those methods. Just my guess, I did not deeply think about it.

    Good luck!

    • Andrew

      My original interface used __callStatic overloading, which may be what you’re implying here, to define queries such as

      SomeClass::objects()->filter(Field::somefield__contains(‘some value’))

      While it makes perfect sense, it was a bit verbose. My “technique” here of using eval() to define methods at runtime (since all object properties/table columns aren’t intended to be explicitly specified) creates a bit more succinct PHP statements:

      SomeClass::objects()->filter(somefield__contains(‘some value’))

      So I have technically used the __callStatic overloading feature, the runtime-created functions was simply just an experiment, but seems pretty stable and reliable in my own testing.

  2. Parker Roth

    Have you ever come across this error?

    Fatal error: Cannot use object of type DB_Error as array in C:\xampp\htdocs\phrames\Collection.class.php on line 34

    I really want to try out your ORM on my project but can’t seem to get it running.

    -Parker

    • Andrew

      Looks like I didn’t do a very good job on error catching there.

      I would suggest double-checking your database connection info in your Config.class.php file and trying again. I’ll look into on my end to see how I can fix up that portion of the code to give some better error handling.

  3. It would be awesome if you post the code to github, so anybody can contribute, and also it will be a lot easier to keep track on the progress.

    • Andrew

      I’ve actually been leaning a lot more towards Mercurial as opposed to git lately. I’m going to look into hosting with bitbucket or Google Code and I’ll post an announcement when I’ve got something set up and rolling along.

      Thank you so much for your support!

  4. […] readying some fairly large projects at work, I haven’t had too much time to progress with my last update of my “phrames” ORM/framework. However, I am happy to announce (to whoever is willing to listen) that I’ve published my […]

  5. […] I haven’t used a PHP-based system with the fantastic model support offered by Django, but this project looks promising: Django-like PHP querying interface […]

  6. Musa Musa

    Nice work, I’m looking at adopting this for a little framework I built for small jobs I get to do some times. can we put this together and have a repo on github? I’ll love to make some contributions to this.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes:

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>