diff --git a/src/Repository/JsonFileRepository.php b/src/Repository/JsonFileRepository.php new file mode 100644 index 0000000..486bbe2 --- /dev/null +++ b/src/Repository/JsonFileRepository.php @@ -0,0 +1,93 @@ +>|null */ + private ?array $rawData = null; + + /** @var array */ + 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 + */ + 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; + } +} diff --git a/tests/JsonFileRepositoryTest.php b/tests/JsonFileRepositoryTest.php new file mode 100644 index 0000000..d307f1a --- /dev/null +++ b/tests/JsonFileRepositoryTest.php @@ -0,0 +1,96 @@ +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); + } +}