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)]; + $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) ); } @@ -79,7 +89,15 @@ 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 ($this->canHandleNullContext($resolver)) { + return $this->resolve($name, $resolver, $context); + } + + return $this->resolve($name, fn() => false, $context); }); } @@ -95,27 +113,37 @@ protected function resolve(string $name, callable $resolver, $context) return $resolver($context); } + 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 +173,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/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/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..2809293 100644 --- a/tests/Feature/FoggleTest.php +++ b/tests/Feature/FoggleTest.php @@ -1,5 +1,6 @@ toEqual([1, 2, 3]); + expect(splode(config('features.billing.sellers')))->toEqual([1, 2, 3]); }); it('registers a generated feature', function () { @@ -58,3 +59,35 @@ expect(foggle()->for(new User('1'))->active('billing'))->toBeTrue() ->and(foggle()->for(new User('0'))->active('billing'))->toBeFalse(); }); + +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 8b99d2b..8cec83a 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 @@ -20,9 +22,14 @@ 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', [ + 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 @@ +foggleId() === '7'; + } +} diff --git a/workbench/app/Models/Cat.php b/workbench/app/Models/Cat.php new file mode 100644 index 0000000..f540284 --- /dev/null +++ b/workbench/app/Models/Cat.php @@ -0,0 +1,20 @@ +id = $id; + } + + public function foggleId(): string + { + return $this->id; + } +}