Skip to content

Commit

Permalink
Fix #52: Add optional reflection caching
Browse files Browse the repository at this point in the history
  • Loading branch information
xepozz committed Aug 26, 2023
1 parent e3968b3 commit af53351
Show file tree
Hide file tree
Showing 8 changed files with 151 additions and 4 deletions.
4 changes: 2 additions & 2 deletions CHANGELOG.md
@@ -1,8 +1,8 @@
# Yii Injector Change Log

## 1.1.1 under development
## 1.2.0 under development

- no changes in this release.
- New #52: Add optional reflection caching (@xepozz, @vjik)

## 1.1.0 July 18, 2022

Expand Down
18 changes: 17 additions & 1 deletion README.md
Expand Up @@ -39,8 +39,9 @@ implementation based on autowiring and
The package could be installed with composer:

```shell
composer require yiisoft/injector --prefer-dist
composer require yiisoft/injector
```

## About

Injector can automatically resolve and inject dependencies when calling
Expand All @@ -67,6 +68,8 @@ arguments of other types.
## Basic Example

```php
use Yiisoft\Injector\Injector;

// A function to call
$fn = function (\App\Foo $a, \App\Bar $b, int $c) { /* ... */ };

Expand All @@ -85,6 +88,19 @@ $result = $injector->invoke($fn, [
]);
```

### Caching reflection objects

Enable caching of reflection objects to improve performance by calling `withCacheReflections(true)`:

```php
use Yiisoft\Injector\Injector;

$injector = (new Injector($container))
->withCacheReflections(true);
```

By default, caching is disabled.

## Documentation

- [English](docs/en/README.md)
Expand Down
16 changes: 16 additions & 0 deletions docs/en/README.md
Expand Up @@ -91,10 +91,26 @@ Additionally:
* Unused named arguments are ignored.
* If parameters are accepting arguments by reference, arguments should be explicitly passed by reference:
```php
use Yiisoft\Injector\Injector;

$foo = 1;
$increment = function (int &$value) {
++$value;
};
(new Injector($container))->invoke($increment, ['value' => &$foo]);
echo $foo; // 2
```

## Reflection caching

`Injector` uses `Reflection API` to analyze class signatures. By default, it creates new `Reflection` objects each time.
Call `withCacheReflections(true)` to prevent this behavior and cache reflection objects.
It is recommended to enable caching in production environment, because it improves performance.
If you use async frameworks such as `RoadRunner`, `AMPHP` or `Swoole` don't forget to reset injector state.

```php
use Yiisoft\Injector\Injector;

$injector = (new Injector($container))
->withCacheReflections(true);
```
16 changes: 16 additions & 0 deletions docs/ru/README.md
Expand Up @@ -94,10 +94,26 @@ $result = $stringFormatter->getFormattedString();
* Если параметры функции принимают аргументы по ссылке, то для правильной передачи переменной по ссылке, вам следует
указывать аргументы также по ссылке:
```php
use Yiisoft\Injector\Injector;

$foo = 1;
$increment = static function (int &$value) {
++$value;
};
(new Injector($container))->invoke($increment, ['value' => &$foo]);
echo $foo; // 2
```

## Кэширование рефлексий

`Injector` использует `Reflection API` для разбора и анализа классов. По умолчанию он создаёт новые объекты `Reflection` при каждом вызове.
Чтобы предотвратить такое поведение и кэшировать объекты, вызовите метод `withCacheReflections(true)`.
Рекомендуется включать кэширование в production-окружении, т.к. это позволит увеличить производительность.
Если вы используете асинхронные фреймворки, такие, как `RoadRunner`, `AMPHP` или `Swoole`, не забудьте сбросить состояние `Injector`.

```php
use Yiisoft\Injector\Injector;

$injector = (new Injector($container))
->withCacheReflections(true);
```
34 changes: 33 additions & 1 deletion src/Injector.php
Expand Up @@ -25,12 +25,30 @@
final class Injector
{
private ContainerInterface $container;
private bool $cacheReflections = false;

/**
* @var ReflectionClass[]
* @psalm-var array<class-string,ReflectionClass>
*/
private array $reflectionsCache = [];

public function __construct(ContainerInterface $container)
{
$this->container = $container;
}

/**
* Enable memoization of class reflections for improved performance when resolving the same objects multiple times.
* Note: Enabling this feature may increase memory usage.
*/
public function withCacheReflections(bool $cacheReflections = true): self

Check warning on line 45 in src/Injector.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.1-ubuntu-latest

Escaped Mutant for Mutator "TrueValue": --- Original +++ New @@ @@ * Enable memoization of class reflections for improved performance when resolving the same objects multiple times. * Note: Enabling this feature may increase memory usage. */ - public function withCacheReflections(bool $cacheReflections = true) : self + public function withCacheReflections(bool $cacheReflections = false) : self { $new = clone $this; $new->cacheReflections = $cacheReflections;
{
$new = clone $this;
$new->cacheReflections = $cacheReflections;
return $new;
}

/**
* Invoke a callback with resolving dependencies based on parameter types.
*
Expand Down Expand Up @@ -110,7 +128,7 @@ public function invoke(callable $callable, array $arguments = [])
*/
public function make(string $class, array $arguments = []): object
{
$classReflection = new ReflectionClass($class);
$classReflection = $this->getClassReflection($class);
if (!$classReflection->isInstantiable()) {
throw new \InvalidArgumentException("Class $class is not instantiable.");
}
Expand Down Expand Up @@ -309,4 +327,18 @@ private function resolveObjectParameter(ResolvingState $state, ?string $class, b
}
return false;
}

/**
* @psalm-param class-string $class
*
* @throws ReflectionException
*/
private function getClassReflection(string $class): ReflectionClass
{
if ($this->cacheReflections) {
return $this->reflectionsCache[$class] ??= new ReflectionClass($class);

Check warning on line 339 in src/Injector.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.1-ubuntu-latest

Escaped Mutant for Mutator "AssignCoalesce": --- Original +++ New @@ @@ private function getClassReflection(string $class) : ReflectionClass { if ($this->cacheReflections) { - return $this->reflectionsCache[$class] ??= new ReflectionClass($class); + return $this->reflectionsCache[$class] = new ReflectionClass($class); } return new ReflectionClass($class); } }
}

return new ReflectionClass($class);
}
}
27 changes: 27 additions & 0 deletions tests/Common/InjectorTest.php
Expand Up @@ -15,6 +15,7 @@
use Yiisoft\Injector\Tests\Common\Support\CallStaticObject;
use Yiisoft\Injector\Tests\Common\Support\CallStaticWithSelfObject;
use Yiisoft\Injector\Tests\Common\Support\CallStaticWithStaticObject;
use Yiisoft\Injector\Tests\Common\Support\Circle;
use Yiisoft\Injector\Tests\Common\Support\ColorInterface;
use Yiisoft\Injector\Tests\Common\Support\EngineInterface;
use Yiisoft\Injector\Tests\Common\Support\EngineMarkTwo;
Expand All @@ -27,6 +28,7 @@
use Yiisoft\Injector\Tests\Common\Support\MakeEngineMatherWithParam;
use Yiisoft\Injector\Tests\Common\Support\MakeNoConstructor;
use Yiisoft\Injector\Tests\Common\Support\MakePrivateConstructor;
use Yiisoft\Injector\Tests\Common\Support\Red;
use Yiisoft\Injector\Tests\Common\Support\StaticWithSelfObject;
use Yiisoft\Injector\Tests\Common\Support\StaticWithStaticObject;

Expand Down Expand Up @@ -779,4 +781,29 @@ public function testMakeWithInvalidCustomParam(): void

(new Injector($container))->make(MakeEngineMatherWithParam::class, ['parameter' => 100500]);
}

public function testWithCacheReflectionIsImmutable(): void
{
$injector1 = new Injector($this->getContainer());
$injector2 = $injector1->withCacheReflections();

$this->assertInstanceOf(Injector::class, $injector2);
$this->assertNotSame($injector1, $injector2);
}

public function testReflectionCacheDoesNotAffectInjector(): void
{
$injector = new Injector(
$this->getContainer([ColorInterface::class => new Red()]),
);
$injector = $injector->withCacheReflections();

$object1 = $injector->make(Circle::class, ['name' => 'obj1']);
$object2 = $injector->make(Circle::class, ['name' => 'obj2']);

$this->assertSame('red', $object1->getColor());
$this->assertSame('obj1', $object1->getName());
$this->assertSame('red', $object2->getColor());
$this->assertSame('obj2', $object2->getName());
}
}
27 changes: 27 additions & 0 deletions tests/Common/Support/Circle.php
@@ -0,0 +1,27 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Injector\Tests\Common\Support;

final class Circle
{
private ColorInterface $color;
private ?string $name;

public function __construct(ColorInterface $color, ?string $name = null)
{
$this->color = $color;
$this->name = $name;
}

public function getColor(): string
{
return $this->color->getColor();
}

public function getName(): ?string
{
return $this->name;
}
}
13 changes: 13 additions & 0 deletions tests/Common/Support/Red.php
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Injector\Tests\Common\Support;

final class Red implements ColorInterface
{
public function getColor(): string
{
return 'red';
}
}

0 comments on commit af53351

Please sign in to comment.