Skip to content

Commit 365af97

Browse files
committed
[DI] Lazy Object #1195
1 parent 752c106 commit 365af97

12 files changed

Lines changed: 425 additions & 60 deletions

File tree

packages/di/src/Attributes/Autowire.php

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public function __invoke(AttributeHandler $handler): callable
2121
$reflector = $handler->getReflector();
2222

2323
if ($reflector instanceof ReflectionParameter && $reflector->getType()) {
24-
return function (...$args) use ($handler, $reflector, $container) {
24+
return function &(...$args) use ($handler, $reflector, $container) {
2525
$value = $handler(...$args);
2626

2727
// Only value is NULL needs autowire.
@@ -31,9 +31,10 @@ public function __invoke(AttributeHandler $handler): callable
3131

3232
$resolver = $container->getDependencyResolver();
3333

34-
return $resolver->resolveParameterValue(
35-
$resolver->resolveParameterDependency($reflector, [], new DIOptions(autowire: true)),
36-
);
34+
// resolveParameterValue() will be called in resolveParameterDependency()
35+
$value = &$resolver->resolveParameterDependency($reflector, [], new DIOptions(autowire: true));
36+
37+
return $value;
3738
};
3839
}
3940

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Windwalker\DI\Attributes;
6+
7+
use Windwalker\DI\DIOptions;
8+
9+
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_PARAMETER | \Attribute::TARGET_PROPERTY)]
10+
class Lazy implements ContainerAttributeInterface
11+
{
12+
public function __invoke(AttributeHandler $handler): callable
13+
{
14+
$container = $handler->getContainer();
15+
16+
// Property
17+
return static function () use ($container, $handler) {
18+
$bak = $container->options->lazy;
19+
$container->options->lazy = true;
20+
21+
$value = $handler();
22+
23+
$container->options->lazy = $bak;
24+
25+
return $value;
26+
};
27+
}
28+
}

packages/di/src/Container.php

Lines changed: 21 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use Windwalker\DI\Attributes\Autowire;
2222
use Windwalker\DI\Attributes\Decorator;
2323
use Windwalker\DI\Attributes\Inject;
24+
use Windwalker\DI\Attributes\Lazy;
2425
use Windwalker\DI\Attributes\Service;
2526
use Windwalker\DI\Attributes\Setup;
2627
use Windwalker\DI\Concern\ConfigRegisterTrait;
@@ -181,6 +182,7 @@ public static function getDefaultAttributes(): array
181182
Inject::class => AttributeType::PROPERTIES | AttributeType::PARAMETERS,
182183
Setup::class => AttributeType::METHODS,
183184
Service::class => AttributeType::PROPERTIES | AttributeType::PARAMETERS,
185+
Lazy::class => AttributeType::PROPERTIES,
184186
];
185187
}
186188

@@ -513,7 +515,11 @@ protected function findDefinition(
513515
}
514516

515517
// If has #[Service] attribute, we will register a new definition instantly.
516-
if ($serviceAutowire && class_exists($id) && $service = static::getServiceAttribute(new ReflectionClass($id))) {
518+
if (
519+
$serviceAutowire
520+
&& class_exists($id)
521+
&& $service = DependencyResolver::getAttributeFromReflection(new ReflectionClass($id), Service::class)
522+
) {
517523
if (($service->tag || $tag) && $service->tag !== $tag) {
518524
// If tag is not matched, we will not register this service.
519525
return null;
@@ -775,8 +781,6 @@ public function createObject(
775781
DIOptions|int $options = new DIOptions(),
776782
\UnitEnum|string|null $tag = null,
777783
): mixed {
778-
$options = DIOptions::wrap($options);
779-
780784
$callback = fn(Container $container) => $container->newInstance($class, $args, $options);
781785

782786
$this->set($class, $callback, $options, $tag);
@@ -890,10 +894,19 @@ public function whenCreating(string $class): ObjectBuilderDefinition
890894
*/
891895
public function newInstance(mixed $class, array $args = [], DIOptions|int $options = new DIOptions()): mixed
892896
{
893-
$options = DIOptions::wrap($options);
894-
895897
if (is_string($class)) {
896-
$class = $this->resolveAliasFromParent($class);
898+
$options = DIOptions::wrap($options);
899+
900+
$class = $this->resolveAliasDeep($class);
901+
902+
if (
903+
$this->dependencyResolver->canLazy($ref = new \ReflectionClass($class), $options)
904+
&& !$this->dependencyResolver::isInternal($ref)
905+
) {
906+
return $ref->newLazyProxy(
907+
fn() => $this->dependencyResolver->newInstance($class, $args, $options)
908+
);
909+
}
897910
}
898911

899912
return $this->dependencyResolver->newInstance($class, $args, $options);
@@ -950,12 +963,12 @@ public function resolveAlias(string $id): string
950963
return $id;
951964
}
952965

953-
protected function resolveAliasFromParent(string $id): string
966+
protected function resolveAliasDeep(string $id): string
954967
{
955968
$id = $this->resolveAlias($id);
956969

957970
if ($this->parent) {
958-
$id = $this->parent->resolveAliasFromParent($id);
971+
$id = $this->parent->resolveAliasDeep($id);
959972
}
960973

961974
return $id;
@@ -1282,22 +1295,6 @@ public function getLevel(): int
12821295
return $this->level;
12831296
}
12841297

1285-
/**
1286-
* @param ReflectionClass $dependency
1287-
*
1288-
* @return ?Service
1289-
*/
1290-
protected static function getServiceAttribute(ReflectionClass $dependency): ?Service
1291-
{
1292-
$attrs = $dependency->getAttributes(Service::class, \ReflectionAttribute::IS_INSTANCEOF);
1293-
1294-
if ($attrs === []) {
1295-
return null;
1296-
}
1297-
1298-
return $attrs[0]->newInstance();
1299-
}
1300-
13011298
public function getAliases(): array
13021299
{
13031300
return $this->aliases;

packages/di/src/DIOptions.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ public function __construct(
3434
* Ignore all attributes when create object or call method.
3535
*/
3636
public ?bool $ignoreAttributes = null,
37+
/**
38+
* Use PHP 8.4 Lazy Proxy to wrap resolved instance.
39+
*/
40+
public ?bool $lazy = null,
3741
) {
3842
}
3943

packages/di/src/DependencyResolver.php

Lines changed: 115 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use ReflectionUnionType;
1717
use TypeError;
1818
use UnexpectedValueException;
19+
use Windwalker\DI\Attributes\Lazy;
1920
use Windwalker\DI\Attributes\Service;
2021
use Windwalker\DI\Definition\DefinitionInterface;
2122
use Windwalker\DI\Definition\ObjectBuilderDefinition;
@@ -176,7 +177,7 @@ public function newInstanceByClassName(
176177
*
177178
* @param ReflectionFunctionAbstract $method Method for which to build the argument array.
178179
* @param array $args The default args if class hint not provided.
179-
* @param int $options
180+
* @param DIOptions|int $options
180181
*
181182
* @return array Array of arguments to pass to the method.
182183
*
@@ -242,12 +243,13 @@ protected function getMethodArgs(
242243
$value = null;
243244
$value = &$this->resolveParameterAttributes($value, $param);
244245

245-
if ($value === null) {
246+
if ($value !== null) {
247+
$value = &$this->resolveParameterValue($value);
248+
} else {
249+
// resolveParameterValue() will be called in resolveParameterDependency()
246250
$value = &$this->resolveParameterDependency($param, $args, $options);
247251
}
248252

249-
$value = &$this->resolveParameterValue($value);
250-
251253
if ($value !== null) {
252254
$methodArgs[$dependencyVarName] = &$value;
253255

@@ -335,33 +337,66 @@ public function &resolveParameterDependency(
335337
if (array_key_exists($dependencyClassName, $args)) {
336338
// If an arg provided, use it.
337339
return $args[$dependencyClassName];
338-
} elseif ($this->container->has($dependencyClassName)) {
339-
// If the dependency class name is registered with this container or a parent, use it.
340-
$depObject = $this->container->get($dependencyClassName);
341-
} elseif (
342-
$autowire
343-
&& !$dependency->isAbstract()
344-
&& !$dependency->isInterface()
345-
&& !$dependency->isTrait()
346-
&& $dependency->isInstantiable()
347-
&& !$param->allowsNull()
340+
}
341+
342+
$create = function &() use (
343+
$options,
344+
$param,
345+
$dependency,
346+
$autowire,
347+
$dependencyClassName
348348
) {
349-
// Otherwise we create this object recursive
349+
$depObject = null;
350+
351+
if ($this->container->has($dependencyClassName)) {
352+
// If the dependency class name is registered with this container or a parent, use it.
353+
$depObject = $this->container->get($dependencyClassName);
354+
} elseif (
355+
$autowire
356+
&& !$dependency->isAbstract()
357+
&& !$dependency->isInterface()
358+
&& !$dependency->isTrait()
359+
&& $dependency->isInstantiable()
360+
&& !$param->allowsNull()
361+
) {
362+
// Otherwise we create this object recursive
363+
364+
// Find child args if set
365+
if (isset($args[$dependencyClassName]) && is_array($args[$dependencyClassName])) {
366+
$childArgs = $args[$dependencyClassName];
367+
} else {
368+
$childArgs = [];
369+
}
350370

351-
// Find child args if set
352-
if (isset($args[$dependencyClassName]) && is_array($args[$dependencyClassName])) {
353-
$childArgs = $args[$dependencyClassName];
354-
} else {
355-
$childArgs = [];
371+
$dependencyClassAlias = $this->container->resolveAlias($dependencyClassName);
372+
373+
$depObject = $this->newInstance($dependencyClassAlias, $childArgs, $options);
356374
}
357375

358-
$dependencyClassAlias = $this->container->resolveAlias($dependencyClassName);
376+
return $depObject;
377+
};
378+
379+
$ref = new ReflectionClass($dependencyClassName);
380+
381+
if (
382+
($this->canLazy($ref, $options) || $this->canLazy($param, $options))
383+
&& !static::isInternal($ref)
384+
) {
385+
$depObject = $ref->newLazyProxy(
386+
function () use ($create) {
387+
$value = $create();
388+
389+
return $this->resolveParameterValue($value);
390+
}
391+
);
359392

360-
$depObject = $this->newInstance($dependencyClassAlias, $childArgs, $options);
393+
return $depObject;
361394
}
362395

396+
$depObject = &$create();
397+
363398
if ($depObject instanceof $dependencyClassName) {
364-
return $depObject;
399+
return $this->resolveParameterValue($depObject);
365400
}
366401
}
367402

@@ -434,10 +469,10 @@ public function &resolveParameterAttributes(
434469
/**
435470
* Execute a callable with dependencies.
436471
*
437-
* @param mixed $callable Do not use callable hint, will check callable after context bounded.
438-
* @param array $args
472+
* @param mixed $callable Do not use callable hint, will check callable after context bounded.
473+
* @param array $args
439474
* @param object|null $context
440-
* @param int $options
475+
* @param int $options
441476
*
442477
* @return mixed
443478
*
@@ -490,4 +525,58 @@ public function mergeOptionsDefaults(int|DIOptions $options): DIOptions
490525
{
491526
return DIOptions::wrap($options)->withDefaults($this->container->getOptions());
492527
}
528+
529+
public function canLazy(
530+
string|ReflectionClass|ReflectionParameter|\ReflectionProperty $classOrRef,
531+
DIOptions $options
532+
): bool {
533+
$ref = is_string($classOrRef) ? new \ReflectionClass($classOrRef) : $classOrRef;
534+
$lazy = $options->lazy ?? $this->container->options->lazy;
535+
536+
return ($lazy || static::getAttributeFromReflection($ref, Lazy::class));
537+
}
538+
539+
public static function isInternal(string|\Reflector $class): bool
540+
{
541+
$ref = is_string($class) ? new \ReflectionClass($class) : $class;
542+
543+
if (!$ref instanceof ReflectionClass) {
544+
return false;
545+
}
546+
547+
while (!$ref->isInternal()) {
548+
$ref = $ref->getParentClass();
549+
550+
if (!$ref) {
551+
return false;
552+
}
553+
554+
if ($ref->isInternal()) {
555+
return true;
556+
}
557+
}
558+
559+
return true;
560+
}
561+
562+
/**
563+
* @template T
564+
*
565+
* @param ReflectionClass|ReflectionParameter|\ReflectionProperty $ref
566+
* @param class-string<T> $attr
567+
*
568+
* @return object|null
569+
*/
570+
public static function getAttributeFromReflection(
571+
ReflectionClass|ReflectionParameter|\ReflectionProperty $ref,
572+
string $attr
573+
): ?object {
574+
$attrs = $ref->getAttributes($attr, \ReflectionAttribute::IS_INSTANCEOF);
575+
576+
if ($attrs === []) {
577+
return null;
578+
}
579+
580+
return $attrs[0]->newInstance();
581+
}
493582
}

0 commit comments

Comments
 (0)