Skip to content

Commit fefdbda

Browse files
Add "skipDestructor" option to lazy loading proxies
1 parent 1022514 commit fefdbda

File tree

8 files changed

+180
-12
lines changed

8 files changed

+180
-12
lines changed

src/ProxyManager/Factory/LazyLoadingValueHolderFactory.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ public function __construct(?Configuration $configuration = null)
3838
* array<string, mixed>=,
3939
* ?Closure=
4040
* ) : bool $initializer
41+
* @psalm-param array{skipDestructor?: bool} $proxyOptions
4142
*
4243
* @psalm-return RealObjectType&ValueHolderInterface<RealObjectType>&VirtualProxyInterface
4344
*
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace ProxyManager\ProxyGenerator\LazyLoadingGhost\MethodGenerator;
6+
7+
use Laminas\Code\Generator\Exception\InvalidArgumentException;
8+
use Laminas\Code\Generator\PropertyGenerator;
9+
use ProxyManager\Generator\MethodGenerator;
10+
11+
/**
12+
* Destructor that skips the original destructor when the proxy is not initialized.
13+
*/
14+
class SkipDestructor extends MethodGenerator
15+
{
16+
/**
17+
* Constructor
18+
*
19+
* @throws InvalidArgumentException
20+
*/
21+
public function __construct(PropertyGenerator $initializerProperty)
22+
{
23+
parent::__construct('__destruct');
24+
25+
$this->setBody(
26+
'$this->' . $initializerProperty->getName() . ' || parent::__destruct();'
27+
);
28+
}
29+
}

src/ProxyManager/ProxyGenerator/LazyLoadingGhostGenerator.php

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
use ProxyManager\ProxyGenerator\LazyLoadingGhost\MethodGenerator\MagicSleep;
2626
use ProxyManager\ProxyGenerator\LazyLoadingGhost\MethodGenerator\MagicUnset;
2727
use ProxyManager\ProxyGenerator\LazyLoadingGhost\MethodGenerator\SetProxyInitializer;
28+
use ProxyManager\ProxyGenerator\LazyLoadingGhost\MethodGenerator\SkipDestructor;
2829
use ProxyManager\ProxyGenerator\LazyLoadingGhost\PropertyGenerator\InitializationTracker;
2930
use ProxyManager\ProxyGenerator\LazyLoadingGhost\PropertyGenerator\InitializerProperty;
3031
use ProxyManager\ProxyGenerator\LazyLoadingGhost\PropertyGenerator\PrivatePropertiesMap;
@@ -53,7 +54,7 @@ class LazyLoadingGhostGenerator implements ProxyGeneratorInterface
5354
* @throws InvalidProxiedClassException
5455
* @throws InvalidArgumentException
5556
*
56-
* @psalm-param array{skippedProperties?: array<int, string>} $proxyOptions
57+
* @psalm-param array{skippedProperties?: array<int, string>, skipDestructor?: bool} $proxyOptions
5758
*/
5859
public function generate(ReflectionClass $originalClass, ClassGenerator $classGenerator, array $proxyOptions = [])
5960
{
@@ -65,6 +66,7 @@ public function generate(ReflectionClass $originalClass, ClassGenerator $classGe
6566
$publicProperties = new PublicPropertiesMap($filteredProperties);
6667
$privateProperties = new PrivatePropertiesMap($filteredProperties);
6768
$protectedProperties = new ProtectedPropertiesMap($filteredProperties);
69+
$skipDestructor = ($proxyOptions['skipDestructor'] ?? false) && $originalClass->hasMethod('__destruct');
6870

6971
$classGenerator->setExtendedClass($originalClass->getName());
7072
$classGenerator->setImplementedInterfaces([GhostObjectInterface::class]);
@@ -81,7 +83,7 @@ static function (MethodGenerator $generatedMethod) use ($originalClass, $classGe
8183
ClassGeneratorUtils::addMethodIfNotFinal($originalClass, $classGenerator, $generatedMethod);
8284
},
8385
array_merge(
84-
$this->getAbstractProxiedMethods($originalClass),
86+
$this->getAbstractProxiedMethods($originalClass, $skipDestructor),
8587
[
8688
$init,
8789
new StaticProxyConstructor($initializer, $filteredProperties),
@@ -124,7 +126,8 @@ static function (MethodGenerator $generatedMethod) use ($originalClass, $classGe
124126
new GetProxyInitializer($initializer),
125127
new InitializeProxy($initializer, $init),
126128
new IsProxyInitialized($initializer),
127-
]
129+
],
130+
$skipDestructor ? [new SkipDestructor($initializer)] : []
128131
)
129132
);
130133
}
@@ -134,8 +137,14 @@ static function (MethodGenerator $generatedMethod) use ($originalClass, $classGe
134137
*
135138
* @return MethodGenerator[]
136139
*/
137-
private function getAbstractProxiedMethods(ReflectionClass $originalClass): array
140+
private function getAbstractProxiedMethods(ReflectionClass $originalClass, bool $skipDestructor): array
138141
{
142+
$excludedMethods = ProxiedMethodsFilter::DEFAULT_EXCLUDED;
143+
144+
if ($skipDestructor) {
145+
$excludedMethods[] = '__destruct';
146+
}
147+
139148
return array_map(
140149
static function (ReflectionMethod $method): ProxyManagerMethodGenerator {
141150
$generated = ProxyManagerMethodGenerator::fromReflectionWithoutBodyAndDocBlock(
@@ -146,7 +155,7 @@ static function (ReflectionMethod $method): ProxyManagerMethodGenerator {
146155

147156
return $generated;
148157
},
149-
ProxiedMethodsFilter::getAbstractProxiedMethods($originalClass)
158+
ProxiedMethodsFilter::getAbstractProxiedMethods($originalClass, $excludedMethods)
150159
);
151160
}
152161
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace ProxyManager\ProxyGenerator\LazyLoadingValueHolder\MethodGenerator;
6+
7+
use Laminas\Code\Generator\PropertyGenerator;
8+
use ProxyManager\Generator\MethodGenerator;
9+
use ReflectionClass;
10+
11+
/**
12+
* Destructor that skips the original destructor when the proxy is not initialized.
13+
*/
14+
class SkipDestructor extends MethodGenerator
15+
{
16+
/**
17+
* Constructor
18+
*/
19+
public function __construct(
20+
PropertyGenerator $initializerProperty,
21+
PropertyGenerator $valueHolderProperty
22+
) {
23+
parent::__construct('__destruct');
24+
25+
$initializer = $initializerProperty->getName();
26+
$valueHolder = $valueHolderProperty->getName();
27+
28+
$this->setBody(
29+
'$this->' . $initializer . ' || $this->' . $valueHolder . '->__destruct();'
30+
);
31+
}
32+
}

src/ProxyManager/ProxyGenerator/LazyLoadingValueHolderGenerator.php

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
use ProxyManager\ProxyGenerator\LazyLoadingValueHolder\MethodGenerator\MagicSleep;
2626
use ProxyManager\ProxyGenerator\LazyLoadingValueHolder\MethodGenerator\MagicUnset;
2727
use ProxyManager\ProxyGenerator\LazyLoadingValueHolder\MethodGenerator\SetProxyInitializer;
28+
use ProxyManager\ProxyGenerator\LazyLoadingValueHolder\MethodGenerator\SkipDestructor;
2829
use ProxyManager\ProxyGenerator\LazyLoadingValueHolder\PropertyGenerator\InitializerProperty;
2930
use ProxyManager\ProxyGenerator\LazyLoadingValueHolder\PropertyGenerator\ValueHolderProperty;
3031
use ProxyManager\ProxyGenerator\PropertyGenerator\PublicPropertiesMap;
@@ -37,6 +38,8 @@
3738

3839
use function array_map;
3940
use function array_merge;
41+
use function func_get_arg;
42+
use function func_num_args;
4043

4144
/**
4245
* Generator for proxies implementing {@see \ProxyManager\Proxy\VirtualProxyInterface}
@@ -52,9 +55,14 @@ class LazyLoadingValueHolderGenerator implements ProxyGeneratorInterface
5255
*
5356
* @throws InvalidProxiedClassException
5457
* @throws InvalidArgumentException
58+
*
59+
* @psalm-param array{skipDestructor?: bool} $proxyOptions
5560
*/
56-
public function generate(ReflectionClass $originalClass, ClassGenerator $classGenerator)
61+
public function generate(ReflectionClass $originalClass, ClassGenerator $classGenerator/*, array $proxyOptions = []*/)
5762
{
63+
/** @psalm-var array{skipDestructor?: bool} $proxyOptions */
64+
$proxyOptions = func_num_args() >= 3 ? func_get_arg(2) : [];
65+
5866
CanProxyAssertion::assertClassCanBeProxied($originalClass);
5967

6068
$interfaces = [VirtualProxyInterface::class];
@@ -71,14 +79,21 @@ public function generate(ReflectionClass $originalClass, ClassGenerator $classGe
7179
$classGenerator->addPropertyFromGenerator($initializer = new InitializerProperty());
7280
$classGenerator->addPropertyFromGenerator($publicProperties);
7381

82+
$skipDestructor = ($proxyOptions['skipDestructor'] ?? false) && $originalClass->hasMethod('__destruct');
83+
$excludedMethods = ProxiedMethodsFilter::DEFAULT_EXCLUDED;
84+
85+
if ($skipDestructor) {
86+
$excludedMethods[] = '__destruct';
87+
}
88+
7489
array_map(
7590
static function (MethodGenerator $generatedMethod) use ($originalClass, $classGenerator): void {
7691
ClassGeneratorUtils::addMethodIfNotFinal($originalClass, $classGenerator, $generatedMethod);
7792
},
7893
array_merge(
7994
array_map(
8095
$this->buildLazyLoadingMethodInterceptor($initializer, $valueHolder),
81-
ProxiedMethodsFilter::getProxiedMethods($originalClass)
96+
ProxiedMethodsFilter::getProxiedMethods($originalClass, $excludedMethods)
8297
),
8398
[
8499
new StaticProxyConstructor($initializer, Properties::fromReflectionClass($originalClass)),
@@ -95,7 +110,8 @@ static function (MethodGenerator $generatedMethod) use ($originalClass, $classGe
95110
new InitializeProxy($initializer, $valueHolder),
96111
new IsProxyInitialized($valueHolder),
97112
new GetWrappedValueHolderValue($valueHolder),
98-
]
113+
],
114+
$skipDestructor ? [new SkipDestructor($initializer, $valueHolder)] : []
99115
)
100116
);
101117
}

src/ProxyManager/ProxyGenerator/Util/ProxiedMethodsFilter.php

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@
1919
*/
2020
final class ProxiedMethodsFilter
2121
{
22-
/** @var array<int, string> */
23-
private static $defaultExcluded = [
22+
/**
23+
* @internal
24+
*/
25+
public const DEFAULT_EXCLUDED = [
2426
'__get',
2527
'__set',
2628
'__isset',
@@ -38,7 +40,7 @@ final class ProxiedMethodsFilter
3840
*/
3941
public static function getProxiedMethods(ReflectionClass $class, ?array $excluded = null): array
4042
{
41-
return self::doFilter($class, $excluded ?? self::$defaultExcluded);
43+
return self::doFilter($class, $excluded ?? self::DEFAULT_EXCLUDED);
4244
}
4345

4446
/**
@@ -49,7 +51,7 @@ public static function getProxiedMethods(ReflectionClass $class, ?array $exclude
4951
*/
5052
public static function getAbstractProxiedMethods(ReflectionClass $class, ?array $excluded = null): array
5153
{
52-
return self::doFilter($class, $excluded ?? self::$defaultExcluded, true);
54+
return self::doFilter($class, $excluded ?? self::DEFAULT_EXCLUDED, true);
5355
}
5456

5557
/**
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
--TEST--
2+
Verifies that generated lazy loading ghost objects can skip calling the proxied destructor
3+
--FILE--
4+
<?php
5+
6+
require_once __DIR__ . '/init.php';
7+
8+
class Destructable
9+
{
10+
public function __destruct()
11+
{
12+
echo __FUNCTION__;
13+
}
14+
}
15+
16+
$factory = new \ProxyManager\Factory\LazyLoadingGhostFactory($configuration);
17+
18+
$init = function ($object, $method, $parameters, & $initializer) {
19+
echo 'init';
20+
$initializer = null;
21+
};
22+
23+
$proxy = $factory->createProxy(Destructable::class, $init, ['skipDestructor' => true]);
24+
echo "NO __destruct\n";
25+
unset($proxy);
26+
27+
$proxy = $factory->createProxy(Destructable::class, $init, ['skipDestructor' => true]);
28+
echo 'DO ';
29+
$proxy->triggerInit = true;
30+
unset($proxy);
31+
32+
$proxy = $factory->createProxy(Destructable::class, $init);
33+
echo "\nDO ";
34+
unset($proxy);
35+
?>
36+
--EXPECT--
37+
NO __destruct
38+
DO init__destruct
39+
DO __destruct
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
--TEST--
2+
Verifies that generated lazy loading value holders can skip calling the proxied destructor
3+
--FILE--
4+
<?php
5+
6+
require_once __DIR__ . '/init.php';
7+
8+
class Destructable
9+
{
10+
public function __destruct()
11+
{
12+
echo __FUNCTION__;
13+
}
14+
}
15+
16+
$factory = new \ProxyManager\Factory\LazyLoadingValueHolderFactory($configuration);
17+
18+
$init = function (& $wrapped, $proxy, $method, $parameters, & $initializer) {
19+
echo 'init';
20+
$wrapped = new Destructable();
21+
$initializer = null;
22+
};
23+
24+
$proxy = $factory->createProxy(Destructable::class, $init, ['skipDestructor' => true]);
25+
echo "NO __destruct\n";
26+
unset($proxy);
27+
28+
$proxy = $factory->createProxy(Destructable::class, $init, ['skipDestructor' => true]);
29+
echo "DO ";
30+
$proxy->triggerInit = true;
31+
unset($proxy);
32+
33+
$proxy = $factory->createProxy(Destructable::class, $init);
34+
echo "\nDO ";
35+
unset($proxy);
36+
?>
37+
--EXPECT--
38+
NO __destruct
39+
DO init__destruct__destruct
40+
DO init__destruct__destruct

0 commit comments

Comments
 (0)