PHP classes and Objects
Aggregating Objects
Problem
You want to compose two or more objects together so that they appear to behave as a single object.Solution
Aggregate the objects together and use the __call() and __callStatic() magic methods to intercept method invocations and route them accordingly:class Address {
protected $city;
public function setCity($city) {
$this->city = $city;
}
public function getCity() {
return $this->city;
}
}
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);
}
}
}
$rasmus = new Person;
$rasmus->setName('Rasmus Lerdorf');
$rasmus->setCity('Sunnyvale');
print $rasmus->getName() . ' lives in ' . $rasmus->getCity() . '.';
An instance of the Address object is created during the construction of every Person. When you invoke methods not defined in Person, the __call() method catches them and, when applicable, dispatches them using call_user_func_array().
Use __callStatic() when you need to route static methods.
Discussion
In this recipe, you cannot say a Person “is an” Address or vice versa. Therefore, it doesn’t make sense for one class to extend the other.However, it makes sense for them to be separate classes so that they provide maximum flexibility and reuse, as well as reduced duplicated code. So you check if another rule— the “has a” rule—applies. Because a Person “has an” Address, it makes sense to aggregate the classes together.
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.
Here’s how this works in practice:
class Address {
protected $city;
public function setCity($city) {
$this->city = $city;
}
public function getCity() {
return $this->city;
}
}
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 stores a city and has two accessor methods to manipulate the data, setCity() and getCity().Person has setName() and getName(), similar to Address, but it also has two other 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, because 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:
class Person {
public function setCity($city) {
$this->address->setCity($city);
}
}
This setCity() method passes along its data to the setCity() method stored in $ad dress. 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:
class Person {
public function __call($method, $arguments) {
if (method_exists($this->address, $method)) {
return call_user_func_array(
array($this->address, $method), $arguments);
}
}
}
The __call() method captures any calls to undefined methods in a class. It 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() . '.';
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.
No comments:
Post a Comment