Augmentation vers version 3.3.0
This commit is contained in:
@@ -1,7 +1,17 @@
|
||||
---
|
||||
title: Lazy Loading Ghost Object Proxies
|
||||
---
|
||||
|
||||
# Lazy Loading Ghost Object Proxies
|
||||
|
||||
A lazy loading ghost object proxy is a ghost proxy that looks exactly like the real instance of the proxied subject,
|
||||
but which has all properties nulled before initialization.
|
||||
A Lazy Loading Ghost is a type of proxy object.
|
||||
|
||||
More specifically, it is a fake object that looks exactly like an object
|
||||
that you want to interact with, but is actually just an empty instance
|
||||
that gets all properties populated as soon as they are needed.
|
||||
|
||||
Those properties do not really exist until the ghost object is actually
|
||||
initialized.
|
||||
|
||||
## Lazy loading with the Ghost Object
|
||||
|
||||
@@ -41,20 +51,20 @@ subject, they are better suited for representing dataset rows.
|
||||
|
||||
## When do I use a ghost object?
|
||||
|
||||
You usually need a ghost object in cases where following applies
|
||||
You usually need a ghost object in cases where following applies:
|
||||
|
||||
* you are building a small data-mapper and want to lazily load data across associations in your object graph
|
||||
* you want to initialize objects representing rows in a large dataset
|
||||
* you want to compare instances of lazily initialized objects without the risk of comparing a proxy with a real subject
|
||||
* you are building a small data-mapper and want to lazily load data across associations in your object graph;
|
||||
* you want to initialize objects representing rows in a large dataset;
|
||||
* you want to compare instances of lazily initialized objects without the risk of comparing a proxy with a real subject;
|
||||
* you are aware of the internal state of the object and are confident in working with its internals via reflection
|
||||
or direct property access
|
||||
or direct property access.
|
||||
|
||||
## Usage examples
|
||||
|
||||
[ProxyManager](https://github.com/Ocramius/ProxyManager) provides a factory that creates lazy loading ghost objects.
|
||||
To use it, follow these steps:
|
||||
|
||||
First of all, define your object's logic without taking care of lazy loading:
|
||||
First, define your object's logic without taking care of lazy loading:
|
||||
|
||||
```php
|
||||
namespace MyApp;
|
||||
@@ -74,88 +84,186 @@ class Customer
|
||||
}
|
||||
```
|
||||
|
||||
Then use the proxy manager to create a ghost object of it.
|
||||
You will be responsible of setting its state during lazy loading:
|
||||
Then, use the proxy manager to create a ghost object of it.
|
||||
You will be responsible for setting its state during lazy loading:
|
||||
|
||||
```php
|
||||
namespace MyApp;
|
||||
|
||||
use ProxyManager\Factory\LazyLoadingGhostFactory;
|
||||
use ProxyManager\Proxy\LazyLoadingInterface;
|
||||
use ProxyManager\Proxy\GhostObjectInterface;
|
||||
|
||||
require_once __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
$factory = new LazyLoadingGhostFactory();
|
||||
$initializer = function (LazyLoadingInterface $proxy, $method, array $parameters, & $initializer) {
|
||||
$initializer = function (
|
||||
GhostObjectInterface $ghostObject,
|
||||
string $method,
|
||||
array $parameters,
|
||||
& $initializer,
|
||||
array $properties
|
||||
) {
|
||||
$initializer = null; // disable initialization
|
||||
|
||||
// load data and modify the object here
|
||||
$proxy->setName('Agent');
|
||||
$proxy->setSurname('Smith');
|
||||
$properties["\0MyApp\\Customer\0name"] = 'Agent';
|
||||
$properties["\0MyApp\\Customer\0surname"] = 'Smith';
|
||||
|
||||
// you may also call methods on the object, but remember that
|
||||
// the constructor was not called yet:
|
||||
$ghostObject->setSurname('Smith');
|
||||
|
||||
return true; // confirm that initialization occurred correctly
|
||||
};
|
||||
|
||||
$instance = $factory->createProxy('MyApp\Customer', $initializer);
|
||||
$ghostObject = $factory->createProxy(\MyApp\Customer::class, $initializer);
|
||||
```
|
||||
|
||||
You can now simply use your object as before:
|
||||
You can now use your object as before:
|
||||
|
||||
```php
|
||||
// this will just work as before
|
||||
echo $proxy->getName() . ' ' . $proxy->getSurname(); // Agent Smith
|
||||
// this will work as before
|
||||
echo $ghostObject->getName() . ' ' . $ghostObject->getSurname(); // Agent Smith
|
||||
```
|
||||
|
||||
## Lazy Initialization
|
||||
|
||||
As you can see, we use a closure to handle lazy initialization of the proxy instance at runtime.
|
||||
The initializer closure signature for ghost objects should be as following:
|
||||
We use a closure to handle lazy initialization of the proxy instance at runtime.
|
||||
The initializer closure signature for ghost objects is:
|
||||
|
||||
```php
|
||||
/**
|
||||
* @var object $proxy the instance the ghost object proxy that is being initialized
|
||||
* @var string $method the name of the method that triggered lazy initialization
|
||||
* @var array $parameters an ordered list of parameters passed to the method that
|
||||
* triggered initialization, indexed by parameter name
|
||||
* @var Closure $initializer a reference to the property that is the initializer for the
|
||||
* proxy. Set it to null to disable further initialization
|
||||
* @var object $ghostObject The instance of the ghost object proxy that is being initialized.
|
||||
* @var string $method The name of the method that triggered lazy initialization.
|
||||
* @var array $parameters An ordered list of parameters passed to the method that
|
||||
* triggered initialization, indexed by parameter name.
|
||||
* @var Closure $initializer A reference to the property that is the initializer for the
|
||||
* proxy. Set it to null to disable further initialization.
|
||||
* @var array $properties By-ref array with the properties defined in the object, with their
|
||||
* default values pre-assigned. Keys are in the same format that
|
||||
* an (array) cast of an object would provide:
|
||||
* - `"\0Ns\\ClassName\0propertyName"` for `private $propertyName`
|
||||
* defined on `Ns\ClassName`
|
||||
* - `"\0Ns\\ClassName\0propertyName"` for `protected $propertyName`
|
||||
* defined in any level of the hierarchy
|
||||
* - `"propertyName"` for `public $propertyName`
|
||||
* defined in any level of the hierarchy
|
||||
*
|
||||
* @return bool true on success
|
||||
*/
|
||||
$initializer = function ($proxy, $method, $parameters, & $initializer) {};
|
||||
$initializer = function (
|
||||
\ProxyManager\Proxy\GhostObjectInterface $ghostObject,
|
||||
string $method,
|
||||
array $parameters,
|
||||
& $initializer,
|
||||
array $properties
|
||||
) {};
|
||||
```
|
||||
|
||||
The initializer closure should usually be coded like following:
|
||||
|
||||
```php
|
||||
$initializer = function ($proxy, $method, $parameters, & $initializer) {
|
||||
$initializer = function (
|
||||
\ProxyManager\Proxy\GhostObjectInterface $ghostObject,
|
||||
string $method,
|
||||
array $parameters,
|
||||
& $initializer,
|
||||
array $properties
|
||||
) {
|
||||
$initializer = null; // disable initializer for this proxy instance
|
||||
|
||||
// modify the object with loaded data
|
||||
$proxy->setFoo(/* ... */);
|
||||
$proxy->setBar(/* ... */);
|
||||
// initialize properties (please read further on)
|
||||
$properties["\0ClassName\0foo"] = 'foo';
|
||||
$properties["\0ClassName\0bar"] = 'bar';
|
||||
|
||||
return true; // report success
|
||||
};
|
||||
```
|
||||
|
||||
### Lazy initialization `$properties` explained
|
||||
|
||||
The assignments to properties in this closure use unusual `"\0"` sequences.
|
||||
This is to be consistent with how PHP represents private and protected properties when
|
||||
casting an object to an array.
|
||||
`ProxyManager` simply copies a reference to the properties into the `$properties` array passed to the
|
||||
initializer, which allows you to set the state of the object without accessing any of its public
|
||||
API. (This is a very important detail for mapper implementations!)
|
||||
|
||||
Specifically:
|
||||
|
||||
* `"\0Ns\\ClassName\0propertyName"` means `private $propertyName` defined in `Ns\ClassName`;
|
||||
* `"\0*\0propertyName"` means `protected $propertyName` defined in any level of the class
|
||||
hierarchy;
|
||||
* `"propertyName"` means `public $propertyName` defined in any level of the class hierarchy.
|
||||
|
||||
Therefore, given this class:
|
||||
|
||||
```php
|
||||
namespace MyNamespace;
|
||||
|
||||
class MyClass
|
||||
{
|
||||
private $property1;
|
||||
protected $property2;
|
||||
public $property3;
|
||||
}
|
||||
```
|
||||
|
||||
Its appropriate initialization code would be:
|
||||
|
||||
```php
|
||||
namespace MyApp;
|
||||
|
||||
use ProxyManager\Factory\LazyLoadingGhostFactory;
|
||||
use ProxyManager\Proxy\GhostObjectInterface;
|
||||
|
||||
require_once __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
$factory = new LazyLoadingGhostFactory();
|
||||
$initializer = function (
|
||||
GhostObjectInterface $ghostObject,
|
||||
string $method,
|
||||
array $parameters,
|
||||
& $initializer,
|
||||
array $properties
|
||||
) {
|
||||
$initializer = null;
|
||||
|
||||
$properties["\0MyNamespace\\MyClass\0property1"] = 'foo'; //private property of MyNamespace\MyClass
|
||||
$properties["\0*\0property2"] = 'bar'; //protected property in MyClass's hierarchy
|
||||
$properties["property3"] = 'baz'; //public property in MyClass's hierarchy
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
$instance = $factory->createProxy(\MyNamespace\MyClass::class, $initializer);
|
||||
```
|
||||
|
||||
This code would initialize `$property1`, `$property2` and `$property3`
|
||||
respectively to `"foo"`, `"bar"` and `"baz"`.
|
||||
|
||||
You may read the default values for those properties by reading the respective array keys.
|
||||
|
||||
Although it is possible to initialize the object by interacting with its public API, it is
|
||||
not safe to do so, because the object only contains default property values since its constructor was not called.
|
||||
|
||||
## Proxy implementation
|
||||
|
||||
The
|
||||
[`ProxyManager\Factory\LazyLoadingGhostFactory`](https://github.com/Ocramius/ProxyManager/blob/master/src/ProxyManager/Factory/LazyLoadingGhostFactory.php)
|
||||
produces proxies that implement both the
|
||||
[`ProxyManager\Proxy\GhostObjectInterface`](https://github.com/Ocramius/ProxyManager/blob/master/src/ProxyManager/Proxy/GhostObjectInterface.php)
|
||||
and the
|
||||
[`ProxyManager\Proxy\LazyLoadingInterface`](https://github.com/Ocramius/ProxyManager/blob/master/src/ProxyManager/Proxy/LazyLoadingInterface.php).
|
||||
produces proxies that implement the
|
||||
[`ProxyManager\Proxy\GhostObjectInterface`](https://github.com/Ocramius/ProxyManager/blob/master/src/ProxyManager/Proxy/GhostObjectInterface.php).
|
||||
|
||||
At any point in time, you can set a new initializer for the proxy:
|
||||
|
||||
```php
|
||||
$proxy->setProxyInitializer($initializer);
|
||||
$ghostObject->setProxyInitializer($initializer);
|
||||
```
|
||||
|
||||
In your initializer, you **MUST** turn off any further initialization:
|
||||
|
||||
```php
|
||||
$proxy->setProxyInitializer(null);
|
||||
$ghostObject->setProxyInitializer(null);
|
||||
```
|
||||
|
||||
or
|
||||
@@ -164,43 +272,137 @@ or
|
||||
$initializer = null; // if you use the initializer passed by reference to the closure
|
||||
```
|
||||
|
||||
Remember to call `$ghostObject->setProxyInitializer(null);`, or to set `$initializer = null` inside your
|
||||
initializer closure to disable initialization of your proxy, or else initialization will trigger
|
||||
more than once.
|
||||
|
||||
## Triggering Initialization
|
||||
|
||||
A lazy loading ghost object is initialized whenever you access any property or method of it.
|
||||
A lazy loading ghost object is initialized whenever you access any of its properties.
|
||||
Any of the following interactions would trigger lazy initialization:
|
||||
|
||||
```php
|
||||
// calling a method
|
||||
$proxy->someMethod();
|
||||
// calling a method (only if the method accesses internal state)
|
||||
$ghostObject->someMethod();
|
||||
|
||||
// reading a property
|
||||
echo $proxy->someProperty;
|
||||
echo $ghostObject->someProperty;
|
||||
|
||||
// writing a property
|
||||
$proxy->someProperty = 'foo';
|
||||
$ghostObject->someProperty = 'foo';
|
||||
|
||||
// checking for existence of a property
|
||||
isset($proxy->someProperty);
|
||||
isset($ghostObject->someProperty);
|
||||
|
||||
// removing a property
|
||||
unset($proxy->someProperty);
|
||||
unset($ghostObject->someProperty);
|
||||
|
||||
// accessing a property via reflection
|
||||
$reflection = new \ReflectionProperty($ghostObject, 'someProperty');
|
||||
$reflection->setAccessible(true);
|
||||
$reflection->getValue($ghostObject);
|
||||
|
||||
// cloning the entire proxy
|
||||
clone $proxy;
|
||||
clone $ghostObject;
|
||||
|
||||
// serializing the proxy
|
||||
$unserialized = unserialize(serialize($proxy));
|
||||
$unserialized = unserialize(serialize($ghostObject));
|
||||
```
|
||||
|
||||
Remember to call `$proxy->setProxyInitializer(null);` to disable initialization of your proxy, or it will happen more
|
||||
than once.
|
||||
A method like following would never trigger lazy loading, in the context of a ghost object:
|
||||
|
||||
```php
|
||||
public function sayHello() : string
|
||||
{
|
||||
return 'Look ma! No property accessed!';
|
||||
}
|
||||
```
|
||||
|
||||
## Skipping properties (properties that should not be initialized)
|
||||
|
||||
In some contexts, you may want some properties to be completely ignored by the lazy-loading
|
||||
system.
|
||||
|
||||
An example for that (in data mappers) is entities with identifiers: an identifier is usually
|
||||
|
||||
* lightweight
|
||||
* known at all times
|
||||
|
||||
This means that it can be set in our object at all times, and we never need to lazy-load
|
||||
it. Here is a typical example:
|
||||
|
||||
```php
|
||||
namespace MyApp;
|
||||
|
||||
class User
|
||||
{
|
||||
private $id;
|
||||
private $username;
|
||||
private $passwordHash;
|
||||
private $email;
|
||||
private $address;
|
||||
// ...
|
||||
|
||||
public function getId() : int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If we want to skip the property `$id` from lazy-loading, we might want to tell that to
|
||||
the `LazyLoadingGhostFactory`. Here is a longer example, with a more near-real-world
|
||||
scenario:
|
||||
|
||||
```php
|
||||
namespace MyApp;
|
||||
|
||||
use ProxyManager\Factory\LazyLoadingGhostFactory;
|
||||
use ProxyManager\Proxy\GhostObjectInterface;
|
||||
|
||||
require_once __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
$factory = new LazyLoadingGhostFactory();
|
||||
$initializer = function (
|
||||
GhostObjectInterface $ghostObject,
|
||||
string $method,
|
||||
array $parameters,
|
||||
& $initializer,
|
||||
array $properties
|
||||
) {
|
||||
$initializer = null;
|
||||
|
||||
// note that `getId` won't initialize our proxy here
|
||||
$properties["\0MyApp\\User\0username"] = $db->fetchField('users', 'username', $ghostObject->getId();
|
||||
$properties["\0MyApp\\User\0passwordHash"] = $db->fetchField('users', 'passwordHash', $ghostObject->getId();
|
||||
$properties["\0MyApp\\User\0email"] = $db->fetchField('users', 'email', $ghostObject->getId();
|
||||
$properties["\0MyApp\\User\0address"] = $db->fetchField('users', 'address', $ghostObject->getId();
|
||||
|
||||
return true;
|
||||
};
|
||||
$proxyOptions = [
|
||||
'skippedProperties' => [
|
||||
"\0MyApp\\User\0id",
|
||||
],
|
||||
];
|
||||
|
||||
$instance = $factory->createProxy(User::class, $initializer, $proxyOptions);
|
||||
|
||||
$idReflection = new \ReflectionProperty(User::class, 'id');
|
||||
|
||||
$idReflection->setAccessible(true);
|
||||
|
||||
// write the identifier into our ghost object (assuming `setId` doesn't exist)
|
||||
$idReflection->setValue($instance, 1234);
|
||||
```
|
||||
|
||||
In this example, we pass a `skippedProperties` array to our proxy factory. Note the use of the `"\0"` parameter syntax as described above.
|
||||
|
||||
## Proxying interfaces
|
||||
|
||||
You can also generate proxies from an interface FQCN. By proxying an interface, you will only be able to access the
|
||||
methods defined by the interface itself, even if the `wrappedObject` implements more methods. This will anyway save
|
||||
some memory since the proxy won't contain any properties.
|
||||
A lazy loading ghost object cannot proxy an interface directly, as it operates directly around
|
||||
the state of an object. Use a [Virtual Proxy](lazy-loading-value-holder.md) for that instead.
|
||||
|
||||
## Tuning performance for production
|
||||
|
||||
See [Tuning ProxyManager for Production](https://github.com/Ocramius/ProxyManager/blob/master/docs/tuning-for-production.md).
|
||||
See [Tuning ProxyManager for Production](tuning-for-production.md).
|
||||
|
||||
Reference in New Issue
Block a user