From c24d4543e84a06d3c869b1290377ae287cf59748 Mon Sep 17 00:00:00 2001 From: jim Date: Thu, 4 Jul 2024 20:07:01 +0200 Subject: [PATCH 1/9] do not fetch from cache every time --- src/Unleash.php | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/src/Unleash.php b/src/Unleash.php index d4ca2f6..404cab2 100644 --- a/src/Unleash.php +++ b/src/Unleash.php @@ -23,6 +23,7 @@ class Unleash protected $config; protected $request; protected $features; + protected $expires; public function __construct(ClientInterface $client, Cache $cache, Config $config, Request $request) { @@ -34,13 +35,17 @@ public function __construct(ClientInterface $client, Cache $cache, Config $confi public function getFeatures(): array { + if ($this->isFresh()) { + return $this->features; + } + try { - $features = $this->getCachedFeatures(); + $this->features = $this->getCachedFeatures(); // Always store the failover cache, in case it is turned on during failure scenarios. - $this->cache->forever('unleash.features.failover', $features); + $this->cache->forever('unleash.features.failover', $this->features); - return $features; + return $this->features; } catch (TransferException | JsonException $e) { if ($this->config->get('unleash.cache.failover') === true) { return $this->cache->get('unleash.features.failover', []); @@ -110,6 +115,11 @@ public function isFeatureDisabled(string $name, ...$args): bool return !$this->isFeatureEnabled($name, ...$args); } + protected function isFresh(): bool + { + return $this->expires > time(); + } + protected function getCachedFeatures(): array { if (!$this->config->get('unleash.isEnabled')) { @@ -117,9 +127,11 @@ protected function getCachedFeatures(): array } if ($this->config->get('unleash.cache.isEnabled')) { + $this->setExpires(); + return $this->cache->remember( 'unleash', - $this->config->get('unleash.cache.ttl', self::DEFAULT_CACHE_TTL), + $this->getCacheTTL(), function () { return $this->fetchFeatures(); } @@ -129,6 +141,16 @@ function () { return $this->features ?? $this->features = $this->fetchFeatures(); } + protected function getCacheTTL(): int + { + return $this->config->get('unleash.cache.ttl', self::DEFAULT_CACHE_TTL); + } + + protected function setExpires(): void + { + $this->expires = $this->getCacheTTL() + time(); + } + protected function fetchFeatures(): array { $response = $this->client->get($this->config->get('unleash.featuresEndpoint')); @@ -139,6 +161,8 @@ protected function fetchFeatures(): array throw new JsonException('Could not decode unleash response body.', $e->getCode(), $e); } + $this->setExpires(); + return $this->formatResponse($data); } From faf5675485fe36da8c2440e8cd98017350bed6ac Mon Sep 17 00:00:00 2001 From: jim Date: Fri, 5 Jul 2024 15:01:54 +0200 Subject: [PATCH 2/9] add RefreshFeatures middleware, that can run during shutdown --- src/Middleware/RefreshFeatures.php | 28 +++++++++++ src/Unleash.php | 75 +++++++++++++++--------------- 2 files changed, 66 insertions(+), 37 deletions(-) create mode 100644 src/Middleware/RefreshFeatures.php diff --git a/src/Middleware/RefreshFeatures.php b/src/Middleware/RefreshFeatures.php new file mode 100644 index 0000000..812618a --- /dev/null +++ b/src/Middleware/RefreshFeatures.php @@ -0,0 +1,28 @@ +getCacheTTL() * $this->ttlThresholdFactor) > $unleash->getExpires()) { + $unleash->refreshCache(); + } + } +} diff --git a/src/Unleash.php b/src/Unleash.php index 404cab2..6f865c4 100644 --- a/src/Unleash.php +++ b/src/Unleash.php @@ -12,11 +12,12 @@ 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; @@ -39,20 +40,26 @@ public function getFeatures(): array return $this->features; } - try { - $this->features = $this->getCachedFeatures(); - - // Always store the failover cache, in case it is turned on during failure scenarios. - $this->cache->forever('unleash.features.failover', $this->features); + if (!$this->config->get('unleash.isEnabled')) { + return []; + } - return $this->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) @@ -93,7 +100,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) { @@ -115,6 +122,11 @@ public function isFeatureDisabled(string $name, ...$args): bool return !$this->isFeatureEnabled($name, ...$args); } + public function refreshCache() + { + $this->fetchFeatures(); + } + protected function isFresh(): bool { return $this->expires > time(); @@ -122,33 +134,22 @@ protected function isFresh(): bool protected function getCachedFeatures(): array { - if (!$this->config->get('unleash.isEnabled')) { - return []; - } - - if ($this->config->get('unleash.cache.isEnabled')) { - $this->setExpires(); - - return $this->cache->remember( - 'unleash', - $this->getCacheTTL(), - function () { - return $this->fetchFeatures(); - } - ); - } - - return $this->features ?? $this->features = $this->fetchFeatures(); + return $this->cache->get('unleash.cache', function () {return $this->fetchFeatures();}); } - protected function getCacheTTL(): int + public function getCacheTTL(): int { return $this->config->get('unleash.cache.ttl', self::DEFAULT_CACHE_TTL); } - protected function setExpires(): void + protected function setExpires(): int { - $this->expires = $this->getCacheTTL() + time(); + return $this->expires = $this->getCacheTTL() + time(); + } + + public function getExpires(): int + { + return $this->expires ?? $this->setExpires(); } protected function fetchFeatures(): array @@ -161,13 +162,13 @@ protected function fetchFeatures(): array throw new JsonException('Could not decode unleash response body.', $e->getCode(), $e); } - $this->setExpires(); + $data['expires'] = $this->setExpires(); - return $this->formatResponse($data); - } + $this->cache->set('unleash.cache', $data, $this->getCacheTTL()); + $this->cache->forever('unleash.failover', $data); - protected function formatResponse($data): array - { - return Arr::get($data, 'features', []); + $this->features = Arr::get($data, 'features', []); + + return $data; } } From 370a30f3c3af2dd5fc07883baabbe793fc61415c Mon Sep 17 00:00:00 2001 From: jim Date: Fri, 5 Jul 2024 15:03:18 +0200 Subject: [PATCH 3/9] cs-fixer --- .gitignore | 2 ++ src/Facades/Feature.php | 1 + src/Facades/Unleash.php | 1 + tests/Strategies/ApplicationHostnameStrategyTest.php | 1 - 4 files changed, 4 insertions(+), 1 deletion(-) 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/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 @@ Date: Fri, 5 Jul 2024 16:37:43 +0200 Subject: [PATCH 4/9] phan fixes --- src/ServiceProvider.php | 6 ++---- .../ApplicationHostnameStrategy.php | 1 - src/Strategies/Contracts/DynamicStrategy.php | 3 +-- src/Strategies/Contracts/Strategy.php | 1 - src/Strategies/DefaultStrategy.php | 4 ++++ src/Unleash.php | 21 +++++++------------ 6 files changed, 15 insertions(+), 21 deletions(-) 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 6f865c4..40e198c 100644 --- a/src/Unleash.php +++ b/src/Unleash.php @@ -2,18 +2,15 @@ namespace MikeFrancis\LaravelUnleash; -use GuzzleHttp\ClientInterface; -use GuzzleHttp\Exception\InvalidArgumentException; +use GuzzleHttp\Client; 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 { @@ -26,7 +23,7 @@ class Unleash 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; @@ -109,7 +106,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; } } @@ -124,7 +121,9 @@ public function isFeatureDisabled(string $name, ...$args): bool public function refreshCache() { - $this->fetchFeatures(); + if ($this->config->get('unleash.isEnabled') && $this->config->get('unleash.cache.isEnabled')) { + $this->fetchFeatures(); + } } protected function isFresh(): bool @@ -156,11 +155,7 @@ protected function fetchFeatures(): array { $response = $this->client->get($this->config->get('unleash.featuresEndpoint')); - 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); - } + $data = (array) json_decode((string)$response->getBody(), true, 512, JSON_BIGINT_AS_STRING + JSON_THROW_ON_ERROR); $data['expires'] = $this->setExpires(); From f14e11e1bde221fccb2ac86555e5b5db1b2b7ab7 Mon Sep 17 00:00:00 2001 From: jim Date: Fri, 5 Jul 2024 18:09:19 +0200 Subject: [PATCH 5/9] fix importing correct guzzle client --- src/Unleash.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Unleash.php b/src/Unleash.php index 40e198c..5235037 100644 --- a/src/Unleash.php +++ b/src/Unleash.php @@ -2,7 +2,6 @@ namespace MikeFrancis\LaravelUnleash; -use GuzzleHttp\Client; use GuzzleHttp\Exception\TransferException; use Illuminate\Contracts\Cache\Repository as Cache; use Illuminate\Contracts\Config\Repository as Config; From 38ec80dd7c5351045035207db280330a011f3d9b Mon Sep 17 00:00:00 2001 From: jim Date: Fri, 22 Nov 2024 17:15:24 +0100 Subject: [PATCH 6/9] add laravel 11 support --- composer.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 7989d65..941290a 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", + "illuminate/http": "^5.8|^6|^7|^8|^9|^10|^11", + "illuminate/contracts": "^5.8|^6|^7|^8|^9|^10|^11" }, "require-dev": { "phpunit/phpunit": "^9.5.10", From 33a65dba7e95e09c4deed701420ef48d6fc45305 Mon Sep 17 00:00:00 2001 From: jim Date: Thu, 5 Dec 2024 17:05:10 +0100 Subject: [PATCH 7/9] get Unleash by classname from service container --- src/Middleware/RefreshFeatures.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Middleware/RefreshFeatures.php b/src/Middleware/RefreshFeatures.php index 812618a..ca0c8a9 100644 --- a/src/Middleware/RefreshFeatures.php +++ b/src/Middleware/RefreshFeatures.php @@ -17,7 +17,7 @@ public function handle(Request $request, Closure $next) public function terminate() { - $unleash = app('unleash'); + $unleash = app(Unleash::class); if (!$unleash instanceof Unleash) { return; } From f51200e31a8d87b80dd0d86f77ba1ede0cce6db7 Mon Sep 17 00:00:00 2001 From: jim Date: Mon, 20 Jan 2025 15:24:59 +0100 Subject: [PATCH 8/9] force getFeature to always return an array, even if the feature is not found --- src/Unleash.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Unleash.php b/src/Unleash.php index 5235037..8975748 100644 --- a/src/Unleash.php +++ b/src/Unleash.php @@ -58,11 +58,11 @@ public function getFeatures(): array 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']; From b009cc43aeb49213ea661063da2c3f9c2eb31fb6 Mon Sep 17 00:00:00 2001 From: Jimbolino Date: Fri, 7 Mar 2025 14:30:11 +0100 Subject: [PATCH 9/9] add laravel 12 --- composer.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 941290a..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|^11", - "illuminate/http": "^5.8|^6|^7|^8|^9|^10|^11", - "illuminate/contracts": "^5.8|^6|^7|^8|^9|^10|^11" + "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",