On Github Ocramius / proxy-pattern-in-php
We're awesome!
(And we love <marquee/>)
ProxyManager, BjyAuthorize, AssetManager, ZeffMu, ZfrRest, OcraDiCompiler, OcraServiceManager, OcraCachedViewResolver, DoctrineModule, DoctrineORMModule, DoctrineMongoODMModule, VersionEyeModule
A proxy is a class functioning as an interface to something else
(carefully pasted from wikipedia)
A proxy replaces subject thanks to the LSP
SubjectProxy extending RealSubject
class SubjectProxy extends RealSubject { /* ... */ }
Though you should use interfaces!
Some definitions here:
And here:
And a lot of confusion over the internets :(
class Tweet { protected $data; public function __construct(array $data) { $this->data = $data; } public function getText() { return $this->data['text']; } }
class TweetProxy extends Tweet { protected $api; protected $id; public function __construct(TwitterApi $api, $id) { $this->api = $api; $this->id = $id; } public function getText() { return $this->api->getTweetText($this->id); } }
$tweet = new Tweet(array('text' => 'Proxies in PHP!')); var_dump($tweet->getText()); // Proxies in PHP! $api = new TwitterApi(/* yadda */); $remoteTweet = new TweetProxy($api, 280643708968386560); var_dump($remoteTweet->getText()); // Tweet text! $remoteTweet = new TweetProxy($api, 280643708968386561); var_dump($remoteTweet->getText()); // Another text!
Share objects across multiple systems
No local memory usage
Can act as an adapter for a completely different remote object
Complex setup
Fails on adapter failure
As slow as the protocol
For very expensive objects:
(A pattern, not a proxy type)
class Customer { public function __construct(DbConnection $db, $id) { $this->db = $db; $this->id = $id; } public function getName() { $this->init(); return $this->data['name']; } private function init() { if (!isset($this->data)) { $this->data = $this->db->load($this->id); } } }
Instantiation logic mixed with class logic
Dependency to loader objects in the object
Not really optimized, and unreadable if optimized
Harder to test
Virtual Proxy An object that looks like the RealSubject, but just holds a lazy reference to it.
Ghost Object An object that looks like the RealSubject, but with all properties not being set and being lazy loaded.
class Image { protected $image; public function __construct($path) { $this->image = imagecreatefromjpeg($path); } public function getSize() { return array( imagesx($this->image), imagesy($this->image), ); } }
class ImageVirtualProxy extends Image { protected $wrapped; public function __construct($path) { $this->path = $path; } public function getSize() { $this->init(); return $this->wrapped->getSize(); } private function init() { if ( ! $this->wrapped) { $this->wrapped = new Image($this->path); } } }
$img1 = new ImageProxy('/path/to/image1.jpg'); var_dump(memory_get_usage()); // ~200Kb $img2 = new ImageProxy('/path/to/image2.jpg'); var_dump(memory_get_usage()); // ~200Kb $img3 = new ImageProxy('/path/to/image3.jpg'); var_dump(memory_get_usage()); // ~200Kb $size1 = $img1->getSize(); var_dump(memory_get_usage()); // ~4Mb $size2 = $img2->getSize(); var_dump(memory_get_usage()); // ~8Mb
Abstracts initialization logic away
Improved memory/performance impact
Low overhead
Easy to implement
Not optimal for data that is always loaded
Lazy loading means lazy failing
Actually, not the same object as RealSubject (different identity)
An object whose properties are the same of the proxied object, but null.
Accessing any method causes loading of the properties.
Useful when the identity of the object has to be preserved
Doctrine Proxies are generated this way.
class ImageGhostProxy extends Image { protected $initialized = false; public function __construct($path) { $this->path = $path; } public function getSize() { $this->init(); return parent::getSize(); } private function init() { if ( ! $this->initialized) { $image = new Image($this->path); $this->image = $image->image; $this->initialized = true; } } }
Same identity as RealSubject
Abstracts initialization logic away
Improved memory/performance impact
Low overhead
Harder to implement
May require reflection to access parent class's properties
Not optimal for data that is always loaded
Lazy loading means lazy failing
A Protection Proxy comes into play when you want to transparently limit access to an API through a set of rules (ACL/limits/custom logic).
It actually is a decorator to the proxied RealSubject.
class APIProtectionProxy extends API { protected $count = 0; public function __construct(API $api, $limit) { $this->api = $api; $this->limit = $limit; } public function doStuff() { $this->count(); return $this->api->doStuff(); } private function count() { if (++$this->count > $this->limit) { throw new RemoteApiLimit('STAHP!'); } } }
$api = new APIProtectionProxy(new API(/* ... */), 50); while (true) { $api->doStuff(); // RemoteApiLimit exception! }
Limiting access to an API does not require changes in the API
Modifies proxied object behavior
A Smart reference executes additional operations when the RealSubject is accessed
Smart reference is actually DecoratorPattern
Good for AOP
Let's cache a complex/slow method call
/** @AOP\Cache(ttl=3600) */ public function doHeavyStuff() { // [...] }
Could become:
class APICachingProxy extends API { public function __construct(API $api, Cache $cache) { $this->api = $api; $this->cache = $cache; } public function doHeavyStuff() { if ($cached = $this->cache->get('doHeavyStuff')) { return $cached; } $result = $this->api->doHeavyStuff(); $this->cache->set('doHeavyStuff', $result, 3600); return $result; } }
class MyProxy { // ... public function __call($method, $args) { return call_user_func_array( $this->someWrappedStuff, $args ); } }
class BankAccount { /* ... */ }
function pay(BankAccount $account) { /* ... */ }
class PoorProxy { public function __construct($wrapped) { $this->wrapped = $wrapped; } public function __call($method, $args) { return call_user_func_array( $this->wrapped, $args ); } }
$account = new PoorProxy(new BankAccount()); pay($account); // KABOOM!
<?php namespace Proxy\__PM__; class A extends \A { private $valueHolder; private $initializer; public function getFoo() { $this->initializer && $this->initializer->__invoke($this->valueHolder, $this, 'getFoo', array(), $this->initializer); return $this->valueHolder->getFoo(); } public function __construct($initializer) { unset($this->foo); $this->initializer = $initializer; } public function __get($name) { $this->initializer && $this->initializer->__invoke($this->valueHolder, $this, '__get', array('name' => $name), $this->initializer); return $this->valueHolder->$name; } public function __set($name, $value) { $this->initializer && $this->initializer->__invoke($this->valueHolder, $this, '__set', array('name' => $name, 'value' => $value), $this->initializer); $this->valueHolder->$name = $value; } public function __isset($name) { $this->initializer && $this->initializer->__invoke($this->valueHolder, $this, '__isset', array('name' => $name), $this->initializer); return isset($this->valueHolder->$name); } public function __unset($name) { $this->initializer && $this->initializer->__invoke($this->valueHolder, $this, '__unset', array('name' => $name), $this->initializer); unset($this->valueHolder->$name); } public function __clone() { $this->initializer && $this->initializer->__invoke($this->valueHolder, $this, '__clone', array(), $this->initializer); $this->valueHolder = clone $this->valueHolder; } public function __sleep() { $this->initializer && $this->initializer->__invoke($this->valueHolder, $this, '__sleep', array(), $this->initializer); return array('valueHolder'); } public function __wakeup() { unset($this->foo); } public function setProxyInitializer(\Closure $initializer = null) { $this->initializer = $initializer; } public function getProxyInitializer() { return $this->initializer; } public function initializeProxy() { return $this->initializer && $this->initializer->__invoke($this->valueHolder, $this, 'initializeProxy', array(), $this->initializer); } public function isProxyInitialized() { return null !== $this->valueHolder; } public function getWrappedValueHolderValue() { return $this->valueHolder; } }
Avoid bringing out the generated garbage yourself!
(Currently) deals with Virtual Proxies, Smart References, Null Objects, Ghost Objects and Remote Objects
namespace My\Slow; class Foo { public function __construct() { sleep(10); } public function doFoo() { echo 'foo!'; } }
require_once 'vendor/autoload.php'; use ProxyManager\Factory\LazyLoadingValueHolderFactory; $factory = new LazyLoadingValueHolderFactory(); $proxy = $factory->createProxy( 'My\Slow\Foo', function (& $wrappedObject, $proxy) { $wrappedObject = new My\Slow\Foo(); $proxy->setProxyInitializer(null); } ); $proxy->doFoo();
require_once 'vendor/autoload.php'; use ProxyManager\Factory\AccessInterceptorValueHolderFactory; $factory = new AccessInterceptorValueHolderFactory(); $db = $factory->createProxy( new \My\Db\Connection(), ['query' => function () { echo "Query being executed!\n"; }], ['query' => function () { echo "Query completed!\n"; }] ); $db->query();
Yes, I'm no good at naming classes - really.
return [ 'service_manager' => [ 'delegators' => [ 'MyServiceName' => ['LazyServiceFactory'] ], ], 'lazy_services' => [ 'map' => ['MyServiceName' => 'My\Service\ClassName'] ] ]
<services> <service id="MyServiceName" class="My\Service\ClassName" lazy="true" /> </services>
Uses proxies for AOP purposes
Very interesting usage of autoloaders!
And before we get to the dirty stuff...
Breaks Virtual Proxies, Protection Proxies, and Smart References
More important:
class BankAccount { protected $amount = 0; public function pay($amount) { $this->amount -= $amount; return $this; } }
$account = new BankAccount(); $account->pay(100)->pay(200)->pay(300);
Dirty hacks!
class BankAccountProxy extends BankAccount { public function __construct(BankAccount $wrapped) { $this->amount = & $wrapped->amount; } public function pay($amount) { echo 'Paid ' . $amount . "!\n"; return parent::pay($amount); } }
$account = new BankAccountProxy(new BankAccount()); $account->pay(100)->pay(200)->pay(300);
Paid 100 Paid 200 Paid 300
(without property accessors!)
class Customer { public $name; public $surname; }
Dirty hacks!
class Customer { public $name; public $surname; }
class CustomerProxy extends Customer { public function __construct(Customer $customer) { unset($this->name, $this->surname); $this->customer = $customer; }
public function __set($name, $value) { $this->customer->$name = $value; } public function __get($name) { return $this->customer->$name; } // __isset, __unset }