diff --git a/.gitignore b/.gitignore index 35aa62f..7b648a5 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ phpunit.xml composer.phar composer.lock + +.php-cs-fixer.cache diff --git a/composer.json b/composer.json index 7989d65..98b14a0 100644 --- a/composer.json +++ b/composer.json @@ -4,9 +4,9 @@ "type": "library", "require": { "guzzlehttp/guzzle": "^6.3|^7.0", - "illuminate/support": "^5.8|^6|^7|^8|^9|^10.0", - "illuminate/http": "^5.8|^6|^7|^8|^9|^10.0", - "illuminate/contracts": "^5.8|^6|^7|^8|^9|^10.0" + "illuminate/support": "^5.8|^6|^7|^8|^9|^10|^11|^12", + "illuminate/http": "^5.8|^6|^7|^8|^9|^10|^11|^12", + "illuminate/contracts": "^5.8|^6|^7|^8|^9|^10|^11|^12" }, "require-dev": { "phpunit/phpunit": "^9.5.10", diff --git a/src/Facades/Feature.php b/src/Facades/Feature.php index 3507345..408212f 100644 --- a/src/Facades/Feature.php +++ b/src/Facades/Feature.php @@ -1,4 +1,5 @@ getCacheTTL() * $this->ttlThresholdFactor) > $unleash->getExpires()) { + $unleash->refreshCache(); + } + } +} diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php index e499cd6..7e1170a 100644 --- a/src/ServiceProvider.php +++ b/src/ServiceProvider.php @@ -4,8 +4,6 @@ use Illuminate\Support\Facades\Blade; use Illuminate\Support\ServiceProvider as IlluminateServiceProvider; -use MikeFrancis\LaravelUnleash\Unleash; -use MikeFrancis\LaravelUnleash\Client; use GuzzleHttp\ClientInterface; class ServiceProvider extends IlluminateServiceProvider @@ -42,6 +40,7 @@ public function boot() function (string $feature) { $client = app(Client::class); $unleash = app(Unleash::class, ['client' => $client]); + assert($unleash instanceof Unleash); return $unleash->isFeatureEnabled($feature); } @@ -52,6 +51,7 @@ function (string $feature) { function (string $feature) { $client = app(Client::class); $unleash = app(Unleash::class, ['client' => $client]); + assert($unleash instanceof Unleash); return !$unleash->isFeatureEnabled($feature); } @@ -60,8 +60,6 @@ function (string $feature) { /** * Get the path to the config. - * - * @return string */ private function getConfigPath(): string { diff --git a/src/Strategies/ApplicationHostnameStrategy.php b/src/Strategies/ApplicationHostnameStrategy.php index 985b026..a787df4 100644 --- a/src/Strategies/ApplicationHostnameStrategy.php +++ b/src/Strategies/ApplicationHostnameStrategy.php @@ -4,7 +4,6 @@ use Illuminate\Http\Request; use Illuminate\Support\Arr; -use Illuminate\Support\Str; use MikeFrancis\LaravelUnleash\Strategies\Contracts\Strategy; class ApplicationHostnameStrategy implements Strategy diff --git a/src/Strategies/Contracts/DynamicStrategy.php b/src/Strategies/Contracts/DynamicStrategy.php index 0e8150e..6364cdf 100644 --- a/src/Strategies/Contracts/DynamicStrategy.php +++ b/src/Strategies/Contracts/DynamicStrategy.php @@ -9,8 +9,7 @@ interface DynamicStrategy /** * @param array $params Strategy Configuration from Unleash * @param Request $request Current Request - * @param mixed $args An arbitrary number of arguments passed to isFeatureEnabled/Disabled - * @return bool + * @param mixed ...$args An arbitrary number of arguments passed to isFeatureEnabled/Disabled */ public function isEnabled(array $params, Request $request, ...$args): bool; } diff --git a/src/Strategies/Contracts/Strategy.php b/src/Strategies/Contracts/Strategy.php index ddad2d3..703c00e 100644 --- a/src/Strategies/Contracts/Strategy.php +++ b/src/Strategies/Contracts/Strategy.php @@ -9,7 +9,6 @@ interface Strategy /** * @param array $params Strategy Configuration from Unleash * @param Request $request Current Request - * @return bool */ public function isEnabled(array $params, Request $request): bool; } diff --git a/src/Strategies/DefaultStrategy.php b/src/Strategies/DefaultStrategy.php index 81cad1a..1d57cbe 100644 --- a/src/Strategies/DefaultStrategy.php +++ b/src/Strategies/DefaultStrategy.php @@ -7,6 +7,10 @@ class DefaultStrategy implements Strategy { + /** + * @unused-param $params + * @unused-param $request + */ public function isEnabled(array $params, Request $request): bool { return true; diff --git a/src/Unleash.php b/src/Unleash.php index d4ca2f6..8975748 100644 --- a/src/Unleash.php +++ b/src/Unleash.php @@ -2,29 +2,27 @@ namespace MikeFrancis\LaravelUnleash; -use GuzzleHttp\ClientInterface; -use GuzzleHttp\Exception\InvalidArgumentException; use GuzzleHttp\Exception\TransferException; use Illuminate\Contracts\Cache\Repository as Cache; use Illuminate\Contracts\Config\Repository as Config; use Illuminate\Http\Request; use Illuminate\Support\Arr; +use JsonException; use MikeFrancis\LaravelUnleash\Strategies\Contracts\DynamicStrategy; use MikeFrancis\LaravelUnleash\Strategies\Contracts\Strategy; -use Symfony\Component\HttpFoundation\Exception\JsonException; -use function GuzzleHttp\json_decode; class Unleash { - const DEFAULT_CACHE_TTL = 15; + public const DEFAULT_CACHE_TTL = 15; protected $client; protected $cache; protected $config; protected $request; protected $features; + protected $expires; - public function __construct(ClientInterface $client, Cache $cache, Config $config, Request $request) + public function __construct(Client $client, Cache $cache, Config $config, Request $request) { $this->client = $client; $this->cache = $cache; @@ -34,27 +32,37 @@ public function __construct(ClientInterface $client, Cache $cache, Config $confi public function getFeatures(): array { - try { - $features = $this->getCachedFeatures(); + if ($this->isFresh()) { + return $this->features; + } - // Always store the failover cache, in case it is turned on during failure scenarios. - $this->cache->forever('unleash.features.failover', $features); + if (!$this->config->get('unleash.isEnabled')) { + return []; + } - return $features; - } catch (TransferException | JsonException $e) { + try { + if ($this->config->get('unleash.cache.isEnabled')) { + $data = $this->getCachedFeatures(); + } else { + $data = $this->fetchFeatures(); + } + } catch (TransferException | JsonException) { if ($this->config->get('unleash.cache.failover') === true) { - return $this->cache->get('unleash.features.failover', []); + $data = $this->cache->get('unleash.failover', []); } } - return []; + $this->features = Arr::get($data, 'features', []); + $this->expires = Arr::get($data, 'expires', $this->getExpires()); + + return $this->features; } - public function getFeature(string $name) + public function getFeature(string $name): array { $features = $this->getFeatures(); - return Arr::first( + return (array) Arr::first( $features, function (array $unleashFeature) use ($name) { return $name === $unleashFeature['name']; @@ -88,7 +96,7 @@ public function isFeatureEnabled(string $name, ...$args): bool if (is_callable($allStrategies[$className])) { $strategy = $allStrategies[$className](); } else { - $strategy = new $allStrategies[$className]; + $strategy = new $allStrategies[$className](); } if (!$strategy instanceof Strategy && !$strategy instanceof DynamicStrategy) { @@ -97,7 +105,7 @@ public function isFeatureEnabled(string $name, ...$args): bool $params = Arr::get($strategyData, 'parameters', []); - if ($strategy->isEnabled($params, $this->request, ...$args)) { + if ($strategy->isEnabled($params, $this->request, ...$args)) { // @phan-suppress-current-line PhanParamTooManyUnpack return true; } } @@ -110,40 +118,51 @@ public function isFeatureDisabled(string $name, ...$args): bool return !$this->isFeatureEnabled($name, ...$args); } - protected function getCachedFeatures(): array + public function refreshCache() { - if (!$this->config->get('unleash.isEnabled')) { - return []; + if ($this->config->get('unleash.isEnabled') && $this->config->get('unleash.cache.isEnabled')) { + $this->fetchFeatures(); } + } - if ($this->config->get('unleash.cache.isEnabled')) { - return $this->cache->remember( - 'unleash', - $this->config->get('unleash.cache.ttl', self::DEFAULT_CACHE_TTL), - function () { - return $this->fetchFeatures(); - } - ); - } + protected function isFresh(): bool + { + return $this->expires > time(); + } - return $this->features ?? $this->features = $this->fetchFeatures(); + protected function getCachedFeatures(): array + { + return $this->cache->get('unleash.cache', function () {return $this->fetchFeatures();}); } - protected function fetchFeatures(): array + public function getCacheTTL(): int { - $response = $this->client->get($this->config->get('unleash.featuresEndpoint')); + return $this->config->get('unleash.cache.ttl', self::DEFAULT_CACHE_TTL); + } - try { - $data = json_decode((string)$response->getBody(), true, 512, \JSON_BIGINT_AS_STRING); - } catch (InvalidArgumentException $e) { - throw new JsonException('Could not decode unleash response body.', $e->getCode(), $e); - } + protected function setExpires(): int + { + return $this->expires = $this->getCacheTTL() + time(); + } - return $this->formatResponse($data); + public function getExpires(): int + { + return $this->expires ?? $this->setExpires(); } - protected function formatResponse($data): array + protected function fetchFeatures(): array { - return Arr::get($data, 'features', []); + $response = $this->client->get($this->config->get('unleash.featuresEndpoint')); + + $data = (array) json_decode((string)$response->getBody(), true, 512, JSON_BIGINT_AS_STRING + JSON_THROW_ON_ERROR); + + $data['expires'] = $this->setExpires(); + + $this->cache->set('unleash.cache', $data, $this->getCacheTTL()); + $this->cache->forever('unleash.failover', $data); + + $this->features = Arr::get($data, 'features', []); + + return $data; } } diff --git a/tests/Strategies/ApplicationHostnameStrategyTest.php b/tests/Strategies/ApplicationHostnameStrategyTest.php index ee8c527..d757178 100644 --- a/tests/Strategies/ApplicationHostnameStrategyTest.php +++ b/tests/Strategies/ApplicationHostnameStrategyTest.php @@ -8,7 +8,6 @@ class ApplicationHostnameStrategyTest extends TestCase { - public function testWithSingleApplicationHostname() { $params = [