Last active
May 29, 2017 01:02
-
-
Save itsjavi/782f3fa392dea55ef4eed54ba4bc0f22 to your computer and use it in GitHub Desktop.
PHP Service Proxy + Lazy Proxy patterns
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace Foo; | |
use BadMethodCallException; | |
use Closure; | |
use DomainException; | |
/* | |
* This example encapsulates before/after method call callbacks inside a the service itself, using a proxy, | |
* so the object can be easily transportable. | |
* This is useful for example in dependency injection. | |
*/ | |
interface Service | |
{ | |
// some methods | |
} | |
class ServiceProxy implements Service | |
{ | |
/** | |
* The original service | |
* | |
* @var Service | |
*/ | |
protected $service; | |
/** | |
* @var Closure | |
*/ | |
protected $before; | |
/** | |
* @var Closure | |
*/ | |
protected $after; | |
public function __construct(Service $service, Closure $before = null, Closure $after = null) | |
{ | |
$this->service = $service; | |
$this->before = $before; | |
$this->after = $after; | |
} | |
public function __call($method, $arguments) | |
{ | |
$service = $this->getOriginalService(); | |
if ( ! method_exists($service, $method)) { | |
throw new BadMethodCallException(get_class($service) . '::' . $method . ' does not exist.'); | |
} | |
if ($this->before) { | |
$this->before->bindTo($service); | |
$arguments = $this->before->__invoke($service, $method, $arguments); | |
if ( ! array($arguments)) { | |
throw new DomainException('The before hook should return back the arguments array.'); | |
} | |
} | |
$result = $service->$method(...$arguments); | |
if ($this->after) { | |
$this->after->bindTo($service); | |
$result = $this->after->__invoke($service, $method, $arguments, $result); | |
} | |
return $result; | |
} | |
// TODO: implement __get, __set, __isset and __unset for proxying them to the original service. | |
/** | |
* @return Service | |
*/ | |
public function getOriginalService() | |
{ | |
return $this->service; | |
} | |
} | |
// Class useful for lazy dependency injection | |
class LazyServiceProxy extends ServiceProxy | |
{ | |
/** | |
* @var callable | |
*/ | |
protected $factory; | |
/** | |
* | |
* @param callable $originalServiceFactory | |
*/ | |
public function __construct(callable $factory, Closure $before = null, Closure $after = null) | |
{ | |
$this->factory = $factory; | |
$this->before = $before; | |
$this->after = $after; | |
} | |
public function getOriginalService() | |
{ | |
if (!$this->service) { | |
$this->service = call_user_func_array($this->factory, func_get_args()); | |
} | |
return $this->service; | |
} | |
} | |
abstract class AbstractService implements Service | |
{ | |
/** | |
* @return ServiceProxy|LazyServiceProxy|static | |
*/ | |
public static function create(array $arguments = [], Closure $before = null, Closure $after = null, $lazy = false) | |
{ | |
$serviceClass = static::class; | |
$factory = function() use($serviceClass, $arguments) { | |
return new $serviceClass(...$arguments); | |
}; | |
if(is_null($before) && is_null($after) && !$lazy) { | |
// No need to proxy | |
return $factory(); | |
} | |
return $lazy ? new LazyServiceProxy($factory, $before, $after) : new ServiceProxy($factory(), $before, $after); | |
} | |
/** | |
* Wraps this service into a service proxy | |
* @return ServiceProxy|static | |
*/ | |
public function getProxy(Closure $before = null, Closure $after = null) | |
{ | |
/** @var ServiceProxy|static $serviceProxy */ | |
return new ServiceProxy($this, $before, $after); | |
} | |
} | |
class FooService extends AbstractService | |
{ | |
public function bar($num1, $num2) | |
{ | |
return $num1 * $num2 - $num1; | |
} | |
} | |
$beforeCall = function ($originalService, $method, $arguments) { | |
echo " : "; | |
return $arguments; | |
}; | |
$afterCall = function ($originalService, $method, $arguments, $result) { | |
echo strval($result) . " "; | |
return $result; | |
}; | |
$proxy = FooService::create([], $beforeCall, $afterCall, true); | |
/** @var FooService|ServiceProxy $proxy */ | |
echo "PROXIED: " . PHP_EOL; | |
for($i = 1; $i < 10; $i++){ | |
FooService::create([], $beforeCall, $afterCall, false)->bar($i, $i + 1); | |
} | |
echo PHP_EOL; | |
echo "NOT PROXIED: " . PHP_EOL; | |
// this won't show any output, because it will use the original services: | |
FooService::create([], $beforeCall, $afterCall)->getOriginalService()->bar(5, 6); | |
FooService::create()->bar(7, 8); | |
echo "END." . PHP_EOL; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Drawbacks of this pattern: when proxying,
instanceof
and type hinting won't work as expected,e.g.
(FooService::create([],null,null,true) instanceof FooService) === false