Skip to content

Commit

Permalink
feat: introduce method to warm the cache up
Browse files Browse the repository at this point in the history
This new method can be used for instance in a pipeline during the build
and deployment of the application.

The cache has to be registered first, otherwise the warmup will end up
being useless.

```php
$cache = new \CuyZ\Valinor\Cache\FileSystemCache('path/to/cache-dir');

$mapperBuilder = (new \CuyZ\Valinor\MapperBuilder())->withCache($cache);

// During the build:
$mapperBuilder->warmup(SomeClass::class, SomeOtherClass::class);

// In the application:
$mapper->mapper()->map(SomeClass::class, [/* … */]);
```

Co-authored-by: Romain Canon <[email protected]>
  • Loading branch information
boesing and romm authored May 23, 2022
1 parent 105eef4 commit ccf09fd
Show file tree
Hide file tree
Showing 8 changed files with 281 additions and 4 deletions.
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -920,6 +920,26 @@ if ($isApplicationInDevelopmentEnvironment) {
->map(SomeClass::class, [/* … */]);
```

### Warming up cache

The cache can be warmed up, for instance in a pipeline during the build and
deployment of the application.

> **Note** The cache has to be registered first, otherwise the warmup will end
> up being useless.
```php
$cache = new \CuyZ\Valinor\Cache\FileSystemCache('path/to/cache-dir');

$mapperBuilder = (new \CuyZ\Valinor\MapperBuilder())->withCache($cache);

// During the build:
$mapperBuilder->warmup(SomeClass::class, SomeOtherClass::class);

// In the application:
$mapper->mapper()->map(SomeClass::class, [/* … */]);
```

## Static analysis

To help static analysis of a codebase using this library, an extension for
Expand Down
21 changes: 21 additions & 0 deletions src/Cache/Exception/InvalidSignatureToWarmup.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace CuyZ\Valinor\Cache\Exception;

use CuyZ\Valinor\Type\Parser\Exception\InvalidType;
use RuntimeException;

/** @internal */
final class InvalidSignatureToWarmup extends RuntimeException
{
public function __construct(string $signature, InvalidType $exception)
{
parent::__construct(
"Cannot warm up invalid signature `$signature`: {$exception->getMessage()}",
1653330261,
$exception
);
}
}
79 changes: 79 additions & 0 deletions src/Cache/Warmup/RecursiveCacheWarmupService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php

declare(strict_types=1);

namespace CuyZ\Valinor\Cache\Warmup;

use CuyZ\Valinor\Cache\Exception\InvalidSignatureToWarmup;
use CuyZ\Valinor\Definition\Repository\ClassDefinitionRepository;
use CuyZ\Valinor\Type\CompositeType;
use CuyZ\Valinor\Type\Parser\Exception\InvalidType;
use CuyZ\Valinor\Type\Parser\TypeParser;
use CuyZ\Valinor\Type\Type;
use CuyZ\Valinor\Type\Types\ClassType;

use function in_array;

/** @internal */
final class RecursiveCacheWarmupService
{
private TypeParser $parser;

private ClassDefinitionRepository $classDefinitionRepository;

/** @var list<class-string> */
private array $classesWarmedUp = [];

public function __construct(TypeParser $parser, ClassDefinitionRepository $classDefinitionRepository)
{
$this->parser = $parser;
$this->classDefinitionRepository = $classDefinitionRepository;
}

public function warmup(string ...$signatures): void
{
foreach ($signatures as $signature) {
try {
$this->warmupType($this->parser->parse($signature));
} catch (InvalidType $exception) {
throw new InvalidSignatureToWarmup($signature, $exception);
}
}
}

private function warmupType(Type $type): void
{
if ($type instanceof ClassType) {
$this->warmupClassType($type);
}

if ($type instanceof CompositeType) {
foreach ($type->traverse() as $subType) {
$this->warmupType($subType);
}
}
}

private function warmupClassType(ClassType $type): void
{
if (in_array($type->className(), $this->classesWarmedUp, true)) {
return;
}

$this->classesWarmedUp[] = $type->className();

$classDefinition = $this->classDefinitionRepository->for($type);

foreach ($classDefinition->properties() as $property) {
$this->warmupType($property->type());
}

foreach ($classDefinition->methods() as $method) {
$this->warmupType($method->returnType());

foreach ($method->parameters() as $parameter) {
$this->warmupType($parameter->type());
}
}
}
}
11 changes: 11 additions & 0 deletions src/Library/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use CuyZ\Valinor\Cache\ChainCache;
use CuyZ\Valinor\Cache\RuntimeCache;
use CuyZ\Valinor\Cache\VersionedCache;
use CuyZ\Valinor\Cache\Warmup\RecursiveCacheWarmupService;
use CuyZ\Valinor\Definition\FunctionsContainer;
use CuyZ\Valinor\Definition\Repository\AttributesRepository;
use CuyZ\Valinor\Definition\Repository\Cache\CacheClassDefinitionRepository;
Expand Down Expand Up @@ -201,6 +202,11 @@ public function __construct(Settings $settings)

TemplateParser::class => fn () => new BasicTemplateParser(),

RecursiveCacheWarmupService::class => fn () => new RecursiveCacheWarmupService(
$this->get(TypeParser::class),
$this->get(ClassDefinitionRepository::class),
),

CacheInterface::class => function () use ($settings) {
$cache = new RuntimeCache();

Expand All @@ -218,6 +224,11 @@ public function treeMapper(): TreeMapper
return $this->get(TreeMapper::class);
}

public function cacheWarmupService(): RecursiveCacheWarmupService
{
return $this->get(RecursiveCacheWarmupService::class);
}

/**
* @template T of object
* @param class-string<T> $name
Expand Down
18 changes: 17 additions & 1 deletion src/MapperBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,17 @@ public function withCacheDir(string $cacheDir): self
return $this->withCache(new FileSystemCache($cacheDir));
}

/**
* Warms up the injected cache implementation with the provided class names.
*
* By passing a class which contains recursive objects, every nested object
* will be cached as well.
*/
public function warmup(string ...$signatures): void
{
$this->container()->cacheWarmupService()->warmup(...$signatures);
}

/**
* @deprecated It is not advised to use DoctrineAnnotation when using
* PHP >= 8, you should use built-in PHP attributes instead.
Expand All @@ -272,12 +283,17 @@ public function bind(callable $callback): self

public function mapper(): TreeMapper
{
return ($this->container ??= new Container($this->settings))->treeMapper();
return $this->container()->treeMapper();
}

public function __clone()
{
$this->settings = clone $this->settings;
unset($this->container);
}

private function container(): Container
{
return ($this->container ??= new Container($this->settings));
}
}
15 changes: 15 additions & 0 deletions tests/Fake/Cache/FakeCache.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

use Psr\SimpleCache\CacheInterface;

use function count;

/**
* @implements CacheInterface<mixed>
*/
Expand All @@ -14,6 +16,8 @@ final class FakeCache implements CacheInterface
/** @var mixed[] */
private array $entries = [];

private int $timesSetWasCalled = 0;

/** @var array<string, int> */
private array $timesEntryWasSet = [];

Expand All @@ -30,6 +34,16 @@ public function timesEntryWasFetched(string $key): int
return $this->timesEntryWasFetched[$key] ?? 0;
}

public function timeSetWasCalled(): int
{
return $this->timesSetWasCalled;
}

public function countEntries(): int
{
return count($this->entries);
}

public function get($key, $default = null)
{
$this->timesEntryWasFetched[$key] ??= 0;
Expand All @@ -42,6 +56,7 @@ public function set($key, $value, $ttl = null): bool
{
$this->entries[$key] = $value;

$this->timesSetWasCalled++;
$this->timesEntryWasSet[$key] ??= 0;
$this->timesEntryWasSet[$key]++;

Expand Down
4 changes: 1 addition & 3 deletions tests/Fixture/Object/StringableObject.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@

namespace CuyZ\Valinor\Tests\Fixture\Object;

/**
* @PHP8.0 implement Stringable
*/
// @PHP8.0 implement Stringable
final class StringableObject
{
private string $value;
Expand Down
117 changes: 117 additions & 0 deletions tests/Integration/Cache/CacheWarmupTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
<?php

declare(strict_types=1);

namespace CuyZ\Valinor\Tests\Integration\Cache;

use CuyZ\Valinor\Cache\Exception\InvalidSignatureToWarmup;
use CuyZ\Valinor\MapperBuilder;
use CuyZ\Valinor\Tests\Fake\Cache\FakeCache;
use CuyZ\Valinor\Tests\Integration\IntegrationTest;
use DateTimeInterface;

final class CacheWarmupTest extends IntegrationTest
{
private FakeCache $cache;

private MapperBuilder $mapper;

protected function setUp(): void
{
parent::setUp();

$this->cache = new FakeCache();
$this->mapper = (new MapperBuilder())->withCache($this->cache);
}

public function test_will_warmup_type_parser_cache(): void
{
$this->mapper->warmup(ObjectToWarmup::class);
$this->mapper->warmup(ObjectToWarmup::class, SomeObjectJ::class);

self::assertSame(11, $this->cache->countEntries());
self::assertSame(11, $this->cache->timeSetWasCalled());
}

public function test_warmup_invalid_signature_throws_exception(): void
{
$this->expectException(InvalidSignatureToWarmup::class);
$this->expectExceptionCode(1653330261);
$this->expectExceptionMessage('Cannot warm up invalid signature `SomeInvalidClass`: Cannot parse unknown symbol `SomeInvalidClass`.');

$this->mapper->warmup('SomeInvalidClass');
}
}

final class ObjectToWarmup
{
public string $string;

public SomeObjectA $objectA;

/** @var array<string> */
public array $arrayOfStrings;

/** @var array<SomeObjectB> */
public array $arrayOfObjects;

/** @var list<SomeObjectC> */
public array $listOfObjects;

/** @var iterable<SomeObjectD> */
public iterable $iterableOfObjects;

/** @var array{foo: string, object: SomeObjectE} */
public array $shapedArrayContainingObject;

/** @var string|SomeObjectF */
public $unionContainingObject; // @PHP8.0 Native union

/** @var SomeObjectG&DateTimeInterface */
public object $intersectionOfObjects;

public static function someMethod(string $string, SomeObjectH $object): SomeObjectI
{
return new SomeObjectI();
}
}

class SomeObjectA
{
}

class SomeObjectB
{
}

class SomeObjectC
{
}

class SomeObjectD
{
}

class SomeObjectE
{
}

class SomeObjectF
{
}

class SomeObjectG
{
}

class SomeObjectH
{
}

class SomeObjectI
{
}

class SomeObjectJ
{
}

0 comments on commit ccf09fd

Please sign in to comment.