diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 6bf1d2d..991bfff 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -1,31 +1,30 @@ name: PHP Composer on: - push: - branches: [ "master" ] - pull_request: - branches: [ "master" ] + push: + branches: ["master"] + pull_request: + branches: ["master"] permissions: - contents: read + contents: read jobs: - build: + build: + runs-on: ubuntu-latest - runs-on: ubuntu-latest + steps: + - name: "Checkout" + uses: actions/checkout@v3 - steps: - - name: "Checkout" - uses: actions/checkout@v3 + - name: "Install PHP" + uses: shivammathur/setup-php@v2 + with: + php-version: "8.3" + tools: composer - - name: "Install PHP" - uses: shivammathur/setup-php@v2 - with: - php-version: '8.2' - tools: composer + - name: Install dependencies + run: composer install --no-interaction - - name: Install dependencies - run: composer install --no-interaction - - - name: Run test suite - run: composer test + - name: Run test suite + run: composer test diff --git a/README.md b/README.md index 8483e96..6e85247 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [![GitHub license](https://img.shields.io/github/license/PabloJoan/feature.svg)](https://github.com/PabloJoan/feature/blob/master/LICENSE) -Requires PHP 8.2 and above. +Requires PHP 8.3 and above. # Installation @@ -33,7 +33,7 @@ $featureConfigs = [ $features = new Features($featureConfigs); -$features->isEnabled(featureName: 'foo'); // true +$features->isEnabled(featureName: 'foo'); // true $features->getEnabledVariant(featureName: 'foo'); // 'variant1' ``` @@ -50,13 +50,17 @@ A feature can be completely enabled, completely disabled, or something in between and can comprise a number of related variants. The two main API entry points are: + ```php $features->isEnabled(featureName: 'my_feature') ``` + which returns true when `my_feature` is enabled and, for multi-variant features: + ```php $features->getEnabledVariant(featureName: 'my_feature') ``` + which returns the name of the particular variant which should be used. The single argument to each of these methods is the name of the @@ -64,13 +68,16 @@ feature to test. A typical use of `$features->isEnabled` for a single-variant feature would look something like this: + ```php if ($features->isEnabled(featureName: 'my_feature')) { // do stuff } ``` + For a multi-variant feature, we can determine the appropriate code to run for each variant with something like this: + ```php switch ($features->getEnabledVariant(featureName: 'my_feature')) { case 'foo': @@ -81,8 +88,10 @@ each variant with something like this: break; } ``` + If a feature is bucketed by id, then we pass the id string to `$features->isEnabled` and `$features->getEnabledVariant` as a second parameter + ```php $isMyFeatureEnabled = $features->isEnabled( featureName: 'my_feature', @@ -95,7 +104,6 @@ If a feature is bucketed by id, then we pass the id string to ); ``` - ## Configuration cookbook There are a number of common configurations so before I explain the complete @@ -103,22 +111,31 @@ syntax of the feature configuration stanzas, here are some of the more common cases along with the most concise way to write the configuration. ### A totally enabled feature: + ```php $server_config['foo'] = ['variants' => ['enabled' => 100]]; ``` + ### A totally disabled feature: + ```php $server_config['foo'] = ['variants' => ['enabled' => 0]]; ``` + ### Feature with winning variant turned on for everyone + ```php $server_config['foo'] = ['variants' => ['blue_background' => 100]]; ``` + ### Single-variant feature ramped up to 1% of users. + ```php $server_config['foo'] = ['variants' => ['enabled' => 1]]; ``` + ### Multi-variant feature ramped up to 1% of users for each variant. + ```php $server_config['foo'] = [ 'variants' => [ @@ -128,31 +145,41 @@ cases along with the most concise way to write the configuration. ], ]; ``` + ### Enabled for 10% of regular users. + ```php $server_config['foo'] = [ 'variants' => ['enabled' => 10] ]; ``` + ### Feature ramped up to 1% of requests, bucketing at random rather than by id + ```php $server_config['foo'] = [ 'variants' => ['enabled' => 1], 'bucketing' => 'random' ]; ``` + ### Feature ramped up to 40% of requests, bucketing by id rather than at random + ```php $server_config['foo'] = [ 'variants' => ['enabled' => 40], 'bucketing' => 'id' ]; ``` + ### Single-variant feature in 50/50 A/B test + ```php $server_config['foo'] = ['variants' => ['enabled' => 50]]; ``` + ### Multi-variant feature in A/B test with 20% of users seeing each variant (and 40% left in control group). + ```php $server_config['foo'] = [ 'variants' => [ @@ -162,6 +189,7 @@ cases along with the most concise way to write the configuration. ], ]; ``` + ## Configuration details Each feature’s config stanza controls when the feature is enabled and what @@ -173,7 +201,7 @@ keys, the most important of which is `'variants'`. The value of the `'variants'` property an array whose keys are names of variants and whose values are the percentage of requests that should see each variant. -The remaining feature config property is `'bucketing'`. Bucketing specifies +The remaining feature config property is `'bucketing'`. Bucketing specifies how users are bucketed when a feature is enabled for only a percentage of users. The default value, `'random'`, causes each request to be bucketed independently, meaning that the same user will be in different buckets on different requests. @@ -189,12 +217,12 @@ There are a few ways to misuse the Feature API or misconfigure a feature that may be detected. (Some of these are not currently detected but may be in the future.) - 1. Setting the percentage value of a variant in `'variants'` to a value less - than 0 or greater than 100. +1. Setting the percentage value of a variant in `'variants'` to a value less + than 0 or greater than 100. - 2. Setting `'variants'` such that the sum of the variant percentages is - greater than 100. +2. Setting `'variants'` such that the sum of the variant percentages is + greater than 100. - 3. Setting `'variants'` to a non-array value. +3. Setting `'variants'` to a non-array value. - 4. Setting `'bucketing'` to any value that is not `'id'` or `'random'`. +4. Setting `'bucketing'` to any value that is not `'id'` or `'random'`. diff --git a/composer.json b/composer.json index 1c1fe14..b1b5824 100644 --- a/composer.json +++ b/composer.json @@ -22,13 +22,13 @@ "toggle" ], "require": { - "php": ">=8.2" + "php": ">=8.3" }, "require-dev": { "phpunit/phpunit": "*" }, "scripts": { - "test": "php ./vendor/bin/phpunit --stop-on-failure --fail-on-warning --fail-on-risky -v tests/" + "test": "php ./vendor/bin/phpunit --stop-on-failure --fail-on-warning --fail-on-risky tests/" }, "autoload": { "psr-4": { diff --git a/src/Bucketing/Id.php b/src/Bucketing/Id.php index 13ff889..8a075fb 100644 --- a/src/Bucketing/Id.php +++ b/src/Bucketing/Id.php @@ -10,20 +10,20 @@ * hexdec('ffffffff') is the largest possible outcome * of hash('crc32c', $idToHash) */ - private const TOTAL = 4294967295; - private const HASH_ALGO = 'crc32c'; + private const int TOTAL = 4294967295; + private const string HASH_ALGO = 'crc32c'; /** * Convert Id string to a Hex * Convert Hex to Dec int - * Get a percentage float + * Get a percentage int */ - public function strToIntHash(string $idToHash = ''): float + public function strToIntHash(string $idToHash): int { $hex = hash(self::HASH_ALGO, $idToHash); $dec = hexdec($hex); $x = $dec / self::TOTAL; - return $x * 100; + return (int) round($x * 100); } } diff --git a/src/Bucketing/Random.php b/src/Bucketing/Random.php index 8611480..eef5b57 100644 --- a/src/Bucketing/Random.php +++ b/src/Bucketing/Random.php @@ -16,9 +16,8 @@ public function __construct() $this->randomizer = new Randomizer(new Xoshiro256StarStar()); } - public function strToIntHash(string $idToHash = ''): float + public function strToIntHash(string $idToHash): int { - $decimal = $this->randomizer->getInt(0, PHP_INT_MAX) / PHP_INT_MAX; - return $decimal * 100; + return $this->randomizer->getInt(0, 100); } } diff --git a/src/Bucketing/Type.php b/src/Bucketing/Type.php index 8206414..9c92d4a 100644 --- a/src/Bucketing/Type.php +++ b/src/Bucketing/Type.php @@ -7,8 +7,8 @@ interface Type { /** - * A hash number between 0 and 100 based on an id string + * A hash that maps the given string to a number between 0 and 100 * unless we are bucketing completely at random */ - public function strToIntHash(string $idToHash = ''): float; + public function strToIntHash(string $idToHash): int; }