Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: context resolvers #8

Merged
merged 3 commits into from
Jul 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions config/foggle.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,8 @@
],

],

'context_resolvers' => [
\Illuminate\Foundation\Auth\User::class => \YouCanShop\Foggle\ContextResolvers\UserContextResolver::class,
],
];
21 changes: 21 additions & 0 deletions src/ContextResolvers/UserContextResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace YouCanShop\Foggle\ContextResolvers;

use Illuminate\Auth\AuthManager;
use Illuminate\Contracts\Auth\Authenticatable;

class UserContextResolver
{
private AuthManager $authManager;

public function __construct(AuthManager $authManager)
{
$this->authManager = $authManager;
}

public function resolve(): ?Authenticatable
{
return $this->authManager->user();
}
}
11 changes: 11 additions & 0 deletions src/Contracts/ContextResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace YouCanShop\Foggle\Contracts;

interface ContextResolver
{
/**
* @return mixed
*/
public function resolve();
}
70 changes: 49 additions & 21 deletions src/Drivers/Decorator.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
use Illuminate\Contracts\Container\Container;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use ReflectionFunction;
use Symfony\Component\Finder\Finder;
use YouCanShop\Foggle\Contracts\Driver;
use YouCanShop\Foggle\Contracts\Foggable;
use YouCanShop\Foggle\FeatureInteraction;
use YouCanShop\Foggle\Lazily;

Expand Down Expand Up @@ -57,29 +57,47 @@ public function discover(string $namespace = 'App\\Features', ?string $path = nu
}

/**
* @param class-string|string $name
* @param callable|null $resolver
*
* @return void
* @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)
);
}

if (!$resolver instanceof Closure) {
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);
});
}

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/FogGen.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
};
}
}
35 changes: 34 additions & 1 deletion src/Foggle.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -15,7 +17,12 @@
*/
final class Foggle
{
/** @var array<Decorator> */
protected array $stores = [];

/** @var array<ContextResolver> */
protected array $contextResolvers = [];

private Container $container;

public function __construct(Container $container)
Expand Down Expand Up @@ -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']
);
}

Expand All @@ -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);
Expand Down Expand Up @@ -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.');
}
Expand Down
35 changes: 34 additions & 1 deletion tests/Feature/FoggleTest.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<?php

use Workbench\App\Models\Cat;
use Workbench\App\Models\User;
use YouCanShop\Foggle\FogGen;

Expand Down Expand Up @@ -42,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 () {
Expand All @@ -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();
});
11 changes: 9 additions & 2 deletions tests/TestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
]);
});
}
Expand Down
14 changes: 14 additions & 0 deletions workbench/app/ContextResolvers/CatContextResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

namespace Workbench\App\ContextResolvers;

use Workbench\App\Models\Cat;
use YouCanShop\Foggle\Contracts\ContextResolver;

class CatContextResolver implements ContextResolver
{
public function resolve(): Cat
{
return new Cat('7');
}
}
16 changes: 16 additions & 0 deletions workbench/app/Features/AllowNumberSeven.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace Workbench\App\Features;

use Workbench\App\Models\Cat;

class AllowNumberSeven
{
public string $name = 'allow-number-seven';
public string $contextType = Cat::class;

public function resolve(Cat $cat): bool
{
return $cat->foggleId() === '7';
}
}
20 changes: 20 additions & 0 deletions workbench/app/Models/Cat.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

namespace Workbench\App\Models;

use YouCanShop\Foggle\Contracts\Foggable;

class Cat implements Foggable
{
private string $id;

public function __construct(string $id)
{
$this->id = $id;
}

public function foggleId(): string
{
return $this->id;
}
}
Loading