diff --git a/src/Configuration/Context.php b/src/Configuration/Context.php index 3ae48421..2708f300 100755 --- a/src/Configuration/Context.php +++ b/src/Configuration/Context.php @@ -25,6 +25,9 @@ public function getSessionId(): ?string; public function getCustomProperty(string $name): string; + /** + * @todo make $value nullable + */ public function setCustomProperty(string $name, string $value): self; public function hasCustomProperty(string $name): bool; diff --git a/src/Configuration/UnleashContext.php b/src/Configuration/UnleashContext.php index 50b8c46e..0451de2d 100755 --- a/src/Configuration/UnleashContext.php +++ b/src/Configuration/UnleashContext.php @@ -55,9 +55,9 @@ public function getCustomProperty(string $name): string return $this->customContext[$name]; } - public function setCustomProperty(string $name, string $value): self + public function setCustomProperty(string $name, ?string $value): self { - $this->customContext[$name] = $value; + $this->customContext[$name] = $value ?? ''; return $this; } diff --git a/src/DTO/DefaultSegment.php b/src/DTO/DefaultSegment.php new file mode 100644 index 00000000..9ec58bae --- /dev/null +++ b/src/DTO/DefaultSegment.php @@ -0,0 +1,25 @@ + $constraints + */ + public function __construct( + private readonly int $id, + private readonly array $constraints, + ) { + } + + public function getId(): int + { + return $this->id; + } + + public function getConstraints(): array + { + return $this->constraints; + } +} diff --git a/src/DTO/DefaultStrategy.php b/src/DTO/DefaultStrategy.php index 4d14f818..7b884f64 100755 --- a/src/DTO/DefaultStrategy.php +++ b/src/DTO/DefaultStrategy.php @@ -7,11 +7,14 @@ final class DefaultStrategy implements Strategy /** * @param array $parameters * @param array $constraints + * @param array $segments */ public function __construct( private readonly string $name, private readonly array $parameters = [], private readonly array $constraints = [], + private readonly array $segments = [], + private readonly bool $nonexistentSegments = false, ) { } @@ -35,4 +38,17 @@ public function getConstraints(): array { return $this->constraints; } + + /** + * @return array + */ + public function getSegments(): array + { + return $this->segments; + } + + public function hasNonexistentSegments(): bool + { + return $this->nonexistentSegments; + } } diff --git a/src/DTO/Segment.php b/src/DTO/Segment.php new file mode 100644 index 00000000..29b85010 --- /dev/null +++ b/src/DTO/Segment.php @@ -0,0 +1,13 @@ + + */ + public function getConstraints(): array; +} diff --git a/src/DTO/Strategy.php b/src/DTO/Strategy.php index f0b3b351..7dcbff17 100755 --- a/src/DTO/Strategy.php +++ b/src/DTO/Strategy.php @@ -2,6 +2,10 @@ namespace Unleash\Client\DTO; +/** + * @method array getSegments() + * @method bool hasNonexistentSegments() + */ interface Strategy { public function getName(): string; diff --git a/src/Repository/DefaultUnleashRepository.php b/src/Repository/DefaultUnleashRepository.php index 2bb59a89..f01f06d0 100755 --- a/src/Repository/DefaultUnleashRepository.php +++ b/src/Repository/DefaultUnleashRepository.php @@ -10,13 +10,16 @@ use Psr\Http\Message\RequestFactoryInterface; use Psr\SimpleCache\InvalidArgumentException; use Unleash\Client\Configuration\UnleashConfiguration; +use Unleash\Client\DTO\Constraint; use Unleash\Client\DTO\DefaultConstraint; use Unleash\Client\DTO\DefaultFeature; +use Unleash\Client\DTO\DefaultSegment; use Unleash\Client\DTO\DefaultStrategy; use Unleash\Client\DTO\DefaultVariant; use Unleash\Client\DTO\DefaultVariantOverride; use Unleash\Client\DTO\DefaultVariantPayload; use Unleash\Client\DTO\Feature; +use Unleash\Client\DTO\Segment; use Unleash\Client\Enum\CacheKey; use Unleash\Client\Enum\Stickiness; use Unleash\Client\Event\FetchingDataFailedEvent; @@ -24,6 +27,16 @@ use Unleash\Client\Exception\HttpResponseException; use Unleash\Client\Exception\InvalidValueException; +/** + * @phpstan-type ConstraintArray array{ + * contextName: string, + * operator: string, + * values?: array, + * value?: string, + * inverted?: bool, + * caseInsensitive?: bool + * } + */ final class DefaultUnleashRepository implements UnleashRepository { public function __construct( @@ -65,7 +78,9 @@ public function getFeatures(): iterable $request = $this->requestFactory ->createRequest('GET', $this->configuration->getUrl() . 'client/features') ->withHeader('UNLEASH-APPNAME', $this->configuration->getAppName()) - ->withHeader('UNLEASH-INSTANCEID', $this->configuration->getInstanceId()); + ->withHeader('UNLEASH-INSTANCEID', $this->configuration->getInstanceId()) + ->withHeader('Unleash-Client-Spec', '4.2.2') + ; foreach ($this->configuration->getHeaders() as $name => $value) { $request = $request->withHeader($name, $value); @@ -141,6 +156,8 @@ private function parseFeatures(string $rawBody): array $body = json_decode($rawBody, true, 512, JSON_THROW_ON_ERROR); assert(is_array($body)); + $globalSegments = $this->parseSegments($body['segments'] ?? []); + if (!isset($body['features']) || !is_array($body['features'])) { throw new InvalidValueException("The body isn't valid because it doesn't contain a 'features' key"); } @@ -150,21 +167,24 @@ private function parseFeatures(string $rawBody): array $variants = []; foreach ($feature['strategies'] as $strategy) { - $constraints = []; - foreach ($strategy['constraints'] ?? [] as $constraint) { - $constraints[] = new DefaultConstraint( - $constraint['contextName'], - $constraint['operator'], - $constraint['values'] ?? null, - $constraint['value'] ?? null, - $constraint['inverted'] ?? false, - $constraint['caseInsensitive'] ?? false, - ); + $constraints = $this->parseConstraints($strategy['constraints'] ?? []); + + $hasNonexistentSegments = false; + $segments = []; + foreach ($strategy['segments'] ?? [] as $segment) { + if (isset($globalSegments[$segment])) { + $segments[] = $globalSegments[$segment]; + } else { + $hasNonexistentSegments = true; + break; + } } $strategies[] = new DefaultStrategy( $strategy['name'], $strategy['parameters'] ?? [], - $constraints + $constraints, + $segments, + $hasNonexistentSegments, ); } foreach ($feature['variants'] ?? [] as $variant) { @@ -183,6 +203,7 @@ private function parseFeatures(string $rawBody): array $overrides, ); } + $features[$feature['name']] = new DefaultFeature( $feature['name'], $feature['enabled'], @@ -222,4 +243,45 @@ private function setLastValidState(string $data): void $this->configuration->getStaleTtl(), ); } + + /** + * @param array}> $segmentsRaw + * + * @return array + */ + private function parseSegments(array $segmentsRaw): array + { + $result = []; + foreach ($segmentsRaw as $segmentRaw) { + $result[$segmentRaw['id']] = new DefaultSegment( + $segmentRaw['id'], + $this->parseConstraints($segmentRaw['constraints']), + ); + } + + return $result; + } + + /** + * @param array $constraintsRaw + * + * @return array + */ + private function parseConstraints(array $constraintsRaw): array + { + $constraints = []; + + foreach ($constraintsRaw as $constraint) { + $constraints[] = new DefaultConstraint( + $constraint['contextName'], + $constraint['operator'], + $constraint['values'] ?? null, + $constraint['value'] ?? null, + $constraint['inverted'] ?? false, + $constraint['caseInsensitive'] ?? false, + ); + } + + return $constraints; + } } diff --git a/src/Strategy/AbstractStrategyHandler.php b/src/Strategy/AbstractStrategyHandler.php index dc168b2b..881a839a 100755 --- a/src/Strategy/AbstractStrategyHandler.php +++ b/src/Strategy/AbstractStrategyHandler.php @@ -3,6 +3,7 @@ namespace Unleash\Client\Strategy; use Unleash\Client\Configuration\Context; +use Unleash\Client\DTO\Constraint; use Unleash\Client\DTO\Strategy; use Unleash\Client\Helper\ConstraintValidatorTrait; @@ -24,13 +25,32 @@ protected function findParameter(string $parameter, Strategy $strategy): ?string protected function validateConstraints(Strategy $strategy, Context $context): bool { - $constraints = $strategy->getConstraints(); + if (method_exists($strategy, 'hasNonexistentSegments') && $strategy->hasNonexistentSegments()) { + return false; + } + + $validator = $this->getValidator(); + $constraints = $this->getConstraintsForStrategy($strategy); + foreach ($constraints as $constraint) { - if (!$this->getValidator()->validateConstraint($constraint, $context)) { + if (!$validator->validateConstraint($constraint, $context)) { return false; } } return true; } + + /** + * @return iterable + */ + private function getConstraintsForStrategy(Strategy $strategy): iterable + { + yield from $strategy->getConstraints(); + + $segments = method_exists($strategy, 'getSegments') ? $strategy->getSegments() : []; + foreach ($segments as $segment) { + yield from $segment->getConstraints(); + } + } } diff --git a/tests/CoverageOnlyTest.php b/tests/CoverageOnlyTest.php new file mode 100644 index 00000000..3f4a1bd1 --- /dev/null +++ b/tests/CoverageOnlyTest.php @@ -0,0 +1,19 @@ +getId()); + } +} diff --git a/tests/client-specification b/tests/client-specification index 294fd471..eda1f785 160000 --- a/tests/client-specification +++ b/tests/client-specification @@ -1 +1 @@ -Subproject commit 294fd47171865c5c3f47729e547f7ef8fbed9291 +Subproject commit eda1f785b941160c2f8f5f6035dc7ddb49117843