Skip to content

Commit

Permalink
Feature: Add application hostname strategy (#24)
Browse files Browse the repository at this point in the history
  • Loading branch information
RikudouSage committed Aug 19, 2021
1 parent 48ae6cb commit 3603a39
Show file tree
Hide file tree
Showing 9 changed files with 205 additions and 5 deletions.
29 changes: 28 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ $builder = $builder
## Strategies

Unleash servers can use multiple strategies for enabling or disabling features. Which strategy gets used is defined
on the server. This implementation supports all non-deprecated v4 strategies except hostnames.
on the server. This implementation supports all non-deprecated v4 strategies.
[More here](https://docs.getunleash.io/user_guide/activation_strategy).

### Default strategy
Expand Down Expand Up @@ -356,6 +356,33 @@ $unleash->isEnabled('some-feature'); // works because the session is started
$unleash->isEnabled('some-feature');
```

### Hostname strategy

This strategy allows you to match against a list of server hostnames (which are not the same as http hostnames).

If you don't specify a hostname in context, it defaults to the current hostname using
[`gethostname()`](https://www.php.net/gethostname).

```php
<?php

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

$unleash = UnleashBuilder::create()
->withAppName('Some app name')
->withAppUrl('https://some-app-url.com')
->withInstanceId('Some instance id')
->build();

// context with custom hostname
$context = new UnleashContext(hostname: 'My-Cool-Hostname');
$enabled = $unleash->isEnabled('some-feature', $context);

// without custom hostname, defaults to gethostname() result or null
$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.
Expand Down
4 changes: 4 additions & 0 deletions src/Configuration/Context.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

namespace Unleash\Client\Configuration;

/**
* @method string|null getHostname()
* @method Context setHostname(string|null $hostname)
*/
interface Context
{
public function getCurrentUserId(): ?string;
Expand Down
18 changes: 18 additions & 0 deletions src/Configuration/UnleashContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ public function __construct(
private ?string $ipAddress = null,
private ?string $sessionId = null,
private array $customContext = [],
?string $hostname = null,
) {
$this->setHostname($hostname);
}

public function getCurrentUserId(): ?string
Expand Down Expand Up @@ -87,6 +89,22 @@ public function setSessionId(?string $sessionId): self
return $this;
}

public function getHostname(): ?string
{
return $this->findContextValue('hostname') ?? (gethostname() ?: null);
}

public function setHostname(?string $hostname): self
{
if ($hostname === null) {
$this->removeCustomProperty('hostname');
} else {
$this->setCustomProperty('hostname', $hostname);
}

return $this;
}

/**
* @param array<string> $values
*/
Expand Down
1 change: 1 addition & 0 deletions src/DTO/DefaultConstraint.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ final class DefaultConstraint implements Constraint
*/
public function __construct(
private string $contextName,
#[ExpectedValues(valuesFromClass: ConstraintOperator::class)]
private string $operator,
private array $values,
) {
Expand Down
34 changes: 34 additions & 0 deletions src/Strategy/ApplicationHostnameStrategyHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

namespace Unleash\Client\Strategy;

use Unleash\Client\Configuration\Context;
use Unleash\Client\DTO\Strategy;

final class ApplicationHostnameStrategyHandler extends AbstractStrategyHandler
{
public function getStrategyName(): string
{
return 'applicationHostname';
}

public function isEnabled(Strategy $strategy, Context $context): bool
{
if (!$hostnames = $this->findParameter('hostNames', $strategy)) {
return false;
}

$hostnames = array_map('trim', explode(',', $hostnames));
$enabled = in_array($context->getHostname(), $hostnames, true);

if (!$enabled) {
return false;
}

if (!$this->validateConstraints($strategy, $context)) {
return false;
}

return true;
}
}
2 changes: 2 additions & 0 deletions src/UnleashBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use Unleash\Client\Metrics\DefaultMetricsSender;
use Unleash\Client\Repository\DefaultUnleashRepository;
use Unleash\Client\Stickiness\MurmurHashCalculator;
use Unleash\Client\Strategy\ApplicationHostnameStrategyHandler;
use Unleash\Client\Strategy\DefaultStrategyHandler;
use Unleash\Client\Strategy\GradualRolloutRandomStrategyHandler;
use Unleash\Client\Strategy\GradualRolloutSessionIdStrategyHandler;
Expand Down Expand Up @@ -77,6 +78,7 @@ public function __construct()
new IpAddressStrategyHandler(),
new UserIdStrategyHandler(),
$rolloutStrategyHandler,
new ApplicationHostnameStrategyHandler(),
new GradualRolloutUserIdStrategyHandler($rolloutStrategyHandler),
new GradualRolloutSessionIdStrategyHandler($rolloutStrategyHandler),
new GradualRolloutRandomStrategyHandler($rolloutStrategyHandler),
Expand Down
19 changes: 19 additions & 0 deletions tests/Configuration/UnleashContextTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,4 +85,23 @@ public function testHasMatchingFieldValue()
self::assertFalse($context->hasMatchingFieldValue(ContextField::USER_ID, []));
self::assertFalse($context->hasMatchingFieldValue('nonexistent', ['someValue']));
}

public function testHostname()
{
// for most standard systems, the hostname should not be null
$context = new UnleashContext();
self::assertNotNull($context->getHostname());

$context = new UnleashContext(null, null, null, [], 'myCustomHostname');
self::assertEquals('myCustomHostname', $context->getHostname());

$context = new UnleashContext();
$context->setHostname('customHostname');
self::assertEquals('customHostname', $context->getHostname());
self::assertTrue($context->hasCustomProperty('hostname'));
self::assertEquals('customHostname', $context->findContextValue('hostname'));

$context->setHostname(null);
self::assertFalse($context->hasCustomProperty('hostname'));
}
}
95 changes: 95 additions & 0 deletions tests/Strategy/ApplicationHostnameStrategyHandlerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?php

namespace Unleash\Client\Tests\Strategy;

use PHPUnit\Framework\TestCase;
use Unleash\Client\Configuration\UnleashContext;
use Unleash\Client\DTO\DefaultConstraint;
use Unleash\Client\DTO\DefaultStrategy;
use Unleash\Client\Enum\ConstraintOperator;
use Unleash\Client\Strategy\ApplicationHostnameStrategyHandler;

final class ApplicationHostnameStrategyHandlerTest extends TestCase
{
/**
* @var ApplicationHostnameStrategyHandler
*/
private $instance;

protected function setUp(): void
{
$this->instance = new ApplicationHostnameStrategyHandler();
}

public function testSupports()
{
self::assertFalse($this->instance->supports(new DefaultStrategy('default', [])));
self::assertFalse($this->instance->supports(new DefaultStrategy('flexibleRollout', [])));
self::assertFalse($this->instance->supports(new DefaultStrategy('remoteAddress', [])));
self::assertFalse($this->instance->supports(new DefaultStrategy('userWithId', [])));
self::assertFalse($this->instance->supports(new DefaultStrategy('nonexistent', [])));
self::assertTrue($this->instance->supports(new DefaultStrategy('applicationHostname', [])));
}

public function testIsEnabled()
{
self::assertFalse($this->instance->isEnabled(
new DefaultStrategy('applicationHostname', []),
new UnleashContext()
));

self::assertFalse($this->instance->isEnabled(
new DefaultStrategy('applicationHostname', [
'hostNames' => 'test1,test2',
]),
new UnleashContext()
));

self::assertTrue($this->instance->isEnabled(
new DefaultStrategy('applicationHostname', [
'hostNames' => 'test1,test2',
]),
(new UnleashContext())->setHostname('test1')
));
self::assertTrue($this->instance->isEnabled(
new DefaultStrategy('applicationHostname', [
'hostNames' => 'test1,test2',
]),
(new UnleashContext())->setHostname('test2')
));

self::assertFalse($this->instance->isEnabled(
new DefaultStrategy('applicationHostname', [
'hostNames' => 'test1,test2',
]),
(new UnleashContext())->setHostname('test3')
));

$currentHostname = (new UnleashContext())->getHostname();
self::assertTrue(
$this->instance->isEnabled(
new DefaultStrategy('applicationHostname', [
'hostNames' => $currentHostname,
]),
new UnleashContext()
)
);

self::assertFalse($this->instance->isEnabled(
new DefaultStrategy('applicationHostname', [
'hostNames' => 'test1,test2',
], [
new DefaultConstraint('something', ConstraintOperator::IN, ['test']),
]),
(new UnleashContext())->setHostname('test2')
));
self::assertTrue($this->instance->isEnabled(
new DefaultStrategy('applicationHostname', [
'hostNames' => 'test1,test2',
], [
new DefaultConstraint('something', ConstraintOperator::IN, ['test']),
]),
(new UnleashContext())->setHostname('test2')->setCustomProperty('something', 'test')
));
}
}
8 changes: 4 additions & 4 deletions tests/UnleashBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public function testWithStrategies()
$strategiesProperty = (new ReflectionObject($this->instance))->getProperty('strategies');
$strategiesProperty->setAccessible(true);

self::assertCount(7, $strategiesProperty->getValue($this->instance));
self::assertCount(8, $strategiesProperty->getValue($this->instance));
$instance = $this->instance->withStrategies(new class implements StrategyHandler {
public function supports(Strategy $strategy): bool
{
Expand Down Expand Up @@ -140,7 +140,7 @@ public function testBuild()
self::assertEquals('test', $configuration->getInstanceId());
self::assertNotNull($configuration->getCache());
self::assertIsInt($configuration->getTtl());
self::assertCount(7, $strategies);
self::assertCount(8, $strategies);

$requestFactory = new HttpFactory();
$httpClient = new Client();
Expand Down Expand Up @@ -414,7 +414,7 @@ public function testWithStrategy()
$strategiesProperty = (new ReflectionObject($this->instance))->getProperty('strategies');
$strategiesProperty->setAccessible(true);

self::assertCount(7, $strategiesProperty->getValue($this->instance));
self::assertCount(8, $strategiesProperty->getValue($this->instance));
$instance = $this->instance->withStrategy(new class implements StrategyHandler {
public function supports(Strategy $strategy): bool
{
Expand All @@ -431,7 +431,7 @@ public function isEnabled(Strategy $strategy, Context $context): bool
return false;
}
});
self::assertCount(8, $strategiesProperty->getValue($instance));
self::assertCount(9, $strategiesProperty->getValue($instance));
}

public function testWithDefaultContext()
Expand Down

0 comments on commit 3603a39

Please sign in to comment.