views:

73

answers:

2

Rails relies on some of the neat aspects of Ruby. One of those is the ability to respond to an undefined method.

Consider a relationship between Dog and Owner. Owner has_many :dogs and Dog belongs_to :owner.

If you go into script/console, get a dog object with fido = Dog.find(1), and look at that object, you won't see a method or attribute called Owner.

What you will see is an owner_id. And if you ask for fido.owner, the object will do something like this (at least, this is how it appears to me):

  1. I'm being asked for my .owner attribute. I don't have one of those!
  2. Before I throw a NoMethodError, do I have a rule about how to deal with this?
  3. Yes, I do: I should check and see if I have an owner_id.
  4. I do! OK, then I'll do a join and return that owner object.

PHP's documentation is - ahem - a bit lacking sometimes, so I wonder if anyone here knows the answer to this: Can I define similar behavior for objects in PHP? If not, do you know of a workaround for flexible model joins like these?

+8  A: 

You can implement the __call() method in PHP which is a catch all for calling an otherwise inaccessible method.

class MyObj {
  public function __call($name, $args) {
    $list = $args ? '"' . implode('", "', $args) . '"' : '';
    echo "Call $name($list)\n";
  }
}

$m = new MyObj;
$m->method1(1, 2, 3);
$m->method2();

Some languages (eg Javascript) also have what are called first-class functions. This essentially allows you to add or remove methods from objects (or classes) on the fly. PHP syntax (as of 5.3) sort of supports this but it isn't really usable.

$obj = new stdClass;
$obj->foo = function() {
  echo "hello\n";
};

print_r($obj);

Output:

stdClass Object
(
    [foo] => Closure Object
        (
        )

)

But try:

$obj->foo();

and you get:

Fatal error:  Call to undefined method stdClass::foo() in C:\xampp\htdocs\test.php on line 8

However:

$f = $obj->foo;
$f();

correctly outputs hello.

cletus
This was interesting. It turned out that `__get()` and `__set()` were what I needed for this case, though.
Nathan Long
A: 

Overloading to the rescue!

After some further research, it appears that what I really wanted was PHP's overloading methods. The code at the link Gordon gave, and especially the downloadable example they offer, was very illuminating.

(Despite my question title, what I was really after was to have an object respond to undefined attributes.)

So, __get() and __set() let you specify methods to use for getting and setting object attributes, and within those methods, you can tell the object what to do if no such attribute exists. Let's just look at __get() for now.

Going back to my Dog example, you could use it like this:

class Dog{
    // Private attributes, not accessible except through the __get method
    private $bark_volume = 'loud';
    private $owner_id = '5';
    public function __get($name){
        // If there's a property by that name, return it
        if (isset($this->$name)){
            return $this->$name;
        }
        // If not, let's see if there's an id with a related name;
        // if you ask for $this->owner, we'll check for $this->owner_id
        $join_id = $name . "_id";
        if(isset($this->$join_id)){
            // This is pretty useless, but the id could be used
            // to do a join query instead
            return $this->$join_id;
        }
    }
}

$Fido = new Dog;
echo $Fido->bark_volume; //outputs 'loud'
echo '<br/>';
echo $Fido->owner; //outputs '5'
Nathan Long