diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..1cf0182
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+composer.lock
+phpunit.xml
+/vendor/
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..ffac8b7
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,13 @@
+language: php
+
+php:
+ - 5.3
+ - 5.4
+ - 5.5
+ - hhvm
+
+before_script:
+ - composer install -n
+
+script:
+ - vendor/bin/phpunit
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..b02e078
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2009 James Golick
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..fec2463
--- /dev/null
+++ b/README.md
@@ -0,0 +1,139 @@
+rollout (for php)
+=================
+
+Feature flippers for PHP. A port of ruby's [rollout](https://github.com/FetLife/rollout).
+
+Install It
+----------
+
+ composer require opensoft/rollout
+
+How it works
+------------
+
+Initialize a rollout object:
+
+```php
+use Opensoft\Rollout\Rollout;
+use Opensoft\Rollout\Storage\ArrayStorage;
+
+$rollout = new Rollout(new ArrayStorage());
+```
+
+Check if a feature is active for a particular user:
+
+```php
+$rollout->isActive('chat', $user); // returns true/false
+```
+
+Check if a feature is activated globally:
+
+```php
+$rollout->isActive('chat'); // returns true/false
+```
+
+Storage
+-------
+
+There are a number of different storage implementations for where the configuration for the rollout is stored.
+
+* ArrayStorage - default storage, not persistent
+* DoctrineCacheStorageAdapter - requires `doctrine/cache`
+
+The storage implementation must implement the `Storage\StorageInterface`'s methods.
+
+Groups
+------
+
+Rollout ships with one group by default: `all`, which does exactly what it sounds like.
+
+You can activate the `all` group for chat features like this:
+
+```php
+$rollout->activateGroup('chat', 'all');
+```
+
+You may also want to define your own groups. We have one for caretakers:
+
+```php
+$rollout->defineGroup('caretakers', function(RolloutUserInterface $user) {
+ return $user->isCaretaker(); // boolean
+});
+```
+
+You can activate multiple groups per feature.
+
+Deactivate groups like this:
+
+```php
+$rollout->deactivateGroup('chat');
+```
+
+Specific Users
+--------------
+
+You may want to let a specific user into a beta test or something. If that user isn't part of an existing group, you can let them in specifically:
+
+```php
+$rollout->activateUser('chat', $user);
+```
+
+Deactivate them like this:
+
+```php
+$rollout->deactivateUser('chat', $user);
+```
+
+Rollout users must implement the `RolloutUserInterface`.
+
+User Percentages
+----------------
+
+If you're rolling out a new feature, you may want to test the waters by slowly enabling it for a percentage of your users.
+
+```php
+$rollout->activatePercentage('chat', 20);
+```
+
+The algorithm for determining which users get let in is this:
+
+```php
+crc32($user->getRolloutIdentifier()) % 100 < $percentage
+```
+
+So, for 20%, users 0, 1, 10, 11, 20, 21, etc would be allowed in. Those users would remain in as the percentage increases.
+
+Deactivate all percentages like this:
+
+```php
+$rollout->deactivatePercentage('chat');
+```
+
+**Note:** Activating a feature for 100% of users will also make it activate `globally`. This is like calling `$rollout->isActive()` without a user object.
+
+Feature is Broken
+-----------------
+
+Deactivate everybody at once:
+
+```php
+$rollout->deactivate('chat');
+```
+
+You may wish to disable features programmatically if monitoring tools detect unusually high error rates for example.
+
+Symfony2 Bundle
+---------------
+
+A Symfony2 bundle is available to integrate rollout into Symfony2 projects. It can be found at http://github.com/opensoft/OpensoftRolloutBundle.
+
+Implementations in other languages
+----------------------------------
+
+* Ruby: http://github.com/FetLife/rollout
+* Python: http://github.com/asenchi/proclaim
+
+Copyright
+---------
+
+Copyright © 2010 James Golick, BitLove, Inc. See LICENSE for details.
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..6470078
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,27 @@
+{
+ "name": "opensoft/rollout",
+ "description": "Feature flippers for PHP",
+ "keywords": ["feature", "flag", "toggle", "rollout", "flipper"],
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "Richard Fullmer",
+ "email": "richard.fullmer@opensoftdev.com"
+ }
+ ],
+ "require": {
+ "php": ">=5.3.3"
+ },
+ "suggest": {
+ "doctrine/cache": "For use with the DoctrineCacheStorageAdapter"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "*",
+ "doctrine/cache": "*"
+ },
+ "autoload": {
+ "psr-4": {
+ "Opensoft\\Rollout\\": "src/"
+ }
+ }
+}
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
new file mode 100644
index 0000000..cf92f0f
--- /dev/null
+++ b/phpunit.xml.dist
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+ tests
+
+
+
+
+
+ src/
+
+
+
+
diff --git a/src/Feature.php b/src/Feature.php
new file mode 100644
index 0000000..f06b917
--- /dev/null
+++ b/src/Feature.php
@@ -0,0 +1,213 @@
+
+ */
+class Feature
+{
+ /**
+ * @var array
+ */
+ private $name;
+
+ /**
+ * @var array
+ */
+ private $groups = array();
+
+ /**
+ * @var array
+ */
+ private $users = array();
+
+ /**
+ * @var integer
+ */
+ private $percentage = 0;
+
+ /**
+ * @param string $name
+ * @param string|null $settings
+ */
+ public function __construct($name, $settings = null)
+ {
+ $this->name = $name;
+ if ($settings) {
+ list($rawPercentage, $rawUsers, $rawGroups) = explode('|', $settings);
+ $this->percentage = (int) $rawPercentage;
+ $this->users = !empty($rawUsers) ? explode(',', $rawUsers) : array();
+ $this->groups = !empty($rawGroups) ? explode(',', $rawGroups) : array();
+ } else {
+ $this->clear();
+ }
+ }
+
+ /**
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ /**
+ * @param integer $percentage
+ */
+ public function setPercentage($percentage)
+ {
+ $this->percentage = $percentage;
+ }
+
+ /**
+ * @return integer
+ */
+ public function getPercentage()
+ {
+ return $this->percentage;
+ }
+
+ /**
+ * @return string
+ */
+ public function serialize()
+ {
+ return implode('|', array(
+ $this->percentage,
+ implode(',', $this->users),
+ implode(',', $this->groups)
+ ));
+ }
+
+ /**
+ * @param RolloutUserInterface $user
+ */
+ public function addUser(RolloutUserInterface $user)
+ {
+ if (!in_array($user, $this->users)) {
+ $this->users[] = $user->getRolloutIdentifier();
+ }
+ }
+
+ /**
+ * @param RolloutUserInterface $user
+ */
+ public function removeUser(RolloutUserInterface $user)
+ {
+ if (($key = array_search($user->getRolloutIdentifier(), $this->users)) !== false) {
+ unset($this->users[$key]);
+ }
+ }
+
+ /**
+ * @return array
+ */
+ public function getUsers()
+ {
+ return $this->users;
+ }
+
+ /**
+ * @param string $group
+ */
+ public function addGroup($group)
+ {
+ if (!in_array($group, $this->groups)) {
+ $this->groups[] = $group;
+ }
+ }
+
+ /**
+ * @param $group
+ */
+ public function removeGroup($group)
+ {
+ if (($key = array_search($group, $this->groups)) !== false) {
+ unset($this->groups[$key]);
+ }
+ }
+
+ /**
+ * @return array
+ */
+ public function getGroups()
+ {
+ return $this->groups;
+ }
+
+ /**
+ * Clear the feature of all configuration
+ */
+ public function clear()
+ {
+ $this->groups = array();
+ $this->users = array();
+ $this->percentage = 0;
+ }
+
+ /**
+ * Is the feature active?
+ *
+ * @param Rollout $rollout
+ * @param RolloutUserInterface|null $user
+ * @return bool
+ */
+ public function isActive(Rollout $rollout, RolloutUserInterface $user = null)
+ {
+ if (null == $user) {
+ return $this->percentage == 100;
+ }
+
+ return $this->isUserInPercentage($user) || $this->isUserInActiveUsers($user) || $this->isUserInActiveGroup($user, $rollout);
+ }
+
+ /**
+ * @return array
+ */
+ public function toArray()
+ {
+ return array(
+ 'percentage' => $this->percentage,
+ 'groups' => $this->groups,
+ 'users' => $this->users,
+ );
+ }
+
+ /**
+ * @param RolloutUserInterface $user
+ * @return bool
+ */
+ private function isUserInPercentage(RolloutUserInterface $user)
+ {
+ return crc32($user->getRolloutIdentifier()) % 100 < $this->percentage;
+ }
+
+ /**
+ * @param RolloutUserInterface $user
+ * @return mixed
+ */
+ private function isUserInActiveUsers(RolloutUserInterface $user)
+ {
+ return in_array($user->getRolloutIdentifier(), $this->users);
+ }
+
+ /**
+ * @param RolloutUserInterface $user
+ * @param Rollout $rollout
+ * @return bool
+ */
+ private function isUserInActiveGroup(RolloutUserInterface $user, Rollout $rollout)
+ {
+ foreach ($this->groups as $group) {
+ if ($rollout->isActiveInGroup($group, $user)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/src/Rollout.php b/src/Rollout.php
new file mode 100644
index 0000000..07b9383
--- /dev/null
+++ b/src/Rollout.php
@@ -0,0 +1,239 @@
+
+ */
+class Rollout
+{
+ /**
+ * @var StorageInterface
+ */
+ private $storage;
+
+ /**
+ * @var array
+ */
+ private $groups;
+
+ /**
+ * @param StorageInterface $storage
+ */
+ public function __construct(StorageInterface $storage)
+ {
+ $this->storage = $storage;
+ $this->groups = array(
+ 'all' => function(RolloutUserInterface $user) { return $user !== null; }
+ );
+ }
+
+ /**
+ * @param string $feature
+ */
+ public function activate($feature)
+ {
+ $feature = $this->get($feature);
+ if ($feature) {
+ $feature->setPercentage(100);
+ $this->save($feature);
+ }
+ }
+
+
+ /**
+ * @param string $feature
+ */
+ public function deactivate($feature)
+ {
+ $feature = $this->get($feature);
+ if ($feature) {
+ $feature->clear();
+ $this->save($feature);
+ }
+ }
+
+ /**
+ * @param string $feature
+ * @param string $group
+ */
+ public function activateGroup($feature, $group)
+ {
+ $feature = $this->get($feature);
+ if ($feature) {
+ $feature->addGroup($group);
+ $this->save($feature);
+ }
+ }
+
+ /**
+ * @param string $feature
+ * @param string $group
+ */
+ public function deactivateGroup($feature, $group)
+ {
+ $feature = $this->get($feature);
+ if ($feature) {
+ $feature->removeGroup($group);
+ $this->save($feature);
+ }
+ }
+
+ /**
+ * @param string $feature
+ * @param RolloutUserInterface $user
+ */
+ public function activateUser($feature, RolloutUserInterface $user)
+ {
+ $feature = $this->get($feature);
+ if ($feature) {
+ $feature->addUser($user);
+ $this->save($feature);
+ }
+ }
+
+ /**
+ * @param string $feature
+ * @param RolloutUserInterface $user
+ */
+ public function deactivateUser($feature, RolloutUserInterface $user)
+ {
+ $feature = $this->get($feature);
+ if ($feature) {
+ $feature->removeUser($user);
+ $this->save($feature);
+ }
+ }
+
+ /**
+ * @param string $group
+ * @param callable $closure
+ */
+ public function defineGroup($group, \Closure $closure)
+ {
+ $this->groups[$group] = $closure;
+ }
+
+ /**
+ * @param string $feature
+ * @param RolloutUserInterface|null $user
+ * @return bool
+ */
+ public function isActive($feature, RolloutUserInterface $user = null)
+ {
+ $feature = $this->get($feature);
+
+ return $feature ? $feature->isActive($this, $user) : false;
+ }
+
+ /**
+ * @param string $feature
+ * @param integer $percentage
+ */
+ public function activatePercentage($feature, $percentage)
+ {
+ $feature = $this->get($feature);
+ if ($feature) {
+ $feature->setPercentage($percentage);
+ $this->save($feature);
+ }
+ }
+
+ /**
+ * @param string $feature
+ */
+ public function deactivatePercentage($feature)
+ {
+ $feature = $this->get($feature);
+ if ($feature) {
+ $feature->setPercentage(0);
+ $this->save($feature);
+ }
+ }
+
+ /**
+ * @param string $group
+ * @param RolloutUserInterface $user
+ * @return bool
+ */
+ public function isActiveInGroup($group, RolloutUserInterface $user)
+ {
+ if (!isset($this->groups[$group])) {
+ return false;
+ }
+
+ $g = $this->groups[$group];
+
+ return $g && $g($user);
+ }
+
+ /**
+ * @param string $feature
+ * @return Feature
+ */
+ public function get($feature)
+ {
+ $settings = $this->storage->get($this->key($feature));
+
+ if (!empty($settings)) {
+ $f = new Feature($feature, $settings);
+ } else {
+ $f = new Feature($feature);
+
+ $this->save($f);
+ }
+
+ return $f;
+ }
+
+ /**
+ * @return array
+ */
+ public function features()
+ {
+ $content = $this->storage->get($this->featuresKey());
+
+ if (!empty($content)) {
+ return explode(',', $content);
+ }
+
+ return array();
+ }
+
+ /**
+ * @param string $name
+ * @return string
+ */
+ private function key($name)
+ {
+ return 'feature:' . $name;
+ }
+
+ /**
+ * @return string
+ */
+ private function featuresKey()
+ {
+ return 'feature:__features__';
+ }
+
+ /**
+ * @param Feature $feature
+ */
+ private function save(Feature $feature)
+ {
+ $name = $feature->getName();
+ $this->storage->set($this->key($name), $feature->serialize());
+
+ $features = $this->features();
+ if (!in_array($name, $features)) {
+ $features[] = $name;
+ }
+ $this->storage->set($this->featuresKey(), implode(',', $features));
+ }
+}
diff --git a/src/RolloutUserInterface.php b/src/RolloutUserInterface.php
new file mode 100644
index 0000000..a8ab1c7
--- /dev/null
+++ b/src/RolloutUserInterface.php
@@ -0,0 +1,18 @@
+
+ */
+interface RolloutUserInterface
+{
+ /**
+ * @return string
+ */
+ public function getRolloutIdentifier();
+}
diff --git a/src/Storage/ArrayStorage.php b/src/Storage/ArrayStorage.php
new file mode 100644
index 0000000..2d5c5f1
--- /dev/null
+++ b/src/Storage/ArrayStorage.php
@@ -0,0 +1,36 @@
+
+ */
+class ArrayStorage implements StorageInterface
+{
+ /**
+ * @var array
+ */
+ private $storage = array();
+
+ /**
+ * @param string $key
+ * @return mixed|null
+ */
+ public function get($key)
+ {
+ return isset($this->storage[$key]) ? $this->storage[$key] : null;
+ }
+
+ /**
+ * @param string $key
+ * @param mixed $value
+ */
+ public function set($key, $value)
+ {
+ $this->storage[$key] = $value;
+ }
+
+}
diff --git a/src/Storage/DoctrineCacheStorageAdapter.php b/src/Storage/DoctrineCacheStorageAdapter.php
new file mode 100644
index 0000000..e0be0f4
--- /dev/null
+++ b/src/Storage/DoctrineCacheStorageAdapter.php
@@ -0,0 +1,47 @@
+
+ */
+class DoctrineCacheStorageAdapter implements StorageInterface
+{
+ /**
+ * @var Cache
+ */
+ private $cache;
+
+ /**
+ * @param Cache $cache
+ */
+ public function __construct(Cache $cache)
+ {
+ $this->cache = $cache;
+ }
+
+ /**
+ * @param string $key
+ * @return mixed|null Null if the value is not found
+ */
+ public function get($key)
+ {
+ return $this->cache->fetch($key);
+ }
+
+ /**
+ * @param string $key
+ * @param mixed $value
+ */
+ public function set($key, $value)
+ {
+ $this->cache->save($key, $value);
+ }
+}
diff --git a/src/Storage/StorageInterface.php b/src/Storage/StorageInterface.php
new file mode 100644
index 0000000..0327718
--- /dev/null
+++ b/src/Storage/StorageInterface.php
@@ -0,0 +1,24 @@
+
+ */
+interface StorageInterface
+{
+ /**
+ * @param string $key
+ * @return mixed|null Null if the value is not found
+ */
+ public function get($key);
+
+ /**
+ * @param string $key
+ * @param mixed $value
+ */
+ public function set($key, $value);
+}
diff --git a/tests/RolloutTest.php b/tests/RolloutTest.php
new file mode 100644
index 0000000..ecba8dc
--- /dev/null
+++ b/tests/RolloutTest.php
@@ -0,0 +1,277 @@
+
+ */
+class RolloutTest extends \PHPUnit_Framework_TestCase
+{
+ /**
+ * @var Rollout
+ */
+ private $rollout;
+
+ protected function setUp()
+ {
+ $this->rollout = new Rollout(new ArrayStorage());
+ }
+
+ public function testActiveForBlockGroup()
+ {
+ // When a group is activated
+ $this->rollout->defineGroup('fivesonly', function(RolloutUserInterface $user) { return $user->getRolloutIdentifier() == 5; });
+ $this->rollout->activateGroup('chat', 'fivesonly');
+
+ // the feature is active for users for which the callback evaluates as true
+ $this->assertTrue($this->rollout->isActive('chat', new User(5)));
+
+ // is not active for users for which the callback evalutates to false
+ $this->assertFalse($this->rollout->isActive('chat', new User(1)));
+
+ // is not active if a group is found in storage, but not defined in the rollout
+ $this->rollout->activateGroup('chat', 'fake');
+ $this->assertFalse($this->rollout->isActive('chat', new User(1)));
+ }
+
+ public function testDefaultAllGroup()
+ {
+ // the default all group
+ $this->rollout->activateGroup('chat', 'all');
+
+ // evaluates to true no matter what
+ $this->assertTrue($this->rollout->isActive('chat', new User(0)));
+ }
+
+ public function testDeactivatingAGroup()
+ {
+ $this->rollout->defineGroup('fivesonly', function(RolloutUserInterface $user) { return $user->getRolloutIdentifier() == 5; });
+ $this->rollout->activateGroup('chat', 'all');
+ $this->rollout->activateGroup('chat', 'some');
+ $this->rollout->activateGroup('chat', 'fivesonly');
+ $this->rollout->deactivateGroup('chat', 'all');
+ $this->rollout->deactivateGroup('chat', 'some');
+
+ // deactivates the rules for that group
+ $this->assertFalse($this->rollout->isActive('chat', new User(10)));
+
+ // leaves the other groups active
+ $this->assertContains('fivesonly', $this->rollout->get('chat')->getGroups());
+ $this->assertCount(1, $this->rollout->get('chat')->getGroups());
+ }
+
+ public function testDeactivatingAFeatureCompletely()
+ {
+ $this->rollout->defineGroup('fivesonly', function(RolloutUserInterface $user) { return $user->getRolloutIdentifier() === 5; });
+ $this->rollout->activateGroup('chat', 'all');
+ $this->rollout->activateGroup('chat', 'fivesonly');
+ $this->rollout->activateUser('chat', new User(51));
+ $this->rollout->activatePercentage('chat', 100);
+ $this->rollout->activate('chat');
+ $this->rollout->deactivate('chat');
+
+ // it should remove all of the groups
+ $this->assertFalse($this->rollout->isActive('chat', new User(0)));
+
+ // it should remove all of the users
+ $this->assertFalse($this->rollout->isActive('chat', new User(51)));
+
+ // it should remove the percentage
+ $this->assertFalse($this->rollout->isActive('chat', new User(24)));
+
+ // it should be removed globally
+ $this->assertFalse($this->rollout->isActive('chat'));
+ }
+
+ public function testActivatingASpecificUser()
+ {
+ $this->rollout->activateUser('chat', new User(42));
+
+ // it should be active for that user
+ $this->assertTrue($this->rollout->isActive('chat', new User(42)));
+
+ // it remains inactive for other users
+ $this->assertFalse($this->rollout->isActive('chat', new User(24)));
+ }
+
+ public function testActivatingASpecificUserWithStringId()
+ {
+ $this->rollout->activateUser('chat', new User('user-72'));
+
+ // it should be active for that user
+ $this->assertTrue($this->rollout->isActive('chat', new User('user-72')));
+
+ // it remains inactive for other users
+ $this->assertFalse($this->rollout->isActive('chat', new User('user-12')));
+ }
+
+ public function testDeactivatingASpecificUser()
+ {
+ $this->rollout->activateUser('chat', new User(42));
+ $this->rollout->activateUser('chat', new User(4242));
+ $this->rollout->activateUser('chat', new User(24));
+ $this->rollout->deactivateUser('chat', new User(42));
+ $this->rollout->deactivateUser('chat', new User('4242'));
+
+ // that user should no longer be active
+ $this->assertFalse($this->rollout->isActive('chat', new User(42)));
+
+ // it remains active for other users
+ $users = $this->rollout->get('chat')->getUsers();
+ $this->assertCount(1, $users);
+ $this->assertEquals(24, $users[0]);
+ }
+
+ public function testActivatingAFeatureGlobally()
+ {
+ $this->rollout->activate('chat');
+
+ // it should activate the feature
+ $this->assertTrue($this->rollout->isActive('chat'));
+ }
+
+ public function testActivatingAFeatureForPercentageOfUsers()
+ {
+ $this->rollout->activatePercentage('chat', 20);
+
+ $activated = [];
+ foreach (range(1, 120) as $id) {
+ if ($this->rollout->isActive('chat', new User($id))) {
+ $activated[] = true;
+ }
+ }
+
+ // it should activate the feature for a percentage of users
+ $this->assertLessThanOrEqual(21, count($activated));
+ $this->assertGreaterThanOrEqual(19, count($activated));
+ }
+
+ public function testActivatingAFeatureForPercentageOfUsers2()
+ {
+ $this->rollout->activatePercentage('chat', 20);
+
+ $activated = [];
+ foreach (range(1, 200) as $id) {
+ if ($this->rollout->isActive('chat', new User($id))) {
+ $activated[] = true;
+ }
+ }
+
+ // it should activate the feature for a percentage of users
+ $this->assertLessThanOrEqual(45, count($activated));
+ $this->assertGreaterThanOrEqual(35, count($activated));
+ }
+
+ public function testActivatingAFeatureForPercentageOfUsers3()
+ {
+ $this->rollout->activatePercentage('chat', 5);
+
+ $activated = [];
+ foreach (range(1, 100) as $id) {
+ if ($this->rollout->isActive('chat', new User($id))) {
+ $activated[] = true;
+ }
+ }
+
+ // it should activate the feature for a percentage of users
+ $this->assertLessThanOrEqual(7, count($activated));
+ $this->assertGreaterThanOrEqual(3, count($activated));
+ }
+
+ public function testActivatingAFeatureForAGroupAsAString()
+ {
+ $this->rollout->defineGroup('admins', function(RolloutUserInterface $user) { return $user->getRolloutIdentifier() == 5; });
+ $this->rollout->activateGroup('chat', 'admins');
+
+ // the feature is active for users for which the block is true
+ $this->assertTrue($this->rollout->isActive('chat', new User(5)));
+
+ // the feature is not active for users for which the block evaluates to false
+ $this->assertFalse($this->rollout->isActive('chat', new User(1)));
+ }
+
+ public function testDeactivatingThePercentageOfUsers()
+ {
+ $this->rollout->activatePercentage('chat', 100);
+ $this->rollout->deactivatePercentage('chat');
+
+ // it becomes inactive for all users
+ $this->assertFalse($this->rollout->isActive('chat', new User(24)));
+ }
+
+ public function testDeactivatingTheFeatureGlobally()
+ {
+ $this->rollout->activate('chat');
+ $this->rollout->deactivate('chat');
+
+ // inactive feature
+ $this->assertFalse($this->rollout->isActive('chat'));
+ }
+
+ public function testKeepsAListOfFeatures()
+ {
+ // saves the feature
+ $this->rollout->activate('chat');
+ $this->assertContains('chat', $this->rollout->features());
+
+ // does not contain doubles
+ $this->rollout->activate('chat');
+ $this->rollout->activate('chat');
+ $this->assertCount(1, $this->rollout->features());
+ }
+
+ public function testGet()
+ {
+ $this->rollout->activatePercentage('chat', 10);
+ $this->rollout->activateGroup('chat', 'caretakers');
+ $this->rollout->activateGroup('chat', 'greeters');
+ $this->rollout->activate('signup');
+ $this->rollout->activateUser('chat', new User(42));
+
+ // it should return the feature object
+ $feature = $this->rollout->get('chat');
+ $this->assertContains('caretakers', $feature->getGroups());
+ $this->assertContains('greeters', $feature->getGroups());
+ $this->assertEquals(10, $feature->getPercentage());
+ $this->assertContains(42, $feature->getUsers());
+ $this->assertEquals(array('groups' => array('caretakers', 'greeters'), 'percentage' => 10, 'users' => array('42')), $feature->toArray());
+
+ $feature = $this->rollout->get('signup');
+ $this->assertEmpty($feature->getGroups());
+ $this->assertEmpty($feature->getUsers());
+ $this->assertEquals(100, $feature->getPercentage());
+ }
+}
+
+
+/**
+ * @author Richard Fullmer
+ */
+class User implements RolloutUserInterface
+{
+ /**
+ * @var string
+ */
+ private $id;
+
+ /**
+ * @param string $id
+ */
+ public function __construct($id)
+ {
+ $this->id = $id;
+ }
+
+ /**
+ * @return string
+ */
+ public function getRolloutIdentifier()
+ {
+ return $this->id;
+ }
+}