From 0eda242445f8b715b7f33a2c79d3a7e9aa4fe687 Mon Sep 17 00:00:00 2001 From: Evandro Myller Date: Tue, 16 Sep 2025 15:28:25 -0300 Subject: [PATCH 01/27] Introduce context values evaluation data model --- src/Engine/Engine.php | 12 +++++ .../Types/Context/EnvironmentContext.php | 22 ++++++++ .../Utils/Types/Context/EvaluationContext.php | 32 +++++++++++ .../Utils/Types/Context/FeatureContext.php | 54 +++++++++++++++++++ .../Utils/Types/Context/FeatureValue.php | 22 ++++++++ .../Utils/Types/Context/IdentityContext.php | 27 ++++++++++ src/Engine/Utils/Types/Context/RuleType.php | 9 ++++ .../Utils/Types/Context/SegmentCondition.php | 27 ++++++++++ .../Context/SegmentConditionOperator.php | 20 +++++++ .../Utils/Types/Context/SegmentContext.php | 32 +++++++++++ .../Utils/Types/Context/SegmentRule.php | 27 ++++++++++ .../Utils/Types/Result/EvaluationResult.php | 29 ++++++++++ src/Engine/Utils/Types/Result/FlagResult.php | 37 +++++++++++++ .../Utils/Types/Result/SegmentResult.php | 22 ++++++++ 14 files changed, 372 insertions(+) create mode 100644 src/Engine/Utils/Types/Context/EnvironmentContext.php create mode 100644 src/Engine/Utils/Types/Context/EvaluationContext.php create mode 100644 src/Engine/Utils/Types/Context/FeatureContext.php create mode 100644 src/Engine/Utils/Types/Context/FeatureValue.php create mode 100644 src/Engine/Utils/Types/Context/IdentityContext.php create mode 100644 src/Engine/Utils/Types/Context/RuleType.php create mode 100644 src/Engine/Utils/Types/Context/SegmentCondition.php create mode 100644 src/Engine/Utils/Types/Context/SegmentConditionOperator.php create mode 100644 src/Engine/Utils/Types/Context/SegmentContext.php create mode 100644 src/Engine/Utils/Types/Context/SegmentRule.php create mode 100644 src/Engine/Utils/Types/Result/EvaluationResult.php create mode 100644 src/Engine/Utils/Types/Result/FlagResult.php create mode 100644 src/Engine/Utils/Types/Result/SegmentResult.php diff --git a/src/Engine/Engine.php b/src/Engine/Engine.php index 9d017ff..16cbcf2 100644 --- a/src/Engine/Engine.php +++ b/src/Engine/Engine.php @@ -7,9 +7,21 @@ use Flagsmith\Engine\Identities\IdentityModel; use Flagsmith\Engine\Segments\SegmentEvaluator; use Flagsmith\Engine\Utils\Exceptions\FeatureStateNotFound; +use Flagsmith\Engine\Utils\Types\Context\EvaluationContext; +use Flagsmith\Engine\Utils\Types\Result\EvaluationResult; class Engine { + /** + * Get the evaluation result for a given context. + * @param EvaluationContext $context + * @return EvaluationResult + */ + public static function getEvaluationResult($context): EvaluationResult + { + // ... + } + /** * Get the environment feature states. * @param EnvironmentModel $environment diff --git a/src/Engine/Utils/Types/Context/EnvironmentContext.php b/src/Engine/Utils/Types/Context/EnvironmentContext.php new file mode 100644 index 0000000..39389a6 --- /dev/null +++ b/src/Engine/Utils/Types/Context/EnvironmentContext.php @@ -0,0 +1,22 @@ +key = $key; + $this->name = $name; + } +} diff --git a/src/Engine/Utils/Types/Context/EvaluationContext.php b/src/Engine/Utils/Types/Context/EvaluationContext.php new file mode 100644 index 0000000..55f0846 --- /dev/null +++ b/src/Engine/Utils/Types/Context/EvaluationContext.php @@ -0,0 +1,32 @@ + */ + public $segments; + + /** @var array */ + public $features; + + /** + * @param EnvironmentContext $environment + * @param ?IdentityContext $identity + * @param ?array $segments + * @param ?array $features + */ + public function __construct($environment, $identity, $segments, $features) + { + $this->environment = $environment; + $this->identity = $identity; + $this->segments = $segments ?? []; + $this->features = $features ?? []; + } +} diff --git a/src/Engine/Utils/Types/Context/FeatureContext.php b/src/Engine/Utils/Types/Context/FeatureContext.php new file mode 100644 index 0000000..84c6eb2 --- /dev/null +++ b/src/Engine/Utils/Types/Context/FeatureContext.php @@ -0,0 +1,54 @@ + */ + public $variants; + + /** + * @param string $key + * @param string $feature_key + * @param string $name + * @param bool $enabled + * @param mixed $value + * @param ?array $variants + * @param ?float $priority + */ + public function __construct( + $key, + $feature_key, + $name, + $enabled, + $value, + $variants, + $priority, + ) { + $this->key = $key; + $this->feature_key = $feature_key; + $this->name = $name; + $this->enabled = $enabled; + $this->value = $value; + $this->priority = $priority ?? INF; + $this->variants = $variants ?? []; + } +} diff --git a/src/Engine/Utils/Types/Context/FeatureValue.php b/src/Engine/Utils/Types/Context/FeatureValue.php new file mode 100644 index 0000000..3b700d8 --- /dev/null +++ b/src/Engine/Utils/Types/Context/FeatureValue.php @@ -0,0 +1,22 @@ +value = $value; + $this->weight = $weight; + } +} diff --git a/src/Engine/Utils/Types/Context/IdentityContext.php b/src/Engine/Utils/Types/Context/IdentityContext.php new file mode 100644 index 0000000..72390de --- /dev/null +++ b/src/Engine/Utils/Types/Context/IdentityContext.php @@ -0,0 +1,27 @@ + */ + public $traits; + + /** + * @param string $key + * @param string $identifier + * @param ?array $traits + */ + public function __construct($key, $identifier, $traits) + { + $this->key = $key; + $this->identifier = $identifier; + $this->traits = $traits ?? []; + } +} diff --git a/src/Engine/Utils/Types/Context/RuleType.php b/src/Engine/Utils/Types/Context/RuleType.php new file mode 100644 index 0000000..47d9e10 --- /dev/null +++ b/src/Engine/Utils/Types/Context/RuleType.php @@ -0,0 +1,9 @@ + */ + public $value; + + /** + * @param string $property + * @param SegmentConditionOperator $operator + * @param string|array $value + */ + public function __construct($property, $operator, $value) + { + $this->property = $property; + $this->operator = $operator; + $this->value = $value; + } +} diff --git a/src/Engine/Utils/Types/Context/SegmentConditionOperator.php b/src/Engine/Utils/Types/Context/SegmentConditionOperator.php new file mode 100644 index 0000000..8c9aa44 --- /dev/null +++ b/src/Engine/Utils/Types/Context/SegmentConditionOperator.php @@ -0,0 +1,20 @@ + */ + public $rules; + + /** @var array */ + public $overrides; + + /** + * @param string $key + * @param string $name + * @param array $rules + * @param ?array $overrides + */ + public function __construct($key, $name, $rules, $overrides) + { + $this->key = $key; + $this->name = $name; + $this->rules = $rules; + $this->overrides = $overrides ?? []; + } +} diff --git a/src/Engine/Utils/Types/Context/SegmentRule.php b/src/Engine/Utils/Types/Context/SegmentRule.php new file mode 100644 index 0000000..19884cc --- /dev/null +++ b/src/Engine/Utils/Types/Context/SegmentRule.php @@ -0,0 +1,27 @@ + */ + public $conditions; + + /** @var array */ + public $rules; + + /** + * @param RuleType $type + * @param ?array $conditions + * @param ?array $rules + */ + public function __construct($type, $conditions, $rules) + { + $this->type = $type; + $this->conditions = $conditions ?? []; + $this->rules = $rules ?? []; + } +} diff --git a/src/Engine/Utils/Types/Result/EvaluationResult.php b/src/Engine/Utils/Types/Result/EvaluationResult.php new file mode 100644 index 0000000..bd80512 --- /dev/null +++ b/src/Engine/Utils/Types/Result/EvaluationResult.php @@ -0,0 +1,29 @@ + */ + public array $flags; + + /** @var array */ + public array $segments; + + /** + * @param EvaluationContext $context + * @param array $flags + * @param array $segments + */ + public function __construct($context, $flags, $segments) + { + $this->context = $context; + $this->flags = $flags; + $this->segments = $segments; + } +} diff --git a/src/Engine/Utils/Types/Result/FlagResult.php b/src/Engine/Utils/Types/Result/FlagResult.php new file mode 100644 index 0000000..88d8b13 --- /dev/null +++ b/src/Engine/Utils/Types/Result/FlagResult.php @@ -0,0 +1,37 @@ +feature_key = $feature_key; + $this->name = $name; + $this->enabled = $enabled; + $this->value = $value; + $this->reason = $reason; + } +} diff --git a/src/Engine/Utils/Types/Result/SegmentResult.php b/src/Engine/Utils/Types/Result/SegmentResult.php new file mode 100644 index 0000000..eb3d253 --- /dev/null +++ b/src/Engine/Utils/Types/Result/SegmentResult.php @@ -0,0 +1,22 @@ +key = $key; + $this->name = $name; + } +} From 3caac934aa6a29fefb526e20fc7d92e5ede7ac8e Mon Sep 17 00:00:00 2001 From: Evandro Myller Date: Thu, 18 Sep 2025 14:19:50 -0300 Subject: [PATCH 02/27] Update engine test data --- .gitmodules | 2 +- tests/Engine/EngineTests/EngineTestData | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitmodules b/.gitmodules index 845c2f1..5417c42 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,4 @@ [submodule "tests/Engine/EngineTests/EngineTestData"] path = tests/Engine/EngineTests/EngineTestData url = git@github.com:Flagsmith/engine-test-data.git - branch = v1.0.0 + branch = feat/context-values-intensifies diff --git a/tests/Engine/EngineTests/EngineTestData b/tests/Engine/EngineTests/EngineTestData index 933f2ba..5e7c413 160000 --- a/tests/Engine/EngineTests/EngineTestData +++ b/tests/Engine/EngineTests/EngineTestData @@ -1 +1 @@ -Subproject commit 933f2ba7aa6430797afc2d053530cfd005b461f6 +Subproject commit 5e7c4139c59e529301f7dc8f784e991f1c8840fb From c3bc44a8a6a587b02a3a5a521f07cb614f792191 Mon Sep 17 00:00:00 2001 From: Evandro Myller Date: Thu, 18 Sep 2025 19:32:50 -0300 Subject: [PATCH 03/27] Get ready for engine tests --- .../Types/Context/EnvironmentContext.php | 10 -- .../Utils/Types/Context/EvaluationContext.php | 92 +++++++++++++++++-- .../Utils/Types/Context/FeatureContext.php | 27 ------ .../Utils/Types/Context/FeatureValue.php | 10 -- .../Utils/Types/Context/IdentityContext.php | 12 --- .../Utils/Types/Context/SegmentCondition.php | 12 --- .../Utils/Types/Context/SegmentContext.php | 14 --- .../Utils/Types/Context/SegmentRule.php | 12 --- .../Utils/Types/Result/EvaluationResult.php | 12 --- src/Engine/Utils/Types/Result/FlagResult.php | 16 ---- tests/Engine/EngineTests/EngineDataTest.php | 57 +++--------- 11 files changed, 98 insertions(+), 176 deletions(-) diff --git a/src/Engine/Utils/Types/Context/EnvironmentContext.php b/src/Engine/Utils/Types/Context/EnvironmentContext.php index 39389a6..b51bdfc 100644 --- a/src/Engine/Utils/Types/Context/EnvironmentContext.php +++ b/src/Engine/Utils/Types/Context/EnvironmentContext.php @@ -9,14 +9,4 @@ class EnvironmentContext /** @var string */ public $name; - - /** - * @param string $key - * @param string $name - */ - public function __construct($key, $name) - { - $this->key = $key; - $this->name = $name; - } } diff --git a/src/Engine/Utils/Types/Context/EvaluationContext.php b/src/Engine/Utils/Types/Context/EvaluationContext.php index 55f0846..36bcd2d 100644 --- a/src/Engine/Utils/Types/Context/EvaluationContext.php +++ b/src/Engine/Utils/Types/Context/EvaluationContext.php @@ -17,16 +17,90 @@ class EvaluationContext public $features; /** - * @param EnvironmentContext $environment - * @param ?IdentityContext $identity - * @param ?array $segments - * @param ?array $features + * @param object $jsonContext + * @return EvaluationContext */ - public function __construct($environment, $identity, $segments, $features) + public static function fromJsonObject($jsonContext) { - $this->environment = $environment; - $this->identity = $identity; - $this->segments = $segments ?? []; - $this->features = $features ?? []; + $context = new EvaluationContext; + + $context->environment = new EnvironmentContext; + $context->environment->key = $jsonContext->environment->key; + $context->environment->name = $jsonContext->environment->name; + + $context->identity = new IdentityContext; + $context->identity->key = $jsonContext->identity->key; + $context->identity->identifier = $jsonContext->identity->identifier; + $context->identity->traits = $jsonContext->identity->traits; + + $context->segments = []; + foreach ($jsonContext->segments as $jsonSegment) { + $segment = new SegmentContext; + $segment->key = $jsonSegment->key; + $segment->name = $jsonSegment->name; + $segment->rules = _convertRules($jsonSegment->rules ?? []); + $segment->overrides = _convertFeatures($jsonSegment->overrides ?? []); + $context->segments[$segment->key] = $segment; + } + + $context->features = _convertFeatures($jsonContext->features ?? []); + + return $context; + } +} + +/** + * @param array $jsonRules + * @return array + */ +function _convertRules($jsonRules) +{ + $rules = []; + foreach ($jsonRules as $jsonRule) { + $rule = new SegmentRule; + $rule->type = RuleType::from($jsonRule->type); + + $rule->conditions = []; + foreach (($jsonRule->conditions ?? []) as $jsonCondition) { + $condition = new SegmentCondition; + $condition->property = $jsonCondition->property; + $condition->operator = SegmentConditionOperator::from($jsonCondition->operator); + $condition->value = $jsonCondition->value; + $rule->conditions[] = $condition; + } + + $rule->rules = empty($jsonRule->rules) ? [] : _convertRules($jsonRule->rules); + + $rules[] = $rule; } + + return $rules; +} + +/** + * @param array $jsonFeatures + * @return array + */ +function _convertFeatures($jsonFeatures) +{ + $features = []; + foreach ($jsonFeatures as $jsonFeature) { + $feature = new FeatureContext; + $feature->key = $jsonFeature->key; + $feature->feature_key = $jsonFeature->feature_key; + $feature->name = $jsonFeature->name; + $feature->enabled = $jsonFeature->enabled; + $feature->value = $jsonFeature->value; + $feature->priority = $jsonFeature->priority; + $feature->variants = []; + foreach (($jsonFeature->variants ?? []) as $jsonVariant) { + $variant = new FeatureValue; + $variant->value = $jsonVariant->value; + $variant->weight = $jsonVariant->weight; + $feature->variants[] = $variant; + } + $features[] = $feature; + } + + return $features; } diff --git a/src/Engine/Utils/Types/Context/FeatureContext.php b/src/Engine/Utils/Types/Context/FeatureContext.php index 84c6eb2..8d12bb3 100644 --- a/src/Engine/Utils/Types/Context/FeatureContext.php +++ b/src/Engine/Utils/Types/Context/FeatureContext.php @@ -24,31 +24,4 @@ class FeatureContext /** @var array */ public $variants; - - /** - * @param string $key - * @param string $feature_key - * @param string $name - * @param bool $enabled - * @param mixed $value - * @param ?array $variants - * @param ?float $priority - */ - public function __construct( - $key, - $feature_key, - $name, - $enabled, - $value, - $variants, - $priority, - ) { - $this->key = $key; - $this->feature_key = $feature_key; - $this->name = $name; - $this->enabled = $enabled; - $this->value = $value; - $this->priority = $priority ?? INF; - $this->variants = $variants ?? []; - } } diff --git a/src/Engine/Utils/Types/Context/FeatureValue.php b/src/Engine/Utils/Types/Context/FeatureValue.php index 3b700d8..ed511ea 100644 --- a/src/Engine/Utils/Types/Context/FeatureValue.php +++ b/src/Engine/Utils/Types/Context/FeatureValue.php @@ -9,14 +9,4 @@ class FeatureValue /** @var float */ public $weight; - - /** - * @param mixed $value - * @param float $weight - */ - public function __construct($value, $weight) - { - $this->value = $value; - $this->weight = $weight; - } } diff --git a/src/Engine/Utils/Types/Context/IdentityContext.php b/src/Engine/Utils/Types/Context/IdentityContext.php index 72390de..a139477 100644 --- a/src/Engine/Utils/Types/Context/IdentityContext.php +++ b/src/Engine/Utils/Types/Context/IdentityContext.php @@ -12,16 +12,4 @@ class IdentityContext /** @var array */ public $traits; - - /** - * @param string $key - * @param string $identifier - * @param ?array $traits - */ - public function __construct($key, $identifier, $traits) - { - $this->key = $key; - $this->identifier = $identifier; - $this->traits = $traits ?? []; - } } diff --git a/src/Engine/Utils/Types/Context/SegmentCondition.php b/src/Engine/Utils/Types/Context/SegmentCondition.php index c4040f4..fe5f77d 100644 --- a/src/Engine/Utils/Types/Context/SegmentCondition.php +++ b/src/Engine/Utils/Types/Context/SegmentCondition.php @@ -12,16 +12,4 @@ class SegmentCondition /** @var string|array */ public $value; - - /** - * @param string $property - * @param SegmentConditionOperator $operator - * @param string|array $value - */ - public function __construct($property, $operator, $value) - { - $this->property = $property; - $this->operator = $operator; - $this->value = $value; - } } diff --git a/src/Engine/Utils/Types/Context/SegmentContext.php b/src/Engine/Utils/Types/Context/SegmentContext.php index ef7cd10..c4cb2c1 100644 --- a/src/Engine/Utils/Types/Context/SegmentContext.php +++ b/src/Engine/Utils/Types/Context/SegmentContext.php @@ -15,18 +15,4 @@ class SegmentContext /** @var array */ public $overrides; - - /** - * @param string $key - * @param string $name - * @param array $rules - * @param ?array $overrides - */ - public function __construct($key, $name, $rules, $overrides) - { - $this->key = $key; - $this->name = $name; - $this->rules = $rules; - $this->overrides = $overrides ?? []; - } } diff --git a/src/Engine/Utils/Types/Context/SegmentRule.php b/src/Engine/Utils/Types/Context/SegmentRule.php index 19884cc..7eba7a7 100644 --- a/src/Engine/Utils/Types/Context/SegmentRule.php +++ b/src/Engine/Utils/Types/Context/SegmentRule.php @@ -12,16 +12,4 @@ class SegmentRule /** @var array */ public $rules; - - /** - * @param RuleType $type - * @param ?array $conditions - * @param ?array $rules - */ - public function __construct($type, $conditions, $rules) - { - $this->type = $type; - $this->conditions = $conditions ?? []; - $this->rules = $rules ?? []; - } } diff --git a/src/Engine/Utils/Types/Result/EvaluationResult.php b/src/Engine/Utils/Types/Result/EvaluationResult.php index bd80512..4fc4efb 100644 --- a/src/Engine/Utils/Types/Result/EvaluationResult.php +++ b/src/Engine/Utils/Types/Result/EvaluationResult.php @@ -14,16 +14,4 @@ class EvaluationResult /** @var array */ public array $segments; - - /** - * @param EvaluationContext $context - * @param array $flags - * @param array $segments - */ - public function __construct($context, $flags, $segments) - { - $this->context = $context; - $this->flags = $flags; - $this->segments = $segments; - } } diff --git a/src/Engine/Utils/Types/Result/FlagResult.php b/src/Engine/Utils/Types/Result/FlagResult.php index 88d8b13..7a67f91 100644 --- a/src/Engine/Utils/Types/Result/FlagResult.php +++ b/src/Engine/Utils/Types/Result/FlagResult.php @@ -18,20 +18,4 @@ class FlagResult /** @var ?string */ public $reason; - - /** - * @param string $feature_key - * @param string $name - * @param bool $enabled - * @param ?mixed $value - * @param ?string $reason - */ - public function __construct($feature_key, $name, $enabled, $value, $reason) - { - $this->feature_key = $feature_key; - $this->name = $name; - $this->enabled = $enabled; - $this->value = $value; - $this->reason = $reason; - } } diff --git a/tests/Engine/EngineTests/EngineDataTest.php b/tests/Engine/EngineTests/EngineDataTest.php index 5938754..23a4a97 100644 --- a/tests/Engine/EngineTests/EngineDataTest.php +++ b/tests/Engine/EngineTests/EngineDataTest.php @@ -1,28 +1,25 @@ > */ public function extractTestCases() { $fileContents = file_get_contents(__DIR__ . '/EngineTestData/data/environment_n9fbf9h3v4fFgH3U3ngWhb.json'); $contents = json_decode($fileContents); - $environmentModel = EnvironmentModel::build($contents->environment); - $parameters = []; - foreach ($contents->identities_and_responses as $testCase) { - $parameters[] = [ - $environmentModel, - IdentityModel::build($testCase->identity), - $testCase->response - ]; + foreach ($contents->test_cases as $testCase) { + $context = EvaluationContext::fromJsonObject($testCase->context); + $parameters[] = [$context, $testCase->response]; } return $parameters; @@ -30,40 +27,16 @@ public function extractTestCases() /** * @dataProvider extractTestCases + * @param EvaluationContext $evaluationContext + * @param object $expectedResult + * @return void */ - public function testEngine($environmentModel, $identityModel, $expectedResponse) + public function testEngine($evaluationContext, $expectedResult): void { - $this->attempt++; - $engineResponse = Engine::getIdentityFeatureStates($environmentModel, $identityModel); - - usort( - $engineResponse, - fn ($fs1, $fs2) => $fs1->getFeature()->getName() <=> $fs2->getFeature()->getName() - ); - - $flags = $expectedResponse->flags; - usort( - $flags, - fn ($fs1, $fs2) => $fs1->feature->name <=> $fs2->feature->name - ); + // When + $evaluationResult = Engine::getEvaluationResult($evaluationContext); - $this->assertEquals( - count($flags), - count($engineResponse) - ); - - foreach ($engineResponse as $index => $featureState) { - $val = $featureState->getValue($identityModel->getDjangoId()); - $expectedVal = $flags[$index]->feature_state_value; - - $this->assertEquals( - $val, - $expectedVal - ); - $this->assertEquals( - count($flags), - count($engineResponse) - ); - } + // Then + $this->assertEquals($expectedResult, $evaluationResult); } } From b285f7de412b9c73055ccb2eacacd02ba38e3fb4 Mon Sep 17 00:00:00 2001 From: Evandro Myller Date: Fri, 19 Sep 2025 20:50:08 -0300 Subject: [PATCH 04/27] Introduce new engine --- composer.json | 3 +- src/Engine/Engine.php | 355 +++++++++++++++++- .../Utils/Types/Context/EvaluationContext.php | 128 ++++--- .../Utils/Types/Context/FeatureContext.php | 30 +- .../{RuleType.php => SegmentRuleType.php} | 2 +- .../Utils/Types/Result/SegmentResult.php | 10 - tests/Engine/EngineTests/EngineDataTest.php | 31 +- tests/Engine/EngineTests/EngineTestData | 2 +- 8 files changed, 477 insertions(+), 84 deletions(-) rename src/Engine/Utils/Types/Context/{RuleType.php => SegmentRuleType.php} (81%) diff --git a/composer.json b/composer.json index 4fa38b2..dcc7222 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,8 @@ "psr/simple-cache": ">=1.0", "psr/http-factory-implementation": "1.0", "psr/http-client-implementation": "1.0", - "psr/http-message-implementation": "1.0" + "psr/http-message-implementation": "1.0", + "softcreatr/jsonpath": "^0.10.0" }, "require-dev": { "guzzlehttp/guzzle": "^7.3", diff --git a/src/Engine/Engine.php b/src/Engine/Engine.php index 16cbcf2..7f96188 100644 --- a/src/Engine/Engine.php +++ b/src/Engine/Engine.php @@ -7,19 +7,368 @@ use Flagsmith\Engine\Identities\IdentityModel; use Flagsmith\Engine\Segments\SegmentEvaluator; use Flagsmith\Engine\Utils\Exceptions\FeatureStateNotFound; +use Flagsmith\Engine\Utils\Hashing; +use Flagsmith\Engine\Utils\Semver; use Flagsmith\Engine\Utils\Types\Context\EvaluationContext; +use Flagsmith\Engine\Utils\Types\Context\FeatureContext; +use Flagsmith\Engine\Utils\Types\Context\SegmentRuleType; +use Flagsmith\Engine\Utils\Types\Context\SegmentCondition; +use Flagsmith\Engine\Utils\Types\Context\SegmentConditionOperator; +use Flagsmith\Engine\Utils\Types\Context\SegmentContext; +use Flagsmith\Engine\Utils\Types\Context\SegmentRule; use Flagsmith\Engine\Utils\Types\Result\EvaluationResult; +use Flagsmith\Engine\Utils\Types\Result\FlagResult; +use Flagsmith\Engine\Utils\Types\Result\SegmentResult; +use Flow\JSONPath\JSONPath; +use Flow\JSONPath\JSONPathException; class Engine { /** * Get the evaluation result for a given context. - * @param EvaluationContext $context - * @return EvaluationResult + * + * @param EvaluationContext $context The evaluation context. + * @return EvaluationResult EvaluationResult containing the context, flags, and segments */ public static function getEvaluationResult($context): EvaluationResult { - // ... + /** @var array */ + $evaluatedSegments = []; + + /** @var array */ + $evaluatedFeatures = []; + + /** @var array */ + $matchedSegmentsByFeatureKey = []; + + /** @var array */ + $evaluatedFlags = []; + + foreach ($context->segments as $segment) { + if (!self::isContextInSegment($context, $segment)) { + continue; + } + + $segmentResult = new SegmentResult(); + $segmentResult->key = $segment->key; + $segmentResult->name = $segment->name; + $evaluatedSegments[] = $segmentResult; + + if (empty($segment->overrides)) { + continue; + } + + foreach ($segment->overrides as $overrideFeature) { + $featureKey = $overrideFeature->feature_key; + $evaluatedFeature = $evaluatedFeatures[$featureKey] ?? null; + if ($evaluatedFeature) { + $overrideWinsPriority = + ($overrideFeature->priority ?? INF) < + ($evaluatedFeature->priority ?? INF); + if (!$overrideWinsPriority) { + continue; + } + } + + $evaluatedFeatures[$featureKey] = $overrideFeature; + $matchedSegmentsByFeatureKey[$featureKey] = $segment; + } + } + + foreach ($context->features as $feature) { + $featureKey = $feature->feature_key; + $evaluatedFeature = $evaluatedFeatures[$featureKey] ?? null; + if ($evaluatedFeature) { + $evaluatedFlags[] = self::getFlagResultFromSegmentContext( + $evaluatedFeature, + $matchedSegmentsByFeatureKey[$featureKey], + ); + continue; + } + + $evaluatedFlags[] = self::getFlagResultFromFeatureContext( + $feature, + $context->identity?->key, + ); + } + + $result = new EvaluationResult(); + $result->context = $context; + $result->flags = $evaluatedFlags; + $result->segments = $evaluatedSegments; + return $result; + } + + /** + * @param EvaluationContext $context + * @param SegmentContext $segment + * @return bool + */ + private static function isContextInSegment($context, $segment): bool + { + if (empty($segment->rules)) { + return false; + } + + foreach ($segment->rules as $rule) { + if (!self::_contextMatchesRule($context, $rule, $segment->key)) { + return false; + } + } + + return true; + } + + /** + * @param FeatureContext $feature + * @param ?string $splitKey + * @return FlagResult + */ + private static function getFlagResultFromFeatureContext($feature, $splitKey) + { + if ($splitKey !== null && !empty($feature->variants)) { + $percentageValue = new Hashing()->getHashedPercentageForObjectIds([ + $feature->key, + $splitKey, + ]); + + $startPercentage = 0.0; + foreach ($feature->variants as $variant) { + $limit = $variant->weight + $startPercentage; + if ( + $startPercentage <= $percentageValue && + $percentageValue < $limit + ) { + $flag = new FlagResult(); + $flag->feature_key = $feature->feature_key; + $flag->name = $feature->name; + $flag->enabled = $feature->enabled; + $flag->value = $variant->value; + $flag->reason = "SPLIT; weight={$variant->weight}"; + return $flag; + } + $startPercentage = $limit; + } + } + + $flag = new FlagResult(); + $flag->feature_key = $feature->feature_key; + $flag->name = $feature->name; + $flag->enabled = $feature->enabled; + $flag->value = $feature->value; + $flag->reason = 'DEFAULT'; + return $flag; + } + + /** + * @param FeatureContext $feature + * @param SegmentContext $segment + * @return FlagResult + */ + private static function getFlagResultFromSegmentContext($feature, $segment) + { + $flag = new FlagResult(); + $flag->feature_key = $feature->feature_key; + $flag->name = $feature->name; + $flag->enabled = $feature->enabled; + $flag->value = $feature->value; + $flag->reason = "TARGETING_MATCH; segment={$segment->name}"; + return $flag; + } + + /** + * @param EvaluationContext $context + * @param SegmentRule $rule + * @param string $segmentKey + * @return bool + */ + private static function _contextMatchesRule( + $context, + $rule, + $segmentKey, + ): bool { + $any = false; + foreach ($rule->conditions as $condition) { + $conditionMatches = self::_contextMatchesCondition( + $context, + $condition, + $segmentKey, + ); + + switch ($rule->type) { + case SegmentRuleType::ALL: + if (!$conditionMatches) { + return false; + } + break; + case SegmentRuleType::NONE: + if ($conditionMatches) { + return false; + } + break; + case SegmentRuleType::ANY: + if ($conditionMatches) { + $any = true; + break 2; + } + break; + } + } + + if ($rule->type === SegmentRuleType::ANY && !$any) { + return false; + } + + foreach ($rule->rules as $subRule) { + $ruleMatches = self::_contextMatchesRule( + $context, + $subRule, + $segmentKey, + ); + if (!$ruleMatches) { + return false; + } + } + + return true; + } + + /** + * @param EvaluationContext $context + * @param SegmentCondition $condition + * @param string $segmentKey + * @return bool + */ + private static function _contextMatchesCondition( + $context, + $condition, + $segmentKey, + ): bool { + $contextValue = self::_getContextValue($context, $condition->property); + + switch ($condition->operator) { + case SegmentConditionOperator::IN: + /** @var array $inValues */ + if (is_array($contextValue)) { + $inValues = $condition->value; + } else { + $inValues = json_decode($condition->value, true); + $jsonDecodingFailed = $inValues === null; + if ($jsonDecodingFailed || !is_array($inValues)) { + $inValues = explode(',', $condition->value); + } + } + return in_array($contextValue, $inValues, strict: true); + + case SegmentConditionOperator::PERCENTAGE_SPLIT: + if (!is_numeric($condition->value)) { + return false; + } + + /** @var array $objectIds */ + if ($contextValue !== null) { + $objectIds = [$segmentKey, $contextValue]; + } elseif ($context->identity !== null) { + $objectIds = [$segmentKey, $context->identity->key]; + } else { + return false; + } + + $threshold = new Hashing()->getHashedPercentageForObjectIds( + $objectIds, + ); + return $threshold <= floatval($condition->value); + + case SegmentConditionOperator::MODULO: + if (!is_numeric($contextValue)) { + return false; + } + + [$divisor, $remainder] = explode('|', $condition->value); + if (!is_numeric($divisor) || !is_numeric($remainder)) { + return false; + } + + return floatval($contextValue) % $divisor === $remainder; + + case SegmentConditionOperator::IS_NOT_SET: + return $contextValue === null; + + case SegmentConditionOperator::IS_SET: + return $contextValue !== null; + + case SegmentConditionOperator::CONTAINS: + return str_contains($contextValue, $condition->value); + + case SegmentConditionOperator::NOT_CONTAINS: + return !str_contains($contextValue, $condition->value); + + case SegmentConditionOperator::REGEX: + return boolval( + preg_match("/{$condition->value}/", $contextValue), + ); + } + + if ($contextValue === null) { + return false; + } + + $operator = match ($condition->operator) { + SegmentConditionOperator::EQUAL => '==', + SegmentConditionOperator::NOT_EQUAL => '!=', + SegmentConditionOperator::GREATER_THAN => '>', + SegmentConditionOperator::GREATER_THAN_INCLUSIVE => '>=', + SegmentConditionOperator::LESS_THAN => '<', + SegmentConditionOperator::LESS_THAN_INCLUSIVE => '<=', + default => null, + }; + + if (is_string($contextValue) && Semver::isSemver($contextValue)) { + $contextValue = Semver::removeSemverSuffix($contextValue); + return $operator !== null && + version_compare($contextValue, $condition->value, $operator); + } + + return match ($operator) { + '==' => $contextValue == $condition->value, + '!=' => $contextValue != $condition->value, + '>' => $contextValue > $condition->value, + '>=' => $contextValue >= $condition->value, + '<' => $contextValue < $condition->value, + '<=' => $contextValue <= $condition->value, + default => false, + }; + } + + /** + * @param EvaluationContext $context + * @param string $property + * @return mixed|array|null + */ + private static function _getContextValue($context, $property) + { + if (str_starts_with($property, '$.')) { + try { + $results = new JSONPath($context)->find($property)->getData(); + } catch (JSONPathException) { + // The unlikely case when a trait starts with "$." but isn't JSONPath + $escapedProperty = addslashes($property); + $path = "$.identity.traits['{$escapedProperty}']"; + $results = new JSONPath()->find($path)->getData(); + } + + return match (count($results)) { + 0 => null, + 1 => $results[0], + default => $results, + }; + } + + if ($context->identity !== null) { + return $context->identity->traits->{$property} ?? null; + } + + return null; } /** diff --git a/src/Engine/Utils/Types/Context/EvaluationContext.php b/src/Engine/Utils/Types/Context/EvaluationContext.php index 36bcd2d..e576ddc 100644 --- a/src/Engine/Utils/Types/Context/EvaluationContext.php +++ b/src/Engine/Utils/Types/Context/EvaluationContext.php @@ -22,85 +22,101 @@ class EvaluationContext */ public static function fromJsonObject($jsonContext) { - $context = new EvaluationContext; + $context = new EvaluationContext(); - $context->environment = new EnvironmentContext; + $context->environment = new EnvironmentContext(); $context->environment->key = $jsonContext->environment->key; $context->environment->name = $jsonContext->environment->name; - $context->identity = new IdentityContext; + $context->identity = new IdentityContext(); $context->identity->key = $jsonContext->identity->key; $context->identity->identifier = $jsonContext->identity->identifier; $context->identity->traits = $jsonContext->identity->traits; $context->segments = []; foreach ($jsonContext->segments as $jsonSegment) { - $segment = new SegmentContext; + $segment = new SegmentContext(); $segment->key = $jsonSegment->key; $segment->name = $jsonSegment->name; - $segment->rules = _convertRules($jsonSegment->rules ?? []); - $segment->overrides = _convertFeatures($jsonSegment->overrides ?? []); + $segment->rules = self::_convertRules($jsonSegment->rules ?? []); + $segment->overrides = self::_convertFeatures( + $jsonSegment->overrides ?? [], + associative: false, + ); $context->segments[$segment->key] = $segment; } - $context->features = _convertFeatures($jsonContext->features ?? []); + $context->features = self::_convertFeatures( + $jsonContext->features ?? [], + associative: true, + ); return $context; } -} -/** - * @param array $jsonRules - * @return array - */ -function _convertRules($jsonRules) -{ - $rules = []; - foreach ($jsonRules as $jsonRule) { - $rule = new SegmentRule; - $rule->type = RuleType::from($jsonRule->type); - - $rule->conditions = []; - foreach (($jsonRule->conditions ?? []) as $jsonCondition) { - $condition = new SegmentCondition; - $condition->property = $jsonCondition->property; - $condition->operator = SegmentConditionOperator::from($jsonCondition->operator); - $condition->value = $jsonCondition->value; - $rule->conditions[] = $condition; + /** + * @param array $jsonRules + * @return array + */ + private static function _convertRules($jsonRules) + { + $rules = []; + foreach ($jsonRules as $jsonRule) { + $rule = new SegmentRule(); + $rule->type = SegmentRuleType::from($jsonRule->type); + + $rule->conditions = []; + foreach ($jsonRule->conditions ?? [] as $jsonCondition) { + $condition = new SegmentCondition(); + $condition->property = $jsonCondition->property; + $condition->operator = SegmentConditionOperator::from( + $jsonCondition->operator, + ); + $condition->value = $jsonCondition->value; + $rule->conditions[] = $condition; + } + + $rule->rules = $jsonRule->rules + ? self::_convertRules($jsonRule->rules) + : []; + + $rules[] = $rule; } - $rule->rules = empty($jsonRule->rules) ? [] : _convertRules($jsonRule->rules); - - $rules[] = $rule; + return $rules; } - return $rules; -} - -/** - * @param array $jsonFeatures - * @return array - */ -function _convertFeatures($jsonFeatures) -{ - $features = []; - foreach ($jsonFeatures as $jsonFeature) { - $feature = new FeatureContext; - $feature->key = $jsonFeature->key; - $feature->feature_key = $jsonFeature->feature_key; - $feature->name = $jsonFeature->name; - $feature->enabled = $jsonFeature->enabled; - $feature->value = $jsonFeature->value; - $feature->priority = $jsonFeature->priority; - $feature->variants = []; - foreach (($jsonFeature->variants ?? []) as $jsonVariant) { - $variant = new FeatureValue; - $variant->value = $jsonVariant->value; - $variant->weight = $jsonVariant->weight; - $feature->variants[] = $variant; + /** + * @param array $jsonFeatures + * @param bool $associative + * @return array + */ + private static function _convertFeatures($jsonFeatures, $associative): array + { + $features = []; + foreach ($jsonFeatures as $jsonFeature) { + $feature = new FeatureContext(); + $feature->key = $jsonFeature->key; + $feature->feature_key = $jsonFeature->feature_key; + $feature->name = $jsonFeature->name; + $feature->enabled = $jsonFeature->enabled; + $feature->value = $jsonFeature->value; + $feature->priority = $jsonFeature->priority ?? null; + $feature->variants = []; + foreach ($jsonFeature->variants ?? [] as $jsonVariant) { + $variant = new FeatureValue(); + $variant->value = $jsonVariant->value; + $variant->weight = $jsonVariant->weight; + $feature->variants[] = $variant; + } + + if ($associative) { + $features[$jsonFeature->name] = $feature; + } else { + $features[] = $feature; + } } - $features[] = $feature; - } - return $features; + return $features; + } } diff --git a/src/Engine/Utils/Types/Context/FeatureContext.php b/src/Engine/Utils/Types/Context/FeatureContext.php index 8d12bb3..51ef4d1 100644 --- a/src/Engine/Utils/Types/Context/FeatureContext.php +++ b/src/Engine/Utils/Types/Context/FeatureContext.php @@ -1,8 +1,10 @@ */ public $variants; + + /** @return array */ + public function jsonSerialize(): array + { + $json = [ + 'key' => $this->key, + 'feature_key' => $this->feature_key, + 'name' => $this->name, + 'enabled' => $this->enabled, + 'value' => $this->value, + ]; + + if ($this->priority !== null) { + $json['priority'] = $this->priority; + } + + if ($this->variants) { + $json['variants'] = $this->variants; + } + + return $json; + } } diff --git a/src/Engine/Utils/Types/Context/RuleType.php b/src/Engine/Utils/Types/Context/SegmentRuleType.php similarity index 81% rename from src/Engine/Utils/Types/Context/RuleType.php rename to src/Engine/Utils/Types/Context/SegmentRuleType.php index 47d9e10..a629c4b 100644 --- a/src/Engine/Utils/Types/Context/RuleType.php +++ b/src/Engine/Utils/Types/Context/SegmentRuleType.php @@ -1,7 +1,7 @@ key = $key; - $this->name = $name; - } } diff --git a/tests/Engine/EngineTests/EngineDataTest.php b/tests/Engine/EngineTests/EngineDataTest.php index 23a4a97..884a513 100644 --- a/tests/Engine/EngineTests/EngineDataTest.php +++ b/tests/Engine/EngineTests/EngineDataTest.php @@ -12,14 +12,16 @@ class EngineDataTest extends TestCase /** @return array> */ public function extractTestCases() { - $fileContents = file_get_contents(__DIR__ . '/EngineTestData/data/environment_n9fbf9h3v4fFgH3U3ngWhb.json'); - - $contents = json_decode($fileContents); + $testDataContent = file_get_contents( + __DIR__ . + '/EngineTestData/data/environment_n9fbf9h3v4fFgH3U3ngWhb.json', + ); + $testData = json_decode($testDataContent, associative: false); $parameters = []; - foreach ($contents->test_cases as $testCase) { + foreach ($testData->test_cases as $testCase) { $context = EvaluationContext::fromJsonObject($testCase->context); - $parameters[] = [$context, $testCase->response]; + $parameters[] = [$context, $testCase->result]; } return $parameters; @@ -28,15 +30,26 @@ public function extractTestCases() /** * @dataProvider extractTestCases * @param EvaluationContext $evaluationContext - * @param object $expectedResult + * @param object $expectedEvaluationResult * @return void */ - public function testEngine($evaluationContext, $expectedResult): void - { + public function testEngine( + $evaluationContext, + $expectedEvaluationResult, + ): void { // When $evaluationResult = Engine::getEvaluationResult($evaluationContext); + // Hack to allow comparing flags as associative arrays (: ) + $wanted = array_column($expectedEvaluationResult->flags, null, 'name'); + $expectedEvaluationResult->flags = $wanted; + $actual = array_column($evaluationResult->flags, null, 'name'); + $evaluationResult->flags = $actual; + // Then - $this->assertEquals($expectedResult, $evaluationResult); + $this->assertEquals( + json_decode(json_encode($expectedEvaluationResult), true), + json_decode(json_encode($evaluationResult), true), + ); } } diff --git a/tests/Engine/EngineTests/EngineTestData b/tests/Engine/EngineTests/EngineTestData index 5e7c413..18c68ef 160000 --- a/tests/Engine/EngineTests/EngineTestData +++ b/tests/Engine/EngineTests/EngineTestData @@ -1 +1 @@ -Subproject commit 5e7c4139c59e529301f7dc8f784e991f1c8840fb +Subproject commit 18c68ef925910622a228af2892aed48b21e532fe From 7295ca8aa797fdb06d36cf248f6d13d4ae344bea Mon Sep 17 00:00:00 2001 From: Evandro Myller Date: Wed, 24 Sep 2025 20:12:21 -0300 Subject: [PATCH 05/27] =?UTF-8?q?Lint=20=E2=9C=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Engine/Engine.php | 12 ++++++++---- .../Utils/Types/Context/EnvironmentContext.php | 1 + src/Engine/Utils/Types/Context/EvaluationContext.php | 1 + src/Engine/Utils/Types/Context/FeatureContext.php | 1 + src/Engine/Utils/Types/Context/FeatureValue.php | 1 + src/Engine/Utils/Types/Context/IdentityContext.php | 1 + src/Engine/Utils/Types/Context/SegmentCondition.php | 1 + .../Utils/Types/Context/SegmentConditionOperator.php | 1 + src/Engine/Utils/Types/Context/SegmentContext.php | 1 + src/Engine/Utils/Types/Context/SegmentRule.php | 1 + src/Engine/Utils/Types/Context/SegmentRuleType.php | 1 + src/Engine/Utils/Types/Result/EvaluationResult.php | 1 + src/Engine/Utils/Types/Result/FlagResult.php | 1 + src/Engine/Utils/Types/Result/SegmentResult.php | 1 + tests/Engine/EngineTests/EngineDataTest.php | 1 + 15 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/Engine/Engine.php b/src/Engine/Engine.php index 7f96188..028114c 100644 --- a/src/Engine/Engine.php +++ b/src/Engine/Engine.php @@ -127,7 +127,8 @@ private static function isContextInSegment($context, $segment): bool private static function getFlagResultFromFeatureContext($feature, $splitKey) { if ($splitKey !== null && !empty($feature->variants)) { - $percentageValue = new Hashing()->getHashedPercentageForObjectIds([ + $hashing = new Hashing(); + $percentageValue = $hashing->getHashedPercentageForObjectIds([ $feature->key, $splitKey, ]); @@ -274,7 +275,8 @@ private static function _contextMatchesCondition( return false; } - $threshold = new Hashing()->getHashedPercentageForObjectIds( + $hashing = new Hashing(); + $threshold = $hashing->getHashedPercentageForObjectIds( $objectIds, ); return $threshold <= floatval($condition->value); @@ -349,12 +351,14 @@ private static function _getContextValue($context, $property) { if (str_starts_with($property, '$.')) { try { - $results = new JSONPath($context)->find($property)->getData(); + $json = new JSONPath($context); + $results = $json->find($property)->getData(); } catch (JSONPathException) { // The unlikely case when a trait starts with "$." but isn't JSONPath $escapedProperty = addslashes($property); $path = "$.identity.traits['{$escapedProperty}']"; - $results = new JSONPath()->find($path)->getData(); + $json = new JSONPath($context); + $results = $json->find($path)->getData(); } return match (count($results)) { diff --git a/src/Engine/Utils/Types/Context/EnvironmentContext.php b/src/Engine/Utils/Types/Context/EnvironmentContext.php index b51bdfc..4d3a7aa 100644 --- a/src/Engine/Utils/Types/Context/EnvironmentContext.php +++ b/src/Engine/Utils/Types/Context/EnvironmentContext.php @@ -1,4 +1,5 @@ Date: Fri, 26 Sep 2025 16:39:12 -0300 Subject: [PATCH 06/27] Make priority clearer --- src/Engine/Engine.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Engine/Engine.php b/src/Engine/Engine.php index 028114c..ded7f2c 100644 --- a/src/Engine/Engine.php +++ b/src/Engine/Engine.php @@ -24,6 +24,9 @@ class Engine { + private const STRONGEST_PRIORITY = -INF; + private const WEAKEST_PRIORITY = +INF; + /** * Get the evaluation result for a given context. * @@ -63,8 +66,8 @@ public static function getEvaluationResult($context): EvaluationResult $evaluatedFeature = $evaluatedFeatures[$featureKey] ?? null; if ($evaluatedFeature) { $overrideWinsPriority = - ($overrideFeature->priority ?? INF) < - ($evaluatedFeature->priority ?? INF); + ($overrideFeature->priority ?? self::WEAKEST_PRIORITY) < + ($evaluatedFeature->priority ?? self::WEAKEST_PRIORITY); if (!$overrideWinsPriority) { continue; } From 7cf046caafe104804c2c219a2dac3ae15438651e Mon Sep 17 00:00:00 2001 From: Evandro Myller Date: Wed, 1 Oct 2025 11:24:30 -0300 Subject: [PATCH 07/27] Update EvaluationResult spec Contributes to https://github.com/Flagsmith/flagsmith/issues/6121 --- src/Engine/Engine.php | 8 ++++---- src/Engine/Utils/Types/Result/EvaluationResult.php | 5 ----- tests/Engine/EngineTests/EngineDataTest.php | 6 ------ tests/Engine/EngineTests/EngineTestData | 2 +- 4 files changed, 5 insertions(+), 16 deletions(-) diff --git a/src/Engine/Engine.php b/src/Engine/Engine.php index ded7f2c..70fec30 100644 --- a/src/Engine/Engine.php +++ b/src/Engine/Engine.php @@ -44,7 +44,7 @@ public static function getEvaluationResult($context): EvaluationResult /** @var array */ $matchedSegmentsByFeatureKey = []; - /** @var array */ + /** @var array */ $evaluatedFlags = []; foreach ($context->segments as $segment) { @@ -80,23 +80,23 @@ public static function getEvaluationResult($context): EvaluationResult foreach ($context->features as $feature) { $featureKey = $feature->feature_key; + $featureName = $feature->name; $evaluatedFeature = $evaluatedFeatures[$featureKey] ?? null; if ($evaluatedFeature) { - $evaluatedFlags[] = self::getFlagResultFromSegmentContext( + $evaluatedFlags[$featureName] = self::getFlagResultFromSegmentContext( $evaluatedFeature, $matchedSegmentsByFeatureKey[$featureKey], ); continue; } - $evaluatedFlags[] = self::getFlagResultFromFeatureContext( + $evaluatedFlags[$featureName] = self::getFlagResultFromFeatureContext( $feature, $context->identity?->key, ); } $result = new EvaluationResult(); - $result->context = $context; $result->flags = $evaluatedFlags; $result->segments = $evaluatedSegments; return $result; diff --git a/src/Engine/Utils/Types/Result/EvaluationResult.php b/src/Engine/Utils/Types/Result/EvaluationResult.php index 8eddd13..8883fe7 100644 --- a/src/Engine/Utils/Types/Result/EvaluationResult.php +++ b/src/Engine/Utils/Types/Result/EvaluationResult.php @@ -2,14 +2,9 @@ namespace Flagsmith\Engine\Utils\Types\Result; -use Flagsmith\Engine\Utils\Types\Context\EvaluationContext; - // TODO: Port this to https://wiki.php.net/rfc/dataclass class EvaluationResult { - /** @var EvaluationContext */ - public $context; - /** @var array */ public array $flags; diff --git a/tests/Engine/EngineTests/EngineDataTest.php b/tests/Engine/EngineTests/EngineDataTest.php index dd4eb7a..c3729ef 100644 --- a/tests/Engine/EngineTests/EngineDataTest.php +++ b/tests/Engine/EngineTests/EngineDataTest.php @@ -41,12 +41,6 @@ public function testEngine( // When $evaluationResult = Engine::getEvaluationResult($evaluationContext); - // Hack to allow comparing flags as associative arrays (: ) - $wanted = array_column($expectedEvaluationResult->flags, null, 'name'); - $expectedEvaluationResult->flags = $wanted; - $actual = array_column($evaluationResult->flags, null, 'name'); - $evaluationResult->flags = $actual; - // Then $this->assertEquals( json_decode(json_encode($expectedEvaluationResult), true), diff --git a/tests/Engine/EngineTests/EngineTestData b/tests/Engine/EngineTests/EngineTestData index 18c68ef..c9343de 160000 --- a/tests/Engine/EngineTests/EngineTestData +++ b/tests/Engine/EngineTests/EngineTestData @@ -1 +1 @@ -Subproject commit 18c68ef925910622a228af2892aed48b21e532fe +Subproject commit c9343de089da92f2ccb1348ab3e36e1697bc20df From e2817c54ab3aae248cb74f94bd7552c9abd27997 Mon Sep 17 00:00:00 2001 From: Evandro Myller Date: Wed, 1 Oct 2025 13:40:09 -0300 Subject: [PATCH 08/27] yay --- .../Utils/Types/Context/FeatureContext.php | 26 +------------------ 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/src/Engine/Utils/Types/Context/FeatureContext.php b/src/Engine/Utils/Types/Context/FeatureContext.php index 78824a5..bedf4d8 100644 --- a/src/Engine/Utils/Types/Context/FeatureContext.php +++ b/src/Engine/Utils/Types/Context/FeatureContext.php @@ -2,10 +2,8 @@ namespace Flagsmith\Engine\Utils\Types\Context; -use JsonSerializable; - // TODO: Port this to https://wiki.php.net/rfc/dataclass -class FeatureContext implements JsonSerializable +class FeatureContext { /** @var string */ public $key; @@ -27,26 +25,4 @@ class FeatureContext implements JsonSerializable /** @var array */ public $variants; - - /** @return array */ - public function jsonSerialize(): array - { - $json = [ - 'key' => $this->key, - 'feature_key' => $this->feature_key, - 'name' => $this->name, - 'enabled' => $this->enabled, - 'value' => $this->value, - ]; - - if ($this->priority !== null) { - $json['priority'] = $this->priority; - } - - if ($this->variants) { - $json['variants'] = $this->variants; - } - - return $json; - } } From cc6ae4cfaefcc3d806a9c8acf65d00df41ee7451 Mon Sep 17 00:00:00 2001 From: Evandro Myller Date: Wed, 1 Oct 2025 22:36:53 -0300 Subject: [PATCH 09/27] Fix identity traits data type --- src/Engine/Engine.php | 2 +- src/Engine/Utils/Types/Context/EvaluationContext.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Engine/Engine.php b/src/Engine/Engine.php index 70fec30..05c50b7 100644 --- a/src/Engine/Engine.php +++ b/src/Engine/Engine.php @@ -372,7 +372,7 @@ private static function _getContextValue($context, $property) } if ($context->identity !== null) { - return $context->identity->traits->{$property} ?? null; + return $context->identity->traits[$property] ?? null; } return null; diff --git a/src/Engine/Utils/Types/Context/EvaluationContext.php b/src/Engine/Utils/Types/Context/EvaluationContext.php index 866d566..26952bc 100644 --- a/src/Engine/Utils/Types/Context/EvaluationContext.php +++ b/src/Engine/Utils/Types/Context/EvaluationContext.php @@ -32,7 +32,7 @@ public static function fromJsonObject($jsonContext) $context->identity = new IdentityContext(); $context->identity->key = $jsonContext->identity->key; $context->identity->identifier = $jsonContext->identity->identifier; - $context->identity->traits = $jsonContext->identity->traits; + $context->identity->traits = (array) ($jsonContext->identity->traits ?? []); $context->segments = []; foreach ($jsonContext->segments as $jsonSegment) { From 8c57eb2060f009e965b810330cc3e89057669b79 Mon Sep 17 00:00:00 2001 From: Evandro Myller Date: Thu, 2 Oct 2025 21:38:51 -0300 Subject: [PATCH 10/27] Update engine test data branch --- .gitmodules | 2 +- tests/Engine/EngineTests/EngineTestData | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitmodules b/.gitmodules index 5417c42..ad75769 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,4 @@ [submodule "tests/Engine/EngineTests/EngineTestData"] path = tests/Engine/EngineTests/EngineTestData url = git@github.com:Flagsmith/engine-test-data.git - branch = feat/context-values-intensifies + branch = feat/context-values diff --git a/tests/Engine/EngineTests/EngineTestData b/tests/Engine/EngineTests/EngineTestData index c9343de..5139edd 160000 --- a/tests/Engine/EngineTests/EngineTestData +++ b/tests/Engine/EngineTests/EngineTestData @@ -1 +1 @@ -Subproject commit c9343de089da92f2ccb1348ab3e36e1697bc20df +Subproject commit 5139eddd050e9f2e4b18541136b9c79a45cbc3f4 From 20ec0f162b0f4a73dc056c19ae418cda6952cf92 Mon Sep 17 00:00:00 2001 From: Evandro Myller Date: Fri, 3 Oct 2025 23:01:25 -0300 Subject: [PATCH 11/27] Fix identity as an optional context --- src/Engine/Utils/Types/Context/EvaluationContext.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Engine/Utils/Types/Context/EvaluationContext.php b/src/Engine/Utils/Types/Context/EvaluationContext.php index 26952bc..302f22a 100644 --- a/src/Engine/Utils/Types/Context/EvaluationContext.php +++ b/src/Engine/Utils/Types/Context/EvaluationContext.php @@ -29,10 +29,12 @@ public static function fromJsonObject($jsonContext) $context->environment->key = $jsonContext->environment->key; $context->environment->name = $jsonContext->environment->name; - $context->identity = new IdentityContext(); - $context->identity->key = $jsonContext->identity->key; - $context->identity->identifier = $jsonContext->identity->identifier; - $context->identity->traits = (array) ($jsonContext->identity->traits ?? []); + if (!empty($jsonContext->identity)) { + $context->identity = new IdentityContext(); + $context->identity->key = $jsonContext->identity->key; + $context->identity->identifier = $jsonContext->identity->identifier; + $context->identity->traits = (array) ($jsonContext->identity->traits ?? []); + } $context->segments = []; foreach ($jsonContext->segments as $jsonSegment) { From 8d0419e6ea5edc1661f7a4abdc66c8ca4c08245f Mon Sep 17 00:00:00 2001 From: Evandro Myller Date: Fri, 3 Oct 2025 23:02:01 -0300 Subject: [PATCH 12/27] Improve API --- .../Utils/Types/Context/EvaluationContext.php | 21 +++++-------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/src/Engine/Utils/Types/Context/EvaluationContext.php b/src/Engine/Utils/Types/Context/EvaluationContext.php index 302f22a..391ded4 100644 --- a/src/Engine/Utils/Types/Context/EvaluationContext.php +++ b/src/Engine/Utils/Types/Context/EvaluationContext.php @@ -42,17 +42,11 @@ public static function fromJsonObject($jsonContext) $segment->key = $jsonSegment->key; $segment->name = $jsonSegment->name; $segment->rules = self::_convertRules($jsonSegment->rules ?? []); - $segment->overrides = self::_convertFeatures( - $jsonSegment->overrides ?? [], - associative: false, - ); + $segment->overrides = array_values(self::_convertFeatures($jsonSegment->overrides ?? [])); $context->segments[$segment->key] = $segment; } - $context->features = self::_convertFeatures( - $jsonContext->features ?? [], - associative: true, - ); + $context->features = self::_convertFeatures($jsonContext->features ?? []); return $context; } @@ -91,10 +85,9 @@ private static function _convertRules($jsonRules) /** * @param array $jsonFeatures - * @param bool $associative - * @return array + * @return array */ - private static function _convertFeatures($jsonFeatures, $associative): array + private static function _convertFeatures($jsonFeatures): array { $features = []; foreach ($jsonFeatures as $jsonFeature) { @@ -113,11 +106,7 @@ private static function _convertFeatures($jsonFeatures, $associative): array $feature->variants[] = $variant; } - if ($associative) { - $features[$jsonFeature->name] = $feature; - } else { - $features[] = $feature; - } + $features[$jsonFeature->name] = $feature; } return $features; From d719f09d8eb94dfed0356e74a00dd4542786ca9b Mon Sep 17 00:00:00 2001 From: Evandro Myller Date: Tue, 7 Oct 2025 23:05:00 -0300 Subject: [PATCH 13/27] Make code reusable --- src/Engine/Engine.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Engine/Engine.php b/src/Engine/Engine.php index 05c50b7..1ffdec9 100644 --- a/src/Engine/Engine.php +++ b/src/Engine/Engine.php @@ -24,8 +24,8 @@ class Engine { - private const STRONGEST_PRIORITY = -INF; - private const WEAKEST_PRIORITY = +INF; + public const STRONGEST_PRIORITY = -INF; + public const WEAKEST_PRIORITY = +INF; /** * Get the evaluation result for a given context. From afa90114bd4f0282327c6e396bb4e16e22b1f990 Mon Sep 17 00:00:00 2001 From: Evandro Myller Date: Tue, 7 Oct 2025 23:05:18 -0300 Subject: [PATCH 14/27] Apples will never be bananas --- src/Engine/Engine.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Engine/Engine.php b/src/Engine/Engine.php index 1ffdec9..1ebc75e 100644 --- a/src/Engine/Engine.php +++ b/src/Engine/Engine.php @@ -253,7 +253,7 @@ private static function _contextMatchesCondition( switch ($condition->operator) { case SegmentConditionOperator::IN: /** @var array $inValues */ - if (is_array($contextValue)) { + if (is_array($condition->value)) { $inValues = $condition->value; } else { $inValues = json_decode($condition->value, true); From 754d83ae8f8ca2048eeed2e909fc4655e440a42a Mon Sep 17 00:00:00 2001 From: Evandro Myller Date: Fri, 10 Oct 2025 14:07:56 -0300 Subject: [PATCH 15/27] Track TODO with an issue https://github.com/Flagsmith/flagsmith-php-client/issues/110 --- src/Engine/Utils/Types/Context/EnvironmentContext.php | 1 - src/Engine/Utils/Types/Context/EvaluationContext.php | 1 - src/Engine/Utils/Types/Context/FeatureContext.php | 1 - src/Engine/Utils/Types/Context/FeatureValue.php | 1 - src/Engine/Utils/Types/Context/IdentityContext.php | 1 - src/Engine/Utils/Types/Context/SegmentCondition.php | 1 - src/Engine/Utils/Types/Context/SegmentContext.php | 1 - src/Engine/Utils/Types/Context/SegmentRule.php | 1 - src/Engine/Utils/Types/Result/EvaluationResult.php | 1 - src/Engine/Utils/Types/Result/FlagResult.php | 1 - src/Engine/Utils/Types/Result/SegmentResult.php | 1 - 11 files changed, 11 deletions(-) diff --git a/src/Engine/Utils/Types/Context/EnvironmentContext.php b/src/Engine/Utils/Types/Context/EnvironmentContext.php index 4d3a7aa..20330b0 100644 --- a/src/Engine/Utils/Types/Context/EnvironmentContext.php +++ b/src/Engine/Utils/Types/Context/EnvironmentContext.php @@ -2,7 +2,6 @@ namespace Flagsmith\Engine\Utils\Types\Context; -// TODO: Port this to https://wiki.php.net/rfc/dataclass class EnvironmentContext { /** @var string */ diff --git a/src/Engine/Utils/Types/Context/EvaluationContext.php b/src/Engine/Utils/Types/Context/EvaluationContext.php index 391ded4..b9c0fae 100644 --- a/src/Engine/Utils/Types/Context/EvaluationContext.php +++ b/src/Engine/Utils/Types/Context/EvaluationContext.php @@ -2,7 +2,6 @@ namespace Flagsmith\Engine\Utils\Types\Context; -// TODO: Port this to https://wiki.php.net/rfc/dataclass class EvaluationContext { /** @var EnvironmentContext */ diff --git a/src/Engine/Utils/Types/Context/FeatureContext.php b/src/Engine/Utils/Types/Context/FeatureContext.php index bedf4d8..a7213d8 100644 --- a/src/Engine/Utils/Types/Context/FeatureContext.php +++ b/src/Engine/Utils/Types/Context/FeatureContext.php @@ -2,7 +2,6 @@ namespace Flagsmith\Engine\Utils\Types\Context; -// TODO: Port this to https://wiki.php.net/rfc/dataclass class FeatureContext { /** @var string */ diff --git a/src/Engine/Utils/Types/Context/FeatureValue.php b/src/Engine/Utils/Types/Context/FeatureValue.php index be7f924..9e296d4 100644 --- a/src/Engine/Utils/Types/Context/FeatureValue.php +++ b/src/Engine/Utils/Types/Context/FeatureValue.php @@ -2,7 +2,6 @@ namespace Flagsmith\Engine\Utils\Types\Context; -// TODO: Port this to https://wiki.php.net/rfc/dataclass class FeatureValue { /** @var mixed */ diff --git a/src/Engine/Utils/Types/Context/IdentityContext.php b/src/Engine/Utils/Types/Context/IdentityContext.php index c17803f..d69ffcf 100644 --- a/src/Engine/Utils/Types/Context/IdentityContext.php +++ b/src/Engine/Utils/Types/Context/IdentityContext.php @@ -2,7 +2,6 @@ namespace Flagsmith\Engine\Utils\Types\Context; -// TODO: Port this to https://wiki.php.net/rfc/dataclass class IdentityContext { /** @var string */ diff --git a/src/Engine/Utils/Types/Context/SegmentCondition.php b/src/Engine/Utils/Types/Context/SegmentCondition.php index 74ec993..be81f96 100644 --- a/src/Engine/Utils/Types/Context/SegmentCondition.php +++ b/src/Engine/Utils/Types/Context/SegmentCondition.php @@ -2,7 +2,6 @@ namespace Flagsmith\Engine\Utils\Types\Context; -// TODO: Port this to https://wiki.php.net/rfc/dataclass class SegmentCondition { /** @var string */ diff --git a/src/Engine/Utils/Types/Context/SegmentContext.php b/src/Engine/Utils/Types/Context/SegmentContext.php index 27a001e..fa244d0 100644 --- a/src/Engine/Utils/Types/Context/SegmentContext.php +++ b/src/Engine/Utils/Types/Context/SegmentContext.php @@ -2,7 +2,6 @@ namespace Flagsmith\Engine\Utils\Types\Context; -// TODO: Port this to https://wiki.php.net/rfc/dataclass class SegmentContext { /** @var string */ diff --git a/src/Engine/Utils/Types/Context/SegmentRule.php b/src/Engine/Utils/Types/Context/SegmentRule.php index 43b4a31..b26998f 100644 --- a/src/Engine/Utils/Types/Context/SegmentRule.php +++ b/src/Engine/Utils/Types/Context/SegmentRule.php @@ -2,7 +2,6 @@ namespace Flagsmith\Engine\Utils\Types\Context; -// TODO: Port this to https://wiki.php.net/rfc/dataclass class SegmentRule { /** @var RuleType */ diff --git a/src/Engine/Utils/Types/Result/EvaluationResult.php b/src/Engine/Utils/Types/Result/EvaluationResult.php index 8883fe7..0fb3fe7 100644 --- a/src/Engine/Utils/Types/Result/EvaluationResult.php +++ b/src/Engine/Utils/Types/Result/EvaluationResult.php @@ -2,7 +2,6 @@ namespace Flagsmith\Engine\Utils\Types\Result; -// TODO: Port this to https://wiki.php.net/rfc/dataclass class EvaluationResult { /** @var array */ diff --git a/src/Engine/Utils/Types/Result/FlagResult.php b/src/Engine/Utils/Types/Result/FlagResult.php index 58dfabb..e9f1408 100644 --- a/src/Engine/Utils/Types/Result/FlagResult.php +++ b/src/Engine/Utils/Types/Result/FlagResult.php @@ -2,7 +2,6 @@ namespace Flagsmith\Engine\Utils\Types\Result; -// TODO: Port this to https://wiki.php.net/rfc/dataclass class FlagResult { /** @var string */ diff --git a/src/Engine/Utils/Types/Result/SegmentResult.php b/src/Engine/Utils/Types/Result/SegmentResult.php index fbfe57e..666720c 100644 --- a/src/Engine/Utils/Types/Result/SegmentResult.php +++ b/src/Engine/Utils/Types/Result/SegmentResult.php @@ -2,7 +2,6 @@ namespace Flagsmith\Engine\Utils\Types\Result; -// TODO: Port this to https://wiki.php.net/rfc/dataclass class SegmentResult { /** @var string */ From 274ba162a7b494fe9ce286986689b71a29fc5036 Mon Sep 17 00:00:00 2001 From: Evandro Myller Date: Fri, 10 Oct 2025 20:00:36 -0300 Subject: [PATCH 16/27] Improve type handling in the IN operator --- src/Engine/Engine.php | 17 +++++++++++++---- src/Engine/Utils/StringValue.php | 21 +++++++++++++++++++++ 2 files changed, 34 insertions(+), 4 deletions(-) create mode 100644 src/Engine/Utils/StringValue.php diff --git a/src/Engine/Engine.php b/src/Engine/Engine.php index 1ebc75e..5972d3c 100644 --- a/src/Engine/Engine.php +++ b/src/Engine/Engine.php @@ -9,6 +9,7 @@ use Flagsmith\Engine\Utils\Exceptions\FeatureStateNotFound; use Flagsmith\Engine\Utils\Hashing; use Flagsmith\Engine\Utils\Semver; +use Flagsmith\Engine\Utils\StringValue; use Flagsmith\Engine\Utils\Types\Context\EvaluationContext; use Flagsmith\Engine\Utils\Types\Context\FeatureContext; use Flagsmith\Engine\Utils\Types\Context\SegmentRuleType; @@ -252,16 +253,24 @@ private static function _contextMatchesCondition( switch ($condition->operator) { case SegmentConditionOperator::IN: - /** @var array $inValues */ if (is_array($condition->value)) { $inValues = $condition->value; } else { - $inValues = json_decode($condition->value, true); - $jsonDecodingFailed = $inValues === null; - if ($jsonDecodingFailed || !is_array($inValues)) { + try { + $inValues = json_decode( + $condition->value, + associative: false, // Possibly catch objects + flags: \JSON_THROW_ON_ERROR, + ); + if (!is_array($inValues)) { + throw new \ValueError('Invalid JSON array'); + } + } catch (\JsonException | \ValueError) { $inValues = explode(',', $condition->value); } } + $inValues = array_map(fn ($value) => StringValue::from($value), $inValues); + $contextValue = StringValue::from($contextValue); return in_array($contextValue, $inValues, strict: true); case SegmentConditionOperator::PERCENTAGE_SPLIT: diff --git a/src/Engine/Utils/StringValue.php b/src/Engine/Utils/StringValue.php new file mode 100644 index 0000000..7d1f44b --- /dev/null +++ b/src/Engine/Utils/StringValue.php @@ -0,0 +1,21 @@ + Date: Fri, 10 Oct 2025 20:08:02 -0300 Subject: [PATCH 17/27] Make MODULO check less prone to error --- src/Engine/Engine.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Engine/Engine.php b/src/Engine/Engine.php index 5972d3c..5e68429 100644 --- a/src/Engine/Engine.php +++ b/src/Engine/Engine.php @@ -298,7 +298,12 @@ private static function _contextMatchesCondition( return false; } - [$divisor, $remainder] = explode('|', $condition->value); + $parts = explode('|', (string) $condition->value); + if (count($parts) !== 2) { + return false; + } + + [$divisor, $remainder] = $parts; if (!is_numeric($divisor) || !is_numeric($remainder)) { return false; } From 3c0ad5b62bd1a2341abca812cf1d34e86d19628f Mon Sep 17 00:00:00 2001 From: Evandro Myller Date: Fri, 10 Oct 2025 20:30:32 -0300 Subject: [PATCH 18/27] Fix obtaining a context value --- src/Engine/Engine.php | 40 +++++++++++++++++++--------------------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/src/Engine/Engine.php b/src/Engine/Engine.php index 5e68429..9d313d0 100644 --- a/src/Engine/Engine.php +++ b/src/Engine/Engine.php @@ -291,7 +291,7 @@ private static function _contextMatchesCondition( $threshold = $hashing->getHashedPercentageForObjectIds( $objectIds, ); - return $threshold <= floatval($condition->value); + return $threshold <= ((float) $condition->value); case SegmentConditionOperator::MODULO: if (!is_numeric($contextValue)) { @@ -308,7 +308,7 @@ private static function _contextMatchesCondition( return false; } - return floatval($contextValue) % $divisor === $remainder; + return ((float) $contextValue) % ((float) $divisor) === ((float) $remainder); case SegmentConditionOperator::IS_NOT_SET: return $contextValue === null; @@ -323,9 +323,7 @@ private static function _contextMatchesCondition( return !str_contains($contextValue, $condition->value); case SegmentConditionOperator::REGEX: - return boolval( - preg_match("/{$condition->value}/", $contextValue), - ); + return (bool) preg_match("/{$condition->value}/", $contextValue); } if ($contextValue === null) { @@ -360,33 +358,33 @@ private static function _contextMatchesCondition( } /** + * Return a trait value by name, or a context value by JSONPath, or null * @param EvaluationContext $context * @param string $property - * @return mixed|array|null + * @return ?mixed */ private static function _getContextValue($context, $property) { + if ($context->identity !== null) { + $hasTrait = array_key_exists($property, $context->identity->traits); + if ($hasTrait) { + return $context->identity->traits[$property]; + } + } + if (str_starts_with($property, '$.')) { try { - $json = new JSONPath($context); - $results = $json->find($property)->getData(); + $jsonpath = new JSONPath($context); + $results = $jsonpath->find($property)->getData(); } catch (JSONPathException) { - // The unlikely case when a trait starts with "$." but isn't JSONPath - $escapedProperty = addslashes($property); - $path = "$.identity.traits['{$escapedProperty}']"; - $json = new JSONPath($context); - $results = $json->find($path)->getData(); + return null; } - return match (count($results)) { - 0 => null, - 1 => $results[0], - default => $results, - }; - } + if (empty($results)) { + return null; + } - if ($context->identity !== null) { - return $context->identity->traits[$property] ?? null; + return $results[0]; } return null; From e8619b0b4efad43ce327f2e39f093e599c6fc423 Mon Sep 17 00:00:00 2001 From: Evandro Myller Date: Fri, 10 Oct 2025 20:32:12 -0300 Subject: [PATCH 19/27] Update comment to latest reality --- src/Engine/Engine.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Engine/Engine.php b/src/Engine/Engine.php index 9d313d0..ce2d0f2 100644 --- a/src/Engine/Engine.php +++ b/src/Engine/Engine.php @@ -32,7 +32,7 @@ class Engine * Get the evaluation result for a given context. * * @param EvaluationContext $context The evaluation context. - * @return EvaluationResult EvaluationResult containing the context, flags, and segments + * @return EvaluationResult EvaluationResult containing the evaluated flags and matched segments. */ public static function getEvaluationResult($context): EvaluationResult { From f7b070c74d5cb3a50aafc9be5cbeb257324354ec Mon Sep 17 00:00:00 2001 From: Evandro Myller Date: Fri, 10 Oct 2025 20:38:29 -0300 Subject: [PATCH 20/27] Delete skippable code --- src/Engine/Engine.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Engine/Engine.php b/src/Engine/Engine.php index ce2d0f2..49c4646 100644 --- a/src/Engine/Engine.php +++ b/src/Engine/Engine.php @@ -58,10 +58,6 @@ public static function getEvaluationResult($context): EvaluationResult $segmentResult->name = $segment->name; $evaluatedSegments[] = $segmentResult; - if (empty($segment->overrides)) { - continue; - } - foreach ($segment->overrides as $overrideFeature) { $featureKey = $overrideFeature->feature_key; $evaluatedFeature = $evaluatedFeatures[$featureKey] ?? null; From 0a201c67950f4823535aadbb1467724d10885d68 Mon Sep 17 00:00:00 2001 From: Evandro Myller Date: Fri, 10 Oct 2025 22:19:27 -0300 Subject: [PATCH 21/27] Update test cases --- .gitmodules | 2 +- composer.json | 3 +- tests/Engine/EngineTests/EngineDataTest.php | 41 +++++++++------------ tests/Engine/EngineTests/EngineTestData | 2 +- 4 files changed, 22 insertions(+), 26 deletions(-) diff --git a/.gitmodules b/.gitmodules index ad75769..4b8a4c0 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,4 @@ [submodule "tests/Engine/EngineTests/EngineTestData"] path = tests/Engine/EngineTests/EngineTestData url = git@github.com:Flagsmith/engine-test-data.git - branch = feat/context-values + tag = v2.1.0 diff --git a/composer.json b/composer.json index dcc7222..73b986d 100644 --- a/composer.json +++ b/composer.json @@ -26,7 +26,8 @@ "phpunit/phpunit": "^9.5", "symfony/cache": "^5.4.6", "friendsofphp/php-cs-fixer": "^3.6", - "doppiogancio/mocked-client": "^3.0" + "doppiogancio/mocked-client": "^3.0", + "colinodell/json5": "^3.0" }, "autoload": { "psr-4": { diff --git a/tests/Engine/EngineTests/EngineDataTest.php b/tests/Engine/EngineTests/EngineDataTest.php index c3729ef..1683329 100644 --- a/tests/Engine/EngineTests/EngineDataTest.php +++ b/tests/Engine/EngineTests/EngineDataTest.php @@ -10,41 +10,36 @@ class EngineDataTest extends TestCase { private int $attempt = 0; - /** @return array> */ - public function extractTestCases() + /** @return \Generator>> */ + public function extractTestCases(): \Generator { - $testDataContent = file_get_contents( - __DIR__ . - '/EngineTestData/data/environment_n9fbf9h3v4fFgH3U3ngWhb.json', - ); - $testData = json_decode($testDataContent, associative: false); - - $parameters = []; - foreach ($testData->test_cases as $testCase) { - $context = EvaluationContext::fromJsonObject($testCase->context); - $parameters[] = [$context, $testCase->result]; + $testCasePaths = glob(__DIR__ . '/EngineTestData/test_cases/test_*.{json,jsonc}', \GLOB_BRACE); + foreach ($testCasePaths as $testCasePath) { + $testCaseJson = file_get_contents($testCasePath); + $testCase = json5_decode($testCaseJson); + + $testName = basename($testCasePath); + yield $testName => [[ + 'context' => EvaluationContext::fromJsonObject($testCase->context), + 'result' => $testCase->result, + ]]; } - - return $parameters; } /** * @dataProvider extractTestCases - * @param EvaluationContext $evaluationContext - * @param object $expectedEvaluationResult + * @param array $case * @return void */ - public function testEngine( - $evaluationContext, - $expectedEvaluationResult, - ): void { + public function testEngine($case): void + { // When - $evaluationResult = Engine::getEvaluationResult($evaluationContext); + $result = Engine::getEvaluationResult($case['context']); // Then $this->assertEquals( - json_decode(json_encode($expectedEvaluationResult), true), - json_decode(json_encode($evaluationResult), true), + json_decode(json_encode($case['result']), associative: true), + json_decode(json_encode($result), associative: true), ); } } diff --git a/tests/Engine/EngineTests/EngineTestData b/tests/Engine/EngineTests/EngineTestData index 5139edd..37606e4 160000 --- a/tests/Engine/EngineTests/EngineTestData +++ b/tests/Engine/EngineTests/EngineTestData @@ -1 +1 @@ -Subproject commit 5139eddd050e9f2e4b18541136b9c79a45cbc3f4 +Subproject commit 37606e4437d1bd0ee6d86d79828c70a46e94fc8e From 49d6cee5591692c0d7629ba1311b91d8a94992d7 Mon Sep 17 00:00:00 2001 From: Evandro Myller Date: Fri, 10 Oct 2025 22:17:38 -0300 Subject: [PATCH 22/27] Fix tests --- src/Engine/Engine.php | 19 ++++++++++++------- .../Utils/Types/Context/EvaluationContext.php | 6 +++--- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/Engine/Engine.php b/src/Engine/Engine.php index 49c4646..fa99fcb 100644 --- a/src/Engine/Engine.php +++ b/src/Engine/Engine.php @@ -249,6 +249,9 @@ private static function _contextMatchesCondition( switch ($condition->operator) { case SegmentConditionOperator::IN: + if ($contextValue === null) { + return false; + } if (is_array($condition->value)) { $inValues = $condition->value; } else { @@ -304,7 +307,7 @@ private static function _contextMatchesCondition( return false; } - return ((float) $contextValue) % ((float) $divisor) === ((float) $remainder); + return fmod($contextValue, $divisor) === ((float) $remainder); case SegmentConditionOperator::IS_NOT_SET: return $contextValue === null; @@ -319,7 +322,7 @@ private static function _contextMatchesCondition( return !str_contains($contextValue, $condition->value); case SegmentConditionOperator::REGEX: - return (bool) preg_match("/{$condition->value}/", $contextValue); + return (bool) preg_match("/{$condition->value}/", (string) $contextValue); } if ($contextValue === null) { @@ -336,10 +339,13 @@ private static function _contextMatchesCondition( default => null, }; - if (is_string($contextValue) && Semver::isSemver($contextValue)) { - $contextValue = Semver::removeSemverSuffix($contextValue); - return $operator !== null && - version_compare($contextValue, $condition->value, $operator); + if ($operator === null) { + return false; + } + + if (Semver::isSemver($condition->value) && is_string($contextValue)) { + $actualVersion = Semver::removeSemverSuffix($condition->value); + return version_compare($contextValue, $actualVersion, $operator); } return match ($operator) { @@ -349,7 +355,6 @@ private static function _contextMatchesCondition( '>=' => $contextValue >= $condition->value, '<' => $contextValue < $condition->value, '<=' => $contextValue <= $condition->value, - default => false, }; } diff --git a/src/Engine/Utils/Types/Context/EvaluationContext.php b/src/Engine/Utils/Types/Context/EvaluationContext.php index b9c0fae..8ae01f6 100644 --- a/src/Engine/Utils/Types/Context/EvaluationContext.php +++ b/src/Engine/Utils/Types/Context/EvaluationContext.php @@ -72,9 +72,9 @@ private static function _convertRules($jsonRules) $rule->conditions[] = $condition; } - $rule->rules = $jsonRule->rules - ? self::_convertRules($jsonRule->rules) - : []; + $rule->rules = empty($jsonRule->rules) + ? [] + : self::_convertRules($jsonRule->rules); $rules[] = $rule; } From d9d977caf0dadd72eecfaecc37e8be6ce31e0c4b Mon Sep 17 00:00:00 2001 From: Evandro Myller Date: Fri, 10 Oct 2025 22:18:17 -0300 Subject: [PATCH 23/27] Carry segment metadata around --- src/Engine/Engine.php | 1 + .../Utils/Types/Context/EvaluationContext.php | 1 + .../Utils/Types/Context/SegmentContext.php | 3 +++ .../Utils/Types/Result/SegmentResult.php | 20 ++++++++++++++++++- 4 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/Engine/Engine.php b/src/Engine/Engine.php index fa99fcb..889be62 100644 --- a/src/Engine/Engine.php +++ b/src/Engine/Engine.php @@ -56,6 +56,7 @@ public static function getEvaluationResult($context): EvaluationResult $segmentResult = new SegmentResult(); $segmentResult->key = $segment->key; $segmentResult->name = $segment->name; + $segmentResult->metadata = $segment->metadata ?? null; $evaluatedSegments[] = $segmentResult; foreach ($segment->overrides as $overrideFeature) { diff --git a/src/Engine/Utils/Types/Context/EvaluationContext.php b/src/Engine/Utils/Types/Context/EvaluationContext.php index 8ae01f6..0af12fe 100644 --- a/src/Engine/Utils/Types/Context/EvaluationContext.php +++ b/src/Engine/Utils/Types/Context/EvaluationContext.php @@ -42,6 +42,7 @@ public static function fromJsonObject($jsonContext) $segment->name = $jsonSegment->name; $segment->rules = self::_convertRules($jsonSegment->rules ?? []); $segment->overrides = array_values(self::_convertFeatures($jsonSegment->overrides ?? [])); + $segment->metadata = (array) ($jsonSegment->metadata ?? []); $context->segments[$segment->key] = $segment; } diff --git a/src/Engine/Utils/Types/Context/SegmentContext.php b/src/Engine/Utils/Types/Context/SegmentContext.php index fa244d0..c1ae502 100644 --- a/src/Engine/Utils/Types/Context/SegmentContext.php +++ b/src/Engine/Utils/Types/Context/SegmentContext.php @@ -15,4 +15,7 @@ class SegmentContext /** @var array */ public $overrides; + + /** @var ?array */ + public $metadata; } diff --git a/src/Engine/Utils/Types/Result/SegmentResult.php b/src/Engine/Utils/Types/Result/SegmentResult.php index 666720c..0daaec0 100644 --- a/src/Engine/Utils/Types/Result/SegmentResult.php +++ b/src/Engine/Utils/Types/Result/SegmentResult.php @@ -2,11 +2,29 @@ namespace Flagsmith\Engine\Utils\Types\Result; -class SegmentResult +class SegmentResult implements \JsonSerializable { /** @var string */ public $key; /** @var string */ public $name; + + /** @var ?array */ + public $metadata; + + public function jsonSerialize(): array + { + $data = [ + 'key' => $this->key, + 'name' => $this->name, + ]; + + // 'metadata' is only added if there is any + if (!empty($this->metadata)) { + $data['metadata'] = $this->metadata; + } + + return $data; + } } From bfd03c3a3408b57986e4356050df6642f6132e96 Mon Sep 17 00:00:00 2001 From: Evandro Myller Date: Thu, 9 Oct 2025 02:49:30 -0300 Subject: [PATCH 24/27] Please run tests (cherry picked from commit 2913611d5d8c62bfd24129e2e057c44136c2b74c) --- .github/workflows/pull-requests.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/pull-requests.yml b/.github/workflows/pull-requests.yml index fb9d9e6..c573c04 100644 --- a/.github/workflows/pull-requests.yml +++ b/.github/workflows/pull-requests.yml @@ -2,9 +2,6 @@ name: Flagsmith PHP Pull Request on: pull_request: - branches: - - main - - release** jobs: test: From 7d0d026db9af6cceffcc35949d148c46e672f767 Mon Sep 17 00:00:00 2001 From: Evandro Myller Date: Tue, 14 Oct 2025 16:33:22 -0300 Subject: [PATCH 25/27] Comply with latest test data --- .gitmodules | 2 +- src/Engine/Engine.php | 48 ++++++++++++++++++------- src/Engine/Utils/StringValue.php | 21 ----------- tests/Engine/EngineTests/EngineTestData | 2 +- 4 files changed, 38 insertions(+), 35 deletions(-) delete mode 100644 src/Engine/Utils/StringValue.php diff --git a/.gitmodules b/.gitmodules index 4b8a4c0..c38aa2d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,4 @@ [submodule "tests/Engine/EngineTests/EngineTestData"] path = tests/Engine/EngineTests/EngineTestData url = git@github.com:Flagsmith/engine-test-data.git - tag = v2.1.0 + tag = v2.3.0 diff --git a/src/Engine/Engine.php b/src/Engine/Engine.php index 889be62..ca61140 100644 --- a/src/Engine/Engine.php +++ b/src/Engine/Engine.php @@ -9,7 +9,6 @@ use Flagsmith\Engine\Utils\Exceptions\FeatureStateNotFound; use Flagsmith\Engine\Utils\Hashing; use Flagsmith\Engine\Utils\Semver; -use Flagsmith\Engine\Utils\StringValue; use Flagsmith\Engine\Utils\Types\Context\EvaluationContext; use Flagsmith\Engine\Utils\Types\Context\FeatureContext; use Flagsmith\Engine\Utils\Types\Context\SegmentRuleType; @@ -28,6 +27,8 @@ class Engine public const STRONGEST_PRIORITY = -INF; public const WEAKEST_PRIORITY = +INF; + private const VALID_CONTEXT_VALUE_TYPES = ['string', 'integer', 'boolean', 'double']; + /** * Get the evaluation result for a given context. * @@ -247,6 +248,7 @@ private static function _contextMatchesCondition( $segmentKey, ): bool { $contextValue = self::_getContextValue($context, $condition->property); + $cast = self::_getCaster($contextValue); switch ($condition->operator) { case SegmentConditionOperator::IN: @@ -269,8 +271,7 @@ private static function _contextMatchesCondition( $inValues = explode(',', $condition->value); } } - $inValues = array_map(fn ($value) => StringValue::from($value), $inValues); - $contextValue = StringValue::from($contextValue); + $inValues = array_map($cast, $inValues); return in_array($contextValue, $inValues, strict: true); case SegmentConditionOperator::PERCENTAGE_SPLIT: @@ -317,10 +318,12 @@ private static function _contextMatchesCondition( return $contextValue !== null; case SegmentConditionOperator::CONTAINS: - return str_contains($contextValue, $condition->value); + return is_string($contextValue) && is_string($condition->value) + && str_contains($contextValue, $condition->value); case SegmentConditionOperator::NOT_CONTAINS: - return !str_contains($contextValue, $condition->value); + return is_string($contextValue) && is_string($condition->value) + && !str_contains($contextValue, $condition->value); case SegmentConditionOperator::REGEX: return (bool) preg_match("/{$condition->value}/", (string) $contextValue); @@ -350,12 +353,12 @@ private static function _contextMatchesCondition( } return match ($operator) { - '==' => $contextValue == $condition->value, - '!=' => $contextValue != $condition->value, - '>' => $contextValue > $condition->value, - '>=' => $contextValue >= $condition->value, - '<' => $contextValue < $condition->value, - '<=' => $contextValue <= $condition->value, + '==' => $contextValue === $cast($condition->value), + '!=' => $contextValue !== $cast($condition->value), + '>' => $contextValue > $cast($condition->value), + '>=' => $contextValue >= $cast($condition->value), + '<' => $contextValue < $cast($condition->value), + '<=' => $contextValue <= $cast($condition->value), }; } @@ -386,12 +389,33 @@ private static function _getContextValue($context, $property) return null; } - return $results[0]; + if (in_array(gettype($results[0]), self::VALID_CONTEXT_VALUE_TYPES)) { + return $results[0]; + }; } return null; } + /** + * Get a condition value type caster according to a context value + * @param mixed $contextValue + * @return ?callable + */ + private static function _getCaster($contextValue): ?callable + { + if (!in_array(gettype($contextValue), self::VALID_CONTEXT_VALUE_TYPES)) { + return null; + } + + return match (gettype($contextValue)) { + 'boolean' => fn ($v) => !in_array($v, ['False', 'false']), + 'string' => 'strval', + 'integer' => fn ($v) => is_numeric($v) ? (int) $v : $v, + 'double' => fn ($v) => is_numeric($v) ? (float) $v : $v, + }; + } + /** * Get the environment feature states. * @param EnvironmentModel $environment diff --git a/src/Engine/Utils/StringValue.php b/src/Engine/Utils/StringValue.php deleted file mode 100644 index 7d1f44b..0000000 --- a/src/Engine/Utils/StringValue.php +++ /dev/null @@ -1,21 +0,0 @@ - Date: Tue, 14 Oct 2025 19:12:16 -0300 Subject: [PATCH 26/27] Ensure variants are selected consistently --- src/Engine/Engine.php | 6 +++++- src/Engine/Utils/Types/Context/EvaluationContext.php | 1 + src/Engine/Utils/Types/Context/FeatureValue.php | 3 +++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Engine/Engine.php b/src/Engine/Engine.php index ca61140..ddcd389 100644 --- a/src/Engine/Engine.php +++ b/src/Engine/Engine.php @@ -135,8 +135,12 @@ private static function getFlagResultFromFeatureContext($feature, $splitKey) $splitKey, ]); + // Ensure variants are selected consistently + $variants = $feature->variants; + usort($variants, fn ($a, $b) => $a->priority <=> $b->priority); + $startPercentage = 0.0; - foreach ($feature->variants as $variant) { + foreach ($variants as $variant) { $limit = $variant->weight + $startPercentage; if ( $startPercentage <= $percentageValue && diff --git a/src/Engine/Utils/Types/Context/EvaluationContext.php b/src/Engine/Utils/Types/Context/EvaluationContext.php index 0af12fe..a9f0782 100644 --- a/src/Engine/Utils/Types/Context/EvaluationContext.php +++ b/src/Engine/Utils/Types/Context/EvaluationContext.php @@ -103,6 +103,7 @@ private static function _convertFeatures($jsonFeatures): array $variant = new FeatureValue(); $variant->value = $jsonVariant->value; $variant->weight = $jsonVariant->weight; + $variant->priority = $jsonVariant->priority; $feature->variants[] = $variant; } diff --git a/src/Engine/Utils/Types/Context/FeatureValue.php b/src/Engine/Utils/Types/Context/FeatureValue.php index 9e296d4..077068d 100644 --- a/src/Engine/Utils/Types/Context/FeatureValue.php +++ b/src/Engine/Utils/Types/Context/FeatureValue.php @@ -9,4 +9,7 @@ class FeatureValue /** @var float */ public $weight; + + /** @var int */ + public $priority; } From ef9b2421a25ea097248b4037299cbb76dfd4be98 Mon Sep 17 00:00:00 2001 From: Evandro Myller Date: Wed, 15 Oct 2025 09:10:00 -0300 Subject: [PATCH 27/27] Update tests --- .gitmodules | 2 +- tests/Engine/EngineTests/EngineTestData | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitmodules b/.gitmodules index c38aa2d..86388ee 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,4 @@ [submodule "tests/Engine/EngineTests/EngineTestData"] path = tests/Engine/EngineTests/EngineTestData url = git@github.com:Flagsmith/engine-test-data.git - tag = v2.3.0 + tag = v2.4.0 diff --git a/tests/Engine/EngineTests/EngineTestData b/tests/Engine/EngineTests/EngineTestData index 3d26dc5..6453b03 160000 --- a/tests/Engine/EngineTests/EngineTestData +++ b/tests/Engine/EngineTests/EngineTestData @@ -1 +1 @@ -Subproject commit 3d26dc53a706880e79af28903fae454657f3be50 +Subproject commit 6453b0391344a4d677a97cc4a9d27a8b8e329787