Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(flags): Add relative date operator and fix numeric ops #58

Merged
merged 5 commits into from
Jan 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 11 additions & 10 deletions .github/workflows/php.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,18 @@ jobs:
steps:
- uses: actions/checkout@v2

- uses: php-actions/composer@v6

- uses: php-actions/phpunit@v3
- name: Set up PHP
uses: shivammathur/setup-php@v2
with:
php_extensions: xdebug
bootstrap: vendor/autoload.php
configuration: phpunit.xml
args: --coverage-text
version: 9.6
env:
XDEBUG_MODE: coverage
php-version: '8.0'
extensions: uopz, xdebug
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to change this because we need the uopz extension for clock-mock

tools: composer, phpunit

- name: Install Dependencies
run: composer install --prefer-dist --no-progress --no-suggest

- name: Run PHPUnit Tests
run: XDEBUG_MODE=coverage ./vendor/bin/phpunit --bootstrap vendor/autoload.php --configuration phpunit.xml --coverage-text

phpcs:
runs-on: ubuntu-latest
Expand Down
5 changes: 3 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
"require-dev": {
"overtrue/phplint": "^3.0",
"phpunit/phpunit": "^9.0",
"squizlabs/php_codesniffer": "^3.7"
"squizlabs/php_codesniffer": "^3.7",
"slope-it/clock-mock": "^0.4.0"
},
"autoload": {
"psr-4": {
Expand All @@ -36,4 +37,4 @@
"bin": [
"bin/posthog"
]
}
}
164 changes: 140 additions & 24 deletions lib/FeatureFlag.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,11 @@ public static function matchProperty($property, $propertyValues)
$overrideValue = $propertyValues[$key];

if ($operator == "exact") {
if (is_array($value)) {
return in_array($overrideValue, $value);
}
return $value == $overrideValue;
return FeatureFlag::computeExactMatch($value, $overrideValue);
}

if ($operator == "is_not") {
if (is_array($value)) {
return !in_array($overrideValue, $value);
}
return $value !== $overrideValue;
return !FeatureFlag::computeExactMatch($value, $overrideValue);
}

if ($operator == "is_set") {
Expand All @@ -48,39 +42,141 @@ public static function matchProperty($property, $propertyValues)
return strpos(strtolower(strval($overrideValue)), strtolower(strval($value))) == false;
}

if ($operator == "regex") {
if (FeatureFlag::isRegularExpression($value)) {
return preg_match($value, $overrideValue) ? true : false;
if (in_array($operator, ["regex", "not_regex"])) {
$regexValue = FeatureFlag::prepareValueForRegex($value);
if (FeatureFlag::isRegularExpression($regexValue)) {
$returnValue = preg_match($regexValue, $overrideValue) ? true : false;
if ($operator == "regex") {
return $returnValue;
} else {
return !$returnValue;
}
} else {
return false;
}
}

if ($operator == "not_regex") {
if (FeatureFlag::isRegularExpression($value)) {
return !(preg_match($value, $overrideValue) ? true : false);
if (in_array($operator, ["gt", "gte", "lt", "lte"])) {
$parsedValue = null;

if (is_numeric($value)) {
$parsedValue = floatval($value);
}

if (!is_null($parsedValue) && !is_null($overrideValue)) {
if (is_string($overrideValue)) {
return FeatureFlag::compare($overrideValue, strval($value), $operator);
} else {
return FeatureFlag::compare($overrideValue, $parsedValue, $operator, "numeric");
}
} else {
return false;
return FeatureFlag::compare(strval($overrideValue), strval($value), $operator);
}
}

if ($operator == "gt") {
return gettype($value) == gettype($overrideValue) && $overrideValue > $value;
if (in_array($operator, ["is_date_before", "is_date_after", "is_relative_date_before", "is_relative_date_after"])) {
if ($operator == 'is_relative_date_before' || $operator == 'is_relative_date_after') {
$parsedDate = FeatureFlag::relativeDateParseForFeatureFlagMatching($value);
} else {
$parsedDate = FeatureFlag::convertToDateTime($value);
}

if (is_null($parsedDate)) {
throw new InconclusiveMatchException("The date set on the flag is not a valid format");
}

$overrideDate = FeatureFlag::convertToDateTime($overrideValue);
if ($operator == 'is_date_before' || $operator == 'is_relative_date_before') {
return $overrideDate < $parsedDate;
} else {
return $overrideDate > $parsedDate;
}
}

return false;
}

public static function relativeDateParseForFeatureFlagMatching($value)
{
$regex = "/^(?<number>[0-9]+)(?<interval>[a-z])$/";
$parsedDt = new \DateTime("now", new \DateTimeZone("UTC"));
if (preg_match($regex, $value, $matches)) {
$number = intval($matches["number"]);

if ($number >= 10_000) {
// Guard against overflow, disallow numbers greater than 10_000
return null;
}

$interval = $matches["interval"];
if ($interval == "h") {
$parsedDt->sub(new \DateInterval("PT{$number}H"));
} elseif ($interval == "d") {
$parsedDt->sub(new \DateInterval("P{$number}D"));
} elseif ($interval == "w") {
$parsedDt->sub(new \DateInterval("P{$number}W"));
} elseif ($interval == "m") {
$parsedDt->sub(new \DateInterval("P{$number}M"));
} elseif ($interval == "y") {
$parsedDt->sub(new \DateInterval("P{$number}Y"));
} else {
return null;
}

return $parsedDt;
} else {
return null;
}

}

private static function convertToDateTime($value) {
if ($value instanceof \DateTime) {
return $value;
} elseif (is_string($value)) {
try {
$date = new \DateTime($value);
if (!is_nan($date->getTimestamp())) {
return $date;
}
} catch (Exception $e) {
throw new InconclusiveMatchException("{$value} is in an invalid date format");
}
} else {
throw new InconclusiveMatchException("The date provided {$value} must be a string or date object");
}
}

if ($operator == "gte") {
return gettype($value) == gettype($overrideValue) && $overrideValue >= $value;
private static function computeExactMatch($value, $overrideValue)
{
if (is_array($value)) {
return in_array(strtolower(strval($overrideValue)), array_map('strtolower', $value));
}
return strtolower(strval($value)) == strtolower(strval($overrideValue));
}

private static function compare($lhs, $rhs, $operator, $type = "string")
{
// If type is string, we use strcmp to compare the two strings
// If type is numeric, we use <=> to compare the two numbers

if ($operator == "lt") {
return gettype($value) == gettype($overrideValue) && $overrideValue < $value;
if ($type == "string") {
$comparison = strcmp($lhs, $rhs);
} else {
$comparison = $lhs <=> $rhs;
}

if ($operator == "lte") {
return gettype($value) == gettype($overrideValue) && $overrideValue <= $value;
if ($operator == "gt") {
return $comparison > 0;
} elseif ($operator == "gte") {
return $comparison >= 0;
} elseif ($operator == "lt") {
return $comparison < 0;
} elseif ($operator == "lte") {
return $comparison <= 0;
}

return false;
throw new \Exception("Invalid operator: " . $operator);
}

private static function hash($key, $distinctId, $salt = "")
Expand Down Expand Up @@ -235,4 +331,24 @@ private static function isRegularExpression($string)
restore_error_handler();
return $isRegularExpression;
}

private static function prepareValueForRegex($value)
{
$regex = $value;

// If delimiter already exists, do nothing
if (FeatureFlag::isRegularExpression($regex)) {
return $regex;
}

if (substr($regex, 0, 1) != "/") {
$regex = "/" . $regex;
}

if (substr($regex, -1) != "/") {
$regex = $regex . "/";
}

return $regex;
}
}
Loading
Loading