Skip to content

Commit

Permalink
Feat: Dependent features (#187)
Browse files Browse the repository at this point in the history
  • Loading branch information
RikudouSage committed Nov 27, 2023
1 parent 62eeea7 commit fea0be6
Show file tree
Hide file tree
Showing 15 changed files with 822 additions and 16 deletions.
14 changes: 12 additions & 2 deletions src/DTO/DefaultFeature.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,17 @@
final readonly class DefaultFeature implements Feature
{
/**
* @param iterable<Strategy> $strategies
* @param array<Variant> $variants
* @param iterable<Strategy> $strategies
* @param array<Variant> $variants
* @param array<FeatureDependency> $dependencies
*/
public function __construct(
private string $name,
private bool $enabled,
private iterable $strategies,
private array $variants = [],
private bool $impressionData = false,
private array $dependencies = [],
) {
}

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

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

namespace Unleash\Client\DTO;

final readonly class DefaultFeatureDependency implements FeatureDependency
{
/**
* @param array<Variant>|null $requiredVariants
*/
public function __construct(
private ?Feature $feature,
private bool $expectedState,
private ?array $requiredVariants,
) {
}

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

public function getExpectedState(): bool
{
return $this->expectedState;
}

public function getRequiredVariants(): ?array
{
return $this->requiredVariants;
}

public function isResolved(): bool
{
return true;
}
}
3 changes: 3 additions & 0 deletions src/DTO/Feature.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

namespace Unleash\Client\DTO;

/**
* @method array<FeatureDependency> getDependencies()
*/
interface Feature
{
public function getName(): string;
Expand Down
17 changes: 17 additions & 0 deletions src/DTO/FeatureDependency.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

namespace Unleash\Client\DTO;

interface FeatureDependency
{
public function getFeature(): ?Feature;

public function getExpectedState(): bool;

/**
* @return array<Variant>|null
*/
public function getRequiredVariants(): ?array;

public function isResolved(): bool;
}
50 changes: 50 additions & 0 deletions src/DTO/Internal/UnresolvedFeature.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

namespace Unleash\Client\DTO\Internal;

use Unleash\Client\DTO\Feature;
use Unleash\Client\DTO\FeatureDependency;

/**
* @internal
*/
final readonly class UnresolvedFeature implements Feature
{
public function __construct(
private string $name,
) {
}

public function getName(): string
{
return $this->name;
}

public function isEnabled(): bool
{
return false;
}

public function getStrategies(): iterable
{
return [];
}

public function getVariants(): array
{
return [];
}

public function hasImpressionData(): bool
{
return false;
}

/**
* @return array<FeatureDependency>
*/
public function getDependencies(): array
{
return [];
}
}
43 changes: 43 additions & 0 deletions src/DTO/Internal/UnresolvedFeatureDependency.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

namespace Unleash\Client\DTO\Internal;

use Unleash\Client\DTO\Feature;
use Unleash\Client\DTO\FeatureDependency;
use Unleash\Client\DTO\Variant;

/**
* @internal
*/
final readonly class UnresolvedFeatureDependency implements FeatureDependency
{
/**
* @param array<Variant>|null $requiredVariants
*/
public function __construct(
private Feature $feature,
private bool $expectedState,
private ?array $requiredVariants,
) {
}

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

public function getExpectedState(): bool
{
return $this->expectedState;
}

public function getRequiredVariants(): ?array
{
return $this->requiredVariants;
}

public function isResolved(): bool
{
return false;
}
}
62 changes: 62 additions & 0 deletions src/DTO/Internal/UnresolvedVariant.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php

namespace Unleash\Client\DTO\Internal;

use JetBrains\PhpStorm\ExpectedValues;
use Unleash\Client\DTO\Variant;
use Unleash\Client\DTO\VariantPayload;
use Unleash\Client\Enum\Stickiness;

/**
* @internal
*/
final readonly class UnresolvedVariant implements Variant
{
public function __construct(
private string $name,
) {
}

public function getName(): string
{
return $this->name;
}

public function isEnabled(): bool
{
return false;
}

public function getPayload(): ?VariantPayload
{
return null;
}

public function getWeight(): int
{
return 0;
}

public function getOverrides(): array
{
return [];
}

#[ExpectedValues(valuesFromClass: Stickiness::class)]
public function getStickiness(): string
{
return Stickiness::DEFAULT;
}

/**
* todo Change to null once rector supports it
*
* @return null
*
* @noinspection PhpMixedReturnTypeCanBeReducedInspection
*/
public function jsonSerialize(): mixed
{
return null;
}
}
50 changes: 50 additions & 0 deletions src/DefaultUnleash.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Unleash\Client\Configuration\UnleashConfiguration;
use Unleash\Client\DTO\DefaultFeatureEnabledResult;
use Unleash\Client\DTO\Feature;
use Unleash\Client\DTO\FeatureDependency;
use Unleash\Client\DTO\FeatureEnabledResult;
use Unleash\Client\DTO\Strategy;
use Unleash\Client\DTO\Variant;
Expand Down Expand Up @@ -155,6 +156,24 @@ private function isFeatureEnabled(?Feature $feature, Context $context, bool $def
return new DefaultFeatureEnabledResult();
}

$dependencies = method_exists($feature, 'getDependencies')
? $feature->getDependencies()
: [];

foreach ($dependencies as $dependency) {
if ($this->isParentDependencySatisfied($dependency, $context, $default) !== $dependency->getExpectedState()) {
$event = new FeatureToggleDisabledEvent($feature, $context);
$this->configuration->getEventDispatcher()->dispatch(
$event,
UnleashEvents::FEATURE_TOGGLE_DISABLED,
);

$this->metricsHandler->handleMetrics($feature, false);

return new DefaultFeatureEnabledResult();
}
}

$strategies = $feature->getStrategies();
if (!is_countable($strategies)) {
$strategies = iterator_to_array($strategies);
Expand Down Expand Up @@ -208,4 +227,35 @@ private function findStrategyHandlers(Strategy $strategy): array

return $handlers;
}

private function isParentDependencySatisfied(FeatureDependency $dependency, Context $context, bool $default): bool
{
if (!$dependency->isResolved()) {
return false;
}

$enabled = $this->isFeatureEnabled($dependency->getFeature(), $context, $default);
if (!$enabled->isEnabled()) {
return false;
}

assert($dependency->getFeature() !== null);

if (
method_exists($dependency->getFeature(), 'getDependencies')
&& count($dependency->getFeature()->getDependencies())
) {
return false;
}

if ($dependency->getRequiredVariants() === null || !count($dependency->getRequiredVariants())) {
return true;
}

$variant = $this->getVariant($dependency->getFeature()->getName(), $context);

$requiredVariants = array_map(fn (Variant $variant) => $variant->getName(), $dependency->getRequiredVariants());

return in_array($variant->getName(), $requiredVariants, true);
}
}
Loading

0 comments on commit fea0be6

Please sign in to comment.