From f39a295254683255f3da2fc0a5f6d407601539e5 Mon Sep 17 00:00:00 2001 From: Aymane Dara Hlamnach Date: Sat, 13 Jul 2024 16:50:08 +0100 Subject: [PATCH 1/3] feat: context resolvers --- config/foggle.php | 4 ++ src/ContextResolvers/UserContextResolver.php | 21 ++++++ src/Contracts/ContextResolver.php | 11 +++ src/Drivers/Decorator.php | 68 ++++++++++++++----- src/Foggle.php | 35 +++++++++- tests/Feature/FoggleTest.php | 8 +++ tests/TestCase.php | 6 ++ .../ContextResolvers/CatContextResolver.php | 14 ++++ workbench/app/Models/Cat.php | 20 ++++++ 9 files changed, 169 insertions(+), 18 deletions(-) create mode 100644 src/ContextResolvers/UserContextResolver.php create mode 100644 src/Contracts/ContextResolver.php create mode 100644 workbench/app/ContextResolvers/CatContextResolver.php create mode 100644 workbench/app/Models/Cat.php diff --git a/config/foggle.php b/config/foggle.php index 8ec6131..9f1c3a1 100644 --- a/config/foggle.php +++ b/config/foggle.php @@ -10,4 +10,8 @@ ], ], + + 'context_resolvers' => [ + \Illuminate\Foundation\Auth\User::class => \YouCanShop\Foggle\ContextResolvers\UserContextResolver::class, + ], ]; diff --git a/src/ContextResolvers/UserContextResolver.php b/src/ContextResolvers/UserContextResolver.php new file mode 100644 index 0000000..4316ae9 --- /dev/null +++ b/src/ContextResolvers/UserContextResolver.php @@ -0,0 +1,21 @@ +authManager = $authManager; + } + + public function resolve(): ?Authenticatable + { + return $this->authManager->user(); + } +} diff --git a/src/Contracts/ContextResolver.php b/src/Contracts/ContextResolver.php new file mode 100644 index 0000000..eb07767 --- /dev/null +++ b/src/Contracts/ContextResolver.php @@ -0,0 +1,11 @@ +container->make($name)->name ?? $name, new Lazily($name)]; @@ -79,7 +77,22 @@ public function define(string $name, callable $resolver = null): void return $this->resolve($name, fn() => $resolver, $context); } - return $this->resolve($name, $resolver, $context); + if ($context !== null) { + return $this->resolve($name, $resolver, $context); + } + + if ( + ($type = $this->getContextType($resolver)) !== null + && ($context = foggle()->resolveContext($type)) !== null + ) { + return $this->resolve($name, $resolver, $context); + } + + if ($this->canHandleNullContext($resolver)) { + return $this->resolve($name, $resolver, $context); + } + + return $this->resolve($name, fn() => false, $context); }); } @@ -95,27 +108,48 @@ protected function resolve(string $name, callable $resolver, $context) return $resolver($context); } + protected function getContextType(callable $resolver): ?ReflectionType + { + $function = new ReflectionFunction(Closure::fromCallable($resolver)); + + if ($function->getNumberOfParameters() !== 1 || !$function->getParameters()[0]->hasType()) { + return null; + } + + return $function->getParameters()[0]->getType(); + } + + protected function canHandleNullContext(callable $resolver): bool + { + $function = new ReflectionFunction(Closure::fromCallable($resolver)); + + return $function->getNumberOfParameters() === 0 + || $function->getParameters()[0]->hasType() + || $function->getParameters()[0]->getType()->allowsNull(); + } + + /** + * @return mixed + */ public function get(string $name, $context) { - $context = $this->parseContext($context); + $key = foggle()->serialize($context); - $item = $this->cache->whereStrict('context', foggle()->serialize($context))->whereStrict('name', $name)->first(); + $item = $this->cache + ->whereStrict('context', foggle()->serialize($key)) + ->whereStrict('name', $name) + ->first(); if ($item !== null) { return $item['value']; } $value = $this->driver->get($name, $context); - $this->cPut($name, $context, $value); + $this->cPut($name, $key, $value); return $value; } - protected function parseContext($context) - { - return $context instanceof Foggable ? $context->foggleId() : $context; - } - /** * @param string $name * @param mixed $context @@ -145,10 +179,10 @@ public function defined(): array public function set(string $name, $context, $value): void { - $context = $this->parseContext($context); - $this->driver->set($name, $context, $value); + $key = foggle()->serialize($context); - $this->cPut($name, $context, $value); + $this->driver->set($name, $key, $value); + $this->cPut($name, $key, $value); } public function cFlush(): void diff --git a/src/Foggle.php b/src/Foggle.php index 2c6c263..ff6186a 100644 --- a/src/Foggle.php +++ b/src/Foggle.php @@ -6,6 +6,8 @@ use Illuminate\Contracts\Container\Container; use InvalidArgumentException; use RuntimeException; +use YouCanShop\Foggle\Contracts\ContextResolver; +use YouCanShop\Foggle\Contracts\Foggable; use YouCanShop\Foggle\Drivers\ArrayDriver; use YouCanShop\Foggle\Drivers\Decorator; use YouCanShop\Foggle\Drivers\RedisDriver; @@ -15,7 +17,12 @@ */ final class Foggle { + /** @var array */ protected array $stores = []; + + /** @var array */ + protected array $contextResolvers = []; + private Container $container; public function __construct(Container $container) @@ -54,7 +61,11 @@ protected function resolve(string $name): Decorator if ($name === 'redis') { $driver = new RedisDriver( - $name, [], $this->container['config'], $this->container['redis'], $this->container['events'] + $name, + [], + $this->container['config'], + $this->container['redis'], + $this->container['events'] ); } @@ -70,6 +81,24 @@ protected function getDriverConfig(string $name): ?array return $this->container['config']["foggle.stores.$name"]; } + /** + * @return mixed + */ + public function resolveContext(string $name) + { + if (!isset($this->contextResolvers[$name])) { + $fqn = $this->container['config']["foggle.context_resolvers.$name"]; + if ($fqn === null || !is_a($fqn, ContextResolver::class, true)) { + throw new InvalidArgumentException("Context resolver for '$name' not found"); + } + + /** @var class-string $fqn */ + $this->contextResolvers[$name] = $this->container[$fqn]; + } + + return $this->contextResolvers[$name]->resolve(); + } + public function __call($name, $arguments) { return $this->driver()->$name(...$arguments); @@ -101,6 +130,10 @@ public function serialize($context): string return (string)$context; } + if ($context instanceof Foggable) { + return $context->foggleId(); + } + // 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 ea12547..b9ce9b1 100644 --- a/tests/Feature/FoggleTest.php +++ b/tests/Feature/FoggleTest.php @@ -1,5 +1,6 @@ for(new User('1'))->active('billing'))->toBeTrue() ->and(foggle()->for(new User('0'))->active('billing'))->toBeFalse(); }); + +it('resolves context', function () { + foggle()->define('allow-number-seven', fn(Cat $cat) => $cat->foggleId() === '7'); + + expect(foggle()->active('allow-number-seven'))->toBeTrue() + ->and(foggle()->for(new Cat('8'))->active('allow-number-seven'))->toBeFalse(); +}); diff --git a/tests/TestCase.php b/tests/TestCase.php index 8b99d2b..264bb7a 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -5,6 +5,8 @@ use Illuminate\Contracts\Config\Repository; use Orchestra\Testbench\Concerns\WithWorkbench; use Orchestra\Testbench\TestCase as OrchestraTestCase; +use Workbench\App\ContextResolvers\CatContextResolver; +use Workbench\App\Models\Cat; use YouCanShop\Foggle\Foggle; abstract class TestCase extends OrchestraTestCase @@ -24,6 +26,10 @@ protected function defineEnvironment($app) 'sellers' => splode('1,2,3'), ], ]); + + $config->set('foggle.context_resolvers', [ + Cat::class => CatContextResolver::class, + ]); }); } } diff --git a/workbench/app/ContextResolvers/CatContextResolver.php b/workbench/app/ContextResolvers/CatContextResolver.php new file mode 100644 index 0000000..88e9a86 --- /dev/null +++ b/workbench/app/ContextResolvers/CatContextResolver.php @@ -0,0 +1,14 @@ +id = $id; + } + + public function foggleId(): string + { + return $this->id; + } +} From 1ceaf1f31ef5d377e82e7fe3c0fba4a8ca8e41eb Mon Sep 17 00:00:00 2001 From: Aymane Dara Hlamnach Date: Sat, 13 Jul 2024 17:56:43 +0100 Subject: [PATCH 2/3] faster universal context resolution --- src/Drivers/Decorator.php | 36 +++++++++++++-------- src/FogGen.php | 2 +- tests/Feature/FoggleTest.php | 31 ++++++++++++++++-- tests/TestCase.php | 5 +-- workbench/app/Features/AllowNumberSeven.php | 16 +++++++++ 5 files changed, 71 insertions(+), 19 deletions(-) create mode 100644 workbench/app/Features/AllowNumberSeven.php diff --git a/src/Drivers/Decorator.php b/src/Drivers/Decorator.php index beaeced..ced31a6 100644 --- a/src/Drivers/Decorator.php +++ b/src/Drivers/Decorator.php @@ -58,18 +58,31 @@ public function discover(string $namespace = 'App\\Features', ?string $path = nu } /** - * @param class-string|string $name + * @param class-string|string $name The feature's name + * @param class-string|null $type The context resolver's type */ - public function define(string $name, ?callable $resolver = null): void + public function define(string $name, ?callable $resolver = null, ?string $type = null): void { if ($resolver === null) { - [$name, $resolver] = [$this->container->make($name)->name ?? $name, new Lazily($name)]; + $feature = $this->container->make($name); + [$name, $resolver, $type] = [ + $feature->name ?? $name, + new Lazily($name), + $feature->contextType ?? null, + ]; } - $this->driver->define($name, function ($context) use ($name, $resolver) { + $this->driver->define($name, function ($context) use ($name, $resolver, $type) { + if ($context === null && $type !== null) { + $context = foggle()->resolveContext($type); + } + 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) ); } @@ -81,13 +94,6 @@ public function define(string $name, ?callable $resolver = null): void return $this->resolve($name, $resolver, $context); } - if ( - ($type = $this->getContextType($resolver)) !== null - && ($context = foggle()->resolveContext($type)) !== null - ) { - return $this->resolve($name, $resolver, $context); - } - if ($this->canHandleNullContext($resolver)) { return $this->resolve($name, $resolver, $context); } @@ -108,8 +114,12 @@ protected function resolve(string $name, callable $resolver, $context) return $resolver($context); } - protected function getContextType(callable $resolver): ?ReflectionType + protected function getContextType($resolver): ?ReflectionType { + if ($resolver instanceof Lazily) { + return null; + } + $function = new ReflectionFunction(Closure::fromCallable($resolver)); if ($function->getNumberOfParameters() !== 1 || !$function->getParameters()[0]->hasType()) { diff --git a/src/FogGen.php b/src/FogGen.php index 3232438..6b9c688 100644 --- a/src/FogGen.php +++ b/src/FogGen.php @@ -32,7 +32,7 @@ public static function inconfig(string $path, string $separator = ','): callable throw new InvalidArgumentException('Context must be an instance of Foggable or a string'); } - return in_array($context, config($path, [])); + return in_array($context, $config); }; } } diff --git a/tests/Feature/FoggleTest.php b/tests/Feature/FoggleTest.php index b9ce9b1..2809293 100644 --- a/tests/Feature/FoggleTest.php +++ b/tests/Feature/FoggleTest.php @@ -43,7 +43,7 @@ }); it('splodes properly', function () { - expect(config('features.billing.sellers'))->toEqual([1, 2, 3]); + expect(splode(config('features.billing.sellers')))->toEqual([1, 2, 3]); }); it('registers a generated feature', function () { @@ -60,8 +60,33 @@ ->and(foggle()->for(new User('0'))->active('billing'))->toBeFalse(); }); -it('resolves context', function () { - foggle()->define('allow-number-seven', fn(Cat $cat) => $cat->foggleId() === '7'); +it('resolves context for classes', function () { + foggle()->discover( + 'Workbench\\App\\Features', + workbench_path('app/Features') + ); + + expect(foggle()->active('allow-number-seven'))->toBeTrue() + ->and(foggle()->for(new Cat('8'))->active('allow-number-seven'))->toBeFalse(); +}); + +it('resolves context for callables', function () { + foggle()->define( + 'allow-number-seven', + fn(Cat $cat) => $cat->foggleId() === '7', + Cat::class + ); + + expect(foggle()->active('allow-number-seven'))->toBeTrue() + ->and(foggle()->for(new Cat('8'))->active('allow-number-seven'))->toBeFalse(); +}); + +it('resolves context for generations', function () { + foggle()->define( + 'allow-number-seven', + FogGen::inconfig('features.allow-number-seven'), + Cat::class, + ); expect(foggle()->active('allow-number-seven'))->toBeTrue() ->and(foggle()->for(new Cat('8'))->active('allow-number-seven'))->toBeFalse(); diff --git a/tests/TestCase.php b/tests/TestCase.php index 264bb7a..8cec83a 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -22,9 +22,10 @@ protected function defineEnvironment($app) { tap($app['config'], function (Repository $config) { $config->set('features', [ - 'billing' => [ - 'sellers' => splode('1,2,3'), + 'billing' => [ + 'sellers' => '1,2,3', ], + 'allow-number-seven' => '7', ]); $config->set('foggle.context_resolvers', [ diff --git a/workbench/app/Features/AllowNumberSeven.php b/workbench/app/Features/AllowNumberSeven.php new file mode 100644 index 0000000..8161b71 --- /dev/null +++ b/workbench/app/Features/AllowNumberSeven.php @@ -0,0 +1,16 @@ +foggleId() === '7'; + } +} From 8cee7cf40657c8ea7ae6410f7a5d467bae29450f Mon Sep 17 00:00:00 2001 From: Aymane Dara Hlamnach Date: Sat, 13 Jul 2024 17:57:36 +0100 Subject: [PATCH 3/3] rm unused method --- src/Drivers/Decorator.php | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/Drivers/Decorator.php b/src/Drivers/Decorator.php index ced31a6..749acb7 100644 --- a/src/Drivers/Decorator.php +++ b/src/Drivers/Decorator.php @@ -7,7 +7,6 @@ use Illuminate\Support\Collection; use Illuminate\Support\Str; use ReflectionFunction; -use ReflectionType; use Symfony\Component\Finder\Finder; use YouCanShop\Foggle\Contracts\Driver; use YouCanShop\Foggle\FeatureInteraction; @@ -114,21 +113,6 @@ protected function resolve(string $name, callable $resolver, $context) return $resolver($context); } - protected function getContextType($resolver): ?ReflectionType - { - if ($resolver instanceof Lazily) { - return null; - } - - $function = new ReflectionFunction(Closure::fromCallable($resolver)); - - if ($function->getNumberOfParameters() !== 1 || !$function->getParameters()[0]->hasType()) { - return null; - } - - return $function->getParameters()[0]->getType(); - } - protected function canHandleNullContext(callable $resolver): bool { $function = new ReflectionFunction(Closure::fromCallable($resolver));