{"id":21894765,"url":"https://github.com/zumba/swivel","last_synced_at":"2025-04-04T06:08:38.381Z","repository":{"id":25972004,"uuid":"29414001","full_name":"zumba/swivel","owner":"zumba","description":"Strategy driven, segmented feature toggles","archived":false,"fork":false,"pushed_at":"2024-04-03T13:31:27.000Z","size":176,"stargazers_count":209,"open_issues_count":1,"forks_count":8,"subscribers_count":46,"default_branch":"master","last_synced_at":"2025-03-28T05:11:12.569Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/zumba.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2015-01-18T03:07:02.000Z","updated_at":"2024-11-21T16:08:26.000Z","dependencies_parsed_at":"2024-06-18T16:56:24.830Z","dependency_job_id":"e2fc0598-8e40-4be8-94bf-1984f75bf2f0","html_url":"https://github.com/zumba/swivel","commit_stats":{"total_commits":101,"total_committers":10,"mean_commits":10.1,"dds":"0.48514851485148514","last_synced_commit":"96b5bd975f09187f97bd394e5e4fc683b23ac72d"},"previous_names":[],"tags_count":22,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zumba%2Fswivel","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zumba%2Fswivel/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zumba%2Fswivel/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zumba%2Fswivel/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/zumba","download_url":"https://codeload.github.com/zumba/swivel/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247128747,"owners_count":20888235,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-28T13:28:02.276Z","updated_at":"2025-04-04T06:08:38.339Z","avatar_url":"https://github.com/zumba.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Zumba ***Swivel***\n\n[![Build Status](https://travis-ci.org/zumba/swivel.svg?branch=master)](https://travis-ci.org/zumba/swivel)\n[![Coverage Status](https://coveralls.io/repos/zumba/swivel/badge.svg)](https://coveralls.io/r/zumba/swivel)\n\n***Swivel*** is a fresh spin on an old idea: [Feature Flags](http://en.wikipedia.org/wiki/Feature_toggle) (toggles, bits, switches, etc.).\n\nTypical Feature Flags are *all* or *nothing*: either the feature is on for everyone, or it is off for everyone.\n\n```php\n// Old School Feature Flag\n\nif ($flagIsOn) {\n    // Do something new\n} else {\n    // Do something old\n}\n```\n\nTypical Feature Flags are based on boolean conditionals with few abstractions (if this, then that).  Although powerful in their simplicity, this typically leads to increased [cyclomatic complexity](http://en.wikipedia.org/wiki/Cyclomatic_complexity) and eventual technical debt.\n\n## Swivel is Different\n***Swivel*** is fundamentally different from Typical Feature Flags in two ways:\n\n* Features can be enabled for a subset of an application's users.\n* Features are not simple conditionals; a developer defines one or more strategies (behaviors) and ***Swivel*** takes care of determining which strategy to use.\n\n### Buckets\nWith ***Swivel***, users are separated into one of ten \"buckets,\" allowing a feature to be enabled for a subset of users.  The advantages of this approach are clear:\n\n* Deploying a new feature to 10% of users enables developers to catch unforeseen bugs/problems with new code without negatively affecting all users.  These kind of deployments are called [Canary Releases](http://martinfowler.com/bliki/CanaryRelease.html).  As soon as it is determined that new code is safe, roll out the new feature to more users in increments (30%, 50%, etc.); eventually the feature can enabled for all users, safely.\n* A/B testing becomes a breeze.  Imagine running up to 9 versions of a new feature with one group kept in reserve as a control.  Is feature \"A\" negatively affecting revenue metrics for 10% of your users? No problem: turn it off and go with version \"B\" instead.  This is easy to do with ***Swivel***.\n\n### Behaviors\nAgile code needs to be simple and easy to change.  Typical Feature Flags allow developers to quickly iterate when business rules change or new features are implemented, but this can often lead to complex, under engineered, brittle blocks of code.\n\n***Swivel*** encourages the developer to implement changes to business logic as independent, high level *strategies* rather than simple, low level deviations.\n\n## Example: Quick Look\n\n```php\n$formula = $swivel-\u003eforFeature('AwesomeSauce')\n    -\u003eaddBehavior('formulaSpicy', [$this, 'getNewSpicyFormula'])\n    -\u003eaddBehavior('formulaSaucy', [$this, 'getNewSaucyFormula'])\n    -\u003edefaultBehavior([$this, 'getFormula'])\n    -\u003eexecute();\n```\n\n## Getting Started\n\nThe first thing you'll want to do is generate a random number between 1 and 10 for each user in your application. We call this the user's \"bucket\" index.  This is what is used by ***Swivel*** to determine which features are enabled for that user.\n\n**Note:** As a best practice, once a user is assigned to a bucket they should remain in that bucket forever. You'll want to store this value in a session or cookie like you would other basic user info.\n\nNext, you'll need to create a map of features.  This map indicates which buckets should have certain features enabled.  Here is an example of a simple feature map:\n\n```php\n$map = [\n    // This is a parent feature slug, arbitrarily named \"Payment.\"\n    // The \"Payment\" feature is enabled for users in buckets 4, 5, and 6\n    'Payment' =\u003e [4,5,6],\n\n    // This is a behavior slug.  It is a subset of the parent slug,\n    // and it is only enabled for users in buckets 4 and 5\n    'Payment.Test' =\u003e [4, 5],\n\n    // Behavior slugs can be nested.\n    // This one is only enabled for users in bucket 5.\n    'Payment.Test.VersionA' =\u003e [5]\n];\n```\n\nWhen your application starts, configure ***Swivel*** and create a new manager instance:\n\n```php\n// Get this value from the session or from persistent storage.\n$userBucket = 5; // $_SESSION['bucket'];\n\n// Get the feature map data from persistent storage.\n$mapData = [ 'Feature' =\u003e [4,5,6], 'Feature.Test' =\u003e [4,5] ];\n\n// Make a new configuration object\n$config = new \\Zumba\\Swivel\\Config($mapData, $userBucket);\n\n// Make a new Swivel Manager.  This is your primary API to the Swivel library.\n$swivel = new \\Zumba\\Swivel\\Manager($config);\n```\n\nWay to go!  ***Swivel*** is now ready to use.\n\n### Using Strategies\nNow that you have a new ***Swivel*** manager created, you can start using it in your application.  To use ***Swivel*** you need to define behaviors for features of your code; ***Swivel*** will decide which behavior to execute based on the current user's bucket and the feature map you loaded in the configuration step.\n\n#### Strategy Example\n\nSay you have coded a new search algorithm for your website.  The Search feature of your site is integral to your business, so you only want to roll out the new algorithm to 10% of your users at first. You decide to only enable the algorithm for users in bucket `5`.  You configure ***Swivel*** and register it with your application:\n\n```php\n$map = [ 'Search' =\u003e [5], 'Search.NewAlgorithm' =\u003e [5] ];\n$config = new \\Zumba\\Swivel\\Config($map, $_SESSION['bucketIndex']);\n$swivel = new \\Zumba\\Swivel\\Manager($config);\n\n// ServiceLocator is fictional in this example.  Use your own framework or repository to store the\n// swivel instance.\nServiceLocator::add('Swivel', $swivel);\n```\n\nIn your code to search the site, you define two distinct strategies, and tell ***Swivel*** about them:\n\n```php\npublic function search($params = []) {\n    $swivel = ServiceLocator::get('Swivel');\n    return $swivel-\u003eforFeature('Search')\n        -\u003eaddBehavior('NewAlgorithm', [$this, 'awesomeSearch'], [$params])\n        -\u003edefaultBehavior([$this, 'normalSearch'], [$params])\n        -\u003eexecute();\n}\n\nprotected function normalSearch($params) {\n    // Tried and True method.\n}\n\nprotected function awesomeSearch($params) {\n    // Super cool new search method.\n}\n```\n\nNow, when you call `search`, ***Swivel*** will execute `awesomeSearch` for users in bucket `5`, and `normalSearch` for all other users.\n\n## ***Swivel*** API\n\n### Zumba\\Swivel\\Config\n\n#### constructor($map, $index, $logger)\n\nUsed to configure your ***Swivel*** Manager instance.\n\nParam                         | Type       | Details\n:-----------------------------|:-----------|:--------\n**$map**\u003cbr/\u003e*(optional)*    | *mixed*   | Can be one of the following:\u003cbr/\u003e\u003cul\u003e\u003cli\u003e`array` \u0026mdash; an array of feature/behavior slugs as keys and enabled buckets as values.\u003c/li\u003e\u003cli\u003e`\\Zumba\\Swivel\\MapInterface` \u0026mdash; An instance of a configured feature map.\u003c/li\u003e\u003cli\u003e`\\Zumba\\Swivel\\DriverInterface` \u0026mdash; An instance of a ***Swivel*** driver that will build a `MapInterface` object.\u003c/li\u003e\u003c/ul\u003e\n**$index**\u003cbr/\u003e*(optional)*  | *integer* | The user's predefined bucket index.  A number between 1 and 10.\n**$logger**\u003cbr/\u003e*(optional)* | `LoggerInterface` | An optional logger that implements `\\Psr\\Log\\LoggerInterface`\n\n##### Examples\n\n```php\n$features = [ 'Feature' =\u003e [1,2,3] ];\n$bucket = 3;\n\n// array\n$config = new \\Zumba\\Swivel\\Config($features, $bucket);\n\n// \\Zumba\\Swivel\\Map\n$config = new \\Zumba\\Swivel\\Config(new \\Zumba\\Swivel\\Map($features), $bucket);\n\n // $driver implements \\Zumba\\Swivel\\DriverInterface\n$config = new \\Zumba\\Swivel\\Config($driver, $bucket);\n\n```\n\n### Zumba\\Swivel\\Manager\n\n#### constructor($config)\n\nThis is the primary ***Swivel*** object that you will use in your app.\n\nParam       | Type     | Details\n:-----------|:---------|:--------\n**$config** | `Config` | A `\\Zumba\\Swivel\\Config` instance.\n\n##### Examples\n\n```php\n$config = new \\Zumba\\Swivel\\Config($features, $bucket);\n$swivel = new \\Zumba\\Swivel\\Manager($config);\n```\n\n#### forFeature($slug)\n\nCreate a point of deviation in your code.  Returns a new `Zumba\\Swivel\\Builder` that accepts multiple behaviors, default behaviors, and executes the appropriate code for the user's bucket.\n\nParam     | Type     | Details\n:---------|:---------|:--------\n**$slug** | *string* | The first section of a feature map slug.  i.e., for the feature slug `\"Test.Version.Two\"`, the `$slug` would be `\"Test\"`\n\n##### Examples\n\n```php\n$builder = $swivel-\u003eforFeature('Test');\n```\n\n#### invoke($slug, $a, $b)\n\nShorthand syntactic sugar for invoking a simple feature behavior.  Useful for ternary style code.\n\nParam                   | Type       | Details\n:-----------------------|:-----------|:--------\n**$slug**               | *string*   | The first section of a feature map slug.  i.e., for the feature slug `\"Test.Version.Two\"`, the `$slug` would be `\"Test\"`\n**$a**                  | *callable* | The strategy to execute if the `$slug` is enabled for the user's bucket.\n**$b**\u003cbr/\u003e*(optional)* | *callable* | The strategy to execute if the `$slug` is not enabled for the user's bucket.  If omitted, `invoke` will return `null` if the feature slug is not enabled.\n\n##### Examples\n\n```php\n// without Swivel\n$result = $newSearch ? $this-\u003esearch() : $this-\u003enoOp();\n\n// Zumba\\Swivel\\Manager::invoke\n$result = $swivel-\u003einvoke('Search.New', [$this, 'search'], [$this, 'noOp']);\n$result = $swivel-\u003einvoke('Search.New', [$this, 'search']);\n```\n\n#### returnValue($slug, $a, $b)\n\nShorthand syntactic sugar for invoking a simple feature behavior using `Builder::addValue`.  Useful for ternary style code.\n\nParam                   | Type     | Details\n:-----------------------|:---------|:--------\n**$slug**               | *string* | The first section of a feature map slug.  i.e., for the feature slug `\"Test.Version.Two\"`, the `$slug` would be `\"Test\"`\n**$a**                  | *mixed*  | The value to return if the `$slug` is enabled for the user's bucket.\n**$b**\u003cbr/\u003e*(optional)* | *mixed*  | The value to return if the `$slug` is not enabled for the user's bucket.  If omitted, `returnValue` will return `null` if the feature slug is not enabled.\n\n##### Examples\n\n```php\n// without Swivel\n$result = $newSearch ? 'Everything' : null;\n\n// Zumba\\Swivel\\Manager::returnValue\n$result = $swivel-\u003ereturnValue('Search.New', 'Everything', null);\n$result = $swivel-\u003ereturnValue('Search.New', 'Everything');\n```\n\n### Zumba\\Swivel\\Builder\n\nThe `Builder` API is the primary way that you will write ***Swivel*** code.  You get a new instance of the `Builder` when you call `Manager::forFeature`.\n\n#### addBehavior($slug, $strategy, $args)\n\nLazily adds a behavior to this feature that will only be executed if the feature is enabled for the user's bucket.\n\nParam                      | Type        | Details\n:--------------------------|:------------|:--------\n**$slug**                  | *string*    | The second section of a feature map slug.  i.e., for the feature slug `\"Test.Version.Two\"`, the `$slug` here would be `\"Version.Two\"`\n**$strategy**              | *callable*  | The strategy to execute if the `$slug` is enabled for the user's bucket. Since version 2.0.0 `$strategy` must be a callable.  If you want to return a simple value, use `Builder::addValue` instead.\n**$args**\u003cbr/\u003e*(optional)* | *array*     | Parameters to pass to the `$strategy` callable if it is executed.\n\n##### Examples\n\n```php\n$builder = $swivel-\u003eforFeature('Test');\n\n$builder\n    // Inline function.  This one will return 'ab'\n    -\u003eaddBehavior('versionA', function($a, $b) { return $a . $b; }, ['a', 'b'])\n\n    // Callable.  Will return the result of $obj-\u003esomeMethod('c', 'd');\n    -\u003eaddBehavior('versionB', [$obj, 'someMethod'], ['c', 'd'])\n\n     // Since version 2.0.0, this will throw a \\LogicException.  Use `addValue` instead.\n    -\u003eaddBehavior('versionC', 'result');\n```\n\n#### addValue($slug, $value)\n\nLazily adds a behavior to this feature that will return the value provided.  It will only be executed if the feature is enabled for the user's bucket.\n\nParam                      | Type     | Details\n:--------------------------|:---------|:--------\n**$slug**                  | *string* | The second section of a feature map slug.  i.e., for the feature slug `\"Test.Version.Two\"`, the `$slug` here would be `\"Version.Two\"`\n**$value**                 | *mixed*  | The value to return if the `$slug` is enabled for the user's bucket. If `$value` is a callable it will not be executed.  If you want ***Swivel*** to execute a callable, use `Builder::addBehavior` instead.\n\n##### Examples\n\n```php\n$builder = $swivel-\u003eforFeature('Test');\n\n$builder\n    // This will return `'result'`\n    -\u003eaddValue('versionA', 'result')\n\n    // Callable.  This will not be executed; Swivel will just return the unexecuted callable.\n    -\u003eaddValue('versionB', [$obj, 'someMethod']);\n```\n\n#### defaultBehavior($strategy, $args)\n\nLazily adds a behavior to this feature that will only be executed if no other feature behaviors are enabled for the user's bucket.\n\nParam                      | Type       | Details\n:--------------------------|:-----------|:--------\n**$strategy**              | *callable* | The strategy to execute if no other feature behaviors are enabled for the user's bucket. Since version 2.0.0 `$strategy` must be a callable.  If you want to return a simple value, use `Builder::defaultValue` instead.\n**$args**\u003cbr/\u003e*(optional)* | *array*    | Parameters to pass to the `$strategy` callable if it is executed.\n\n##### Examples\n\n```php\n$swivel\n    -\u003eforFeature('Test');\n    -\u003eaddBehavior('New.Version', [$this, 'someMethod'], $args)\n    -\u003edefaultBehavior([$this, 'defaultMethod'], $args);\n```\n\n#### defaultValue($value)\n\nLazily adds a behavior to this feature that will return the provided value.  It will only be executed if no other feature behaviors are enabled for the user's bucket.\n\nParam                      | Type       | Details\n:--------------------------|:-----------|:--------\n**$value**              | *mixed* | The value to return if no other feature behaviors are enabled for the user's bucket. If you want ***Swivel*** to execute a callable, use `Builder::defaultBehavior` instead.\n\n##### Examples\n\n```php\n$swivel\n    -\u003eforFeature('Test');\n    -\u003eaddBehavior('New.Version', [$this, 'someMethod'], $args)\n    -\u003edefaultValue('some default value');\n```\n\n#### execute()\n\nExecutes the appropriate behavior strategy based on the user's bucket.\n\n##### Examples\n\n```php\n// $result will contain either the result of\n// $this-\u003esomeMethod(1, 2, 3) or $this-\u003edefaultMethod('test')\n// depending on the user's bucket\n$result = $swivel\n    -\u003eforFeature('Test');\n    -\u003eaddBehavior('New.Version', [$this, 'someMethod'], [1, 2, 3])\n    -\u003edefaultBehavior([$this, 'defaultMethod'], ['test'])\n    -\u003eexecute();\n```\n\n#### noDefault()\n\nIf you do not need to define a default behavior to be executed when a feature is not enabled for a user's bucket, call `noDefault` on the `Builder`.  ***Swivel*** will throw a `\\LogicException` if you neglect to define a default behavior and do not call `noDefault`.  Likewise, ***Swivel*** will throw a `\\LogicException` if you call both `noDefault` and `defaultBehavior` on the same `Builder` instance.\n\n##### Examples\n\n```php\n$swivel\n    -\u003eforFeature('Test');\n    -\u003eaddBehavior('A', [$obj, 'someMethod'])\n    -\u003eexecute(); // throws \\LogicException here.\n\n$swivel\n    -\u003eforFeature('Test');\n    -\u003eaddBehavior('A', [$obj, 'someMethod'])\n    -\u003enoDefault()\n    -\u003eexecute(); // no exception thrown.\n\n$swivel\n    -\u003eforFeature('Test');\n    -\u003eaddBehavior('A', [$obj, 'someMethod'])\n    -\u003edefaultBehavior([$obj, 'anotherMethod'])\n    -\u003enoDefault() // throws \\LogicException here.\n    -\u003eexecute();\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzumba%2Fswivel","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fzumba%2Fswivel","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzumba%2Fswivel/lists"}