Skip to content

Commit

Permalink
Injection lazy loading (#107)
Browse files Browse the repository at this point in the history
Co-authored-by: Alexander Makarov <sam@rmcreative.ru>
  • Loading branch information
vjik and samdark authored May 14, 2024
1 parent 08ceced commit 75d6981
Show file tree
Hide file tree
Showing 12 changed files with 218 additions and 12 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
- Enh #79: Add debug collector for yiisoft/yii-debug (@xepozz)
- Bug #82: Fixed find for layout file due to compatibility with `yiisoft/view` (@rustamwin)
- Enh #99: Make `viewPath` in `ViewRenderer` constructor optional (@vjik)
- Enh #107: Implement lazy loading for injections (@vjik)
- New #102: Add layout specific injections (@vjik)

## 6.0.0 February 16, 2023
Expand Down
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,33 @@ $viewRenderer = $viewRenderer->withAddedInjections($parameters, $tags);
The parameters passed to `render()` method have more priority
and will overwrite the injected content parameters if their names match.

#### Injections lazy loading

You can use lazy loading for injections. Injections will be created by container that implements
`Yiisoft\Yii\View\InjectionContainerInterface`. Out of the box, it is available in `InjectionContainer` that is based on PSR-11 compatible
container.

1. Add injection container to `ViewRenderer` constructor:

```php
use Yiisoft\Yii\View\ViewRenderer;
use Yiisoft\Yii\View\InjectionContainer\InjectionContainer;

/**
* @var Psr\Container\ContainerInterface $container
*/

$viewRenderer = new ViewRenderer(
injectionContainer: new InjectionContainer($container)
)
```

2. Use injection class names instead of instances.

```php
$viewRenderer->withInjections(MyParametersInjection::class, MyTagsInjection::class);
```

### Localize view file

You can set a specific locale that will be used to localize view files with `withLocale()` method:
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
},
"require": {
"php": "^8.0",
"psr/container": "^1.0|^2.0",
"yiisoft/aliases": "^2.0|^3.0",
"yiisoft/csrf": "^1.2|^2.0",
"yiisoft/data-response": "^1.0|^2.0",
Expand Down
3 changes: 3 additions & 0 deletions config/di-web.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@

declare(strict_types=1);

use Yiisoft\Yii\View\InjectionContainer\InjectionContainer;
use Yiisoft\Yii\View\InjectionContainer\InjectionContainerInterface;
use Yiisoft\Yii\View\ViewRenderer;

/** @var array $params */

return [
InjectionContainerInterface::class => InjectionContainer::class,
ViewRenderer::class => [
'__construct()' => [
'viewPath' => $params['yiisoft/yii-view']['viewPath'],
Expand Down
21 changes: 21 additions & 0 deletions src/InjectionContainer/InjectionContainer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Yii\View\InjectionContainer;

use Psr\Container\ContainerInterface;

final class InjectionContainer implements InjectionContainerInterface
{
public function __construct(
private ContainerInterface $container,
) {
}

public function get(string $id): object
{
/** @var object */
return $this->container->get($id);
}
}
10 changes: 10 additions & 0 deletions src/InjectionContainer/InjectionContainerInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Yii\View\InjectionContainer;

interface InjectionContainerInterface
{
public function get(string $id): object;
}
18 changes: 18 additions & 0 deletions src/InjectionContainer/StubInjectionContainer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Yii\View\InjectionContainer;

use RuntimeException;

/**
* @internal
*/
final class StubInjectionContainer implements InjectionContainerInterface
{
public function get(string $id): object
{
throw new RuntimeException('Injections container is not set.');
}
}
56 changes: 47 additions & 9 deletions src/ViewRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
use Yiisoft\View\WebView;
use Yiisoft\Yii\View\Exception\InvalidLinkTagException;
use Yiisoft\Yii\View\Exception\InvalidMetaTagException;
use Yiisoft\Yii\View\InjectionContainer\InjectionContainerInterface;
use Yiisoft\Yii\View\InjectionContainer\StubInjectionContainer;

use function array_key_exists;
use function array_merge;
Expand Down Expand Up @@ -47,23 +49,34 @@ final class ViewRenderer implements ViewContextInterface
private ?string $name = null;
private ?string $locale = null;

/**
* @var object[]|null
*/
private ?array $preparedInjections = null;

private InjectionContainerInterface $injectionContainer;

/**
* @param DataResponseFactoryInterface $responseFactory The data response factory instance.
* @param Aliases $aliases The aliases instance.
* @param WebView $view The web view instance.
* @param string|null $viewPath The full path to the directory of views or its alias.
* @param string|null $layout The layout name (e.g. "layout/main") to be applied to views.
* If null, the layout will not be applied.
* @param object[] $injections The injection instances.
* @param array $injections The injection instances or class names.
*
* @psalm-param array<object|string> $injections
*/
public function __construct(
private DataResponseFactoryInterface $responseFactory,
private Aliases $aliases,
private WebView $view,
?string $viewPath = null,
private ?string $layout = null,
private array $injections = []
private array $injections = [],
?InjectionContainerInterface $injectionContainer = null,
) {
$this->injectionContainer = $injectionContainer ?? new StubInjectionContainer();
$this->setViewPath($viewPath);
}

Expand Down Expand Up @@ -241,24 +254,24 @@ public function withLayout(?string $layout): self
/**
* Return a new instance with the appended specified injections.
*
* @param object ...$injections The injection instances.
* @param object|string ...$injections The injection instances or class names.
*/
public function withAddedInjections(object ...$injections): self
public function withAddedInjections(object|string ...$injections): self
{
$new = clone $this;
$new->injections = array_merge($this->injections, $injections);
$new->setInjections(array_merge($this->injections, $injections));

Check warning on line 262 in src/ViewRenderer.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.2-ubuntu-latest

Escaped Mutant for Mutator "UnwrapArrayMerge": --- Original +++ New @@ @@ public function withAddedInjections(object|string ...$injections) : self { $new = clone $this; - $new->setInjections(array_merge($this->injections, $injections)); + $new->setInjections($this->injections); return $new; } /**

Check warning on line 262 in src/ViewRenderer.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.2-ubuntu-latest

Escaped Mutant for Mutator "UnwrapArrayMerge": --- Original +++ New @@ @@ public function withAddedInjections(object|string ...$injections) : self { $new = clone $this; - $new->setInjections(array_merge($this->injections, $injections)); + $new->setInjections($injections); return $new; } /**

Check warning on line 262 in src/ViewRenderer.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.2-ubuntu-latest

Escaped Mutant for Mutator "MethodCallRemoval": --- Original +++ New @@ @@ public function withAddedInjections(object|string ...$injections) : self { $new = clone $this; - $new->setInjections(array_merge($this->injections, $injections)); + return $new; } /**
return $new;
}

/**
* Returns a new instance with the specified injections.
*
* @param object ...$injections The injection instances.
* @param object|string ...$injections The injection instances or class names.
*/
public function withInjections(object ...$injections): self
public function withInjections(object|string ...$injections): self
{
$new = clone $this;
$new->injections = $injections;
$new->setInjections($injections);
return $new;
}

Expand Down Expand Up @@ -403,7 +416,7 @@ private function getLinkTags(): array
private function getInjections(?string $layout, string $injectionInterface): array
{
$result = [];
foreach ($this->injections as $injection) {
foreach ($this->getPreparedInjections() as $injection) {
if ($injection instanceof $injectionInterface) {
$result[] = $injection;
continue;

Check warning on line 422 in src/ViewRenderer.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.2-ubuntu-latest

Escaped Mutant for Mutator "Continue_": --- Original +++ New @@ @@ foreach ($this->getPreparedInjections() as $injection) { if ($injection instanceof $injectionInterface) { $result[] = $injection; - continue; + break; } if ($injection instanceof LayoutSpecificInjections && $injection->getLayout() === $layout) { foreach ($injection->getInjections() as $layoutInjection) {
Expand Down Expand Up @@ -576,4 +589,29 @@ private function setViewPath(?string $path): void
{
$this->viewPath = $path === null ? null : rtrim($path, '/');
}

private function getPreparedInjections(): array
{
if ($this->preparedInjections !== null) {
return $this->preparedInjections;
}

$this->preparedInjections = [];
foreach ($this->injections as $injection) {
$this->preparedInjections[] = is_object($injection)
? $injection
: $this->injectionContainer->get($injection);
}

return $this->preparedInjections;
}

/**
* @psalm-param array<object|string> $injections
*/
private function setInjections(array $injections): void
{
$this->injections = $injections;
$this->preparedInjections = null;
}
}
17 changes: 17 additions & 0 deletions tests/Support/CharsetInjection.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Yii\View\Tests\Support;

use Yiisoft\Yii\View\MetaTagsInjectionInterface;

final class CharsetInjection implements MetaTagsInjectionInterface
{
public function getMetaTags(): array
{
return [
['charset' => 'utf-8'],
];
}
}
51 changes: 48 additions & 3 deletions tests/ViewRendererTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,16 @@
use stdClass;
use Yiisoft\Aliases\Aliases;
use Yiisoft\DataResponse\DataResponseFactory;
use Yiisoft\Test\Support\Container\SimpleContainer;
use Yiisoft\Test\Support\EventDispatcher\SimpleEventDispatcher;
use Yiisoft\View\WebView;
use Yiisoft\Yii\View\Exception\InvalidLinkTagException;
use Yiisoft\Yii\View\Exception\InvalidMetaTagException;
use Yiisoft\Yii\View\InjectionContainer\InjectionContainer;
use Yiisoft\Yii\View\InjectionContainer\InjectionContainerInterface;
use Yiisoft\Yii\View\LayoutSpecificInjections;
use Yiisoft\Yii\View\MetaTagsInjectionInterface;
use Yiisoft\Yii\View\Tests\Support\CharsetInjection;
use Yiisoft\Yii\View\Tests\Support\FakeCntrl;
use Yiisoft\Yii\View\Tests\Support\FakeController;
use Yiisoft\Yii\View\Tests\Support\InvalidLinkTagInjection;
Expand Down Expand Up @@ -482,6 +486,44 @@ public function testWithoutViewPath(): void
$viewRenderer->getViewPath();
}

public function testLazyLoadingInjection(): void
{
$container = new SimpleContainer([
CharsetInjection::class => new CharsetInjection(),
]);

$renderer = $this
->getRenderer(injectionContainer: new InjectionContainer($container))
->withLayout('@views/simple/layout')
->withControllerName('simple')
->withInjections(CharsetInjection::class);

$response = $renderer->render('view');

$expected = <<<'EOD'
<html>
<head><meta charset="utf-8"></head>
<body>
content</body>
</html>
EOD;

$this->assertEqualStringsIgnoringLineEndings($expected, (string) $response->getBody());
}

public function testLazyLoadingInjectionWithoutContainer(): void
{
$renderer = $this
->getRenderer()
->withLayout('@views/simple/layout')
->withControllerName('simple')
->withInjections(CharsetInjection::class);

$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Injections container is not set.');
$renderer->render('view');
}

public function testLayoutSpecificInjections(): void
{
$renderer = $this
Expand Down Expand Up @@ -525,14 +567,17 @@ public function testImmutability(): void
$this->assertNotSame($original, $original->withInjections());
}

private function getRenderer(string $defaultExtension = 'php'): ViewRenderer
{
private function getRenderer(
string $defaultExtension = 'php',
?InjectionContainerInterface $injectionContainer = null,
): ViewRenderer {
return new ViewRenderer(
new DataResponseFactory(new ResponseFactory(), new StreamFactory()),
new Aliases(['@views' => $this->getViewsDir()]),
(new WebView('@views', new SimpleEventDispatcher()))->withDefaultExtension($defaultExtension),
'@views',
'@views/layout'
'@views/layout',
injectionContainer: $injectionContainer
);
}

Expand Down
20 changes: 20 additions & 0 deletions tests/views/simple/layout.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

/**
* @var Yiisoft\View\WebView $this
* @var string $content
*/

$this->beginPage();
?>
<html>
<head><?php $this->head() ?></head>
<body>
<?php $this->beginBody(); ?>
<?= $content ?>
<?php $this->endBody() ?>
</body>
</html><?php
$this->endPage();
5 changes: 5 additions & 0 deletions tests/views/simple/view.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?php

declare(strict_types=1);

echo 'content';

0 comments on commit 75d6981

Please sign in to comment.