Other Entries

Log

Creating Magic Methods in PHP

I’ve learned a lot of things from various open source frameworks, especially CakePHP. One of the most impressive things about Cake, and Ruby on Rails for that matter, is its magic methods in its data model class, findBy and findAllBy which allow you to query the database by calling a non-existent method like findAllByColor ('blue'). I’ve implemented similar methods in the home-grown framework that I use at work. Here’s how to do it.

Through its overloading mechanism, PHP allows you to catch calls to methods that don’t exist. You do need to be using classes and generally good object-oriented techniques in order to do this, but you know that, right?

Here’s the catch: overloading is native to PHP5, but needs some coersion in PHP4.

The basic bit of magic here is the __call method. In PHP5, __call takes two arguments: the name of the method called and a numerically-indexed array of arguments passed to the called method; in PHP4, it takes three arguments: the method name, the arguments array, and a variable to hold the return value, passed by reference. Let’s take a look:

PHP5:

    class MyClass
        {
        private function __call ($method, $args)
            {
            return 'called ' . $method . ' (' . implode (',', $args) . ')';
            }
        }

PHP4:

    class MyClass
        {
        function __call ($method, $args, &$return)
            {
            $return =  'called ' . $method . ' (' . implode (',', $args) . ')';
            return true;
            }
        }

The difference between the PHP5 and PHP4 versions is pretty much in the way that the result is returned. In PHP5, you can return directly. In PHP4, you have to set your return variable to the return value and then return true if the operation you performed was successful.

There are two other things that need to be done for PHP4.

  1. You need to explicitly overload the class: overload ('MyClass');
  2. You need to suppress the warnings when calling a non-existent method. Otherwise, you’ll get an error displaying, depending on your error level: $data = @$instance->magicMethod ();

So if you do the following (PHP5, which I’m using from here on out—you can figure out the PHP4 version, right?):

    $instance = new MyClass ();
    echo $instance->findAllByColor ('blue');

You’ll get “called findAllByColor (blue)” output. OK, but what can you do with it?

Let’s say that your class has a method findAll ($conditions = array (), $fields=>array ()) that performs a database query. Basically, we want to map our findAllByColor ('blue', array ('id', 'name')) to findAll (array ('color'=>'blue'), array ('id', 'name')). Using __call () it’s pretty easy:

    class MyClass
        {
        public function findAll ($conditions = array (), $fields = array ())
            {
            print_r ($conditions);
            print_r ($fields);
            }
        private function __call ($method, $args)
            {
            if (strpos ($method, 'findAllBy') === 0)
                {
                $field = strtolower (str_replace ('findAllBy', '', $method));
                $conditions = array ($field=>array_shift ($args));
                array_unshift ($args, $conditions);
                return call_user_func_array (array ($this,'findAll'), $args);
                }
            return false;
            }
        }

So the magic happens inside the __call method.

  1. First we look at the method that was called. If it matches our simple pattern (is “findAllBy” at the beginning?), we isolate the field name. I’m converting to lowercase because that’s how I name my database fields.
  2. Then we build a conditions array: the key is the fieldname and its value is the first argument that we passed to findAllByColor, which we get by shifting off the first item from $args. $args only contains one item now, the $fields array.
  3. We rebuild the arguments array by putting our $conditions array at the beginning of the $args array. $args now contains two items, the conditions and the fields.
  4. Finally we pass the arguments to findAll () via the call_user_func_array() callback function and return the result. It takes two arguments: the function or method to call and an array of arguments for the function called. Since we’re within the context of a class, we need to assert the scope to call_user_func_array () by passing an array as the first argument: array ($this, 'findAll'). The first item establishes the scope where the method exists, the second item is the method name as a string. If call_user_func_array () is called with a string as the first argument, it uses a global scope, so you can only use functions that have been declared outside of a class or core PHP functions.

This is a pretty simple example of how to create magic methods. There are lots of things that you can do with this:

  1. You could have multiple magic methods such as findByColor, etc. to retrieve a single record.
  2. You could call findAllByColorAndSize ('blue', 'large'). To handle this, in the __call method you’d call explode ('And', $field) and loop through the resulting array to build the conditions array.

One thing to watch out for, though: since __call catches any non-existent methods in an overloaded class, any typos in method names won’t generate error messages, so you’ll probably want to use trigger_error to generate an error before returning.

08/09/07 10:32PM PHP

Comments

stojce:

There is no ‘public’ scope in PHP4

08/12/07 1:11PM

Chris:

@stojce: You’re right… Updated the example code.

08/12/07 7:48PM

bill nye:

I wish I understood all of this. It seems like, instead of just having a findBy(“color”,”blue”) function, you’re making it kinda dynamic, so you can move color to the functions name, and have fewer arguments. Are there benefits to this, is it personal preference, or is it just because it can be done? My first thought, is it would make reading other peoples code harder, as my first instinct is to search for the findByColor() function (which doesn’t exist) to see what the arguments are supposed to be.

08/12/07 9:28PM

Thomas Schaaf:

Pretty cool :) Thanks for sharing!

BTW, I think you dont need to write PHP 4 stuff anymore since it will be discontinued in 2008

08/12/07 10:04PM

Chris:

@bill nye: I personally find that it expresses my intent very clearly, but you’re right, it does obscure some of how it works. I guess the key there is good documentation.

@Thomas: I know PHP4 will no longer be supported officially, but there’s still a huge chunk of hosts out there that will be very slow to upgrade to 5. Just wanted to note the difference between the two.

08/13/07 11:15AM

pcdinh:

Magic __method is great for some case but it is not tool-friendly. Code completion in Eclipse PDT, Zend Studio does not support it. You have to write your document carefully to advise people what happens when they try to call a not-so-predefined method. No tool can support that this time.

08/13/07 1:30PM

foobar:

See good examples of Zend_Db magic usage.

08/14/07 2:17AM

Add a Comment

Have something to say about what I wrote here? Let’s hear it!

The Rules

Personal Information




Remember Information


Comment Preview