feat: add JsonFileRepository with lazy-loading
This commit is contained in:
parent
1951ce9c7d
commit
a326084346
93
src/Repository/JsonFileRepository.php
Normal file
93
src/Repository/JsonFileRepository.php
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FlagPole\Repository;
|
||||
|
||||
use FlagPole\Flag;
|
||||
use FlagPole\FlagHydrator;
|
||||
use FlagPole\Rule;
|
||||
|
||||
/**
|
||||
* Loads flags from a JSON file.
|
||||
*/
|
||||
final class JsonFileRepository implements FlagRepositoryInterface
|
||||
{
|
||||
/** @var array<string, array<string, mixed>>|null */
|
||||
private ?array $rawData = null;
|
||||
|
||||
/** @var array<string, Flag> */
|
||||
private array $flags = [];
|
||||
|
||||
private ?FlagHydrator $hydrator = null;
|
||||
|
||||
public function __construct(
|
||||
private readonly string $filePath
|
||||
) {
|
||||
}
|
||||
|
||||
public function get(string $name): ?Flag
|
||||
{
|
||||
$this->ensureLoaded();
|
||||
|
||||
if (isset($this->flags[$name])) {
|
||||
return $this->flags[$name];
|
||||
}
|
||||
|
||||
if (isset($this->rawData[$name])) {
|
||||
$this->hydrator ??= new FlagHydrator();
|
||||
$this->flags[$name] = $this->hydrator->hydrate($name, $this->rawData[$name]);
|
||||
return $this->flags[$name];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<string, Flag>
|
||||
*/
|
||||
public function all(): iterable
|
||||
{
|
||||
$this->ensureLoaded();
|
||||
|
||||
if ($this->rawData !== null) {
|
||||
foreach (array_keys($this->rawData) as $name) {
|
||||
$this->get((string)$name);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->flags;
|
||||
}
|
||||
|
||||
private function ensureLoaded(): void
|
||||
{
|
||||
if ($this->rawData !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!file_exists($this->filePath)) {
|
||||
$this->rawData = [];
|
||||
return;
|
||||
}
|
||||
|
||||
$content = file_get_contents($this->filePath);
|
||||
if ($content === false) {
|
||||
$this->rawData = [];
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$data = json_decode($content, true, 512, JSON_THROW_ON_ERROR);
|
||||
} catch (\JsonException $e) {
|
||||
$this->rawData = [];
|
||||
return;
|
||||
}
|
||||
|
||||
if (!is_array($data)) {
|
||||
$this->rawData = [];
|
||||
return;
|
||||
}
|
||||
|
||||
$this->rawData = $data;
|
||||
}
|
||||
}
|
||||
96
tests/JsonFileRepositoryTest.php
Normal file
96
tests/JsonFileRepositoryTest.php
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FlagPole\Tests;
|
||||
|
||||
use FlagPole\Repository\JsonFileRepository;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class JsonFileRepositoryTest extends TestCase
|
||||
{
|
||||
private string $tempFile;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->tempFile = tempnam(sys_get_temp_dir(), 'flagpole_test_');
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
if (file_exists($this->tempFile)) {
|
||||
unlink($this->tempFile);
|
||||
}
|
||||
}
|
||||
|
||||
public function testLoadsFlagsFromJson(): void
|
||||
{
|
||||
$json = json_encode([
|
||||
'test-flag' => [
|
||||
'enabled' => true,
|
||||
'rules' => [
|
||||
['attribute' => 'plan', 'operator' => 'eq', 'value' => 'pro']
|
||||
]
|
||||
]
|
||||
]);
|
||||
file_put_contents($this->tempFile, $json);
|
||||
|
||||
$repo = new JsonFileRepository($this->tempFile);
|
||||
$flag = $repo->get('test-flag');
|
||||
|
||||
$this->assertNotNull($flag);
|
||||
$this->assertEquals('test-flag', $flag->name);
|
||||
$this->assertTrue($flag->enabled);
|
||||
$this->assertCount(1, $flag->rules);
|
||||
$this->assertEquals('plan', $flag->rules[0]->attribute);
|
||||
}
|
||||
|
||||
public function testHandlesMissingFile(): void
|
||||
{
|
||||
$repo = new JsonFileRepository('/non/existent/file.json');
|
||||
$this->assertNull($repo->get('any'));
|
||||
$this->assertEmpty($repo->all());
|
||||
}
|
||||
|
||||
public function testHandlesInvalidJson(): void
|
||||
{
|
||||
file_put_contents($this->tempFile, 'invalid json');
|
||||
$repo = new JsonFileRepository($this->tempFile);
|
||||
$this->assertNull($repo->get('any'));
|
||||
}
|
||||
|
||||
public function testLazyHydration(): void
|
||||
{
|
||||
$json = json_encode([
|
||||
'flag-1' => ['enabled' => true],
|
||||
'flag-2' => ['enabled' => false],
|
||||
]);
|
||||
file_put_contents($this->tempFile, $json);
|
||||
|
||||
$repo = new JsonFileRepository($this->tempFile);
|
||||
|
||||
// Access internal property via reflection to verify it's NOT hydrated yet
|
||||
$reflection = new \ReflectionClass($repo);
|
||||
$flagsProp = $reflection->getProperty('flags');
|
||||
$flagsProp->setAccessible(true);
|
||||
|
||||
$this->assertEmpty($flagsProp->getValue($repo));
|
||||
|
||||
// Hydrate flag-1
|
||||
$flag1 = $repo->get('flag-1');
|
||||
$this->assertNotNull($flag1);
|
||||
$this->assertTrue($flag1->enabled);
|
||||
|
||||
$hydratedFlags = $flagsProp->getValue($repo);
|
||||
$this->assertArrayHasKey('flag-1', $hydratedFlags);
|
||||
$this->assertArrayNotHasKey('flag-2', $hydratedFlags);
|
||||
|
||||
// Hydrate all
|
||||
$all = $repo->all();
|
||||
$this->assertCount(2, $all);
|
||||
|
||||
$hydratedFlagsAfterAll = $flagsProp->getValue($repo);
|
||||
$this->assertArrayHasKey('flag-1', $hydratedFlagsAfterAll);
|
||||
$this->assertArrayHasKey('flag-2', $hydratedFlagsAfterAll);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue