Commit bb4fa180 authored by Emiliano Balbuena's avatar Emiliano Balbuena

(wip): Implement strategies

1 merge request!2WIP: Clean up and strategies
Pipeline #105654264 passed with stage
in 1 minute and 7 seconds
bin/
vendor/
.phpunit.result.cache
.php_cs.cache
\ No newline at end of file
.php_cs.cache
.idea
.vscode
\ No newline at end of file
......@@ -13,7 +13,6 @@ class ConfigSpec extends ObjectBehavior
private const TEST_APPLICATION_NAME = "test_application_name";
private const TEST_POLLING_INTERVAL = 15;
private const TEST_METRICS_INTERVAL = 20;
private const TEST_CACHE_TTL = 3000;
public function let()
{
......@@ -22,7 +21,6 @@ class ConfigSpec extends ObjectBehavior
$_ENV["UNLEASH_APPLICATION_NAME"] = ConfigSpec::TEST_APPLICATION_NAME;
$_ENV["UNLEASH_POLLING_INTERVAL_SECONDS"] = ConfigSpec::TEST_POLLING_INTERVAL;
$_ENV["UNLEASH_METRICS_INTERVAL_SECONDS"] = ConfigSpec::TEST_METRICS_INTERVAL;
$_ENV["UNLEASH_CACHE_TTL"] = ConfigSpec::TEST_CACHE_TTL;
}
public function it_is_initializable()
......@@ -37,7 +35,6 @@ class ConfigSpec extends ObjectBehavior
$this->getApplicationName()->shouldEqual(ConfigSpec::TEST_APPLICATION_NAME);
$this->getPollingIntervalSeconds()->shouldEqual(ConfigSpec::TEST_POLLING_INTERVAL);
$this->getMetricsIntervalSeconds()->shouldEqual(ConfigSpec::TEST_METRICS_INTERVAL);
$this->getCacheTtl()->shouldEqual(ConfigSpec::TEST_CACHE_TTL);
}
public function it_reads_from_constructed_values()
......@@ -47,14 +44,12 @@ class ConfigSpec extends ObjectBehavior
$applicationName = "new_application_name";
$pollingIntervalSeconds = 100;
$metricsIntervalSeconds = 200;
$metricsCacheTtl = 4500;
$this->beConstructedWith($url, $instanceId, $applicationName, $pollingIntervalSeconds, $metricsIntervalSeconds, $metricsCacheTtl);
$this->beConstructedWith($url, $instanceId, $applicationName, $pollingIntervalSeconds, $metricsIntervalSeconds);
$this->getApiUrl()->shouldEqual($url);
$this->getInstanceId()->shouldEqual($instanceId);
$this->getApplicationName()->shouldEqual($applicationName);
$this->getPollingIntervalSeconds()->shouldEqual($pollingIntervalSeconds);
$this->getMetricsIntervalSeconds()->shouldEqual($metricsIntervalSeconds);
$this->getCacheTtl()->shouldEqual($metricsCacheTtl);
}
public function it_sets_and_gets_values()
......@@ -64,28 +59,24 @@ class ConfigSpec extends ObjectBehavior
$applicationName = "new_application_name";
$pollingIntervalSeconds = 100;
$metricsIntervalSeconds = 200;
$metricsCacheTtl = 4500;
$this->getApiUrl()->shouldEqual(ConfigSpec::TEST_URL);
$this->getInstanceId()->shouldEqual(ConfigSpec::TEST_INSTANCE_ID);
$this->getApplicationName()->shouldEqual(ConfigSpec::TEST_APPLICATION_NAME);
$this->getPollingIntervalSeconds()->shouldEqual(ConfigSpec::TEST_POLLING_INTERVAL);
$this->getMetricsIntervalSeconds()->shouldEqual(ConfigSpec::TEST_METRICS_INTERVAL);
$this->getCacheTtl()->shouldEqual(ConfigSpec::TEST_CACHE_TTL);
$this->setApiUrl($url);
$this->setInstanceId($instanceId);
$this->setApplicationName($applicationName);
$this->setPollingIntervalSeconds($pollingIntervalSeconds);
$this->setMetricsIntervalSeconds($metricsIntervalSeconds);
$this->setCacheTtl($metricsCacheTtl);
$this->getApiUrl()->shouldEqual($url);
$this->getInstanceId()->shouldEqual($instanceId);
$this->getApplicationName()->shouldEqual($applicationName);
$this->getPollingIntervalSeconds()->shouldEqual($pollingIntervalSeconds);
$this->getMetricsIntervalSeconds()->shouldEqual($metricsIntervalSeconds);
$this->getCacheTtl()->shouldEqual($metricsCacheTtl);
}
public function it_should_return_a_version()
......
<?php
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";
/** @var Config */
private $config;
public function let(Config $config)
{
$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
);
}
public function it_is_initializable()
{
$this->shouldHaveType(Context::class);
}
}
......@@ -30,7 +30,7 @@ class SimpleCache extends SimpleCacheDecorator
*/
protected function buildAdapter(): StorageInterface
{
$this->logger->debug('Building cache storage adapter...');
$this->logger->debug('Building cache storage adapter');
// Build a simple filesystem based cache
......
......@@ -11,13 +11,17 @@ 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", 15, 15, 300);
$config = new Config("https://gitlab.com/api/v4/feature_flags/unleash/14894840/", "F2qZp9PyWKXDas9mkEsH", "test", 300, 15);
$unleash = new Unleash($config, $logger);
$logger->debug('Registering client');
$unleash->register();
$logger->debug('Checking enabled');
$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}");
}
main();
......@@ -7,8 +7,7 @@ use GuzzleHttp\Client as HttpClient;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
use GuzzleHttp\MessageFormatter;
use Minds\UnleashClient\Config;
use Minds\UnleashClient\Logger;
use Minds\UnleashClient\Factories\FeatureFactory;
use Minds\UnleashClient\Entities\Feature;
use Psr\Log\LoggerInterface;
......@@ -26,41 +25,53 @@ class Client
/** @var Logger */
protected $logger;
/** @var FeatureFactory */
protected $featureFactory;
/**
* 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
HttpClient $httpClient = null,
FeatureFactory $featureFactory = null
) {
$this->config = $config;
$this->logger = $logger ?: new Logger();
$this->httpClient = $httpClient ?: $this->createHttpClient();
$this->featureFactory = $featureFactory ?: new FeatureFactory();
$this->logger->debug("Client configured. Base URL: {$this->config->getApiUrl()}");
}
/**
* 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('Registering client...');
$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('Client payload', $payload);
$response = $this->httpClient->post('client/register', $payload);
return
$response->getStatusCode() >= 200 &&
$response->getStatusCode() < 300;
......@@ -72,15 +83,14 @@ class Client
/**
* 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
{
$this->logger->debug('Getting feature flags');
try {
$response = $this->httpClient->get('client/features');
$this->logger->debug("Got feature flags [{$response->getStatusCode()}]");
......@@ -92,23 +102,20 @@ class Client
$data = json_decode((string) $response->getBody(), true);
return array_map(function ($feature) {
return new Feature($feature);
return $this->featureFactory->build($feature);
}, $data['features']);
}
} catch (\Exception $e) {
} catch (Exception $e) {
$this->logger->error($e);
return [];
}
}
/**
* Sends
*/
return [];
}
/**
* Creates an http client with the auth headers
* and middleware
*/
*/
protected function createHttpClient(): HttpClient
{
$stack = HandlerStack::create();
......
......@@ -7,7 +7,7 @@ namespace Minds\UnleashClient;
class Config
{
/** @var string */
public const VERSION = "0.0.1";
public const VERSION = '0.0.1';
/** @var string */
protected $apiUrl;
......@@ -27,25 +27,30 @@ class Config
/** @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,
string $applicationName = null,
int $pollingIntervalSeconds = null,
int $metricsIntervalSeconds = null,
int $cacheTtl = 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->cacheTtl = $cacheTtl ?? $_ENV['UNLEASH_CACHE_TTL'] ?? 300;
$this->metricsIntervalSeconds = $metricsIntervalSeconds ?? $_ENV['UNLEASH_METRICS_INTERVAL_SECONDS'] ?? 60;
}
/**
* Gets the current version number of the library
*
* @return string
*/
public function getVersion(): string
......@@ -55,7 +60,6 @@ class Config
/**
* Gets the Unleash server URL
*
* @return string
*/
public function getApiUrl(): string
......@@ -65,7 +69,7 @@ class Config
/**
* Sets the Unleash server URL
*
* @param string $apiUrl
* @return Config
*/
public function setApiUrl(string $apiUrl): Config
......@@ -76,7 +80,6 @@ class Config
/**
* Gets the Unleash instance ID
*
* @return string
*/
public function getInstanceId(): string
......@@ -86,10 +89,10 @@ class Config
/**
* 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;
......@@ -97,7 +100,6 @@ class Config
/**
* Gets the Unleash application name
*
* @return string
*/
public function getApplicationName(): string
......@@ -107,7 +109,7 @@ class Config
/**
* Sets the Unleash application name
*
* @param string $applicationName
* @return Config
*/
public function setApplicationName(string $applicationName): Config
......@@ -118,7 +120,6 @@ class Config
/**
* Gets the Unleash polling interval in seconds
*
* @return int
*/
public function getPollingIntervalSeconds(): int
......@@ -128,7 +129,7 @@ class Config
/**
* Sets the Unleash polling interval in seconds
*
* @param int $pollingIntervalSeconds
* @return Config
*/
public function setPollingIntervalSeconds(int $pollingIntervalSeconds): Config
......@@ -139,7 +140,6 @@ class Config
/**
* Gets the Unleash metrics send interval in seconds
*
* @return int
*/
public function getMetricsIntervalSeconds(): int
......@@ -149,7 +149,7 @@ class Config
/**
* Sets the Unleash metrics send interval in seconds
*
* @param int $metricsIntervalSeconds
* @return Config
*/
public function setMetricsIntervalSeconds(int $metricsIntervalSeconds): Config
......@@ -157,25 +157,4 @@ class Config
$this->metricsIntervalSeconds = $metricsIntervalSeconds;
return $this;
}
/**
* Gets the cache TTL in seconds
*
* @return int
*/
public function getCacheTtl(): int
{
return $this->cacheTtl;
}
/**
* Sets the cache TTL in seconds
*
* @return Config
*/
public function setCacheTtl(int $cacheTtl): Config
{
$this->cacheTtl = $cacheTtl;
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
*/
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 ?? "";
}
/**
* Get the value of environment
*
* @return string
*/
public function getEnvironment() : string
{
return $this->environment;
}
/**
* Set the value of environment
*
* @return string
*/
public function setEnvironment(string $environment) : Context
{
$this->environment = $environment;
return $this;
}
/**
* Get the value of userId
*
* @return string
*/
public function getUserId() : string
{
return $this->userId;
}
/**
* Set the value of userId
*
* @return Context
*/
public function setUserId($userId) : Context
{
$this->userId = $userId;
return $this;
}
/**
* Get the value of sessionId
*
* @
*/
public function getSessionId() : string
{
return $this->sessionId;
}
/**
* Set the value of sessionId
*
* @return self
*/
public function setSessionId($sessionId) : Context
{
$this->sessionId = $sessionId;
return $this;
}
/**
* Get the value of remoteAddress
* @return string
*/
public function getRemoteAddress() : string
{
return $this->remoteAddress;
}
/**
* Set the value of remoteAddress
*
* @return Contextg
*/
public function setRemoteAddress($remoteAddress) : Context
{
$this->remoteAddress = $remoteAddress;
return $this;
}
/**
* Get the value of appName
*/
public function getAppName() : string
{
return $this->appName;
}
/**
* Set the value of appName
*
* @return Context
*/
public function setAppName($appName) : Context
{
$this->appName = $appName;
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
/**
* 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;
}
}
......@@ -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()
......
......@@ -2,26 +2,34 @@
namespace Minds\UnleashClient;
use Minds\UnleashClient\Config;
use Minds\UnleashClient\Client;
use Psr\SimpleCache\CacheInterface;
use Minds\UnleashClient\Logger;
use Psr\Log\LoggerInterface;
use Psr\SimpleCache\InvalidArgumentException;
class Unleash
{
/** @var Config */
private $config;
protected $config;
/** @var LoggerInterface */
protected $logger;
/** @var Client */
private $client;
protected $client;
/** @var CacheInterface */
private $cache;
/** @var Cache\SimpleCache|CacheInterface */
protected $cache;
/** @var LoggerInterface */
private $logger;
/** @var bool */
protected $isClientRegistered = false;
/**
* Unleash constructor.
* @param Config|null $config
* @param LoggerInterface|null $logger
* @param Client|null $client
* @param CacheInterface|null $cache
*/
public function __construct(
Config $config = null,
LoggerInterface $logger = null,
......@@ -36,54 +44,65 @@ class Unleash
}
/**
* Checks the current collection of feature flags to determine if a flag is
* enabled
*
* Checks the current collection of feature flags to determine if a flag is enabled
* @param string $featureName
* @param bool $default
* @return bool
* @throws InvalidArgumentException
*/
public function isEnabled(string $featureFlag, bool $default = false) : bool
public function isEnabled(string $featureName, bool $default = false): bool
{
try {
$this->logger->debug("Checking for {$featureFlag}");
$this->logger->debug("Checking for {$featureName}");
if (!$this->cache->has($featureFlag)) {
$this->logger->debug("{$featureFlag} not found in cache, fetching");
if ($this->isCacheInvalid()) {
$this->logger->debug('Cache timeout, fetching');
$this->fetch();
}
/** @var Entities\Feature|null */
$flag = $this->cache->get($featureFlag, null);
$feature = $this->cache->get($featureName, null);
if ($flag === null) {
if ($feature === null) {
return $default;
}
return $flag->isEnabled();
return $feature->isEnabled();
} catch (\Exception $e) {
$this->logger->error("Error checking feature flag {$featureFlag}");
$this->logger->error("Error checking feature flag {$featureName}");
$this->logger->error($e);
return false;
}
}
/**
* Fetches the current collection of feature flags from server, and
* caches it
* Checks if features cache is invalid
* @return bool
* @throws InvalidArgumentException
*/
public function isCacheInvalid()
{
return $this->cache->get('_unleash_client_cache_timeout', -1) <= time();
}
/**
* Fetches the current collection of feature flags from server, and caches it
*/
public function fetch(): void
{
$this->logger->debug('Fetching feature flags from server...');
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($feature->getName(), $feature);
}
}
/**
* Registers the client with the configured api
* @return bool
*/
public function register() : bool
{
return $this->client->register();
$timeout = time() + $this->config->getPollingIntervalSeconds();
$this->logger->debug('Cache will timeout at ' . date('c', $timeout));
$this->cache->set('_unleash_client_cache_timeout', $timeout);
}
}
Please register or to comment