Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lazy services #232

Open
wants to merge 27 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
a0fdad8
Decorate definition when it contains lazy
xepozz Aug 1, 2021
61f2bff
Add tests
xepozz Aug 1, 2021
1b4cfae
Normalize definition out of the building process
xepozz Aug 1, 2021
abb2e83
Rework test cases
xepozz Aug 1, 2021
15f8a66
Add class resolver
xepozz Aug 1, 2021
dbed5b3
Apply fixes from StyleCI
samdark Aug 1, 2021
4bdd4bc
Merge branch 'master' into lazy-services
xepozz Aug 7, 2021
592c239
Fix tests
xepozz Aug 7, 2021
e4bd46f
Apply fixes from StyleCI
samdark Aug 7, 2021
2f8c8b5
Merge branch 'master' into lazy-services
xepozz Aug 10, 2021
8212a39
Merge branch 'master' into lazy-services
xepozz Nov 20, 2022
5136d8d
[ci-review] Apply changes from Rector action.
Nov 20, 2022
47dc088
Fix resolving lazy service
xepozz Nov 20, 2022
40ad592
Merge remote-tracking branch 'origin/lazy-services' into lazy-services
xepozz Nov 20, 2022
125231d
Merge branch 'master' into lazy-services
xepozz Dec 2, 2022
51789d0
Merge branch 'master' into lazy-services
xepozz Dec 3, 2022
1d59fbd
Use `friendsofphp/proxy-manager-lts` package
xepozz Dec 3, 2022
95cdd9e
Fix psalm
xepozz Dec 3, 2022
0b0b784
Rework lazy definition
xepozz Dec 3, 2022
20db77b
Merge branch 'master' into lazy-services
xepozz Jul 29, 2023
d1b0c55
Apply Rector changes (CI)
xepozz Jul 29, 2023
feff3b7
Apply fixes from StyleCI
StyleCIBot Jul 29, 2023
5bf1e81
Fix annotation
xepozz Aug 1, 2023
1d04fbc
Add new test case
xepozz Aug 1, 2023
bc7f70e
Fix psalm
xepozz Aug 1, 2023
fedff5e
Merge branch 'master' into lazy-services
xepozz Mar 3, 2024
51fefa2
Suggestion for lazy service (#355)
vjik Apr 16, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,12 @@
"php": "^8.0",
"ext-mbstring": "*",
"psr/container": "^1.1|^2.0",
"yiisoft/definitions": "^3.0"
"yiisoft/definitions": "dev-lazy-definition"
},
"require-dev": {
"league/container": "^4.2",
"maglnet/composer-require-checker": "^4.2",
"friendsofphp/proxy-manager-lts": "^1.0",
"phpbench/phpbench": "^1.2.0",
"phpunit/phpunit": "^9.5",
"rector/rector": "^1.0.0",
Expand All @@ -41,7 +42,8 @@
},
"suggest": {
"yiisoft/injector": "^1.0",
"phpbench/phpbench": "To run benchmarks."
"phpbench/phpbench": "To run benchmarks.",
"friendsofphp/proxy-manager-lts": "Install this package if you want to use lazy loading."
},
"provide": {
"psr/container-implementation": "1.0.0"
Expand Down
1 change: 1 addition & 0 deletions psalm.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
</ignoreFiles>
</projectFiles>
<issueHandlers>
<MixedAssignment errorLevel="suppress" />
<InvalidCatch>
<errorLevel type="suppress">
<referencedClass name="Psr\Container\NotFoundExceptionInterface" />
Expand Down
61 changes: 58 additions & 3 deletions src/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@
use Psr\Container\NotFoundExceptionInterface;
use Throwable;
use Yiisoft\Definitions\ArrayDefinition;
use Yiisoft\Definitions\Contract\DefinitionInterface;
use Yiisoft\Definitions\DefinitionStorage;
use Yiisoft\Definitions\Exception\CircularReferenceException;
use Yiisoft\Definitions\Exception\InvalidConfigException;
use Yiisoft\Definitions\Exception\NotInstantiableException;
use Yiisoft\Definitions\Helpers\DefinitionValidator;
use Yiisoft\Definitions\LazyDefinition;
use Yiisoft\Di\Helpers\DefinitionNormalizer;
use Yiisoft\Di\Helpers\DefinitionParser;
use Yiisoft\Di\Reference\TagReference;
Expand All @@ -30,12 +32,15 @@

/**
* Container implements a [dependency injection](https://en.wikipedia.org/wiki/Dependency_injection) container.
*
* @psalm-import-type MethodOrPropertyItem from ArrayDefinition
*/
final class Container implements ContainerInterface
{
private const META_TAGS = 'tags';
private const META_RESET = 'reset';
private const ALLOWED_META = [self::META_TAGS, self::META_RESET];
private const META_LAZY = 'lazy';
private const ALLOWED_META = [self::META_TAGS, self::META_RESET, self::META_LAZY];

/**
* @var DefinitionStorage Storage of object definitions.
Expand Down Expand Up @@ -199,14 +204,13 @@
*/
private function addDefinition(string $id, mixed $definition): void
{
/** @var mixed $definition */
[$definition, $meta] = DefinitionParser::parse($definition);
if ($this->validate) {
$this->validateDefinition($definition, $id);
$this->validateMeta($meta);
}
/**
* @psalm-var array{reset?:Closure,tags?:string[]} $meta
* @psalm-var array{reset?:Closure,lazy?:bool,tags?:string[]} $meta
*/

if (isset($meta[self::META_TAGS])) {
Expand All @@ -215,6 +219,9 @@
if (isset($meta[self::META_RESET])) {
$this->setDefinitionResetter($id, $meta[self::META_RESET]);
}
if (isset($meta[self::META_LAZY]) && $meta[self::META_LAZY] === true) {
$definition = $this->decorateLazy($id, $definition);
}

unset($this->instances[$id]);
$this->addDefinitionToStorage($id, $definition);
Expand Down Expand Up @@ -276,7 +283,7 @@

$this->delegates->attach($delegate);
}
$this->definitions->setDelegateContainer($this->delegates);

Check warning on line 286 in src/Container.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.1-ubuntu-latest

Escaped Mutant for Mutator "MethodCallRemoval": --- Original +++ New @@ @@ } $this->delegates->attach($delegate); } - $this->definitions->setDelegateContainer($this->delegates); + } /** * @param mixed $definition Definition to validate.

Check warning on line 286 in src/Container.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.1-ubuntu-latest

Escaped Mutant for Mutator "MethodCallRemoval": --- Original +++ New @@ @@ } $this->delegates->attach($delegate); } - $this->definitions->setDelegateContainer($this->delegates); + } /** * @param mixed $definition Definition to validate.
}

/**
Expand All @@ -303,7 +310,7 @@

$definition = array_merge(
$class === null ? [] : [ArrayDefinition::CLASS_NAME => $class],
[ArrayDefinition::CONSTRUCTOR => $constructorArguments],

Check warning on line 313 in src/Container.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.1-ubuntu-latest

Escaped Mutant for Mutator "ArrayItemRemoval": --- Original +++ New @@ @@ $methodsAndProperties = $definition['methodsAndProperties']; $definition = array_merge( $class === null ? [] : [ArrayDefinition::CLASS_NAME => $class], - [ArrayDefinition::CONSTRUCTOR => $constructorArguments], + [], // extract only value from parsed definition method array_map(fn(array $data): mixed => $data[2], $methodsAndProperties) );

Check warning on line 313 in src/Container.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.1-ubuntu-latest

Escaped Mutant for Mutator "ArrayItemRemoval": --- Original +++ New @@ @@ $methodsAndProperties = $definition['methodsAndProperties']; $definition = array_merge( $class === null ? [] : [ArrayDefinition::CLASS_NAME => $class], - [ArrayDefinition::CONSTRUCTOR => $constructorArguments], + [], // extract only value from parsed definition method array_map(fn(array $data): mixed => $data[2], $methodsAndProperties) );
// extract only value from parsed definition method
array_map(fn (array $data): mixed => $data[2], $methodsAndProperties),
);
Expand Down Expand Up @@ -489,7 +496,7 @@
);
}

$this->building[$id] = 1;

Check warning on line 499 in src/Container.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.1-ubuntu-latest

Escaped Mutant for Mutator "IncrementInteger": --- Original +++ New @@ @@ } throw new CircularReferenceException(sprintf('Circular reference to "%s" detected while building: %s.', $id, implode(', ', array_keys($this->building)))); } - $this->building[$id] = 1; + $this->building[$id] = 2; try { /** @var mixed $object */ $object = $this->buildInternal($id);

Check warning on line 499 in src/Container.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.1-ubuntu-latest

Escaped Mutant for Mutator "IncrementInteger": --- Original +++ New @@ @@ } throw new CircularReferenceException(sprintf('Circular reference to "%s" detected while building: %s.', $id, implode(', ', array_keys($this->building)))); } - $this->building[$id] = 1; + $this->building[$id] = 2; try { /** @var mixed $object */ $object = $this->buildInternal($id);
try {
/** @var mixed $object */
$object = $this->buildInternal($id);
Expand Down Expand Up @@ -621,4 +628,52 @@

return $providerInstance;
}

private function decorateLazy(string $id, mixed $definition): DefinitionInterface
{
$class = class_exists($id) || interface_exists($id) ? $id : null;

if (is_array($definition) && isset($definition[DefinitionParser::IS_PREPARED_ARRAY_DEFINITION_DATA])) {
/**
* @psalm-var array{
* class: class-string|null,
* '__construct()': array,
* methodsAndProperties: array<string, MethodOrPropertyItem>
* } $definition
*/
if (empty($class)) {
$class = $definition[ArrayDefinition::CLASS_NAME];
}
$this->checkClassOnNullForLazyService($class, $id, $definition);
$preparedDefinition = ArrayDefinition::fromPreparedData(
$definition[ArrayDefinition::CLASS_NAME] ?? $class,
$definition[ArrayDefinition::CONSTRUCTOR],
$definition['methodsAndProperties'],
);
} else {
$this->checkClassOnNullForLazyService($class, $id, $definition);
$preparedDefinition = $definition;
}



return new LazyDefinition($preparedDefinition, $class);
}

/**
* @psalm-param class-string|null $class
* @psalm-assert class-string $class
*/
private function checkClassOnNullForLazyService(?string $class, string $id, mixed $definition): void
{
if (empty($class)) {
throw new InvalidConfigException(
sprintf(
'Invalid definition: lazy services are only available with array definitions or references. Got type "%s" for definition ID: "%s"',
get_debug_type($definition),
$id,
)
);
}
}
}
9 changes: 6 additions & 3 deletions tests/Unit/Helpers/DefinitionParserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,23 @@ public function testParseCallableDefinition(): void
$definition = [
'definition' => $fn,
'tags' => ['one', 'two'],
'lazy' => true,
];
[$definition, $meta] = DefinitionParser::parse($definition);
$this->assertSame($fn, $definition);
$this->assertSame(['tags' => ['one', 'two']], $meta);
$this->assertSame(['tags' => ['one', 'two'], 'lazy' => true], $meta);
}

public function testParseArrayCallableDefinition(): void
{
$definition = [
'definition' => [StaticFactory::class, 'create'],
'tags' => ['one', 'two'],
'lazy' => true,
];
[$definition, $meta] = DefinitionParser::parse($definition);
$this->assertSame([StaticFactory::class, 'create'], $definition);
$this->assertSame(['tags' => ['one', 'two']], $meta);
$this->assertSame(['tags' => ['one', 'two'], 'lazy' => true], $meta);
}

public function testParseArrayDefinition(): void
Expand All @@ -40,6 +42,7 @@ public function testParseArrayDefinition(): void
'class' => EngineMarkOne::class,
'__construct()' => [42],
'tags' => ['one', 'two'],
'lazy' => true,
];
[$definition, $meta] = DefinitionParser::parse($definition);
$this->assertSame([
Expand All @@ -48,6 +51,6 @@ public function testParseArrayDefinition(): void
'methodsAndProperties' => [],
DefinitionParser::IS_PREPARED_ARRAY_DEFINITION_DATA => true,
], $definition);
$this->assertSame(['tags' => ['one', 'two']], $meta);
$this->assertSame(['tags' => ['one', 'two'], 'lazy' => true], $meta);
}
}
120 changes: 120 additions & 0 deletions tests/Unit/LazyServiceContainerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Di\Tests\Unit;

use PHPUnit\Framework\TestCase;
use ProxyManager\Factory\LazyLoadingValueHolderFactory;
use ProxyManager\Proxy\LazyLoadingInterface;
use Yiisoft\Di\Container;
use Yiisoft\Di\ContainerConfig;
use Yiisoft\Di\Tests\Support\EngineMarkOne;

class LazyServiceContainerTest extends TestCase
{
protected function setUp(): void
{
if (!class_exists(LazyLoadingValueHolderFactory::class)) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's always false in tests

$this->markTestSkipped(
'You should install `friendsofphp/proxy-manager-lts` if you want to use lazy services.'
);
}
}

public function testIsTheSameObject(): void
{
$class = EngineMarkOne::class;
$number = 55;

$config = ContainerConfig::create()
->withDefinitions([
EngineMarkOne::class => [
'class' => $class,
'setNumber()' => [$number],
'lazy' => true,
],
]);
$container = new Container($config);

/* @var EngineMarkOne $object */
$object = $container->get($class);

self::assertInstanceOf(LazyLoadingInterface::class, $object);
self::assertInstanceOf(EngineMarkOne::class, $object);
self::assertFalse($object->isProxyInitialized());
self::assertEquals($number, $object->getNumber());
self::assertTrue($object->isProxyInitialized());

/* @var EngineMarkOne $object */
$object = $container->get($class);

self::assertInstanceOf(LazyLoadingInterface::class, $object);
self::assertInstanceOf(EngineMarkOne::class, $object);
self::assertTrue($object->isProxyInitialized());
}

/**
* @dataProvider lazyDefinitionDataProvider
*/
public function testLazy(array $definitions, string $id): void
{
$config = ContainerConfig::create()
->withDefinitions($definitions);
$container = new Container($config);

$object = $container->get($id);

self::assertInstanceOf(LazyLoadingInterface::class, $object);
}

public function lazyDefinitionDataProvider(): array
{
return [
'class as definition name' => [
[
EngineMarkOne::class => [
'lazy' => true,
],
],
EngineMarkOne::class,
],
'class as key' => [
[
EngineMarkOne::class => [
'class' => EngineMarkOne::class,
'lazy' => true,
],
],
EngineMarkOne::class,
],
'alias as key' => [
[
'mark_one' => [
'class' => EngineMarkOne::class,
'lazy' => true,
],
],
'mark_one',
],
'dedicated array definition' => [
[
EngineMarkOne::class => [
'definition' => ['class' => EngineMarkOne::class],
'lazy' => true,
],
],
EngineMarkOne::class,
],
'dedicated callback definition' => [
[
EngineMarkOne::class => [
'definition' => fn () => new EngineMarkOne(),
'lazy' => true,
],
],
EngineMarkOne::class,
],
];
}
}
Loading