...
 
Commits (2)
bin/
vendor/
.phpunit.result.cache
bin
.php_cs.cache
.idea
.vscode
\ No newline at end of file
......@@ -20,6 +20,12 @@ before_script:
stages:
- test
lint:
stage: test
script:
- bin/php-cs-fixer fix --allow-risky=yes --verbose --dry-run
test:
stage: test
script:
- php composer.phar test
- php composer.phar test -- -f pretty
\ No newline at end of file
<?php
$finder = PhpCsFixer\Finder::create()
->exclude(['vendor','lib','classes'])
->in(__DIR__);
return PhpCsFixer\Config::create()
->setRules([
'@PSR2' => true,
'strict_param' => true,
'array_syntax' => ['syntax' => 'short'],
'no_blank_lines_after_class_opening' => true,
])
->setFinder($finder);
......@@ -2,16 +2,26 @@
"name": "minds/unleash-client-php",
"type": "library",
"description": "PHP client for Unleash",
"keywords": ["unleash", "client", "minds"],
"keywords": [
"unleash",
"client",
"minds"
],
"homepage": "https://gitlab.com/minds/unleash-client-php",
"license": "MIT",
"authors": [
{
"name": "Brian Hatchet",
"email": "brian@minds.com",
"homepage": "http://www.minds.com/brianhatchet",
"role": "Developer"
}
{
"name": "Brian Hatchet",
"email": "brian@minds.com",
"homepage": "http://www.minds.com/brianhatchet",
"role": "Developer"
},
{
"name": "Emiliano Balbuena",
"email": "emiliano@minds.com",
"homepage": "http://www.minds.com/edgebal",
"role": "Developer"
}
],
"minimum-stability": "dev",
"config": {
......@@ -23,17 +33,19 @@
"monolog/monolog": "^2.0@dev",
"psr/simple-cache": "^1.0@dev",
"zendframework/zend-cache": "^2.9@dev",
"zendframework/zend-serializer": "^2.9@dev"
"zendframework/zend-serializer": "^2.9@dev",
"lastguest/murmurhash": "dev-master"
},
"require-dev": {
"bossa/phpspec2-expect": "^3.0",
"phpspec/phpspec": "^4.0",
"phpspec/prophecy": "~1.0"
"bossa/phpspec2-expect": "^3.0",
"phpspec/phpspec": "^4.0",
"phpspec/prophecy": "~1.0",
"friendsofphp/php-cs-fixer": "^2.17@dev"
},
"autoload": {
"files": [
"src/Client.php",
"src/Unleash.php"
"src/Client.php",
"src/Unleash.php"
],
"psr-4": {
"Minds\\UnleashClient\\": "src/",
......
This diff is collapsed.
<?php
require('vendor/autoload.php');
use Minds\UnleashClient\Config;
use Minds\UnleashClient\Entities\Context;
use Minds\UnleashClient\Logger;
use Minds\UnleashClient\Unleash;
function main() : void
{
$logger = new Logger();
$logger->debug('Unleash client demo');
$config = new Config(
"https://gitlab.com/api/v4/feature_flags/unleash/14894840/",
"F2qZp9PyWKXDas9mkEsH",
"test",
300,
15
);
$context = new Context();
$context
->setUserId('1000')
->setSessionId('asdasdqweqwe123123')
->setRemoteAddress('127.0.0.1')
->setHostName('www.minds.com');
$unleash = new Unleash($config, $logger);
$unleash
->setContext($context);
$enabled = $unleash->isEnabled('test', false);
$logger->info("'test' flag evaluates to {$enabled}");
$enabled = $unleash->isEnabled('test-fiftypercent', false);
$logger->info("'test-fiftypercent' flag evaluates to {$enabled}");
$enabled = $unleash->isEnabled('test-selective', false);
$logger->info("'test-selective' flag evaluates to {$enabled}");
$enabled = $unleash->isEnabled('test_on_test', false);
$logger->info("'test_on_test' flag evaluates to {$enabled}");
}
main();
<?php
require('vendor/autoload.php');
use Minds\UnleashClient\Helpers\NormalizedValue;
use Minds\UnleashClient\Logger;
function main()
{
$logger = new Logger();
$logger->debug('Unleash normalizer stats');
$normalizedValue = new NormalizedValue();
$stats = array_fill(1, 100, 0);
$min = 1;
$max = 999999;
$groupId = 'default';
for ($i = $min; $i <= $max; $i++) {
$id = $normalizedValue->build("$i", $groupId, 100, 1);
$stats[(int) $id]++;
}
var_export($stats);
}
main();
......@@ -8,6 +8,7 @@ use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
use GuzzleHttp\Client as HttpClient;
use Psr\Http\Message\ResponseInterface;
use Psr\Log\LoggerInterface;
class ClientSpec extends ObjectBehavior
{
......@@ -16,22 +17,33 @@ class ClientSpec extends ObjectBehavior
private const TEST_INSTANCE_ID = "test_instance_id";
private const TEST_POLLING_INTERVAL_SECONDS = 15;
private const TEST_METRICS_INTERVAL_SECONDS = 20;
/** @var Config; */
private $config;
/** @var LoggerInterface */
private $logger;
/** @var HttpClient */
private $httpClient;
public function let(Config $config, HttpClient $httpClient)
{
public function let(
Config $config,
LoggerInterface $logger,
HttpClient $httpClient
) {
$this->config = $config;
$this->logger = $logger;
$this->httpClient = $httpClient;
$this->config->getApiUrl()->willReturn(ClientSpec::TEST_URL);
$this->config->getApplicationName()->willReturn(ClientSpec::TEST_APPLICATION_NAME);
$this->config->getInstanceId()->willReturn(ClientSpec::TEST_INSTANCE_ID);
$this->config->getPollingIntervalSeconds()->willReturn(ClientSpec::TEST_POLLING_INTERVAL_SECONDS);
$this->config->getMetricsIntervalSeconds()->willReturn(ClientSpec::TEST_METRICS_INTERVAL_SECONDS);
$this->config->getVersion()->willReturn(Config::VERSION);
$this->beConstructedWith($this->config, $this->httpClient);
$this->beConstructedWith($this->config, $this->logger, $this->httpClient);
}
public function it_is_initializable()
......
......@@ -59,7 +59,7 @@ class ConfigSpec extends ObjectBehavior
$applicationName = "new_application_name";
$pollingIntervalSeconds = 100;
$metricsIntervalSeconds = 200;
$this->getApiUrl()->shouldEqual(ConfigSpec::TEST_URL);
$this->getInstanceId()->shouldEqual(ConfigSpec::TEST_INSTANCE_ID);
$this->getApplicationName()->shouldEqual(ConfigSpec::TEST_APPLICATION_NAME);
......
......@@ -3,40 +3,61 @@
namespace spec\Minds\UnleashClient\Entities;
use Minds\UnleashClient\Entities\Context;
use Minds\UnleashClient\Config;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
class ContextSpec extends ObjectBehavior
{
private const TEST_APP_NAME = "test_app_name";
private const TEST_USER_ID = "test_user_id";
private const TEST_SESSION_ID = "test_session_id";
private const TEST_REMOTE_ADDRESS = "test_remote_address";
private const TEST_ENVIRONMENT = "test_environment";
public function it_is_initializable()
{
$this->shouldHaveType(Context::class);
}
/** @var Config */
private $config;
public function it_should_set_and_get_user_id()
{
$this
->getUserId()
->shouldReturn(null);
$this
->setUserId('phpspec')
->getUserId()
->shouldReturn('phpspec');
}
public function let(Config $config)
public function it_should_set_and_get_session_id()
{
$this->config = $config;
$this->config->getApplicationName()
->willReturn(ContextSpec::TEST_APP_NAME);
$this->beConstructedWith(
$this->config,
ContextSpec::TEST_USER_ID,
ContextSpec::TEST_SESSION_ID,
ContextSpec::TEST_REMOTE_ADDRESS,
ContextSpec::TEST_ENVIRONMENT
);
$this
->getSessionId()
->shouldReturn(null);
$this
->setSessionId('phpspec')
->getSessionId()
->shouldReturn('phpspec');
}
public function it_should_set_and_get_remote_address()
{
$this
->getRemoteAddress()
->shouldReturn(null);
$this
->setRemoteAddress('127.0.0.1')
->getRemoteAddress()
->shouldReturn('127.0.0.1');
}
public function it_is_initializable()
public function it_should_set_and_get_host_name()
{
$this->shouldHaveType(Context::class);
$this
->getHostName()
->shouldReturn(null);
$this
->setHostName('phpspec.test')
->getHostName()
->shouldReturn('phpspec.test');
}
}
<?php
namespace spec\Minds\UnleashClient\Entities;
use Minds\UnleashClient\Entities\Feature;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
class FeatureSpec extends ObjectBehavior
{
public function it_is_initializable()
{
$this->shouldHaveType(Feature::class);
}
public function it_should_set_and_get_name()
{
$this
->getName()
->shouldReturn('');
$this
->setName('phpspec')
->getName()
->shouldReturn('phpspec');
}
public function it_should_set_and_get_description()
{
$this
->getDescription()
->shouldReturn('');
$this
->setDescription('phpspec')
->getDescription()
->shouldReturn('phpspec');
}
public function it_should_set_and_get_enabled_flag()
{
$this
->isEnabled()
->shouldReturn(false);
$this
->setEnabled(true)
->isEnabled()
->shouldReturn(true);
$this
->setEnabled(false)
->isEnabled()
->shouldReturn(false);
}
}
<?php
namespace spec\Minds\UnleashClient\Entities;
use Minds\UnleashClient\Entities\Strategy;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
class StrategySpec extends ObjectBehavior
{
public function it_is_initializable()
{
$this->shouldHaveType(Strategy::class);
}
public function it_should_set_and_get_name()
{
$this
->getName()
->shouldReturn('');
$this
->setName('phpspec')
->getName()
->shouldReturn('phpspec');
}
public function it_should_set_and_get_parameters()
{
$this
->getParameters()
->shouldReturn([]);
$this
->setParameters([
'phpspec' => 1
])
->getParameters()
->shouldReturn([
'phpspec' => 1
]);
}
}
<?php
namespace spec\Minds\UnleashClient\Factories;
use Minds\UnleashClient\Entities\Feature;
use Minds\UnleashClient\Entities\Strategy;
use Minds\UnleashClient\Factories\FeatureFactory;
use Minds\UnleashClient\Factories\StrategyFactory;
use PhpSpec\Exception\Example\FailureException;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
class FeatureFactorySpec extends ObjectBehavior
{
/** @var StrategyFactory */
protected $strategyFactory;
public function let(
StrategyFactory $strategyFactory
) {
$this->strategyFactory = $strategyFactory;
$this->beConstructedWith($strategyFactory);
}
public function it_is_initializable()
{
$this->shouldHaveType(FeatureFactory::class);
}
public function it_should_build(
Strategy $strategy1,
Strategy $strategy2
) {
$this->strategyFactory->build(['strategy.1'])
->shouldBeCalled()
->willReturn($strategy1);
$this->strategyFactory->build(['strategy.2'])
->shouldBeCalled()
->willReturn($strategy2);
$this
->build([
'name' => 'phpspec',
'description' => 'a phpspec feature',
'enabled' => true,
'strategies' => [
['strategy.1'],
['strategy.2']
]
])
->shouldBeAFeature([
'name' => 'phpspec',
'description' => 'a phpspec feature',
'enabled' => true,
'strategies' => [
$strategy1,
$strategy2
]
]);
}
public function getMatchers(): array
{
return [
'beAFeature' => function ($subject, $data) {
if (!($subject instanceof Feature)) {
throw new FailureException(sprintf("Subject should be a %s instance", Feature::class));
}
if ($subject->getName() !== $data['name']) {
throw new FailureException('Unexpected subject getName() value');
}
if ($subject->getDescription() !== $data['description']) {
throw new FailureException('Unexpected subject getDescription() value');
}
if ($subject->isEnabled() !== $data['enabled']) {
throw new FailureException('Unexpected subject isEnabled() value');
}
if ($subject->getStrategies() !== $data['strategies']) {
throw new FailureException('Unexpected subject getStrategies() value');
}
return true;
}
];
}
}
<?php
namespace spec\Minds\UnleashClient\Factories;
use Minds\UnleashClient\Entities\Strategy;
use Minds\UnleashClient\Factories\StrategyAlgorithmFactory;
use Minds\UnleashClient\StrategyAlgorithms\DefaultStrategyAlgorithm;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
use Psr\Log\LoggerInterface;
class StrategyAlgorithmFactorySpec extends ObjectBehavior
{
/** @var LoggerInterface */
protected $logger;
public function let(
LoggerInterface $logger
) {
$this->logger = $logger;
$this->beConstructedWith($logger);
}
public function it_is_initializable()
{
$this->shouldHaveType(StrategyAlgorithmFactory::class);
}
public function it_should_build(
Strategy $strategy
) {
$strategy->getName()
->shouldBeCalled()
->willReturn('default');
$this
->build($strategy)
->shouldBeAnInstanceOf(DefaultStrategyAlgorithm::class);
}
public function it_should_return_null_if_class_does_not_exist(
Strategy $strategy
) {
$strategy->getName()
->shouldBeCalled()
->willReturn('php~notexisting~class');
$this->logger->warning(Argument::cetera())
->shouldBeCalled();
$this
->build($strategy)
->shouldReturn(null);
}
}
<?php
namespace spec\Minds\UnleashClient\Factories;
use Minds\UnleashClient\Entities\Strategy;
use Minds\UnleashClient\Factories\StrategyFactory;
use PhpSpec\Exception\Example\FailureException;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
class StrategyFactorySpec extends ObjectBehavior
{
public function it_is_initializable()
{
$this->shouldHaveType(StrategyFactory::class);
}
public function it_should_build()
{
$this
->build([
'name' => 'phpspec',
'parameters' => [
'test' => 1,
]
])
->shouldBeAStrategy([
'name' => 'phpspec',
'parameters' => [
'test' => 1,
]
]);
}
public function getMatchers(): array
{
return [
'beAStrategy' => function ($subject, $data) {
if (!($subject instanceof Strategy)) {
throw new FailureException(sprintf("Subject should be a %s instance", Strategy::class));
}
if ($subject->getName() !== $data['name']) {
throw new FailureException('Unexpected subject getName() value');
}
if ($subject->getParameters() !== $data['parameters']) {
throw new FailureException('Unexpected subject getParameters() value');
}
return true;
}
];
}
}
<?php
namespace spec\Minds\UnleashClient\Helpers;
use Minds\UnleashClient\Helpers\NormalizedValue;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
class NormalizedValueSpec extends ObjectBehavior
{
public function it_is_initializable()
{
$this->shouldHaveType(NormalizedValue::class);
}
public function it_should_build()
{
$this
->build('', 'test', 100)
->shouldReturn(0);
$this
->build('phpspec', 'test', 100)
->shouldReturn(96);
$this
->build('minds', 'test', 100)
->shouldReturn(89);
}
}
<?php
namespace spec\Minds\UnleashClient\StrategyAlgorithms;
use Minds\UnleashClient\Entities\Context;
use Minds\UnleashClient\Entities\Strategy;
use Minds\UnleashClient\StrategyAlgorithms\ApplicationHostnameStrategyAlgorithm;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
use Psr\Log\LoggerInterface;
class ApplicationHostnameStrategyAlgorithmSpec extends ObjectBehavior
{
/** @var LoggerInterface */
protected $logger;
public function let(
LoggerInterface $logger
) {
$this->logger = $logger;
$this->beConstructedWith($logger);
}
public function it_is_initializable()
{
$this->shouldHaveType(ApplicationHostnameStrategyAlgorithm::class);
}
public function it_should_check_it_is_enabled(
Strategy $strategy,
Context $context
) {
$strategy->getParameters()
->shouldBeCalled()
->willReturn([
'hostNames' => 'foo.bar, phpspec.test, minds.com'
]);
$context->getHostName()
->shouldBeCalled()
->willReturn('phpspec.test');
$this
->isEnabled($strategy, $context)
->shouldReturn(true);
}
public function it_should_check_it_is_not_enabled(
Strategy $strategy,
Context $context
) {
$strategy->getParameters()
->shouldBeCalled()
->willReturn([
'hostNames' => 'foo.bar, phpspec.test, minds.com'
]);
$context->getHostName()
->shouldBeCalled()
->willReturn('notawhitelisteddomain.com');
$this
->isEnabled($strategy, $context)
->shouldReturn(false);
}
}
<?php
namespace spec\Minds\UnleashClient\StrategyAlgorithms;
use Minds\UnleashClient\Entities\Context;
use Minds\UnleashClient\Entities\Strategy;
use Minds\UnleashClient\StrategyAlgorithms\DefaultStrategyAlgorithm;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
use Psr\Log\LoggerInterface;
class DefaultStrategyAlgorithmSpec extends ObjectBehavior
{
/** @var LoggerInterface */
protected $logger;
public function let(
LoggerInterface $logger
) {
$this->logger = $logger;
$this->beConstructedWith($logger);
}
public function it_is_initializable()
{
$this->shouldHaveType(DefaultStrategyAlgorithm::class);
}
public function it_should_check_it_is_enabled(
Strategy $strategy,
Context $context
) {
$this
->isEnabled($strategy, $context)
->shouldReturn(true);
}
}
<?php
namespace spec\Minds\UnleashClient\StrategyAlgorithms;
use Minds\UnleashClient\Entities\Context;
use Minds\UnleashClient\Entities\Strategy;
use Minds\UnleashClient\Helpers\NormalizedValue;
use Minds\UnleashClient\StrategyAlgorithms\FlexibleRolloutStrategyAlgorithm;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
use Psr\Log\LoggerInterface;
class FlexibleRolloutStrategyAlgorithmSpec extends ObjectBehavior
{
/** @var LoggerInterface */
protected $logger;
/** @var NormalizedValue */
protected $normalizedValue;
public function let(
LoggerInterface $logger,
NormalizedValue $normalizedValue
) {
$this->logger = $logger;
$this->normalizedValue = $normalizedValue;
$this->beConstructedWith($logger, $normalizedValue);
}
public function it_is_initializable()
{
$this->shouldHaveType(FlexibleRolloutStrategyAlgorithm::class);
}
public function it_should_check_it_is_enabled_with_user_id_stickiness(
Strategy $strategy,
Context $context
) {
$strategy->getParameters()
->shouldBeCalled()
->willReturn([
'rollout' => 20,
'stickiness' => 'userId',
'groupId' => 'test'
]);
$context->getUserId()
->shouldBeCalled()
->willReturn('1000');
$context->getSessionId()
->willReturn(null);
$this->normalizedValue->random(999999, 1)
->willReturn(99);
$this->normalizedValue->build('1000', 'test')
->shouldBeCalled()
->willReturn(10);
$this
->isEnabled($strategy, $context)
->shouldReturn(true);
}
public function it_should_check_it_is_not_enabled_with_user_id_stickiness(
Strategy $strategy,
Context $context
) {
$strategy->getParameters()
->shouldBeCalled()
->willReturn([
'rollout' => 20,
'stickiness' => 'userId',
'groupId' => 'test'
]);
$context->getUserId()
->shouldBeCalled()
->willReturn('1000');
$context->getSessionId()
->willReturn(null);
$this->normalizedValue->random(999999, 1)
->willReturn(99);
$this->normalizedValue->build('1000', 'test')
->shouldBeCalled()
->willReturn(90);
$this
->isEnabled($strategy, $context)
->shouldReturn(false);
}
public function it_should_check_it_is_enabled_with_session_id_stickiness(
Strategy $strategy,
Context $context
) {
$strategy->getParameters()
->shouldBeCalled()
->willReturn([
'rollout' => 20,
'stickiness' => 'sessionId',
'groupId' => 'test'
]);
$context->getUserId()
->willReturn(null);
$context->getSessionId()
->shouldBeCalled()
->willReturn('phpspec~123123');
$this->normalizedValue->random(999999, 1)
->willReturn(99);
$this->normalizedValue->build('phpspec~123123', 'test')
->shouldBeCalled()
->willReturn(10);
$this
->isEnabled($strategy, $context)
->shouldReturn(true);
}
public function it_should_check_it_is_not_enabled_with_session_id_stickiness(
Strategy $strategy,
Context $context
) {
$strategy->getParameters()
->shouldBeCalled()
->willReturn([
'rollout' => 20,
'stickiness' => 'sessionId',
'groupId' => 'test'
]);
$context->getUserId()
->willReturn(null);
$context->getSessionId()
->shouldBeCalled()
->willReturn('phpspec~123123');
$this->normalizedValue->random(999999, 1)
->willReturn(99);
$this->normalizedValue->build('phpspec~123123', 'test')
->shouldBeCalled()
->willReturn(90);
$this
->isEnabled($strategy, $context)
->shouldReturn(false);
}
public function it_should_check_it_is_enabled_with_random_stickiness(
Strategy $strategy,
Context $context
) {
$strategy->getParameters()
->shouldBeCalled()
->willReturn([
'rollout' => 20,
'stickiness' => 'random',
'groupId' => 'test'
]);
$context->getUserId()
->willReturn(null);
$context->getSessionId()
->willReturn(null);
$this->normalizedValue->random(999999, 1)
->willReturn(99);
$this->normalizedValue->random(999999, 1)
->shouldBeCalled()
->willReturn(99);
$this->normalizedValue->build('99', 'test')
->shouldBeCalled()
->willReturn(10);
$this
->isEnabled($strategy, $context)
->shouldReturn(true);
}
public function it_should_check_it_is_not_enabled_with_random_stickiness(
Strategy $strategy,
Context $context
) {
$strategy->getParameters()
->shouldBeCalled()
->willReturn([
'rollout' => 20,
'stickiness' => 'random',
'groupId' => 'test'
]);
$context->getUserId()
->willReturn(null);
$context->getSessionId()
->willReturn(null);
$this->normalizedValue->random(999999, 1)
->shouldBeCalled()
->willReturn(99);
$this->normalizedValue->build('99', 'test')
->shouldBeCalled()
->willReturn(90);
$this
->isEnabled($strategy, $context)
->shouldReturn(false);
}
public function it_should_check_it_is_enabled_with_default_stickiness_using_user_id(
Strategy $strategy,
Context $context
) {
$strategy->getParameters()
->shouldBeCalled()
->willReturn([
'rollout' => 20,
'stickiness' => 'default',
'groupId' => 'test'
]);
$context->getUserId()
->shouldBeCalled()
->willReturn('1000');
$context->getSessionId()
->willReturn(null);
$this->normalizedValue->random(999999, 1)
->willReturn(99);
$this->normalizedValue->build('1000', 'test')
->shouldBeCalled()
->willReturn(10);
$this
->isEnabled($strategy, $context)
->shouldReturn(true);
}
public function it_should_check_it_is_enabled_with_default_stickiness_using_session_id(
Strategy $strategy,
Context $context
) {
$strategy->getParameters()
->shouldBeCalled()
->willReturn([
'rollout' => 20,
'stickiness' => 'default',
'groupId' => 'test'
]);
$context->getUserId()
->willReturn(null);
$context->getSessionId()
->shouldBeCalled()
->willReturn('phpspec~123123');
$this->normalizedValue->random(999999, 1)
->willReturn(99);
$this->normalizedValue->build('phpspec~123123', 'test')
->shouldBeCalled()
->willReturn(10);
$this
->isEnabled($strategy, $context)
->shouldReturn(true);
}
public function it_should_check_it_is_enabled_with_default_stickiness_using_random(
Strategy $strategy,
Context $context
) {
$strategy->getParameters()
->shouldBeCalled()
->willReturn([
'rollout' => 20,
'stickiness' => 'default',
'groupId' => 'test'
]);
$context->getUserId()
->willReturn(null);
$context->getSessionId()
->willReturn(null);
$this->normalizedValue->random(999999, 1)
->shouldBeCalled()
->willReturn(99);
$this->normalizedValue->build('99', 'test')
->shouldBeCalled()
->willReturn(10);
$this
->isEnabled($strategy, $context)
->shouldReturn(true);
}
}
<?php
namespace spec\Minds\UnleashClient\StrategyAlgorithms;
use Minds\UnleashClient\Entities\Context;
use Minds\UnleashClient\Entities\Strategy;
use Minds\UnleashClient\Helpers\NormalizedValue;
use Minds\UnleashClient\StrategyAlgorithms\GradualRolloutRandomStrategyAlgorithm;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
use Psr\Log\LoggerInterface;
class GradualRolloutRandomStrategyAlgorithmSpec extends ObjectBehavior
{
/** @var LoggerInterface */
protected $logger;
/** @var NormalizedValue */
protected $normalizedValue;
public function let(
LoggerInterface $logger,
NormalizedValue $normalizedValue
) {
$this->logger = $logger;
$this->normalizedValue = $normalizedValue;
$this->beConstructedWith($logger, $normalizedValue);
}
public function it_is_initializable()
{
$this->shouldHaveType(GradualRolloutRandomStrategyAlgorithm::class);
}
public function it_should_check_it_is_enabled(
Strategy $strategy,
Context $context
) {
$strategy->getParameters()
->shouldBeCalled()
->willReturn([
'percentage' => 20
]);
$this->normalizedValue->random()
->shouldBeCalled()
->willReturn(10);
$this
->isEnabled($strategy, $context)
->shouldReturn(true);
}
public function it_should_check_it_is_not_enabled(
Strategy $strategy,
Context $context
) {
$strategy->getParameters()
->shouldBeCalled()
->willReturn([
'percentage' => 20
]);
$this->normalizedValue->random()
->shouldBeCalled()
->willReturn(90);
$this
->isEnabled($strategy, $context)
->shouldReturn(false);
}
}
<?php
namespace spec\Minds\UnleashClient\StrategyAlgorithms;
use Minds\UnleashClient\Entities\Context;
use Minds\UnleashClient\Entities\Strategy;
use Minds\UnleashClient\Helpers\NormalizedValue;
use Minds\UnleashClient\StrategyAlgorithms\GradualRolloutSessionIdStrategyAlgorithm;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
use Psr\Log\LoggerInterface;
class GradualRolloutSessionIdStrategyAlgorithmSpec extends ObjectBehavior
{
/** @var LoggerInterface */
protected $logger;
/** @var NormalizedValue */
protected $normalizedValue;
public function let(
LoggerInterface $logger,
NormalizedValue $normalizedValue
) {
$this->logger = $logger;
$this->normalizedValue = $normalizedValue;
$this->beConstructedWith($logger, $normalizedValue);
}
public function it_is_initializable()
{
$this->shouldHaveType(GradualRolloutSessionIdStrategyAlgorithm::class);
}
public function it_should_check_it_is_enabled(
Strategy $strategy,
Context $context
) {
$strategy->getParameters()
->shouldBeCalled()
->willReturn([
'percentage' => 20,
'groupId' => 'test'
]);
$context->getSessionId()
->shouldBeCalled()
->willReturn('phpspec~123');
$this->normalizedValue->build('phpspec~123', 'test')
->shouldBeCalled()
->willReturn(10);
$this
->isEnabled($strategy, $context)
->shouldReturn(true);
}
public function it_should_check_it_is_no_enabled(
Strategy $strategy,
Context $context
) {
$strategy->getParameters()
->shouldBeCalled()
->willReturn([
'percentage' => 20,
'groupId' => 'test'
]);
$context->getSessionId()
->shouldBeCalled()
->willReturn('phpspec~123');
$this->normalizedValue->build('phpspec~123', 'test')
->shouldBeCalled()
->willReturn(90);
$this
->isEnabled($strategy, $context)
->shouldReturn(false);
}
}
<?php
namespace spec\Minds\UnleashClient\StrategyAlgorithms;
use Minds\UnleashClient\Entities\Context;
use Minds\UnleashClient\Entities\Strategy;
use Minds\UnleashClient\Helpers\NormalizedValue;
use Minds\UnleashClient\StrategyAlgorithms\GradualRolloutUserIdStrategyAlgorithm;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
use Psr\Log\LoggerInterface;
class GradualRolloutUserIdStrategyAlgorithmSpec extends ObjectBehavior
{
/** @var LoggerInterface */
protected $logger;
/** @var NormalizedValue */
protected $normalizedValue;
public function let(
LoggerInterface $logger,
NormalizedValue $normalizedValue
) {
$this->logger = $logger;
$this->normalizedValue = $normalizedValue;
$this->beConstructedWith($logger, $normalizedValue);
}
public function it_is_initializable()
{
$this->shouldHaveType(GradualRolloutUserIdStrategyAlgorithm::class);
}
public function it_should_check_it_is_enabled(
Strategy $strategy,
Context $context
) {
$strategy->getParameters()
->shouldBeCalled()
->willReturn([
'percentage' => 20,
'groupId' => 'test'
]);
$context->getUserId()
->shouldBeCalled()
->willReturn('1000');
$this->normalizedValue->build('1000', 'test')
->shouldBeCalled()
->willReturn(10);
$this
->isEnabled($strategy, $context)
->shouldReturn(true);
}
public function it_should_check_it_is_no_enabled(
Strategy $strategy,
Context $context
) {
$strategy->getParameters()
->shouldBeCalled()
->willReturn([
'percentage' => 20,
'groupId' => 'test'
]);
$context->getUserId()
->shouldBeCalled()
->willReturn('1000');
$this->normalizedValue->build('1000', 'test')
->shouldBeCalled()
->willReturn(90);
$this
->isEnabled($strategy, $context)
->shouldReturn(false);
}
}
<?php
namespace spec\Minds\UnleashClient\StrategyAlgorithms;
use Minds\UnleashClient\Entities\Context;
use Minds\UnleashClient\Entities\Strategy;
use Minds\UnleashClient\StrategyAlgorithms\RemoteAddressStrategyAlgorithm;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
use Psr\Log\LoggerInterface;
class RemoteAddressStrategyAlgorithmSpec extends ObjectBehavior
{
/** @var LoggerInterface */
protected $logger;
public function let(
LoggerInterface $logger
) {
$this->logger = $logger;
$this->beConstructedWith($logger);
}
public function it_is_initializable()
{
$this->shouldHaveType(RemoteAddressStrategyAlgorithm::class);
}
public function it_should_check_it_is_enabled(
Strategy $strategy,
Context $context
) {
$strategy->getParameters()
->shouldBeCalled()
->willReturn([
'IPs' => '10.0.0.1, 127.0.0.1, 2001:0db8:85a3:0000:0000:8a2e:0370:7334'
]);
$context->getRemoteAddress()
->shouldBeCalled()
->willReturn('127.0.0.1');
$this
->isEnabled($strategy, $context)
->shouldReturn(true);
}
public function it_should_check_it_is_enabled_with_ipv6_casing(
Strategy $strategy,
Context $context
) {
$strategy->getParameters()
->shouldBeCalled()
->willReturn([
'IPs' => '10.0.0.1, 127.0.0.1, 2001:0db8:85a3:0000:0000:8a2e:0370:7334'
]);
$context->getRemoteAddress()
->shouldBeCalled()
->willReturn('2001:0DB8:85A3:0000:0000:8A2E:0370:7334');
$this
->isEnabled($strategy, $context)
->shouldReturn(true);
}
public function it_should_check_it_is_not_enabled(
Strategy $strategy,
Context $context
) {
$strategy->getParameters()
->shouldBeCalled()
->willReturn([
'IPs' => '10.0.0.1, 127.0.0.1, 2001:0db8:85a3:0000:0000:8a2e:0370:7334'
]);
$context->getRemoteAddress()
->shouldBeCalled()
->willReturn('192.168.0.200');
$this
->isEnabled($strategy, $context)
->shouldReturn(false);
}
}
<?php
namespace spec\Minds\UnleashClient\StrategyAlgorithms;
use Minds\UnleashClient\Entities\Context;
use Minds\UnleashClient\Entities\Strategy;
use Minds\UnleashClient\StrategyAlgorithms\UserWithIdStrategyAlgorithm;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
use Psr\Log\LoggerInterface;
class UserWithIdStrategyAlgorithmSpec extends ObjectBehavior
{
/** @var LoggerInterface */
protected $logger;
public function let(
LoggerInterface $logger
) {
$this->logger = $logger;
$this->beConstructedWith($logger);
}
public function it_is_initializable()
{
$this->shouldHaveType(UserWithIdStrategyAlgorithm::class);
}
public function it_should_check_it_is_enabled(
Strategy $strategy,
Context $context
) {
$strategy->getParameters()
->shouldBeCalled()
->willReturn([
'userIds' => '1000,1005, 1010'
]);
$context->getUserId()
->shouldBeCalled()
->willReturn('1005');
$this
->isEnabled($strategy, $context)
->shouldReturn(true);
}
public function it_should_check_it_is_not_enabled(
Strategy $strategy,
Context $context
) {
$strategy->getParameters()
->shouldBeCalled()
->willReturn([
'userIds' => '1000,1005, 1010'
]);
$context->getUserId()
->shouldBeCalled()
->willReturn('7777');
$this
->isEnabled($strategy, $context)
->shouldReturn(false);
}
}
......@@ -2,29 +2,295 @@
namespace spec\Minds\UnleashClient;
use Exception;
use Minds\UnleashClient\Entities\Context;
use Minds\UnleashClient\Entities\Feature;
use Minds\UnleashClient\Entities\Strategy;
use Minds\UnleashClient\StrategyResolver;
use Minds\UnleashClient\Unleash;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
use Minds\UnleashClient\Config;
use Minds\UnleashClient\Client;
use Zend\Cache\Psr\SimpleCache\SimpleCacheDecorator as Cache;
use Psr\Log\LoggerInterface;
use Psr\SimpleCache\CacheInterface;
class UnleashSpec extends ObjectBehavior
{
/** @var Config */
private $config;
protected $config;
/** @var LoggerInterface */
protected $logger;
/** @var Client */
private $client;
protected $client;
public function let(Config $config, Client $client, Cache $cache)
{
/** @var CacheInterface */
protected $cache;
/** @var StrategyResolver */
protected $strategyResolver;
public function let(
Config $config,
LoggerInterface $logger,
Client $client,
CacheInterface $cache,
StrategyResolver $strategyResolver
) {
$this->config = $config;
$this->logger = $logger;
$this->client = $client;
$this->beConstructedWith($config, $client, $cache);
$this->cache = $cache;
$this->strategyResolver = $strategyResolver;
$this->beConstructedWith(
$config,
$logger,
$client,
$cache,
$strategyResolver
);
}
public function it_is_initializable()
{
$this->shouldHaveType(Unleash::class);
}
public function it_should_check_if_it_is_enabled_from_server(
Feature $feature,
Strategy $strategy,
Context $context
) {
$this->client->getId()
->shouldBeCalled()
->willReturn('phpspec');
$cacheKeyPrefix = Unleash::UNLEASH_CLIENT_CACHE_PREFIX . 'phpspec' . '-';
$this->cache->get($cacheKeyPrefix . Unleash::UNLEASH_CLIENT_CACHE_TIMEOUT_KEY, -1)
->shouldBeCalled()
->willReturn(-1);
$this->client->register()
->shouldBeCalled()
->willReturn(true);
$this->client->getFeatureFlags()
->shouldBeCalled()
->willReturn([$feature]);
$feature->getName()
->shouldBeCalled()
->willReturn('test');
$this->cache->set($cacheKeyPrefix . 'test', $feature)
->shouldBeCalled()
->willReturn(true);
$this->config->getPollingIntervalSeconds()
->shouldBeCalled()
->willReturn(1000);
$this->cache->set(
$cacheKeyPrefix . Unleash::UNLEASH_CLIENT_CACHE_TIMEOUT_KEY,
Argument::type('int') // TODO: Mock time()
)
->shouldBeCalled()
->willReturn(true);
$this->cache->get($cacheKeyPrefix . 'test', null)
->shouldBeCalled()
->willReturn($feature);
$feature->isEnabled()
->shouldBeCalled()
->willReturn(true);
$feature->getStrategies()
->shouldBeCalled()
->willReturn([$strategy]);
$this->strategyResolver->isEnabled(
[$strategy],
$context
)
->shouldBeCalled()
->willReturn(true);
$this
->setContext($context)
->isEnabled('test', false)
->shouldReturn(true);
}
public function it_should_check_if_it_is_enabled_from_cache(
Feature $feature,
Strategy $strategy,
Context $context
) {
$this->client->getId()
->shouldBeCalled()
->willReturn('phpspec');
$cacheKeyPrefix = Unleash::UNLEASH_CLIENT_CACHE_PREFIX . 'phpspec' . '-';
$this->cache->get($cacheKeyPrefix . Unleash::UNLEASH_CLIENT_CACHE_TIMEOUT_KEY, -1)
->shouldBeCalled()
->willReturn(time() + 100000); // TODO: Mock time
$this->client->getFeatureFlags()
->shouldNotBeCalled();
$this->cache->get($cacheKeyPrefix . 'test', null)
->shouldBeCalled()
->willReturn($feature);
$feature->isEnabled()
->shouldBeCalled()
->willReturn(true);
$feature->getStrategies()
->shouldBeCalled()
->willReturn([$strategy]);
$this->strategyResolver->isEnabled(
[$strategy],
$context
)
->shouldBeCalled()
->willReturn(true);
$this
->setContext($context)
->isEnabled('test', false)
->shouldReturn(true);
}
public function it_should_check_if_it_is_not_enabled_on_feature_from_cache(
Feature $feature,
Strategy $strategy,
Context $context
) {
$this->client->getId()
->shouldBeCalled()
->willReturn('phpspec');
$cacheKeyPrefix = Unleash::UNLEASH_CLIENT_CACHE_PREFIX . 'phpspec' . '-';
$this->cache->get($cacheKeyPrefix . Unleash::UNLEASH_CLIENT_CACHE_TIMEOUT_KEY, -1)
->shouldBeCalled()
->willReturn(time() + 100000); // TODO: Mock time
$this->client->getFeatureFlags()
->shouldNotBeCalled();
$this->cache->get($cacheKeyPrefix . 'test', null)
->shouldBeCalled()
->willReturn($feature);
$feature->isEnabled()
->shouldBeCalled()
->willReturn(false);
$this->strategyResolver->isEnabled(Argument::cetera())
->shouldNotBeCalled();
$this
->setContext($context)
->isEnabled('test', false)
->shouldReturn(false);
}
public function it_should_check_if_it_is_not_enabled_on_strategy_from_cache(
Feature $feature,
Strategy $strategy,
Context $context
) {
$this->client->getId()
->shouldBeCalled()
->willReturn('phpspec');
$cacheKeyPrefix = Unleash::UNLEASH_CLIENT_CACHE_PREFIX . 'phpspec' . '-';
$this->cache->get($cacheKeyPrefix . Unleash::UNLEASH_CLIENT_CACHE_TIMEOUT_KEY, -1)
->shouldBeCalled()
->willReturn(time() + 100000); // TODO: Mock time
$this->client->getFeatureFlags()
->shouldNotBeCalled();
$this->cache->get($cacheKeyPrefix . 'test', null)
->shouldBeCalled()
->willReturn($feature);
$feature->isEnabled()
->shouldBeCalled()
->willReturn(true);
$feature->getStrategies()
->shouldBeCalled()
->willReturn([$strategy]);
$this->strategyResolver->isEnabled(
[$strategy],
$context
)
->shouldBeCalled()
->willReturn(false);
$this
->setContext($context)
->isEnabled('test', false)
->shouldReturn(false);
}
public function it_should_return_false_during_is_enabled_if_exception_thrown(
Feature $feature,
Strategy $strategy,
Context $context
) {
$this->client->getId()
->shouldBeCalled()
->willReturn('phpspec');
$cacheKeyPrefix = Unleash::UNLEASH_CLIENT_CACHE_PREFIX . 'phpspec' . '-';
$this->cache->get($cacheKeyPrefix . Unleash::UNLEASH_CLIENT_CACHE_TIMEOUT_KEY, -1)
->shouldBeCalled()
->willReturn(time() + 100000); // TODO: Mock time
$this->client->getFeatureFlags()
->shouldNotBeCalled();
$this->cache->get($cacheKeyPrefix . 'test', null)
->shouldBeCalled()
->willReturn($feature);
$feature->isEnabled()
->shouldBeCalled()
->willReturn(true);
$feature->getStrategies()
->shouldBeCalled()
->willReturn([$strategy]);
$this->strategyResolver->isEnabled(Argument::cetera())
->shouldBeCalled()
->willThrow(new Exception('Failed call'));
$this->logger->debug(Argument::cetera())
->willReturn(null);
$this->logger->error(Argument::cetera())
->shouldBeCalled()
->willReturn(null);
$this
->setContext($context)
->isEnabled('test', false)
->shouldReturn(false);
}
}
<?php
namespace Minds\UnleashClient\Cache;
use Minds\UnleashClient\Logger;
use Psr\Log\LoggerInterface;
use Zend\Cache\Psr\SimpleCache\SimpleCacheDecorator;
use Zend\Cache\Storage\StorageInterface;
use Zend\Cache\StorageFactory;
class SimpleCache extends SimpleCacheDecorator
{
/** @var Logger */
protected $logger;
public function __construct(
LoggerInterface $logger = null
) {
// Setup local dependencies
$this->logger = $logger ?: new Logger();
// Instantiate cache
parent::__construct($this->buildAdapter());
}
/**
* Builds the cache storage adapter
*
* @return StorageInterface
*/
protected function buildAdapter(): StorageInterface
{
$this->logger->debug('Building cache storage adapter');
// Build a simple filesystem based cache
$cache = StorageFactory::adapterFactory('filesystem');
$serializer = StorageFactory::pluginFactory('serializer');
$cache->addPlugin($serializer);
return $cache;
}
}
<?php
namespace Minds\UnleashClient;
require('vendor/autoload.php');
use Minds\UnleashClient\Unleash;
use Minds\UnleashClient\Config;
use Minds\UnleashClient\Logger;
function main() : void
{
$logger = new Logger();
$logger->debug('Unleash client demo');
$config = new Config("https://gitlab.com/api/v4/feature_flags/unleash/14894840/", "F2qZp9PyWKXDas9mkEsH", "test");
$unleash = new Unleash($config);
$logger->debug('Registering client');
$unleash->register();
$logger->debug('Getting available feature flags');
$features = $unleash->getFeatureFlags();
$logger->debug('Checking enabled');
$enabled = $unleash->isEnabled('test', false);
$logger->info("'test' flag evaluates to {$enabled}");
}
main();
......@@ -2,107 +2,148 @@
namespace Minds\UnleashClient;
use Exception;
use GuzzleHttp\Client as HttpClient;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
use GuzzleHttp\MessageFormatter;
use GuzzleHttp\Request;
use Minds\UnleashClient\Config;
use Minds\UnleashClient\Logger;
use Minds\UnleashClient\Factories\FeatureFactory;
use Minds\UnleashClient\Entities\Feature;
use Psr\Log\LoggerInterface;
/**
* Wraps a guzzle http client in unleash specific functions
* Wraps a Guzzle HTTP client in Unleash specific functions
*/
class Client
{
/** @var HttpClient */
private $httpClient;
protected $httpClient;
/** @var Config */
private $config;
protected $config;
/** @var Logger */
private $logger;
protected $logger;
public function __construct(Config $config = null, HttpClient $httpClient = null)
{
$this->logger = new Logger();
/** @var FeatureFactory */
protected $featureFactory;
/** @var string */
protected $id;
/**
* Client constructor.
* @param Config $config
* @param LoggerInterface|null $logger
* @param HttpClient|null $httpClient
* @param FeatureFactory|null $featureFactory
*/
public function __construct(
Config $config,
LoggerInterface $logger = null,
HttpClient $httpClient = null,
FeatureFactory $featureFactory = null
) {
$this->config = $config;
$this->logger = $logger ?: new Logger();
$this->httpClient = $httpClient ?: $this->createHttpClient();
$this->logger->debug("Client configured");
$this->logger->debug("Base uri: " . $this->config->getApiUrl());
$this->featureFactory = $featureFactory ?: new FeatureFactory();
$this->logger->debug("Client configured. Base URL: {$this->config->getApiUrl()}");
}
/**
* Calls the unleash api and registers the client
* Gets the client ID based on configuration
* @return string
*/
public function register(string $date = null) : bool
public function getId(): string
{
if (!$this->id) {
$this->generateId();
}
return $this->id;
}
/**
* Calls the Unleash api and registers the client
* @param string|null $date
* @return bool
*/
public function register(string $date = null): bool
{
$date = $date ?? date("c");
$this->logger->debug('Client->register');
$this->logger->debug('Registering client');
try {
$payload = [
'appName' => $this->config->getApplicationName(),
'instanceId' => $this->config->getInstanceId(),
'sdkVersion' => "unleash-client-php:" . $this->config->getVersion(),
'strategies'=> [],
'strategies' => [],
'started' => $date,
'interval' => $this->config->getMetricsIntervalSeconds() * 1000
];
$this->logger->debug('Register payload', $payload);
$this->logger->debug('Client payload', $payload);
$this->generateId();
$response = $this->httpClient->post('client/register', $payload);
if ($response->getStatusCode() >= 200
&& $response->getStatusCode() < 300) {
return true;
}
} catch (\Exception $ex) {
$this->logger->error($ex->getMessage());
return
$response->getStatusCode() >= 200 &&
$response->getStatusCode() < 300;
} catch (Exception $e) {
$this->logger->error($e);
return false;
}
return false;
}
/**
* Calls the unleash api for getting feature flags
* On a 200, reconstitutes the feature flags and returns an array
* On an error, logs and error and returns an empty array
* @return array Entities\Feature
* Calls the unleash api for getting feature flags.
* If HTTP 2xx, reconstitutes the feature flags and returns an array.
* Else, logs and error and returns an empty array.
* @return Feature[]
*/
public function getFeatureFlags() : array
public function getFeatureFlags(): array
{
$this->logger->debug('Client->getFeatureFlags');
$features = [];
$this->logger->debug('Getting feature flags');
try {
$response = $this->httpClient->get('client/features');
$this->logger->debug('Response: ' .$response->getStatusCode());
if ($response->getStatusCode() >= 200
&& $response->getStatusCode() < 300) {
$this->logger->debug("Got feature flags [{$response->getStatusCode()}]");
if (
$response->getStatusCode() >= 200 &&
$response->getStatusCode() < 300
) {
$data = json_decode((string) $response->getBody(), true);
foreach ($data['features'] as $feature) {
$features[] = new Feature($feature);
}
return array_map(function ($feature) {
return $this->featureFactory->build($feature);
}, $data['features']);
}
} catch (\Exception $ex) {
$this->logger->error($ex->getMessage());
} catch (Exception $e) {
$this->logger->error($e);
}
$this->logger->debug("Returning " . count($features) . " features", $features);
return $features;
}
/**
* Sends
*/
return [];
}
/**
* Creates an http client with the auth headers
* and middleware
*/
private function createHttpClient() : HttpClient
*/
protected function createHttpClient(): HttpClient
{
$stack = HandlerStack::create();
$stack->push(Middleware::log(
$this->logger,
new MessageFormatter('{req_body} - {res_body}')
));
$stack->push(
Middleware::log(
$this->logger,
new MessageFormatter('{uri} -> {req_body} => {res_body}')
)
);
return new HttpClient([
'base_uri' => $this->config->getApiUrl(),
'headers' => [
......@@ -112,4 +153,14 @@ class Client
'handler' => $stack
]);
}
protected function generateId(): void
{
$this->id = substr(sha1(implode(':', [
$this->config->getApiUrl(),
$this->config->getApplicationName(),
$this->config->getInstanceId(),
$this->config->getVersion()
])), 4, 9);
}
}
<?php
namespace Minds\UnleashClient;
/**
* Configuration object for the Unleash client
*/
class Config
{
public const VERSION = "0.0.1";
/** @var string */
public const VERSION = '0.0.1';
/** @var string $apiUrl */
private $apiUrl;
/** @var string $instanceId */
private $instanceId;
/** @var string $applicationName */
private $applicationName;
/** @var int $pollingIntervalSeconds */
private $pollingIntervalSeconds;
/** @var int $metricsIntervalSeconds */
private $metricsIntervalSeconds;
/** @var string */
protected $apiUrl;
/** @var string */
protected $instanceId;
/** @var string */
protected $applicationName;
/** @var int */
protected $pollingIntervalSeconds;
/** @var int */
protected $metricsIntervalSeconds;
/** @var int */
protected $cacheTtl;
/**
* Config constructor.
* @param string|null $apiUrl
* @param string|null $instanceId
* @param string|null $applicationName
* @param int|null $pollingIntervalSeconds
* @param int|null $metricsIntervalSeconds
*/
public function __construct(
string $apiUrl = null,
string $instanceId = null,
......@@ -24,112 +42,117 @@ class Config
int $pollingIntervalSeconds = null,
int $metricsIntervalSeconds = null
) {
$this->apiUrl = $apiUrl ?? $_ENV['UNLEASH_API_URL'] ?? "";
$this->instanceId = $instanceId ?? $_ENV['UNLEASH_INSTANCE_ID'] ?? "";
$this->applicationName = $applicationName ?? $_ENV['UNLEASH_APPLICATION_NAME'] ?? "";
$this->apiUrl = $apiUrl ?? $_ENV['UNLEASH_API_URL'] ?? '';
$this->instanceId = $instanceId ?? $_ENV['UNLEASH_INSTANCE_ID'] ?? '';
$this->applicationName = $applicationName ?? $_ENV['UNLEASH_APPLICATION_NAME'] ?? '';
$this->pollingIntervalSeconds = $pollingIntervalSeconds ?? $_ENV['UNLEASH_POLLING_INTERVAL_SECONDS'] ?? 15;
$this->metricsIntervalSeconds = $metricsIntervalSeconds ?? $_ENV['UNLEASH_METRICS_INTERVAL_SECONDS'] ?? 15;
$this->metricsIntervalSeconds = $metricsIntervalSeconds ?? $_ENV['UNLEASH_METRICS_INTERVAL_SECONDS'] ?? 60;
}
/**
* Gets the current version number of the library;
* @return string version
* Gets the current version number of the library
* @return string
*/
public function getVersion() : string
public function getVersion(): string
{
return Config::VERSION;
return static::VERSION;
}
/**
* Gets the Unleash url from the config
* @return string api url
* Gets the Unleash server URL
* @return string
*/
public function getApiUrl() : string
public function getApiUrl(): string
{
return $this->apiUrl;
}
/**
* Sets the Unleash url in the config
* @return Config this
* Sets the Unleash server URL
* @param string $apiUrl
* @return Config
*/
public function setApiUrl(string $apiUrl) : Config
public function setApiUrl(string $apiUrl): Config
{
$this->apiUrl = $apiUrl;
return $this;
}
/**
* Gets the Unleash instance id from the config
* @return string instance id
* Gets the Unleash instance ID
* @return string
*/
public function getInstanceId() : string
public function getInstanceId(): string
{
return $this->instanceId;
}
/**
* Sets the Unleash instance id in the config
* @return Config this
* Sets the Unleash instance ID
* @param string $instanceId
* @return Config
*/
public function setInstanceID(string $instanceId) : Config
public function setInstanceId(string $instanceId): Config
{
$this->instanceId = $instanceId;
return $this;
}
/**
* Gets the Unleash application name from the config
* @return string instance id
*/
public function getApplicationName() : string
* Gets the Unleash application name
* @return string
*/
public function getApplicationName(): string
{
return $this->applicationName;
}
/**
* Sets the Unleash application name in the config
* @return Config this
* Sets the Unleash application name
* @param string $applicationName
* @return Config
*/
public function setApplicationName(string $applicationName) : Config
public function setApplicationName(string $applicationName): Config
{
$this->applicationName = $applicationName;
return $this;
}
/**
* Gets the Unleash polling interval
* @return int polling interval in seconds
*/
public function getPollingIntervalSeconds() : int
* Gets the Unleash polling interval in seconds
* @return int
*/
public function getPollingIntervalSeconds(): int
{
return $this->pollingIntervalSeconds;
}
/**
* Sets the Unleash polling interval seconds
* @return Config this
* Sets the Unleash polling interval in seconds
* @param int $pollingIntervalSeconds
* @return Config
*/
public function setPollingIntervalSeconds(int $pollingIntervalSeconds) : Config
public function setPollingIntervalSeconds(int $pollingIntervalSeconds): Config
{
$this->pollingIntervalSeconds = $pollingIntervalSeconds;
return $this;
}
/**
* Gets the Unleash metrics send interval
* @return int metrics send interval in seconds
*/
public function getMetricsIntervalSeconds() : int
* Gets the Unleash metrics send interval in seconds
* @return int
*/
public function getMetricsIntervalSeconds(): int
{
return $this->metricsIntervalSeconds;
}
/**
* Sets the Unleash metrics send interval
* @return Config this
* Sets the Unleash metrics send interval in seconds
* @param int $metricsIntervalSeconds
* @return Config
*/
public function setMetricsIntervalSeconds(int $metricsIntervalSeconds) : Config
public function setMetricsIntervalSeconds(int $metricsIntervalSeconds): Config
{
$this->metricsIntervalSeconds = $metricsIntervalSeconds;
return $this;
......
<?php
namespace Minds\UnleashClient\Entities;
use Minds\UnleashClient\Config;
/**
* Context data object for Unleash api calls
* Send when the client checks feature flags
* Context
*
* @author edgebal
*/
namespace Minds\UnleashClient\Entities;
class Context
{
/** @var string */
private $userId;
/** @var string */
private $sessionId;
/** @var string */
private $remoteAddress;
/** @var string */
private $appName;
/** @var string */
private $environment;
public function __construct(
Config $config,
string $userId = null,
string $sessionId = null,
string $remoteAddress = null,
string $environment = null
) {
$this->appName = $config->getApplicationName();
$this->userId = $userId ?? "";
$this->sessionId = $sessionId ?? "";
$this->remoteAddress = $remoteAddress ?? "";
$this->environment = $environment ?? "";
}
protected $userId;
/**
* Get the value of environment
*
* @return string
*/
public function getEnvironment() : string
{
return $this->environment;
}
/** @var string */
protected $sessionId;
/**
* Set the value of environment
*
* @return string
*/
public function setEnvironment(string $environment) : Context
{
$this->environment = $environment;
/** @var string */
protected $remoteAddress;
return $this;
}
/** @var string */
protected $hostName;
/**
* Get the value of userId
*
* @return string
* Gets the context user ID
* @return string|null
*/
public function getUserId() : string
public function getUserId(): ?string
{
return $this->userId;
}
/**
* Set the value of userId
*
* Sets the context user ID
* @param string $userId
* @return Context
*/
public function setUserId($userId) : Context
public function setUserId(string $userId): Context
{
$this->userId = $userId;
return $this;
}
/**
* Get the value of sessionId
*
* @
* Gets the context session ID
* @return string|null
*/
public function getSessionId() : string
public function getSessionId(): ?string
{
return $this->sessionId;
}
/**
* Set the value of sessionId
*
* @return self
* Sets the context session ID
* @param string $sessionId
* @return Context
*/
public function setSessionId($sessionId) : Context
public function setSessionId(string $sessionId): Context
{
$this->sessionId = $sessionId;
return $this;
}
/**
* Get the value of remoteAddress
* @return string
* Gets the context remote address
* @return string|null
*/
public function getRemoteAddress() : string
public function getRemoteAddress(): ?string
{
return $this->remoteAddress;
}
/**
* Set the value of remoteAddress
*
* @return Contextg
* Sets the context remote address
* @param string $remoteAddress
* @return Context
*/
public function setRemoteAddress($remoteAddress) : Context
public function setRemoteAddress(string $remoteAddress): Context
{
$this->remoteAddress = $remoteAddress;
return $this;
}
/**
* Get the value of appName
* Gets the context host name
* @return string|null
*/
public function getAppName() : string
public function getHostName(): ?string
{
return $this->appName;
return $this->hostName;
}
/**
* Set the value of appName
*
* Sets the context host name
* @param string $hostName
* @return Context
*/
public function setAppName($appName) : Context
public function setHostName(string $hostName): Context
{
$this->appName = $appName;
$this->hostName = $hostName;
return $this;
}
}
<?php
namespace Minds\UnleashClient\Entities;
/**
* Feature data object for Unleash api response.
* Returned by the client when processing feature flags
* Feature
*
* @author edgebal
*/
namespace Minds\UnleashClient\Entities;
use Exception;
class Feature
{
/** @var string */
private $name;
protected $name = '';
/** @var string */
private $description;
protected $description = '';
/** @var bool */
private $enabled;
protected $enabled = false;
public function __construct(array $data)
{
$this->name = $data['name'];
$this->description = $data['description'];
$this->enabled = $data['enabled'];
}
/** @var Strategy[] */
protected $strategies = [];
/**
* Gets the name of an unleash feature flag
* Gets the feature name
* @return string
*/
public function getName() : string
public function getName(): string
{
return $this->name;
}
/**
* Gets the description of an unleash feature flag
* Sets the feature name
* @param string $name
* @return Feature
*/
public function setName(string $name): Feature
{
$this->name = $name;
return $this;
}
/**
* Gets the feature description
* @return string
*/
public function getDescription() : string
public function getDescription(): string
{
return $this->description;
}
/**
* Gets the description of an unleash feature flag
* Sets the feature description
* @param string $description
* @return Feature
*/
public function setDescription(string $description): Feature
{
$this->description = $description;
return $this;
}
/**
* Gets the feature enabled flag
* @return bool
*/
public function isEnabled() : bool
public function isEnabled(): bool
{
return $this->enabled;
}
/**
* Sets the enabled flag for the feature
* @param bool $enabled
* @return Feature
*/
public function setEnabled(bool $enabled): Feature
{
$this->enabled = $enabled;
return $this;
}
/**
* Gets the feature strategies
* @return Strategy[]
*/
public function getStrategies(): array
{
return $this->strategies;
}
/**
* Sets the feature strategies
* @param Strategy[] $strategies
* @return Feature
* @throws Exception
*/
public function setStrategies(array $strategies): Feature
{
foreach ($strategies as $strategy) {
if (!($strategy instanceof Strategy)) {
throw new Exception(sprintf("Strategy should be an instance of %s", Strategy::class));
}
}
$this->strategies = $strategies;
return $this;
}
}
<?php
/**
* Strategy
*
* @author edgebal
*/
namespace Minds\UnleashClient\Entities;
class Feature
class Strategy
{
public function __construct(array $data)
/** @var string */
protected $name = '';
/** @var array */
protected $parameters = [];
/**
* Gets the strategy name
* @return string
*/
public function getName(): string
{
return $this->name;
}
/**
* Sets the strategy name
* @param string $name
* @return Strategy
*/
public function setName(string $name): Strategy
{
$this->name = $name;
return $this;
}
/**
* Gets the strategy parameters
* @return array
*/
public function getParameters(): array
{
return $this->parameters;
}
/**
* Sets the strategy parameters
* @param array $parameters
* @return Strategy
*/
public function setParameters(array $parameters): Strategy
{
$this->parameters = $parameters;
return $this;
}
}
<?php
/**
* FeatureFactory
*
* @author edgebal
*/
namespace Minds\UnleashClient\Factories;
use Exception;
use Minds\UnleashClient\Entities\Feature;
/**
* Factory class to make Feature instantiation testable
* @package Minds\UnleashClient\Factories
*/
class FeatureFactory
{
/** @var StrategyFactory */
protected $strategyFactory;
/**
* FeatureFactory constructor.
* @param StrategyFactory|null $strategyFactory
*/
public function __construct(
StrategyFactory $strategyFactory = null
) {
$this->strategyFactory = $strategyFactory ?: new StrategyFactory();
}
/**
* Creates a new Feature instance
* @param array $data
* @return Feature
* @throws Exception
*/
public function build(array $data)
{
$feature = new Feature();
$strategies = [];
foreach ($data['strategies'] as $strategy) {
$strategies[] = $this->strategyFactory
->build($strategy);
}
$feature
->setName($data['name'])
->setDescription($data['description'])
->setEnabled($data['enabled'])
->setStrategies($strategies);
return $feature;
}
}
<?php
/**
* StrategyAlgorithmFactory
*
* @author edgebal
*/
namespace Minds\UnleashClient\Factories;
use Minds\UnleashClient\Entities\Strategy;
use Minds\UnleashClient\Logger;
use Minds\UnleashClient\StrategyAlgorithms\StrategyAlgorithm;
use Psr\Log\LoggerInterface;
class StrategyAlgorithmFactory
{
/** @var LoggerInterface|Logger */
protected $logger;
/**
* StrategyAlgorithmFactory constructor.
* @param LoggerInterface|null $logger
*/
public function __construct(
LoggerInterface $logger = null
) {
$this->logger = $logger ?: new Logger();
}
/**
* Builds a new strategy algorithm instance
* @param Strategy $strategy
* @return StrategyAlgorithm|null
*/
public function build(Strategy $strategy): ?StrategyAlgorithm
{
$className = sprintf("\\Minds\\UnleashClient\\StrategyAlgorithms\\%sStrategyAlgorithm", ucfirst($strategy->getName()));
if (!class_exists($className)) {
$this->logger->warning("{$className} does not exist");
return null;
}
$strategyAlgorithm = new $className($this->logger);
if (!($strategyAlgorithm instanceof StrategyAlgorithm)) {
$this->logger->warning(sprintf("%s is not an %s instance", $strategyAlgorithm, StrategyAlgorithm::class));
return null;
}
$this->logger->debug("Created a new {$className} instance");
return $strategyAlgorithm;
}
}
<?php
/**
* StrategyFactory
*
* @author edgebal
*/
namespace Minds\UnleashClient\Factories;
use Minds\UnleashClient\Entities\Strategy;
/**
* Factory class to make Strategy instantiation testable
* @package Minds\UnleashClient\Factories
*/
class StrategyFactory
{
/**
* Creates a new Strategy instance
* @param array $data
* @return Strategy
*/
public function build(array $data)
{
$strategy = new Strategy();
$strategy
->setName($data['name'])
->setParameters($data['parameters']);
return $strategy;
}
}
<?php
/**
* NormalizedValue
*
* @author edgebal
*/
namespace Minds\UnleashClient\Helpers;
use lastguest\Murmur;
class NormalizedValue
{
/**
* Normalizes a value using Murmur3 algorithm hash and a normalizer modulus.
* Returns a value from $min (default 1) to $normalizer (default 100) if
* ID is truthy, if not it returns $min - 1 (default 0).
* @param string $id
* @param string $groupId
* @param int $normalizer
* @param int $min
* @return int
*/
public function build(string $id, string $groupId, int $normalizer = 100, int $min = 1): int
{
if (!$id) {
return $min - 1;
}
return (Murmur::hash3_int("{$id}:{$groupId}") % $normalizer) + $min;
}
/**
* Returns a random value from $min (default 1) to $normalizer (default 100).
* @param int $normalizer
* @param int $min
* @return int
*/
public function random($normalizer = 100, $min = 1): int
{
return mt_rand($min, $normalizer);
}
}
......@@ -4,6 +4,10 @@ namespace Minds\UnleashClient;
use Monolog\Logger as MonoLogger;
use Monolog\Handler\ErrorLogHandler;
/**
* Monolog wrapper for Unleash client
* @package Minds\UnleashClient
*/
class Logger extends MonoLogger
{
public function __construct()
......
<?php
/**
* ApplicationHostnameStrategyAlgorithm
*
* @author edgebal
*/
namespace Minds\UnleashClient\StrategyAlgorithms;
use Minds\UnleashClient\Entities\Context;
use Minds\UnleashClient\Entities\Strategy;
use Minds\UnleashClient\Logger;
use Psr\Log\LoggerInterface;
class ApplicationHostnameStrategyAlgorithm implements StrategyAlgorithm
{
/** @var LoggerInterface|Logger */
protected $logger;
/**
* @inheritDoc
*/
public function __construct(
LoggerInterface $logger = null
) {
$this->logger = $logger ?: new Logger();
}
/**
* @inheritDoc
*/
public function isEnabled(Strategy $strategy, Context $context): bool
{
$parameters = $strategy->getParameters() ?? [];
$hostNamesList = $parameters['hostNames'] ?? '';
if (!$hostNamesList) {
return false;
}
$hostNames = array_map([$this, 'normalizeHostName'], explode(',', $hostNamesList));
$this->logger->debug(static::class, [
$context->getHostName(),
$hostNames
]);
return in_array(strtolower($context->getHostName()), $hostNames, true);
}
/**
* Normalizes host names for lookup
* @param string $hostName
* @return string
*/
protected function normalizeHostName(string $hostName): string
{
return trim(strtolower($hostName));
}
}
<?php
/**
* DefaultStrategyAlgorithm
*
* @author edgebal
*/
namespace Minds\UnleashClient\StrategyAlgorithms;
use Minds\UnleashClient\Entities\Context;
use Minds\UnleashClient\Entities\Strategy;
use Minds\UnleashClient\Logger;
use Psr\Log\LoggerInterface;
class DefaultStrategyAlgorithm implements StrategyAlgorithm
{
/** @var LoggerInterface|Logger */
protected $logger;
/**
* @inheritDoc
*/
public function __construct(
LoggerInterface $logger = null
) {
$this->logger = $logger ?: new Logger();
}
/**
* @inheritDoc
*/
public function isEnabled(Strategy $strategy, Context $context): bool
{
$this->logger->debug(static::class, [ true ]);
return true;
}
}
<?php
/**
* FlexibleRolloutStrategyAlgorithm
*
* @author edgebal
*/
namespace Minds\UnleashClient\StrategyAlgorithms;
use Minds\UnleashClient\Entities\Context;
use Minds\UnleashClient\Entities\Strategy;
use Minds\UnleashClient\Helpers\NormalizedValue;
use Minds\UnleashClient\Logger;
use Psr\Log\LoggerInterface;
class FlexibleRolloutStrategyAlgorithm implements StrategyAlgorithm
{
/** @var LoggerInterface|Logger */
protected $logger;
/** @var NormalizedValue */
protected $normalizedValue;
/**
* @inheritDoc
* @param NormalizedValue $normalizedValue
*/
public function __construct(
LoggerInterface $logger = null,
NormalizedValue $normalizedValue = null
) {
$this->logger = $logger ?: new Logger();
$this->normalizedValue = $normalizedValue ?: new NormalizedValue();
}
/**
* @inheritDoc
*/
public function isEnabled(Strategy $strategy, Context $context): bool
{
$parameters = $strategy->getParameters();
$percentage = intval($parameters['rollout'] ?? 0);
$stickiness = $parameters['stickiness'] ?? 'default';
$groupId = trim($parameters['groupId'] ?? '');
$userId = trim($context->getUserId() ?? '');
$sessionId = trim($context->getSessionId() ?? '');
$randomId = sprintf("%s", $this->normalizedValue->random(999999, 1));
switch ($stickiness) {
case 'userId':
$stickinessId = $userId;
break;
case 'sessionId':
$stickinessId = $sessionId;
break;
case 'random':
$stickinessId = $randomId;
break;
default:
$stickinessId = $userId ?: $sessionId ?: $randomId;
break;
}
if (!$stickinessId) {
return false;
}
$stickinessValue = $this->normalizedValue
->build($stickinessId, $groupId);
$this->logger->debug(static::class, [
$stickiness,
$stickinessValue,
$percentage
]);
return $percentage > 0 &&
$stickinessValue <= $percentage;
}
}
<?php
/**
* GradualRolloutRandomStrategyAlgorithm
*
* @author edgebal
*/
namespace Minds\UnleashClient\StrategyAlgorithms;
use Minds\UnleashClient\Entities\Context;
use Minds\UnleashClient\Entities\Strategy;
use Minds\UnleashClient\Helpers\NormalizedValue;
use Minds\UnleashClient\Logger;
use Psr\Log\LoggerInterface;
class GradualRolloutRandomStrategyAlgorithm implements StrategyAlgorithm
{
/** @var LoggerInterface|Logger */
protected $logger;
/** @var NormalizedValue */
protected $normalizedValue;
/**
* @inheritDoc
*/
public function __construct(
LoggerInterface $logger = null,
NormalizedValue $normalizedValue = null
) {
$this->logger = $logger ?: new Logger();
$this->normalizedValue = $normalizedValue ?: new NormalizedValue();
}
/**
* @inheritDoc
*/
public function isEnabled(Strategy $strategy, Context $context): bool
{
$parameters = $strategy->getParameters();
$percentage = intval($parameters['percentage'] ?? 0);
$random = $this->normalizedValue->random();
$this->logger->debug(static::class, [
$random,
$percentage
]);
return $percentage > 0 &&
$random <= $percentage;
}
}
<?php
/**
* GradualRolloutSessionIdStrategyAlgorithm
*
* @author edgebal
*/
namespace Minds\UnleashClient\StrategyAlgorithms;
use Minds\UnleashClient\Entities\Context;
use Minds\UnleashClient\Entities\Strategy;
use Minds\UnleashClient\Helpers\NormalizedValue;
use Minds\UnleashClient\Logger;
use Psr\Log\LoggerInterface;
class GradualRolloutSessionIdStrategyAlgorithm implements StrategyAlgorithm
{
/** @var LoggerInterface|Logger */
protected $logger;
/** @var NormalizedValue */
protected $normalizedValue;
/**
* @inheritDoc
* @param NormalizedValue $normalizedValue
*/
public function __construct(
LoggerInterface $logger = null,
NormalizedValue $normalizedValue = null
) {
$this->logger = $logger ?: new Logger();
$this->normalizedValue = $normalizedValue ?: new NormalizedValue();
}
/**
* @inheritDoc
*/
public function isEnabled(Strategy $strategy, Context $context): bool
{
$parameters = $strategy->getParameters();
$percentage = intval($parameters['percentage'] ?? 0);
$groupId = trim($parameters['groupId'] ?? '');
$sessionId = trim($context->getSessionId() ?? '');
if (!$sessionId) {
return false;
}
$sessionIdValue = $this->normalizedValue
->build($sessionId, $groupId);
$this->logger->debug(static::class, [
$sessionIdValue,
$percentage
]);
return $percentage > 0 &&
$sessionIdValue <= $percentage;
}
}
<?php
/**
* GradualRolloutUserIdStrategyAlgorithm
*
* @author edgebal
*/
namespace Minds\UnleashClient\StrategyAlgorithms;
use Minds\UnleashClient\Entities\Context;
use Minds\UnleashClient\Entities\Strategy;
use Minds\UnleashClient\Helpers\NormalizedValue;
use Minds\UnleashClient\Logger;
use Psr\Log\LoggerInterface;
class GradualRolloutUserIdStrategyAlgorithm implements StrategyAlgorithm
{
/** @var LoggerInterface|Logger */
protected $logger;
/** @var NormalizedValue */
protected $normalizedValue;
/**
* @inheritDoc
* @param NormalizedValue $normalizedValue
*/
public function __construct(
LoggerInterface $logger = null,
NormalizedValue $normalizedValue = null
) {
$this->logger = $logger ?: new Logger();
$this->normalizedValue = $normalizedValue ?: new NormalizedValue();
}
/**
* @inheritDoc
*/
public function isEnabled(Strategy $strategy, Context $context): bool
{
$parameters = $strategy->getParameters();
$percentage = intval($parameters['percentage'] ?? 0);
$groupId = trim($parameters['groupId'] ?? '');
$userId = trim($context->getUserId() ?? '');
if (!$userId) {
return false;
}
$userIdValue = $this->normalizedValue
->build($userId, $groupId);
$this->logger->debug(static::class, [
$userIdValue,
$percentage
]);
return $percentage > 0 &&
$userIdValue <= $percentage;
}
}
<?php
/**
* RemoteAddressStrategyAlgorithm
*
* @author edgebal
*/
namespace Minds\UnleashClient\StrategyAlgorithms;
use Minds\UnleashClient\Entities\Context;
use Minds\UnleashClient\Entities\Strategy;
use Minds\UnleashClient\Logger;
use Psr\Log\LoggerInterface;
class RemoteAddressStrategyAlgorithm implements StrategyAlgorithm
{
/** @var LoggerInterface|Logger */
protected $logger;
/**
* @inheritDoc
*/
public function __construct(
LoggerInterface $logger = null
) {
$this->logger = $logger ?: new Logger();
}
/**
* @inheritDoc
*/
public function isEnabled(Strategy $strategy, Context $context): bool
{
$parameters = $strategy->getParameters() ?? [];
$ipAddressesList = $parameters['IPs'] ?? '';
if (!$ipAddressesList) {
return false;
}
$ipAddresses = array_map([$this, 'normalizeIpAddress'], explode(',', $ipAddressesList));
$this->logger->debug(static::class, [
$context->getRemoteAddress(),
$ipAddresses
]);
return in_array(strtolower($context->getRemoteAddress()), $ipAddresses, true);
}
/**
* Normalizes IP addresses for lookup, using lowercase for IPv6
* @param string $ipAddress
* @return string
*/
protected function normalizeIpAddress(string $ipAddress): string
{
return trim(strtolower($ipAddress));
}
}
<?php
/**
* StrategyAlgorithm
*
* @author edgebal
*/
namespace Minds\UnleashClient\StrategyAlgorithms;
use Minds\UnleashClient\Entities\Context;
use Minds\UnleashClient\Entities\Strategy;
use Psr\Log\LoggerInterface;
interface StrategyAlgorithm
{
/**
* StrategyAlgorithm constructor.
* @param LoggerInterface $logger
*/
public function __construct(LoggerInterface $logger);
/**
* Resolves a strategy using the context
* @param Strategy $strategy
* @param Context $context
* @return bool
*/
public function isEnabled(Strategy $strategy, Context $context): bool;
}
<?php
/**
* UserWithIdStrategyAlgorithm
*
* @author edgebal
*/
namespace Minds\UnleashClient\StrategyAlgorithms;
use Minds\UnleashClient\Entities\Context;
use Minds\UnleashClient\Entities\Strategy;
use Minds\UnleashClient\Logger;
use Psr\Log\LoggerInterface;
class UserWithIdStrategyAlgorithm implements StrategyAlgorithm
{
/** @var LoggerInterface|Logger */
protected $logger;
/**
* @inheritDoc
*/
public function __construct(
LoggerInterface $logger = null
) {
$this->logger = $logger ?: new Logger();
}
/**
* @inheritDoc
*/
public function isEnabled(Strategy $strategy, Context $context): bool
{
$parameters = $strategy->getParameters() ?? [];
$userIdsList = $parameters['userIds'] ?? '';
if (!$userIdsList) {
return false;
}
$userIds = array_map([$this, 'normalizeUserId'], explode(',', $userIdsList));
$this->logger->debug(static::class, [
$context->getUserId(),
$userIds
]);
return in_array($context->getUserId(), $userIds, true);
}
/**
* Normalizes user IDs for lookup
* @param string $userId
* @return string
*/
protected function normalizeUserId(string $userId): string
{
return trim($userId);
}
}
<?php
/**
* StrategyResolver
*
* @author edgebal
*/
namespace Minds\UnleashClient;
use Minds\UnleashClient\Entities\Context;
use Minds\UnleashClient\Factories\StrategyAlgorithmFactory;
use Minds\UnleashClient\StrategyAlgorithms\StrategyAlgorithm;
use Psr\Log\LoggerInterface;
class StrategyResolver
{
/** @var LoggerInterface|Logger */
protected $logger;
/** @var StrategyAlgorithmFactory */
protected $strategyAlgorithmFactory;
/**
* StrategyResolver constructor.
* @param LoggerInterface|null $logger
* @param StrategyAlgorithm|null $strategyAlgorithmFactory
*/
public function __construct(
LoggerInterface $logger = null,
StrategyAlgorithm $strategyAlgorithmFactory = null
) {
$this->logger = $logger ?: new Logger();
$this->strategyAlgorithmFactory = $strategyAlgorithmFactory ?: new StrategyAlgorithmFactory($this->logger);
}
/**
* Instantiates a new strategy algorithm based on the passed strategy and run it against
* the context
* @param array $strategies
* @param Context $context
* @return bool
*/
public function isEnabled(array $strategies, Context $context): bool
{
foreach ($strategies as $strategy) {
$strategyAlgorithm = $this->strategyAlgorithmFactory
->build($strategy);
if ($strategyAlgorithm->isEnabled($strategy, $context)) {
return true;
}
}
return false;
}
}
......@@ -2,89 +2,151 @@
namespace Minds\UnleashClient;
use Minds\UnleashClient\Config;
use Minds\UnleashClient\Client;
use Minds\UnleashClient\Entities\Context;
use Psr\SimpleCache\CacheInterface;
use Zend\Cache\StorageFactory;
use Zend\Cache\Psr\SimpleCache\SimpleCacheDecorator;
use Zend\Cache\Storage\Adapter\Filesystem;
use Minds\UnleashClient\Logger;
use Psr\Log\LoggerInterface;
use Psr\SimpleCache\InvalidArgumentException;
class Unleash
{
const UNLEASH_CLIENT_CACHE_PREFIX = '_unlsh';
/** @var string */
const UNLEASH_CLIENT_CACHE_TIMEOUT_KEY = 'cache_timeout';
/** @var Config */
private $config;
protected $config;
/** @var Logger|LoggerInterface */
protected $logger;
/** @var Client */
private $client;
/** @var CacheInterface */
private $cache;
/** @var Logger */
private $logger;
protected $client;
public function __construct(Config $config = null, Client $client = null, CacheInterface $cache = null)
{
$this->logger = new Logger();
/** @var Cache\SimpleCache|CacheInterface */
protected $cache;
/** @var StrategyResolver */
protected $strategyResolver;
/** @var bool */
protected $isClientRegistered = false;
/** @var Context */
protected $context;
/**
* Unleash constructor.
* @param Config|null $config
* @param LoggerInterface|null $logger
* @param Client|null $client
* @param CacheInterface|null $cache
* @param StrategyResolver|null $strategyResolver
*/
public function __construct(
Config $config = null,
LoggerInterface $logger = null,
Client $client = null,
CacheInterface $cache = null,
StrategyResolver $strategyResolver = null
) {
$this->config = $config ?: new Config();
$this->client = $client ?: new Client($this->config);
$this->cache = $cache ?: $this->buildSimpleCache();
$this->logger = $logger ?: new Logger();
$this->client = $client ?: new Client($this->config, $this->logger);
$this->cache = $cache ?: new Cache\SimpleCache($this->logger);
$this->strategyResolver = $strategyResolver ?: new StrategyResolver($this->logger);
}
/**
* checks the current collection of feature flags to determine if a flag is enabled
* @return bool
*/
public function isEnabled(string $featureFlag, bool $default = false) : bool
* Sets the context for the upcoming feature flag checks
* @param Context $context
* @return Unleash
*/
public function setContext(Context $context): Unleash
{
$this->context = $context;
return $this;
}
/**
* Checks the current collection of feature flags to determine if a flag is enabled for said context
* @param string $featureName
* @param bool $default
* @return bool
* @throws InvalidArgumentException
*/
public function isEnabled(string $featureName, bool $default = false): bool
{
try {
$this->logger->debug('checking cache');
if (!$this->cache->has($featureFlag)) {
$this->logger->debug("{$featureFlag} not found in cache, refreshing");
$featureFlags = $this->getFeatureFlags();
foreach ($featureFlags as $feature) {
$this->cache->set($feature->getName(), $feature);
}
} else {
$this->logger->debug("{$featureFlag} cached!");
$this->logger->debug("Checking for {$featureName}");
if ($this->isCacheInvalid()) {
$this->logger->debug('Cache timeout, fetching');
$this->fetch();
}
/** @var Entities\Feature|null $feature */
$feature = $this->cache->get($this->buildCacheKey($featureName), null);
if ($feature === null) {
return $default;
}
$flag = $this->cache->get($featureFlag, null);
return $flag->isEnabled() ?? $default;
} catch (\Exception $ex) {
$this->logger->error('Error getting feature flags');
$this->logger->error($ex->getMessage());
return
$feature->isEnabled() &&
$this->strategyResolver->isEnabled(
$feature->getStrategies(),
$this->context
);
} catch (\Exception $e) {
$this->logger->error("Error checking feature flag {$featureName}");
$this->logger->error($e);
return false;
}
}
/**
* gets the feature flags from the configured api
* @return array feature
*/
public function getFeatureFlags() : array
* Checks if features cache is invalid
* @return bool
* @throws InvalidArgumentException
*/
protected function isCacheInvalid()
{
return $this->client->getFeatureFlags();
return $this->cache->get($this->buildCacheKey(static::UNLEASH_CLIENT_CACHE_TIMEOUT_KEY), -1) <= time();
}
/**
* Registers the client with the configured api
* @return bool
* Fetches the current collection of feature flags from server, and caches it
*/
public function register() : bool
protected function fetch(): void
{
return $this->client->register();
if (!$this->isClientRegistered) {
$this->logger->debug('Client is not registered');
$this->isClientRegistered = $this->client->register();
}
$this->logger->debug('Fetching feature flags from server');
foreach ($this->client->getFeatureFlags() as $feature) {
$this->logger->debug("Storing {$feature->getName()}");
$this->cache->set($this->buildCacheKey($feature->getName()), $feature);
}
$timeout = time() + $this->config->getPollingIntervalSeconds();
$this->logger->debug('Cache will timeout at ' . date('c', $timeout));
$this->cache
->set($this->buildCacheKey(static::UNLEASH_CLIENT_CACHE_TIMEOUT_KEY), $timeout);
}
private function buildSimpleCache() : CacheInterface
/**
* Builds a config-aware cache key
* @param string $key
* @return string
*/
protected function buildCacheKey(string $key): string
{
$this->logger->debug('building cache');
$storage = StorageFactory::factory([
'adapter' => [
'name' => 'filesystem',
'options' => ''
],
'plugins' => ['serializer']
]);
$plugin = StorageFactory::pluginFactory('serializer');
$storage->addPlugin($plugin);
return new SimpleCacheDecorator($storage);
return static::UNLEASH_CLIENT_CACHE_PREFIX . $this->client->getId() . '-' . $key;
}
}