diff --git a/src/Contracts/Driver.php b/src/Contracts/Driver.php index 8b30a1c..28b730b 100644 --- a/src/Contracts/Driver.php +++ b/src/Contracts/Driver.php @@ -21,4 +21,11 @@ public function get(string $name, $context); * @param (callable(mixed $context): mixed) $resolver */ public function define(string $name, callable $resolver): void; + + /** + * @param mixed $context + * @param mixed $value + * + */ + public function set(string $name, $context, $value): void; } diff --git a/src/Drivers/ArrayDriver.php b/src/Drivers/ArrayDriver.php index f791010..9f68f55 100644 --- a/src/Drivers/ArrayDriver.php +++ b/src/Drivers/ArrayDriver.php @@ -3,6 +3,7 @@ namespace YouCanShop\Foggle\Drivers; use Illuminate\Contracts\Events\Dispatcher; +use stdClass; use YouCanShop\Foggle\Contracts\Driver; class ArrayDriver implements Driver @@ -13,6 +14,12 @@ class ArrayDriver implements Driver /** @var array */ protected array $resolvers; + /** @var array> */ + protected array $resolved = []; + + /** @var stdClass */ + protected stdClass $unknown; + /** * @param Dispatcher $dispatcher * @param array $resolvers @@ -21,6 +28,8 @@ public function __construct(Dispatcher $dispatcher, array $resolvers) { $this->dispatcher = $dispatcher; $this->resolvers = $resolvers; + + $this->unknown = new stdClass; } /** @@ -36,9 +45,46 @@ public function defined(): array */ public function get(string $name, $context) { + $key = foggle()->serialize($context); + + if (isset($this->resolved[$name][$key])) { + return $this->resolved[$name][$key]; + } + + return with( + $this->resolveValue($name, $context), + function ($value) use ($name, $key) { + if ($value === $this->unknown) { + return false; + } + + $this->set($name, $key, $value); + + return $value; + } + ); + } + + /** + * @param mixed $context + * + * @return mixed + */ + protected function resolveValue(string $name, $context) + { + if (!array_key_exists($name, $this->resolvers)) { + return $this->unknown; + } + return $this->resolvers[$name]($context); } + public function set(string $name, $context, $value): void + { + $this->resolved[$name] = $this->resolved[$name] ?? []; + $this->resolved[$name][foggle()->serialize($context)] = $value; + } + /** * @inheritDoc */ diff --git a/src/Drivers/Decorator.php b/src/Drivers/Decorator.php index 14a5ffe..8fca01b 100644 --- a/src/Drivers/Decorator.php +++ b/src/Drivers/Decorator.php @@ -4,9 +4,11 @@ use Closure; use Illuminate\Contracts\Container\Container; +use Illuminate\Support\Collection; use Illuminate\Support\Str; use Symfony\Component\Finder\Finder; use YouCanShop\Foggle\Contracts\Driver; +use YouCanShop\Foggle\Contracts\Foggable; use YouCanShop\Foggle\FeatureInteraction; use YouCanShop\Foggle\Lazily; @@ -15,31 +17,41 @@ */ class Decorator implements Driver { + /** @var Collection */ + protected $cache; + + /** @var string */ private string $name; + /** @var Driver */ private Driver $driver; + /** @var Container */ private Container $container; + /** + * @param string $name + * @param Driver $driver + * @param Container $container + * @param Collection $cache + */ public function __construct( string $name, Driver $driver, - Container $container + Container $container, + Collection $cache ) { $this->name = $name; $this->driver = $driver; $this->container = $container; + $this->cache = $cache; } public function discover(string $namespace = 'App\\Features', ?string $path = null): void { $namespace = Str::finish($namespace, '\\'); - $entries = (new Finder) - ->files() - ->name('*.php') - ->depth(0) - ->in($path ?? base_path('app/Features')); + $entries = (new Finder)->files()->name('*.php')->depth(0)->in($path ?? base_path('app/Features')); collect($entries)->each(fn($file) => $this->define("$namespace{$file->getBasename('.php')}")); } @@ -59,10 +71,7 @@ public function define(string $name, callable $resolver = null): void $this->driver->define($name, function ($context) use ($name, $resolver) { if ($resolver instanceof Lazily) { $resolver = with( - $this->container[$resolver->feature], - fn($i) => method_exists($i, 'resolve') - ? $i->resolve($context) - : $i($context) + $this->container[$resolver->feature], fn($i) => method_exists($i, 'resolve') ? $i->resolve($context) : $i($context) ); } @@ -88,7 +97,45 @@ protected function resolve(string $name, callable $resolver, $context) public function get(string $name, $context) { - return $this->driver->get($name, $context); + $context = $this->parseContext($context); + + $item = $this->cache->whereStrict('context', foggle()->serialize($context))->whereStrict('name', $name)->first(); + + if ($item !== null) { + return $item['value']; + } + + $value = $this->driver->get($name, $context); + $this->cPut($name, $context, $value); + + return $value; + } + + protected function parseContext($context) + { + return $context instanceof Foggable ? $context->foggleId() : $context; + } + + /** + * @param string $name + * @param mixed $context + * @param mixed $value + * + * @return void + */ + protected function cPut(string $name, $context, $value): void + { + $key = foggle()->serialize($context); + + $index = $this->cache->search( + fn($i) => $i['name'] === $name && $i['context'] === $key + ); + + $index === false ? $this->cache[] = ['name' => $name, 'context' => $key, 'value' => $value] : $this->cache[$index] = [ + 'name' => $name, + 'context' => $key, + 'value' => $value, + ]; } public function defined(): array @@ -96,6 +143,14 @@ public function defined(): array return $this->driver->defined(); } + public function set(string $name, $context, $value): void + { + $context = $this->parseContext($context); + $this->driver->set($name, $context, $value); + + $this->cPut($name, $context, $value); + } + /** * @param string $name * @param array $arguments diff --git a/src/Drivers/RedisDriver.php b/src/Drivers/RedisDriver.php new file mode 100644 index 0000000..2822d84 --- /dev/null +++ b/src/Drivers/RedisDriver.php @@ -0,0 +1,102 @@ + */ + protected array $resolvers; + + public function __construct( + string $name, + array $resolvers, + RedisManager $redis, + Config $config, + Dispatcher $dispatcher + ) { + $this->name = $name; + $this->resolvers = $resolvers; + $this->redis = $redis; + $this->config = $config; + $this->dispatcher = $dispatcher; + + $this->unknown = new stdClass; + $this->prefix = $this->config->get("foggle.stores.$this->name.prefix"); + } + + public function get(string $name, $context) + { + $key = foggle()->serialize($context); + + $result = $this->connection()->command( + 'HGET', + ["$this->prefix:$name", $key] + ); + + if ($result) { + return $result; + } + + return with( + $this->resolveValue($name, $context), + function ($value) use ($name, $key) { + if ($value === $this->unknown) { + return false; + } + + $this->set($name, $key, $value); + + return $value; + } + ); + } + + protected function connection(): Connection + { + return $this->redis->connection( + $this->config->get("foggle.stores.$this->name.connection") + ); + } + + protected function resolveValue(string $feature, $context) + { + if (!array_key_exists($feature, $this->resolvers)) { + return $this->unknown; + } + + return $this->resolvers[$feature]($context); + } + + public function set(string $name, $context, $value): void + { + if ($context) { + $key = foggle()->serialize($context); + $this->connection()->command('HSET', ["$this->prefix:$name", $key, $value]); + } + } + + public function defined(): array + { + return array_keys($this->resolvers); + } + + public function define(string $name, callable $resolver): void + { + $this->resolvers[$name] = $resolver; + } +} diff --git a/src/FogGen.php b/src/FogGen.php index 9608eea..3232438 100644 --- a/src/FogGen.php +++ b/src/FogGen.php @@ -9,13 +9,19 @@ class FogGen { /** * @param string $path The config's dotted path + * @param string $separator * * @return callable */ - public static function inconfig(string $path): callable + public static function inconfig(string $path, string $separator = ','): callable { - return function ($context) use ($path) { + return function ($context) use ($path, $separator) { $config = config($path, []); + + if (is_string($config)) { + $config = splode($config, $separator); + } + if (in_array('*', $config)) { return true; } diff --git a/src/Foggle.php b/src/Foggle.php index dc7f78a..4d8b6b4 100644 --- a/src/Foggle.php +++ b/src/Foggle.php @@ -5,8 +5,10 @@ use Illuminate\Contracts\Container\BindingResolutionException; use Illuminate\Contracts\Container\Container; use InvalidArgumentException; +use RuntimeException; use YouCanShop\Foggle\Drivers\ArrayDriver; use YouCanShop\Foggle\Drivers\Decorator; +use YouCanShop\Foggle\Drivers\RedisDriver; /** * @mixin Decorator @@ -57,13 +59,23 @@ protected function resolve(string $name): Decorator $driver = new ArrayDriver($this->container['events'], []); } + if ($name === 'redis') { + $driver = new RedisDriver( + $name, + [], + $this->container['config'], + $this->container['redis'], + $this->container['events'] + ); + } + if (!isset($driver)) { throw new InvalidArgumentException("Driver [{$config['driver']}] is not supported"); } - return new Decorator($name, $driver, $this->container); + return new Decorator($name, $driver, $this->container, collect()); } - + protected function getDriverConfig(string $name): ?array { return $this->container['config']["foggle.stores.$name"]; @@ -78,4 +90,22 @@ public function __call($name, $arguments) { return $this->driver()->$name(...$arguments); } + + public function serialize($context): string + { + if ($context === null) { + return '__foggle_nil'; + } + + if (is_string($context)) { + return $context; + } + + if (is_numeric($context)) { + return (string)$context; + } + + // Foggables normally get parsed before they reach this part + throw new RuntimeException('Unable to serialize context, please implement the Foggable contract.'); + } } diff --git a/tests/Feature/FoggleTest.php b/tests/Feature/FoggleTest.php index 10ab4d5..ea12547 100644 --- a/tests/Feature/FoggleTest.php +++ b/tests/Feature/FoggleTest.php @@ -27,14 +27,18 @@ expect($foggle->get('always-true', null))->toBeTrue(); }); +it('considers undefined features false', function () { + expect(foggle()->get('non-existent-feature', null))->toBeFalse(); +}); + it('resolves with a context', function () { foggle()->discover( 'Workbench\\App\\Features', workbench_path('app/Features') ); - expect(foggle()->for(true)->active('resolves-to-context'))->toBeTrue() - ->and(foggle()->for(false)->active('resolves-to-context'))->toBeFalse(); + expect(foggle()->for('true')->active('resolves-to-context'))->toBeTrue() + ->and(foggle()->for('false')->active('resolves-to-context'))->toBeFalse(); }); it('splodes properly', function () { diff --git a/workbench/app/Features/ResolvesToContext.php b/workbench/app/Features/ResolvesToContext.php index d52c063..95aeace 100644 --- a/workbench/app/Features/ResolvesToContext.php +++ b/workbench/app/Features/ResolvesToContext.php @@ -6,8 +6,8 @@ class ResolvesToContext { public string $name = 'resolves-to-context'; - public function resolve(bool $context): bool + public function resolve(string $context): bool { - return $context; + return $context === 'true'; } }