Skip to content
This repository was archived by the owner on Jun 23, 2025. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ phpunit.xml

composer.phar
composer.lock

.php-cs-fixer.cache
6 changes: 3 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions src/Facades/Feature.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<?php

namespace MikeFrancis\LaravelUnleash\Facades;

use Illuminate\Support\Facades\Facade;
Expand Down
1 change: 1 addition & 0 deletions src/Facades/Unleash.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<?php

namespace MikeFrancis\LaravelUnleash\Facades;

use Illuminate\Support\Facades\Facade;
Expand Down
28 changes: 28 additions & 0 deletions src/Middleware/RefreshFeatures.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

namespace MikeFrancis\LaravelUnleash\Middleware;

use Closure;
use Illuminate\Http\Request;
use MikeFrancis\LaravelUnleash\Unleash;

class RefreshFeatures
{
public float $ttlThresholdFactor = 0.75; // when ttl has reached 75%, refresh the cache

public function handle(Request $request, Closure $next)
{
return $next($request);
}

public function terminate()
{
$unleash = app(Unleash::class);
if (!$unleash instanceof Unleash) {
return;
}
if (time() + ($unleash->getCacheTTL() * $this->ttlThresholdFactor) > $unleash->getExpires()) {
$unleash->refreshCache();
}
}
}
6 changes: 2 additions & 4 deletions src/ServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
}
Expand All @@ -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);
}
Expand All @@ -60,8 +60,6 @@ function (string $feature) {

/**
* Get the path to the config.
*
* @return string
*/
private function getConfigPath(): string
{
Expand Down
1 change: 0 additions & 1 deletion src/Strategies/ApplicationHostnameStrategy.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 1 addition & 2 deletions src/Strategies/Contracts/DynamicStrategy.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
1 change: 0 additions & 1 deletion src/Strategies/Contracts/Strategy.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
4 changes: 4 additions & 0 deletions src/Strategies/DefaultStrategy.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@

class DefaultStrategy implements Strategy
{
/**
* @unused-param $params
* @unused-param $request
*/
public function isEnabled(array $params, Request $request): bool
{
return true;
Expand Down
101 changes: 60 additions & 41 deletions src/Unleash.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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'];
Expand Down Expand Up @@ -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) {
Expand All @@ -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;
}
}
Expand All @@ -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;
}
}
1 change: 0 additions & 1 deletion tests/Strategies/ApplicationHostnameStrategyTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@

class ApplicationHostnameStrategyTest extends TestCase
{

public function testWithSingleApplicationHostname()
{
$params = [
Expand Down
Loading