2.6 Magical Methods

PHP 5 has a few methods that are implicitly invoked behind the scenes. You've already seen two, _ _construct( ) and _ _destruct( ), but these are not the only special methods in PHP 5.

There are seven special methods, and they are as follows:


_ _construct( )

Called when instantiating an object


_ _destruct( )

Called when deleting an object


_ _get( )

Called when reading from a nonexistent property


_ _set( )

Called when writing to a nonexistent property


_ _call( )

Called when invoking a nonexistent method


_ _toString( )

Called when printing an object


_ _clone( )

Called when cloning an object

These methods are easy to spot, since they all begin with two underscores (_ _).

2.6.1 _ _get( ) and _ _set( )

You've read a lot about the benefits of encapsulation and why your classes must have accessor methods. Writing these methods, however, induces numbness as you repeatedly create methods for each property in your class. PHP 5 has two special methods, _ _set( ) and _ _get( ), to ease your pain.

In PHP 4, the only way to handle accessors is to write your own set of methods for each property. By convention, these methods often begin with the word set and end with the property name. So, to set a Person's name, you call setName( ).

Using PHP 4 and accessors, the code looks like this:

class Person {

    var $name;

    var $email;



    function setName($name) {

        $this->name = $name;

    }



    function getName( ) {

        return $this->name;

    }

}



$rasmus = new Person;

$rasmus->setName('Rasmus Lerdorf');

print $ramsus->getName( )

Rasmus Lerdorf

This isn't a problem when there's only a single property, but when Person adds email, age, address, and other details, the class quickly spirals into a long list of almost identical accessors.

PHP 4 requires you to manually write specific accessor functions for every property.[1] That quickly becomes tiresome as the number of object properties increases, and it makes changing from one backend to another complicated because you need to edit every single method.

[1] Unless you use the experimental overload extension. If you do, you should know that you can no longer pass the accessor functions a variable by reference as a final parameter to get( ) or set( ).

Using methods such as getName( ) and setName( ) provides a great degree of protection against broken code, but forcing people to call getName( ) instead of reading name is clunky. In PHP 4, you don't have a choice, because that's the only way to implement accessors.

However, since accessors are so important, PHP 5 has two specific methods that implement them: _ _get( ) and _ _set( ). The _ _get( ) method automatically intercepts access to any undefined property reads. Likewise, _ _set( ) does the same for property writes.

Example 2-3 reimplements the class in PHP 5 using _ _get( ) and _ _set( ).

Example 2-3. Implementing magic accessor methods
class Person {

    private $data;



    public function _ _get($property) {

        if (isset($this->data[$property])) {

            return $this->data[$property];

        } else {

            return false;

        }

    }



    public function _ _set($property, $value) {

        $this->data[$property] = $value;

    }

}



$rasmus = new Person;

$rasmus->name = 'Rasmus Lerdorf';

print $rasmus->name;

Rasmus Lerdorf

With _ _get( ) and _ _set( ), you can use what appear to be public properties, such as $rasmus->name, without violating encapsulation. This is because the programmer isn't reading from and writing to those properties directly, but is instead being routed through accessor methods.

The _ _get( ) method takes the property name as its single parameter. Within the method, you check to see whether that property has a value inside $data. If it does, the method returns that value; otherwise, it returns false.

This lets you continue accessing properties using the familiar property syntax, even though the actual data is someplace else.

When you read $rasmus->name, you actually call _ _get('name') and it's returning $data['name'], but for all external purposes that's irrelevant.


The _ _set( ) method takes two arguments: the property name and the new value. Otherwise, the logic inside the method is similar to _ _get( ).

There are two downsides to using _ _get( ) and _ _set( ). First, these methods only catch missing properties. If you define a property for your class, _ _get( ) and _ _set( ) are not invoked by PHP.

This is the case even if the property you're trying to access isn't visible in the current scope (for instance, when you're reading a property that exists in the class but isn't accessible to you, because it's declared private). Doing this causes PHP to emit a fatal error:

PHP Fatal error:  Cannot access private property...

Second, these methods completely destroy any notion of property inheritance. If a parent object has a _ _get( ) method and you implement your own version of _ _get( ) in the child, your object won't function correctly, because the parent's _ _get( ) method is never called.

2.6.2 _ _call( )

PHP 5 also has a _ _call( ) method. It captures any calls to undefined methods in a class. The _ _call( ) method is useful for implementing an OO concept known as aggregation.

With aggregation, one object acts as a container for one or more additional objects. This is another way of solving the problem of multiple inheritance because you can easily piece together an object out of smaller components.

For example, a Person object can contain an Address object. Clearly, People have addresses. However, addresses aren't unique to people; they also belong to businesses and other entities. Therefore, instead of hardcoding address information inside of Person, it makes sense to create a separate Address class that can be used by multiple classes.

Example 2-4 shows how this works in practice.

Example 2-4. Aggregating an Address object
class Address {

    protected $city;

    protected $country;



    public function setCity($city) {

        $this->city = $city;

    }

        

    public function getCity( ) {

        return $this->city;

    }



    public function setCountry($country) {

        $this->country = $country;

    }

        

    public function getCountry( ) {

        return $this->country;

    }

}



class Person {

    protected $name;

    protected $address;



    public function _ _construct( ) {

        $this->address = new Address;

    }



    public function setName($name) {

        $this->name = $name;

    }



    public function getName( ) {

        return $this->name;

    }



    public function _ _call($method, $arguments) {

        if (method_exists($this->address, $method)) {

            return call_user_func_array(

                array($this->address, $method), $arguments);

        }

    }

}

The Address class is set up just like the Person class you have seen before. However, it stores a city and country instead of a name.

Person has setName( ) and getName( ), like before, but it also has two new methods: _ _construct( ) and _ _call( ).

Its constructor instantiates an Address object and stores it in a protected $address property. This allows methods inside Person to access $address, but prevents others from talking directly to the class.

Ideally, when you call a method that exists in Address, PHP would automatically execute it. This does not occur, since Person does not extend Address. You must write code to glue these calls to the appropriate methods yourself.

Wrapper methods are one option. For example:

    public function setCity($city) {

        $this->address->setCity($city);

    }

This setCity( ) method passes along its data to the setCity( ) method stored in $address. This is simple, but it is also tedious because you must write a wrapper for every method.

Using _ _call( ) lets you automate this process by centralizing these methods into a single place:

    public function _ _call($method, $arguments) {

        if (method_exists($this->address, $method)) {

            return call_user_func_array(

                array($this->address, $method), $arguments);

        }

    }

The _ _call( ) method is invoked with two arguments: the name of the method and an array holding the parameters passed to the method. The first argument lets you see which method was called, so you can determine whether it's appropriate to dispatch it to $address.

Here, you want to pass along the method if it's a valid method of the Address class. Check this using method_exists( ), providing the object as the first parameter and the method name as the second.

If the function returns true, you know this method is valid, so you can call it. Unfortunately, you're still left with the burden of unwrapping the arguments out of the $arguments array. That can be painful.

The seldom used and oddly named call_user_func_array( ) function solves this problem. This function lets you call a user function and pass along arguments in an array. Its first parameter is your function name, and the second is the array of arguments.

In this case, however, you want to call an object method instead of a function. There's a special syntax to cover this situation. Instead of passing the function name, you pass an array with two elements. The first element is the object, and the other is the method name.

This causes call_user_func_array( ) to invoke the method on your object. You must then return the result of call_user_func_array( ) back to the original caller, or your return values will be silently discarded.

Here's an example of Person that calls both a method defined in Person and one from Address:

$rasmus = new Person;

$rasmus->setName('Rasmus Lerdorf');

$rasmus->setCity('Sunnyvale');



print $rasmus->getName( ) . ' lives in ' . $rasmus->getCity( ) . '.';

Rasmus Lerdorf lives in Sunnyvale.

Even though setCity( ) and getCity( ) aren't methods of Person, you have aggregated them into that class.

You can aggregate additional objects into a single class, and also be more selective as to which methods you expose to the outside user. This requires some basic filtering based on the method name.

When deciding if aggregation is the right technique, ask yourself if the "has a" rule applies. Here a Person has an Address, so it makes sense to aggregate. However, keep in mind that aggregate relationships are weaker than inheritance relationships because they're not as intricately bound to the object's identity.

2.6.3 _ _toString( )

PHP 5 provides objects with a way to control how they are converted to strings. This allows you to print an object in a friendly way without resorting to lots of additional code.

PHP 4 requires you to call a method that returns a string. For example:

class Person {

    var $name;

    var $email;

    

    function setName($name) {

        $this->name = $name;

    }



    function setEmail($email) {

        $this->email = $email;

    }



    function toString( ) {

        return "$this->name <$this->email>";

    }

}



$rasmus = new Person;

$rasmus->setName('Rasmus Lerdorf');

$rasmus->setEmail('rasmus@php.net');

print $rasmus->toString( );





Rasmus Lerdorf <rasmus@php.net>

This is effective, but there's no requirement that the string conversion method is always toString( ), so you can't rely on it from class to class.

Also, it adds visual clutter to your code because you're constantly writing ->toString( ) whenever you want to print an object. PHP should know that when you try to print an object, you want a special string representation of the contents of the object.

PHP 5 introduces a special method that lets you to control how PHP displays objects. PHP calls an object's _ _toString( ) method when you echo or print the object by itself.

The PHP 5 Person class is identical to the PHP 4 version, except that toString( ) has been renamed _ _toString( ):

class Person {

    protected $name;

    protected $email;

    

    public function setName($name) {

        $this->name = $name;

    }



    public function setEmail($email) {

        $this->email = $email;

    }



    public function _ _toString( ) {

        return "$this->name <$this->email>";

    }

}

Now, instead of writing:

$rasmus = new Person;

$rasmus->setName('Rasmus Lerdorf');

$rasmus->setEmail('rasmus@php.net');

print $rasmus->toString( );

you can write:

$rasmus = new Person;

$rasmus->setName('Rasmus Lerdorf');

$rasmus->setEmail('rasmus@php.net');

print $rasmus;

Rasmus Lerdorf <rasmus@php.net>

This feature does not work for interpolated or concatenated strings:

print  "PHP was created by $rasmus";

print  'PHP was created by  '. $rasmus;

printf('PHP was created by %s', $rasmus);

PHP was created by Object id #1

PHP was created by Object id #1

PHP was created by Object id #1

The one exception is a dusty corner of PHP that uses echo and a comma (,) instead of period (.) to combine items:

echo   'PHP was created by ', $rasmus;

PHP was created by Rasmus Lerdorf <rasmus@php.net>

PHP 5 will not autoconvert objects to strings when you pass them to a function that requires a string argument. You should call _ _toString( ) on them instead:

print htmlentities($rasmus);                   // bad

print htmlentities($rasmus->_ _toString( ));     // good

While such limited support for _ _toString( ) reduces its usefulness, future versions of PHP should extend this behavior to also call _ _toString( ) when you:

  • Place the object inside double quotes or a heredoc

  • Concatenate with the object using dot (.)

  • Cast the object to a string using (string) or strval( )

  • Treat the object as a string in printf( ) by indicating it should be formatted with %s

Therefore, it's best to start using the _ _toString( ) naming convention when you transition to PHP 5 because then there will be less code to modify when you want to take advantage of the newer features.

2.6.4 _ _clone( )

Since PHP 5 copies objects by reference instead of value, you need to use clone to duplicate the contents of an object. Otherwise, the second object is simply a reference to the first.

This cloning process copies every property in the first object to the second. This includes properties holding objects, so the cloned object may end up sharing object references with the original.

This is frequently not the desired behavior. For example, consider the aggregated version of Person that holds an Address object, shown earlier in Example 2-4. The key point to remember is that the $address property holds an Address object.

With this class, here's what happens when you clone an object:

$rasmus = new Person;

$rasmus->setName('Rasmus Lerdorf');

$rasmus->setCity('Sunnyvale');



$zeev = clone $rasmus;

$zeev->setName('Zeev Suraski');

$zeev->setCity('Tel Aviv');



print $rasmus->getName( ) . ' lives in ' . $rasmus->getCity( ) . '.';

print $zeev->getName( ) . ' lives in ' . $zeev->getCity( ) . '.';

Rasmus Lerdorf lives in Tel Aviv.

Zeev Suraski lives in Tel Aviv.

Interesting. Calling setName( ) worked correctly because the $name property is a string, so it's copied by value. However, since $address is an object, it's copied by reference, so getCity( ) doesn't produce the correct results, and you end up relocating Rasmus to Tel Aviv.

This type of object cloning is known as a shallow clone or a shallow copy because PHP does not clone objects held in properties. In contrast, a deep clone occurs when all objects involved are cloned. This is PHP 4's cloning method.

Control how PHP 5 clones an object by implementing a _ _clone( ) method in your class. When this method exists, PHP allows _ _clone( ) to override its default behavior:

class Person {



    // ... everything from before



    public function _ _clone( ) {

        $this->address = clone $this->address;

    }

}

Inside of _ _clone( ), you're automatically presented with a shallow copy of the variable, stored in $this, the object that PHP provides when _ _clone( ) does not exist.

Since PHP has already copied all the properties, you only need to overwrite the ones you dislike. Here, $name is okay, but $address needs to be explicitly cloned.

Now the clone behaves correctly:

$rasmus = new Person;

$rasmus->setName('Rasmus Lerdorf');

$rasmus->setCity('Sunnyvale');



$zeev = clone $rasmus;

$zeev->setName('Zeev Suraski');

$zeev->setCity('Tel Aviv');



print $rasmus->getName( ) . ' lives in ' . $rasmus->getCity( ) . '.';

print $zeev->getName( ) . ' lives in ' . $zeev->getCity( ) . '.';

Rasmus Lerdorf lives in Sunnyvale.

Zeev Suraski lives in Tel Aviv.

Using the clone operator on objects stored in properties causes PHP to check whether any of those objects contain a _ _clone( ) method. If one exists, PHP calls it. This repeats for any objects that are nested even further.

This process correctly clones the entire object and demonstrates why it's called a deep copy.