[ Team LiB ] Previous Section Next Section

Intercepting Calls to Object Properties and Methods

PHP 5 introduces three built-in methods that you can use to trap messages to your objects and alter default behavior.

Usually, if an undefined method is called, a fatal error will be generated and your script will fail. If you define a __call() method in your class, however, you can catch the message and decide what to do with it. Every time an undeclared method is called on your object, the __call() method will be invoked. Two arguments will automatically be populated for you: a string variable holding the name of the method that was called and an array variable holding any arguments that were passed. Anything you return from __call() is returned to the client code as if the undefined method existed. Let's put some code together to illustrate this point:


class TestCall {
  function __call( $method, $args ) {
    $ret = "method '$method' called<br />\n";
    $ret .= "<pre>\n";
    $ret .= print_r( $args, true );
    $ret .= "</pre>";
    return $ret;
  }
}
$item = new TestCall();
print $item->arbitrary( "a", "b", 1 );

We create a class called TestCall and give it a __call() method. __call() expects the method name and an array of method arguments. It assigns a string quoting the contents of both of these to a variable, $ret, and returns it to the calling code. We call the nonexistent $test->arbitrary() method and print the result:


method 'arbitrary' called<br />
<pre>
Array
(
  [0] => a
  [1] => b
  [2] => 1
)
</pre>

The output from this code fragment illustrates that __call() intercepted our method call and accessed both the method name and arguments. As Harry Fuecks points out in his article at http://www.phppatterns.com/index.php/article/article-view/28/1/2, one of the best uses of __call() is in the construction of wrapper objects, which provide object-based interfaces to built-in or third-party standalone functions.

Let's look at an example. In Listing 17.3, we simulate a procedural third-party library provided by an online store called Bloggs. We cannot change these functions, which add items from our shop to the Bloggs store, but we would like to be able to tell an Item object to add itself to or remove itself from the Bloggs store using our object interface. We could create methods that mirror each of the functions. If however there are a lot of functions to mirror, and the functions expect similar arguments, then we can create virtual methods to map to functions dynamically.

Listing 17.3 Intercepting Method Calls with the __call() Method (PHP 5 Only)
 1: <?php
 2:
 3: // third party function
 4: function bloggsRegister( $item_array, $immediately=false ) {
 5:   return "Registering item with Bloggs stores<br />\n";
 6: }
 7:
 8: // third party function
 9: function bloggsRemove( $item_array, $immediately=false ) {
10:   return "Removing item from Bloggs stores<br />\n";
11: }
12:
13:
14: class Item {
15:   public $name = "item";
16:   public $price = 0;
17:
18:   function __call( $method, $args ) {
19:     $bloggsfuncs = array ( "bloggsRegister", "bloggsRemove" );
20:     if ( in_array( $method, $bloggsfuncs ) ) {
21:       array_unshift( $args, get_object_vars( $this ) );
22:       return call_user_func( $method, $args );
23:     }
24:   }
25: }
26:
27: $item = new Item();
28: print $item->bloggsRegister( true );
29: print $item->bloggsRemove( true );
30: ?>

We set up two fake third-party methods, bloggsRegister() and bloggsRemove() on lines 4 and 8. They do nothing but report that they have been called. We create an Item class on line 14, providing some sample properties. The heart of the class is the __call() method on line 18. We set up an array of acceptable methods on line 19 and store it in the local $bloggsfuncs variable. We don't want to work with methods that we know nothing about, so we test whether the method name used by the client code is stored in the $bloggsfuncs array. If the method name is found in the array, we call the standalone function of the same name, passing it an array consisting of the Item object's properties and the first user-defined argument (extracted from the $args array). We return the function's return value.

On lines 28 and 29, we test our dynamic methods. When we call the bloggsRegister() method, __call() is invoked because the Item class does not define bloggsRegister(). The string "bloggsRegister" is passed to __call() and stored in the $method argument. Because the string is found in $bloggsfuncs, the bloggsRegister() function is called.

With just two functions in our example, there is little gain, but if you imagine a Bloggs API with tens of utility functions, we could create an effective object wrapper quickly and easily.

You might want to do something similar with built-in functions. You might, for example, bundle a suite of file-related methods into a single MyFile class.

The __get() and __set() methods are similar in nature to __call(). __get() is called whenever client code attempts to access a property that is not explicitly defined. It is passed the name of the property accessed:


function __get( $prop ) {
         print "property $prop was accessed";
}

__set() is called when client code attempts to assign a value to a property that has not been explicitly defined. It is passed a string argument containing the name of the property and a mixed argument (an argument of any type) containing the value the client code attempted to set:


function __set( $prop, $val ) {
         print "client wishes to store $val in $prop";
}

In Listing 17.4, we use these methods to create a read-only property that always holds the current date array.

Listing 17.4 Intercepting Property Access with __get() and __set() (PHP 5 Only)
 1: <?php
 2: class TimeThing {
 3:   function __get( $arg ) {
 4:     if ( $arg == "time" ) {
 5:       return getdate();
 6:     }
 7:   }
 8:
 9:   function __set( $arg, $val ) {
10:     if ( $arg == "time" ) {
11:       trigger_error( "cannot set property $arg" );
12:       return false;
13:     }
14:   }
15: }
16:
17: $cal = new TimeThing();
18: print $cal->time['mday']."/";
19: print $cal->time['mon']."/";
20: print $cal->time['year'];
21:
22: // illegal call
23: $cal->time = 555;
24: ?>

We create a class called TimeThing on line 2. The __get() method on line 3 tests the property name provided by the client code. If the string is "time"—that is, if the client coder has attempted to access a $time property—then the method returns the date array as generated by getdate() on line 5. When we access the $time property on lines 18 to 20, we see that it is automatically populated with the date array.

We don't want the client coder to be able to override the $time property, so we implement a __set() method on line 9. If we detect an attempt to write to $time, we trigger an error message. We block all attempts to set nonexistent properties by providing no further implementation. To allow dynamic setting of properties we would have included the following line:


$this->$arg=$val;


    [ Team LiB ] Previous Section Next Section