Skip to content

Commit 779e732

Browse files
committed
Merge branch 'feat/context-values' into feat/context-values-client
2 parents f5eaea7 + bfd03c3 commit 779e732

17 files changed

+119
-77
lines changed

.gitmodules

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
[submodule "tests/Engine/EngineTests/EngineTestData"]
22
path = tests/Engine/EngineTests/EngineTestData
33
url = [email protected]:Flagsmith/engine-test-data.git
4-
branch = feat/context-values
4+
tag = v2.1.0

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@
2626
"phpunit/phpunit": "^9.5",
2727
"symfony/cache": "^5.4.6",
2828
"friendsofphp/php-cs-fixer": "^3.6",
29-
"doppiogancio/mocked-client": "^3.0"
29+
"doppiogancio/mocked-client": "^3.0",
30+
"colinodell/json5": "^3.0"
3031
},
3132
"autoload": {
3233
"psr-4": {

src/Engine/Engine.php

Lines changed: 50 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use Flagsmith\Engine\Utils\Exceptions\FeatureStateNotFound;
1010
use Flagsmith\Engine\Utils\Hashing;
1111
use Flagsmith\Engine\Utils\Semver;
12+
use Flagsmith\Engine\Utils\StringValue;
1213
use Flagsmith\Engine\Utils\Types\Context\EvaluationContext;
1314
use Flagsmith\Engine\Utils\Types\Context\FeatureContext;
1415
use Flagsmith\Engine\Utils\Types\Context\SegmentRuleType;
@@ -31,7 +32,7 @@ class Engine
3132
* Get the evaluation result for a given context.
3233
*
3334
* @param EvaluationContext $context The evaluation context.
34-
* @return EvaluationResult EvaluationResult containing the context, flags, and segments
35+
* @return EvaluationResult EvaluationResult containing the evaluated flags and matched segments.
3536
*/
3637
public static function getEvaluationResult($context): EvaluationResult
3738
{
@@ -55,12 +56,9 @@ public static function getEvaluationResult($context): EvaluationResult
5556
$segmentResult = new SegmentResult();
5657
$segmentResult->key = $segment->key;
5758
$segmentResult->name = $segment->name;
59+
$segmentResult->metadata = $segment->metadata ?? null;
5860
$evaluatedSegments[] = $segmentResult;
5961

60-
if (empty($segment->overrides)) {
61-
continue;
62-
}
63-
6462
foreach ($segment->overrides as $overrideFeature) {
6563
$featureKey = $overrideFeature->feature_key;
6664
$evaluatedFeature = $evaluatedFeatures[$featureKey] ?? null;
@@ -252,16 +250,27 @@ private static function _contextMatchesCondition(
252250

253251
switch ($condition->operator) {
254252
case SegmentConditionOperator::IN:
255-
/** @var array<mixed> $inValues */
253+
if ($contextValue === null) {
254+
return false;
255+
}
256256
if (is_array($condition->value)) {
257257
$inValues = $condition->value;
258258
} else {
259-
$inValues = json_decode($condition->value, true);
260-
$jsonDecodingFailed = $inValues === null;
261-
if ($jsonDecodingFailed || !is_array($inValues)) {
259+
try {
260+
$inValues = json_decode(
261+
$condition->value,
262+
associative: false, // Possibly catch objects
263+
flags: \JSON_THROW_ON_ERROR,
264+
);
265+
if (!is_array($inValues)) {
266+
throw new \ValueError('Invalid JSON array');
267+
}
268+
} catch (\JsonException | \ValueError) {
262269
$inValues = explode(',', $condition->value);
263270
}
264271
}
272+
$inValues = array_map(fn ($value) => StringValue::from($value), $inValues);
273+
$contextValue = StringValue::from($contextValue);
265274
return in_array($contextValue, $inValues, strict: true);
266275

267276
case SegmentConditionOperator::PERCENTAGE_SPLIT:
@@ -282,19 +291,24 @@ private static function _contextMatchesCondition(
282291
$threshold = $hashing->getHashedPercentageForObjectIds(
283292
$objectIds,
284293
);
285-
return $threshold <= floatval($condition->value);
294+
return $threshold <= ((float) $condition->value);
286295

287296
case SegmentConditionOperator::MODULO:
288297
if (!is_numeric($contextValue)) {
289298
return false;
290299
}
291300

292-
[$divisor, $remainder] = explode('|', $condition->value);
301+
$parts = explode('|', (string) $condition->value);
302+
if (count($parts) !== 2) {
303+
return false;
304+
}
305+
306+
[$divisor, $remainder] = $parts;
293307
if (!is_numeric($divisor) || !is_numeric($remainder)) {
294308
return false;
295309
}
296310

297-
return floatval($contextValue) % $divisor === $remainder;
311+
return fmod($contextValue, $divisor) === ((float) $remainder);
298312

299313
case SegmentConditionOperator::IS_NOT_SET:
300314
return $contextValue === null;
@@ -309,9 +323,7 @@ private static function _contextMatchesCondition(
309323
return !str_contains($contextValue, $condition->value);
310324

311325
case SegmentConditionOperator::REGEX:
312-
return boolval(
313-
preg_match("/{$condition->value}/", $contextValue),
314-
);
326+
return (bool) preg_match("/{$condition->value}/", (string) $contextValue);
315327
}
316328

317329
if ($contextValue === null) {
@@ -328,10 +340,13 @@ private static function _contextMatchesCondition(
328340
default => null,
329341
};
330342

331-
if (is_string($contextValue) && Semver::isSemver($contextValue)) {
332-
$contextValue = Semver::removeSemverSuffix($contextValue);
333-
return $operator !== null &&
334-
version_compare($contextValue, $condition->value, $operator);
343+
if ($operator === null) {
344+
return false;
345+
}
346+
347+
if (Semver::isSemver($condition->value) && is_string($contextValue)) {
348+
$actualVersion = Semver::removeSemverSuffix($condition->value);
349+
return version_compare($contextValue, $actualVersion, $operator);
335350
}
336351

337352
return match ($operator) {
@@ -341,38 +356,37 @@ private static function _contextMatchesCondition(
341356
'>=' => $contextValue >= $condition->value,
342357
'<' => $contextValue < $condition->value,
343358
'<=' => $contextValue <= $condition->value,
344-
default => false,
345359
};
346360
}
347361

348362
/**
363+
* Return a trait value by name, or a context value by JSONPath, or null
349364
* @param EvaluationContext $context
350365
* @param string $property
351-
* @return mixed|array<mixed>|null
366+
* @return ?mixed
352367
*/
353368
private static function _getContextValue($context, $property)
354369
{
370+
if ($context->identity !== null) {
371+
$hasTrait = array_key_exists($property, $context->identity->traits);
372+
if ($hasTrait) {
373+
return $context->identity->traits[$property];
374+
}
375+
}
376+
355377
if (str_starts_with($property, '$.')) {
356378
try {
357-
$json = new JSONPath($context);
358-
$results = $json->find($property)->getData();
379+
$jsonpath = new JSONPath($context);
380+
$results = $jsonpath->find($property)->getData();
359381
} catch (JSONPathException) {
360-
// The unlikely case when a trait starts with "$." but isn't JSONPath
361-
$escapedProperty = addslashes($property);
362-
$path = "$.identity.traits['{$escapedProperty}']";
363-
$json = new JSONPath($context);
364-
$results = $json->find($path)->getData();
382+
return null;
365383
}
366384

367-
return match (count($results)) {
368-
0 => null,
369-
1 => $results[0],
370-
default => $results,
371-
};
372-
}
385+
if (empty($results)) {
386+
return null;
387+
}
373388

374-
if ($context->identity !== null) {
375-
return $context->identity->traits[$property] ?? null;
389+
return $results[0];
376390
}
377391

378392
return null;

src/Engine/Utils/StringValue.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
namespace Flagsmith\Engine\Utils;
4+
5+
class StringValue
6+
{
7+
/**
8+
* @param mixed $value
9+
* @return string
10+
*/
11+
public static function from($value): string
12+
{
13+
if ($value === null) {
14+
return 'null';
15+
}
16+
if (is_bool($value)) {
17+
return $value ? 'true' : 'false';
18+
}
19+
return (string) $value;
20+
}
21+
}

src/Engine/Utils/Types/Context/EnvironmentContext.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
namespace Flagsmith\Engine\Utils\Types\Context;
44

5-
// TODO: Port this to https://wiki.php.net/rfc/dataclass
65
class EnvironmentContext
76
{
87
/** @var string */

src/Engine/Utils/Types/Context/EvaluationContext.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
namespace Flagsmith\Engine\Utils\Types\Context;
44

5-
// TODO: Port this to https://wiki.php.net/rfc/dataclass
65
class EvaluationContext
76
{
87
/** @var EnvironmentContext */
@@ -43,6 +42,7 @@ public static function fromJsonObject($jsonContext)
4342
$segment->name = $jsonSegment->name;
4443
$segment->rules = self::_convertRules($jsonSegment->rules ?? []);
4544
$segment->overrides = array_values(self::_convertFeatures($jsonSegment->overrides ?? []));
45+
$segment->metadata = (array) ($jsonSegment->metadata ?? []);
4646
$context->segments[$segment->key] = $segment;
4747
}
4848

@@ -84,9 +84,9 @@ private static function _convertRules($jsonRules)
8484
$rule->conditions[] = $condition;
8585
}
8686

87-
$rule->rules = $jsonRule->rules
88-
? self::_convertRules($jsonRule->rules)
89-
: [];
87+
$rule->rules = empty($jsonRule->rules)
88+
? []
89+
: self::_convertRules($jsonRule->rules);
9090

9191
$rules[] = $rule;
9292
}

src/Engine/Utils/Types/Context/FeatureContext.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
namespace Flagsmith\Engine\Utils\Types\Context;
44

5-
// TODO: Port this to https://wiki.php.net/rfc/dataclass
65
class FeatureContext
76
{
87
/** @var string */

src/Engine/Utils/Types/Context/FeatureValue.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
namespace Flagsmith\Engine\Utils\Types\Context;
44

5-
// TODO: Port this to https://wiki.php.net/rfc/dataclass
65
class FeatureValue
76
{
87
/** @var mixed */

src/Engine/Utils/Types/Context/IdentityContext.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
namespace Flagsmith\Engine\Utils\Types\Context;
44

5-
// TODO: Port this to https://wiki.php.net/rfc/dataclass
65
class IdentityContext
76
{
87
/** @var string */

src/Engine/Utils/Types/Context/SegmentCondition.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
namespace Flagsmith\Engine\Utils\Types\Context;
44

5-
// TODO: Port this to https://wiki.php.net/rfc/dataclass
65
class SegmentCondition
76
{
87
/** @var string */

0 commit comments

Comments
 (0)