Skip to content

Commit

Permalink
Merge 958cc74 into a6b08fb
Browse files Browse the repository at this point in the history
  • Loading branch information
Tymek committed Oct 2, 2023
2 parents a6b08fb + 958cc74 commit d3c3b77
Show file tree
Hide file tree
Showing 13 changed files with 327 additions and 49 deletions.
5 changes: 5 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,5 +55,10 @@
"fixer": "php-cs-fixer fix --verbose --allow-risky=yes",
"phpstan": "phpstan analyse --level=max src",
"phpunit": "phpunit"
},
"config": {
"allow-plugins": {
"php-http/discovery": true
}
}
}
52 changes: 52 additions & 0 deletions src/DTO/DefaultDepencency.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

namespace Unleash\Client\DTO;

use JetBrains\PhpStorm\ArrayShape;

final class DefaultDepencency implements Dependency
{
/**
* @param array<string> $variants
*/
public function __construct(
private readonly string $feature,
private readonly ?bool $enabled,
private readonly ?array $variants = null,
) {
}

/**
* @phpstan-return array<string|bool|array<string>>
*/
#[ArrayShape(['name' => 'string', 'enabled' => 'bool', 'variants' => 'array'])]
public function jsonSerialize(): array
{
$result = [
'name' => $this->feature,
];
if ($this->enabled !== null) {
$result['enabled'] = $this->enabled;
}
if ($this->variants !== null) {
$result['variants'] = $this->variants;
}

return $result;
}

public function getFeature(): string
{
return $this->feature;
}

public function getEnabled(): ?bool
{
return $this->enabled;
}

public function getVariants(): ?array
{
return $this->variants;
}
}
10 changes: 10 additions & 0 deletions src/DTO/DefaultFeature.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ final class DefaultFeature implements Feature
/**
* @param iterable<Strategy> $strategies
* @param array<Variant> $variants
* @param array<Dependency> $dependencies
*/
public function __construct(
private readonly string $name,
private readonly bool $enabled,
private readonly iterable $strategies,
private readonly array $variants = [],
private readonly bool $impressionData = false,
private readonly array $dependencies = [],
) {
}

Expand Down Expand Up @@ -47,4 +49,12 @@ public function hasImpressionData(): bool
{
return $this->impressionData;
}

/**
* @return array<Dependency>
*/
public function getDependencies(): array
{
return $this->dependencies;
}
}
17 changes: 17 additions & 0 deletions src/DTO/Dependency.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

namespace Unleash\Client\DTO;

use JsonSerializable;

interface Dependency extends JsonSerializable
{
public function getFeature(): string;

public function getEnabled(): ?bool;

/**
* @return array<string>
*/
public function getVariants(): ?array;
}
5 changes: 5 additions & 0 deletions src/DTO/Feature.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,9 @@ public function getStrategies(): iterable;
* @return array<Variant>
*/
public function getVariants(): array;

/**
* @return array<Dependency>
*/
public function getDependencies(): array;
}
161 changes: 140 additions & 21 deletions src/DefaultUnleash.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@
use Unleash\Client\Configuration\Context;
use Unleash\Client\Configuration\UnleashConfiguration;
use Unleash\Client\DTO\DefaultFeatureEnabledResult;
use Unleash\Client\DTO\Dependency;
use Unleash\Client\DTO\Feature;
use Unleash\Client\DTO\FeatureEnabledResult;
use Unleash\Client\DTO\Strategy;
use Unleash\Client\DTO\Variant;
use Unleash\Client\Enum\ImpressionDataEventType;
use Unleash\Client\Event\FeatureToggleDependencyNotFoundEvent;
use Unleash\Client\Event\FeatureToggleDisabledEvent;
use Unleash\Client\Event\FeatureToggleMissingStrategyHandlerEvent;
use Unleash\Client\Event\FeatureToggleNotFoundEvent;
Expand Down Expand Up @@ -43,7 +45,9 @@ public function __construct(
public function isEnabled(string $featureName, ?Context $context = null, bool $default = false): bool
{
$context ??= $this->configuration->getContextProvider()->getContext();
$feature = $this->findFeature($featureName, $context);
$featureAndDependencies = $this->findFeatureAndDependencies($featureName, $context);
$feature = $featureAndDependencies['feature'] ?? null;
$dependencies = $featureAndDependencies['dependencies'] ?? [];

if ($feature !== null) {
if (method_exists($feature, 'hasImpressionData') && $feature->hasImpressionData()) {
Expand All @@ -59,21 +63,38 @@ public function isEnabled(string $featureName, ?Context $context = null, bool $d
}
}

return $this->isFeatureEnabled($feature, $context, $default)->isEnabled();
return $this->isFeatureEnabled($feature, $context, $dependencies, $default)->isEnabled();
}

public function getVariant(string $featureName, ?Context $context = null, ?Variant $fallbackVariant = null): Variant
{
$featureAndDependencies = $this->findFeatureAndDependencies($featureName, $context);
$feature = $featureAndDependencies['feature'] ?? null;
$dependencies = $featureAndDependencies['dependencies'] ?? [];

return $this->getVariantForFeature($feature, $context, $dependencies, $fallbackVariant);
}

public function register(): bool
{
return $this->registrationService->register($this->strategyHandlers);
}

private function getVariantForFeature(?Feature $feature, ?Context $context = null, array $dependencies = [], ?Variant $fallbackVariant = null): Variant
{

$fallbackVariant ??= $this->variantHandler->getDefaultVariant();
$context ??= $this->configuration->getContextProvider()->getContext();

$feature = $this->findFeature($featureName, $context);
$enabledResult = $this->isFeatureEnabled($feature, $context);
$enabledResult = $this->isFeatureEnabled($feature, $context, $dependencies);
$strategyVariants = $enabledResult->getStrategy()?->getVariants() ?? [];
if ($feature === null || $enabledResult->isEnabled() === false ||
(!count($feature->getVariants()) && empty($strategyVariants))) {
if (
$feature === null || $enabledResult->isEnabled() === false ||
(!count($feature->getVariants()) && empty($strategyVariants))
) {
return $fallbackVariant;
}
$featureName = $feature->getName();

if (empty($strategyVariants)) {
$variant = $this->variantHandler->selectVariant($feature->getVariants(), $featureName, $context);
Expand All @@ -100,41 +121,127 @@ public function getVariant(string $featureName, ?Context $context = null, ?Varia
return $resolvedVariant;
}

public function register(): bool
{
return $this->registrationService->register($this->strategyHandlers);
}

/**
* Finds a feature and posts events if the feature is not found.
* Finds a feature with it's parent features. Posts events if the feature is not found.
*
* @param string $featureName name of the feature to find
* @param Context $context the context to use
*
* @return Feature|null
*
* @return null|array{
* feature: Feature|null,
* dependencies: array<string, Feature>,
* }
*/
private function findFeature(string $featureName, Context $context): ?Feature
public function findFeatureAndDependencies(string $featureName, ?Context $context): ?array
{
$feature = $this->repository->findFeature($featureName);
$features = $this->repository->getFeatures();
assert(is_array($features));
$context ??= $this->configuration->getContextProvider()->getContext();

$feature = $features[$featureName] ?? null;

if ($feature === null) {
$event = new FeatureToggleNotFoundEvent($context, $featureName);
$this->configuration->getEventDispatcherOrNull()?->dispatch(
$event,
UnleashEvents::FEATURE_TOGGLE_NOT_FOUND,
);

return [
"feature" => null,
"dependencies" => []
];
}

$dependencyDefinitions = $feature->getDependencies();

if ($dependencyDefinitions === null) {
return [
"feature" => $feature,
"dependencies" => []
];
}

$dependencies = [];
foreach ($dependencyDefinitions as $dependencyDefinition) {
$name = $dependencyDefinition->getFeature();
$dependency = $features[$name] ?? null;
if ($dependency !== null) {
$dependencies[$name] = $dependency;
} else {
$event = new FeatureToggleDependencyNotFoundEvent($context, $name);
$this->configuration->getEventDispatcherOrNull()?->dispatch(
$event,
UnleashEvents::FEATURE_TOGGLE_NOT_FOUND,
);
}
}

return $feature;
return [
"feature" => $feature,
"dependencies" => $dependencies
];
}

/**
* Checks if parent feature flag requirement is satisfied.
*
* @param Dependency $dependency the dependency to check
* @param Feature $parentFeature the parent feature to check
* @param Context $context the context to use
*/
public function isDependencySatisfied(
?Dependency $dependency = null,
?Feature $parentFeature = null,
?Context $context = null,
): bool {
if ($dependency === null) {
return true;
}
$context ??= $this->configuration->getContextProvider()->getContext();

if ($parentFeature === null) {
return false;
}

if (count($parentFeature->getDependencies()) > 0) {
return false;
}

$parentFeatureEnabled = $this->isFeatureEnabled($parentFeature, $context);

if ($parentFeatureEnabled->isEnabled() && $dependency->getEnabled() === false) {
return false;
}
if (!$parentFeatureEnabled->isEnabled() && $dependency->getEnabled() !== false) {
return false;
}

$dependencyVariants = $dependency->getVariants();
if (!empty($dependencyVariants)) {
$parentFeatureVariantName = $this->getVariantForFeature($parentFeature, $context)->getName();

foreach ($dependencyVariants as $dependencyVariant) {
if ($dependencyVariant === $parentFeatureVariantName) {
return true;
}
}

return false;
}

return true;
}

/**
* Underlying method to check if a feature is enabled.
*
* @param Feature|null $feature the feature to check
* @param Context $context the context to use
* @param bool $default the default value to return if the feature is not found
* @param Feature|null $feature the feature to check
* @param Feature[]|null $parentFeatures the dependencies to check
* @param Context $context the context to use
* @param bool $default the default value to return if the feature is not found
*/
private function isFeatureEnabled(?Feature $feature, Context $context, bool $default = false): FeatureEnabledResult
private function isFeatureEnabled(?Feature $feature, Context $context, ?array $parentFeatures = [], bool $default = false): FeatureEnabledResult
{
if ($feature === null) {
return new DefaultFeatureEnabledResult($default);
Expand All @@ -152,6 +259,18 @@ private function isFeatureEnabled(?Feature $feature, Context $context, bool $def
return new DefaultFeatureEnabledResult();
}

$dependencies = $feature->getDependencies();
if (!empty($dependencies)) {
foreach ($dependencies as $dependency) {
$parentFeature = $parentFeatures[$dependency->getFeature()] ?? null;
if (!$this->isDependencySatisfied($dependency, $parentFeature, $context)) {
$this->metricsHandler->handleMetrics($feature, false);

return new DefaultFeatureEnabledResult();
}
}
}

$strategies = $feature->getStrategies();
if (!is_countable($strategies)) {
$strategies = iterator_to_array($strategies);
Expand Down
27 changes: 27 additions & 0 deletions src/Event/FeatureToggleDependencyNotFoundEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

namespace Unleash\Client\Event;

use Unleash\Client\Configuration\Context;

final class FeatureToggleDependencyNotFoundEvent extends AbstractEvent
{
/**
* @internal
*/
public function __construct(
private readonly Context $context,
private readonly string $featureName,
) {
}

public function getContext(): Context
{
return $this->context;
}

public function getFeatureName(): string
{
return $this->featureName;
}
}
7 changes: 7 additions & 0 deletions src/Event/UnleashEvents.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,11 @@ final class UnleashEvents
* @Event("Unleash\Client\Event\ImpressionDataEvent")
*/
public const IMPRESSION_DATA = 'unleash.events.impression_data';

/**
* Triggered when feature toggle dependency is not found.
*
* @Event("Unleash\Client\Event\FeatureToggleDependencyNotFoundEvent")
*/
public const FEATURE_TOGGLE_DEPENDENCY_NOT_FOUND = 'unleash.event.toggle.dependency_not_found';
}
Loading

0 comments on commit d3c3b77

Please sign in to comment.