From 50aa8f32ca3f2a45b556dc35aa936bc6eff872de Mon Sep 17 00:00:00 2001 From: Aymane Dara Hlamnach Date: Sat, 13 Jul 2024 14:00:31 +0100 Subject: [PATCH 1/4] feat: redis driver --- src/Drivers/ArrayDriver.php | 4 +- src/Drivers/Decorator.php | 10 +++ src/Drivers/RedisDriver.php | 89 ++++++++++++++++++++ src/Foggle.php | 21 ++++- tests/Feature/FoggleTest.php | 4 +- workbench/app/Features/ResolvesToContext.php | 4 +- 6 files changed, 126 insertions(+), 6 deletions(-) create mode 100644 src/Drivers/RedisDriver.php diff --git a/src/Drivers/ArrayDriver.php b/src/Drivers/ArrayDriver.php index f791010..3cdd0ca 100644 --- a/src/Drivers/ArrayDriver.php +++ b/src/Drivers/ArrayDriver.php @@ -36,7 +36,9 @@ public function defined(): array */ public function get(string $name, $context) { - return $this->resolvers[$name]($context); + $key = foggle()->serialize($context); + + return $this->resolvers[$name]($key); } /** diff --git a/src/Drivers/Decorator.php b/src/Drivers/Decorator.php index 14a5ffe..26acef3 100644 --- a/src/Drivers/Decorator.php +++ b/src/Drivers/Decorator.php @@ -7,6 +7,7 @@ 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; @@ -74,6 +75,13 @@ public function define(string $name, callable $resolver = null): void }); } + protected function parseContext($context) + { + return $context instanceof Foggable + ? $context->foggleId() + : $context; + } + /** * @param string $name * @param callable $resolver @@ -88,6 +96,8 @@ protected function resolve(string $name, callable $resolver, $context) public function get(string $name, $context) { + $context = $this->parseContext($context); + return $this->driver->get($name, $context); } diff --git a/src/Drivers/RedisDriver.php b/src/Drivers/RedisDriver.php new file mode 100644 index 0000000..a1fe230 --- /dev/null +++ b/src/Drivers/RedisDriver.php @@ -0,0 +1,89 @@ + */ + protected array $resolvers; + + public function __construct( + string $name, + Config $config, + array $resolvers, + RedisManager $redis, + Dispatcher $dispatcher + ) { + $this->name = $name; + $this->redis = $redis; + $this->config = $config; + $this->resolvers = $resolvers; + $this->dispatcher = $dispatcher; + + $this->unknown = new stdClass; + $this->prefix = $this->config->get("foggle.stores.$this->name.prefix"); + } + + public function defined(): array + { + return array_keys($this->resolvers); + } + + public function get(string $name, $context) + { + $result = $this->connection()->command( + 'HGET', + ["$this->prefix:$name", $context] + ); + + if ($result) { + return $result; + } + + return with($this->resolveValue($name, $context), function ($value) use ($name, $context) { + if ($value === $this->unknown) { + return false; + } + +// $this->set($name, $context, $value); + + return $value; + }); + } + + public function define(string $name, callable $resolver): void + { + $this->resolvers[$name] = $resolver; + } + + public 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); + } +} diff --git a/src/Foggle.php b/src/Foggle.php index dc7f78a..bd1c9da 100644 --- a/src/Foggle.php +++ b/src/Foggle.php @@ -5,6 +5,7 @@ use Illuminate\Contracts\Container\BindingResolutionException; use Illuminate\Contracts\Container\Container; use InvalidArgumentException; +use RuntimeException; use YouCanShop\Foggle\Drivers\ArrayDriver; use YouCanShop\Foggle\Drivers\Decorator; @@ -63,7 +64,7 @@ protected function resolve(string $name): Decorator return new Decorator($name, $driver, $this->container); } - + protected function getDriverConfig(string $name): ?array { return $this->container['config']["foggle.stores.$name"]; @@ -78,4 +79,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..5befb9b 100644 --- a/tests/Feature/FoggleTest.php +++ b/tests/Feature/FoggleTest.php @@ -33,8 +33,8 @@ 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'; } } From dddfbc8bde44e44e9c87decb049685cb05f2713f Mon Sep 17 00:00:00 2001 From: Aymane Dara Hlamnach Date: Sat, 13 Jul 2024 14:43:35 +0100 Subject: [PATCH 2/4] cache & sets --- src/Contracts/Driver.php | 7 ++++ src/Drivers/ArrayDriver.php | 46 ++++++++++++++++++++- src/Drivers/Decorator.php | 81 ++++++++++++++++++++++++++++--------- src/Drivers/RedisDriver.php | 57 ++++++++++++++++---------- src/Foggle.php | 13 +++++- 5 files changed, 162 insertions(+), 42 deletions(-) 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 3cdd0ca..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; } /** @@ -38,7 +47,42 @@ public function get(string $name, $context) { $key = foggle()->serialize($context); - return $this->resolvers[$name]($key); + 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; } /** diff --git a/src/Drivers/Decorator.php b/src/Drivers/Decorator.php index 26acef3..8fca01b 100644 --- a/src/Drivers/Decorator.php +++ b/src/Drivers/Decorator.php @@ -4,6 +4,7 @@ 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; @@ -16,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')}")); } @@ -60,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) ); } @@ -75,13 +83,6 @@ public function define(string $name, callable $resolver = null): void }); } - protected function parseContext($context) - { - return $context instanceof Foggable - ? $context->foggleId() - : $context; - } - /** * @param string $name * @param callable $resolver @@ -98,7 +99,43 @@ public function get(string $name, $context) { $context = $this->parseContext($context); - return $this->driver->get($name, $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 @@ -106,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 index a1fe230..2822d84 100644 --- a/src/Drivers/RedisDriver.php +++ b/src/Drivers/RedisDriver.php @@ -15,8 +15,8 @@ class RedisDriver implements Driver protected string $name; protected string $prefix; - protected Config $config; protected RedisManager $redis; + protected Config $config; protected Dispatcher $dispatcher; /** @var array */ @@ -24,54 +24,49 @@ class RedisDriver implements Driver public function __construct( string $name, - Config $config, array $resolvers, RedisManager $redis, + Config $config, Dispatcher $dispatcher ) { $this->name = $name; + $this->resolvers = $resolvers; $this->redis = $redis; $this->config = $config; - $this->resolvers = $resolvers; $this->dispatcher = $dispatcher; $this->unknown = new stdClass; $this->prefix = $this->config->get("foggle.stores.$this->name.prefix"); } - public function defined(): array - { - return array_keys($this->resolvers); - } - public function get(string $name, $context) { + $key = foggle()->serialize($context); + $result = $this->connection()->command( 'HGET', - ["$this->prefix:$name", $context] + ["$this->prefix:$name", $key] ); if ($result) { return $result; } - return with($this->resolveValue($name, $context), function ($value) use ($name, $context) { - if ($value === $this->unknown) { - return false; - } - -// $this->set($name, $context, $value); + return with( + $this->resolveValue($name, $context), + function ($value) use ($name, $key) { + if ($value === $this->unknown) { + return false; + } - return $value; - }); - } + $this->set($name, $key, $value); - public function define(string $name, callable $resolver): void - { - $this->resolvers[$name] = $resolver; + return $value; + } + ); } - public function connection(): Connection + protected function connection(): Connection { return $this->redis->connection( $this->config->get("foggle.stores.$this->name.connection") @@ -86,4 +81,22 @@ protected function resolveValue(string $feature, $context) 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/Foggle.php b/src/Foggle.php index bd1c9da..4d8b6b4 100644 --- a/src/Foggle.php +++ b/src/Foggle.php @@ -8,6 +8,7 @@ use RuntimeException; use YouCanShop\Foggle\Drivers\ArrayDriver; use YouCanShop\Foggle\Drivers\Decorator; +use YouCanShop\Foggle\Drivers\RedisDriver; /** * @mixin Decorator @@ -58,11 +59,21 @@ 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 From 6e96cd4f2977958d5aa070b8df0eb0b1a60eb88f Mon Sep 17 00:00:00 2001 From: Aymane Dara Hlamnach Date: Sat, 13 Jul 2024 14:50:52 +0100 Subject: [PATCH 3/4] extra test --- tests/Feature/FoggleTest.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/Feature/FoggleTest.php b/tests/Feature/FoggleTest.php index 5befb9b..ea12547 100644 --- a/tests/Feature/FoggleTest.php +++ b/tests/Feature/FoggleTest.php @@ -27,6 +27,10 @@ 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', From 475bb8f5f794e149f79569a9a0916bdfa5fd52f3 Mon Sep 17 00:00:00 2001 From: Aymane Dara Hlamnach Date: Sat, 13 Jul 2024 15:00:00 +0100 Subject: [PATCH 4/4] helye --- src/FogGen.php | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) 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; }