I while back I had a look at using the Decorator Pattern to simplify some code:
- Using a decorator pattern to reduce inappropriate code complexity
- Decorator Pattern vs simple inheritance
One again me mate Brian has come to the fore with a tweak to make this tactic an even more appealing prospect: leveraging PHP's "magic"
__call
method to simplify decorator code.Let's have a look at one of the examples I used earlier:
class User {
public $id;
public $firstName;
public $lastName;
function __construct($id, $firstName, $lastName){
$this->id = $id;
$this->firstName = $firstName;
$this->lastName = $lastName;
}
}
class DataSource {
function getById($id){
return json_encode([
'id' => $id,
'firstName' => 'Zachary',
'lastName' => 'Cameron Lynch',
]);
}
}
class LoggerService {
function logText($text){
echo "LOGGED: $text" . PHP_EOL;
}
}
class UserRepository {
private $dataSource;
public function __construct($dataSource) {
$this->dataSource = $dataSource;
}
public function getById($id) {
$userAsJson = $this->dataSource->getById($id);
$rawUser = json_decode($userAsJson);
$user = new User($rawUser->id, $rawUser->firstName, $rawUser->lastName);
return $user;
}
}
class LoggedUserRepository {
private $repository;
private $loggerService;
public function __construct($repository, $loggerService) {
$this->repository = $repository;
$this->loggerService = $loggerService;
}
public function getById($id) {
$this->loggerService->logText("$id requested");
$object = $this->repository->getById($id);
return $object;
}
}
$dataSource = new DataSource();
$userRepository = new UserRepository($dataSource);
$loggerService = new LoggerService();
$loggedUserRepository = new LoggedUserRepository($userRepository, $loggerService);
$user = $loggedUserRepository->getById(5);
var_dump($user);
That looks like a chunk of code, but the User, DataSource and LoggerService are just dependencies. The code you really wannna look at are the two repo variations: the basic UserRepository, and the decorated LoggedUserRepository.
This code all works fine, and outputs:
C:\src>php baseline.php
LOGGED: 5 requested
object(User)#6 (3) {
["id"]=>
int(5)
["firstName"]=>
string(7) "Zachary"
["lastName"]=>
string(13) "Cameron Lynch"
}
C:\src>
LOGGED: 5 requested
object(User)#6 (3) {
["id"]=>
int(5)
["firstName"]=>
string(7) "Zachary"
["lastName"]=>
string(13) "Cameron Lynch"
}
C:\src>
Fine.
But this is a very simple example: the repo has only one one method. So the decorator only needs to implement one method. But what if the repo has ten public methods? Suddenly the decorator is getting rather busy, as it needs to implement wrappers for those methods too. Yikes. This is made even worse if the decorator is only really interested in decorating one method... it still needs those other nine wrappers. And it'd be getting very boiler-plate-ish to have to implement all these "empty" wrapper methods.
Let's add a coupla more methods to our repo:
class UserRepository {
private $dataSource;
public function __construct($dataSource) {
$this->dataSource = $dataSource;
}
public function getById($id) {
$userAsJson = $this->dataSource->getById($id);
$rawUser = json_decode($userAsJson);
$user = new User($rawUser->id, $rawUser->firstName, $rawUser->lastName);
return $user;
}
public function getByFilters($filters) {
$usersAsJson = $this->dataSource->getByFilters($filters);
$rawUsers = json_decode($usersAsJson);
$users = array_map(function($rawUser){
return new User($rawUser->id, $rawUser->firstName, $rawUser->lastName);
}, $rawUsers);
return $users;
}
public function create($firstName, $lastName){
$rawNewUser = json_decode($this->dataSource->create($firstName, $lastName));
return new User($rawNewUser->id, $rawNewUser->firstName, $rawNewUser->lastName);
}
}
We have two new methods:
getByFilters()
and create()
. Even if we don't want to log calls to those for some reason, we still need the LoggedUserRepository to implement "pass-through" methods for them:class LoggedUserRepository {
private $repository;
private $loggerService;
public function __construct($repository, $loggerService) {
$this->repository = $repository;
$this->loggerService = $loggerService;
}
public function getById($id) {
$this->loggerService->logText("$id requested");
$object = $this->repository->getById($id);
return $object;
}
public function getByFilters($filters) {
return $this->repository->getByFilters($filters);
}
public function create($firstName, $lastName) {
return $this->repository->create($firstName, $lastName);
}
}
See how we still need methods for
getByFilters
and create
? Suck. I mean it's not a huge amount of code, but given the LoggedUserRepository doesn't actually wanna log those methods, they're out of place.
Fortunately, we don't have to. We can just use
__call()
to cater for all the methods we don't want to decorate.class LoggedUserRepository {
private $repository;
private $loggerService;
public function __construct($repository, $loggerService) {
$this->repository = $repository;
$this->loggerService = $loggerService;
}
public function getById($id) {
$this->loggerService->logText("$id requested");
$object = $this->repository->getById($id);
return $object;
}
public function __call($method, $arguments) {
return call_user_func_array([$this->repository, $method], $arguments);
}
}
Now any method that the LoggedUserRepository needs to focus on will get called, but any methods UserRepository has but LoggedUserRepository isn't interested in will still get called. Nice.
Indeed the same can apply in reverse too. Say the decoration for all methods is the same? We can just implement the
__call()
method and that's it:class LoggedUserRepository {
private $repository;
private $loggerService;
public function __construct($repository, $loggerService) {
$this->repository = $repository;
$this->loggerService = $loggerService;
}
public function __call($method, $arguments) {
$serialisedArgs = json_encode($arguments);
$this->loggerService->logText("$method called with $serialisedArgs");
return call_user_func_array([$this->repository, $method], $arguments);
}
}
What's more, there's an opportunity here to push that
__call()
implementation out into a trait, in case it can be reused in other logging decoration situations:
trait LogAllMethods {
public function __call($method, $arguments) {
$serialisedArgs = json_encode($arguments);
$this->loggerService->logText("$method called with $serialisedArgs");
return call_user_func_array([$this->repository, $method], $arguments);
}
}
class LoggedUserRepository {
use LogAllMethods;
private $repository;
private $loggerService;
public function __construct($repository, $loggerService) {
$this->repository = $repository;
$this->loggerService = $loggerService;
}
}
TBH, this level of magic abstraction is beginning to have me think it might all be too magical. But on the other hand it does make life easier. We could - of course - take it further still:
trait LogAllMethods {
public function __call($method, $arguments=[]) {
$serialisedArgs = json_encode($arguments);
$this->loggerService->logText("$method called with $serialisedArgs");
return call_user_func_array([$this->wrappedObject, $method], $arguments);
}
}
class LoggingDecorator {
use LogAllMethods;
private $wrappedObject;
private $loggerService;
public function __construct($wrappedObject, $loggerService) {
$this->wrappedObject = $wrappedObject;
$this->loggerService = $loggerService;
}
}
This is now a generic logging decoration solution for any wrapped object. As a very contrived example, here's some calling code (using the same repos and other dependencies as listed before):
$loggerService = new LoggerService();
$dataSource = new DataSource();
$loggedDataSource = new LoggingDecorator($dataSource, $loggerService);
$userRepository = new UserRepository($loggedDataSource);
$loggedUserRepository = new LoggingDecorator($userRepository, $loggerService);
$user = $loggedUserRepository->getById(5);
var_dump($user);
$users = $loggedUserRepository->getByFilters(['firstName'=>'Zachary', 'lastName'=>'Cameron Lynch']);
var_dump($users);
$newUser = $loggedUserRepository->create('Zachary', 'Cameron Lynch');
var_dump($newUser);
As a proof of concept here, we're using the same one generic logging decorator to log all calls made by both the UserRepository and now the DataSource too. And it just works:
C:\src>c:\apps\php\7\php.exe logEverything.php
LOGGED: getById called with [5]
LOGGED: getById called with [5]
object(User)#7 (3) {
["id"]=>
int(5)
["firstName"]=>
string(7) "Zachary"
["lastName"]=>
string(13) "Cameron Lynch"
}
LOGGED: getByFilters called with [{"firstName":"Zachary","lastName":"Cameron Lynch"}]
LOGGED: getByFilters called with [{"firstName":"Zachary","lastName":"Cameron Lynch"}]
array(2) {
[0]=>
object(User)#10 (3) {
["id"]=>
int(1)
["firstName"]=>
string(9) "firstName"
["lastName"]=>
string(7) "Zachary"
}
[1]=>
object(User)#11 (3) {
["id"]=>
int(2)
["firstName"]=>
string(8) "lastName"
["lastName"]=>
string(13) "Cameron Lynch"
}
}
LOGGED: create called with ["Zachary","Cameron Lynch"]
LOGGED: create called with ["Zachary","Cameron Lynch"]
object(User)#6 (3) {
["id"]=>
int(1)
["firstName"]=>
string(7) "Zachary"
["lastName"]=>
string(13) "Cameron Lynch"
}
C:\src>
NB: the DataSource's implementation of
getByFilters()
is a nonsense one, it just returns each key/value passed in as the firstName/lastName of a contrived user. It's just a stub for the purposes of demonstration. But that explains the dodgy data in the output.Note how we're getting two log lines now: one is from the repo call, the other is from the datasource call it makes. Cool. Slightly daft and very contrived, but it's one of those "good to know" sort of things.
Righto... this Guinness does not seem to be drinking itself, so I better crack on with it.
--
Adam