Verzeichnisstruktur phpBB-3.3.15
- Veröffentlicht
- 28.08.2024
So funktioniert es
|
Auf das letzte Element klicken. Dies geht jeweils ein Schritt zurück |
Auf das Icon klicken, dies öffnet das Verzeichnis. Nochmal klicken schließt das Verzeichnis. |
|
(Beispiel Datei-Icons)
|
Auf das Icon klicken um den Quellcode anzuzeigen |
lazy-loading-ghost-object.md
001 ---
002 title: Lazy Loading Ghost Object Proxies
003 ---
004
005 # Lazy Loading Ghost Object Proxies
006
007 A Lazy Loading Ghost is a type of proxy object.
008
009 More specifically, it is a fake object that looks exactly like an object
010 that you want to interact with, but is actually just an empty instance
011 that gets all properties populated as soon as they are needed.
012
013 Those properties do not really exist until the ghost object is actually
014 initialized.
015
016 ## Lazy loading with the Ghost Object
017
018 In pseudo-code, in userland, [lazy loading](http://www.martinfowler.com/eaaCatalog/lazyLoad.html) in a ghost object
019 looks like following:
020
021 ```php
022 class MyObjectProxy
023 {
024 private $initialized = false;
025 private $name;
026 private $surname;
027
028 public function doFoo()
029 {
030 $this->init();
031
032 // Perform doFoo routine using loaded variables
033 }
034
035 private function init()
036 {
037 if (! $this->initialized) {
038 $data = some_logic_that_loads_data();
039
040 $this->name = $data['name'];
041 $this->surname = $data['surname'];
042
043 $this->initialized = true;
044 }
045 }
046 }
047 ```
048
049 Ghost objects work similarly to virtual proxies, but since they don't wrap around a "real" instance of the proxied
050 subject, they are better suited for representing dataset rows.
051
052 ## When do I use a ghost object?
053
054 You usually need a ghost object in cases where following applies:
055
056 * you are building a small data-mapper and want to lazily load data across associations in your object graph;
057 * you want to initialize objects representing rows in a large dataset;
058 * you want to compare instances of lazily initialized objects without the risk of comparing a proxy with a real subject;
059 * you are aware of the internal state of the object and are confident in working with its internals via reflection
060 or direct property access.
061
062 ## Usage examples
063
064 [ProxyManager](https://github.com/Ocramius/ProxyManager) provides a factory that creates lazy loading ghost objects.
065 To use it, follow these steps:
066
067 First, define your object's logic without taking care of lazy loading:
068
069 ```php
070 namespace MyApp;
071
072 class Customer
073 {
074 private $name;
075 private $surname;
076
077 // just write your business logic or generally logic
078 // don't worry about how complex this object will be!
079 // don't code lazy-loading oriented optimizations in here!
080 public function getName() { return $this->name; }
081 public function setName($name) { $this->name = (string) $name; }
082 public function getSurname() { return $this->surname; }
083 public function setSurname($surname) { $this->surname = (string) $surname; }
084 }
085 ```
086
087 Then, use the proxy manager to create a ghost object of it.
088 You will be responsible for setting its state during lazy loading:
089
090 ```php
091 namespace MyApp;
092
093 use ProxyManager\Factory\LazyLoadingGhostFactory;
094 use ProxyManager\Proxy\GhostObjectInterface;
095
096 require_once __DIR__ . '/vendor/autoload.php';
097
098 $factory = new LazyLoadingGhostFactory();
099 $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).
409