PHP and ActiveRecord

Posted by acts_as_flinn Wed, 08 Aug 2007 21:37:00 GMT

PHP on Rails

Updated: 2007-08-10 : Check out my response to Arnold Daniels’ article.

I’m starting a new job soon and I’ll be working primarily with PHP. Since I’ve been a rubyist for the last 2 years I’m looking at PHP from a Rails development perspective. Before working exclusively with Ruby I hung onto to PHP (because of the project I was working on) by porting Rails bits to PHP. I eventually gave up on porting Rails to PHP after my project’s funding was cut.

What went wrong

So since I’m moving back to a PHP environment my mind is again on Rails bits in PHP. When I first ported ActiveRecord to PHP from Ruby I wasn’t nearly as familiar with Ruby as I am now. I’m looking at PHP again and I understand why the ActiveRecord pattern doesn’t work in PHP nearly as well as it does in Ruby. ActiveRecord in PHP probably works just as well in Java.

Why ActiveRecord Doesn’t work in PHP
(or why everything is better in Ruby)

In Ruby everything is an object. Everything.

In ruby the class is an object and you can tell the class to do things using class methods and class variables with a separate scope than an instance. You simply don’t have this type of flexibility in PHP. Take the following for example…


class Person < ActiveRecord::Base
  acts_as_list
  has_many :hobbies
end

What exactly is happening here? The two lines within the class are actually calling class methods. The class methods operate on the class which in turn determines how the instance is created.

Here we are telling the person class to act as a list. That will include some instance methods in the Person class using Ruby’s mix-in feature which is far different from simple inheritance.

We’re also telling the class that a person instance has many hobby instances associated with it. Rails performs some magic here and again adds in a number of instance methods that enable this to happen.

In PHP 4 ; 5 this is impossible but we may get something new in PHP6 with the namespace/module feature ideas that have been floating around for the last two years. And why not PHP/Zend? The Zend Framework is clearly an attempt to copy jump on the MVC boat that Ruby on Rails made (more) popular…so let’s just go all the way and copy a feature that will actually enable PHP to evolve. Sorry…side tracked.

If you try to implement the above in PHP you get the following:


Class Person Extends ActiveRecord_Base {
  public function __constructor() {
    $this->acts_as_list();
    $this->has_many('hobbies');
  }
}

You might be saying to yourself “big woop, what’s the difference.” The woop is the fact that the class isn’t able to access acts_as_list or has_many unless you define them some place in the inheritance chain. If that’s the case you’ve got to add a lot of conditional garbage to your instances in order to come up with functionality close to this.

A better example becomes really clear when you toss in plugins.


class Hobby < ActiveRecord::Base
  limit_by_scope :person
end

Person.current = Person.find_by_name('Flinn')
Hobby.count #=> 0
Hobby.limit #=> 3
Hobby.available #=> 3

%w[kayaking hiking frobnicating].each do |hobby|
  Hobby.find_or_create_by_name(hobby)
end

Hobby.count #=> 3
Hobby.limit #=> 3
Hobby.available #=> 0

In the above code we’re setting a class attribute (current) to set the scope for all other classes and objects. Then we’re asking the Hobby class to tell us how many hobby’s the current person has, how many it can have and how many we are free to add.

Where do limit and available come from?

They come from the class method limit_by_scope added by the plugin of the same name. The method adds a few class methods and class attributes to enable this functionality. It IS IMPOSSIBLE to do this in PHP.

Reflection, Reflection, Reflection.

In PHP you have no ability to reflect on a class the way you would in Ruby. This becomes very clear for anyone who has ever tried implement something like this in PHP:
Person::find(1)

For the record the above IS IMPOSSIBLE to do the right way™ but let’s explore it for fun.


Class Base {
  function me() {
    return get_class();
  }
}

Class Foo Extends Base { }

echo Foo::me(); # returns "Base" 

What does that mean?

It means you can never ever ever reflect on the data using static methods.

Ok, so what.

Why not just instantiate the object like so?


Class Base {
  function me() {
    return get_class($this);
  }
}

Class Foo Extends Base { }
$foo = new Foo();
echo $foo->me(); # returns Foo

Well that works in this tiny little demo – but for ActiveRecord it sucks. I can hear you now, “but it works for Cake”. Ok so it does – but it’s wrong. It’s wrong because you are relying on the instantiated object to find a like object. It just doesn’t make any sense. The finder object has all the bits to make it a proper record but it’s also got all the bits to make it a finder object. So let’s say for example:


$person = new Person;
$people = $person->find_all();
$person->name = "Flinn";

The purpose of the person class and object is not so clear. Is it a record or knower of the record?

Last but not least according to Martin Fowler’s definition of ActiveRecord in Patterns of Enterprise Application Architecture , finder code should be static ie. Person::find(1).

So in PHP you CAN do ActiveRecord if you manually define finders but that’s not nearly as elegant as in Ruby and it certainly requires a lot more arm twisting programming.

Symbols & Flexible Arguments

This one is simple, in Ruby you want to specify a symbol :person rather than ‘person’ because it’s fast and painless.

ActiveRecord makes some pretty heavy usage of dynamic argument passing using hashes, eg.


flinn = Person.create(:name => 'Flinn', :nerdy => true)
gina = Person.create(:name => 'Gina', :hotty => true)

nerds = Person.find(:all, :conditions => { :nerdy => true })

This isn’t exactly part of the ActiveRecord pattern, just a niceity that Rails adds. This is a Ruby hack but it’s an awesome one.

So what’s the solution?

I don’t know!

The Row Data Gateway seems to be doable in PHP. The RDG pattern would seem to enable us to add a static finder object to the class that would enable us to handle the meta class programming issues a little easier using a singleton factory implementation.

Here’s a crude example (i don’t even know if it works updated to work):

Class RDGMeta {
  static public $instances = array();
  public $sender;
  //an example of the kind of meta info we'll store in the class
  public $table_name;
  public $attributes;

  public function __construct($sender) {
    $this->sender = $sender;
  }

  public function find() {
    ...

Class RDG {
  static public $instances = array();
  static public function meta($sender) {
    if (!array_key_exists($sender, self::$instances)) {
      self::$instances[$sender] =& new RDGMeta($sender);
    }
    return self::$instances[$sender];
  }

  public function save() {
    ...

Class Person extends RDG {
  static public function meta() {
    parent::meta(get_class())->has_many('hobbies');
    return parent::meta(get_class());
  }
}

Class Hobby extends RDG {
  static public function meta() { return parent::meta(get_class()); }
}

$people = Person::meta()->find_all();
$flinn = Person::meta()->find_by_name('flinn');
$flinn->update_attributes(array('is_sexy' => true));
$nerds = Person::meta()->update(array('is_sexy' => false), array('is_nerdy' => true)); # that would set is_sexy to false for every person who is a nerd.

You can see here the clear distinction between the meta object and the record object. Fowler explains the similarities between the ActiveRecord and Row Data Gateway patterns by saying there really isn’t a huge difference just that if you have domain logic in the class, use ActiveRecord.

My interpretation is that in this context the finder object acts like a Table Data Gateway in many ways giving us access to finder methods and table manipulation methods but returning row data gateway objects.

But that doesn’t relieve us of the lack of mix-ins which I think is essential for a proper plugin architecture.

More to come on a later date for the whole subject. It’s more complicated than I want to go into right now.

Related

Trackbacks

Use the following link to trackback from your own site:
http://www.actsasflinn.com/trackbacks?article_id=php-and-activerecord&day=08&month=08&year=2007


ss_blog_claim=746d258dc975cb7923cc57154dbf1d71