Skip to content

Commit

Permalink
Feature: Context provider (#23)
Browse files Browse the repository at this point in the history
  • Loading branch information
RikudouSage committed Aug 24, 2021
1 parent fdb5d28 commit d81e76a
Show file tree
Hide file tree
Showing 10 changed files with 290 additions and 8 deletions.
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,42 @@ $enabled = $unleash->isEnabled('some-feature');
> Note: This library also implements some deprecated strategies, namely `gradualRolloutRandom`, `gradualRolloutSessionId`
> and `gradualRolloutUserId` which all alias to the Gradual rollout strategy.
### Context provider

Manually creating relevant context can get tiring real fast. Luckily you can create your own context provider that
will do it for you!

```php
<?php

use Unleash\Client\ContextProvider\UnleashContextProvider;
use Unleash\Client\Configuration\UnleashContext;
use Unleash\Client\UnleashBuilder;

final class MyContextProvider implements UnleashContextProvider
{
public function getContext(): Context
{
$context = new UnleashContext();
$context->setCurrentUserId('user id from my app');

return $context;
}
}

$unleash = UnleashBuilder::create()
->withAppName('Some app name')
->withAppUrl('https://some-app-url.com')
->withInstanceId('Some instance id')
// here we set the custom provider
->withContextProvider(new MyContextProvider())
->build();

if ($unleash->isEnabled('someFeature')) { // this call will use your context provider with the provided user id

}
```

### Custom strategies

To implement your own strategy you need to create a class implementing `StrategyHandler` (or `AbstractStrategyHandler`
Expand Down
39 changes: 34 additions & 5 deletions src/Configuration/UnleashConfiguration.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@

namespace Unleash\Client\Configuration;

use JetBrains\PhpStorm\Pure;
use JetBrains\PhpStorm\Deprecated;
use LogicException;
use Psr\SimpleCache\CacheInterface;
use Unleash\Client\ContextProvider\DefaultUnleashContextProvider;
use Unleash\Client\ContextProvider\SettableUnleashContextProvider;
use Unleash\Client\ContextProvider\UnleashContextProvider;

final class UnleashConfiguration
{
Expand All @@ -21,8 +24,13 @@ public function __construct(
private bool $metricsEnabled = true,
private array $headers = [],
private bool $autoRegistrationEnabled = true,
private ?Context $defaultContext = null,
?Context $defaultContext = null,
private ?UnleashContextProvider $contextProvider = null,
) {
$this->contextProvider ??= new DefaultUnleashContextProvider();
if ($defaultContext !== null) {
$this->setDefaultContext($defaultContext);
}
}

public function getCache(): CacheInterface
Expand Down Expand Up @@ -154,15 +162,36 @@ public function setAutoRegistrationEnabled(bool $autoRegistrationEnabled): self
return $this;
}

#[Pure]
public function getDefaultContext(): Context
{
return $this->defaultContext ?? new UnleashContext();
return $this->getContextProvider()->getContext();
}

/**
* @todo remove on next major version
*/
#[Deprecated(reason: 'Support for context provider was added, default context logic should be handled in a provider')]
public function setDefaultContext(?Context $defaultContext): self
{
$this->defaultContext = $defaultContext;
if ($this->getContextProvider() instanceof SettableUnleashContextProvider) {
$this->getContextProvider()->setDefaultContext($defaultContext ?? new UnleashContext());
} else {
throw new LogicException("Default context cannot be set via configuration for a context provider that doesn't implement SettableUnleashContextProvider");
}

return $this;
}

public function getContextProvider(): UnleashContextProvider
{
assert($this->contextProvider !== null);

return $this->contextProvider;
}

public function setContextProvider(UnleashContextProvider $contextProvider): self
{
$this->contextProvider = $contextProvider;

return $this;
}
Expand Down
32 changes: 32 additions & 0 deletions src/ContextProvider/DefaultUnleashContextProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

namespace Unleash\Client\ContextProvider;

use JetBrains\PhpStorm\Pure;
use Unleash\Client\Configuration\Context;
use Unleash\Client\Configuration\UnleashContext;

final class DefaultUnleashContextProvider implements UnleashContextProvider, SettableUnleashContextProvider
{
public function __construct(private ?Context $defaultContext = null)
{
}

#[Pure]
public function getContext(): Context
{
return $this->defaultContext ? clone $this->defaultContext : new UnleashContext();
}

/**
* @todo remove in next major version
*
* @internal
*/
public function setDefaultContext(Context $context): self
{
$this->defaultContext = $context;

return $this;
}
}
15 changes: 15 additions & 0 deletions src/ContextProvider/SettableUnleashContextProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

namespace Unleash\Client\ContextProvider;

use JetBrains\PhpStorm\Deprecated;
use Unleash\Client\Configuration\Context;

/**
* @todo Remove in next major version
*/
#[Deprecated('This interface will be removed in next major version')]
interface SettableUnleashContextProvider extends UnleashContextProvider
{
public function setDefaultContext(Context $context): self;
}
12 changes: 12 additions & 0 deletions src/ContextProvider/UnleashContextProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

namespace Unleash\Client\ContextProvider;

use JetBrains\PhpStorm\Pure;
use Unleash\Client\Configuration\Context;

interface UnleashContextProvider
{
#[Pure]
public function getContext(): Context;
}
4 changes: 2 additions & 2 deletions src/DefaultUnleash.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public function __construct(

public function isEnabled(string $featureName, ?Context $context = null, bool $default = false): bool
{
$context ??= $this->configuration->getDefaultContext();
$context ??= $this->configuration->getContextProvider()->getContext();

$feature = $this->repository->findFeature($featureName);
if ($feature === null) {
Expand Down Expand Up @@ -77,7 +77,7 @@ public function isEnabled(string $featureName, ?Context $context = null, bool $d
public function getVariant(string $featureName, ?Context $context = null, ?Variant $fallbackVariant = null): Variant
{
$fallbackVariant ??= $this->variantHandler->getDefaultVariant();
$context ??= $this->configuration->getDefaultContext();
$context ??= $this->configuration->getContextProvider()->getContext();

$feature = $this->repository->findFeature($featureName);
if ($feature === null || !$feature->isEnabled() || !count($feature->getVariants())) {
Expand Down
23 changes: 22 additions & 1 deletion src/UnleashBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Unleash\Client;

use JetBrains\PhpStorm\Deprecated;
use JetBrains\PhpStorm\Immutable;
use JetBrains\PhpStorm\Pure;
use Psr\Http\Client\ClientInterface;
Expand All @@ -11,6 +12,9 @@
use Unleash\Client\Client\RegistrationService;
use Unleash\Client\Configuration\Context;
use Unleash\Client\Configuration\UnleashConfiguration;
use Unleash\Client\ContextProvider\DefaultUnleashContextProvider;
use Unleash\Client\ContextProvider\SettableUnleashContextProvider;
use Unleash\Client\ContextProvider\UnleashContextProvider;
use Unleash\Client\Exception\InvalidValueException;
use Unleash\Client\Helper\DefaultImplementationLocator;
use Unleash\Client\Metrics\DefaultMetricsHandler;
Expand Down Expand Up @@ -57,6 +61,8 @@ final class UnleashBuilder

private ?Context $defaultContext = null;

private ?UnleashContextProvider $contextProvider = null;

/**
* @var array<string,string>
*/
Expand Down Expand Up @@ -204,11 +210,18 @@ public function withMetricsInterval(int $milliseconds): self
}

#[Pure]
#[Deprecated(reason: 'Context provider support was added, use custom context provider using withContextProvider()')]
public function withDefaultContext(?Context $context): self
{
return $this->with('defaultContext', $context);
}

#[Pure]
public function withContextProvider(?UnleashContextProvider $contextProvider): self
{
return $this->with('contextProvider', $contextProvider);
}

public function build(): Unleash
{
if ($this->appUrl === null) {
Expand Down Expand Up @@ -237,6 +250,14 @@ public function build(): Unleash
}
assert($cache instanceof CacheInterface);

$contextProvider = $this->contextProvider;
if ($contextProvider === null) {
$contextProvider = new DefaultUnleashContextProvider();
}
if ($this->defaultContext !== null && $contextProvider instanceof SettableUnleashContextProvider) {
$contextProvider->setDefaultContext($this->defaultContext);
}

$configuration = new UnleashConfiguration($this->appUrl, $this->appName, $this->instanceId);
$configuration
->setCache($cache)
Expand All @@ -245,7 +266,7 @@ public function build(): Unleash
->setMetricsInterval($this->metricsInterval ?? $configuration->getMetricsInterval())
->setHeaders($this->headers)
->setAutoRegistrationEnabled($this->autoregister)
->setDefaultContext($this->defaultContext)
->setContextProvider($contextProvider)
;

$httpClient = $this->httpClient;
Expand Down
63 changes: 63 additions & 0 deletions tests/Configuration/UnleashConfigurationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,15 @@

namespace Unleash\Client\Tests\Configuration;

use JetBrains\PhpStorm\Pure;
use LogicException;
use PHPUnit\Framework\TestCase;
use Psr\SimpleCache\CacheInterface;
use Unleash\Client\Configuration\Context;
use Unleash\Client\Configuration\UnleashConfiguration;
use Unleash\Client\Configuration\UnleashContext;
use Unleash\Client\ContextProvider\DefaultUnleashContextProvider;
use Unleash\Client\ContextProvider\UnleashContextProvider;
use Unleash\Client\Tests\Traits\FakeCacheImplementationTrait;

final class UnleashConfigurationTest extends TestCase
Expand All @@ -16,6 +21,21 @@ public function testConstructor()
{
$instance = new UnleashConfiguration('https://www.example.com/test', '', '');
self::assertEquals('https://www.example.com/test/', $instance->getUrl());

$context = new UnleashContext('147');
$instance = new UnleashConfiguration(
'https://www.example.com/test',
'',
'',
null,
0,
0,
false,
[],
false,
$context
);
self::assertEquals($context->getCurrentUserId(), $instance->getDefaultContext()->getCurrentUserId());
}

public function testSetUrl()
Expand All @@ -35,4 +55,47 @@ public function testGetCache()
$this->expectException(LogicException::class);
$instance->getCache();
}

public function testGetDefaultContext()
{
$instance = new UnleashConfiguration('', '', '');
self::assertInstanceOf(UnleashContext::class, $instance->getDefaultContext());
}

public function testSetDefaultContext()
{
$context = new UnleashContext('123');
$instance = (new UnleashConfiguration('', '', ''))
->setDefaultContext($context);

self::assertEquals('123', $instance->getDefaultContext()->getCurrentUserId());
self::assertSame('123', $instance->getContextProvider()->getContext()->getCurrentUserId());

$contextProvider = new class implements UnleashContextProvider {
#[Pure]
public function getContext(): Context
{
return new UnleashContext();
}
};

$instance->setContextProvider($contextProvider);
$this->expectException(LogicException::class);
$instance->setDefaultContext($context);
}

public function testGetContextProvider()
{
$instance = new UnleashConfiguration('', '', '');
self::assertNotNull($instance->getContextProvider());
}

public function testSetContextProvider()
{
$instance = new UnleashConfiguration('', '', '');
$provider = new DefaultUnleashContextProvider();
$instance->setContextProvider($provider);

self::assertSame($provider, $instance->getContextProvider());
}
}
40 changes: 40 additions & 0 deletions tests/ContextProvider/DefaultUnleashContextProviderTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

namespace Unleash\Client\Tests\ContextProvider;

use PHPUnit\Framework\TestCase;
use Unleash\Client\Configuration\UnleashContext;
use Unleash\Client\ContextProvider\DefaultUnleashContextProvider;

final class DefaultUnleashContextProviderTest extends TestCase
{
public function testGetContext()
{
$instance = new DefaultUnleashContextProvider();
self::assertInstanceOf(UnleashContext::class, $instance->getContext());
self::assertNull($instance->getContext()->getCurrentUserId());
self::assertNull($instance->getContext()->getIpAddress());
self::assertNull($instance->getContext()->getSessionId());
self::assertNotSame($instance->getContext(), $instance->getContext());

$instance = new DefaultUnleashContextProvider(new UnleashContext('123', '456', '789'));
self::assertInstanceOf(UnleashContext::class, $instance->getContext());
self::assertEquals('123', $instance->getContext()->getCurrentUserId());
self::assertEquals('456', $instance->getContext()->getIpAddress());
self::assertEquals('789', $instance->getContext()->getSessionId());
self::assertNotSame($instance->getContext(), $instance->getContext());
}

public function testSetDefaultContext()
{
$instance = new DefaultUnleashContextProvider();
$context = new UnleashContext('123', '456', '789');
$instance->setDefaultContext($context);

self::assertInstanceOf(UnleashContext::class, $instance->getContext());
self::assertEquals('123', $instance->getContext()->getCurrentUserId());
self::assertEquals('456', $instance->getContext()->getIpAddress());
self::assertEquals('789', $instance->getContext()->getSessionId());
self::assertNotSame($instance->getContext(), $instance->getContext());
}
}
Loading

0 comments on commit d81e76a

Please sign in to comment.