[ Index ] |
PHP Cross Reference of phpBB-3.3.14-deutsch |
[Summary view] [Print] [Text view]
1 --- 2 title: Lazy Loading Ghost Object Proxies 3 --- 4 5 # Lazy Loading Ghost Object Proxies 6 7 A Lazy Loading Ghost is a type of proxy object. 8 9 More specifically, it is a fake object that looks exactly like an object 10 that you want to interact with, but is actually just an empty instance 11 that gets all properties populated as soon as they are needed. 12 13 Those properties do not really exist until the ghost object is actually 14 initialized. 15 16 ## Lazy loading with the Ghost Object 17 18 In pseudo-code, in userland, [lazy loading](http://www.martinfowler.com/eaaCatalog/lazyLoad.html) in a ghost object 19 looks like following: 20 21 ```php 22 class MyObjectProxy 23 { 24 private $initialized = false; 25 private $name; 26 private $surname; 27 28 public function doFoo() 29 { 30 $this->init(); 31 32 // Perform doFoo routine using loaded variables 33 } 34 35 private function init() 36 { 37 if (! $this->initialized) { 38 $data = some_logic_that_loads_data(); 39 40 $this->name = $data['name']; 41 $this->surname = $data['surname']; 42 43 $this->initialized = true; 44 } 45 } 46 } 47 ``` 48 49 Ghost objects work similarly to virtual proxies, but since they don't wrap around a "real" instance of the proxied 50 subject, they are better suited for representing dataset rows. 51 52 ## When do I use a ghost object? 53 54 You usually need a ghost object in cases where following applies: 55 56 * you are building a small data-mapper and want to lazily load data across associations in your object graph; 57 * you want to initialize objects representing rows in a large dataset; 58 * you want to compare instances of lazily initialized objects without the risk of comparing a proxy with a real subject; 59 * you are aware of the internal state of the object and are confident in working with its internals via reflection 60 or direct property access. 61 62 ## Usage examples 63 64 [ProxyManager](https://github.com/Ocramius/ProxyManager) provides a factory that creates lazy loading ghost objects. 65 To use it, follow these steps: 66 67 First, define your object's logic without taking care of lazy loading: 68 69 ```php 70 namespace MyApp; 71 72 class Customer 73 { 74 private $name; 75 private $surname; 76 77 // just write your business logic or generally logic 78 // don't worry about how complex this object will be! 79 // don't code lazy-loading oriented optimizations in here! 80 public function getName() { return $this->name; } 81 public function setName($name) { $this->name = (string) $name; } 82 public function getSurname() { return $this->surname; } 83 public function setSurname($surname) { $this->surname = (string) $surname; } 84 } 85 ``` 86 87 Then, use the proxy manager to create a ghost object of it. 88 You will be responsible for setting its state during lazy loading: 89 90 ```php 91 namespace MyApp; 92 93 use ProxyManager\Factory\LazyLoadingGhostFactory; 94 use ProxyManager\Proxy\GhostObjectInterface; 95 96 require_once __DIR__ . '/vendor/autoload.php'; 97 98 $factory = new LazyLoadingGhostFactory(); 99 $initializer = function ( 100 GhostObjectInterface $ghostObject, 101 string $method, 102 array $parameters, 103 & $initializer, 104 array $properties 105 ) { 106 $initializer = null; // disable initialization 107 108 // load data and modify the object here 109 $properties["\0MyApp\\Customer\0name"] = 'Agent'; 110 $properties["\0MyApp\\Customer\0surname"] = 'Smith'; 111 112 // you may also call methods on the object, but remember that 113 // the constructor was not called yet: 114 $ghostObject->setSurname('Smith'); 115 116 return true; // confirm that initialization occurred correctly 117 }; 118 119 $ghostObject = $factory->createProxy(\MyApp\Customer::class, $initializer); 120 ``` 121 122 You can now use your object as before: 123 124 ```php 125 // this will work as before 126 echo $ghostObject->getName() . ' ' . $ghostObject->getSurname(); // Agent Smith 127 ``` 128 129 ## Lazy Initialization 130 131 We use a closure to handle lazy initialization of the proxy instance at runtime. 132 The initializer closure signature for ghost objects is: 133 134 ```php 135 /** 136 * @var object $ghostObject The instance of the ghost object proxy that is being initialized. 137 * @var string $method The name of the method that triggered lazy initialization. 138 * @var array $parameters An ordered list of parameters passed to the method that 139 * triggered initialization, indexed by parameter name. 140 * @var Closure $initializer A reference to the property that is the initializer for the 141 * proxy. Set it to null to disable further initialization. 142 * @var array $properties By-ref array with the properties defined in the object, with their 143 * default values pre-assigned. Keys are in the same format that 144 * an (array) cast of an object would provide: 145 * - `"\0Ns\\ClassName\0propertyName"` for `private $propertyName` 146 * defined on `Ns\ClassName` 147 * - `"\0Ns\\ClassName\0propertyName"` for `protected $propertyName` 148 * defined in any level of the hierarchy 149 * - `"propertyName"` for `public $propertyName` 150 * defined in any level of the hierarchy 151 * 152 * @return bool true on success 153 */ 154 $initializer = function ( 155 \ProxyManager\Proxy\GhostObjectInterface $ghostObject, 156 string $method, 157 array $parameters, 158 & $initializer, 159 array $properties 160 ) {}; 161 ``` 162 163 The initializer closure should usually be coded like following: 164 165 ```php 166 $initializer = function ( 167 \ProxyManager\Proxy\GhostObjectInterface $ghostObject, 168 string $method, 169 array $parameters, 170 & $initializer, 171 array $properties 172 ) { 173 $initializer = null; // disable initializer for this proxy instance 174 175 // initialize properties (please read further on) 176 $properties["\0ClassName\0foo"] = 'foo'; 177 $properties["\0ClassName\0bar"] = 'bar'; 178 179 return true; // report success 180 }; 181 ``` 182 183 ### Lazy initialization `$properties` explained 184 185 The assignments to properties in this closure use unusual `"\0"` sequences. 186 This is to be consistent with how PHP represents private and protected properties when 187 casting an object to an array. 188 `ProxyManager` simply copies a reference to the properties into the `$properties` array passed to the 189 initializer, which allows you to set the state of the object without accessing any of its public 190 API. (This is a very important detail for mapper implementations!) 191 192 Specifically: 193 194 * `"\0Ns\\ClassName\0propertyName"` means `private $propertyName` defined in `Ns\ClassName`; 195 * `"\0*\0propertyName"` means `protected $propertyName` defined in any level of the class 196 hierarchy; 197 * `"propertyName"` means `public $propertyName` defined in any level of the class hierarchy. 198 199 Therefore, given this class: 200 201 ```php 202 namespace MyNamespace; 203 204 class MyClass 205 { 206 private $property1; 207 protected $property2; 208 public $property3; 209 } 210 ``` 211 212 Its appropriate initialization code would be: 213 214 ```php 215 namespace MyApp; 216 217 use ProxyManager\Factory\LazyLoadingGhostFactory; 218 use ProxyManager\Proxy\GhostObjectInterface; 219 220 require_once __DIR__ . '/vendor/autoload.php'; 221 222 $factory = new LazyLoadingGhostFactory(); 223 $initializer = function ( 224 GhostObjectInterface $ghostObject, 225 string $method, 226 array $parameters, 227 & $initializer, 228 array $properties 229 ) { 230 $initializer = null; 231 232 $properties["\0MyNamespace\\MyClass\0property1"] = 'foo'; //private property of MyNamespace\MyClass 233 $properties["\0*\0property2"] = 'bar'; //protected property in MyClass's hierarchy 234 $properties["property3"] = 'baz'; //public property in MyClass's hierarchy 235 236 return true; 237 }; 238 239 $instance = $factory->createProxy(\MyNamespace\MyClass::class, $initializer); 240 ``` 241 242 This code would initialize `$property1`, `$property2` and `$property3` 243 respectively to `"foo"`, `"bar"` and `"baz"`. 244 245 You may read the default values for those properties by reading the respective array keys. 246 247 Although it is possible to initialize the object by interacting with its public API, it is 248 not safe to do so, because the object only contains default property values since its constructor was not called. 249 250 ## Proxy implementation 251 252 The 253 [`ProxyManager\Factory\LazyLoadingGhostFactory`](https://github.com/Ocramius/ProxyManager/blob/master/src/ProxyManager/Factory/LazyLoadingGhostFactory.php) 254 produces proxies that implement the 255 [`ProxyManager\Proxy\GhostObjectInterface`](https://github.com/Ocramius/ProxyManager/blob/master/src/ProxyManager/Proxy/GhostObjectInterface.php). 256 257 At any point in time, you can set a new initializer for the proxy: 258 259 ```php 260 $ghostObject->setProxyInitializer($initializer); 261 ``` 262 263 In your initializer, you **MUST** turn off any further initialization: 264 265 ```php 266 $ghostObject->setProxyInitializer(null); 267 ``` 268 269 or 270 271 ```php 272 $initializer = null; // if you use the initializer passed by reference to the closure 273 ``` 274 275 Remember to call `$ghostObject->setProxyInitializer(null);`, or to set `$initializer = null` inside your 276 initializer closure to disable initialization of your proxy, or else initialization will trigger 277 more than once. 278 279 ## Triggering Initialization 280 281 A lazy loading ghost object is initialized whenever you access any of its properties. 282 Any of the following interactions would trigger lazy initialization: 283 284 ```php 285 // calling a method (only if the method accesses internal state) 286 $ghostObject->someMethod(); 287 288 // reading a property 289 echo $ghostObject->someProperty; 290 291 // writing a property 292 $ghostObject->someProperty = 'foo'; 293 294 // checking for existence of a property 295 isset($ghostObject->someProperty); 296 297 // removing a property 298 unset($ghostObject->someProperty); 299 300 // accessing a property via reflection 301 $reflection = new \ReflectionProperty($ghostObject, 'someProperty'); 302 $reflection->setAccessible(true); 303 $reflection->getValue($ghostObject); 304 305 // cloning the entire proxy 306 clone $ghostObject; 307 308 // serializing the proxy 309 $unserialized = unserialize(serialize($ghostObject)); 310 ``` 311 312 A method like following would never trigger lazy loading, in the context of a ghost object: 313 314 ```php 315 public function sayHello() : string 316 { 317 return 'Look ma! No property accessed!'; 318 } 319 ``` 320 321 ## Skipping properties (properties that should not be initialized) 322 323 In some contexts, you may want some properties to be completely ignored by the lazy-loading 324 system. 325 326 An example for that (in data mappers) is entities with identifiers: an identifier is usually 327 328 * lightweight 329 * known at all times 330 331 This means that it can be set in our object at all times, and we never need to lazy-load 332 it. Here is a typical example: 333 334 ```php 335 namespace MyApp; 336 337 class User 338 { 339 private $id; 340 private $username; 341 private $passwordHash; 342 private $email; 343 private $address; 344 // ... 345 346 public function getId() : int 347 { 348 return $this->id; 349 } 350 } 351 ``` 352 353 If we want to skip the property `$id` from lazy-loading, we might want to tell that to 354 the `LazyLoadingGhostFactory`. Here is a longer example, with a more near-real-world 355 scenario: 356 357 ```php 358 namespace MyApp; 359 360 use ProxyManager\Factory\LazyLoadingGhostFactory; 361 use ProxyManager\Proxy\GhostObjectInterface; 362 363 require_once __DIR__ . '/vendor/autoload.php'; 364 365 $factory = new LazyLoadingGhostFactory(); 366 $initializer = function ( 367 GhostObjectInterface $ghostObject, 368 string $method, 369 array $parameters, 370 & $initializer, 371 array $properties 372 ) { 373 $initializer = null; 374 375 // note that `getId` won't initialize our proxy here 376 $properties["\0MyApp\\User\0username"] = $db->fetchField('users', 'username', $ghostObject->getId(); 377 $properties["\0MyApp\\User\0passwordHash"] = $db->fetchField('users', 'passwordHash', $ghostObject->getId(); 378 $properties["\0MyApp\\User\0email"] = $db->fetchField('users', 'email', $ghostObject->getId(); 379 $properties["\0MyApp\\User\0address"] = $db->fetchField('users', 'address', $ghostObject->getId(); 380 381 return true; 382 }; 383 $proxyOptions = [ 384 'skippedProperties' => [ 385 "\0MyApp\\User\0id", 386 ], 387 ]; 388 389 $instance = $factory->createProxy(User::class, $initializer, $proxyOptions); 390 391 $idReflection = new \ReflectionProperty(User::class, 'id'); 392 393 $idReflection->setAccessible(true); 394 395 // write the identifier into our ghost object (assuming `setId` doesn't exist) 396 $idReflection->setValue($instance, 1234); 397 ``` 398 399 In this example, we pass a `skippedProperties` array to our proxy factory. Note the use of the `"\0"` parameter syntax as described above. 400 401 ## Proxying interfaces 402 403 A lazy loading ghost object cannot proxy an interface directly, as it operates directly around 404 the state of an object. Use a [Virtual Proxy](lazy-loading-value-holder.md) for that instead. 405 406 ## Tuning performance for production 407 408 See [Tuning ProxyManager for Production](tuning-for-production.md).
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Mon Nov 25 19:05:08 2024 | Cross-referenced by PHPXref 0.7.1 |